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.CalculatedFieldType;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.configuration.Argument;
 | 
					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.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.OutputType;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
 | 
					import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
 | 
				
			||||||
import org.thingsboard.server.common.data.id.CalculatedFieldId;
 | 
					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.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_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.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;
 | 
					import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@TbRuleEngineComponent
 | 
					@TbRuleEngineComponent
 | 
				
			||||||
@ -132,16 +132,17 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
 | 
				
			|||||||
        Map<String, ListenableFuture<ArgumentEntry>> argFutures = new HashMap<>();
 | 
					        Map<String, ListenableFuture<ArgumentEntry>> argFutures = new HashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (ctx.getCalculatedField().getType().equals(CalculatedFieldType.GEOFENCING)) {
 | 
					        if (ctx.getCalculatedField().getType().equals(CalculatedFieldType.GEOFENCING)) {
 | 
				
			||||||
            // Ignoring any other arguments except ENTITY_ID_LATITUDE_ARGUMENT_KEY,
 | 
					            var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
 | 
				
			||||||
            // ENTITY_ID_LONGITUDE_ARGUMENT_KEY, SAVE_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY.
 | 
					            var zoneGroupConfigs = configuration.getGeofencingZoneGroupConfigurations();
 | 
				
			||||||
            for (var entry : ctx.getArguments().entrySet()) {
 | 
					            for (var entry : ctx.getArguments().entrySet()) {
 | 
				
			||||||
                switch (entry.getKey()) {
 | 
					                switch (entry.getKey()) {
 | 
				
			||||||
                    case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY ->
 | 
					                    case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY ->
 | 
				
			||||||
                            argFutures.put(entry.getKey(), fetchKvEntry(ctx.getTenantId(), resolveEntityId(entityId, entry), entry.getValue()));
 | 
					                            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);
 | 
					                        var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry);
 | 
				
			||||||
                        argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds ->
 | 
					                        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) {
 | 
					        if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) {
 | 
				
			||||||
            throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType());
 | 
					            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);
 | 
					        ListenableFuture<List<Map.Entry<EntityId, AttributeKvEntry>>> allFutures = Futures.allAsList(kvFutures);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Futures.transform(allFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream()
 | 
					        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
 | 
					                calculatedFieldCallbackExecutor
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
 | 
				
			|||||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
 | 
					import com.fasterxml.jackson.annotation.JsonSubTypes;
 | 
				
			||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 | 
					import com.fasterxml.jackson.annotation.JsonTypeInfo;
 | 
				
			||||||
import org.thingsboard.script.api.tbel.TbelCfArg;
 | 
					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.id.EntityId;
 | 
				
			||||||
import org.thingsboard.server.common.data.kv.KvEntry;
 | 
					import org.thingsboard.server.common.data.kv.KvEntry;
 | 
				
			||||||
import org.thingsboard.server.common.data.kv.TsKvEntry;
 | 
					import org.thingsboard.server.common.data.kv.TsKvEntry;
 | 
				
			||||||
@ -61,8 +62,8 @@ public interface ArgumentEntry {
 | 
				
			|||||||
        return new TsRollingArgumentEntry(kvEntries, limit, timeWindow);
 | 
					        return new TsRollingArgumentEntry(kvEntries, limit, timeWindow);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    static ArgumentEntry createGeofencingValueArgument(Map<EntityId, KvEntry> entityIdkvEntryMap) {
 | 
					    static ArgumentEntry createGeofencingValueArgument(Map<EntityId, KvEntry> entityIdkvEntryMap, GeofencingZoneGroupConfiguration zoneGroupConfiguration) {
 | 
				
			||||||
        return new GeofencingArgumentEntry(entityIdkvEntryMap);
 | 
					        return new GeofencingArgumentEntry(entityIdkvEntryMap, zoneGroupConfiguration);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,7 @@ import lombok.Data;
 | 
				
			|||||||
import lombok.extern.slf4j.Slf4j;
 | 
					import lombok.extern.slf4j.Slf4j;
 | 
				
			||||||
import org.thingsboard.script.api.tbel.TbelCfArg;
 | 
					import org.thingsboard.script.api.tbel.TbelCfArg;
 | 
				
			||||||
import org.thingsboard.script.api.tbel.TbelCfTsGeofencingArg;
 | 
					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.id.EntityId;
 | 
				
			||||||
import org.thingsboard.server.common.data.kv.KvEntry;
 | 
					import org.thingsboard.server.common.data.kv.KvEntry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -31,13 +32,17 @@ import java.util.stream.Collectors;
 | 
				
			|||||||
public class GeofencingArgumentEntry implements ArgumentEntry {
 | 
					public class GeofencingArgumentEntry implements ArgumentEntry {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private Map<EntityId, GeofencingZoneState> zoneStates;
 | 
					    private Map<EntityId, GeofencingZoneState> zoneStates;
 | 
				
			||||||
 | 
					    private GeofencingZoneGroupConfiguration zoneGroupConfiguration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private boolean forceResetPrevious;
 | 
					    private boolean forceResetPrevious;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public GeofencingArgumentEntry() {
 | 
					    public GeofencingArgumentEntry() {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public GeofencingArgumentEntry(Map<EntityId, KvEntry> entityIdKvEntryMap) {
 | 
					    public GeofencingArgumentEntry(Map<EntityId, KvEntry> entityIdkvEntryMap,
 | 
				
			||||||
        this.zoneStates = toZones(entityIdKvEntryMap);
 | 
					                                   GeofencingZoneGroupConfiguration zoneGroupConfiguration) {
 | 
				
			||||||
 | 
					        this.zoneStates = toZones(entityIdkvEntryMap);
 | 
				
			||||||
 | 
					        this.zoneGroupConfiguration = zoneGroupConfiguration;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
 | 
				
			|||||||
@ -23,6 +23,7 @@ import lombok.Data;
 | 
				
			|||||||
import org.thingsboard.common.util.JacksonUtil;
 | 
					import org.thingsboard.common.util.JacksonUtil;
 | 
				
			||||||
import org.thingsboard.common.util.geo.Coordinates;
 | 
					import org.thingsboard.common.util.geo.Coordinates;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
 | 
					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.CalculatedFieldResult;
 | 
				
			||||||
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
 | 
					import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
 | 
				
			||||||
import org.thingsboard.server.utils.CalculatedFieldUtils;
 | 
					import org.thingsboard.server.utils.CalculatedFieldUtils;
 | 
				
			||||||
@ -31,11 +32,13 @@ import java.util.ArrayList;
 | 
				
			|||||||
import java.util.HashMap;
 | 
					import java.util.HashMap;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.Map;
 | 
					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_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.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
 | 
					@Data
 | 
				
			||||||
@AllArgsConstructor
 | 
					@AllArgsConstructor
 | 
				
			||||||
@ -70,7 +73,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        boolean stateUpdated = false;
 | 
					        boolean stateUpdated = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (Map.Entry<String, ArgumentEntry> entry : argumentValues.entrySet()) {
 | 
					        for (var entry : argumentValues.entrySet()) {
 | 
				
			||||||
            String key = entry.getKey();
 | 
					            String key = entry.getKey();
 | 
				
			||||||
            ArgumentEntry newEntry = entry.getValue();
 | 
					            ArgumentEntry newEntry = entry.getValue();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -80,26 +83,22 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
 | 
				
			|||||||
            boolean entryUpdated;
 | 
					            boolean entryUpdated;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (existingEntry == null || newEntry.isForceResetPrevious()) {
 | 
					            if (existingEntry == null || newEntry.isForceResetPrevious()) {
 | 
				
			||||||
                switch (key) {
 | 
					                entryUpdated = switch (key) {
 | 
				
			||||||
                    case ENTITY_ID_LATITUDE_ARGUMENT_KEY:
 | 
					                    case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
 | 
				
			||||||
                    case ENTITY_ID_LONGITUDE_ARGUMENT_KEY:
 | 
					 | 
				
			||||||
                        if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) {
 | 
					                        if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) {
 | 
				
			||||||
                            throw new IllegalArgumentException(key + " argument must be a single value argument.");
 | 
					                            throw new IllegalArgumentException(key + " argument must be a single value argument.");
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        arguments.put(key, singleValueArgumentEntry);
 | 
					                        arguments.put(key, singleValueArgumentEntry);
 | 
				
			||||||
                        entryUpdated = true;
 | 
					                        yield true;
 | 
				
			||||||
                        break;
 | 
					                    }
 | 
				
			||||||
                    case ALLOWED_ZONES_ARGUMENT_KEY:
 | 
					                    default -> {
 | 
				
			||||||
                    case RESTRICTED_ZONES_ARGUMENT_KEY:
 | 
					 | 
				
			||||||
                        if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
 | 
					                        if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
 | 
				
			||||||
                            throw new IllegalArgumentException(key + " argument must be a geofencing argument entry.");
 | 
					                            throw new IllegalArgumentException(key + " argument must be a geofencing argument entry.");
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        arguments.put(key, geofencingArgumentEntry);
 | 
					                        arguments.put(key, geofencingArgumentEntry);
 | 
				
			||||||
                        entryUpdated = true;
 | 
					                        yield true;
 | 
				
			||||||
                        break;
 | 
					                    }
 | 
				
			||||||
                    default:
 | 
					                };
 | 
				
			||||||
                        throw new IllegalArgumentException("Unsupported argument: " + key);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                entryUpdated = existingEntry.updateEntry(newEntry);
 | 
					                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
 | 
					    @Override
 | 
				
			||||||
    public ListenableFuture<List<CalculatedFieldResult>> performCalculation(CalculatedFieldCtx ctx) {
 | 
					    public ListenableFuture<List<CalculatedFieldResult>> performCalculation(CalculatedFieldCtx ctx) {
 | 
				
			||||||
        double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue();
 | 
					        double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue();
 | 
				
			||||||
        double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue();
 | 
					        double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue();
 | 
				
			||||||
        Coordinates entityCoordinates = new Coordinates(latitude, longitude);
 | 
					        Coordinates entityCoordinates = new Coordinates(latitude, longitude);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        List<CalculatedFieldResult> savedZonesStatesResults = updateGeofencingZonesState(ctx, entityCoordinates, false);
 | 
					        ObjectNode resultNode = JacksonUtil.newObjectNode();
 | 
				
			||||||
        List<CalculatedFieldResult> restrictedZonesStatesResults = updateGeofencingZonesState(ctx, entityCoordinates, true);
 | 
					        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 =
 | 
					    private Optional<GeofencingEvent> aggregateZoneGroupEvent(Set<GeofencingEvent> zoneEvents) {
 | 
				
			||||||
                new ArrayList<>(savedZonesStatesResults.size() + restrictedZonesStatesResults.size());
 | 
					        boolean hasEntered = false;
 | 
				
			||||||
        allZoneStatesResults.addAll(savedZonesStatesResults);
 | 
					        boolean hasLeft = false;
 | 
				
			||||||
        allZoneStatesResults.addAll(restrictedZonesStatesResults);
 | 
					        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
 | 
					    @Override
 | 
				
			||||||
@ -142,26 +180,12 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private List<CalculatedFieldResult> updateGeofencingZonesState(CalculatedFieldCtx ctx, Coordinates entityCoordinates, boolean restricted) {
 | 
					    // TODO: Create a new class field to not do this on each calculation.
 | 
				
			||||||
        String zoneKey = restricted ? RESTRICTED_ZONES_ARGUMENT_KEY : ALLOWED_ZONES_ARGUMENT_KEY;
 | 
					    private Map<String, GeofencingArgumentEntry> getGeofencingArguments() {
 | 
				
			||||||
        GeofencingArgumentEntry zonesEntry = (GeofencingArgumentEntry) arguments.get(zoneKey);
 | 
					        return arguments.entrySet()
 | 
				
			||||||
 | 
					                .stream()
 | 
				
			||||||
        if (zonesEntry == null) {
 | 
					                .filter(entry -> !coordinateKeys.contains(entry.getKey()))
 | 
				
			||||||
            return List.of();
 | 
					                .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue()));
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -20,7 +20,7 @@ import lombok.EqualsAndHashCode;
 | 
				
			|||||||
import org.thingsboard.common.util.JacksonUtil;
 | 
					import org.thingsboard.common.util.JacksonUtil;
 | 
				
			||||||
import org.thingsboard.common.util.geo.Coordinates;
 | 
					import org.thingsboard.common.util.geo.Coordinates;
 | 
				
			||||||
import org.thingsboard.common.util.geo.PerimeterDefinition;
 | 
					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.EntityId;
 | 
				
			||||||
import org.thingsboard.server.common.data.id.EntityIdFactory;
 | 
					import org.thingsboard.server.common.data.id.EntityIdFactory;
 | 
				
			||||||
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 | 
					import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 | 
				
			||||||
@ -81,16 +81,20 @@ public class GeofencingZoneState {
 | 
				
			|||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public String evaluate(Coordinates entityCoordinates) {
 | 
					    public GeofencingEvent evaluate(Coordinates entityCoordinates) {
 | 
				
			||||||
        boolean inside = perimeterDefinition.checkMatches(entityCoordinates);
 | 
					        boolean inside = perimeterDefinition.checkMatches(entityCoordinates);
 | 
				
			||||||
        // TODO: maybe handle this.inside == null as ENTERED or OUTSIDE.
 | 
					        // Initial evaluation — no prior state
 | 
				
			||||||
        //  Since if this.inside == null then we don't have a state for this zone yet
 | 
					        if (this.inside == null) {
 | 
				
			||||||
        //  and logically say that we are OUTSIDE instead of LEFT.
 | 
					 | 
				
			||||||
        if (this.inside == null || this.inside != inside) {
 | 
					 | 
				
			||||||
            this.inside = inside;
 | 
					            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.common.util.JacksonUtil;
 | 
				
			||||||
import org.thingsboard.server.common.data.StringUtils;
 | 
					import org.thingsboard.server.common.data.StringUtils;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
 | 
					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.CalculatedFieldId;
 | 
				
			||||||
import org.thingsboard.server.common.data.id.EntityId;
 | 
					import org.thingsboard.server.common.data.id.EntityId;
 | 
				
			||||||
import org.thingsboard.server.common.data.id.EntityIdFactory;
 | 
					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.CalculatedFieldIdProto;
 | 
				
			||||||
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
 | 
					import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
 | 
				
			||||||
import org.thingsboard.server.gen.transport.TransportProtos.GeofencingArgumentProto;
 | 
					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.GeofencingZoneIdProto;
 | 
				
			||||||
import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto;
 | 
					import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto;
 | 
				
			||||||
import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto;
 | 
					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.SingleValueArgumentEntry;
 | 
				
			||||||
import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry;
 | 
					import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
import java.util.Optional;
 | 
					import java.util.Optional;
 | 
				
			||||||
import java.util.TreeMap;
 | 
					import java.util.TreeMap;
 | 
				
			||||||
@ -123,12 +127,15 @@ public class CalculatedFieldUtils {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static GeofencingArgumentProto toGeofencingArgumentProto(String argName, GeofencingArgumentEntry geofencingArgumentEntry) {
 | 
					    private static GeofencingArgumentProto toGeofencingArgumentProto(String argName, GeofencingArgumentEntry geofencingArgumentEntry) {
 | 
				
			||||||
        GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder()
 | 
					        var zoneGroupConfiguration = geofencingArgumentEntry.getZoneGroupConfiguration();
 | 
				
			||||||
                .setArgName(argName);
 | 
					 | 
				
			||||||
        Map<EntityId, GeofencingZoneState> zoneStates = geofencingArgumentEntry.getZoneStates();
 | 
					        Map<EntityId, GeofencingZoneState> zoneStates = geofencingArgumentEntry.getZoneStates();
 | 
				
			||||||
        zoneStates.forEach((entityId, zoneState) -> {
 | 
					        GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder()
 | 
				
			||||||
            builder.addZones(toGeofencingZoneProto(entityId, zoneState));
 | 
					                .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();
 | 
					        return builder.build();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -206,8 +213,14 @@ public class CalculatedFieldUtils {
 | 
				
			|||||||
                .stream()
 | 
					                .stream()
 | 
				
			||||||
                .map(GeofencingZoneState::new)
 | 
					                .map(GeofencingZoneState::new)
 | 
				
			||||||
                .collect(Collectors.toMap(GeofencingZoneState::getZoneId, Function.identity()));
 | 
					                .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 geofencingArgumentEntry = new GeofencingArgumentEntry();
 | 
				
			||||||
        geofencingArgumentEntry.setZoneStates(zoneStates);
 | 
					        geofencingArgumentEntry.setZoneStates(zoneStates);
 | 
				
			||||||
 | 
					        geofencingArgumentEntry.setZoneGroupConfiguration(zoneGroupConfiguration);
 | 
				
			||||||
        return geofencingArgumentEntry;
 | 
					        return geofencingArgumentEntry;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -17,10 +17,14 @@ package org.thingsboard.server.common.data.cf.configuration;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import lombok.Data;
 | 
					import lombok.Data;
 | 
				
			||||||
import lombok.EqualsAndHashCode;
 | 
					import lombok.EqualsAndHashCode;
 | 
				
			||||||
 | 
					import org.thingsboard.server.common.data.StringUtils;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
 | 
					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.Map;
 | 
				
			||||||
import java.util.Set;
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import static org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType.RELATION_QUERY;
 | 
					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_LATITUDE_ARGUMENT_KEY = "latitude";
 | 
				
			||||||
    public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude";
 | 
					    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(
 | 
					    public static final Set<String> coordinateKeys = 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(
 | 
					 | 
				
			||||||
            ENTITY_ID_LATITUDE_ARGUMENT_KEY,
 | 
					            ENTITY_ID_LATITUDE_ARGUMENT_KEY,
 | 
				
			||||||
            ENTITY_ID_LONGITUDE_ARGUMENT_KEY
 | 
					            ENTITY_ID_LONGITUDE_ARGUMENT_KEY
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Map<String, GeofencingZoneGroupConfiguration> geofencingZoneGroupConfigurations;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public CalculatedFieldType getType() {
 | 
					    public CalculatedFieldType getType() {
 | 
				
			||||||
        return CalculatedFieldType.GEOFENCING;
 | 
					        return CalculatedFieldType.GEOFENCING;
 | 
				
			||||||
@ -56,74 +53,100 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
 | 
				
			|||||||
        if (arguments == null) {
 | 
					        if (arguments == null) {
 | 
				
			||||||
            throw new IllegalArgumentException("Geofencing calculated field arguments are empty!");
 | 
					            throw new IllegalArgumentException("Geofencing calculated field arguments are empty!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if (arguments.size() < 3) {
 | 
				
			||||||
        // Check key count
 | 
					            throw new IllegalArgumentException("Geofencing calculated field must contain at least 3 arguments!");
 | 
				
			||||||
        if (arguments.size() < 3 || arguments.size() > 4) {
 | 
					 | 
				
			||||||
            throw new IllegalArgumentException("Geofencing calculated field must contain 3 or 4 arguments: " + allowedKeys);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if (arguments.size() > 5) {
 | 
				
			||||||
 | 
					            throw new IllegalArgumentException("Geofencing calculated field size exceeds limit of 5 arguments!");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        validateCoordinateArguments();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Check for unsupported argument keys
 | 
					        Map<String, Argument> zoneGroupsArguments = getZoneGroupArguments();
 | 
				
			||||||
        for (String key : arguments.keySet()) {
 | 
					        if (zoneGroupsArguments.isEmpty()) {
 | 
				
			||||||
            if (!allowedKeys.contains(key)) {
 | 
					            throw new IllegalArgumentException("Geofencing calculated field must contain at least one geofencing zone group defined!");
 | 
				
			||||||
                throw new IllegalArgumentException("Unsupported argument key: '" + key + "'. Allowed keys: " + allowedKeys);
 | 
					        }
 | 
				
			||||||
 | 
					        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);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					            if (config == null) {
 | 
				
			||||||
 | 
					                throw new IllegalArgumentException("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 (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
 | 
					    private void validateCoordinateArguments() {
 | 
				
			||||||
        boolean hasAllowedZones = arguments.containsKey(ALLOWED_ZONES_ARGUMENT_KEY);
 | 
					        for (String coordinateKey : coordinateKeys) {
 | 
				
			||||||
        boolean hasRestrictedZones = arguments.containsKey(RESTRICTED_ZONES_ARGUMENT_KEY);
 | 
					            Argument argument = arguments.get(coordinateKey);
 | 
				
			||||||
 | 
					 | 
				
			||||||
        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();
 | 
					 | 
				
			||||||
            if (argument == null) {
 | 
					            if (argument == null) {
 | 
				
			||||||
                throw new IllegalArgumentException("Missing required argument: " + argumentKey);
 | 
					                throw new IllegalArgumentException("Missing required coordinates argument: " + coordinateKey);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ReferencedEntityKey refEntityKey = argument.getRefEntityKey();
 | 
					            ReferencedEntityKey refEntityKey = validateAndGetRefEntityKey(argument, coordinateKey);
 | 
				
			||||||
            if (refEntityKey == null || refEntityKey.getType() == null) {
 | 
					            if (!ArgumentType.TS_LATEST.equals(refEntityKey.getType())) {
 | 
				
			||||||
                throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + argumentKey);
 | 
					                throw new IllegalArgumentException("Argument '" + coordinateKey + "' must be of type TS_LATEST.");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            if (argument.getRefDynamicSource() != null) {
 | 
				
			||||||
            switch (argumentKey) {
 | 
					                throw new IllegalArgumentException("Dynamic source is not allowed for argument: '" + coordinateKey + "'.");
 | 
				
			||||||
                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();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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;
 | 
					  optional bool inside = 5;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum GeofencingEventProto {
 | 
				
			||||||
 | 
					  ENTERED = 0;
 | 
				
			||||||
 | 
					  LEFT = 1;
 | 
				
			||||||
 | 
					  INSIDE = 2;
 | 
				
			||||||
 | 
					  OUTSIDE = 3;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
message GeofencingArgumentProto {
 | 
					message GeofencingArgumentProto {
 | 
				
			||||||
  string argName = 1; // e.g., "restrictedZones" or "allowedZones"
 | 
					  string argName = 1;
 | 
				
			||||||
  repeated GeofencingZoneProto zones = 2;
 | 
					  string telemetryPrefix = 2;
 | 
				
			||||||
 | 
					  repeated GeofencingEventProto reportEvents = 3;
 | 
				
			||||||
 | 
					  repeated GeofencingZoneProto zones = 4;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
message CalculatedFieldStateProto {
 | 
					message CalculatedFieldStateProto {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user