Resolved TODOs, refactoring: make GeofencingCalculatedFieldState extends Base state class
This commit is contained in:
		
							parent
							
								
									29934d08bd
								
							
						
					
					
						commit
						1421f9cc9f
					
				@ -93,7 +93,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected abstract void validateNewEntry(ArgumentEntry newEntry);
 | 
					    protected void validateNewEntry(ArgumentEntry newEntry) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private void updateLastUpdateTimestamp(ArgumentEntry entry) {
 | 
					    private void updateLastUpdateTimestamp(ArgumentEntry entry) {
 | 
				
			||||||
        long newTs = this.latestTimestamp;
 | 
					        long newTs = this.latestTimestamp;
 | 
				
			||||||
 | 
				
			|||||||
@ -19,8 +19,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
				
			|||||||
import com.google.common.util.concurrent.Futures;
 | 
					import com.google.common.util.concurrent.Futures;
 | 
				
			||||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
					import com.google.common.util.concurrent.ListenableFuture;
 | 
				
			||||||
import com.google.common.util.concurrent.MoreExecutors;
 | 
					import com.google.common.util.concurrent.MoreExecutors;
 | 
				
			||||||
import lombok.AllArgsConstructor;
 | 
					 | 
				
			||||||
import lombok.Data;
 | 
					import lombok.Data;
 | 
				
			||||||
 | 
					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.server.common.data.cf.CalculatedFieldType;
 | 
					import org.thingsboard.server.common.data.cf.CalculatedFieldType;
 | 
				
			||||||
@ -30,8 +30,6 @@ import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionE
 | 
				
			|||||||
import org.thingsboard.server.common.data.id.EntityId;
 | 
					import org.thingsboard.server.common.data.id.EntityId;
 | 
				
			||||||
import org.thingsboard.server.common.data.relation.EntityRelation;
 | 
					import org.thingsboard.server.common.data.relation.EntityRelation;
 | 
				
			||||||
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.utils.CalculatedFieldUtils;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.ArrayList;
 | 
					import java.util.ArrayList;
 | 
				
			||||||
import java.util.HashMap;
 | 
					import java.util.HashMap;
 | 
				
			||||||
@ -45,24 +43,18 @@ import static org.thingsboard.server.common.data.cf.configuration.GeofencingPres
 | 
				
			|||||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE;
 | 
					import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Data
 | 
					@Data
 | 
				
			||||||
@AllArgsConstructor
 | 
					@EqualsAndHashCode(callSuper = true)
 | 
				
			||||||
public class GeofencingCalculatedFieldState implements CalculatedFieldState {
 | 
					public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    private List<String> requiredArguments;
 | 
					 | 
				
			||||||
    Map<String, ArgumentEntry> arguments;
 | 
					 | 
				
			||||||
    private boolean sizeExceedsLimit;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private long latestTimestamp = -1;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private boolean dirty;
 | 
					    private boolean dirty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public GeofencingCalculatedFieldState() {
 | 
					    public GeofencingCalculatedFieldState() {
 | 
				
			||||||
        this(new ArrayList<>(), new HashMap<>(), false, -1, false);
 | 
					        super(new ArrayList<>(), new HashMap<>(), false, -1);
 | 
				
			||||||
 | 
					        this.dirty = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public GeofencingCalculatedFieldState(List<String> argNames) {
 | 
					    public GeofencingCalculatedFieldState(List<String> argNames) {
 | 
				
			||||||
        this.requiredArguments = argNames;
 | 
					        super(argNames);
 | 
				
			||||||
        this.arguments = new HashMap<>();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
@ -129,21 +121,6 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
 | 
				
			|||||||
        return calculateWithoutRelations(ctx, entityCoordinates, configuration);
 | 
					        return calculateWithoutRelations(ctx, entityCoordinates, configuration);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    public boolean isReady() {
 | 
					 | 
				
			||||||
        return arguments.keySet().containsAll(requiredArguments) &&
 | 
					 | 
				
			||||||
               arguments.values().stream().noneMatch(ArgumentEntry::isEmpty);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    public void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize) {
 | 
					 | 
				
			||||||
        if (!sizeExceedsLimit && maxStateSize > 0 && CalculatedFieldUtils.toProto(ctxId, this).getSerializedSize() > maxStateSize) {
 | 
					 | 
				
			||||||
            arguments.clear();
 | 
					 | 
				
			||||||
            sizeExceedsLimit = true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private ListenableFuture<CalculatedFieldResult> calculateWithRelations(
 | 
					    private ListenableFuture<CalculatedFieldResult> calculateWithRelations(
 | 
				
			||||||
            EntityId entityId,
 | 
					            EntityId entityId,
 | 
				
			||||||
            CalculatedFieldCtx ctx,
 | 
					            CalculatedFieldCtx ctx,
 | 
				
			||||||
 | 
				
			|||||||
@ -20,4 +20,4 @@ import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceSta
 | 
				
			|||||||
import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent;
 | 
					import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public record GeofencingEvalResult(@Nullable GeofencingTransitionEvent transition,
 | 
					public record GeofencingEvalResult(@Nullable GeofencingTransitionEvent transition,
 | 
				
			||||||
                                   GeofencingPresenceStatus status) {}
 | 
					                                   GeofencingPresenceStatus status) {}
 | 
				
			||||||
 | 
				
			|||||||
@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures;
 | 
				
			|||||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
					import com.google.common.util.concurrent.ListenableFuture;
 | 
				
			||||||
import com.google.common.util.concurrent.MoreExecutors;
 | 
					import com.google.common.util.concurrent.MoreExecutors;
 | 
				
			||||||
import lombok.Data;
 | 
					import lombok.Data;
 | 
				
			||||||
 | 
					import lombok.EqualsAndHashCode;
 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
import lombok.extern.slf4j.Slf4j;
 | 
					import lombok.extern.slf4j.Slf4j;
 | 
				
			||||||
import org.thingsboard.script.api.tbel.TbelCfArg;
 | 
					import org.thingsboard.script.api.tbel.TbelCfArg;
 | 
				
			||||||
@ -38,6 +39,7 @@ import java.util.Map;
 | 
				
			|||||||
@Data
 | 
					@Data
 | 
				
			||||||
@Slf4j
 | 
					@Slf4j
 | 
				
			||||||
@NoArgsConstructor
 | 
					@NoArgsConstructor
 | 
				
			||||||
 | 
					@EqualsAndHashCode(callSuper = true)
 | 
				
			||||||
public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
 | 
					public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public ScriptCalculatedFieldState(List<String> requiredArguments) {
 | 
					    public ScriptCalculatedFieldState(List<String> requiredArguments) {
 | 
				
			||||||
@ -49,10 +51,6 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
 | 
				
			|||||||
        return CalculatedFieldType.SCRIPT;
 | 
					        return CalculatedFieldType.SCRIPT;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    protected void validateNewEntry(ArgumentEntry newEntry) {
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx) {
 | 
					    public ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx) {
 | 
				
			||||||
        Map<String, TbelCfArg> arguments = new LinkedHashMap<>();
 | 
					        Map<String, TbelCfArg> arguments = new LinkedHashMap<>();
 | 
				
			||||||
 | 
				
			|||||||
@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
				
			|||||||
import com.google.common.util.concurrent.Futures;
 | 
					import com.google.common.util.concurrent.Futures;
 | 
				
			||||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
					import com.google.common.util.concurrent.ListenableFuture;
 | 
				
			||||||
import lombok.Data;
 | 
					import lombok.Data;
 | 
				
			||||||
 | 
					import lombok.EqualsAndHashCode;
 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
import org.thingsboard.common.util.JacksonUtil;
 | 
					import org.thingsboard.common.util.JacksonUtil;
 | 
				
			||||||
import org.thingsboard.script.api.tbel.TbUtils;
 | 
					import org.thingsboard.script.api.tbel.TbUtils;
 | 
				
			||||||
@ -34,6 +35,7 @@ import java.util.Map;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@Data
 | 
					@Data
 | 
				
			||||||
@NoArgsConstructor
 | 
					@NoArgsConstructor
 | 
				
			||||||
 | 
					@EqualsAndHashCode(callSuper = true)
 | 
				
			||||||
public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
 | 
					public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public SimpleCalculatedFieldState(List<String> requiredArguments) {
 | 
					    public SimpleCalculatedFieldState(List<String> requiredArguments) {
 | 
				
			||||||
 | 
				
			|||||||
@ -57,7 +57,7 @@
 | 
				
			|||||||
    <!--    <logger name="org.thingsboard.server.actors.device.DeviceActorMessageProcessor" level="DEBUG" />-->
 | 
					    <!--    <logger name="org.thingsboard.server.actors.device.DeviceActorMessageProcessor" level="DEBUG" />-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- CF actors message processors trace -->
 | 
					    <!-- CF actors message processors trace -->
 | 
				
			||||||
    <!--    <logger name="org.thingsboard.server.actors.calculatedField" level="TRACE" />-->
 | 
					    <logger name="org.thingsboard.server.actors.calculatedField" level="TRACE" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" />
 | 
					    <logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.cf.configuration.Argument;
 | 
				
			|||||||
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
 | 
					import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
 | 
					import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration;
 | 
					import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration;
 | 
				
			||||||
 | 
					import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.configuration.Output;
 | 
					import org.thingsboard.server.common.data.cf.configuration.Output;
 | 
				
			||||||
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.ReferencedEntityKey;
 | 
					import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
 | 
				
			||||||
@ -217,7 +218,6 @@ public class GeofencingCalculatedFieldStateTest {
 | 
				
			|||||||
        assertThat(state.isReady()).isFalse();
 | 
					        assertThat(state.isReady()).isFalse();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // TODO: test different reporting strategies
 | 
					 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    void testPerformCalculation() throws ExecutionException, InterruptedException {
 | 
					    void testPerformCalculation() throws ExecutionException, InterruptedException {
 | 
				
			||||||
        state.arguments = new HashMap<>(Map.of(
 | 
					        state.arguments = new HashMap<>(Map.of(
 | 
				
			||||||
@ -264,6 +264,147 @@ public class GeofencingCalculatedFieldStateTest {
 | 
				
			|||||||
                        .put("restrictedZonesStatus", "INSIDE")
 | 
					                        .put("restrictedZonesStatus", "INSIDE")
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Check relations are created and deleted correctly for both iterations.
 | 
				
			||||||
 | 
					        ArgumentCaptor<EntityRelation> saveCaptor = ArgumentCaptor.forClass(EntityRelation.class);
 | 
				
			||||||
 | 
					        verify(relationService, times(2)).saveRelationAsync(eq(ctx.getTenantId()), saveCaptor.capture());
 | 
				
			||||||
 | 
					        List<EntityRelation> saveValues = saveCaptor.getAllValues();
 | 
				
			||||||
 | 
					        assertThat(saveValues).hasSize(2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        EntityRelation relationFromFirstIteration = saveValues.get(0);
 | 
				
			||||||
 | 
					        assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId());
 | 
				
			||||||
 | 
					        assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID);
 | 
				
			||||||
 | 
					        assertThat(relationFromFirstIteration.getType()).isEqualTo(configuration.getZoneRelationType());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        EntityRelation relationFromSecondIteration = saveValues.get(1);
 | 
				
			||||||
 | 
					        assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId());
 | 
				
			||||||
 | 
					        assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID);
 | 
				
			||||||
 | 
					        assertThat(relationFromSecondIteration.getType()).isEqualTo(configuration.getZoneRelationType());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class);
 | 
				
			||||||
 | 
					        verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture());
 | 
				
			||||||
 | 
					        EntityRelation leftRelation = deleteCaptor.getValue();
 | 
				
			||||||
 | 
					        assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID);
 | 
				
			||||||
 | 
					        assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    void testPerformCalculationWithOnlyTransitionEventsReportingStrategy() throws ExecutionException, InterruptedException {
 | 
				
			||||||
 | 
					        state.arguments = new HashMap<>(Map.of(
 | 
				
			||||||
 | 
					                ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry,
 | 
				
			||||||
 | 
					                ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry,
 | 
				
			||||||
 | 
					                "allowedZones", geofencingAllowedZoneArgEntry,
 | 
				
			||||||
 | 
					                "restrictedZones", geofencingRestrictedZoneArgEntry
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Output output = ctx.getOutput();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var calculatedFieldConfig = getCalculatedFieldConfig(GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_ONLY);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ctx.setCalculatedField(getCalculatedField(calculatedFieldConfig));
 | 
				
			||||||
 | 
					        ctx.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
 | 
				
			||||||
 | 
					        when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assertThat(result).isNotNull();
 | 
				
			||||||
 | 
					        assertThat(result.getType()).isEqualTo(output.getType());
 | 
				
			||||||
 | 
					        assertThat(result.getScope()).isEqualTo(output.getScope());
 | 
				
			||||||
 | 
					        assertThat(result.getResult()).isEqualTo(
 | 
				
			||||||
 | 
					                JacksonUtil.newObjectNode().put("allowedZonesEvent", "ENTERED")
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L);
 | 
				
			||||||
 | 
					        SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // move the device to new coordinates → leaves allowed, enters restricted
 | 
				
			||||||
 | 
					        state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assertThat(result2).isNotNull();
 | 
				
			||||||
 | 
					        assertThat(result2.getType()).isEqualTo(output.getType());
 | 
				
			||||||
 | 
					        assertThat(result2.getScope()).isEqualTo(output.getScope());
 | 
				
			||||||
 | 
					        assertThat(result2.getResult()).isEqualTo(
 | 
				
			||||||
 | 
					                JacksonUtil.newObjectNode()
 | 
				
			||||||
 | 
					                        .put("allowedZonesEvent", "LEFT")
 | 
				
			||||||
 | 
					                        .put("restrictedZonesEvent", "ENTERED")
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Check relations are created and deleted correctly for both iterations.
 | 
				
			||||||
 | 
					        ArgumentCaptor<EntityRelation> saveCaptor = ArgumentCaptor.forClass(EntityRelation.class);
 | 
				
			||||||
 | 
					        verify(relationService, times(2)).saveRelationAsync(eq(ctx.getTenantId()), saveCaptor.capture());
 | 
				
			||||||
 | 
					        List<EntityRelation> saveValues = saveCaptor.getAllValues();
 | 
				
			||||||
 | 
					        assertThat(saveValues).hasSize(2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        EntityRelation relationFromFirstIteration = saveValues.get(0);
 | 
				
			||||||
 | 
					        assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId());
 | 
				
			||||||
 | 
					        assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID);
 | 
				
			||||||
 | 
					        assertThat(relationFromFirstIteration.getType()).isEqualTo(configuration.getZoneRelationType());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        EntityRelation relationFromSecondIteration = saveValues.get(1);
 | 
				
			||||||
 | 
					        assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId());
 | 
				
			||||||
 | 
					        assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID);
 | 
				
			||||||
 | 
					        assertThat(relationFromSecondIteration.getType()).isEqualTo(configuration.getZoneRelationType());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class);
 | 
				
			||||||
 | 
					        verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture());
 | 
				
			||||||
 | 
					        EntityRelation leftRelation = deleteCaptor.getValue();
 | 
				
			||||||
 | 
					        assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID);
 | 
				
			||||||
 | 
					        assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    void testPerformCalculationWithOnlyPresenceStatusReportingStrategy() throws ExecutionException, InterruptedException {
 | 
				
			||||||
 | 
					        state.arguments = new HashMap<>(Map.of(
 | 
				
			||||||
 | 
					                ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry,
 | 
				
			||||||
 | 
					                ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry,
 | 
				
			||||||
 | 
					                "allowedZones", geofencingAllowedZoneArgEntry,
 | 
				
			||||||
 | 
					                "restrictedZones", geofencingRestrictedZoneArgEntry
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Output output = ctx.getOutput();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var calculatedFieldConfig = getCalculatedFieldConfig(GeofencingReportStrategy.REPORT_PRESENCE_STATUS_ONLY);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ctx.setCalculatedField(getCalculatedField(calculatedFieldConfig));
 | 
				
			||||||
 | 
					        ctx.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
 | 
				
			||||||
 | 
					        when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assertThat(result).isNotNull();
 | 
				
			||||||
 | 
					        assertThat(result.getType()).isEqualTo(output.getType());
 | 
				
			||||||
 | 
					        assertThat(result.getScope()).isEqualTo(output.getScope());
 | 
				
			||||||
 | 
					        assertThat(result.getResult()).isEqualTo(
 | 
				
			||||||
 | 
					                JacksonUtil.newObjectNode()
 | 
				
			||||||
 | 
					                        .put("allowedZonesStatus", "INSIDE")
 | 
				
			||||||
 | 
					                        .put("restrictedZonesStatus", "OUTSIDE")
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L);
 | 
				
			||||||
 | 
					        SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // move the device to new coordinates → leaves allowed, enters restricted
 | 
				
			||||||
 | 
					        state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assertThat(result2).isNotNull();
 | 
				
			||||||
 | 
					        assertThat(result2.getType()).isEqualTo(output.getType());
 | 
				
			||||||
 | 
					        assertThat(result2.getScope()).isEqualTo(output.getScope());
 | 
				
			||||||
 | 
					        assertThat(result2.getResult()).isEqualTo(
 | 
				
			||||||
 | 
					                JacksonUtil.newObjectNode()
 | 
				
			||||||
 | 
					                        .put("allowedZonesStatus", "OUTSIDE")
 | 
				
			||||||
 | 
					                        .put("restrictedZonesStatus", "INSIDE")
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Check relations are created and deleted correctly for both iterations.
 | 
					        // Check relations are created and deleted correctly for both iterations.
 | 
				
			||||||
        ArgumentCaptor<EntityRelation> saveCaptor = ArgumentCaptor.forClass(EntityRelation.class);
 | 
					        ArgumentCaptor<EntityRelation> saveCaptor = ArgumentCaptor.forClass(EntityRelation.class);
 | 
				
			||||||
@ -289,18 +430,22 @@ public class GeofencingCalculatedFieldStateTest {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private CalculatedField getCalculatedField() {
 | 
					    private CalculatedField getCalculatedField() {
 | 
				
			||||||
 | 
					        return getCalculatedField(getCalculatedFieldConfig(REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private CalculatedField getCalculatedField(CalculatedFieldConfiguration configuration) {
 | 
				
			||||||
        CalculatedField calculatedField = new CalculatedField();
 | 
					        CalculatedField calculatedField = new CalculatedField();
 | 
				
			||||||
        calculatedField.setTenantId(TENANT_ID);
 | 
					        calculatedField.setTenantId(TENANT_ID);
 | 
				
			||||||
        calculatedField.setEntityId(DEVICE_ID);
 | 
					        calculatedField.setEntityId(DEVICE_ID);
 | 
				
			||||||
        calculatedField.setType(CalculatedFieldType.GEOFENCING);
 | 
					        calculatedField.setType(CalculatedFieldType.GEOFENCING);
 | 
				
			||||||
        calculatedField.setName("Test Geofencing Calculated Field");
 | 
					        calculatedField.setName("Test Geofencing Calculated Field");
 | 
				
			||||||
        calculatedField.setConfigurationVersion(1);
 | 
					        calculatedField.setConfigurationVersion(1);
 | 
				
			||||||
        calculatedField.setConfiguration(getCalculatedFieldConfig());
 | 
					        calculatedField.setConfiguration(configuration);
 | 
				
			||||||
        calculatedField.setVersion(1L);
 | 
					        calculatedField.setVersion(1L);
 | 
				
			||||||
        return calculatedField;
 | 
					        return calculatedField;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private CalculatedFieldConfiguration getCalculatedFieldConfig() {
 | 
					    private CalculatedFieldConfiguration getCalculatedFieldConfig(GeofencingReportStrategy reportStrategy) {
 | 
				
			||||||
        var config = new GeofencingCalculatedFieldConfiguration();
 | 
					        var config = new GeofencingCalculatedFieldConfiguration();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Argument argument1 = new Argument();
 | 
					        Argument argument1 = new Argument();
 | 
				
			||||||
@ -335,7 +480,7 @@ public class GeofencingCalculatedFieldStateTest {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        config.setArguments(Map.of("latitude", argument1, "longitude", argument2, "allowedZones", argument3, "restrictedZones", argument4));
 | 
					        config.setArguments(Map.of("latitude", argument1, "longitude", argument2, "allowedZones", argument3, "restrictedZones", argument4));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        config.setZoneGroupReportStrategies(Map.of("allowedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, "restrictedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS));
 | 
					        config.setZoneGroupReportStrategies(Map.of("allowedZones", reportStrategy, "restrictedZones", reportStrategy));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        config.setCreateRelationsWithMatchedZones(true);
 | 
					        config.setCreateRelationsWithMatchedZones(true);
 | 
				
			||||||
        config.setZoneRelationType("CurrentZone");
 | 
					        config.setZoneRelationType("CurrentZone");
 | 
				
			||||||
 | 
				
			|||||||
@ -84,4 +84,84 @@ public class GeofencingZoneStateTest {
 | 
				
			|||||||
        assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE));
 | 
					        assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    void update_withNewerVersion_updatesState_andResetsPresence() {
 | 
				
			||||||
 | 
					        // arrange: establish a prior presence to ensure it’s reset on update
 | 
				
			||||||
 | 
					        var inside = new Coordinates(50.4730, 30.5050);
 | 
				
			||||||
 | 
					        assertThat(state.evaluate(inside)).isNotNull(); // sets lastPresence internally
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        String NEW_POLYGON = "[[50.470000, 30.502000], [50.470000, 30.503000], [50.471000, 30.503000], [50.471000, 30.502000]]";
 | 
				
			||||||
 | 
					        GeofencingZoneState newer = new GeofencingZoneState(
 | 
				
			||||||
 | 
					                ZONE_ID,
 | 
				
			||||||
 | 
					                new BaseAttributeKvEntry(new JsonDataEntry("zone", NEW_POLYGON), 200L, 2L)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // act
 | 
				
			||||||
 | 
					        boolean changed = state.update(newer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // assert
 | 
				
			||||||
 | 
					        assertThat(changed).isTrue();
 | 
				
			||||||
 | 
					        assertThat(state.getTs()).isEqualTo(200L);
 | 
				
			||||||
 | 
					        assertThat(state.getVersion()).isEqualTo(2L);
 | 
				
			||||||
 | 
					        assertThat(state.getPerimeterDefinition()).isNotNull();
 | 
				
			||||||
 | 
					        assertThat(state.getLastPresence()).isNull(); // must be reset on successful update
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    void update_withEqualVersion_doesNothing() {
 | 
				
			||||||
 | 
					        // arrange: same version (1L) but different ts/polygon should still be ignored
 | 
				
			||||||
 | 
					        String SOME_POLYGON = "[[50.472500, 30.504500], [50.472500, 30.505500], [50.473500, 30.505500], [50.473500, 30.504500]]";
 | 
				
			||||||
 | 
					        GeofencingZoneState sameVersion = new GeofencingZoneState(
 | 
				
			||||||
 | 
					                ZONE_ID,
 | 
				
			||||||
 | 
					                new BaseAttributeKvEntry(new JsonDataEntry("zone", SOME_POLYGON), 300L, 1L)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // act
 | 
				
			||||||
 | 
					        boolean changed = state.update(sameVersion);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // assert: nothing changes
 | 
				
			||||||
 | 
					        assertThat(changed).isFalse();
 | 
				
			||||||
 | 
					        assertThat(state.getTs()).isEqualTo(100L);
 | 
				
			||||||
 | 
					        assertThat(state.getVersion()).isEqualTo(1L);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    void update_withNullNewVersion_alwaysApplies_andCopiesNull() {
 | 
				
			||||||
 | 
					        // arrange: the implementation updates if newVersion == null
 | 
				
			||||||
 | 
					        String OTHER_POLYGON = "[[50.471000, 30.506000], [50.471000, 30.507000], [50.472000, 30.507000], [50.472000, 30.506000]]";
 | 
				
			||||||
 | 
					        GeofencingZoneState nullVersion = new GeofencingZoneState(
 | 
				
			||||||
 | 
					                ZONE_ID,
 | 
				
			||||||
 | 
					                new BaseAttributeKvEntry(new JsonDataEntry("zone", OTHER_POLYGON), 400L, null)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // act
 | 
				
			||||||
 | 
					        boolean changed = state.update(nullVersion);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // assert: applied and version copied as null
 | 
				
			||||||
 | 
					        assertThat(changed).isTrue();
 | 
				
			||||||
 | 
					        assertThat(state.getTs()).isEqualTo(400L);
 | 
				
			||||||
 | 
					        assertThat(state.getVersion()).isNull();
 | 
				
			||||||
 | 
					        assertThat(state.getLastPresence()).isNull();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    void update_withNewVersionWhenExistingIsNull_alwaysApplies_andCopiesNew() {
 | 
				
			||||||
 | 
					        // arrange: the implementation updates if newVersion == null
 | 
				
			||||||
 | 
					        String OTHER_POLYGON = "[[50.471000, 30.506000], [50.471000, 30.507000], [50.472000, 30.507000], [50.472000, 30.506000]]";
 | 
				
			||||||
 | 
					        GeofencingZoneState newVersion = new GeofencingZoneState(
 | 
				
			||||||
 | 
					                ZONE_ID,
 | 
				
			||||||
 | 
					                new BaseAttributeKvEntry(new JsonDataEntry("zone", OTHER_POLYGON), 400L, 2L)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        state.setVersion(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // act
 | 
				
			||||||
 | 
					        boolean changed = state.update(newVersion);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // assert: applied and version copied as null
 | 
				
			||||||
 | 
					        assertThat(changed).isTrue();
 | 
				
			||||||
 | 
					        assertThat(state.getTs()).isEqualTo(400L);
 | 
				
			||||||
 | 
					        assertThat(state.getVersion()).isEqualTo(2);
 | 
				
			||||||
 | 
					        assertThat(state.getLastPresence()).isNull();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -35,4 +35,4 @@ public class ArgumentTest {
 | 
				
			|||||||
        assertThat(argument.hasDynamicSource()).isTrue();
 | 
					        assertThat(argument.hasDynamicSource()).isTrue();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user