Added validation for new configuration + fixed relation creation for profile entities

This commit is contained in:
dshvaika 2025-08-11 13:23:17 +03:00
parent 3643b54985
commit baba433f0f
9 changed files with 42 additions and 29 deletions

View File

@ -321,7 +321,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
boolean stateSizeChecked = false; boolean stateSizeChecked = false;
try { try {
if (ctx.isInitialized() && state.isReady()) { if (ctx.isInitialized() && state.isReady()) {
CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); CalculatedFieldResult calculationResult = state.performCalculation(entityId, ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS);
state.checkStateSize(ctxId, ctx.getMaxStateSize()); state.checkStateSize(ctxId, ctx.getMaxStateSize());
stateSizeChecked = true; stateSizeChecked = true;
if (state.isSizeOk()) { if (state.isSizeOk()) {

View File

@ -35,15 +35,13 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
protected long latestTimestamp = -1; protected long latestTimestamp = -1;
private boolean dirty;
public BaseCalculatedFieldState(List<String> requiredArguments) { public BaseCalculatedFieldState(List<String> requiredArguments) {
this.requiredArguments = requiredArguments; this.requiredArguments = requiredArguments;
this.arguments = new HashMap<>(); this.arguments = new HashMap<>();
} }
public BaseCalculatedFieldState() { public BaseCalculatedFieldState() {
this(new ArrayList<>(), new HashMap<>(), false, -1, false); this(new ArrayList<>(), new HashMap<>(), false, -1);
} }
@Override @Override

View File

@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
@ -47,15 +48,18 @@ public interface CalculatedFieldState {
long getLatestTimestamp(); long getLatestTimestamp();
void setDirty(boolean dirty); default void setDirty(boolean dirty) {
}
boolean isDirty(); default boolean isDirty() {
return false;
}
void setRequiredArguments(List<String> requiredArguments); void setRequiredArguments(List<String> requiredArguments);
boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues); boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues);
ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx); ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx);
@JsonIgnore @JsonIgnore
boolean isReady(); boolean isReady();
@ -70,7 +74,6 @@ public interface CalculatedFieldState {
void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize); void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize);
default void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { default void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) {
// TODO: Do we need to restrict the size of Geofencing arguments? Number of zones?
if (entry instanceof TsRollingArgumentEntry || entry instanceof GeofencingArgumentEntry) { if (entry instanceof TsRollingArgumentEntry || entry instanceof GeofencingArgumentEntry) {
return; return;
} }

View File

@ -43,7 +43,6 @@ import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY;
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY;
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.coordinateKeys;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@ -116,20 +115,20 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
} }
@Override @Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) { public ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx) {
double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue();
double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue();
Coordinates entityCoordinates = new Coordinates(latitude, longitude); Coordinates entityCoordinates = new Coordinates(latitude, longitude);
var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
if (configuration.isTrackRelationToZones()) { if (configuration.isCreateRelationsWithMatchedZones()) {
// TODO: currently creates relation to device profile if CF created for profile) return calculateWithRelations(entityId, ctx, entityCoordinates, configuration);
return calculateWithRelations(ctx, entityCoordinates, configuration);
} }
return calculateWithoutRelations(ctx, entityCoordinates, configuration); return calculateWithoutRelations(ctx, entityCoordinates, configuration);
} }
private ListenableFuture<CalculatedFieldResult> calculateWithRelations( private ListenableFuture<CalculatedFieldResult> calculateWithRelations(
EntityId entityId,
CalculatedFieldCtx ctx, CalculatedFieldCtx ctx,
Coordinates entityCoordinates, Coordinates entityCoordinates,
GeofencingCalculatedFieldConfiguration configuration) { GeofencingCalculatedFieldConfiguration configuration) {
@ -160,7 +159,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
List<ListenableFuture<Boolean>> relationFutures = zoneEventMap.entrySet().stream() List<ListenableFuture<Boolean>> relationFutures = zoneEventMap.entrySet().stream()
.filter(entry -> entry.getValue().isTransitionEvent()) .filter(entry -> entry.getValue().isTransitionEvent())
.map(entry -> { .map(entry -> {
EntityRelation relation = toRelation(entry.getKey(), ctx, configuration); EntityRelation relation = toRelation(entry.getKey(), entityId, configuration);
return switch (entry.getValue()) { return switch (entry.getValue()) {
case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation); case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation);
case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation); case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation);
@ -218,11 +217,10 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
} }
} }
// TODO: Create a new class field to not do this on each calculation.
private Map<String, GeofencingArgumentEntry> getGeofencingArguments() { private Map<String, GeofencingArgumentEntry> getGeofencingArguments() {
return arguments.entrySet() return arguments.entrySet()
.stream() .stream()
.filter(entry -> !coordinateKeys.contains(entry.getKey())) .filter(entry -> entry.getValue().getType().equals(ArgumentEntryType.GEOFENCING))
.collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue()));
} }
@ -259,10 +257,10 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
return Optional.empty(); return Optional.empty();
} }
private EntityRelation toRelation(EntityId zoneId, CalculatedFieldCtx ctx, GeofencingCalculatedFieldConfiguration configuration) { private EntityRelation toRelation(EntityId zoneId, EntityId entityId, GeofencingCalculatedFieldConfiguration configuration) {
return switch (configuration.getZoneRelationDirection()) { return switch (configuration.getZoneRelationDirection()) {
case TO -> new EntityRelation(zoneId, ctx.getEntityId(), configuration.getZoneRelationType()); case TO -> new EntityRelation(zoneId, entityId, configuration.getZoneRelationType());
case FROM -> new EntityRelation(ctx.getEntityId(), zoneId, configuration.getZoneRelationType()); case FROM -> new EntityRelation(entityId, zoneId, configuration.getZoneRelationType());
}; };
} }

View File

@ -27,6 +27,7 @@ import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.ArrayList; import java.util.ArrayList;
@ -53,7 +54,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
} }
@Override @Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) { public ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx) {
Map<String, TbelCfArg> arguments = new LinkedHashMap<>(); Map<String, TbelCfArg> arguments = new LinkedHashMap<>();
List<Object> args = new ArrayList<>(ctx.getArgNames().size() + 1); List<Object> args = new ArrayList<>(ctx.getArgNames().size() + 1);
args.add(new Object()); // first element is a ctx, but we will set it later; args.add(new Object()); // first element is a ctx, but we will set it later;

View File

@ -25,6 +25,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbUtils; import org.thingsboard.script.api.tbel.TbUtils;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
@ -52,7 +53,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
} }
@Override @Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) { public ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx) {
var expr = ctx.getCustomExpression().get(); var expr = ctx.getCustomExpression().get();
for (Map.Entry<String, ArgumentEntry> entry : this.arguments.entrySet()) { for (Map.Entry<String, ArgumentEntry> entry : this.arguments.entrySet()) {

View File

@ -125,7 +125,7 @@ public class ScriptCalculatedFieldStateTest {
void testPerformCalculation() throws ExecutionException, InterruptedException { void testPerformCalculation() throws ExecutionException, InterruptedException {
state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry));
CalculatedFieldResult result = state.performCalculation(ctx).get(); CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
assertThat(result).isNotNull(); assertThat(result).isNotNull();
Output output = getCalculatedFieldConfig().getOutput(); Output output = getCalculatedFieldConfig().getOutput();

View File

@ -134,7 +134,7 @@ public class SimpleCalculatedFieldStateTest {
"key3", key3ArgEntry "key3", key3ArgEntry
)); ));
CalculatedFieldResult result = state.performCalculation(ctx).get(); CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
assertThat(result).isNotNull(); assertThat(result).isNotNull();
Output output = getCalculatedFieldConfig().getOutput(); Output output = getCalculatedFieldConfig().getOutput();
@ -151,7 +151,7 @@ public class SimpleCalculatedFieldStateTest {
"key3", key3ArgEntry "key3", key3ArgEntry
)); ));
assertThatThrownBy(() -> state.performCalculation(ctx)) assertThatThrownBy(() -> state.performCalculation(ctx.getEntityId(), ctx))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Argument 'key2' is not a number."); .hasMessage("Argument 'key2' is not a number.");
} }
@ -164,7 +164,7 @@ public class SimpleCalculatedFieldStateTest {
"key3", key3ArgEntry "key3", key3ArgEntry
)); ));
CalculatedFieldResult result = state.performCalculation(ctx).get(); CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
assertThat(result).isNotNull(); assertThat(result).isNotNull();
Output output = getCalculatedFieldConfig().getOutput(); Output output = getCalculatedFieldConfig().getOutput();
@ -185,7 +185,7 @@ public class SimpleCalculatedFieldStateTest {
output.setDecimalsByDefault(3); output.setDecimalsByDefault(3);
ctx.setOutput(output); ctx.setOutput(output);
CalculatedFieldResult result = state.performCalculation(ctx).get(); CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
assertThat(result).isNotNull(); assertThat(result).isNotNull();
assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getType()).isEqualTo(output.getType());

View File

@ -41,7 +41,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
ENTITY_ID_LONGITUDE_ARGUMENT_KEY ENTITY_ID_LONGITUDE_ARGUMENT_KEY
); );
private boolean trackRelationToZones; private boolean createRelationsWithMatchedZones;
private String zoneRelationType; private String zoneRelationType;
private EntitySearchDirection zoneRelationDirection; private EntitySearchDirection zoneRelationDirection;
private Map<String, GeofencingZoneGroupConfiguration> geofencingZoneGroupConfigurations; private Map<String, GeofencingZoneGroupConfiguration> geofencingZoneGroupConfigurations;
@ -52,7 +52,6 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
} }
// TODO: update validate method in PE version. // TODO: update validate method in PE version.
// Add relation tracking configuration validation
@Override @Override
public void validate() { public void validate() {
if (arguments == null) { if (arguments == null) {
@ -72,6 +71,19 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
} }
validateZoneGroupAruguments(zoneGroupsArguments); validateZoneGroupAruguments(zoneGroupsArguments);
validateZoneGroupConfigurations(zoneGroupsArguments); validateZoneGroupConfigurations(zoneGroupsArguments);
validateZoneRelationsConfiguration();
}
private void validateZoneRelationsConfiguration() {
if (!createRelationsWithMatchedZones) {
return;
}
if (StringUtils.isBlank(zoneRelationType)) {
throw new IllegalArgumentException("Zone relation type must be specified when to maintain relations with matched zones!");
}
if (zoneRelationDirection == null) {
throw new IllegalArgumentException("Zone relation direction must be specified to maintain relations with matched zones!");
}
} }
private void validateZoneGroupConfigurations(Map<String, Argument> zoneGroupsArguments) { private void validateZoneGroupConfigurations(Map<String, Argument> zoneGroupsArguments) {
@ -146,7 +158,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} }
private static ReferencedEntityKey validateAndGetRefEntityKey(Argument argument, String argumentKey) { private ReferencedEntityKey validateAndGetRefEntityKey(Argument argument, String argumentKey) {
ReferencedEntityKey refEntityKey = argument.getRefEntityKey(); ReferencedEntityKey refEntityKey = argument.getRefEntityKey();
if (refEntityKey == null || refEntityKey.getType() == null) { if (refEntityKey == null || refEntityKey.getType() == null) {
throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + argumentKey); throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + argumentKey);