Browse Source

Added reporting strategies instead of single zone group event

pull/13920/head
dshvaika 1 year ago
parent
commit
cc3ecfc027
  1. 156
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java
  2. 23
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java
  3. 41
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java
  4. 5
      application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java
  5. 68
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  6. 9
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
  7. 25
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java
  8. 7
      application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java
  9. 25
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java
  10. 15
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java
  11. 11
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java
  12. 24
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java
  13. 20
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java
  14. 110
      common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java
  15. 13
      dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java

156
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<CalculatedFieldResult> calculateWithRelations(
EntityId entityId,
CalculatedFieldCtx ctx,
Coordinates entityCoordinates,
GeofencingCalculatedFieldConfiguration configuration) {
var geofencingZoneGroupConfigurations = configuration.getZoneGroupConfigurations();
Map<EntityId, GeofencingEvent> zoneEventMap = new HashMap<>();
var zoneGroupReportStrategies = configuration.getZoneGroupReportStrategies();
ObjectNode resultNode = JacksonUtil.newObjectNode();
List<ListenableFuture<Boolean>> relationFutures = new ArrayList<>();
getGeofencingArguments().forEach((argumentKey, argumentEntry) -> {
var zoneGroupConfig = geofencingZoneGroupConfigurations.get(argumentKey);
Set<GeofencingEvent> groupEvents = new HashSet<>();
GeofencingReportStrategy geofencingReportStrategy = zoneGroupReportStrategies.get(argumentKey);
List<GeofencingEvalResult> 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<Boolean> 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<ListenableFuture<Boolean>> 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<CalculatedFieldResult> 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<GeofencingEvent> groupEvents = argumentEntry.getZoneStates().values().stream()
var geofencingReportStrategy = zoneGroupReportStrategies.get(argumentKey);
List<GeofencingEvalResult> 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<String, GeofencingArgumentEntry> 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<GeofencingEvent> aggregateZoneGroupEvent(Set<GeofencingEvent> 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<GeofencingEvalResult> 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<GeofencingEvalResult> 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) {

23
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) {}

41
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) {

5
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();
}

68
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<GeofencingEvent> 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<String, String> 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<String, String> 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<GeofencingEvent> 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<String, String> 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<String, String> 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<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZoneEvent", "ENTERED");
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
.containsEntry("allowedZonesStatus", "INSIDE");
});
}

9
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<GeofencingEvent> 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");

25
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));
}
}

7
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();
}
}

25
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<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations;
private Map<String, GeofencingReportStrategy> 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<String, Argument> zoneGroupsArguments) {
if (zoneGroupConfigurations == null || zoneGroupConfigurations.isEmpty()) {
if (zoneGroupReportStrategies == null || zoneGroupReportStrategies.isEmpty()) {
throw new IllegalArgumentException("Zone groups configuration should be specified!");
}
Set<String> 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!");
}
});
}

15
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 { }

11
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java → 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<GeofencingEvent> reportEvents;
INSIDE, OUTSIDE;
}

24
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
}

20
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
}

110
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<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
Map<String, GeofencingReportStrategy> 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<String, Argument>();
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<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration, "restrictedZones", restrictedZoneConfiguration);
Map<String, GeofencingReportStrategy> 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<String, Argument>();
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<String, GeofencingZoneGroupConfiguration> 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<String, Argument>();
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<String, GeofencingZoneGroupConfiguration> 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<String, Argument>();
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<String, GeofencingZoneGroupConfiguration> 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<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
Map<String, GeofencingReportStrategy> 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<String, GeofencingZoneGroupConfiguration> zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration);
Map<String, GeofencingReportStrategy> 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<String, GeofencingReportStrategy> zoneGroupReportStrategies =
Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS);
cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies);
cfg.setCreateRelationsWithMatchedZones(true);
cfg.setZoneRelationType("Contains");

13
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)

Loading…
Cancel
Save