Updated to use zone groups
This commit is contained in:
parent
ede9fd5e05
commit
c783176e71
@ -35,6 +35,8 @@ import org.thingsboard.server.common.data.StringUtils;
|
||||
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.GeofencingZoneGroupConfiguration;
|
||||
import org.thingsboard.server.common.data.cf.configuration.OutputType;
|
||||
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
|
||||
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
||||
@ -95,8 +97,6 @@ import java.util.stream.Collectors;
|
||||
import static org.thingsboard.server.common.data.DataConstants.SCOPE;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.RESTRICTED_ZONES_ARGUMENT_KEY;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ALLOWED_ZONES_ARGUMENT_KEY;
|
||||
import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto;
|
||||
|
||||
@TbRuleEngineComponent
|
||||
@ -132,16 +132,17 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
|
||||
Map<String, ListenableFuture<ArgumentEntry>> argFutures = new HashMap<>();
|
||||
|
||||
if (ctx.getCalculatedField().getType().equals(CalculatedFieldType.GEOFENCING)) {
|
||||
// Ignoring any other arguments except ENTITY_ID_LATITUDE_ARGUMENT_KEY,
|
||||
// ENTITY_ID_LONGITUDE_ARGUMENT_KEY, SAVE_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY.
|
||||
var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
|
||||
var zoneGroupConfigs = configuration.getGeofencingZoneGroupConfigurations();
|
||||
for (var entry : ctx.getArguments().entrySet()) {
|
||||
switch (entry.getKey()) {
|
||||
case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY ->
|
||||
argFutures.put(entry.getKey(), fetchKvEntry(ctx.getTenantId(), resolveEntityId(entityId, entry), entry.getValue()));
|
||||
case ALLOWED_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY -> {
|
||||
default -> {
|
||||
var zoneGroupConfiguration = zoneGroupConfigs.get(entry.getKey());
|
||||
var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry);
|
||||
argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds ->
|
||||
fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), calculatedFieldCallbackExecutor));
|
||||
fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue(), zoneGroupConfiguration), calculatedFieldCallbackExecutor));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -304,7 +305,8 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
|
||||
};
|
||||
}
|
||||
|
||||
private ListenableFuture<ArgumentEntry> fetchGeofencingKvEntry(TenantId tenantId, List<EntityId> geofencingEntities, Argument argument) {
|
||||
private ListenableFuture<ArgumentEntry> fetchGeofencingKvEntry(TenantId tenantId, List<EntityId> geofencingEntities,
|
||||
Argument argument, GeofencingZoneGroupConfiguration zoneGroupConfiguration) {
|
||||
if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) {
|
||||
throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType());
|
||||
}
|
||||
@ -326,7 +328,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
|
||||
ListenableFuture<List<Map.Entry<EntityId, AttributeKvEntry>>> allFutures = Futures.allAsList(kvFutures);
|
||||
|
||||
return Futures.transform(allFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))),
|
||||
.collect(Collectors.toMap(Entry::getKey, Entry::getValue)), zoneGroupConfiguration),
|
||||
calculatedFieldCallbackExecutor
|
||||
);
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import org.thingsboard.script.api.tbel.TbelCfArg;
|
||||
import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration;
|
||||
import org.thingsboard.server.common.data.id.EntityId;
|
||||
import org.thingsboard.server.common.data.kv.KvEntry;
|
||||
import org.thingsboard.server.common.data.kv.TsKvEntry;
|
||||
@ -61,8 +62,8 @@ public interface ArgumentEntry {
|
||||
return new TsRollingArgumentEntry(kvEntries, limit, timeWindow);
|
||||
}
|
||||
|
||||
static ArgumentEntry createGeofencingValueArgument(Map<EntityId, KvEntry> entityIdkvEntryMap) {
|
||||
return new GeofencingArgumentEntry(entityIdkvEntryMap);
|
||||
static ArgumentEntry createGeofencingValueArgument(Map<EntityId, KvEntry> entityIdkvEntryMap, GeofencingZoneGroupConfiguration zoneGroupConfiguration) {
|
||||
return new GeofencingArgumentEntry(entityIdkvEntryMap, zoneGroupConfiguration);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.thingsboard.script.api.tbel.TbelCfArg;
|
||||
import org.thingsboard.script.api.tbel.TbelCfTsGeofencingArg;
|
||||
import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration;
|
||||
import org.thingsboard.server.common.data.id.EntityId;
|
||||
import org.thingsboard.server.common.data.kv.KvEntry;
|
||||
|
||||
@ -31,13 +32,17 @@ import java.util.stream.Collectors;
|
||||
public class GeofencingArgumentEntry implements ArgumentEntry {
|
||||
|
||||
private Map<EntityId, GeofencingZoneState> zoneStates;
|
||||
private GeofencingZoneGroupConfiguration zoneGroupConfiguration;
|
||||
|
||||
private boolean forceResetPrevious;
|
||||
|
||||
public GeofencingArgumentEntry() {
|
||||
}
|
||||
|
||||
public GeofencingArgumentEntry(Map<EntityId, KvEntry> entityIdKvEntryMap) {
|
||||
this.zoneStates = toZones(entityIdKvEntryMap);
|
||||
public GeofencingArgumentEntry(Map<EntityId, KvEntry> entityIdkvEntryMap,
|
||||
GeofencingZoneGroupConfiguration zoneGroupConfiguration) {
|
||||
this.zoneStates = toZones(entityIdkvEntryMap);
|
||||
this.zoneGroupConfiguration = zoneGroupConfiguration;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -23,6 +23,7 @@ import lombok.Data;
|
||||
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.GeofencingEvent;
|
||||
import org.thingsboard.server.service.cf.CalculatedFieldResult;
|
||||
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
|
||||
import org.thingsboard.server.utils.CalculatedFieldUtils;
|
||||
@ -31,11 +32,13 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
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.ALLOWED_ZONES_ARGUMENT_KEY;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.RESTRICTED_ZONES_ARGUMENT_KEY;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.coordinateKeys;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@ -70,7 +73,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
|
||||
|
||||
boolean stateUpdated = false;
|
||||
|
||||
for (Map.Entry<String, ArgumentEntry> entry : argumentValues.entrySet()) {
|
||||
for (var entry : argumentValues.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
ArgumentEntry newEntry = entry.getValue();
|
||||
|
||||
@ -80,26 +83,22 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
|
||||
boolean entryUpdated;
|
||||
|
||||
if (existingEntry == null || newEntry.isForceResetPrevious()) {
|
||||
switch (key) {
|
||||
case ENTITY_ID_LATITUDE_ARGUMENT_KEY:
|
||||
case ENTITY_ID_LONGITUDE_ARGUMENT_KEY:
|
||||
entryUpdated = switch (key) {
|
||||
case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
|
||||
if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) {
|
||||
throw new IllegalArgumentException(key + " argument must be a single value argument.");
|
||||
}
|
||||
arguments.put(key, singleValueArgumentEntry);
|
||||
entryUpdated = true;
|
||||
break;
|
||||
case ALLOWED_ZONES_ARGUMENT_KEY:
|
||||
case RESTRICTED_ZONES_ARGUMENT_KEY:
|
||||
yield true;
|
||||
}
|
||||
default -> {
|
||||
if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
|
||||
throw new IllegalArgumentException(key + " argument must be a geofencing argument entry.");
|
||||
}
|
||||
arguments.put(key, geofencingArgumentEntry);
|
||||
entryUpdated = true;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported argument: " + key);
|
||||
}
|
||||
yield true;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
entryUpdated = existingEntry.updateEntry(newEntry);
|
||||
}
|
||||
@ -111,21 +110,60 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
|
||||
}
|
||||
|
||||
|
||||
// TODO: Probably returning list of CalculatedFieldResult no needed anymore,
|
||||
// since logic changed to use zone groups with telemetry prefix.
|
||||
@Override
|
||||
public ListenableFuture<List<CalculatedFieldResult>> performCalculation(CalculatedFieldCtx ctx) {
|
||||
double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue();
|
||||
double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue();
|
||||
Coordinates entityCoordinates = new Coordinates(latitude, longitude);
|
||||
|
||||
List<CalculatedFieldResult> savedZonesStatesResults = updateGeofencingZonesState(ctx, entityCoordinates, false);
|
||||
List<CalculatedFieldResult> restrictedZonesStatesResults = updateGeofencingZonesState(ctx, entityCoordinates, true);
|
||||
ObjectNode resultNode = JacksonUtil.newObjectNode();
|
||||
getGeofencingArguments().forEach((argumentKey, argumentEntry) -> {
|
||||
var zoneGroupConfig = argumentEntry.getZoneGroupConfiguration();
|
||||
Set<GeofencingEvent> zoneEvents = argumentEntry.getZoneStates()
|
||||
.values()
|
||||
.stream()
|
||||
.map(zoneState -> zoneState.evaluate(entityCoordinates))
|
||||
.collect(Collectors.toSet());
|
||||
aggregateZoneGroupEvent(zoneEvents).ifPresent(event ->
|
||||
resultNode.put(zoneGroupConfig.getReportTelemetryPrefix() + "Event", event.name())
|
||||
);
|
||||
});
|
||||
return Futures.immediateFuture(List.of(new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode)));
|
||||
}
|
||||
|
||||
List<CalculatedFieldResult> allZoneStatesResults =
|
||||
new ArrayList<>(savedZonesStatesResults.size() + restrictedZonesStatesResults.size());
|
||||
allZoneStatesResults.addAll(savedZonesStatesResults);
|
||||
allZoneStatesResults.addAll(restrictedZonesStatesResults);
|
||||
private Optional<GeofencingEvent> aggregateZoneGroupEvent(Set<GeofencingEvent> zoneEvents) {
|
||||
boolean hasEntered = false;
|
||||
boolean hasLeft = false;
|
||||
boolean hasInside = false;
|
||||
boolean hasOutside = false;
|
||||
|
||||
return Futures.immediateFuture(allZoneStatesResults);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
if (hasInside || hasEntered) {
|
||||
return Optional.of(GeofencingEvent.INSIDE);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -142,26 +180,12 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
|
||||
}
|
||||
}
|
||||
|
||||
private List<CalculatedFieldResult> updateGeofencingZonesState(CalculatedFieldCtx ctx, Coordinates entityCoordinates, boolean restricted) {
|
||||
String zoneKey = restricted ? RESTRICTED_ZONES_ARGUMENT_KEY : ALLOWED_ZONES_ARGUMENT_KEY;
|
||||
GeofencingArgumentEntry zonesEntry = (GeofencingArgumentEntry) arguments.get(zoneKey);
|
||||
|
||||
if (zonesEntry == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
var results = new ArrayList<CalculatedFieldResult>();
|
||||
for (var zoneEntry : zonesEntry.getZoneStates().entrySet()) {
|
||||
GeofencingZoneState state = zoneEntry.getValue();
|
||||
String event = state.evaluate(entityCoordinates);
|
||||
ObjectNode stateNode = JacksonUtil.newObjectNode();
|
||||
stateNode.put("entityId", ctx.getEntityId().toString());
|
||||
stateNode.put("zoneId", state.getZoneId().toString());
|
||||
stateNode.put("restricted", restricted);
|
||||
stateNode.put("event", event);
|
||||
results.add(new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), stateNode));
|
||||
}
|
||||
return results;
|
||||
// TODO: Create a new class field to not do this on each calculation.
|
||||
private Map<String, GeofencingArgumentEntry> getGeofencingArguments() {
|
||||
return arguments.entrySet()
|
||||
.stream()
|
||||
.filter(entry -> !coordinateKeys.contains(entry.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ 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.rule.engine.util.GpsGeofencingEvents;
|
||||
import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent;
|
||||
import org.thingsboard.server.common.data.id.EntityId;
|
||||
import org.thingsboard.server.common.data.id.EntityIdFactory;
|
||||
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
|
||||
@ -81,16 +81,20 @@ public class GeofencingZoneState {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String evaluate(Coordinates entityCoordinates) {
|
||||
public GeofencingEvent evaluate(Coordinates entityCoordinates) {
|
||||
boolean inside = perimeterDefinition.checkMatches(entityCoordinates);
|
||||
// TODO: maybe handle this.inside == null as ENTERED or OUTSIDE.
|
||||
// Since if this.inside == null then we don't have a state for this zone yet
|
||||
// and logically say that we are OUTSIDE instead of LEFT.
|
||||
if (this.inside == null || this.inside != inside) {
|
||||
// Initial evaluation — no prior state
|
||||
if (this.inside == null) {
|
||||
this.inside = inside;
|
||||
return inside ? GpsGeofencingEvents.ENTERED : GpsGeofencingEvents.LEFT;
|
||||
return inside ? GeofencingEvent.ENTERED : GeofencingEvent.OUTSIDE;
|
||||
}
|
||||
return inside ? GpsGeofencingEvents.INSIDE : GpsGeofencingEvents.OUTSIDE;
|
||||
// State changed
|
||||
if (this.inside != inside) {
|
||||
this.inside = inside;
|
||||
return inside ? GeofencingEvent.ENTERED : GeofencingEvent.LEFT;
|
||||
}
|
||||
// State unchanged
|
||||
return inside ? GeofencingEvent.INSIDE : GeofencingEvent.OUTSIDE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -18,6 +18,8 @@ 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.GeofencingEvent;
|
||||
import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration;
|
||||
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
||||
import org.thingsboard.server.common.data.id.EntityId;
|
||||
import org.thingsboard.server.common.data.id.EntityIdFactory;
|
||||
@ -28,6 +30,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntit
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.GeofencingArgumentProto;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.GeofencingEventProto;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneIdProto;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto;
|
||||
@ -45,6 +48,7 @@ import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState;
|
||||
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
|
||||
import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.TreeMap;
|
||||
@ -123,12 +127,15 @@ public class CalculatedFieldUtils {
|
||||
|
||||
|
||||
private static GeofencingArgumentProto toGeofencingArgumentProto(String argName, GeofencingArgumentEntry geofencingArgumentEntry) {
|
||||
GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder()
|
||||
.setArgName(argName);
|
||||
var zoneGroupConfiguration = geofencingArgumentEntry.getZoneGroupConfiguration();
|
||||
Map<EntityId, GeofencingZoneState> zoneStates = geofencingArgumentEntry.getZoneStates();
|
||||
zoneStates.forEach((entityId, zoneState) -> {
|
||||
builder.addZones(toGeofencingZoneProto(entityId, zoneState));
|
||||
});
|
||||
GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder()
|
||||
.setArgName(argName)
|
||||
.setTelemetryPrefix(zoneGroupConfiguration.getReportTelemetryPrefix());
|
||||
zoneStates.forEach((entityId, zoneState) ->
|
||||
builder.addZones(toGeofencingZoneProto(entityId, zoneState)));
|
||||
zoneGroupConfiguration.getReportEvents().forEach(event ->
|
||||
builder.addReportEvents(GeofencingEventProto.forNumber(event.getProtoNumber())));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@ -206,8 +213,14 @@ public class CalculatedFieldUtils {
|
||||
.stream()
|
||||
.map(GeofencingZoneState::new)
|
||||
.collect(Collectors.toMap(GeofencingZoneState::getZoneId, Function.identity()));
|
||||
List<GeofencingEvent> geofencingEvents = proto.getReportEventsList()
|
||||
.stream()
|
||||
.map(geofencingEventProto -> GeofencingEvent.fromProtoNumber(geofencingEventProto.getNumber()))
|
||||
.toList();
|
||||
var zoneGroupConfiguration = new GeofencingZoneGroupConfiguration(proto.getTelemetryPrefix(), geofencingEvents);
|
||||
GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry();
|
||||
geofencingArgumentEntry.setZoneStates(zoneStates);
|
||||
geofencingArgumentEntry.setZoneGroupConfiguration(zoneGroupConfiguration);
|
||||
return geofencingArgumentEntry;
|
||||
}
|
||||
|
||||
|
||||
@ -17,10 +17,14 @@ package org.thingsboard.server.common.data.cf.configuration;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
|
||||
import org.thingsboard.server.common.data.util.CollectionsUtil;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType.RELATION_QUERY;
|
||||
|
||||
@ -30,21 +34,14 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
|
||||
|
||||
public static final String ENTITY_ID_LATITUDE_ARGUMENT_KEY = "latitude";
|
||||
public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude";
|
||||
public static final String ALLOWED_ZONES_ARGUMENT_KEY = "allowedZones";
|
||||
public static final String RESTRICTED_ZONES_ARGUMENT_KEY = "restrictedZones";
|
||||
|
||||
private static final Set<String> allowedKeys = Set.of(
|
||||
ENTITY_ID_LATITUDE_ARGUMENT_KEY,
|
||||
ENTITY_ID_LONGITUDE_ARGUMENT_KEY,
|
||||
ALLOWED_ZONES_ARGUMENT_KEY,
|
||||
RESTRICTED_ZONES_ARGUMENT_KEY
|
||||
);
|
||||
|
||||
private static final Set<String> requiredKeys = Set.of(
|
||||
public static final Set<String> coordinateKeys = Set.of(
|
||||
ENTITY_ID_LATITUDE_ARGUMENT_KEY,
|
||||
ENTITY_ID_LONGITUDE_ARGUMENT_KEY
|
||||
);
|
||||
|
||||
Map<String, GeofencingZoneGroupConfiguration> geofencingZoneGroupConfigurations;
|
||||
|
||||
@Override
|
||||
public CalculatedFieldType getType() {
|
||||
return CalculatedFieldType.GEOFENCING;
|
||||
@ -56,74 +53,100 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
|
||||
if (arguments == null) {
|
||||
throw new IllegalArgumentException("Geofencing calculated field arguments are empty!");
|
||||
}
|
||||
|
||||
// Check key count
|
||||
if (arguments.size() < 3 || arguments.size() > 4) {
|
||||
throw new IllegalArgumentException("Geofencing calculated field must contain 3 or 4 arguments: " + allowedKeys);
|
||||
if (arguments.size() < 3) {
|
||||
throw new IllegalArgumentException("Geofencing calculated field must contain at least 3 arguments!");
|
||||
}
|
||||
if (arguments.size() > 5) {
|
||||
throw new IllegalArgumentException("Geofencing calculated field size exceeds limit of 5 arguments!");
|
||||
}
|
||||
validateCoordinateArguments();
|
||||
|
||||
// Check for unsupported argument keys
|
||||
for (String key : arguments.keySet()) {
|
||||
if (!allowedKeys.contains(key)) {
|
||||
throw new IllegalArgumentException("Unsupported argument key: '" + key + "'. Allowed keys: " + allowedKeys);
|
||||
Map<String, Argument> zoneGroupsArguments = getZoneGroupArguments();
|
||||
if (zoneGroupsArguments.isEmpty()) {
|
||||
throw new IllegalArgumentException("Geofencing calculated field must contain at least one geofencing zone group defined!");
|
||||
}
|
||||
validateZoneGroupAruguments(zoneGroupsArguments);
|
||||
validateZoneGroupConfigurations(zoneGroupsArguments);
|
||||
}
|
||||
|
||||
private void validateZoneGroupConfigurations(Map<String, Argument> zoneGroupsArguments) {
|
||||
if (geofencingZoneGroupConfigurations == null) {
|
||||
throw new IllegalArgumentException("Geofencing calculated field zone group configurations are empty!");
|
||||
}
|
||||
Set<String> usedPrefixes = new HashSet<>();
|
||||
geofencingZoneGroupConfigurations.forEach((zoneGroupName, config) -> {
|
||||
Argument zoneGroupArgument = zoneGroupsArguments.get(zoneGroupName);
|
||||
if (zoneGroupArgument == null) {
|
||||
throw new IllegalArgumentException("Geofencing calculated field zone group configuration is not configured for zone group: " + zoneGroupName);
|
||||
}
|
||||
}
|
||||
|
||||
// Check required fields: latitude and longitude
|
||||
for (String requiredKey : requiredKeys) {
|
||||
if (!arguments.containsKey(requiredKey)) {
|
||||
throw new IllegalArgumentException("Missing required argument: " + requiredKey);
|
||||
if (config == null) {
|
||||
throw new IllegalArgumentException("Zone group configuration is not configured for zone group: " + zoneGroupName);
|
||||
}
|
||||
}
|
||||
if (CollectionsUtil.isEmpty(config.getReportEvents())) {
|
||||
throw new IllegalArgumentException("Zone group configuration report events must be specified for zone group: " + zoneGroupName);
|
||||
}
|
||||
String prefix = config.getReportTelemetryPrefix();
|
||||
if (StringUtils.isBlank(prefix)) {
|
||||
throw new IllegalArgumentException("Report telemetry prefix should be specified for zone group: " + zoneGroupName);
|
||||
}
|
||||
if (!usedPrefixes.add(prefix)) {
|
||||
throw new IllegalArgumentException("Duplicate report telemetry prefix found: '" + prefix + "'. Must be unique!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure at least one of the zone types is configured
|
||||
boolean hasAllowedZones = arguments.containsKey(ALLOWED_ZONES_ARGUMENT_KEY);
|
||||
boolean hasRestrictedZones = arguments.containsKey(RESTRICTED_ZONES_ARGUMENT_KEY);
|
||||
|
||||
if (!hasAllowedZones && !hasRestrictedZones) {
|
||||
throw new IllegalArgumentException("Geofencing calculated field must contain at least one of the following arguments: 'allowedZones' or 'restrictedZones'");
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Argument> entry : arguments.entrySet()) {
|
||||
String argumentKey = entry.getKey();
|
||||
Argument argument = entry.getValue();
|
||||
private void validateCoordinateArguments() {
|
||||
for (String coordinateKey : coordinateKeys) {
|
||||
Argument argument = arguments.get(coordinateKey);
|
||||
if (argument == null) {
|
||||
throw new IllegalArgumentException("Missing required argument: " + argumentKey);
|
||||
throw new IllegalArgumentException("Missing required coordinates argument: " + coordinateKey);
|
||||
}
|
||||
ReferencedEntityKey refEntityKey = argument.getRefEntityKey();
|
||||
if (refEntityKey == null || refEntityKey.getType() == null) {
|
||||
throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + argumentKey);
|
||||
ReferencedEntityKey refEntityKey = validateAndGetRefEntityKey(argument, coordinateKey);
|
||||
if (!ArgumentType.TS_LATEST.equals(refEntityKey.getType())) {
|
||||
throw new IllegalArgumentException("Argument '" + coordinateKey + "' must be of type TS_LATEST.");
|
||||
}
|
||||
|
||||
switch (argumentKey) {
|
||||
case ENTITY_ID_LATITUDE_ARGUMENT_KEY,
|
||||
ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
|
||||
if (!ArgumentType.TS_LATEST.equals(refEntityKey.getType())) {
|
||||
throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type TS_LATEST.");
|
||||
}
|
||||
if (argument.getRefDynamicSource() != null) {
|
||||
throw new IllegalArgumentException("Dynamic source is not allowed for argument: '" + argumentKey + "'.");
|
||||
}
|
||||
}
|
||||
case ALLOWED_ZONES_ARGUMENT_KEY,
|
||||
RESTRICTED_ZONES_ARGUMENT_KEY -> {
|
||||
if (!ArgumentType.ATTRIBUTE.equals(refEntityKey.getType())) {
|
||||
throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type ATTRIBUTE.");
|
||||
}
|
||||
var dynamicSource = argument.getRefDynamicSource();
|
||||
if (dynamicSource == null) {
|
||||
continue;
|
||||
}
|
||||
if (!RELATION_QUERY.equals(dynamicSource)) {
|
||||
throw new IllegalArgumentException("Only relation query dynamic source is supported for argument: '" + argumentKey + "'.");
|
||||
}
|
||||
if (argument.getRefDynamicSourceConfiguration() == null) {
|
||||
throw new IllegalArgumentException("Missing dynamic source configuration for argument: '" + argumentKey + "'.");
|
||||
}
|
||||
argument.getRefDynamicSourceConfiguration().validate();
|
||||
}
|
||||
if (argument.getRefDynamicSource() != null) {
|
||||
throw new IllegalArgumentException("Dynamic source is not allowed for argument: '" + coordinateKey + "'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateZoneGroupAruguments(Map<String, Argument> zoneGroupsArguments) {
|
||||
zoneGroupsArguments.forEach((argumentKey, argument) -> {
|
||||
if (argument == null) {
|
||||
throw new IllegalArgumentException("Zone group argument is not configured: " + argumentKey);
|
||||
}
|
||||
ReferencedEntityKey refEntityKey = validateAndGetRefEntityKey(argument, argumentKey);
|
||||
if (!ArgumentType.ATTRIBUTE.equals(refEntityKey.getType())) {
|
||||
throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type ATTRIBUTE.");
|
||||
}
|
||||
var dynamicSource = argument.getRefDynamicSource();
|
||||
if (dynamicSource == null) {
|
||||
return;
|
||||
}
|
||||
if (!RELATION_QUERY.equals(dynamicSource)) {
|
||||
throw new IllegalArgumentException("Only relation query dynamic source is supported for argument: '" + argumentKey + "'.");
|
||||
}
|
||||
if (argument.getRefDynamicSourceConfiguration() == null) {
|
||||
throw new IllegalArgumentException("Missing dynamic source configuration for argument: '" + argumentKey + "'.");
|
||||
}
|
||||
argument.getRefDynamicSourceConfiguration().validate();
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, Argument> getZoneGroupArguments() {
|
||||
return arguments.entrySet()
|
||||
.stream()
|
||||
.filter(entry -> !coordinateKeys.contains(entry.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
private static ReferencedEntityKey validateAndGetRefEntityKey(Argument argument, String argumentKey) {
|
||||
ReferencedEntityKey refEntityKey = argument.getRefEntityKey();
|
||||
if (refEntityKey == null || refEntityKey.getType() == null) {
|
||||
throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + argumentKey);
|
||||
}
|
||||
return refEntityKey;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright © 2016-2025 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.common.data.cf.configuration;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Getter
|
||||
public enum GeofencingEvent {
|
||||
|
||||
ENTERED(0), LEFT(1), INSIDE(2), OUTSIDE(3);
|
||||
|
||||
private final int protoNumber; // Corresponds to GeofencingEvent
|
||||
|
||||
GeofencingEvent(int protoNumber) {
|
||||
this.protoNumber = protoNumber;
|
||||
}
|
||||
|
||||
private static final GeofencingEvent[] BY_PROTO;
|
||||
|
||||
static {
|
||||
BY_PROTO = new GeofencingEvent[Arrays.stream(values()).mapToInt(GeofencingEvent::getProtoNumber).max().orElse(0) + 1];
|
||||
for (var event : values()) {
|
||||
BY_PROTO[event.getProtoNumber()] = event;
|
||||
}
|
||||
}
|
||||
|
||||
public static GeofencingEvent fromProtoNumber(int protoNumber) {
|
||||
if (protoNumber < 0 || protoNumber >= BY_PROTO.length) {
|
||||
throw new IllegalArgumentException("Invalid GeofencingEvent proto number " + protoNumber);
|
||||
}
|
||||
return BY_PROTO[protoNumber];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright © 2016-2025 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.common.data.cf.configuration;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class GeofencingZoneGroupConfiguration {
|
||||
|
||||
private final String reportTelemetryPrefix;
|
||||
private final List<GeofencingEvent> reportEvents;
|
||||
|
||||
}
|
||||
@ -908,9 +908,18 @@ message GeofencingZoneProto {
|
||||
optional bool inside = 5;
|
||||
}
|
||||
|
||||
enum GeofencingEventProto {
|
||||
ENTERED = 0;
|
||||
LEFT = 1;
|
||||
INSIDE = 2;
|
||||
OUTSIDE = 3;
|
||||
}
|
||||
|
||||
message GeofencingArgumentProto {
|
||||
string argName = 1; // e.g., "restrictedZones" or "allowedZones"
|
||||
repeated GeofencingZoneProto zones = 2;
|
||||
string argName = 1;
|
||||
string telemetryPrefix = 2;
|
||||
repeated GeofencingEventProto reportEvents = 3;
|
||||
repeated GeofencingZoneProto zones = 4;
|
||||
}
|
||||
|
||||
message CalculatedFieldStateProto {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user