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;
try {
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());
stateSizeChecked = true;
if (state.isSizeOk()) {

View File

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

View File

@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
@ -47,15 +48,18 @@ public interface CalculatedFieldState {
long getLatestTimestamp();
void setDirty(boolean dirty);
default void setDirty(boolean dirty) {
}
boolean isDirty();
default boolean isDirty() {
return false;
}
void setRequiredArguments(List<String> requiredArguments);
boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues);
ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx);
ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx);
@JsonIgnore
boolean isReady();
@ -70,7 +74,6 @@ public interface CalculatedFieldState {
void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize);
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) {
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_LONGITUDE_ARGUMENT_KEY;
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.coordinateKeys;
@Data
@AllArgsConstructor
@ -116,20 +115,20 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
}
@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 longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue();
Coordinates entityCoordinates = new Coordinates(latitude, longitude);
var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
if (configuration.isTrackRelationToZones()) {
// TODO: currently creates relation to device profile if CF created for profile)
return calculateWithRelations(ctx, entityCoordinates, configuration);
if (configuration.isCreateRelationsWithMatchedZones()) {
return calculateWithRelations(entityId, ctx, entityCoordinates, configuration);
}
return calculateWithoutRelations(ctx, entityCoordinates, configuration);
}
private ListenableFuture<CalculatedFieldResult> calculateWithRelations(
EntityId entityId,
CalculatedFieldCtx ctx,
Coordinates entityCoordinates,
GeofencingCalculatedFieldConfiguration configuration) {
@ -160,7 +159,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
List<ListenableFuture<Boolean>> relationFutures = zoneEventMap.entrySet().stream()
.filter(entry -> entry.getValue().isTransitionEvent())
.map(entry -> {
EntityRelation relation = toRelation(entry.getKey(), ctx, configuration);
EntityRelation relation = toRelation(entry.getKey(), entityId, configuration);
return switch (entry.getValue()) {
case ENTERED -> ctx.getRelationService().saveRelationAsync(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() {
return arguments.entrySet()
.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()));
}
@ -259,10 +257,10 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
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()) {
case TO -> new EntityRelation(zoneId, ctx.getEntityId(), configuration.getZoneRelationType());
case FROM -> new EntityRelation(ctx.getEntityId(), zoneId, configuration.getZoneRelationType());
case TO -> new EntityRelation(zoneId, entityId, 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.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.ArrayList;
@ -53,7 +54,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
}
@Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) {
public ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx) {
Map<String, TbelCfArg> arguments = new LinkedHashMap<>();
List<Object> args = new ArrayList<>(ctx.getArgNames().size() + 1);
args.add(new Object()); // first element is a ctx, but we will set it later;

View File

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

View File

@ -125,7 +125,7 @@ public class ScriptCalculatedFieldStateTest {
void testPerformCalculation() throws ExecutionException, InterruptedException {
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();
Output output = getCalculatedFieldConfig().getOutput();

View File

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

View File

@ -41,7 +41,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
ENTITY_ID_LONGITUDE_ARGUMENT_KEY
);
private boolean trackRelationToZones;
private boolean createRelationsWithMatchedZones;
private String zoneRelationType;
private EntitySearchDirection zoneRelationDirection;
private Map<String, GeofencingZoneGroupConfiguration> geofencingZoneGroupConfigurations;
@ -52,7 +52,6 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
}
// TODO: update validate method in PE version.
// Add relation tracking configuration validation
@Override
public void validate() {
if (arguments == null) {
@ -72,6 +71,19 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
}
validateZoneGroupAruguments(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) {
@ -146,7 +158,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
.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();
if (refEntityKey == null || refEntityKey.getType() == null) {
throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + argumentKey);