From dd53892df2d1a573b90d36d31847fa4e64ba0272 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 27 Aug 2025 20:12:01 +0300 Subject: [PATCH] Geofencing CF refactoring to new configuration init commit --- ...alculatedFieldManagerMessageProcessor.java | 8 +- ...faultCalculatedFieldProcessingService.java | 4 +- .../cf/ctx/state/CalculatedFieldCtx.java | 18 +- .../state/GeofencingCalculatedFieldState.java | 99 ++--- .../cf/CalculatedFieldIntegrationTest.java | 100 ++--- .../GeofencingCalculatedFieldStateTest.java | 83 ++-- .../service/sync/vc/VersionControlTest.java | 8 +- ...entsBasedCalculatedFieldConfiguration.java | 35 +- .../BaseCalculatedFieldConfiguration.java | 19 +- .../CalculatedFieldConfiguration.java | 18 +- ...sionBasedCalculatedFieldConfiguration.java | 24 ++ ...eofencingCalculatedFieldConfiguration.java | 124 ++---- ...lationQueryDynamicSourceConfiguration.java | 5 +- ...SupportedCalculatedFieldConfiguration.java | 25 ++ .../SimpleCalculatedFieldConfiguration.java | 2 +- .../geofencing/EntityCoordinates.java | 63 +++ .../geofencing/ZoneGroupConfiguration.java | 81 ++++ ...ncingCalculatedFieldConfigurationTest.java | 368 ++++-------------- ...onQueryDynamicSourceConfigurationTest.java | 17 - .../geofencing/EntityCoordinatesTest.java | 80 ++++ .../ZoneGroupConfigurationTest.java | 21 + .../dao/cf/BaseCalculatedFieldService.java | 4 +- .../service/CalculatedFieldServiceTest.java | 103 ++--- .../server/msa/cf/CalculatedFieldTest.java | 83 ++-- 24 files changed, 659 insertions(+), 733 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ExpressionBasedCalculatedFieldConfiguration.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduleSupportedCalculatedFieldConfiguration.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java create mode 100644 common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java create mode 100644 common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index a396feef2d..6aa47a48b8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ScheduleSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.DeviceId; @@ -482,17 +482,17 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) { CalculatedField cf = cfCtx.getCalculatedField(); - if (!(cf.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration cfConfig)) { + if (!(cf.getConfiguration() instanceof ScheduleSupportedCalculatedFieldConfiguration scheduledCfConfig)) { return; } - if (!cfConfig.isScheduledUpdateEnabled()) { + if (!scheduledCfConfig.isScheduledUpdateEnabled()) { return; } if (cfDynamicArgumentsRefreshTasks.containsKey(cf.getId())) { log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId()); return; } - long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(cfConfig.getScheduledUpdateIntervalSec()); + long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(scheduledCfConfig.getScheduledUpdateIntervalSec()); var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId()); ScheduledFuture scheduledFuture = systemContext diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 995180e507..a6e64bde81 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -94,8 +94,8 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.SCOPE; -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.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.utils.CalculatedFieldUtils.toProto; @TbRuleEngineComponent diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 3108b463af..139218e8f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -26,8 +26,10 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ExpressionBasedCalculatedFieldConfiguration; 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.ScheduleSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; @@ -87,8 +89,9 @@ public class CalculatedFieldCtx { this.mainEntityArguments = new HashMap<>(); this.linkedEntityArguments = new HashMap<>(); this.argNames = new ArrayList<>(); - if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration configuration) { - this.arguments.putAll(configuration.getArguments()); + this.output = calculatedField.getConfiguration().getOutput(); + if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { + this.arguments.putAll(argBasedConfig.getArguments()); for (Map.Entry entry : arguments.entrySet()) { var refId = entry.getValue().getRefEntityId(); var refKey = entry.getValue().getRefEntityKey(); @@ -102,9 +105,10 @@ public class CalculatedFieldCtx { } } this.argNames.addAll(arguments.keySet()); - this.output = configuration.getOutput(); - this.expression = configuration.getExpression(); - this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) configuration).isUseLatestTs(); + if (argBasedConfig instanceof ExpressionBasedCalculatedFieldConfiguration expressionBasedConfig) { + this.expression = expressionBasedConfig.getExpression(); + this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) argBasedConfig).isUseLatestTs(); + } } this.tbelInvokeService = tbelInvokeService; this.relationService = relationService; @@ -319,8 +323,8 @@ public class CalculatedFieldCtx { } public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) { - if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration otherConfig) { + if (calculatedField.getConfiguration() instanceof ScheduleSupportedCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof ScheduleSupportedCalculatedFieldConfiguration otherConfig) { boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled(); boolean refreshIntervalChanged = thisConfig.getScheduledUpdateIntervalSec() != otherConfig.getScheduledUpdateIntervalSec(); return refreshTriggerChanged || refreshIntervalChanged; 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 80a0534cdf..fc5e3106ea 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 @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.service.cf.CalculatedFieldResult; @@ -35,10 +36,11 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; 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.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.GeofencingPresenceStatus.INSIDE; import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; @@ -115,45 +117,54 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { Coordinates entityCoordinates = new Coordinates(latitude, longitude); var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); - if (configuration.isCreateRelationsWithMatchedZones()) { - return calculateWithRelations(entityId, ctx, entityCoordinates, configuration); - } - return calculateWithoutRelations(ctx, entityCoordinates, configuration); + // TODO: refactor + return calculate(entityId, ctx, entityCoordinates, configuration); } - private ListenableFuture calculateWithRelations( + private ListenableFuture calculate( EntityId entityId, CalculatedFieldCtx ctx, Coordinates entityCoordinates, GeofencingCalculatedFieldConfiguration configuration) { - var zoneGroupReportStrategies = configuration.getZoneGroupReportStrategies(); + Map zoneGroups = configuration + .getZoneGroups() + .stream() + .collect(Collectors.toMap(ZoneGroupConfiguration::getName, Function.identity())); + ObjectNode resultNode = JacksonUtil.newObjectNode(); List> relationFutures = new ArrayList<>(); getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { - GeofencingReportStrategy geofencingReportStrategy = zoneGroupReportStrategies.get(argumentKey); - List zoneResults = new ArrayList<>(); - - argumentEntry.getZoneStates().forEach((zoneId, zoneState) -> { - GeofencingEvalResult eval = zoneState.evaluate(entityCoordinates); - zoneResults.add(eval); - - GeofencingTransitionEvent transitionEvent = eval.transition(); - if (transitionEvent == null) { - return; - } - EntityRelation relation = toRelation(zoneId, entityId, configuration); - ListenableFuture f = switch (transitionEvent) { - case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation); - case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation); - }; - relationFutures.add(f); - }); - updateResultNode(argumentKey, zoneResults, geofencingReportStrategy, resultNode); + ZoneGroupConfiguration zoneGroupConfiguration = zoneGroups.get(argumentKey); + if (zoneGroupConfiguration.isCreateRelationsWithMatchedZones()) { + List zoneResults = new ArrayList<>(); + + argumentEntry.getZoneStates().forEach((zoneId, zoneState) -> { + GeofencingEvalResult eval = zoneState.evaluate(entityCoordinates); + zoneResults.add(eval); + + GeofencingTransitionEvent transitionEvent = eval.transition(); + if (transitionEvent == null) { + return; + } + EntityRelation relation = toRelation(zoneId, entityId, zoneGroupConfiguration); + ListenableFuture f = switch (transitionEvent) { + case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation); + case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation); + }; + relationFutures.add(f); + }); + updateResultNode(argumentKey, zoneResults, zoneGroupConfiguration.getReportStrategy(), resultNode); + } else { + List zoneResults = argumentEntry.getZoneStates().values().stream() + .map(zs -> zs.evaluate(entityCoordinates)) + .toList(); + updateResultNode(argumentKey, zoneResults, zoneGroupConfiguration.getReportStrategy(), resultNode); + } }); - var result = calculationResult(ctx, resultNode); + var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode); if (relationFutures.isEmpty()) { return Futures.immediateFuture(result); } @@ -162,30 +173,6 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { MoreExecutors.directExecutor()); } - private ListenableFuture calculateWithoutRelations( - CalculatedFieldCtx ctx, - Coordinates entityCoordinates, - GeofencingCalculatedFieldConfiguration configuration) { - - var zoneGroupReportStrategies = configuration.getZoneGroupReportStrategies(); - ObjectNode resultNode = JacksonUtil.newObjectNode(); - - getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { - var geofencingReportStrategy = zoneGroupReportStrategies.get(argumentKey); - - List zoneResults = argumentEntry.getZoneStates().values().stream() - .map(zs -> zs.evaluate(entityCoordinates)) - .toList(); - - updateResultNode(argumentKey, zoneResults, geofencingReportStrategy, resultNode); - }); - return Futures.immediateFuture(calculationResult(ctx, resultNode)); - } - - private CalculatedFieldResult calculationResult(CalculatedFieldCtx ctx, ObjectNode resultNode) { - return new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode); - } - private Map getGeofencingArguments() { return arguments.entrySet() .stream() @@ -226,10 +213,10 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { return new GeofencingEvalResult(transition, nowInside ? INSIDE : OUTSIDE); } - private EntityRelation toRelation(EntityId zoneId, EntityId entityId, GeofencingCalculatedFieldConfiguration configuration) { - return switch (configuration.getZoneRelationDirection()) { - case TO -> new EntityRelation(zoneId, entityId, configuration.getZoneRelationType()); - case FROM -> new EntityRelation(entityId, zoneId, configuration.getZoneRelationType()); + private EntityRelation toRelation(EntityId zoneId, EntityId entityId, ZoneGroupConfiguration configuration) { + return switch (configuration.getDirection()) { + case TO -> new EntityRelation(zoneId, entityId, configuration.getRelationType()); + case FROM -> new EntityRelation(entityId, zoneId, configuration.getRelationType()); }; } diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index b7f32f2506..f9e5dec789 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; @@ -39,6 +40,8 @@ import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; 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.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -48,6 +51,7 @@ import org.thingsboard.server.controller.CalculatedFieldControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -55,6 +59,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; +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; @DaoSqlTest public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest { @@ -131,7 +137,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("86.0"); }); - Argument savedArgument = savedCalculatedField.getConfiguration().getArguments().get("T"); + Argument savedArgument = ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).getArguments().get("T"); savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); @@ -143,7 +149,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("104.0"); }); - savedCalculatedField.getConfiguration().setExpression("1.8 * T + 32"); + ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).setExpression("1.8 * T + 32"); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); await().alias("update CF expression -> perform calculation with new expression").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -664,41 +670,27 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); // Coordinates: TS_LATEST on the device - Argument lat = new Argument(); - lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null)); - Argument lon = new Argument(); - lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null)); + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); // Zone groups: ATTRIBUTE on specific assets (one zone per group) - Argument allowedZones = new Argument(); - var allowedZonesRefDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - allowedZonesRefDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); - allowedZonesRefDynamicSourceConfiguration.setRelationType("AllowedZone"); - allowedZonesRefDynamicSourceConfiguration.setMaxLevel(1); - allowedZonesRefDynamicSourceConfiguration.setFetchLastLevelOnly(true); - allowedZones.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); - allowedZones.setRefDynamicSourceConfiguration(allowedZonesRefDynamicSourceConfiguration); - - Argument restrictedZones = new Argument(); - var restrictedZonesRefDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - restrictedZonesRefDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); - restrictedZonesRefDynamicSourceConfiguration.setRelationType("RestrictedZone"); - restrictedZonesRefDynamicSourceConfiguration.setMaxLevel(1); - restrictedZonesRefDynamicSourceConfiguration.setFetchLastLevelOnly(true); - restrictedZones.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); - restrictedZones.setRefDynamicSourceConfiguration(restrictedZonesRefDynamicSourceConfiguration); - - cfg.setArguments(Map.of( - GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat, - GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon, - "allowedZones", allowedZones, - "restrictedZones", restrictedZones - )); - - cfg.setZoneGroupReportStrategies(Map.of( - "allowedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, - "restrictedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS - )); + ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("allowedZones", "zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone"); + allowedZoneDynamicSourceConfiguration.setMaxLevel(1); + allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); + + ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("restrictedZones", "zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone"); + restrictedZoneDynamicSourceConfiguration.setMaxLevel(1); + restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration); + + cfg.setZoneGroups(List.of(allowedZonesGroup, restrictedZonesGroup)); // Output to server attributes Output out = new Output(); @@ -789,31 +781,16 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cf.setDebugSettings(DebugSettings.off()); GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY)); - // Coordinates (TS_LATEST) - Argument lat = new Argument(); - lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null)); - Argument lon = new Argument(); - lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null)); - - // Dynamic group 'allowedZones' resolved by relations (FROM device -> assets of type AllowedZone) - Argument allowedZones = new Argument(); - var dyn = new RelationQueryDynamicSourceConfiguration(); - dyn.setDirection(EntitySearchDirection.FROM); - dyn.setRelationType("AllowedZone"); - dyn.setMaxLevel(1); - dyn.setFetchLastLevelOnly(true); - allowedZones.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); - allowedZones.setRefDynamicSourceConfiguration(dyn); - - cfg.setArguments(Map.of( - GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat, - GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon, - "allowedZones", allowedZones - )); - - // Report all for the group - cfg.setZoneGroupReportStrategies(Map.of("allowedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); + var allowedZonesGroup = new ZoneGroupConfiguration("allowedZones", "zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone"); + allowedZoneDynamicSourceConfiguration.setMaxLevel(1); + allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); + cfg.setZoneGroups(List.of(allowedZonesGroup)); // Server attributes output Output out = new Output(); @@ -827,7 +804,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cf.setConfiguration(cfg); CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class); assertThat(savedCalculatedField).isNotNull(); - assertThat(savedCalculatedField.getConfiguration().isScheduledUpdateEnabled()).isTrue(); + CalculatedFieldConfiguration configuration = savedCalculatedField.getConfiguration(); + assertThat(configuration).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + var geofencingConfiguration = (GeofencingCalculatedFieldConfiguration) configuration; + assertThat(geofencingConfiguration.isScheduledUpdateEnabled()).isTrue(); // --- Assert initial evaluation (ENTERED) --- await().alias("initial geofencing evaluation") diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index 73320b3aca..21198133df 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -25,15 +25,14 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.cf.configuration.Argument; -import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; 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.RelationQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; @@ -59,9 +58,9 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -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.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; +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; @ExtendWith(MockitoExtension.class) public class GeofencingCalculatedFieldStateTest { @@ -273,12 +272,12 @@ public class GeofencingCalculatedFieldStateTest { EntityRelation relationFromFirstIteration = saveValues.get(0); assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID); - assertThat(relationFromFirstIteration.getType()).isEqualTo(configuration.getZoneRelationType()); + assertThat(relationFromFirstIteration.getType()).isEqualTo("CurrentZone"); EntityRelation relationFromSecondIteration = saveValues.get(1); assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID); - assertThat(relationFromSecondIteration.getType()).isEqualTo(configuration.getZoneRelationType()); + assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); @@ -343,12 +342,12 @@ public class GeofencingCalculatedFieldStateTest { EntityRelation relationFromFirstIteration = saveValues.get(0); assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID); - assertThat(relationFromFirstIteration.getType()).isEqualTo(configuration.getZoneRelationType()); + assertThat(relationFromFirstIteration.getType()).isEqualTo("CurrentZone"); EntityRelation relationFromSecondIteration = saveValues.get(1); assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID); - assertThat(relationFromSecondIteration.getType()).isEqualTo(configuration.getZoneRelationType()); + assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); @@ -415,12 +414,12 @@ public class GeofencingCalculatedFieldStateTest { EntityRelation relationFromFirstIteration = saveValues.get(0); assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID); - assertThat(relationFromFirstIteration.getType()).isEqualTo(configuration.getZoneRelationType()); + assertThat(relationFromFirstIteration.getType()).isEqualTo("CurrentZone"); EntityRelation relationFromSecondIteration = saveValues.get(1); assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID); - assertThat(relationFromSecondIteration.getType()).isEqualTo(configuration.getZoneRelationType()); + assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); @@ -448,43 +447,31 @@ public class GeofencingCalculatedFieldStateTest { private CalculatedFieldConfiguration getCalculatedFieldConfig(GeofencingReportStrategy reportStrategy) { var config = new GeofencingCalculatedFieldConfiguration(); - Argument argument1 = new Argument(); - argument1.setRefEntityId(DEVICE_ID); - var refEntityKey1 = new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null); - argument1.setRefEntityKey(refEntityKey1); - - Argument argument2 = new Argument(); - argument2.setRefEntityId(DEVICE_ID); - var refEntityKey2 = new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null); - argument2.setRefEntityKey(refEntityKey2); - - Argument argument3 = new Argument(); - var refEntityKey3 = new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, null); - var refDynamicSourceConfiguration3 = new RelationQueryDynamicSourceConfiguration(); - refDynamicSourceConfiguration3.setDirection(EntitySearchDirection.TO); - refDynamicSourceConfiguration3.setRelationType("AllowedZone"); - refDynamicSourceConfiguration3.setMaxLevel(1); - refDynamicSourceConfiguration3.setFetchLastLevelOnly(true); - argument3.setRefEntityKey(refEntityKey3); - argument3.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration3); - - Argument argument4 = new Argument(); - var refEntityKey4 = new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, null); - var refDynamicSourceConfiguration4 = new RelationQueryDynamicSourceConfiguration(); - refDynamicSourceConfiguration4.setDirection(EntitySearchDirection.TO); - refDynamicSourceConfiguration4.setRelationType("RestrictedZone"); - refDynamicSourceConfiguration4.setMaxLevel(1); - refDynamicSourceConfiguration4.setFetchLastLevelOnly(true); - argument4.setRefEntityKey(refEntityKey4); - argument4.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration4); - - config.setArguments(Map.of("latitude", argument1, "longitude", argument2, "allowedZones", argument3, "restrictedZones", argument4)); - - config.setZoneGroupReportStrategies(Map.of("allowedZones", reportStrategy, "restrictedZones", reportStrategy)); - - config.setCreateRelationsWithMatchedZones(true); - config.setZoneRelationType("CurrentZone"); - config.setZoneRelationDirection(EntitySearchDirection.TO); + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + entityCoordinates.setRefEntityId(DEVICE_ID); + config.setEntityCoordinates(entityCoordinates); + + ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("allowedZones", "zone", reportStrategy, true); + var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO); + allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone"); + allowedZoneDynamicSourceConfiguration.setMaxLevel(1); + allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); + allowedZonesGroup.setRelationType("CurrentZone"); + allowedZonesGroup.setDirection(EntitySearchDirection.TO); + + ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("restrictedZones", "zone", reportStrategy, true); + var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO); + restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone"); + restrictedZoneDynamicSourceConfiguration.setMaxLevel(1); + restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration); + restrictedZonesGroup.setRelationType("CurrentZone"); + restrictedZonesGroup.setDirection(EntitySearchDirection.TO); + + config.setZoneGroups(List.of(allowedZonesGroup, restrictedZonesGroup)); Output output = new Output(); output.setType(OutputType.TIME_SERIES); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 461ca5a2ec..2db78871d4 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -638,7 +638,9 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedField.getName()).isEqualTo(deviceCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(deviceCalculatedField.getType()); assertThat(importedField.getId()).isNotEqualTo(deviceCalculatedField.getId()); - assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedAsset.getId()); + assertThat(importedField.getConfiguration()).isInstanceOf(SimpleCalculatedFieldConfiguration.class); + SimpleCalculatedFieldConfiguration simpleCfg = (SimpleCalculatedFieldConfiguration) importedField.getConfiguration(); + assertThat(simpleCfg.getArguments().get("T").getRefEntityId()).isEqualTo(importedAsset.getId()); }); List importedAssetCalculatedFields = findCalculatedFieldsByEntityId(importedAsset.getId()); @@ -647,7 +649,9 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedField.getName()).isEqualTo(assetCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(assetCalculatedField.getType()); assertThat(importedField.getId()).isNotEqualTo(assetCalculatedField.getId()); - assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedDevice.getId()); + assertThat(importedField.getConfiguration()).isInstanceOf(SimpleCalculatedFieldConfiguration.class); + SimpleCalculatedFieldConfiguration simpleCfg = (SimpleCalculatedFieldConfiguration) importedField.getConfiguration(); + assertThat(simpleCfg.getArguments().get("T").getRefEntityId()).isEqualTo(importedDevice.getId()); }); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java index e5d5a4d3cb..225278e776 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java @@ -1,29 +1,24 @@ +/** + * 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; - import java.util.Map; public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { Map getArguments(); - String getExpression(); - - void setExpression(String expression); - - Output getOutput(); - - @JsonIgnore - default boolean isScheduledUpdateEnabled() { - return false; - } - - default void setScheduledUpdateIntervalSec(int scheduledUpdateIntervalSec) { - } - - default int getScheduledUpdateIntervalSec() { - return 0; - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java index 9df433fc0a..c270874605 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java @@ -27,7 +27,7 @@ import java.util.Objects; import java.util.stream.Collectors; @Data -public abstract class BaseCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { +public abstract class BaseCalculatedFieldConfiguration implements ExpressionBasedCalculatedFieldConfiguration { protected Map arguments; protected String expression; @@ -41,23 +41,6 @@ public abstract class BaseCalculatedFieldConfiguration implements ArgumentsBased .collect(Collectors.toList()); } - @Override - public List buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId) { - return getReferencedEntities().stream() - .filter(referencedEntity -> !referencedEntity.equals(cfEntityId)) - .map(referencedEntityId -> buildCalculatedFieldLink(tenantId, referencedEntityId, calculatedFieldId)) - .collect(Collectors.toList()); - } - - @Override - public CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId) { - CalculatedFieldLink link = new CalculatedFieldLink(); - link.setTenantId(tenantId); - link.setEntityId(referencedEntityId); - link.setCalculatedFieldId(calculatedFieldId); - return link; - } - @Override public void validate() { if (arguments.containsKey("ctx")) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index 6d2c3c2e0f..2fe554b801 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.TenantId; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -44,6 +45,8 @@ public interface CalculatedFieldConfiguration { @JsonIgnore CalculatedFieldType getType(); + Output getOutput(); + void validate(); @JsonIgnore @@ -51,8 +54,19 @@ public interface CalculatedFieldConfiguration { return Collections.emptyList(); } - CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId); + default CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId) { + CalculatedFieldLink link = new CalculatedFieldLink(); + link.setTenantId(tenantId); + link.setEntityId(referencedEntityId); + link.setCalculatedFieldId(calculatedFieldId); + return link; + } - List buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId); + default List buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId) { + return getReferencedEntities().stream() + .filter(referencedEntity -> !referencedEntity.equals(cfEntityId)) + .map(referencedEntityId -> buildCalculatedFieldLink(tenantId, referencedEntityId, calculatedFieldId)) + .collect(Collectors.toList()); + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ExpressionBasedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ExpressionBasedCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..17e6a0ba80 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ExpressionBasedCalculatedFieldConfiguration.java @@ -0,0 +1,24 @@ +/** + * 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 interface ExpressionBasedCalculatedFieldConfiguration extends ArgumentsBasedCalculatedFieldConfiguration { + + String getExpression(); + + void setExpression(String expression); + +} 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 d9968e9926..1c51db2274 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 @@ -15,34 +15,26 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; @Data -@EqualsAndHashCode(callSuper = true) -public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { - - public static final String ENTITY_ID_LATITUDE_ARGUMENT_KEY = "latitude"; - public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude"; - - private static final Set coordinateKeys = Set.of( - ENTITY_ID_LATITUDE_ARGUMENT_KEY, - ENTITY_ID_LONGITUDE_ARGUMENT_KEY - ); +public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduleSupportedCalculatedFieldConfiguration { + private EntityCoordinates entityCoordinates; + private List zoneGroups; private int scheduledUpdateIntervalSec; - private boolean createRelationsWithMatchedZones; - private String zoneRelationType; - private EntitySearchDirection zoneRelationDirection; - private Map zoneGroupReportStrategies; + private Output output; @Override public CalculatedFieldType getType() { @@ -50,91 +42,39 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC } @Override - public boolean isScheduledUpdateEnabled() { - return scheduledUpdateIntervalSec > 0 && arguments.values().stream().anyMatch(Argument::hasDynamicSource); + @JsonIgnore + public Map getArguments() { + Map args = new HashMap<>(entityCoordinates.toArguments()); + zoneGroups.forEach(zg -> args.put(zg.getName(), zg.toArgument())); + return args; } @Override - public void validate() { - if (arguments == null) { - throw new IllegalArgumentException("Geofencing calculated field arguments must be specified!"); - } - validateCoordinateArguments(); - Map zoneGroupsArguments = getZoneGroupArguments(); - if (zoneGroupsArguments.isEmpty()) { - throw new IllegalArgumentException("Geofencing calculated field must contain at least one geofencing zone group defined!"); - } - validateZoneGroupAruguments(zoneGroupsArguments); - validateZoneGroupConfigurations(zoneGroupsArguments); - validateZoneRelationsConfiguration(); + public Output getOutput() { + return output; } - private void validateZoneRelationsConfiguration() { - if (!createRelationsWithMatchedZones) { - return; - } - if (StringUtils.isBlank(zoneRelationType)) { - throw new IllegalArgumentException("Zone relation type must be specified to create relations with matched zones!"); - } - if (zoneRelationDirection == null) { - throw new IllegalArgumentException("Zone relation direction must be specified to create relations with matched zones!"); - } + @Override + public boolean isScheduledUpdateEnabled() { + return scheduledUpdateIntervalSec > 0 && zoneGroups.stream().anyMatch(ZoneGroupConfiguration::hasDynamicSource); } - private void validateZoneGroupConfigurations(Map zoneGroupsArguments) { - if (zoneGroupReportStrategies == null || zoneGroupReportStrategies.isEmpty()) { - throw new IllegalArgumentException("Zone groups reporting strategies should be specified!"); + @Override + public void validate() { + if (entityCoordinates == null) { + throw new IllegalArgumentException("Geofencing calculated field entity coordinates must be specified!"); } - zoneGroupsArguments.forEach((zoneGroupName, zoneGroupArgument) -> { - GeofencingReportStrategy geofencingReportStrategy = zoneGroupReportStrategies.get(zoneGroupName); - if (geofencingReportStrategy == null) { - throw new IllegalArgumentException("Zone group report strategy is not configured for '" + zoneGroupName + "' argument!"); - } - }); - } - - private void validateCoordinateArguments() { - for (String coordinateKey : coordinateKeys) { - Argument argument = arguments.get(coordinateKey); - if (argument == null) { - throw new IllegalArgumentException("Missing required coordinates argument: " + coordinateKey + "!"); - } - ReferencedEntityKey refEntityKey = validateAndGetRefEntityKey(argument, coordinateKey); - if (!ArgumentType.TS_LATEST.equals(refEntityKey.getType())) { - throw new IllegalArgumentException("Argument '" + coordinateKey + "' must be of type TS_LATEST!"); - } - if (argument.hasDynamicSource()) { - throw new IllegalArgumentException("Dynamic source is not allowed for '" + coordinateKey + "' argument!"); - } + if (zoneGroups == null || zoneGroups.isEmpty()) { + throw new IllegalArgumentException("Geofencing calculated field must contain at least one geofencing zone group defined!"); } - } - - private void validateZoneGroupAruguments(Map zoneGroupsArguments) { - zoneGroupsArguments.forEach((argumentKey, argument) -> { - ReferencedEntityKey refEntityKey = validateAndGetRefEntityKey(argument, argumentKey); - if (!ArgumentType.ATTRIBUTE.equals(refEntityKey.getType())) { - throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type ATTRIBUTE!"); + entityCoordinates.validate(); + Set seen = new HashSet<>(); + for (var zg : zoneGroups) { + if (!seen.add(zg.getName())) { + throw new IllegalArgumentException("Geofencing calculated field zone group name must be unique!"); } - if (argument.hasDynamicSource()) { - argument.getRefDynamicSourceConfiguration().validate(); - } - }); - } - - private Map getZoneGroupArguments() { - return arguments.entrySet() - .stream() - .filter(entry -> entry.getValue() != null) - .filter(entry -> !coordinateKeys.contains(entry.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - 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); + zg.validate(); } - return refEntityKey; } } 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 c34199a9d7..ac8bfb691d 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 @@ -37,7 +37,6 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami private boolean fetchLastLevelOnly; private EntitySearchDirection direction; private String relationType; - private List entityTypes; @Override public CFArgumentDynamicSourceType getType() { @@ -62,7 +61,7 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami @JsonIgnore public boolean isSimpleRelation() { - return maxLevel == 1 && CollectionsUtil.isEmpty(entityTypes); + return maxLevel == 1; } public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) { @@ -71,7 +70,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, entityTypes))); + entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, Collections.emptyList()))); return entityRelationsQuery; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduleSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduleSupportedCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..f8faf8856f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduleSupportedCalculatedFieldConfiguration.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +public interface ScheduleSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { + + boolean isScheduledUpdateEnabled(); + + int getScheduledUpdateIntervalSec(); + + void setScheduledUpdateIntervalSec(int interval); +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java index 46395a3361..471bac3653 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java @@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data @EqualsAndHashCode(callSuper = true) -public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { +public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements ExpressionBasedCalculatedFieldConfiguration { private boolean useLatestTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java new file mode 100644 index 0000000000..07876f16b9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java @@ -0,0 +1,63 @@ +/** + * 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.geofencing; + + +import lombok.Data; +import org.springframework.lang.Nullable; +import org.thingsboard.server.common.data.StringUtils; +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.ReferencedEntityKey; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.Map; + +@Data +public class EntityCoordinates { + + public static final String ENTITY_ID_LATITUDE_ARGUMENT_KEY = "latitude"; + public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude"; + + private final String latitudeKeyName; + private final String longitudeKeyName; + + @Nullable + private EntityId refEntityId; + + public void validate() { + if (StringUtils.isBlank(latitudeKeyName)) { + throw new IllegalArgumentException("Entity coordinates latitude key name must be specified!"); + } + if (StringUtils.isBlank(longitudeKeyName)) { + throw new IllegalArgumentException("Entity coordinates longitude key name must be specified!"); + } + } + + public Map toArguments() { + return Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument(latitudeKeyName), + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument(longitudeKeyName) + ); + } + + private Argument toArgument(String keyName) { + var argument = new Argument(); + argument.setRefEntityId(refEntityId); + argument.setRefEntityKey(new ReferencedEntityKey(keyName, ArgumentType.TS_LATEST, null)); + return argument; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java new file mode 100644 index 0000000000..891940bf08 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java @@ -0,0 +1,81 @@ +/** + * 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.geofencing; + +import lombok.Data; +import org.springframework.lang.Nullable; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.StringUtils; +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.CfArgumentDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; + +@Data +public class ZoneGroupConfiguration { + + @Nullable + private EntityId refEntityId; + private CfArgumentDynamicSourceConfiguration refDynamicSourceConfiguration; + + private final String name; + private final String perimeterKeyName; + + private final GeofencingReportStrategy reportStrategy; + private final boolean createRelationsWithMatchedZones; + + private String relationType; + private EntitySearchDirection direction; + + public void validate() { + if (StringUtils.isBlank(name)) { + throw new IllegalArgumentException("Zone group name must be specified!"); + } + if (EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY.equals(name) || EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY.equals(name)) { + throw new IllegalArgumentException("Name '" + name + "' is reserved and cannot be used for zone group!"); + } + if (StringUtils.isBlank(perimeterKeyName)) { + throw new IllegalArgumentException("Perimeter key name must be specified for '" + name + "' zone group!"); + } + if (reportStrategy == null) { + throw new IllegalArgumentException("Report strategy must be specified for '" + name + "' zone group!"); + } + if (!createRelationsWithMatchedZones) { + return; + } + if (StringUtils.isBlank(relationType)) { + throw new IllegalArgumentException("Relation type must be specified for '" + name + "' zone group!"); + } + if (direction == null) { + throw new IllegalArgumentException("Relation direction must be specified for '" + name + "' zone group!"); + } + } + + public boolean hasDynamicSource() { + return refDynamicSourceConfiguration != null; + } + + public Argument toArgument() { + var argument = new Argument(); + argument.setRefEntityId(refEntityId); + argument.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); + argument.setRefEntityKey(new ReferencedEntityKey(perimeterKeyName, ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + return argument; + } +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java index ebdd915c45..1b12c37820 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java @@ -17,26 +17,24 @@ package org.thingsboard.server.common.data.cf.configuration; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; -import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -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.mockito.Mockito.when; +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; @ExtendWith(MockitoExtension.class) public class GeofencingCalculatedFieldConfigurationTest { @@ -48,134 +46,20 @@ public class GeofencingCalculatedFieldConfigurationTest { } @Test - void validateShouldThrowWhenArgumentsNull() { + void validateShouldThrowWhenEntityCoordinatesNull() { var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setArguments(null); + cfg.setEntityCoordinates(null); assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Geofencing calculated field arguments must be specified!"); - } - - @ParameterizedTest - @MethodSource("missingCoordinateArgs") - void validateShouldThrowWhenCoordinateArgIsMissing(String missingKey, String presentKey) { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var arguments = new HashMap(); - arguments.put(missingKey, null); - arguments.put(presentKey, toArgument(presentKey, ArgumentType.TS_LATEST)); - cfg.setArguments(arguments); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Missing required coordinates argument: " + missingKey + "!"); - } - - private static Stream missingCoordinateArgs() { - return Stream.of( - Arguments.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY), - Arguments.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ENTITY_ID_LATITUDE_ARGUMENT_KEY) - ); - } - - @ParameterizedTest - @MethodSource("nullRefKeyCoordinateArgs") - void validateShouldThrowWhenReferenceKeyIsNullOrTypeNull( - String brokenKey, Argument brokenArg, String okKey) { - - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setArguments(Map.of( - brokenKey, brokenArg, - okKey, toArgument(okKey, ArgumentType.TS_LATEST) - )); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Missing or invalid reference entity key for argument: " + brokenKey); - } - - private static Stream nullRefKeyCoordinateArgs() { - return Stream.of( - // null ref key on latitude - Arguments.of( - ENTITY_ID_LATITUDE_ARGUMENT_KEY, - toArgument(null), - ENTITY_ID_LONGITUDE_ARGUMENT_KEY - ), - // null ref key on longitude - Arguments.of( - ENTITY_ID_LONGITUDE_ARGUMENT_KEY, - toArgument(null), - ENTITY_ID_LATITUDE_ARGUMENT_KEY - ), - // null type on latitude - Arguments.of( - ENTITY_ID_LATITUDE_ARGUMENT_KEY, - toArgument(new ReferencedEntityKey("latitude", null, null)), - ENTITY_ID_LONGITUDE_ARGUMENT_KEY - ), - // null type on longitude - Arguments.of( - ENTITY_ID_LONGITUDE_ARGUMENT_KEY, - toArgument(new ReferencedEntityKey("longitude", null, null)), - ENTITY_ID_LATITUDE_ARGUMENT_KEY - ) - ); - } - - @ParameterizedTest - @MethodSource("wrongTypeCoordinateArgs") - void validateShouldThrowWhenCoordinateHasWrongType(String wrongKey, String okKey) { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setArguments(Map.of( - wrongKey, toArgument(wrongKey, ArgumentType.ATTRIBUTE), - okKey, toArgument(okKey, ArgumentType.TS_LATEST) - )); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Argument '" + wrongKey + "' must be of type TS_LATEST!"); - } - - private static Stream wrongTypeCoordinateArgs() { - return Stream.of( - Arguments.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY), - Arguments.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ENTITY_ID_LATITUDE_ARGUMENT_KEY) - ); - } - - @ParameterizedTest - @MethodSource("dynamicCoordinateArgs") - void validateShouldThrowWhenCoordinateHasDynamicSource(String dynamicKey, String okKey) { - var cfg = new GeofencingCalculatedFieldConfiguration(); - - var dynamicArg = toArgument(dynamicKey, ArgumentType.TS_LATEST); - dynamicArg.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration()); - - cfg.setArguments(Map.of( - dynamicKey, dynamicArg, - okKey, toArgument(okKey, ArgumentType.TS_LATEST) - )); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dynamic source is not allowed for '" + dynamicKey + "' argument!"); - } - - private static Stream dynamicCoordinateArgs() { - return Stream.of( - Arguments.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY), - Arguments.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ENTITY_ID_LATITUDE_ARGUMENT_KEY) - ); + .hasMessage("Geofencing calculated field entity coordinates must be specified!"); } @Test - void validateShouldThrowWhenGeofencingArgumentsMissing() { + void validateShouldThrowWhenZoneGroupsNull() { var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setArguments(Map.of( - ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST), - ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST) - )); + cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY)); + cfg.setZoneGroups(null); assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) @@ -183,149 +67,59 @@ public class GeofencingCalculatedFieldConfigurationTest { } @Test - void validateShouldThrowWhenZoneGroupArgumentIsNull() { + void validateShouldCallValidateOnEntityCoordinatesAndZoneGroups() { var cfg = new GeofencingCalculatedFieldConfiguration(); - var arguments = new HashMap(); - arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); - arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); - arguments.put("someZones", null); + EntityCoordinates entityCoordinatesMock = mock(EntityCoordinates.class); + cfg.setEntityCoordinates(entityCoordinatesMock); + var zoneGroupConfiguration = mock(ZoneGroupConfiguration.class); + cfg.setZoneGroups(List.of(zoneGroupConfiguration)); - cfg.setArguments(arguments); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Geofencing calculated field must contain at least one geofencing zone group defined!"); - } - - @Test - void validateShouldThrowWhenZoneGroupArgumentHasInvalidArgumentType() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var arguments = new HashMap(); - arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); - arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); - arguments.put("allowedZones", toArgument("allowedZone", ArgumentType.TS_LATEST)); - - cfg.setArguments(arguments); + cfg.validate(); - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Argument 'allowedZones' must be of type ATTRIBUTE!"); + verify(entityCoordinatesMock).validate(); + verify(zoneGroupConfiguration).validate(); } @Test - void validateShouldCallDynamicSourceConfigValidationWhenZoneGroupArgumentHasDynamicSourceConfiguration() { + void validateShouldCallValidateOnEntityCoordinatesAndZoneGroupsWithoutAnyExceptions() { var cfg = new GeofencingCalculatedFieldConfiguration(); - var arguments = new HashMap(); - arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); - arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); - Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); - var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); - allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); - arguments.put("allowedZones", allowedZonesArg); - - Map zoneGroupReportStrategies = - Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); + EntityCoordinates entityCoordinatesMock = mock(EntityCoordinates.class); + cfg.setEntityCoordinates(entityCoordinatesMock); + var zoneGroupConfigurationA = mock(ZoneGroupConfiguration.class); + var zoneGroupConfigurationB = mock(ZoneGroupConfiguration.class); - cfg.setArguments(arguments); - cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); - - cfg.validate(); + when(zoneGroupConfigurationA.getName()).thenReturn("zoneGroupA"); + when(zoneGroupConfigurationB.getName()).thenReturn("zoneGroupB"); - verify(refDynamicSourceConfigurationMock).validate(); - } + cfg.setZoneGroups(List.of(zoneGroupConfigurationA, zoneGroupConfigurationB)); - @Test - void validateShouldThrowWhenZoneGroupConfigurationIsMissing() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var arguments = new HashMap(); - arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); - arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); - Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); - var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); - allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); - arguments.put("allowedZones", allowedZonesArg); - - cfg.setArguments(arguments); - cfg.setZoneGroupReportStrategies(null); - cfg.setCreateRelationsWithMatchedZones(false); + assertThatCode(cfg::validate).doesNotThrowAnyException(); - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Zone groups reporting strategies should be specified!"); + verify(entityCoordinatesMock).validate(); + verify(zoneGroupConfigurationA).validate(); + verify(zoneGroupConfigurationB).validate(); } @Test - void validateShouldThrowWhenZoneGroupArgumentReportStrategyIsMissing() { + void validateShouldThrowWhenZoneGroupNamesDuplicated() { var cfg = new GeofencingCalculatedFieldConfiguration(); - var arguments = new HashMap(); - arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); - arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); - Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); - var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); - allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); - arguments.put("allowedZones", allowedZonesArg); + EntityCoordinates entityCoordinatesMock = mock(EntityCoordinates.class); + cfg.setEntityCoordinates(entityCoordinatesMock); + var zoneGroupConfigurationA = mock(ZoneGroupConfiguration.class); + var zoneGroupConfigurationB = mock(ZoneGroupConfiguration.class); - Map zoneGroupReportStrategies = - Map.of("someOtherZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); + when(zoneGroupConfigurationA.getName()).thenReturn("zoneGroupDuplicated"); + when(zoneGroupConfigurationB.getName()).thenReturn("zoneGroupDuplicated"); - cfg.setArguments(arguments); - cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); - cfg.setCreateRelationsWithMatchedZones(false); + cfg.setZoneGroups(List.of(zoneGroupConfigurationA, zoneGroupConfigurationB)); assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Zone group report strategy is not configured for 'allowedZones' argument!"); - } + .hasMessage("Geofencing calculated field zone group name must be unique!"); - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = " ") - void validateShouldThrowWhenHasBlankOrNullZoneRelationType(String zoneRelationType) { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var arguments = new HashMap(); - arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); - arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); - Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); - var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); - allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); - arguments.put("allowedZones", allowedZonesArg); - - Map zoneGroupReportStrategies = - Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); - - cfg.setArguments(arguments); - cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); - cfg.setCreateRelationsWithMatchedZones(true); - cfg.setZoneRelationType(zoneRelationType); - cfg.setZoneRelationDirection(EntitySearchDirection.TO); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Zone relation type must be specified to create relations with matched zones!"); - } - - @Test - void validateShouldThrowWhenNoZoneRelationDirectionSpecified() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var arguments = new HashMap(); - arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); - arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); - Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); - var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); - allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); - arguments.put("allowedZones", allowedZonesArg); - - Map zoneGroupReportStrategies = - Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); - - cfg.setArguments(arguments); - cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); - cfg.setCreateRelationsWithMatchedZones(true); - cfg.setZoneRelationType("SomeRelationType"); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Zone relation direction must be specified to create relations with matched zones!"); + verify(entityCoordinatesMock).validate(); + verify(zoneGroupConfigurationA).validate(); + verify(zoneGroupConfigurationB, never()).validate(); } @Test @@ -336,17 +130,11 @@ public class GeofencingCalculatedFieldConfigurationTest { } @Test - void scheduledUpdateDisabledWhenIntervalIsGreaterThanZeroButArgumentsAreEmpty() { + void scheduledUpdateDisabledWhenIntervalIsGreaterThanZeroButNoZonesWithDynamicArguments() { var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setArguments(Map.of()); - cfg.setScheduledUpdateIntervalSec(60); - assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); - } - - @Test - void scheduledUpdateDisabledWhenIntervalIsGreaterThanZeroButDynamicArgumentsAreMissing() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setArguments(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST))); + var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); + when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(false); + cfg.setZoneGroups(List.of(zoneGroupConfigurationMock)); cfg.setScheduledUpdateIntervalSec(60); assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); } @@ -354,45 +142,41 @@ public class GeofencingCalculatedFieldConfigurationTest { @Test void scheduledUpdateEnabledWhenIntervalIsGreaterThanZeroAndDynamicArgumentsPresent() { var cfg = new GeofencingCalculatedFieldConfiguration(); - Argument someDynamicArgument = toArgument("someDynamicArgument", ArgumentType.ATTRIBUTE); - someDynamicArgument.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration()); - cfg.setArguments(Map.of("someDynamicArugument", someDynamicArgument)); + var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); + when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(true); + cfg.setZoneGroups(List.of(zoneGroupConfigurationMock)); cfg.setScheduledUpdateIntervalSec(60); assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); } @Test - void validateShouldPassOnMinimalValidConfig() { + void testGetArgumentsOverride() { var cfg = new GeofencingCalculatedFieldConfiguration(); - var args = new HashMap(); - args.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); - args.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); - Argument allowed = toArgument("allowed", ArgumentType.ATTRIBUTE); - var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); - allowed.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); - args.put("allowedZones", allowed); - cfg.setArguments(args); - - Map zoneGroupReportStrategies = - Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); - cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); - - cfg.setCreateRelationsWithMatchedZones(true); - cfg.setZoneRelationType("Contains"); - cfg.setZoneRelationDirection(EntitySearchDirection.FROM); + cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY)); + cfg.setZoneGroups(List.of(new ZoneGroupConfiguration("allowedZones", "perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false))); - assertThatCode(cfg::validate).doesNotThrowAnyException(); - } + Map arguments = cfg.getArguments(); - private Argument toArgument(String key, ArgumentType type) { - var referencedEntityKey = new ReferencedEntityKey(key, type, null); - return toArgument(referencedEntityKey); - } + assertThat(arguments).isNotNull().hasSize(3); + assertThat(arguments).containsKeys(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, "allowedZones"); + + Argument latitudeArgument = arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY); + assertThat(latitudeArgument).isNotNull(); + assertThat(latitudeArgument.getRefDynamicSourceConfiguration()).isNull(); + assertThat(latitudeArgument.getRefEntityId()).isNull(); + assertThat(latitudeArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ArgumentType.TS_LATEST, null)); + + Argument longitudeArgument = arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY); + assertThat(longitudeArgument).isNotNull(); + assertThat(longitudeArgument.getRefDynamicSourceConfiguration()).isNull(); + assertThat(longitudeArgument.getRefEntityId()).isNull(); + assertThat(longitudeArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ArgumentType.TS_LATEST, null)); - private static Argument toArgument(ReferencedEntityKey referencedEntityKey) { - Argument argument = new Argument(); - argument.setRefEntityKey(referencedEntityKey); - return argument; + Argument allowedZonesArgument = arguments.get("allowedZones"); + assertThat(allowedZonesArgument).isNotNull(); + assertThat(allowedZonesArgument.getRefDynamicSourceConfiguration()).isNull(); + assertThat(allowedZonesArgument.getRefEntityId()).isNull(); + assertThat(allowedZonesArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("perimeter", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); } } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java index 648a7c985e..1fb55673d1 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java @@ -109,8 +109,6 @@ public class RelationQueryDynamicSourceConfigurationTest { void isSimpleRelationTrueWhenLevelIsOneAndEntityTypesEmptyOrNull(List entityTypes) { var cfg = new RelationQueryDynamicSourceConfiguration(); cfg.setMaxLevel(1); - cfg.setEntityTypes(entityTypes); - assertThat(cfg.isSimpleRelation()).isTrue(); } @@ -118,17 +116,6 @@ public class RelationQueryDynamicSourceConfigurationTest { void isSimpleRelationFalseWhenMaxLevelNotOne() { var cfg = new RelationQueryDynamicSourceConfiguration(); cfg.setMaxLevel(2); - cfg.setEntityTypes(null); - - assertThat(cfg.isSimpleRelation()).isFalse(); - } - - @Test - void isSimpleRelationFalseWhenEntityTypesProvided() { - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(1); - cfg.setEntityTypes(List.of(EntityType.DEVICE)); - assertThat(cfg.isSimpleRelation()).isFalse(); } @@ -140,7 +127,6 @@ public class RelationQueryDynamicSourceConfigurationTest { cfg.setFetchLastLevelOnly(false); cfg.setDirection(EntitySearchDirection.FROM); cfg.setRelationType(EntityRelation.CONTAINS_TYPE); - cfg.setEntityTypes(entityTypes); assertThatThrownBy(() -> cfg.toEntityRelationsQuery(rootEntityId)) .isInstanceOf(IllegalArgumentException.class) @@ -154,7 +140,6 @@ public class RelationQueryDynamicSourceConfigurationTest { cfg.setFetchLastLevelOnly(true); cfg.setDirection(EntitySearchDirection.TO); cfg.setRelationType(EntityRelation.MANAGES_TYPE); - cfg.setEntityTypes(List.of(EntityType.DEVICE, EntityType.ASSET)); var query = cfg.toEntityRelationsQuery(rootEntityId); @@ -170,7 +155,6 @@ public class RelationQueryDynamicSourceConfigurationTest { assertThat(query.getFilters().get(0)).isInstanceOf(RelationEntityTypeFilter.class); RelationEntityTypeFilter filter = query.getFilters().get(0); assertThat(filter.getRelationType()).isEqualTo(EntityRelation.MANAGES_TYPE); - assertThat(filter.getEntityTypes()).containsExactly(EntityType.DEVICE, EntityType.ASSET); } @Test @@ -206,7 +190,6 @@ public class RelationQueryDynamicSourceConfigurationTest { cfg.setFetchLastLevelOnly(false); cfg.setDirection(EntitySearchDirection.FROM); cfg.setRelationType(EntityRelation.CONTAINS_TYPE); - cfg.setEntityTypes(List.of(EntityType.DEVICE)); assertThatCode(cfg::validate).doesNotThrowAnyException(); } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java new file mode 100644 index 0000000000..c5d627c3e6 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java @@ -0,0 +1,80 @@ +/** + * 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.geofencing; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +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.ReferencedEntityKey; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +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; + +public class EntityCoordinatesTest { + + @ParameterizedTest + @ValueSource(strings = " ") + @NullAndEmptySource + void validateShouldThrowWhenLatitudeCoordinateIsNullEmptyOrBlank(String latitudeKey) { + var entityCoordinates = new EntityCoordinates(latitudeKey, "longitude"); + assertThatThrownBy(entityCoordinates::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Entity coordinates latitude key name must be specified!"); + } + + @ParameterizedTest + @ValueSource(strings = " ") + @NullAndEmptySource + void validateShouldThrowWhenLongitudeCoordinateIsNullEmptyOrBlank(String longitudeKey) { + var entityCoordinates = new EntityCoordinates("latitude", longitudeKey); + assertThatThrownBy(entityCoordinates::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Entity coordinates longitude key name must be specified!"); + } + + @Test + void validateShouldPassOnMinimalValidConfig() { + var entityCoordinates = new EntityCoordinates("latitude", "longitude"); + assertThatCode(entityCoordinates::validate).doesNotThrowAnyException(); + } + + @Test + void validateToArgumentsMethodCallWithoutRefEntityId() { + var entityCoordinates = new EntityCoordinates("xPos", "yPos"); + + var arguments = entityCoordinates.toArguments(); + assertThat(arguments).isNotNull().hasSize(2); + + Argument latitudeArgument = arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY); + assertThat(latitudeArgument).isNotNull(); + assertThat(latitudeArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("xPos", ArgumentType.TS_LATEST, null)); + assertThat(latitudeArgument.getRefEntityId()).isNull(); + assertThat(latitudeArgument.getRefDynamicSourceConfiguration()).isNull(); + + Argument longitudeArgument = arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY); + assertThat(longitudeArgument).isNotNull(); + assertThat(longitudeArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("yPos", ArgumentType.TS_LATEST, null)); + assertThat(longitudeArgument.getRefEntityId()).isNull(); + assertThat(longitudeArgument.getRefDynamicSourceConfiguration()).isNull(); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java new file mode 100644 index 0000000000..cfe5a4c4e2 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java @@ -0,0 +1,21 @@ +/** + * 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.geofencing; + +// TODO: add tests +public class ZoneGroupConfigurationTest { + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 465e127238..b0924b83f7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -21,8 +21,8 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ScheduleSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; @@ -94,7 +94,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } private void updatedSchedulingConfiguration(CalculatedField calculatedField) { - if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration configuration) { + if (calculatedField.getConfiguration() instanceof ScheduleSupportedCalculatedFieldConfiguration configuration) { if (!configuration.isScheduledUpdateEnabled()) { return; } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 81dbe7b799..80e82bb25f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -29,12 +29,13 @@ import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; 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.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -43,10 +44,12 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldServiceTest extends AbstractServiceTest { @@ -105,27 +108,14 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); // Coordinates: TS_LATEST, no dynamic source - Argument lat = new Argument(); - lat.setRefEntityId(device.getId()); - lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null)); - - Argument lon = new Argument(); - lon.setRefEntityId(device.getId()); - lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null)); + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + entityCoordinates.setRefEntityId(device.getId()); + cfg.setEntityCoordinates(entityCoordinates); // Zone-group argument (ATTRIBUTE) — no DYNAMIC configuration, so no scheduling even if the scheduled interval is set - Argument allowed = new Argument(); - lat.setRefEntityId(device.getId()); - allowed.setRefEntityKey(new ReferencedEntityKey("allowed", ArgumentType.ATTRIBUTE, null)); - - cfg.setArguments(Map.of( - GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat, - GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon, - "allowed", allowed - )); - - // Matching zone-group configuration - cfg.setZoneGroupReportStrategies(Map.of("allowed", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + zoneGroupConfiguration.setRefEntityId(device.getId()); + cfg.setZoneGroups(List.of(zoneGroupConfiguration)); // Set a scheduled interval to some value cfg.setScheduledUpdateIntervalSec(600); @@ -141,9 +131,14 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { CalculatedField saved = calculatedFieldService.save(cf); + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + + var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); + // Assert: the interval is saved, but scheduling is not enabled - int savedInterval = saved.getConfiguration().getScheduledUpdateIntervalSec(); - boolean scheduledUpdateEnabled = saved.getConfiguration().isScheduledUpdateEnabled(); + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateIntervalSec(); + boolean scheduledUpdateEnabled = geofencingCalculatedFieldConfiguration.isScheduledUpdateEnabled(); assertThat(savedInterval).isEqualTo(600); assertThat(scheduledUpdateEnabled).isFalse(); @@ -160,31 +155,18 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); // Coordinates: TS_LATEST, no dynamic source - Argument lat = new Argument(); - lat.setRefEntityId(device.getId()); - lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null)); - - Argument lon = new Argument(); - lon.setRefEntityId(device.getId()); - lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null)); + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + entityCoordinates.setRefEntityId(device.getId()); + cfg.setEntityCoordinates(entityCoordinates); // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled - Argument allowed = new Argument(); - allowed.setRefEntityKey(new ReferencedEntityKey("allowed", ArgumentType.ATTRIBUTE, null)); + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); var dynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); dynamicSourceConfiguration.setMaxLevel(1); dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); - allowed.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); - - cfg.setArguments(Map.of( - GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat, - GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon, - "allowed", allowed - )); - - // Matching zone-group configuration - cfg.setZoneGroupReportStrategies(Map.of("allowed", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(List.of(zoneGroupConfiguration)); // Enable scheduling with an interval below tenant min cfg.setScheduledUpdateIntervalSec(600); @@ -200,8 +182,13 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { CalculatedField saved = calculatedFieldService.save(cf); + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + + var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); + // Assert: the interval is clamped up to tenant profile min - int savedInterval = saved.getConfiguration().getScheduledUpdateIntervalSec(); + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateIntervalSec(); int min = tbTenantProfileCache.get(tenantId) .getDefaultProfileConfiguration() @@ -220,31 +207,18 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); // Coordinates: TS_LATEST, no dynamic source - Argument lat = new Argument(); - lat.setRefEntityId(device.getId()); - lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null)); - - Argument lon = new Argument(); - lon.setRefEntityId(device.getId()); - lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null)); + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + entityCoordinates.setRefEntityId(device.getId()); + cfg.setEntityCoordinates(entityCoordinates); // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled - Argument allowed = new Argument(); - allowed.setRefEntityKey(new ReferencedEntityKey("allowed", ArgumentType.ATTRIBUTE, null)); + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); var dynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); dynamicSourceConfiguration.setMaxLevel(1); dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); - allowed.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); - - cfg.setArguments(Map.of( - GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat, - GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon, - "allowed", allowed - )); - - // Matching zone-group configuration - cfg.setZoneGroupReportStrategies(Map.of("allowed", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(List.of(zoneGroupConfiguration)); // Get tenant profile min. int min = tbTenantProfileCache.get(tenantId) @@ -267,8 +241,13 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { CalculatedField saved = calculatedFieldService.save(cf); + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + + var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); + // Assert: the interval is clamped up to tenant profile min (or stays >= original if already >= min) - int savedInterval = saved.getConfiguration().getScheduledUpdateIntervalSec(); + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateIntervalSec(); assertThat(savedInterval).isEqualTo(valueFromConfig); calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index 150546d700..bf6ac7cf20 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -37,6 +37,8 @@ import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; 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.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; @@ -52,6 +54,7 @@ import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.ui.utils.EntityPrototypes; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -71,17 +74,17 @@ public class CalculatedFieldTest extends AbstractContainerTest { private final String deviceToken = "zmzURIVRsq3lvnTP2XBE"; private final String exampleScript = "var avgTemperature = temperature.mean(); // Get average temperature\n" + - " var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin\n" + - "\n" + - " // Estimate air pressure based on altitude\n" + - " var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588);\n" + - "\n" + - " // Air density formula\n" + - " var airDensity = pressure / (287.05 * temperatureK);\n" + - "\n" + - " return {\n" + - " \"airDensity\": toFixed(airDensity, 2)\n" + - " };"; + " var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin\n" + + "\n" + + " // Estimate air pressure based on altitude\n" + + " var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588);\n" + + "\n" + + " // Air density formula\n" + + " var airDensity = pressure / (287.05 * temperatureK);\n" + + "\n" + + " return {\n" + + " \"airDensity\": toFixed(airDensity, 2)\n" + + " };"; private TenantId tenantId; private UserId tenantAdminId; @@ -152,8 +155,9 @@ public class CalculatedFieldTest extends AbstractContainerTest { testRestClient.getAndSetUserToken(tenantAdminId); CalculatedField savedCalculatedField = createSimpleCalculatedField(); + assertThat(savedCalculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration).isTrue(); - Argument savedArgument = savedCalculatedField.getConfiguration().getArguments().get("T"); + Argument savedArgument = ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).getArguments().get("T"); savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, SERVER_SCOPE)); testRestClient.postCalculatedField(savedCalculatedField); @@ -200,9 +204,10 @@ public class CalculatedFieldTest extends AbstractContainerTest { testRestClient.getAndSetUserToken(tenantAdminId); CalculatedField savedCalculatedField = createSimpleCalculatedField(); + assertThat(savedCalculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration).isTrue(); savedCalculatedField.setName("F to C"); - savedCalculatedField.getConfiguration().setExpression("(T - 32) / 1.8"); + ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).setExpression("(T - 32) / 1.8"); testRestClient.postCalculatedField(savedCalculatedField); await().alias("update CF expression -> perform calculation with new expression").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -357,40 +362,28 @@ public class CalculatedFieldTest extends AbstractContainerTest { GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); - Argument lat = new Argument(); - lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null)); - Argument lon = new Argument(); - lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null)); + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); // Dynamic groups via relations - Argument allowedArg = new Argument(); - var dynAllowed = new RelationQueryDynamicSourceConfiguration(); - dynAllowed.setDirection(EntitySearchDirection.FROM); - dynAllowed.setRelationType("AllowedZone"); - dynAllowed.setMaxLevel(1); - dynAllowed.setFetchLastLevelOnly(true); - allowedArg.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, SERVER_SCOPE)); - allowedArg.setRefDynamicSourceConfiguration(dynAllowed); - - Argument restrictedArg = new Argument(); - var dynRestricted = new RelationQueryDynamicSourceConfiguration(); - dynRestricted.setDirection(EntitySearchDirection.FROM); - dynRestricted.setRelationType("RestrictedZone"); - dynRestricted.setMaxLevel(1); - dynRestricted.setFetchLastLevelOnly(true); - restrictedArg.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, SERVER_SCOPE)); - restrictedArg.setRefDynamicSourceConfiguration(dynRestricted); - - cfg.setArguments(Map.of( - GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat, - GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon, - "allowedZones", allowedArg, - "restrictedZones", restrictedArg - )); - cfg.setZoneGroupReportStrategies(Map.of( - "allowedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, - "restrictedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS - )); + ZoneGroupConfiguration allowedZoneGroupConfiguration = new ZoneGroupConfiguration("allowedZones", "zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var allowedDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + allowedDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + allowedDynamicSourceConfiguration.setMaxLevel(1); + allowedDynamicSourceConfiguration.setFetchLastLevelOnly(true); + allowedDynamicSourceConfiguration.setRelationType("AllowedZone"); + allowedZoneGroupConfiguration.setRefDynamicSourceConfiguration(allowedDynamicSourceConfiguration); + + ZoneGroupConfiguration restrictedZoneGroupConfiguration = new ZoneGroupConfiguration("restrictedZones", "zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var restrictedDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + restrictedDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + restrictedDynamicSourceConfiguration.setMaxLevel(1); + restrictedDynamicSourceConfiguration.setFetchLastLevelOnly(true); + restrictedDynamicSourceConfiguration.setRelationType("RestrictedZone"); + restrictedZoneGroupConfiguration.setRefDynamicSourceConfiguration(restrictedDynamicSourceConfiguration); + + cfg.setZoneGroups(List.of(allowedZoneGroupConfiguration, restrictedZoneGroupConfiguration)); + Output out = new Output(); out.setType(OutputType.ATTRIBUTES); out.setScope(SERVER_SCOPE);