Added mock tests for Geofencing CF state and utils logic to/from proto
This commit is contained in:
parent
baba433f0f
commit
efc20a93aa
@ -49,7 +49,7 @@ import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalc
|
|||||||
public class GeofencingCalculatedFieldState implements CalculatedFieldState {
|
public class GeofencingCalculatedFieldState implements CalculatedFieldState {
|
||||||
|
|
||||||
private List<String> requiredArguments;
|
private List<String> requiredArguments;
|
||||||
private Map<String, ArgumentEntry> arguments;
|
Map<String, ArgumentEntry> arguments;
|
||||||
private boolean sizeExceedsLimit;
|
private boolean sizeExceedsLimit;
|
||||||
|
|
||||||
private long latestTimestamp = -1;
|
private long latestTimestamp = -1;
|
||||||
@ -91,14 +91,16 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState {
|
|||||||
entryUpdated = switch (key) {
|
entryUpdated = switch (key) {
|
||||||
case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
|
case ENTITY_ID_LATITUDE_ARGUMENT_KEY, 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("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
|
||||||
|
"Only SINGLE_VALUE type is allowed.");
|
||||||
}
|
}
|
||||||
arguments.put(key, singleValueArgumentEntry);
|
arguments.put(key, singleValueArgumentEntry);
|
||||||
yield true;
|
yield true;
|
||||||
}
|
}
|
||||||
default -> {
|
default -> {
|
||||||
if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
|
if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
|
||||||
throw new IllegalArgumentException(key + " argument must be a geofencing argument entry.");
|
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
|
||||||
|
"Only GEOFENCING type is allowed.");
|
||||||
}
|
}
|
||||||
arguments.put(key, geofencingArgumentEntry);
|
arguments.put(key, geofencingArgumentEntry);
|
||||||
yield true;
|
yield true;
|
||||||
|
|||||||
@ -25,7 +25,6 @@ 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;
|
||||||
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.gen.transport.TransportProtos.GeofencingZoneProto;
|
import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -44,13 +43,11 @@ public class GeofencingZoneState {
|
|||||||
|
|
||||||
public GeofencingZoneState(EntityId zoneId, KvEntry entry) {
|
public GeofencingZoneState(EntityId zoneId, KvEntry entry) {
|
||||||
this.zoneId = zoneId;
|
this.zoneId = zoneId;
|
||||||
if (entry instanceof TsKvEntry tsKvEntry) {
|
if (!(entry instanceof AttributeKvEntry attributeKvEntry)) {
|
||||||
this.ts = tsKvEntry.getTs();
|
throw new IllegalArgumentException("Unsupported KvEntry type for geofencing zone state: " + entry.getClass().getSimpleName());
|
||||||
this.version = tsKvEntry.getVersion();
|
|
||||||
} else if (entry instanceof AttributeKvEntry attributeKvEntry) {
|
|
||||||
this.ts = attributeKvEntry.getLastUpdateTs();
|
|
||||||
this.version = attributeKvEntry.getVersion();
|
|
||||||
}
|
}
|
||||||
|
this.ts = attributeKvEntry.getLastUpdateTs();
|
||||||
|
this.version = attributeKvEntry.getVersion();
|
||||||
this.perimeterDefinition = JacksonUtil.fromString(entry.getJsonValue().orElseThrow(), PerimeterDefinition.class);
|
this.perimeterDefinition = JacksonUtil.fromString(entry.getJsonValue().orElseThrow(), PerimeterDefinition.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -121,7 +121,6 @@ public class CalculatedFieldUtils {
|
|||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static GeofencingArgumentProto toGeofencingArgumentProto(String argName, GeofencingArgumentEntry geofencingArgumentEntry) {
|
private static GeofencingArgumentProto toGeofencingArgumentProto(String argName, GeofencingArgumentEntry geofencingArgumentEntry) {
|
||||||
Map<EntityId, GeofencingZoneState> zoneStates = geofencingArgumentEntry.getZoneStates();
|
Map<EntityId, GeofencingZoneState> zoneStates = geofencingArgumentEntry.getZoneStates();
|
||||||
GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder()
|
GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder()
|
||||||
|
|||||||
@ -0,0 +1,360 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.service.cf.ctx.state;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.thingsboard.common.util.JacksonUtil;
|
||||||
|
import org.thingsboard.server.common.data.cf.CalculatedField;
|
||||||
|
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.Argument;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.Output;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.OutputType;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
|
||||||
|
import org.thingsboard.server.common.data.id.AssetId;
|
||||||
|
import org.thingsboard.server.common.data.id.DeviceId;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
|
||||||
|
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
|
||||||
|
import org.thingsboard.server.common.data.kv.JsonDataEntry;
|
||||||
|
import org.thingsboard.server.common.data.relation.EntityRelation;
|
||||||
|
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
|
||||||
|
import org.thingsboard.server.dao.relation.RelationService;
|
||||||
|
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
|
||||||
|
import org.thingsboard.server.service.cf.CalculatedFieldResult;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY;
|
||||||
|
import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class GeofencingCalculatedFieldStateTest {
|
||||||
|
|
||||||
|
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("8f83eeca-b5cd-4955-9241-09d1393768c6"));
|
||||||
|
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("688b529d-cfbe-4430-91c5-60b4f4e5d3cf"));
|
||||||
|
private final AssetId ZONE_1_ID = new AssetId(UUID.fromString("c0e3031c-7df1-45e4-9590-cfd621a4d714"));
|
||||||
|
private final AssetId ZONE_2_ID = new AssetId(UUID.fromString("e7da6200-2096-4038-a343-ade9ea4fa3e4"));
|
||||||
|
|
||||||
|
private final SingleValueArgumentEntry latitudeArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("latitude", 50.4730), 145L);
|
||||||
|
private final SingleValueArgumentEntry longitudeArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 6, new DoubleDataEntry("longitude", 30.5050), 165L);
|
||||||
|
|
||||||
|
private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", """
|
||||||
|
{"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"}""");
|
||||||
|
private final BaseAttributeKvEntry allowedZoneAttributeKvEntry = new BaseAttributeKvEntry(allowedZoneDataEntry, System.currentTimeMillis(), 0L);
|
||||||
|
private final GeofencingArgumentEntry geofencingAllowedZoneArgEntry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry));
|
||||||
|
|
||||||
|
private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", """
|
||||||
|
{"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"}""");
|
||||||
|
private final BaseAttributeKvEntry restrictedZoneAttributeKvEntry = new BaseAttributeKvEntry(restrictedZoneDataEntry, System.currentTimeMillis(), 0L);
|
||||||
|
private final GeofencingArgumentEntry geofencingRestrictedZoneArgEntry = new GeofencingArgumentEntry(Map.of(ZONE_2_ID, restrictedZoneAttributeKvEntry));
|
||||||
|
|
||||||
|
|
||||||
|
private GeofencingCalculatedFieldState state;
|
||||||
|
private CalculatedFieldCtx ctx;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApiLimitService apiLimitService;
|
||||||
|
@Mock
|
||||||
|
private RelationService relationService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
when(apiLimitService.getLimit(any(), any())).thenReturn(1000L);
|
||||||
|
ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService, relationService);
|
||||||
|
ctx.init();
|
||||||
|
state = new GeofencingCalculatedFieldState(ctx.getArgNames());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testType() {
|
||||||
|
assertThat(state.getType()).isEqualTo(CalculatedFieldType.GEOFENCING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateState() {
|
||||||
|
state.arguments = new HashMap<>(Map.of(
|
||||||
|
ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry,
|
||||||
|
ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry
|
||||||
|
));
|
||||||
|
|
||||||
|
Map<String, ArgumentEntry> newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry);
|
||||||
|
boolean stateUpdated = state.updateState(ctx, newArgs);
|
||||||
|
|
||||||
|
assertThat(stateUpdated).isTrue();
|
||||||
|
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
|
||||||
|
Map.of(
|
||||||
|
ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry,
|
||||||
|
ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry,
|
||||||
|
"allowedZones", geofencingAllowedZoneArgEntry
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateStateWithInvalidArgumentTypeForLatitudeArgument() {
|
||||||
|
assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry)))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("Unsupported argument entry type for latitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateStateWithInvalidArgumentTypeForLongitudeArgument() {
|
||||||
|
assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry)))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("Unsupported argument entry type for longitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateStateWithInvalidArgumentTypeForGeofencingArgument() {
|
||||||
|
assertThatThrownBy(() -> state.updateState(ctx, Map.of("someArgumentName", latitudeArgEntry)))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("Unsupported argument entry type for someArgumentName argument: SINGLE_VALUE. Only GEOFENCING type is allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateStateWhenUpdateExistingSingleValueArgumentEntry() {
|
||||||
|
state.arguments = new HashMap<>(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry));
|
||||||
|
|
||||||
|
SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 190L);
|
||||||
|
Map<String, ArgumentEntry> newArgs = Map.of("latitude", newArgEntry);
|
||||||
|
boolean stateUpdated = state.updateState(ctx, newArgs);
|
||||||
|
|
||||||
|
assertThat(stateUpdated).isTrue();
|
||||||
|
assertThat(state.getArguments()).isEqualTo(newArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: write opposite test for this. See TODO in the GeofencingZoneState class.
|
||||||
|
@Test
|
||||||
|
void testUpdateStateWhenUpdateExistingGeofencingValueArgumentEntryWithTheSameValue() {
|
||||||
|
state.arguments = new HashMap<>(Map.of("allowedZones", geofencingAllowedZoneArgEntry));
|
||||||
|
|
||||||
|
Map<String, ArgumentEntry> newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry);
|
||||||
|
|
||||||
|
boolean stateUpdated = state.updateState(ctx, newArgs);
|
||||||
|
|
||||||
|
assertThat(stateUpdated).isFalse();
|
||||||
|
assertThat(state.getArguments()).isEqualTo(newArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateStateWhenUpdateExistingSingleValueArgumentEntryWithValueOfAnotherType() {
|
||||||
|
state.arguments = new HashMap<>(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry)))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("Unsupported argument entry type for single value argument entry: GEOFENCING");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateStateWhenUpdateExistingGeofencingValueArgumentEntryWithValueOfAnotherType() {
|
||||||
|
state.arguments = new HashMap<>(Map.of("allowedZones", geofencingAllowedZoneArgEntry));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> state.updateState(ctx, Map.of("allowedZones", latitudeArgEntry)))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsReadyWhenNotAllArgPresent() {
|
||||||
|
assertThat(state.isReady()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsReadyWhenAllArgPresent() {
|
||||||
|
state.arguments = new HashMap<>(Map.of(
|
||||||
|
ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry,
|
||||||
|
ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry,
|
||||||
|
"allowedZones", geofencingAllowedZoneArgEntry,
|
||||||
|
"restrictedZones", geofencingRestrictedZoneArgEntry
|
||||||
|
));
|
||||||
|
assertThat(state.isReady()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsReadyWhenEmptyEntryPresents() {
|
||||||
|
state.arguments = new HashMap<>(Map.of(
|
||||||
|
ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry,
|
||||||
|
ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry,
|
||||||
|
"allowedZones", geofencingAllowedZoneArgEntry,
|
||||||
|
"restrictedZones", geofencingRestrictedZoneArgEntry
|
||||||
|
));
|
||||||
|
|
||||||
|
state.getArguments().put("noParkingZones", new GeofencingArgumentEntry());
|
||||||
|
|
||||||
|
assertThat(state.isReady()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPerformCalculation() 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 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("allowedZoneEvent", "ENTERED")
|
||||||
|
.put("restrictedZoneEvent", "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("allowedZoneEvent", "LEFT")
|
||||||
|
.put("restrictedZoneEvent", "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());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CalculatedField getCalculatedField() {
|
||||||
|
CalculatedField calculatedField = new CalculatedField();
|
||||||
|
calculatedField.setTenantId(TENANT_ID);
|
||||||
|
calculatedField.setEntityId(DEVICE_ID);
|
||||||
|
calculatedField.setType(CalculatedFieldType.GEOFENCING);
|
||||||
|
calculatedField.setName("Test Geofencing Calculated Field");
|
||||||
|
calculatedField.setConfigurationVersion(1);
|
||||||
|
calculatedField.setConfiguration(getCalculatedFieldConfig());
|
||||||
|
calculatedField.setVersion(1L);
|
||||||
|
return calculatedField;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CalculatedFieldConfiguration getCalculatedFieldConfig() {
|
||||||
|
var config = new GeofencingCalculatedFieldConfiguration();
|
||||||
|
|
||||||
|
Argument argument1 = new Argument();
|
||||||
|
argument1.setRefEntityId(DEVICE_ID);
|
||||||
|
var refEntityKey1 = new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null);
|
||||||
|
argument1.setRefEntityKey(refEntityKey1);
|
||||||
|
|
||||||
|
Argument argument2 = new Argument();
|
||||||
|
argument2.setRefEntityId(DEVICE_ID);
|
||||||
|
var refEntityKey2 = new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null);
|
||||||
|
argument2.setRefEntityKey(refEntityKey2);
|
||||||
|
|
||||||
|
Argument argument3 = new Argument();
|
||||||
|
var refEntityKey3 = new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, null);
|
||||||
|
var refDynamicSourceConfiguration3 = new RelationQueryDynamicSourceConfiguration();
|
||||||
|
refDynamicSourceConfiguration3.setDirection(EntitySearchDirection.TO);
|
||||||
|
refDynamicSourceConfiguration3.setRelationType("AllowedZone");
|
||||||
|
refDynamicSourceConfiguration3.setMaxLevel(1);
|
||||||
|
refDynamicSourceConfiguration3.setFetchLastLevelOnly(true);
|
||||||
|
argument3.setRefEntityKey(refEntityKey3);
|
||||||
|
argument3.setRefDynamicSource(CFArgumentDynamicSourceType.RELATION_QUERY);
|
||||||
|
argument3.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration3);
|
||||||
|
|
||||||
|
Argument argument4 = new Argument();
|
||||||
|
var refEntityKey4 = new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, null);
|
||||||
|
var refDynamicSourceConfiguration4 = new RelationQueryDynamicSourceConfiguration();
|
||||||
|
refDynamicSourceConfiguration4.setDirection(EntitySearchDirection.TO);
|
||||||
|
refDynamicSourceConfiguration4.setRelationType("RestrictedZone");
|
||||||
|
refDynamicSourceConfiguration4.setMaxLevel(1);
|
||||||
|
refDynamicSourceConfiguration4.setFetchLastLevelOnly(true);
|
||||||
|
argument4.setRefEntityKey(refEntityKey4);
|
||||||
|
argument4.setRefDynamicSource(CFArgumentDynamicSourceType.RELATION_QUERY);
|
||||||
|
argument4.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration4);
|
||||||
|
|
||||||
|
config.setArguments(Map.of("latitude", argument1, "longitude", argument2, "allowedZones", argument3, "restrictedZones", argument4));
|
||||||
|
|
||||||
|
List<GeofencingEvent> reportEvents = Arrays.stream(GeofencingEvent.values()).toList();
|
||||||
|
GeofencingZoneGroupConfiguration allowedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents);
|
||||||
|
GeofencingZoneGroupConfiguration restrictedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents);
|
||||||
|
config.setGeofencingZoneGroupConfigurations(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration));
|
||||||
|
|
||||||
|
config.setCreateRelationsWithMatchedZones(true);
|
||||||
|
config.setZoneRelationType("CurrentZone");
|
||||||
|
config.setZoneRelationDirection(EntitySearchDirection.TO);
|
||||||
|
|
||||||
|
// TODO: Does CF possible to save with null?
|
||||||
|
config.setExpression("latitude + longitude");
|
||||||
|
|
||||||
|
Output output = new Output();
|
||||||
|
output.setType(OutputType.TIME_SERIES);
|
||||||
|
config.setOutput(output);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* 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.utils;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.thingsboard.server.common.data.id.AssetId;
|
||||||
|
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
||||||
|
import org.thingsboard.server.common.data.id.DeviceId;
|
||||||
|
import org.thingsboard.server.common.data.id.EntityId;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
|
||||||
|
import org.thingsboard.server.common.data.kv.JsonDataEntry;
|
||||||
|
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
|
||||||
|
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
|
||||||
|
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
|
||||||
|
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
|
||||||
|
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
|
||||||
|
import org.thingsboard.server.service.cf.ctx.state.GeofencingArgumentEntry;
|
||||||
|
import org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState;
|
||||||
|
import org.thingsboard.server.service.cf.ctx.state.GeofencingZoneState;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CalculatedFieldUtilsTest {
|
||||||
|
|
||||||
|
private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("0a69e1e2-fcbc-4234-a4cd-3844bf54035c"));
|
||||||
|
private static final CalculatedFieldId CF_ID = CalculatedFieldId.fromString("ec0e91b9-6f27-4e93-946a-5fbc2707d8bc");
|
||||||
|
private static final DeviceId DEVICE_ID = DeviceId.fromString("1e03bd38-2010-4739-9362-160c288e36c4");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toProtoAndFromProto_shouldMapGeofencingArgumentsAndZones() {
|
||||||
|
// given
|
||||||
|
CalculatedFieldEntityCtxId stateId = mock(CalculatedFieldEntityCtxId.class);
|
||||||
|
given(stateId.tenantId()).willReturn(TENANT_ID);
|
||||||
|
given(stateId.cfId()).willReturn(CF_ID);
|
||||||
|
given(stateId.entityId()).willReturn(DEVICE_ID);
|
||||||
|
|
||||||
|
// Build a geofencing argument with two zones (one with inside=true, one with inside=null)
|
||||||
|
GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry();
|
||||||
|
Map<EntityId, GeofencingZoneState> zoneStates = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
UUID zoneId1 = UUID.fromString("624a8fff-71a2-4847-a100-ff1cf52dbe71");
|
||||||
|
UUID zoneId2 = UUID.fromString("e2adf6ce-9478-40b1-b0e9-4a6860cc46bb");
|
||||||
|
|
||||||
|
AssetId z1 = new AssetId(zoneId1);
|
||||||
|
AssetId z2 = new AssetId(zoneId2);
|
||||||
|
|
||||||
|
JsonDataEntry zone1 = new JsonDataEntry("zone", "{\"type\":\"POLYGON\",\"polygonsDefinition\":\"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]\"}");
|
||||||
|
JsonDataEntry zone2 = new JsonDataEntry("zone", "{\"type\":\"POLYGON\",\"polygonsDefinition\":\"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]\"}");
|
||||||
|
|
||||||
|
BaseAttributeKvEntry zone1PerimeterAttribute = new BaseAttributeKvEntry(zone1, System.currentTimeMillis(), 0L);
|
||||||
|
BaseAttributeKvEntry zone2PerimeterAttribute = new BaseAttributeKvEntry(zone2, System.currentTimeMillis(), 0L);
|
||||||
|
|
||||||
|
GeofencingZoneState s1 = new GeofencingZoneState(z1, zone1PerimeterAttribute);
|
||||||
|
s1.setInside(true);
|
||||||
|
GeofencingZoneState s2 = new GeofencingZoneState(z2, zone2PerimeterAttribute);
|
||||||
|
|
||||||
|
zoneStates.put(z1, s1);
|
||||||
|
zoneStates.put(z2, s2);
|
||||||
|
geofencingArgumentEntry.setZoneStates(zoneStates);
|
||||||
|
|
||||||
|
// Create cf state with the geofencing argument and add it to the state map
|
||||||
|
CalculatedFieldState state = new GeofencingCalculatedFieldState(List.of("geofencingArgumentTest"));
|
||||||
|
state.updateState(mock(CalculatedFieldCtx.class), Map.of("geofencingArgumentTest", geofencingArgumentEntry));
|
||||||
|
|
||||||
|
// when
|
||||||
|
CalculatedFieldStateProto proto = toProto(stateId, state);
|
||||||
|
|
||||||
|
// then
|
||||||
|
CalculatedFieldState fromProto = CalculatedFieldUtils.fromProto(proto);
|
||||||
|
assertThat(fromProto)
|
||||||
|
.usingRecursiveComparison()
|
||||||
|
.ignoringFields("requiredArguments")
|
||||||
|
.isEqualTo(state);
|
||||||
|
|
||||||
|
ArgumentEntry fromProtoArgument = fromProto.getArguments().get("geofencingArgumentTest");
|
||||||
|
assertThat(fromProtoArgument).isInstanceOf(GeofencingArgumentEntry.class);
|
||||||
|
GeofencingArgumentEntry fromProtoGeoArgument = (GeofencingArgumentEntry) fromProtoArgument;
|
||||||
|
assertThat(fromProtoGeoArgument.getZoneStates()).hasSize(2);
|
||||||
|
assertThat(fromProtoGeoArgument.getZoneStates().get(z1).getInside()).isTrue();
|
||||||
|
assertThat(fromProtoGeoArgument.getZoneStates().get(z2).getInside()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -33,8 +33,6 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel
|
|||||||
protected String expression;
|
protected String expression;
|
||||||
protected Output output;
|
protected Output output;
|
||||||
|
|
||||||
protected int scheduledUpdateIntervalSec;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<EntityId> getReferencedEntities() {
|
public List<EntityId> getReferencedEntities() {
|
||||||
return arguments.values().stream()
|
return arguments.values().stream()
|
||||||
@ -71,8 +69,4 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isScheduledUpdateEnabled() {
|
|
||||||
return scheduledUpdateIntervalSec > 0 && arguments.values().stream().anyMatch(arg -> arg.getRefDynamicSource() != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,8 +66,13 @@ public interface CalculatedFieldConfiguration {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setScheduledUpdateIntervalSec(int scheduledUpdateIntervalSec);
|
@JsonIgnore
|
||||||
|
default void setScheduledUpdateIntervalSec(int scheduledUpdateIntervalSec) {
|
||||||
|
}
|
||||||
|
|
||||||
int getScheduledUpdateIntervalSec();
|
@JsonIgnore
|
||||||
|
default int getScheduledUpdateIntervalSec() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,11 +36,13 @@ 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 Set<String> coordinateKeys = Set.of(
|
private static final Set<String> coordinateKeys = Set.of(
|
||||||
ENTITY_ID_LATITUDE_ARGUMENT_KEY,
|
ENTITY_ID_LATITUDE_ARGUMENT_KEY,
|
||||||
ENTITY_ID_LONGITUDE_ARGUMENT_KEY
|
ENTITY_ID_LONGITUDE_ARGUMENT_KEY
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private int scheduledUpdateIntervalSec;
|
||||||
|
|
||||||
private boolean createRelationsWithMatchedZones;
|
private boolean createRelationsWithMatchedZones;
|
||||||
private String zoneRelationType;
|
private String zoneRelationType;
|
||||||
private EntitySearchDirection zoneRelationDirection;
|
private EntitySearchDirection zoneRelationDirection;
|
||||||
@ -51,6 +53,11 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC
|
|||||||
return CalculatedFieldType.GEOFENCING;
|
return CalculatedFieldType.GEOFENCING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isScheduledUpdateEnabled() {
|
||||||
|
return scheduledUpdateIntervalSec > 0 && arguments.values().stream().anyMatch(arg -> arg.getRefDynamicSource() != null);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: update validate method in PE version.
|
// TODO: update validate method in PE version.
|
||||||
@Override
|
@Override
|
||||||
public void validate() {
|
public void validate() {
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
|
|||||||
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
|
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
|
||||||
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
|
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
|
||||||
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
|
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
|
||||||
|
import org.thingsboard.server.common.data.util.CollectionsUtil;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -56,7 +57,7 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSimpleRelation() {
|
public boolean isSimpleRelation() {
|
||||||
return maxLevel == 1 && (entityTypes == null || entityTypes.isEmpty());
|
return maxLevel == 1 && CollectionsUtil.isEmpty(entityTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user