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 9e598db69a..b6b8d89928 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 @@ -25,7 +25,8 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; 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.GeofencingEvent; +import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; +import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.service.cf.CalculatedFieldResult; @@ -34,15 +35,14 @@ import org.thingsboard.server.utils.CalculatedFieldUtils; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.Set; 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.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; @Data @AllArgsConstructor @@ -129,54 +129,60 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { return calculateWithoutRelations(ctx, entityCoordinates, configuration); } + @Override + public boolean isReady() { + return arguments.keySet().containsAll(requiredArguments) && + arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); + } + + @Override + public void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize) { + if (!sizeExceedsLimit && maxStateSize > 0 && CalculatedFieldUtils.toProto(ctxId, this).getSerializedSize() > maxStateSize) { + arguments.clear(); + sizeExceedsLimit = true; + } + } + + private ListenableFuture calculateWithRelations( EntityId entityId, CalculatedFieldCtx ctx, Coordinates entityCoordinates, GeofencingCalculatedFieldConfiguration configuration) { - var geofencingZoneGroupConfigurations = configuration.getZoneGroupConfigurations(); - - Map zoneEventMap = new HashMap<>(); + var zoneGroupReportStrategies = configuration.getZoneGroupReportStrategies(); ObjectNode resultNode = JacksonUtil.newObjectNode(); + List> relationFutures = new ArrayList<>(); getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { - var zoneGroupConfig = geofencingZoneGroupConfigurations.get(argumentKey); - Set groupEvents = new HashSet<>(); + GeofencingReportStrategy geofencingReportStrategy = zoneGroupReportStrategies.get(argumentKey); + List zoneResults = new ArrayList<>(); argumentEntry.getZoneStates().forEach((zoneId, zoneState) -> { - GeofencingEvent event = zoneState.evaluate(entityCoordinates); - zoneEventMap.put(zoneId, event); - groupEvents.add(event); + 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); }); - - aggregateZoneGroupEvent(groupEvents) - .filter(zoneGroupConfig.getReportEvents()::contains) - .ifPresent(geofencingGroupEvent -> - resultNode.put(zoneGroupConfig.getReportTelemetryPrefix() + "Event", geofencingGroupEvent.name())); + updateResultNode(argumentKey, zoneResults, geofencingReportStrategy, resultNode); }); var result = calculationResult(ctx, resultNode); - - List> relationFutures = zoneEventMap.entrySet().stream() - .filter(entry -> entry.getValue().isTransitionEvent()) - .map(entry -> { - EntityRelation relation = toRelation(entry.getKey(), entityId, configuration); - return switch (entry.getValue()) { - case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation); - case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation); - default -> throw new IllegalStateException("Unexpected transition event: " + entry.getValue()); - }; - }) - .toList(); - if (relationFutures.isEmpty()) { return Futures.immediateFuture(result); } - - return Futures.whenAllComplete(relationFutures).call(() -> - new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode), - MoreExecutors.directExecutor()); + return Futures.whenAllComplete(relationFutures) + .call(() -> new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode), + MoreExecutors.directExecutor()); } private ListenableFuture calculateWithoutRelations( @@ -184,19 +190,17 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { Coordinates entityCoordinates, GeofencingCalculatedFieldConfiguration configuration) { - var geofencingZoneGroupConfigurations = configuration.getZoneGroupConfigurations(); + var zoneGroupReportStrategies = configuration.getZoneGroupReportStrategies(); ObjectNode resultNode = JacksonUtil.newObjectNode(); getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { - var zoneGroupConfig = geofencingZoneGroupConfigurations.get(argumentKey); - Set groupEvents = argumentEntry.getZoneStates().values().stream() + var geofencingReportStrategy = zoneGroupReportStrategies.get(argumentKey); + + List zoneResults = argumentEntry.getZoneStates().values().stream() .map(zs -> zs.evaluate(entityCoordinates)) - .collect(Collectors.toSet()); - aggregateZoneGroupEvent(groupEvents) - .filter(zoneGroupConfig.getReportEvents()::contains) - .ifPresent(e -> resultNode.put( - zoneGroupConfig.getReportTelemetryPrefix() + "Event", - e.name())); + .toList(); + + updateResultNode(argumentKey, zoneResults, geofencingReportStrategy, resultNode); }); return Futures.immediateFuture(calculationResult(ctx, resultNode)); } @@ -205,20 +209,6 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { return new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode); } - @Override - public boolean isReady() { - return arguments.keySet().containsAll(requiredArguments) && - arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); - } - - @Override - public void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize) { - if (!sizeExceedsLimit && maxStateSize > 0 && CalculatedFieldUtils.toProto(ctxId, this).getSerializedSize() > maxStateSize) { - arguments.clear(); - sizeExceedsLimit = true; - } - } - private Map getGeofencingArguments() { return arguments.entrySet() .stream() @@ -226,37 +216,37 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); } - private Optional aggregateZoneGroupEvent(Set zoneEvents) { - boolean hasEntered = false; - boolean hasLeft = false; - boolean hasInside = false; - boolean hasOutside = false; - - for (GeofencingEvent event : zoneEvents) { - if (event == null) { - continue; - } - switch (event) { - case ENTERED -> hasEntered = true; - case LEFT -> hasLeft = true; - case INSIDE -> hasInside = true; - case OUTSIDE -> hasOutside = true; + private void updateResultNode(String argumentKey, List zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) { + GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults); + final String eventKey = argumentKey + "Event"; + final String statusKey = argumentKey + "Status"; + switch (geofencingReportStrategy) { + case REPORT_TRANSITION_EVENTS_ONLY -> addTransitionEventIfExists(resultNode, aggregationResult, eventKey); + case REPORT_PRESENCE_STATUS_ONLY -> resultNode.put(statusKey, aggregationResult.status().name()); + case REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS -> { + addTransitionEventIfExists(resultNode, aggregationResult, eventKey); + resultNode.put(statusKey, aggregationResult.status().name()); } } + } - if (hasOutside && !hasInside && !hasEntered && !hasLeft) { - return Optional.of(GeofencingEvent.OUTSIDE); - } - if (hasLeft && !hasEntered && !hasInside) { - return Optional.of(GeofencingEvent.LEFT); - } - if (hasEntered && !hasLeft && !hasInside) { - return Optional.of(GeofencingEvent.ENTERED); + private void addTransitionEventIfExists(ObjectNode resultNode, GeofencingEvalResult aggregationResult, String eventKey) { + if (aggregationResult.transition() != null) { + resultNode.put(eventKey, aggregationResult.transition().name()); } - if (hasInside || hasEntered) { - return Optional.of(GeofencingEvent.INSIDE); + } + + private GeofencingEvalResult aggregateZoneGroup(List zoneResults) { + boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status())); + boolean prevInside = zoneResults.stream() + .anyMatch(r -> GeofencingTransitionEvent.LEFT.equals(r.transition()) || r.transition() == null && r.status() == INSIDE); + GeofencingTransitionEvent transition = null; + if (!prevInside && nowInside) { + transition = GeofencingTransitionEvent.ENTERED; + } else if (prevInside && !nowInside) { + transition = GeofencingTransitionEvent.LEFT; } - return Optional.empty(); + return new GeofencingEvalResult(transition, nowInside ? INSIDE : OUTSIDE); } private EntityRelation toRelation(EntityId zoneId, EntityId entityId, GeofencingCalculatedFieldConfiguration configuration) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java new file mode 100644 index 0000000000..65b862b4f5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java @@ -0,0 +1,23 @@ +/** + * 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.service.cf.ctx.state; + +import jakarta.annotation.Nullable; +import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; + +public record GeofencingEvalResult(@Nullable GeofencingTransitionEvent transition, + GeofencingPresenceStatus status) {} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java index 761245cb73..ad8de7454e 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java @@ -20,7 +20,8 @@ import lombok.EqualsAndHashCode; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.common.util.geo.PerimeterDefinition; -import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; +import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -30,6 +31,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import java.util.UUID; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; + @Data public class GeofencingZoneState { @@ -40,7 +44,7 @@ public class GeofencingZoneState { private PerimeterDefinition perimeterDefinition; @EqualsAndHashCode.Exclude - private Boolean inside; + private GeofencingPresenceStatus lastPresence; public GeofencingZoneState(EntityId zoneId, KvEntry entry) { this.zoneId = zoneId; @@ -58,7 +62,7 @@ public class GeofencingZoneState { this.version = proto.getVersion(); this.perimeterDefinition = JacksonUtil.fromString(proto.getPerimeterDefinition(), PerimeterDefinition.class); if (proto.hasInside()) { - this.inside = proto.getInside(); + this.lastPresence = proto.getInside() ? INSIDE : OUTSIDE; } } @@ -71,26 +75,35 @@ public class GeofencingZoneState { this.ts = newZoneState.getTs(); this.version = newVersion; this.perimeterDefinition = newZoneState.getPerimeterDefinition(); - this.inside = null; + this.lastPresence = null; return true; } return false; } - public GeofencingEvent evaluate(Coordinates entityCoordinates) { - boolean inside = perimeterDefinition.checkMatches(entityCoordinates); - // Initial evaluation — no prior state - if (this.inside == null) { - this.inside = inside; - return inside ? GeofencingEvent.ENTERED : GeofencingEvent.OUTSIDE; + public GeofencingEvalResult evaluate(Coordinates entityCoordinates) { + boolean nowInside = perimeterDefinition.checkMatches(entityCoordinates); + + GeofencingPresenceStatus status = nowInside ? INSIDE : OUTSIDE; + + // first evaluation + if (this.lastPresence == null) { + this.lastPresence = status; + GeofencingTransitionEvent transition = null; + if (status == GeofencingPresenceStatus.INSIDE) { + transition = GeofencingTransitionEvent.ENTERED; + } + return new GeofencingEvalResult(transition, status); } // State changed - if (this.inside != inside) { - this.inside = inside; - return inside ? GeofencingEvent.ENTERED : GeofencingEvent.LEFT; + if (this.lastPresence != status) { + this.lastPresence = status; + GeofencingTransitionEvent transition = (status == GeofencingPresenceStatus.INSIDE) ? + GeofencingTransitionEvent.ENTERED : GeofencingTransitionEvent.LEFT; + return new GeofencingEvalResult(transition, status); } // State unchanged - return inside ? GeofencingEvent.INSIDE : GeofencingEvent.OUTSIDE; + return new GeofencingEvalResult(null, status); } private EntityId toZoneId(GeofencingZoneProto proto) { diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 7658409662..e4240ef104 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -18,6 +18,7 @@ package org.thingsboard.server.utils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -138,8 +139,8 @@ public class CalculatedFieldUtils { .setTs(zoneState.getTs()) .setVersion(zoneState.getVersion()) .setPerimeterDefinition(JacksonUtil.toString(zoneState.getPerimeterDefinition())); - if (zoneState.getInside() != null) { - builder.setInside(zoneState.getInside()); + if (zoneState.getLastPresence() != null) { + builder.setInside(zoneState.getLastPresence().equals(GeofencingPresenceStatus.INSIDE)); } return builder.build(); } 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 688096dcae..c2ff2342ab 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -33,8 +33,6 @@ 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.GeofencingCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; 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; @@ -49,15 +47,14 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.controller.CalculatedFieldControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; 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; @DaoSqlTest public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest { @@ -702,15 +699,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes "restrictedZones", restrictedZones )); - // Zone group reporting config - List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); - - GeofencingZoneGroupConfiguration allowedCfg = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); - GeofencingZoneGroupConfiguration restrictedCfg = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents); - - cfg.setZoneGroupConfigurations(Map.of( - "allowedZones", allowedCfg, - "restrictedZones", restrictedCfg + cfg.setZoneGroupReportStrategies(Map.of( + "allowedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, + "restrictedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS )); // Output to server attributes @@ -728,14 +719,16 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent", "restrictedZoneEvent"); - assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(3); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZoneEvent", "ENTERED") - .containsEntry("restrictedZoneEvent", "OUTSIDE"); + assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "INSIDE") + .containsEntry("restrictedZonesStatus", "OUTSIDE"); }); - // --- Move device into Restricted zone (and outside Allowed) --- + // --- Move the device into Restricted zone (and outside Allowed) --- doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")); @@ -744,11 +737,15 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent", "restrictedZoneEvent"); - assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", + "restrictedZonesEvent", "restrictedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(4); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZoneEvent", "LEFT") - .containsEntry("restrictedZoneEvent", "ENTERED"); + assertThat(m).containsEntry("allowedZonesEvent", "LEFT") + .containsEntry("restrictedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "OUTSIDE") + .containsEntry("restrictedZonesStatus", "INSIDE"); }); } @@ -821,10 +818,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes "allowedZones", allowedZones )); - // Report all events for the group - List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); - GeofencingZoneGroupConfiguration allowedCfg = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); - cfg.setZoneGroupConfigurations(Map.of("allowedZones", allowedCfg)); + // Report all for the group + cfg.setZoneGroupReportStrategies(Map.of("allowedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); // Server attributes output Output out = new Output(); @@ -845,10 +840,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent"); - assertThat(attrs).isNotNull().isNotEmpty().hasSize(1); + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZoneEvent", "ENTERED"); + assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "INSIDE"); }); // --- Move device OUTSIDE Zone A (expect LEFT) --- @@ -859,10 +855,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent"); - assertThat(attrs).isNotNull().isNotEmpty().hasSize(1); + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZoneEvent", "LEFT"); + assertThat(m).containsEntry("allowedZonesEvent", "LEFT") + .containsEntry("allowedZonesStatus", "OUTSIDE"); }); // --- Create Allowed Zone B covering the CURRENT location --- @@ -892,10 +889,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(1, TimeUnit.SECONDS) .untilAsserted(() -> { - ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent"); - assertThat(attrs).isNotNull().isNotEmpty().hasSize(1); + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZoneEvent", "ENTERED"); + assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "INSIDE"); }); } 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 fe9444461e..c6be855c91 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 @@ -29,8 +29,6 @@ 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.GeofencingEvent; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; 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; @@ -47,7 +45,6 @@ import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.CalculatedFieldResult; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -63,6 +60,7 @@ 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; @ExtendWith(MockitoExtension.class) public class GeofencingCalculatedFieldStateTest { @@ -335,10 +333,7 @@ public class GeofencingCalculatedFieldStateTest { config.setArguments(Map.of("latitude", argument1, "longitude", argument2, "allowedZones", argument3, "restrictedZones", argument4)); - List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); - GeofencingZoneGroupConfiguration allowedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); - GeofencingZoneGroupConfiguration restrictedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents); - config.setZoneGroupConfigurations(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration)); + config.setZoneGroupReportStrategies(Map.of("allowedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, "restrictedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); config.setCreateRelationsWithMatchedZones(true); config.setZoneRelationType("CurrentZone"); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java index e31e62deef..e5f95b97c5 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -21,11 +21,14 @@ import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; -import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent.ENTERED; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent.LEFT; public class GeofencingZoneStateTest { @@ -45,18 +48,18 @@ public class GeofencingZoneStateTest { void evaluate_initialInside_thenInsideAgain() { var inside = new Coordinates(50.4730, 30.5050); // first evaluation: no prior state -> ENTERED - assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.ENTERED); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); // same position again -> INSIDE (steady state) - assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.INSIDE); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); } @Test void evaluate_initialOutside_thenOutsideAgain() { var outside = new Coordinates(50.4760, 30.5110); // first evaluation: no prior state -> OUTSIDE - assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.OUTSIDE); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); // same position again -> OUTSIDE (steady state) - assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.OUTSIDE); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); } @Test @@ -64,11 +67,11 @@ public class GeofencingZoneStateTest { var inside = new Coordinates(50.4730, 30.5050); var outside = new Coordinates(50.4760, 30.5110); // enter - assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.ENTERED); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); // leave -> LEFT - assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.LEFT); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(LEFT, OUTSIDE)); // still outside -> OUTSIDE - assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.OUTSIDE); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); } @Test @@ -76,11 +79,11 @@ public class GeofencingZoneStateTest { var outside = new Coordinates(50.4760, 30.5110); var inside = new Coordinates(50.4730, 30.5050); // start outside - assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.OUTSIDE); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); // cross boundary -> ENTERED - assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.ENTERED); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); // remain inside -> INSIDE - assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.INSIDE); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); } } diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index 3d51420f2e..9cae7f59d6 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.utils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.DeviceId; @@ -76,7 +77,7 @@ class CalculatedFieldUtilsTest { BaseAttributeKvEntry zone2PerimeterAttribute = new BaseAttributeKvEntry(zone2, System.currentTimeMillis(), 0L); GeofencingZoneState s1 = new GeofencingZoneState(z1, zone1PerimeterAttribute); - s1.setInside(true); + s1.setLastPresence(GeofencingPresenceStatus.INSIDE); GeofencingZoneState s2 = new GeofencingZoneState(z2, zone2PerimeterAttribute); zoneStates.put(z1, s1); @@ -101,8 +102,8 @@ class CalculatedFieldUtilsTest { assertThat(fromProtoArgument).isInstanceOf(GeofencingArgumentEntry.class); GeofencingArgumentEntry fromProtoGeoArgument = (GeofencingArgumentEntry) fromProtoArgument; assertThat(fromProtoGeoArgument.getZoneStates()).hasSize(2); - assertThat(fromProtoGeoArgument.getZoneStates().get(z1).getInside()).isTrue(); - assertThat(fromProtoGeoArgument.getZoneStates().get(z2).getInside()).isNull(); + assertThat(fromProtoGeoArgument.getZoneStates().get(z1).getLastPresence()).isEqualTo(GeofencingPresenceStatus.INSIDE); + assertThat(fromProtoGeoArgument.getZoneStates().get(z2).getLastPresence()).isNull(); } } 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 34b2820cd0..2edd5cc07a 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 @@ -20,9 +20,7 @@ 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.util.CollectionsUtil; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -44,7 +42,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC private boolean createRelationsWithMatchedZones; private String zoneRelationType; private EntitySearchDirection zoneRelationDirection; - private Map zoneGroupConfigurations; + private Map zoneGroupReportStrategies; @Override public CalculatedFieldType getType() { @@ -56,7 +54,6 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC return scheduledUpdateIntervalSec > 0 && arguments.values().stream().anyMatch(Argument::hasDynamicSource); } - // TODO: update validate method in PE version. @Override public void validate() { if (arguments == null) { @@ -85,25 +82,13 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC } private void validateZoneGroupConfigurations(Map zoneGroupsArguments) { - if (zoneGroupConfigurations == null || zoneGroupConfigurations.isEmpty()) { + if (zoneGroupReportStrategies == null || zoneGroupReportStrategies.isEmpty()) { throw new IllegalArgumentException("Zone groups configuration should be specified!"); } - Set usedPrefixes = new HashSet<>(); - zoneGroupsArguments.forEach((zoneGroupName, zoneGroupArgument) -> { - GeofencingZoneGroupConfiguration config = zoneGroupConfigurations.get(zoneGroupName); - if (config == null) { - throw new IllegalArgumentException("Zone group configuration is not configured for '" + zoneGroupName + "' argument!"); - } - if (CollectionsUtil.isEmpty(config.getReportEvents())) { - throw new IllegalArgumentException("Zone group configuration report events must be specified for '" + zoneGroupName + "' argument!"); - } - String prefix = config.getReportTelemetryPrefix(); - if (StringUtils.isBlank(prefix)) { - throw new IllegalArgumentException("Report telemetry prefix should be specified for '" + zoneGroupName + "' argument!"); - } - if (!usedPrefixes.add(prefix)) { - throw new IllegalArgumentException("Duplicate report telemetry prefix found: '" + prefix + "'. Must be unique!"); + GeofencingReportStrategy geofencingReportStrategy = zoneGroupReportStrategies.get(zoneGroupName); + if (geofencingReportStrategy == null) { + throw new IllegalArgumentException("Zone group report strategy is not configured for '" + zoneGroupName + "' argument!"); } }); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java index d770520daa..ca3b91baec 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java @@ -15,16 +15,5 @@ */ package org.thingsboard.server.common.data.cf.configuration; -import lombok.Getter; - -@Getter -public enum GeofencingEvent { - - ENTERED(true), LEFT(true), INSIDE(false), OUTSIDE(false); - - private final boolean transitionEvent; - - GeofencingEvent(boolean transitionEvent) { - this.transitionEvent = transitionEvent; - } -} +public sealed interface GeofencingEvent + permits GeofencingTransitionEvent, GeofencingPresenceStatus { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java similarity index 77% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java index c82151fc64..3e88744132 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java @@ -15,14 +15,11 @@ */ package org.thingsboard.server.common.data.cf.configuration; -import lombok.Data; +import lombok.Getter; -import java.util.List; +@Getter +public enum GeofencingPresenceStatus implements GeofencingEvent { -@Data -public class GeofencingZoneGroupConfiguration { - - private final String reportTelemetryPrefix; - private final List reportEvents; + INSIDE, OUTSIDE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java new file mode 100644 index 0000000000..774e725650 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.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 enum GeofencingReportStrategy { + + REPORT_TRANSITION_EVENTS_ONLY, + REPORT_PRESENCE_STATUS_ONLY, + REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java new file mode 100644 index 0000000000..edd747587e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +public enum GeofencingTransitionEvent implements GeofencingEvent { + ENTERED, LEFT +} 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 44e6365b2e..23e4dbefca 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 @@ -26,7 +26,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; @@ -224,11 +223,11 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + Map zoneGroupReportStrategies = + Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); cfg.validate(); @@ -247,7 +246,7 @@ public class GeofencingCalculatedFieldConfigurationTest { arguments.put("allowedZones", allowedZonesArg); cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(null); + cfg.setZoneGroupReportStrategies(null); cfg.setCreateRelationsWithMatchedZones(false); assertThatThrownBy(cfg::validate) @@ -256,100 +255,26 @@ public class GeofencingCalculatedFieldConfigurationTest { } @Test - void validateShouldThrowWhenReportTelemetryPrefixDuplicate() { + void validateShouldThrowWhenZoneGroupArgumentReportStrategyIsMissing() { 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); - Argument restrictedZonesArg = toArgument("restrictedZone", ArgumentType.ATTRIBUTE); var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - arguments.put("restrictedZones", restrictedZonesArg); - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values())); - GeofencingZoneGroupConfiguration restrictedZoneConfiguration = new GeofencingZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration, "restrictedZones", restrictedZoneConfiguration); + Map zoneGroupReportStrategies = + Map.of("someOtherZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); cfg.setCreateRelationsWithMatchedZones(false); assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Duplicate report telemetry prefix found: 'theSamePrefixTest'. Must be unique!"); - } - - @Test - void validateShouldThrowWhenZoneGroupArgumentConfigurationIsMissing() { - 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); - - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("someOtherZones", allowedZoneConfiguration); - - cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); - cfg.setCreateRelationsWithMatchedZones(false); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Zone group configuration is not configured for 'allowedZones' argument!"); - } - - @Test - void validateShouldThrowWhenZoneGroupConfigurationReportEventsAreNotSpecified() { - 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); - - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", null); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); - - cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); - cfg.setCreateRelationsWithMatchedZones(false); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Zone group configuration report events must be specified for 'allowedZones' argument!"); - } - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = " ") - void validateShouldThrowWhenZoneGroupConfigurationTelemetryPrefixIsBlankOrNull(String reportTelemetryPrefix) { - 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); - - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration(reportTelemetryPrefix, Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); - - cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); - cfg.setCreateRelationsWithMatchedZones(false); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Report telemetry prefix should be specified for 'allowedZones' argument!"); + .hasMessage("Zone group report strategy is not configured for 'allowedZones' argument!"); } @ParameterizedTest @@ -365,11 +290,11 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + Map zoneGroupReportStrategies = + Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); cfg.setCreateRelationsWithMatchedZones(true); cfg.setZoneRelationType(zoneRelationType); cfg.setZoneRelationDirection(EntitySearchDirection.TO); @@ -390,11 +315,11 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + Map zoneGroupReportStrategies = + Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); cfg.setCreateRelationsWithMatchedZones(true); cfg.setZoneRelationType("SomeRelationType"); @@ -448,8 +373,9 @@ public class GeofencingCalculatedFieldConfigurationTest { args.put("allowedZones", allowed); cfg.setArguments(args); - var zc = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); - cfg.setZoneGroupConfigurations(Map.of("allowedZones", zc)); + Map zoneGroupReportStrategies = + Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); + cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); cfg.setCreateRelationsWithMatchedZones(true); cfg.setZoneRelationType("Contains"); 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 3c6a30ca5c..81dbe7b799 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,8 +29,7 @@ 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.GeofencingEvent; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; +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; @@ -44,7 +43,6 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import java.util.Arrays; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; @@ -127,8 +125,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { )); // Matching zone-group configuration - var geofencingZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); - cfg.setZoneGroupConfigurations(Map.of("allowed", geofencingZoneGroupConfiguration)); + cfg.setZoneGroupReportStrategies(Map.of("allowed", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); // Set a scheduled interval to some value cfg.setScheduledUpdateIntervalSec(600); @@ -187,8 +184,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { )); // Matching zone-group configuration - var geofencingZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); - cfg.setZoneGroupConfigurations(Map.of("allowed", geofencingZoneGroupConfiguration)); + cfg.setZoneGroupReportStrategies(Map.of("allowed", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); // Enable scheduling with an interval below tenant min cfg.setScheduledUpdateIntervalSec(600); @@ -248,8 +244,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { )); // Matching zone-group configuration - var geofencingZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); - cfg.setZoneGroupConfigurations(Map.of("allowed", geofencingZoneGroupConfiguration)); + cfg.setZoneGroupReportStrategies(Map.of("allowed", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); // Get tenant profile min. int min = tbTenantProfileCache.get(tenantId)