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