9 changed files with 494 additions and 21 deletions
@ -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(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue