TbGpsGeofencingActionNode: renamed key to reportPresenceStatusOnEachMessage + removed told variable + tests refactored

This commit is contained in:
artem 2024-01-24 18:28:17 +02:00
parent f27ce0686e
commit 167d8758f6
4 changed files with 120 additions and 85 deletions

View File

@ -62,14 +62,16 @@ import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.OUTSIDE;
"<br><br>" +
"If an object with coordinates extracted from incoming message enters the geofence, sends a message with the type <code>Entered</code>. " +
"If an object leaves the geofence, sends a message with the type <code>Left</code>. " +
"If the presence monitoring strategy <b>\"On first message\"</b> is selected, sends messages with types <code>Inside</code> or <code>Outside</code> only the first time the geofencing and duration conditions are satisfied; otherwise <code>Success</code>. " +
"If the presence monitoring strategy <b>\"On each message\"</b> is selected, sends messages with types <code>Inside</code> or <code>Outside</code> every time the geofencing condition is satisfied.",
"If the presence monitoring strategy <b>\"On first message\"</b> is selected, sends messages via rule node connection type <code>Inside</code> or <code>Outside</code> only the first time the geofencing and duration conditions are satisfied; otherwise sends messages via rule node connection type <code>Success</code>. " +
"If the presence monitoring strategy <b>\"On each message\"</b> is selected, sends messages via rule node connection type <code>Inside</code> or <code>Outside</code> every time the geofencing condition is satisfied. " +
"<br><br>" +
"Output connections: <code>Entered</code>, <code>Left</code>, <code>Inside</code>, <code>Outside</code>, <code>Success</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeGpsGeofencingConfig"
)
public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofencingActionNodeConfiguration> {
private static final String PRESENCE_MONITORING_STRATEGY_ON_EACH_MESSAGE = "presenceMonitoringStrategyOnEachMessage";
private static final String REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE = "reportPresenceStatusOnEachMessage";
private final Map<EntityId, EntityGeofencingState> entityStates = new HashMap<>();
private final Gson gson = new Gson();
private final JsonParser parser = new JsonParser();
@ -95,28 +97,32 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofe
}
});
boolean told = false;
if (entityState.getStateSwitchTime() == 0L || entityState.isInside() != matches) {
switchState(ctx, msg.getOriginator(), entityState, matches, ts);
ctx.tellNext(msg, matches ? ENTERED : LEFT);
told = true;
} else if (!config.isPresenceMonitoringStrategyOnEachMessage()) {
if (!entityState.isStayed()) {
long stayTime = ts - entityState.getStateSwitchTime();
if (stayTime > (entityState.isInside() ?
TimeUnit.valueOf(config.getMinInsideDurationTimeUnit()).toMillis(config.getMinInsideDuration()) : TimeUnit.valueOf(config.getMinOutsideDurationTimeUnit()).toMillis(config.getMinOutsideDuration()))) {
setStaid(ctx, msg.getOriginator(), entityState);
ctx.tellNext(msg, entityState.isInside() ? INSIDE : OUTSIDE);
told = true;
}
}
} else {
return;
}
if (config.isReportPresenceStatusOnEachMessage()) {
ctx.tellNext(msg, entityState.isInside() ? INSIDE : OUTSIDE);
told = true;
return;
}
if (!told) {
if (entityState.isStayed()) {
ctx.tellSuccess(msg);
return;
}
long stayTime = ts - entityState.getStateSwitchTime();
if (stayTime > (entityState.isInside() ?
TimeUnit.valueOf(config.getMinInsideDurationTimeUnit()).toMillis(config.getMinInsideDuration()) :
TimeUnit.valueOf(config.getMinOutsideDurationTimeUnit()).toMillis(config.getMinOutsideDuration()))) {
setStaid(ctx, msg.getOriginator(), entityState);
ctx.tellNext(msg, entityState.isInside() ? INSIDE : OUTSIDE);
return;
}
ctx.tellSuccess(msg);
}
private void switchState(TbContext ctx, EntityId entityId, EntityGeofencingState entityState, boolean matches, long ts) {
@ -150,9 +156,9 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofe
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
boolean hasChanges = false;
if (fromVersion == 0) {
if (!oldConfiguration.has(PRESENCE_MONITORING_STRATEGY_ON_EACH_MESSAGE)) {
if (!oldConfiguration.has(REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE)) {
hasChanges = true;
((ObjectNode) oldConfiguration).put(PRESENCE_MONITORING_STRATEGY_ON_EACH_MESSAGE, false);
((ObjectNode) oldConfiguration).put(REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE, false);
}
}
return new TbPair<>(hasChanges, oldConfiguration);

View File

@ -31,7 +31,7 @@ public class TbGpsGeofencingActionNodeConfiguration extends TbGpsGeofencingFilte
private String minInsideDurationTimeUnit;
private String minOutsideDurationTimeUnit;
private boolean presenceMonitoringStrategyOnEachMessage;
private boolean reportPresenceStatusOnEachMessage;
@Override
public TbGpsGeofencingActionNodeConfiguration defaultConfiguration() {
@ -45,7 +45,7 @@ public class TbGpsGeofencingActionNodeConfiguration extends TbGpsGeofencingFilte
configuration.setMinOutsideDurationTimeUnit(TimeUnit.MINUTES.name());
configuration.setMinInsideDuration(1);
configuration.setMinOutsideDuration(1);
configuration.setPresenceMonitoringStrategyOnEachMessage(false);
configuration.setReportPresenceStatusOnEachMessage(true);
return configuration;
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2024 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.rule.engine.geo;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.HashMap;
import java.util.Map;
@Data
public class GpsGeofencingActionTestCase {
private EntityId entityId;
private Map<EntityId, EntityGeofencingState> entityStates;
private boolean msgInside;
private boolean reportPresenceStatusOnEachMessage;
public GpsGeofencingActionTestCase(EntityId entityId, boolean msgInside, boolean reportPresenceStatusOnEachMessage, EntityGeofencingState entityGeofencingState) {
this.entityId = entityId;
this.msgInside = msgInside;
this.reportPresenceStatusOnEachMessage = reportPresenceStatusOnEachMessage;
this.entityStates = new HashMap<>();
this.entityStates.put(entityId, entityGeofencingState);
}
}

View File

@ -24,6 +24,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -37,8 +38,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.attributes.AttributesService;
import java.util.List;
import java.util.Map;
import java.time.Duration;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream;
@ -51,6 +51,8 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.ENTERED;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.INSIDE;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.LEFT;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.OUTSIDE;
import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.SUCCESS;
class TbGpsGeofencingActionNodeTest {
@ -70,42 +72,47 @@ class TbGpsGeofencingActionNodeTest {
node.destroy();
}
private static Stream<Arguments> givenPresenceMonitoringStrategyOnEachMessage_whenOnMsg_thenVerifyOutputMsgTypes() {
private static Stream<Arguments> givenReportPresenceStatusOnEachMessage_whenOnMsg_thenVerifyOutputMsgType() {
DeviceId deviceId = new DeviceId(UUID.randomUUID());
long tsNow = System.currentTimeMillis();
long tsNowMinusMinuteAndMillis = tsNow - Duration.ofMinutes(1).plusMillis(1).toMillis();
return Stream.of(
// default config with presenceMonitoringStrategyOnEachMessage false
Arguments.of(false, List.of(
Map.of(ENTERED, 0, INSIDE, 0, SUCCESS, 0),
Map.of(ENTERED, 1, INSIDE, 0, SUCCESS, 0),
Map.of(ENTERED, 1, INSIDE, 0, SUCCESS, 1),
Map.of(ENTERED, 1, INSIDE, 1, SUCCESS, 1),
Map.of(ENTERED, 1, INSIDE, 1, SUCCESS, 2)
)),
// default config with presenceMonitoringStrategyOnEachMessage true
Arguments.of(true, List.of(
Map.of(ENTERED, 0, INSIDE, 0, SUCCESS, 0),
Map.of(ENTERED, 1, INSIDE, 0, SUCCESS, 0),
Map.of(ENTERED, 1, INSIDE, 1, SUCCESS, 0),
Map.of(ENTERED, 1, INSIDE, 2, SUCCESS, 0),
Map.of(ENTERED, 1, INSIDE, 3, SUCCESS, 0)
))
// default config with presenceMonitoringStrategyOnEachMessage false and msgInside true
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, false, new EntityGeofencingState(false, 0, false)), ENTERED),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, false, new EntityGeofencingState(true, tsNow, false)), SUCCESS),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, false, new EntityGeofencingState(true, tsNowMinusMinuteAndMillis, false)), INSIDE),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, false, new EntityGeofencingState(true, tsNow, true)), SUCCESS),
// default config with presenceMonitoringStrategyOnEachMessage false and msgInside false
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, false, new EntityGeofencingState(false, 0, false)), LEFT),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, false, new EntityGeofencingState(false, tsNow, false)), SUCCESS),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, false, new EntityGeofencingState(false, tsNowMinusMinuteAndMillis, false)), OUTSIDE),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, false, new EntityGeofencingState(false, tsNow, true)), SUCCESS),
// default config with presenceMonitoringStrategyOnEachMessage true and msgInside true
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, true, new EntityGeofencingState(false, 0, false)), ENTERED),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, true, new EntityGeofencingState(true, tsNow, false)), INSIDE),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, true, new EntityGeofencingState(true, tsNowMinusMinuteAndMillis, false)), INSIDE),
// default config with presenceMonitoringStrategyOnEachMessage true and msgInside false
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, true, new EntityGeofencingState(false, 0, false)), LEFT),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, true, new EntityGeofencingState(false, tsNow, false)), OUTSIDE),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, true, new EntityGeofencingState(false, tsNowMinusMinuteAndMillis, false)), OUTSIDE)
);
}
@ParameterizedTest
@MethodSource
void givenPresenceMonitoringStrategyOnEachMessage_whenOnMsg_thenVerifyOutputMsgTypes(
boolean presenceMonitoringStrategyOnEachMessage,
List<Map<String, Integer>> outputMsgTypesCountList
void givenReportPresenceStatusOnEachMessage_whenOnMsg_thenVerifyOutputMsgType(
GpsGeofencingActionTestCase gpsGeofencingActionTestCase,
String expectedOutput
) throws TbNodeException {
// GIVEN
var config = new TbGpsGeofencingActionNodeConfiguration().defaultConfiguration();
config.setPresenceMonitoringStrategyOnEachMessage(presenceMonitoringStrategyOnEachMessage);
config.setReportPresenceStatusOnEachMessage(gpsGeofencingActionTestCase.isReportPresenceStatusOnEachMessage());
node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
DeviceId deviceId = new DeviceId(UUID.randomUUID());
TbMsgMetaData metadata = getMetadataForNewVersionPolygonPerimeter();
TbMsg msg = getTbMsg(deviceId, metadata,
GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLatitude(), GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLongitude());
TbMsg msg = gpsGeofencingActionTestCase.isMsgInside() ?
getInsideRectangleTbMsg(gpsGeofencingActionTestCase.getEntityId()) :
getOutsideRectangleTbMsg(gpsGeofencingActionTestCase.getEntityId());
when(ctx.getAttributesService()).thenReturn(attributesService);
when(ctx
@ -113,40 +120,30 @@ class TbGpsGeofencingActionNodeTest {
.find(ctx.getTenantId(), msg.getOriginator(), DataConstants.SERVER_SCOPE, ctx.getServiceId()))
.thenReturn(Futures.immediateFuture(Optional.empty()));
ReflectionTestUtils.setField(node, "entityStates", gpsGeofencingActionTestCase.getEntityStates());
// WHEN
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
node.onMsg(ctx, msg);
// THEN
verifyNodeOutputs(newMsgCaptor, outputMsgTypesCountList.get(0));
if (SUCCESS.equals(expectedOutput)) {
verify(ctx, times(1)).tellSuccess(newMsgCaptor.capture());
} else {
verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(expectedOutput));
}
}
// WHEN
msg = getTbMsg(deviceId, metadata,
GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(), GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude());
node.onMsg(ctx, msg);
private TbMsg getOutsideRectangleTbMsg(EntityId entityId) {
return getTbMsg(entityId, getMetadataForNewVersionPolygonPerimeter(),
GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLatitude(),
GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLongitude());
}
// THEN
verifyNodeOutputs(newMsgCaptor, outputMsgTypesCountList.get(1));
// WHEN
node.onMsg(ctx, msg);
// THEN
verifyNodeOutputs(newMsgCaptor, outputMsgTypesCountList.get(2));
// WHEN
config.setMinInsideDuration(0);
node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
node.onMsg(ctx, msg);
// THEN
verifyNodeOutputs(newMsgCaptor, outputMsgTypesCountList.get(3));
// WHEN
node.onMsg(ctx, msg);
// THEN
verifyNodeOutputs(newMsgCaptor, outputMsgTypesCountList.get(4));
private TbMsg getInsideRectangleTbMsg(EntityId entityId) {
return getTbMsg(entityId, getMetadataForNewVersionPolygonPerimeter(),
GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(),
GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude());
}
private TbMsg getTbMsg(EntityId entityId, TbMsgMetaData metadata, double latitude, double longitude) {
@ -160,12 +157,6 @@ class TbGpsGeofencingActionNodeTest {
return metadata;
}
private void verifyNodeOutputs(ArgumentCaptor<TbMsg> newMsgCaptor, Map<String, Integer> outputMsgTypesCount) {
verify(this.ctx, times(outputMsgTypesCount.get(ENTERED))).tellNext(newMsgCaptor.capture(), eq(ENTERED));
verify(this.ctx, times(outputMsgTypesCount.get(INSIDE))).tellNext(newMsgCaptor.capture(), eq(INSIDE));
verify(this.ctx, times(outputMsgTypesCount.get(SUCCESS))).tellSuccess(newMsgCaptor.capture());
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
@ -193,7 +184,7 @@ class TbGpsGeofencingActionNodeTest {
" \"minOutsideDuration\": 1,\n" +
" \"minInsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"presenceMonitoringStrategyOnEachMessage\": false,\n" +
" \"reportPresenceStatusOnEachMessage\": false,\n" +
" \"latitudeKeyName\": \"latitude\",\n" +
" \"longitudeKeyName\": \"longitude\",\n" +
" \"perimeterType\": \"POLYGON\",\n" +
@ -212,7 +203,7 @@ class TbGpsGeofencingActionNodeTest {
" \"minOutsideDuration\": 1,\n" +
" \"minInsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"presenceMonitoringStrategyOnEachMessage\": false,\n" +
" \"reportPresenceStatusOnEachMessage\": false,\n" +
" \"latitudeKeyName\": \"latitude\",\n" +
" \"longitudeKeyName\": \"longitude\",\n" +
" \"perimeterType\": \"POLYGON\",\n" +
@ -230,7 +221,7 @@ class TbGpsGeofencingActionNodeTest {
" \"minOutsideDuration\": 1,\n" +
" \"minInsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"presenceMonitoringStrategyOnEachMessage\": false,\n" +
" \"reportPresenceStatusOnEachMessage\": false,\n" +
" \"latitudeKeyName\": \"latitude\",\n" +
" \"longitudeKeyName\": \"longitude\",\n" +
" \"perimeterType\": \"POLYGON\",\n" +