From ede9fd5e05e91665e8da7d39407e1e7df1d27eac Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 6 Aug 2025 16:12:33 +0300 Subject: [PATCH] Added support to use only one zone type instead of two + minor validation fixes --- .../state/GeofencingCalculatedFieldState.java | 27 ++++--- .../cf/ctx/state/GeofencingZoneState.java | 3 + ...eofencingCalculatedFieldConfiguration.java | 72 ++++++++++++++----- ...lationQueryDynamicSourceConfiguration.java | 6 +- .../CalculatedFieldDataValidator.java | 4 +- 5 files changed, 77 insertions(+), 35 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index a98db7f0f0..960fdf217f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.cf.ctx.state; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; @@ -31,12 +32,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ALLOWED_ZONES_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.RESTRICTED_ZONES_ARGUMENT_KEY; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ALLOWED_ZONES_ARGUMENT_KEY; @Data +@AllArgsConstructor public class GeofencingCalculatedFieldState implements CalculatedFieldState { private List requiredArguments; @@ -46,9 +48,8 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { private long latestTimestamp = -1; - public GeofencingCalculatedFieldState() { - this(List.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ALLOWED_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY)); + this(new ArrayList<>(), new HashMap<>(), false, -1); } public GeofencingCalculatedFieldState(List argNames) { @@ -112,8 +113,12 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { @Override public ListenableFuture> performCalculation(CalculatedFieldCtx ctx) { - List savedZonesStatesResults = updateGeofencingZonesState(ctx, false); - List restrictedZonesStatesResults = updateGeofencingZonesState(ctx, true); + 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); + + List savedZonesStatesResults = updateGeofencingZonesState(ctx, entityCoordinates, false); + List restrictedZonesStatesResults = updateGeofencingZonesState(ctx, entityCoordinates, true); List allZoneStatesResults = new ArrayList<>(savedZonesStatesResults.size() + restrictedZonesStatesResults.size()); @@ -137,15 +142,15 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { } } - private List updateGeofencingZonesState(CalculatedFieldCtx ctx, boolean restricted) { - var results = new ArrayList(); - 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); + private List updateGeofencingZonesState(CalculatedFieldCtx ctx, Coordinates entityCoordinates, boolean restricted) { String zoneKey = restricted ? RESTRICTED_ZONES_ARGUMENT_KEY : ALLOWED_ZONES_ARGUMENT_KEY; GeofencingArgumentEntry zonesEntry = (GeofencingArgumentEntry) arguments.get(zoneKey); + if (zonesEntry == null) { + return List.of(); + } + + var results = new ArrayList(); for (var zoneEntry : zonesEntry.getZoneStates().entrySet()) { GeofencingZoneState state = zoneEntry.getValue(); String event = state.evaluate(entityCoordinates); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java index 9e27907b73..d4a42d0645 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java @@ -83,6 +83,9 @@ public class GeofencingZoneState { public String evaluate(Coordinates entityCoordinates) { boolean inside = perimeterDefinition.checkMatches(entityCoordinates); + // TODO: maybe handle this.inside == null as ENTERED or OUTSIDE. + // Since if this.inside == null then we don't have a state for this zone yet + // and logically say that we are OUTSIDE instead of LEFT. if (this.inside == null || this.inside != inside) { this.inside = inside; return inside ? GpsGeofencingEvents.ENTERED : GpsGeofencingEvents.LEFT; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 98850f0797..b5482b3e96 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -19,6 +19,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import java.util.Map; import java.util.Set; import static org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType.RELATION_QUERY; @@ -32,13 +33,18 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC public static final String ALLOWED_ZONES_ARGUMENT_KEY = "allowedZones"; public static final String RESTRICTED_ZONES_ARGUMENT_KEY = "restrictedZones"; - private static final Set requiredKeys = Set.of( + private static final Set allowedKeys = Set.of( ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ALLOWED_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY ); + private static final Set requiredKeys = Set.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY + ); + @Override public CalculatedFieldType getType() { return CalculatedFieldType.GEOFENCING; @@ -47,44 +53,72 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC // TODO: update validate method in PE version. @Override public void validate() { - if (arguments == null || arguments.size() != 4) { - throw new IllegalArgumentException("Geofencing calculated field configuration must contain exactly 4 arguments: " + requiredKeys); + if (arguments == null) { + throw new IllegalArgumentException("Geofencing calculated field arguments are empty!"); } + + // Check key count + if (arguments.size() < 3 || arguments.size() > 4) { + throw new IllegalArgumentException("Geofencing calculated field must contain 3 or 4 arguments: " + allowedKeys); + } + + // Check for unsupported argument keys + for (String key : arguments.keySet()) { + if (!allowedKeys.contains(key)) { + throw new IllegalArgumentException("Unsupported argument key: '" + key + "'. Allowed keys: " + allowedKeys); + } + } + + // Check required fields: latitude and longitude for (String requiredKey : requiredKeys) { - Argument argument = arguments.get(requiredKey); - if (argument == null) { + if (!arguments.containsKey(requiredKey)) { throw new IllegalArgumentException("Missing required argument: " + requiredKey); } + } + + // Ensure at least one of the zone types is configured + boolean hasAllowedZones = arguments.containsKey(ALLOWED_ZONES_ARGUMENT_KEY); + boolean hasRestrictedZones = arguments.containsKey(RESTRICTED_ZONES_ARGUMENT_KEY); + + if (!hasAllowedZones && !hasRestrictedZones) { + throw new IllegalArgumentException("Geofencing calculated field must contain at least one of the following arguments: 'allowedZones' or 'restrictedZones'"); + } + + for (Map.Entry entry : arguments.entrySet()) { + String argumentKey = entry.getKey(); + Argument argument = entry.getValue(); + if (argument == null) { + throw new IllegalArgumentException("Missing required argument: " + argumentKey); + } ReferencedEntityKey refEntityKey = argument.getRefEntityKey(); if (refEntityKey == null || refEntityKey.getType() == null) { - throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + requiredKey); + throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + argumentKey); } - switch (requiredKey) { - case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { + + switch (argumentKey) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { if (!ArgumentType.TS_LATEST.equals(refEntityKey.getType())) { - throw new IllegalArgumentException("Argument: '" + requiredKey + "' must be set to " + ArgumentType.TS_LATEST + " type!"); + throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type TS_LATEST."); } - var dynamicSource = argument.getRefDynamicSource(); - if (dynamicSource != null) { - String test = "test"; - throw new IllegalArgumentException("Dynamic source configuration is forbidden for '" + requiredKey + "' argument. " + - "Only '" + ALLOWED_ZONES_ARGUMENT_KEY + "' and '" + RESTRICTED_ZONES_ARGUMENT_KEY + "' " + - "may use dynamic source configuration."); + if (argument.getRefDynamicSource() != null) { + throw new IllegalArgumentException("Dynamic source is not allowed for argument: '" + argumentKey + "'."); } } - case ALLOWED_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY -> { + case ALLOWED_ZONES_ARGUMENT_KEY, + RESTRICTED_ZONES_ARGUMENT_KEY -> { if (!ArgumentType.ATTRIBUTE.equals(refEntityKey.getType())) { - throw new IllegalArgumentException("Argument: '" + requiredKey + "' must be set to " + ArgumentType.ATTRIBUTE + " type!"); + throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type ATTRIBUTE."); } var dynamicSource = argument.getRefDynamicSource(); if (dynamicSource == null) { continue; } if (!RELATION_QUERY.equals(dynamicSource)) { - throw new IllegalArgumentException("Only relation query dynamic source is supported for argument: " + requiredKey); + throw new IllegalArgumentException("Only relation query dynamic source is supported for argument: '" + argumentKey + "'."); } if (argument.getRefDynamicSourceConfiguration() == null) { - throw new IllegalArgumentException("Missing dynamic source configuration for: " + requiredKey); + throw new IllegalArgumentException("Missing dynamic source configuration for argument: '" + argumentKey + "'."); } argument.getRefDynamicSourceConfiguration().validate(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java index 85f1ee21bd..b6e085395e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -34,7 +34,7 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami private boolean fetchLastLevelOnly; private EntitySearchDirection direction; private String relationType; - private List profiles; + private List entityTypes; @Override public CFArgumentDynamicSourceType getType() { @@ -56,7 +56,7 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami @Override public boolean isSimpleRelation() { - return maxLevel == 1 && (profiles == null || profiles.isEmpty()); + return maxLevel == 1 && (entityTypes == null || entityTypes.isEmpty()); } @Override @@ -66,7 +66,7 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami } var entityRelationsQuery = new EntityRelationsQuery(); entityRelationsQuery.setParameters(new RelationsSearchParameters(rootEntityId, direction, maxLevel, fetchLastLevelOnly)); - entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, profiles))); + entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, entityTypes))); return entityRelationsQuery; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index 12d9764af2..4fe663ff5d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -70,8 +70,8 @@ public class CalculatedFieldDataValidator extends DataValidator if (maxArgumentsPerCF <= 0) { return; } - if (CalculatedFieldType.GEOFENCING.equals(calculatedField.getType()) && maxArgumentsPerCF < 4) { - throw new DataValidationException("Geofencing calculated field requires 4 arguments, but the system limit is " + + if (CalculatedFieldType.GEOFENCING.equals(calculatedField.getType()) && maxArgumentsPerCF < 3) { + throw new DataValidationException("Geofencing calculated field requires at least 3 arguments, but the system limit is " + maxArgumentsPerCF + ". Contact your administrator to increase the limit." ); }