Browse Source

Merge pull request #14225 from irynamatveieva/feature/cf-output

Calculated field output strategies
pull/14369/head
Viacheslav Klimov 11 months ago
committed by GitHub
parent
commit
eae777b5e3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 19
      application/src/main/data/upgrade/basic/schema_update.sql
  2. 2
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  3. 126
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
  4. 4
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java
  5. 55
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java
  7. 1
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java
  8. 1
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
  9. 1
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java
  10. 1
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java
  11. 1
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
  12. 1
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java
  13. 6
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java
  14. 105
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  15. 5
      application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java
  16. 15
      application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java
  17. 20
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  18. 6
      application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java
  19. 6
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
  20. 5
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java
  21. 5
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java
  22. 5
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
  23. 6
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  24. 6
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  25. 37
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java
  26. 38
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutput.java
  27. 33
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutputStrategy.java
  28. 29
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java
  29. 36
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java
  30. 25
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java
  31. 22
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java
  32. 38
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java
  33. 37
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutput.java
  34. 31
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutputStrategy.java
  35. 29
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java
  36. 8
      common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java
  37. 37
      dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java
  38. 6
      dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java
  39. 6
      dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
  40. 6
      dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java
  41. 6
      dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java
  42. 7
      monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java
  43. 30
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java
  44. 22
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java
  45. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java
  46. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  47. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts
  48. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts
  49. 84
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html
  50. 22
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss
  51. 58
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts
  52. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts
  53. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts
  54. 7
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  55. 64
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  56. 22
      ui-ngx/src/assets/locale/locale.constant-en_US.json

19
application/src/main/data/upgrade/basic/schema_update.sql

@ -78,6 +78,25 @@ ALTER TABLE calculated_field ADD CONSTRAINT calculated_field_unq_key UNIQUE (ent
-- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE END -- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE END
-- CALCULATED FIELD OUTPUT STRATEGY UPDATE START
UPDATE calculated_field
SET configuration = jsonb_set(
configuration::jsonb,
'{output}',
(configuration::jsonb -> 'output')
|| jsonb_build_object(
'strategy',
jsonb_build_object(
'type', 'RULE_CHAIN'
)
),
false
)
WHERE (configuration::jsonb -> 'output' -> 'strategy') IS NULL;
-- CALCULATED FIELD OUTPUT STRATEGY UPDATE END
-- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE START -- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE START
DROP TABLE IF EXISTS calculated_field_link; DROP TABLE IF EXISTS calculated_field_link;

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

@ -486,7 +486,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
stateSizeChecked = true; stateSizeChecked = true;
if (state.isSizeOk()) { if (state.isSizeOk()) {
if (!calculationResult.isEmpty()) { if (!calculationResult.isEmpty()) {
cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); cfService.processResult(tenantId, entityId, calculationResult, cfIdList, callback);
} else { } else {
callback.onSuccess(); callback.onSuccess();
} }

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

@ -15,24 +15,39 @@
*/ */
package org.thingsboard.server.service.cf; 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.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; 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.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.ThingsBoardExecutors; 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.AttributeScope;
import org.thingsboard.server.common.data.cf.CalculatedField; 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.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
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; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.TimeSeriesImmediateOutputStrategy;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric;
import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval; import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
@ -40,12 +55,14 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; 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.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntityRelationPathQuery;
import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; 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.attributes.AttributesService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
@ -54,11 +71,13 @@ 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.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -70,6 +89,8 @@ import static org.thingsboard.server.common.data.cf.CalculatedFieldType.PROPAGAT
import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; 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_LATITUDE_ARGUMENT_KEY;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_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.createDefaultAttributeEntry;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultKvEntry; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultKvEntry;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformAggMetricArgument; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformAggMetricArgument;
@ -83,6 +104,7 @@ public abstract class AbstractCalculatedFieldProcessingService {
protected final AttributesService attributesService; protected final AttributesService attributesService;
protected final TimeseriesService timeseriesService; protected final TimeseriesService timeseriesService;
protected final TelemetrySubscriptionService tsSubService;
protected final ApiLimitService apiLimitService; protected final ApiLimitService apiLimitService;
protected final RelationService relationService; protected final RelationService relationService;
protected final OwnerService ownerService; protected final OwnerService ownerService;
@ -370,4 +392,108 @@ public abstract class AbstractCalculatedFieldProcessingService {
return new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, endTs, 0, limit, Aggregation.NONE); return new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, endTs, 0, limit, Aggregation.NONE);
} }
protected void saveTelemetryResult(TenantId tenantId, EntityId entityId, TelemetryCalculatedFieldResult cfResult, List<CalculatedFieldId> cfIds, TbCallback callback) {
OutputType type = cfResult.getType();
JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue()));
log.trace("[{}][{}] Saving CF result: {}", tenantId, entityId, jsonResult);
SettableFuture<Void> future = SettableFuture.create();
switch (type) {
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<>() {
@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, OutputStrategy outputStrategy, AttributeScope scope, List<CalculatedFieldId> cfIds, SettableFuture<Void> future) {
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());
List<AttributeKvEntry> newAttributes = JsonConverter.convertToAttributes(jsonResult);
if (!attOutputStrategy.isUpdateAttributesOnlyOnValueChange()) {
saveAttributesInternal(tenantId, entityId, scope, cfIds, newAttributes, strategy, future);
return;
}
List<String> keys = newAttributes.stream().map(KvEntry::getKey).collect(Collectors.toList());
ListenableFuture<List<AttributeKvEntry>> findFuture = attributesService.find(tenantId, entityId, scope, keys);
DonAsynchron.withCallback(findFuture,
existingAttributes -> {
List<AttributeKvEntry> 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,
AttributeScope scope,
List<CalculatedFieldId> cfIds,
List<AttributeKvEntry> entries,
AttributesSaveRequest.Strategy strategy,
SettableFuture<Void> future) {
tsSubService.saveAttributes(AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(entityId)
.scope(scope)
.entries(entries)
.strategy(strategy)
.previousCalculatedFieldIds(cfIds)
.future(future)
.build());
}
private void saveTimeSeries(TenantId tenantId, EntityId entityId, JsonElement jsonResult, OutputStrategy outputStrategy, List<CalculatedFieldId> cfIds, long ts, SettableFuture<Void> 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);
}
}
private void saveTimeSeriesInternal(TenantId tenantId, EntityId entityId, JsonElement jsonResult, Long ttl, List<CalculatedFieldId> cfIds, long ts, TimeseriesSaveRequest.Strategy strategy, SettableFuture<Void> future) {
Map<Long, List<KvEntry>> tsKvMap = JsonConverter.convertToTelemetry(jsonResult, ts);
if (tsKvMap.isEmpty()) {
future.set(null);
return;
}
List<TsKvEntry> tsEntries = toTsKvEntryList(tsKvMap);
TimeseriesSaveRequest.Builder builder = TimeseriesSaveRequest.builder()
.tenantId(tenantId)
.entityId(entityId)
.entries(tsEntries)
.strategy(strategy)
.future(future);
if (ttl != null) {
builder.ttl(ttl);
}
if (cfIds != null && !cfIds.isEmpty()) {
builder.previousCalculatedFieldIds(cfIds);
}
tsSubService.saveTimeseries(builder.build());
}
} }

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

@ -41,9 +41,9 @@ public interface CalculatedFieldProcessingService {
Map<String, ArgumentEntry> fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map<String, Argument> arguments); Map<String, ArgumentEntry> fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map<String, Argument> arguments);
ArgumentEntry fetchMetricDuringInterval(TenantId tenantId, EntityId entityId, String argKey, AggMetric metric, AggIntervalEntry interval); void processResult(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback);
void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback); ArgumentEntry fetchMetricDuringInterval(TenantId tenantId, EntityId entityId, String argKey, AggMetric metric, AggIntervalEntry interval);
void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List<CalculatedFieldEntityCtxId> linkedCalculatedFields, TbCallback callback); void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List<CalculatedFieldEntityCtxId> linkedCalculatedFields, TbCallback callback);

55
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 com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.function.TriConsumer;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg;
import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; import org.thingsboard.server.actors.calculatedField.MultipleTbCallback;
@ -50,6 +51,7 @@ 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.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -76,8 +78,9 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF
RelationService relationService, RelationService relationService,
OwnerService ownerService, OwnerService ownerService,
TbClusterService clusterService, TbClusterService clusterService,
TelemetrySubscriptionService tsSubService,
PartitionService partitionService) { PartitionService partitionService) {
super(attributesService, timeseriesService, apiLimitService, relationService, ownerService); super(attributesService, timeseriesService, tsSubService, apiLimitService, relationService, ownerService);
this.clusterService = clusterService; this.clusterService = clusterService;
this.partitionService = partitionService; this.partitionService = partitionService;
} }
@ -133,27 +136,57 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF
return super.fetchMetricDuringInterval(tenantId, entityId, argKey, metric, interval); return super.fetchMetricDuringInterval(tenantId, entityId, argKey, metric, interval);
} }
@Override public void processResult(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback) {
public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback) { if (result instanceof AlarmCalculatedFieldResult) {
if (!(result instanceof PropagationCalculatedFieldResult propagationCalculatedFieldResult)) { sendMsgToRuleEngine(tenantId, entityId, callback, result.toTbMsg(entityId, cfIds));
TbMsg msg = result.toTbMsg(entityId, cfIds); return;
sendMsgToRuleEngine(tenantId, entityId, callback, msg); }
TelemetryCalculatedFieldResult telemetryResult = result instanceof TelemetryCalculatedFieldResult telemetryRes
? telemetryRes : ((PropagationCalculatedFieldResult) result).getResult();
switch (telemetryResult.getOutputStrategy().getType()) {
case IMMEDIATE -> processImmediately(tenantId, entityId, result, cfIds, callback);
case RULE_CHAIN -> pushMsgToRuleEngine(tenantId, entityId, result, cfIds, callback);
}
}
private void processImmediately(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List<CalculatedFieldId> 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));
return; return;
} }
List<EntityId> propagationEntityIds = propagationCalculatedFieldResult.getPropagationEntityIds(); callback.onSuccess();
}
private void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback) {
if (result instanceof PropagationCalculatedFieldResult propagationResult) {
handlePropagationResults(propagationResult, callback,
(entity, res, cb) -> sendMsgToRuleEngine(tenantId, entityId, cb, res.toTbMsg(entity, cfIds)));
return;
}
sendMsgToRuleEngine(tenantId, entityId, callback, result.toTbMsg(entityId, cfIds));
}
private void handlePropagationResults(PropagationCalculatedFieldResult propagationResult, TbCallback callback,
TriConsumer<EntityId, TelemetryCalculatedFieldResult, TbCallback> telemetryResultHandler) {
List<EntityId> propagationEntityIds = propagationResult.getPropagationEntityIds();
if (propagationEntityIds.isEmpty()) { if (propagationEntityIds.isEmpty()) {
callback.onSuccess(); callback.onSuccess();
return;
} }
if (propagationEntityIds.size() == 1) { if (propagationEntityIds.size() == 1) {
EntityId propagationEntityId = propagationEntityIds.get(0); EntityId propagationEntityId = propagationEntityIds.get(0);
TbMsg msg = result.toTbMsg(propagationEntityId, cfIds); telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), callback);
sendMsgToRuleEngine(tenantId, propagationEntityId, callback, msg);
return; return;
} }
MultipleTbCallback multipleTbCallback = new MultipleTbCallback(propagationEntityIds.size(), callback); MultipleTbCallback multipleTbCallback = new MultipleTbCallback(propagationEntityIds.size(), callback);
for (var propagationEntityId : propagationEntityIds) { for (var propagationEntityId : propagationEntityIds) {
TbMsg msg = result.toTbMsg(propagationEntityId, cfIds); telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), multipleTbCallback);
sendMsgToRuleEngine(tenantId, propagationEntityId, multipleTbCallback, msg);
} }
} }

2
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.Builder;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.AttributeScope; 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.cf.configuration.OutputType;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -37,6 +38,7 @@ public final class TelemetryCalculatedFieldResult implements CalculatedFieldResu
private final OutputType type; private final OutputType type;
private final AttributeScope scope; private final AttributeScope scope;
private final OutputStrategy outputStrategy;
private final JsonNode result; private final JsonNode result;
public static final TelemetryCalculatedFieldResult EMPTY = TelemetryCalculatedFieldResult.builder().result(null).build(); public static final TelemetryCalculatedFieldResult EMPTY = TelemetryCalculatedFieldResult.builder().result(null).build();

1
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(); Output output = ctx.getOutput();
return Futures.transform(resultFuture, return Futures.transform(resultFuture,
result -> TelemetryCalculatedFieldResult.builder() result -> TelemetryCalculatedFieldResult.builder()
.outputStrategy(output.getStrategy())
.type(output.getType()) .type(output.getType())
.scope(output.getScope()) .scope(output.getScope())
.result(JacksonUtil.valueToTree(result)) .result(JacksonUtil.valueToTree(result))

1
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); JsonNode outputResult = createResultJson(ctx.isUseLatestTs(), output.getName(), result);
return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder()
.outputStrategy(output.getStrategy())
.type(output.getType()) .type(output.getType())
.scope(output.getScope()) .scope(output.getScope())
.result(outputResult) .result(outputResult)

1
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java

@ -174,6 +174,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
lastMetricsEvalTs = System.currentTimeMillis(); lastMetricsEvalTs = System.currentTimeMillis();
scheduleReevaluation(); scheduleReevaluation();
return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder()
.outputStrategy(output.getStrategy())
.type(output.getType()) .type(output.getType())
.scope(output.getScope()) .scope(output.getScope())
.result(toSimpleResult(ctx.isUseLatestTs(), aggResult)) .result(toSimpleResult(ctx.isUseLatestTs(), aggResult))

1
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.EMPTY);
} }
return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder()
.outputStrategy(output.getStrategy())
.type(output.getType()) .type(output.getType())
.scope(output.getScope()) .scope(output.getScope())
.result(result) .result(result)

1
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(); OutputType outputType = ctx.getOutput().getType();
var result = TelemetryCalculatedFieldResult.builder() var result = TelemetryCalculatedFieldResult.builder()
.outputStrategy(ctx.getOutput().getStrategy())
.type(outputType) .type(outputType)
.scope(ctx.getOutput().getScope()) .scope(ctx.getOutput().getScope())
.result(toResultNode(outputType, valuesNode)) .result(toResultNode(outputType, valuesNode))

1
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java

@ -85,6 +85,7 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState
Output output = ctx.getOutput(); Output output = ctx.getOutput();
TelemetryCalculatedFieldResult.TelemetryCalculatedFieldResultBuilder telemetryCfBuilder = TelemetryCalculatedFieldResult.TelemetryCalculatedFieldResultBuilder telemetryCfBuilder =
TelemetryCalculatedFieldResult.builder() TelemetryCalculatedFieldResult.builder()
.outputStrategy(output.getStrategy())
.type(output.getType()) .type(output.getType())
.scope(output.getScope()); .scope(output.getScope());
ObjectNode valuesNode = JacksonUtil.newObjectNode(); ObjectNode valuesNode = JacksonUtil.newObjectNode();

6
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.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; 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.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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; 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.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
@ -175,9 +174,8 @@ public class CalculatedFieldCurrentOwnerTest extends AbstractControllerTest {
config.setExpression("a + 100"); config.setExpression("a + 100");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("result"); output.setName("result");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(0); output.setDecimalsByDefault(0);
config.setOutput(output); config.setOutput(output);

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

@ -33,14 +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.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; 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.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.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.PropagationCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; 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.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; 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.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.EntityCoordinates;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration;
@ -93,13 +94,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
config.setArguments(Map.of("T", argument)); config.setArguments(Map.of("T", argument));
config.setExpression("(T * 9/5) + 32"); config.setExpression("(T * 9/5) + 32");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("fahrenheitTemp"); output.setName("fahrenheitTemp");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);
calculatedField.setConfiguration(config); calculatedField.setConfiguration(config);
calculatedField.setVersion(1L);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
@ -121,10 +120,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("86.0"); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("86.0");
}); });
Output savedOutput = savedCalculatedField.getConfiguration().getOutput(); AttributesOutput newOutput = new AttributesOutput();
savedOutput.setType(OutputType.ATTRIBUTES); newOutput.setScope(AttributeScope.SERVER_SCOPE);
savedOutput.setScope(AttributeScope.SERVER_SCOPE); newOutput.setName("temperatureF");
savedOutput.setName("temperatureF"); config.setOutput(newOutput);
savedCalculatedField.setConfiguration(config);
savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS) await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS)
@ -179,9 +180,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
config.setArguments(Map.of("T", argument)); config.setArguments(Map.of("T", argument));
config.setExpression("(T * 9/5) + 32"); config.setExpression("(T * 9/5) + 32");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("fahrenheitTemp"); output.setName("fahrenheitTemp");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);
calculatedField.setConfiguration(config); calculatedField.setConfiguration(config);
@ -228,9 +228,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
config.setArguments(Map.of("T", argument)); config.setArguments(Map.of("T", argument));
config.setExpression("(T * 9/5) + 32"); config.setExpression("(T * 9/5) + 32");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("fahrenheitTemp"); output.setName("fahrenheitTemp");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);
calculatedField.setConfiguration(config); calculatedField.setConfiguration(config);
@ -292,9 +291,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
config.setExpression("x + y"); config.setExpression("x + y");
Output output = new Output(); AttributesOutput output = new AttributesOutput();
output.setName("z"); output.setName("z");
output.setType(OutputType.ATTRIBUTES);
output.setScope(AttributeScope.SERVER_SCOPE); output.setScope(AttributeScope.SERVER_SCOPE);
config.setOutput(output); config.setOutput(output);
@ -449,9 +447,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
config.setArguments(Map.of("T", argument)); config.setArguments(Map.of("T", argument));
config.setExpression("(T * 9/0) + 32"); config.setExpression("(T * 9/0) + 32");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("fahrenheitTemp"); output.setName("fahrenheitTemp");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);
calculatedField.setConfiguration(config); calculatedField.setConfiguration(config);
@ -499,9 +496,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
config.setArguments(Map.of("T", argument)); config.setArguments(Map.of("T", argument));
config.setExpression("(T * 9/5) + 32"); config.setExpression("(T * 9/5) + 32");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("fahrenheitTemp"); output.setName("fahrenheitTemp");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);
config.setUseLatestTs(true); config.setUseLatestTs(true);
@ -549,9 +545,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
config.setArguments(Map.of("a", argument1, "b", argument2)); config.setArguments(Map.of("a", argument1, "b", argument2));
config.setExpression("a + b"); config.setExpression("a + b");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("c"); output.setName("c");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);
config.setUseLatestTs(true); config.setUseLatestTs(true);
@ -603,9 +598,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
config.setArguments(Map.of("T", argument)); config.setArguments(Map.of("T", argument));
config.setExpression("return {\"ts\": ctx.latestTs, \"values\": {\"fahrenheitTemp\": (T * 1.8) + 32}};"); config.setExpression("return {\"ts\": ctx.latestTs, \"values\": {\"fahrenheitTemp\": (T * 1.8) + 32}};");
Output output = new Output(); config.setOutput(new TimeSeriesOutput());
output.setType(OutputType.TIME_SERIES);
config.setOutput(output);
calculatedField.setConfiguration(config); calculatedField.setConfiguration(config);
@ -639,9 +632,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
config.setArguments(Map.of("m", argument)); config.setArguments(Map.of("m", argument));
config.setExpression("m + 1"); config.setExpression("m + 1");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("m1"); output.setName("m1");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(0); output.setDecimalsByDefault(0);
config.setOutput(output); config.setOutput(output);
@ -714,8 +706,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup));
// Output to server attributes // Output to server attributes
Output out = new Output(); AttributesOutput out = new AttributesOutput();
out.setType(OutputType.ATTRIBUTES);
out.setScope(AttributeScope.SERVER_SCOPE); out.setScope(AttributeScope.SERVER_SCOPE);
cfg.setOutput(out); cfg.setOutput(out);
@ -825,8 +816,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup));
// Output to server attributes // Output to server attributes
Output out = new Output(); AttributesOutput out = new AttributesOutput();
out.setType(OutputType.ATTRIBUTES);
out.setScope(AttributeScope.SERVER_SCOPE); out.setScope(AttributeScope.SERVER_SCOPE);
cfg.setOutput(out); cfg.setOutput(out);
@ -923,8 +913,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup)); cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup));
// Server attributes output // Server attributes output
Output out = new Output(); AttributesOutput out = new AttributesOutput();
out.setType(OutputType.ATTRIBUTES);
out.setScope(AttributeScope.SERVER_SCOPE); out.setScope(AttributeScope.SERVER_SCOPE);
cfg.setOutput(out); cfg.setOutput(out);
@ -1034,8 +1023,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setExpression("{\"testResult\": t * 2}"); cfg.setExpression("{\"testResult\": t * 2}");
Output output = new Output(); AttributesOutput output = new AttributesOutput();
output.setType(OutputType.ATTRIBUTES);
output.setScope(AttributeScope.SERVER_SCOPE); output.setScope(AttributeScope.SERVER_SCOPE);
cfg.setOutput(output); cfg.setOutput(output);
@ -1111,9 +1099,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
cfg.setArguments(Map.of("temperatureComputed", arg)); cfg.setArguments(Map.of("temperatureComputed", arg));
Output output = new Output(); cfg.setOutput(new TimeSeriesOutput());
output.setType(OutputType.TIME_SERIES);
cfg.setOutput(output);
cf.setConfiguration(cfg); cf.setConfiguration(cfg);
@ -1181,9 +1167,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
config.setArguments(Map.of("a", argumentA, "b", argumentB)); config.setArguments(Map.of("a", argumentA, "b", argumentB));
config.setExpression("a + b"); config.setExpression("a + b");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("c"); output.setName("c");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(0); output.setDecimalsByDefault(0);
config.setOutput(output); config.setOutput(output);
@ -1210,6 +1195,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");
TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("fahrenheitTemp");
output.setDecimalsByDefault(1);
output.setStrategy(new TimeSeriesImmediateOutputStrategy(1000L, true, true, true, true));
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 { 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); return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class);
} }

5
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.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; 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.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.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.AggFunction;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric;
@ -209,8 +209,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
consumption.setDefaultValue(9999L); consumption.setDefaultValue(9999L);
aggMetrics.put("consumption", consumption); aggMetrics.put("consumption", consumption);
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(0); output.setDecimalsByDefault(0);
return createAggCf("Consumption per minute", entityId, return createAggCf("Consumption per minute", entityId,

15
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.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; 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.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.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.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.AggFunction;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; 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.AggKeyInput;
@ -626,8 +627,7 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
}); });
var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration();
Output output = new Output(); AttributesOutput output = new AttributesOutput();
output.setType(OutputType.ATTRIBUTES);
output.setScope(AttributeScope.SERVER_SCOPE); output.setScope(AttributeScope.SERVER_SCOPE);
configuration.setOutput(output); configuration.setOutput(output);
saveCalculatedField(cf); saveCalculatedField(cf);
@ -702,8 +702,7 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
avgMetric.setInput(new AggKeyInput("temp")); avgMetric.setInput(new AggKeyInput("temp"));
aggMetrics.put("avgTemperature", avgMetric); aggMetrics.put("avgTemperature", avgMetric);
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(0); output.setDecimalsByDefault(0);
return createAggCf("Average temperature", entityId, return createAggCf("Average temperature", entityId,
@ -739,8 +738,7 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
totalSpaces.setInput(new AggFunctionInput("return 1;")); totalSpaces.setInput(new AggFunctionInput("return 1;"));
aggMetrics.put("totalSpaces", totalSpaces); aggMetrics.put("totalSpaces", totalSpaces);
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(0); output.setDecimalsByDefault(0);
return createAggCf("Occupied spaces", entityId, return createAggCf("Occupied spaces", entityId,
@ -776,8 +774,7 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
totalSpaces.setInput(new AggFunctionInput("return 1;")); totalSpaces.setInput(new AggFunctionInput("return 1;"));
aggMetrics.put("totalSpaces", totalSpaces); aggMetrics.put("totalSpaces", totalSpaces);
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(0); output.setDecimalsByDefault(0);
return createAggCf("Occupied spaces", entityId, return createAggCf("Occupied spaces", entityId,

20
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.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; 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.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.PropagationCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; 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.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; 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.aggregation.AggKeyInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric;
import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration;
@ -286,13 +285,10 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
zoneGroupConfiguration.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); zoneGroupConfiguration.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration);
Output output = new Output();
output.setType(OutputType.TIME_SERIES);
config.setEntityCoordinates(new EntityCoordinates("latitide", "longitude")); config.setEntityCoordinates(new EntityCoordinates("latitide", "longitude"));
config.setZoneGroups(Map.of("safeArea", zoneGroupConfiguration)); config.setZoneGroups(Map.of("safeArea", zoneGroupConfiguration));
config.setScheduledUpdateEnabled(false); config.setScheduledUpdateEnabled(false);
config.setOutput(output); config.setOutput(new TimeSeriesOutput());
return config; return config;
} }
@ -310,10 +306,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
config.setApplyExpressionToResolvedArguments(false); config.setApplyExpressionToResolvedArguments(false);
config.setExpression(null); config.setExpression(null);
config.setOutput(new TimeSeriesOutput());
Output output = new Output();
output.setType(OutputType.TIME_SERIES);
config.setOutput(output);
Argument arg = new Argument(); Argument arg = new Argument();
arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
@ -337,9 +330,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
config.setWatermark(new Watermark(TimeUnit.DAYS.toSeconds(1))); config.setWatermark(new Watermark(TimeUnit.DAYS.toSeconds(1)));
config.setInterval(new HourInterval("Europe/Kiev", TimeUnit.MINUTES.toSeconds(15))); config.setInterval(new HourInterval("Europe/Kiev", TimeUnit.MINUTES.toSeconds(15)));
Output output = new Output(); config.setOutput(new TimeSeriesOutput());
output.setType(OutputType.TIME_SERIES);
config.setOutput(output);
return config; return config;
} }
@ -356,9 +347,8 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
config.setExpression("T - (100 - H) / 5"); config.setExpression("T - (100 - H) / 5");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("output"); output.setName("output");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);

6
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.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; 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.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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; 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.debug.DebugSettings;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -224,9 +223,8 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
config.setExpression("(T * 9/5) + 32"); config.setExpression("(T * 9/5) + 32");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("fahrenheitTemp"); output.setName("fahrenheitTemp");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(2); output.setDecimalsByDefault(2);
config.setOutput(output); config.setOutput(output);

6
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.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; 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.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.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.EntityCoordinates;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; 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)); config.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup));
Output output = new Output(); config.setOutput(new TimeSeriesOutput());
output.setType(OutputType.TIME_SERIES);
config.setOutput(output);
return config; return config;
} }

5
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.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; 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.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.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.OutputType;
import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; 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.ReferencedEntityKey;
@ -235,8 +235,7 @@ public class PropagationCalculatedFieldStateTest {
config.setArguments(Map.of(TEMPERATURE_ARGUMENT_NAME, temperatureArg)); config.setArguments(Map.of(TEMPERATURE_ARGUMENT_NAME, temperatureArg));
config.setExpression("{" + TEST_RESULT_EXPRESSION_KEY + ": " + TEMPERATURE_ARGUMENT_NAME + " * 2}"); config.setExpression("{" + TEST_RESULT_EXPRESSION_KEY + ": " + TEMPERATURE_ARGUMENT_NAME + " * 2}");
Output output = new Output(); AttributesOutput output = new AttributesOutput();
output.setType(OutputType.ATTRIBUTES);
output.setScope(AttributeScope.SERVER_SCOPE); output.setScope(AttributeScope.SERVER_SCOPE);
config.setOutput(output); config.setOutput(output);

5
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.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; 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.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.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Output; 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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
@ -221,8 +221,7 @@ public class ScriptCalculatedFieldStateTest {
config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity / 2 }"); config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity / 2 }");
Output output = new Output(); AttributesOutput output = new AttributesOutput();
output.setType(OutputType.ATTRIBUTES);
output.setScope(AttributeScope.SERVER_SCOPE); output.setScope(AttributeScope.SERVER_SCOPE);
config.setOutput(output); config.setOutput(output);

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

@ -28,9 +28,9 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; 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.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; 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.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Output; 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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
@ -263,9 +263,8 @@ public class SimpleCalculatedFieldStateTest {
config.setExpression("key1 + key2 + key3"); config.setExpression("key1 + key2 + key3");
Output output = new Output(); AttributesOutput output = new AttributesOutput();
output.setName("output"); output.setName("output");
output.setType(OutputType.ATTRIBUTES);
output.setScope(AttributeScope.SERVER_SCOPE); output.setScope(AttributeScope.SERVER_SCOPE);
output.setDecimalsByDefault(0); output.setDecimalsByDefault(0);

6
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.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; 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.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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; 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.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
@ -627,9 +626,8 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
config.setExpression("T - (100 - H) / 5"); config.setExpression("T - (100 - H) / 5");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("output"); output.setName("output");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);

6
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.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; 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.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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; 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.debug.DebugSettings;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; 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.data.DeviceData;
@ -1147,9 +1146,8 @@ public class VersionControlTest extends AbstractControllerTest {
config.setExpression("T - (100 - H) / 5"); config.setExpression("T - (100 - H) / 5");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("output"); output.setName("output");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);

37
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.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.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AttributesImmediateOutputStrategy implements AttributesOutputStrategy {
private boolean updateAttributesOnlyOnValueChange;
private boolean saveAttribute;
private boolean sendWsUpdate;
private boolean processCfs;
@Override
public OutputStrategyType getType() {
return OutputStrategyType.IMMEDIATE;
}
}

38
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 AttributesOutputStrategy strategy;
public AttributesOutput() {
this.strategy = new AttributesRuleChainOutputStrategy();
}
@Override
public OutputType getType() {
return OutputType.ATTRIBUTES;
}
}

33
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutputStrategy.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 = AttributesImmediateOutputStrategy.class, name = "IMMEDIATE"),
@JsonSubTypes.Type(value = AttributesRuleChainOutputStrategy.class, name = "RULE_CHAIN"),
})
public interface AttributesOutputStrategy extends OutputStrategy {
}

29
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.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 AttributesRuleChainOutputStrategy implements AttributesOutputStrategy {
@Override
public OutputStrategyType getType() {
return OutputStrategyType.RULE_CHAIN;
}
}

36
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java

@ -15,17 +15,39 @@
*/ */
package org.thingsboard.server.common.data.cf.configuration; 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 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; 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) @JsonInclude(JsonInclude.Include.NON_NULL)
public class Output { @JsonIgnoreProperties(ignoreUnknown = true)
public interface Output {
private String name; @JsonIgnore
private OutputType type; OutputType getType();
private AttributeScope scope;
private Integer decimalsByDefault; String getName();
OutputStrategy getStrategy();
default AttributeScope getScope() {
return null;
}
Integer getDecimalsByDefault();
void setDecimalsByDefault(Integer decimalsByDefault);
} }

25
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.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;
import com.fasterxml.jackson.annotation.JsonIgnore;
public interface OutputStrategy {
@JsonIgnore
OutputStrategyType getType();
}

22
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 {
IMMEDIATE, RULE_CHAIN
}

38
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.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.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TimeSeriesImmediateOutputStrategy implements TimeSeriesOutputStrategy {
private long ttl;
private boolean saveTimeSeries;
private boolean saveLatest;
private boolean sendWsUpdate;
private boolean processCfs;
@Override
public OutputStrategyType getType() {
return OutputStrategyType.IMMEDIATE;
}
}

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

31
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 {
}

29
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 implements TimeSeriesOutputStrategy {
@Override
public OutputStrategyType getType() {
return OutputStrategyType.RULE_CHAIN;
}
}

8
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.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; 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.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.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.AggFunctionInput;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric;
@ -74,7 +74,7 @@ public class EntityAggregationCalculatedFieldConfigurationTest {
cfg.setMetrics(Map.of("m", metric)); cfg.setMetrics(Map.of("m", metric));
cfg.setInterval(new HourInterval("Europe/Kiev", null)); cfg.setInterval(new HourInterval("Europe/Kiev", null));
cfg.setOutput(new Output()); cfg.setOutput(new TimeSeriesOutput());
assertThatThrownBy(cfg::validate) assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
@ -92,7 +92,7 @@ public class EntityAggregationCalculatedFieldConfigurationTest {
cfg.setMetrics(Map.of("m", metric)); cfg.setMetrics(Map.of("m", metric));
cfg.setInterval(new HourInterval("Europe/Kiev", null)); cfg.setInterval(new HourInterval("Europe/Kiev", null));
cfg.setOutput(new Output()); cfg.setOutput(new TimeSeriesOutput());
assertThatThrownBy(cfg::validate) assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
@ -106,7 +106,7 @@ public class EntityAggregationCalculatedFieldConfigurationTest {
cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST))); cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST)));
cfg.setMetrics(Map.of("m", validMetric())); cfg.setMetrics(Map.of("m", validMetric()));
cfg.setInterval(null); cfg.setInterval(null);
cfg.setOutput(new Output()); cfg.setOutput(new TimeSeriesOutput());
assertThatThrownBy(cfg::validate) assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)

37
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.Cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
import org.thingsboard.server.common.data.StringUtils; 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.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.NoXssValidator; import org.thingsboard.server.dao.service.NoXssValidator;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
public class KvUtils { public class KvUtils {
@ -74,4 +82,33 @@ public class KvUtils {
} }
} }
} }
public static List<TsKvEntry> toTsKvEntryList(Map<Long, List<KvEntry>> tsKvMap) {
List<TsKvEntry> tsKvEntryList = new ArrayList<>();
for (Map.Entry<Long, List<KvEntry>> tsKvEntry : tsKvMap.entrySet()) {
for (KvEntry kvEntry : tsKvEntry.getValue()) {
tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry));
}
}
return tsKvEntryList;
}
public static List<AttributeKvEntry> filterChangedAttr(List<AttributeKvEntry> currentAttributes, List<AttributeKvEntry> newAttributes) {
if (currentAttributes == null || currentAttributes.isEmpty()) {
return newAttributes;
}
Map<String, AttributeKvEntry> 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());
}
} }

6
dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java

@ -42,10 +42,9 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; 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.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; 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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; 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.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
@ -956,9 +955,8 @@ public class AssetServiceTest extends AbstractServiceTest {
config.setExpression("T - (100 - H) / 5"); config.setExpression("T - (100 - H) / 5");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("output"); output.setName("output");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);

6
dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java

@ -23,11 +23,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.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; 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.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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; 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.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.EntityCoordinates;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration;
@ -283,9 +282,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
config.setExpression("T - (100 - H) / 5"); config.setExpression("T - (100 - H) / 5");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("output"); output.setName("output");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);

6
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.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; 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.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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; 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.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -389,9 +388,8 @@ public class CustomerServiceTest extends AbstractServiceTest {
config.setExpression("T - (100 - H) / 5"); config.setExpression("T - (100 - H) / 5");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("output"); output.setName("output");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);

6
dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java

@ -49,10 +49,9 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; 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.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; 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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; 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.CustomerId;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.OtaPackageId;
@ -1288,9 +1287,8 @@ public class DeviceServiceTest extends AbstractServiceTest {
config.setExpression("T - (100 - H) / 5"); config.setExpression("T - (100 - H) / 5");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("output"); output.setName("output");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);

7
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.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; 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.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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; 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.LwM2MBootstrapClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential;
@ -241,9 +240,7 @@ public class MonitoringEntityService {
TEST_TELEMETRY_KEY, testDataArgument TEST_TELEMETRY_KEY, testDataArgument
)); ));
configuration.setExpression("return { \"" + TEST_CF_TELEMETRY_KEY + "\": " + TEST_TELEMETRY_KEY + " + \"-cf\" };"); configuration.setExpression("return { \"" + TEST_CF_TELEMETRY_KEY + "\": " + TEST_TELEMETRY_KEY + " + \"-cf\" };");
Output output = new Output(); configuration.setOutput(new TimeSeriesOutput());
output.setType(OutputType.TIME_SERIES);
configuration.setOutput(output);
calculatedField.setConfiguration(configuration); calculatedField.setConfiguration(configuration);
calculatedField.setDebugMode(true); calculatedField.setDebugMode(true);
tbClient.saveCalculatedField(calculatedField); tbClient.saveCalculatedField(calculatedField);

30
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.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; 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.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.AttributesOutput;
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.PropagationCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; 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.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; 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.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.EntityCoordinates;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration;
@ -183,10 +183,11 @@ public class CalculatedFieldTest extends AbstractContainerTest {
CalculatedField savedCalculatedField = createSimpleCalculatedField(); CalculatedField savedCalculatedField = createSimpleCalculatedField();
Output savedOutput = savedCalculatedField.getConfiguration().getOutput(); AttributesOutput output = new AttributesOutput();
savedOutput.setType(OutputType.ATTRIBUTES); output.setScope(SERVER_SCOPE);
savedOutput.setScope(SERVER_SCOPE); output.setName("temperatureF");
savedOutput.setName("temperatureF"); ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).setOutput(output);
testRestClient.postCalculatedField(savedCalculatedField); testRestClient.postCalculatedField(savedCalculatedField);
await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS) 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)); cfg.setZoneGroups(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration));
Output out = new Output(); AttributesOutput out = new AttributesOutput();
out.setType(OutputType.ATTRIBUTES);
out.setScope(SERVER_SCOPE); out.setScope(SERVER_SCOPE);
cfg.setOutput(out); cfg.setOutput(out);
cf.setConfiguration(cfg); cf.setConfiguration(cfg);
@ -458,8 +458,7 @@ public class CalculatedFieldTest extends AbstractContainerTest {
cfg.setExpression("{\"testResult\": t * 2}"); cfg.setExpression("{\"testResult\": t * 2}");
Output output = new Output(); AttributesOutput output = new AttributesOutput();
output.setType(OutputType.ATTRIBUTES);
output.setScope(AttributeScope.SERVER_SCOPE); output.setScope(AttributeScope.SERVER_SCOPE);
cfg.setOutput(output); cfg.setOutput(output);
@ -541,9 +540,7 @@ public class CalculatedFieldTest extends AbstractContainerTest {
arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
cfg.setArguments(Map.of("temperatureComputed", arg)); cfg.setArguments(Map.of("temperatureComputed", arg));
Output output = new Output(); cfg.setOutput(new TimeSeriesOutput());
output.setType(OutputType.TIME_SERIES);
cfg.setOutput(output);
cf.setConfiguration(cfg); cf.setConfiguration(cfg);
@ -613,9 +610,8 @@ public class CalculatedFieldTest extends AbstractContainerTest {
config.setExpression("(T * 9/5) + 32"); config.setExpression("(T * 9/5) + 32");
Output output = new Output(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("fahrenheitTemp"); output.setName("fahrenheitTemp");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(2); output.setDecimalsByDefault(2);
config.setOutput(output); config.setOutput(output);
@ -647,9 +643,7 @@ public class CalculatedFieldTest extends AbstractContainerTest {
config.setExpression(exampleScript); config.setExpression(exampleScript);
Output output = new Output(); config.setOutput(new TimeSeriesOutput());
output.setType(OutputType.TIME_SERIES);
config.setOutput(output);
calculatedField.setConfiguration(config); calculatedField.setConfiguration(config);

22
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java

@ -41,10 +41,7 @@ import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessingSettings.Advanced; import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessingSettings.Advanced;
@ -54,6 +51,7 @@ import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessin
import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY; 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.DataConstants.SCOPE;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST;
import static org.thingsboard.server.dao.util.KvUtils.filterChangedAttr;
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.ACTION,
@ -216,24 +214,6 @@ public class TbMsgAttributesNode implements TbNode {
.build()); .build());
} }
private List<AttributeKvEntry> filterChangedAttr(List<AttributeKvEntry> currentAttributes, List<AttributeKvEntry> newAttributes) {
if (currentAttributes == null || currentAttributes.isEmpty()) {
return newAttributes;
}
Map<String, AttributeKvEntry> 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) { private boolean checkSendNotification(AttributeScope scope) {
return config.isSendAttributesUpdatedNotification() && AttributeScope.CLIENT_SCOPE != scope; return config.isSendAttributesUpdatedNotification() && AttributeScope.CLIENT_SCOPE != scope;
} }

10
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.adaptor.JsonConverter;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile; 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.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.plugin.ComponentType; 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.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -50,6 +48,7 @@ import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessin
import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessingSettings.OnEveryMessage; 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.telemetry.settings.TimeseriesProcessingSettings.WebSocketsOnly;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST;
import static org.thingsboard.server.dao.util.KvUtils.toTsKvEntryList;
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.ACTION,
@ -148,12 +147,7 @@ public class TbMsgTimeseriesNode implements TbNode {
ctx.tellFailure(msg, new IllegalArgumentException("Msg body is empty: " + src)); ctx.tellFailure(msg, new IllegalArgumentException("Msg body is empty: " + src));
return; return;
} }
List<TsKvEntry> tsKvEntryList = new ArrayList<>(); List<TsKvEntry> tsKvEntryList = toTsKvEntryList(tsKvMap);
for (Map.Entry<Long, List<KvEntry>> tsKvEntry : tsKvMap.entrySet()) {
for (KvEntry kvEntry : tsKvEntry.getValue()) {
tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry));
}
}
String ttlValue = msg.getMetaData().getValue("TTL"); String ttlValue = msg.getMetaData().getValue("TTL");
long ttl = !StringUtils.isEmpty(ttlValue) ? Long.parseLong(ttlValue) : config.getDefaultTTL(); long ttl = !StringUtils.isEmpty(ttlValue) ? Long.parseLong(ttlValue) : config.getDefaultTTL();
if (ttl == 0L) { if (ttl == 0L) {

10
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts

@ -26,7 +26,8 @@ import {
CalculatedFieldConfiguration, CalculatedFieldConfiguration,
CalculatedFieldTestScriptFn, CalculatedFieldTestScriptFn,
CalculatedFieldType, CalculatedFieldType,
CalculatedFieldTypeTranslations CalculatedFieldTypeTranslations,
OutputStrategyType
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { oneSpaceInsideRegex } from '@shared/models/regex.constants';
import { EntityType } from '@shared/models/entity-type.models'; 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 { Observable } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; 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 { export interface CalculatedFieldDialogData {
value?: CalculatedField; value?: CalculatedField;
@ -121,6 +122,11 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
private applyDialogData(): void { private applyDialogData(): void {
const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {};
if (configuration.type !== CalculatedFieldType.ALARM) {
if (isDefined(configuration?.output) && !configuration?.output?.strategy) {
configuration.output.strategy = {type: OutputStrategyType.RULE_CHAIN};
}
}
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false}); this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false});
setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true}));
} }

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts

@ -32,8 +32,8 @@ import {
CalculatedFieldEntityAggregationConfiguration, CalculatedFieldEntityAggregationConfiguration,
CalculatedFieldOutput, CalculatedFieldOutput,
CalculatedFieldType, CalculatedFieldType,
defaultCalculatedFieldOutput,
notEmptyObjectValidator, notEmptyObjectValidator,
OutputType
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { filter, map } from 'rxjs/operators'; import { filter, map } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -102,9 +102,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
watermark: this.fb.group({ watermark: this.fb.group({
duration: [HOUR/SECOND, Validators.required], duration: [HOUR/SECOND, Validators.required],
}), }),
output: this.fb.control<CalculatedFieldOutput>({ output: this.fb.control<CalculatedFieldOutput>(defaultCalculatedFieldOutput),
type: OutputType.Timeseries,
}),
}); });
arguments$ = this.entityAggregationConfiguration.get('arguments').valueChanges.pipe( arguments$ = this.entityAggregationConfiguration.get('arguments').valueChanges.pipe(

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts

@ -30,9 +30,9 @@ import {
CalculatedFieldGeofencingConfiguration, CalculatedFieldGeofencingConfiguration,
CalculatedFieldOutput, CalculatedFieldOutput,
CalculatedFieldType, CalculatedFieldType,
defaultCalculatedFieldOutput,
getCalculatedFieldCurrentEntityFilter, getCalculatedFieldCurrentEntityFilter,
notEmptyObjectValidator, notEmptyObjectValidator
OutputType
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
@ -83,7 +83,7 @@ export class GeofencingConfigurationComponent implements ControlValueAccessor, V
zoneGroups: this.fb.control<Record<string, CalculatedFieldGeofencing>>({}, notEmptyObjectValidator()), zoneGroups: this.fb.control<Record<string, CalculatedFieldGeofencing>>({}, notEmptyObjectValidator()),
scheduledUpdateEnabled: [true], scheduledUpdateEnabled: [true],
scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF], scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF],
output: this.fb.control<CalculatedFieldOutput>({type: OutputType.Timeseries}) output: this.fb.control<CalculatedFieldOutput>(defaultCalculatedFieldOutput)
}); });
currentEntityFilter: EntityFilter; currentEntityFilter: EntityFilter;

84
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html

@ -78,6 +78,90 @@
} }
} }
</div> </div>
<div class="tb-form-panel stroked" formGroupName="strategy">
<div class="flex flex-row items-center justify-between gap-2">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.strategy' | translate }}">
{{ 'calculated-fields.output-strategy.strategy' | translate }}
</div>
<tb-toggle-select formControlName="type" selectMediaBreakpoint="xs" disablePagination appearance="fill">
@for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) {
<tb-toggle-option [value]="outputStrategyType">{{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }}</tb-toggle-option>
}
</tb-toggle-select>
</div>
@if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) {
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title tb-normal" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.processing-options' | translate }}">
{{ 'calculated-fields.output-strategy.processing-options' | translate }}
</div>
<mat-chip-listbox multiple>
@if (outputForm.get('type').value === OutputType.Timeseries) {
<mat-chip-option
[selected]="outputForm.get('strategy.saveTimeSeries').value"
[disabled]="outputForm.get('strategy.saveTimeSeries').disabled"
(click)="toggleChip('saveTimeSeries')">
{{ 'calculated-fields.output-strategy.save-time-series' | translate }}
</mat-chip-option>
<mat-chip-option
[selected]="outputForm.get('strategy.saveLatest').value"
[disabled]="outputForm.get('strategy.saveLatest').disabled"
(click)="toggleChip('saveLatest')">
{{ 'calculated-fields.output-strategy.save-latest-values' | translate }}
</mat-chip-option>
} @else {
<mat-chip-option
[selected]="outputForm.get('strategy.saveAttribute').value"
[disabled]="outputForm.get('strategy.saveAttribute').disabled"
(click)="toggleChip('saveAttribute')">
{{ 'calculated-fields.output-strategy.save-database' | translate }}
</mat-chip-option>
}
<mat-chip-option
[selected]="outputForm.get('strategy.sendWsUpdate').value"
[disabled]="outputForm.get('strategy.sendWsUpdate').disabled"
(click)="toggleChip('sendWsUpdate')">
{{ 'calculated-fields.output-strategy.send-web-sockets' | translate }}
</mat-chip-option>
<mat-chip-option
[selected]="outputForm.get('strategy.processCfs').value"
[disabled]="outputForm.get('strategy.processCfs').disabled"
(click)="toggleChip('processCfs')">
{{ 'calculated-fields.output-strategy.save-calculated-fields' | translate }}
</mat-chip-option>
</mat-chip-listbox>
</div>
@if (outputForm.get('type').value === OutputType.Attribute) {
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="updateAttributesOnlyOnValueChange">
<div tb-hint-tooltip-icon="{{ (outputForm.get('strategy.updateAttributesOnlyOnValueChange').value
? 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change-enabled'
: 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change') | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.update-attributes-only-on-value-change</div>
</div>
</mat-slide-toggle>
</div>
} @else {
<tb-time-unit-input
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 }}"
formControlName="ttl">
<mat-icon class="help-icon mr-2 cursor-pointer"
aria-hidden="false"
aria-label="help-icon"
color="primary"
matSuffix
matTooltip="{{ 'calculated-fields.output-strategy.hint.ttl' | translate }}">
help_outline
</mat-icon>
</tb-time-unit-input>
}
}
</div>
</div> </div>
<ng-template #decimalsByDefaultField> <ng-template #decimalsByDefaultField>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic" [formGroup]="outputForm"> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic" [formGroup]="outputForm">

22
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss

@ -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.
*/
:host ::ng-deep {
.mat-mdc-chip-disabled {
.mdc-evolution-chip__action {
cursor: default
}
}
}

58
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 { import {
CalculatedFieldOutput, CalculatedFieldOutput,
CalculatedFieldSimpleOutput, CalculatedFieldSimpleOutput,
OutputStrategyType,
OutputStrategyTypeTranslations,
OutputType, OutputType,
OutputTypeTranslations OutputTypeTranslations
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
@ -52,6 +54,7 @@ import { coerceBoolean } from '@shared/decorators/coercion';
multi: true multi: true
} }
], ],
styleUrls: ['./calculated-field-output.component.scss'],
}) })
export class CalculatedFieldOutputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { export class CalculatedFieldOutputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges {
@ -79,6 +82,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
readonly OutputTypeTranslations = OutputTypeTranslations; readonly OutputTypeTranslations = OutputTypeTranslations;
readonly EntityType = EntityType; readonly EntityType = EntityType;
readonly OutputStrategyType = OutputStrategyType;
readonly OutputStrategyTypes = Object.values(OutputStrategyType) as OutputStrategyType[];
readonly OutputStrategyTypeTranslations = OutputStrategyTypeTranslations;
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
private destroyRef = inject(DestroyRef); private destroyRef = inject(DestroyRef);
@ -87,6 +94,16 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
scope: [{value: AttributeScope.SERVER_SCOPE, disabled: true}], scope: [{value: AttributeScope.SERVER_SCOPE, disabled: true}],
type: [OutputType.Timeseries], type: [OutputType.Timeseries],
decimalsByDefault: [null as number, [Validators.min(0), Validators.max(15), Validators.pattern(digitsRegex)]], 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 = () => { }; private propagateChange: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void = () => { };
@ -94,14 +111,23 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
ngOnInit() { ngOnInit() {
this.outputForm.get('type').valueChanges this.outputForm.get('type').valueChanges
.pipe(takeUntilDestroyed(this.destroyRef)) .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.updatedFormWithMode();
this.outputForm.valueChanges.pipe( this.outputForm.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe((value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => { ).subscribe((value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => {
this.updatedModel(value) this.updatedModel(value);
}) })
} }
@ -141,6 +167,14 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
this.outputForm.enable({emitEvent: false}); this.outputForm.enable({emitEvent: false});
this.updatedFormWithMode(); this.updatedFormWithMode();
this.toggleScopeByOutputType(this.outputForm.get('type').value); 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);
} }
} }
@ -177,4 +211,24 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
this.outputForm.get('type').disable({emitEvent: false}); this.outputForm.get('type').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});
}
}
}
} }

5
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts

@ -31,6 +31,7 @@ import {
CalculatedFieldOutput, CalculatedFieldOutput,
CalculatedFieldPropagationConfiguration, CalculatedFieldPropagationConfiguration,
CalculatedFieldType, CalculatedFieldType,
defaultCalculatedFieldOutput,
getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights, getCalculatedFieldArgumentsHighlights,
notEmptyObjectValidator, notEmptyObjectValidator,
@ -84,9 +85,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor,
relationType: ['Contains', Validators.required], relationType: ['Contains', Validators.required],
}), }),
expression: [calculatedFieldDefaultScript], expression: [calculatedFieldDefaultScript],
output: this.fb.control<CalculatedFieldOutput>({ output: this.fb.control<CalculatedFieldOutput>(defaultCalculatedFieldOutput),
type: OutputType.Timeseries,
}),
}); });
readonly ScriptLanguage = ScriptLanguage; readonly ScriptLanguage = ScriptLanguage;

5
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, CalculatedFieldOutput,
CalculatedFieldRelatedAggregationConfiguration, CalculatedFieldRelatedAggregationConfiguration,
CalculatedFieldType, CalculatedFieldType,
defaultCalculatedFieldOutput,
getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights, getCalculatedFieldArgumentsHighlights,
notEmptyObjectValidator, notEmptyObjectValidator,
@ -87,9 +88,7 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu
arguments: this.fb.control({}, notEmptyObjectValidator()), arguments: this.fb.control({}, notEmptyObjectValidator()),
metrics: this.fb.control({}, notEmptyObjectValidator()), metrics: this.fb.control({}, notEmptyObjectValidator()),
deduplicationIntervalInSec: [this.minAllowedDeduplicationIntervalInSecForCF], deduplicationIntervalInSec: [this.minAllowedDeduplicationIntervalInSecForCF],
output: this.fb.control<CalculatedFieldOutput>({ output: this.fb.control<CalculatedFieldOutput>(defaultCalculatedFieldOutput),
type: OutputType.Timeseries,
}),
useLatestTs: [false] useLatestTs: [false]
}); });

7
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts

@ -31,6 +31,7 @@ import {
CalculatedFieldSimpleConfiguration, CalculatedFieldSimpleConfiguration,
CalculatedFieldSimpleOutput, CalculatedFieldSimpleOutput,
CalculatedFieldType, CalculatedFieldType,
defaultSimpleCalculatedFieldOutput,
getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights, getCalculatedFieldArgumentsHighlights,
OutputType OutputType
@ -84,11 +85,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
arguments: this.fb.control({}), arguments: this.fb.control({}),
expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
expressionSCRIPT: [calculatedFieldDefaultScript], expressionSCRIPT: [calculatedFieldDefaultScript],
output: this.fb.control<CalculatedFieldSimpleOutput>({ output: this.fb.control<CalculatedFieldSimpleOutput>(defaultSimpleCalculatedFieldOutput),
name: '',
type: OutputType.Timeseries,
decimalsByDefault: null
}),
useLatestTs: [false] useLatestTs: [false]
}); });

64
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -206,10 +206,12 @@ export type CalculatedFieldOutput =
export interface CalculatedFieldOutputAttribute { export interface CalculatedFieldOutputAttribute {
type: OutputType.Attribute, type: OutputType.Attribute,
scope: AttributeScope; scope: AttributeScope;
strategy: AttributeOutputStrategy;
} }
export interface CalculatedFieldOutputTimeSeries { export interface CalculatedFieldOutputTimeSeries {
type: OutputType.Timeseries; type: OutputType.Timeseries;
strategy: TimeSeriesOutputStrategy;
} }
export type CalculatedFieldSimpleOutput = CalculatedFieldOutput & { export type CalculatedFieldSimpleOutput = CalculatedFieldOutput & {
@ -217,6 +219,51 @@ export type CalculatedFieldSimpleOutput = CalculatedFieldOutput & {
decimalsByDefault?: number; 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, string>(
[
[OutputStrategyType.IMMEDIATE, 'calculated-fields.output-strategy.process-right-away'],
[OutputStrategyType.RULE_CHAIN, 'calculated-fields.output-strategy.process-rule-chains'],
]
)
export enum ArgumentEntityType { export enum ArgumentEntityType {
Current = 'CURRENT', Current = 'CURRENT',
Device = 'DEVICE', Device = 'DEVICE',
@ -511,6 +558,23 @@ export type CalculatedFieldArgumentEventValue<ValueType = unknown> = CalculatedF
export type CalculatedFieldEventArguments<ValueType = unknown> = Record<string, CalculatedFieldArgumentEventValue<ValueType>>; export type CalculatedFieldEventArguments<ValueType = unknown> = Record<string, CalculatedFieldArgumentEventValue<ValueType>>;
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 = { export const CalculatedFieldCtxLatestTelemetryArgumentAutocomplete = {
meta: 'object', meta: 'object',
type: '{ ts: number; value: any; }', type: '{ ts: number; value: any; }',

22
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1227,6 +1227,28 @@
"filter": "Filter", "filter": "Filter",
"filter-hint": "Enables filtering of entities during aggregation. The filter function must return a boolean value and can use all configured arguments." "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."
}
},
"aggregate-interval-type": "Aggregate interval type", "aggregate-interval-type": "Aggregate interval type",
"aggregate-interval-value": "Aggregate interval value", "aggregate-interval-value": "Aggregate interval value",
"aggregate-interval-value-required": "Aggregate interval value is required", "aggregate-interval-value-required": "Aggregate interval value is required",

Loading…
Cancel
Save