From 167d8758f6c1d16f6e45945ac13be7495e406913 Mon Sep 17 00:00:00 2001 From: artem Date: Wed, 24 Jan 2024 18:28:17 +0200 Subject: [PATCH] TbGpsGeofencingActionNode: renamed key to reportPresenceStatusOnEachMessage + removed told variable + tests refactored --- .../engine/geo/TbGpsGeofencingActionNode.java | 46 ++++--- ...bGpsGeofencingActionNodeConfiguration.java | 4 +- .../geo/GpsGeofencingActionTestCase.java | 38 ++++++ .../geo/TbGpsGeofencingActionNodeTest.java | 117 ++++++++---------- 4 files changed, 120 insertions(+), 85 deletions(-) create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java index cd88a1eda7..0a9d0deb1d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java @@ -62,14 +62,16 @@ import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.OUTSIDE; "

" + "If an object with coordinates extracted from incoming message enters the geofence, sends a message with the type Entered. " + "If an object leaves the geofence, sends a message with the type Left. " + - "If the presence monitoring strategy \"On first message\" is selected, sends messages with types Inside or Outside only the first time the geofencing and duration conditions are satisfied; otherwise Success. " + - "If the presence monitoring strategy \"On each message\" is selected, sends messages with types Inside or Outside every time the geofencing condition is satisfied.", + "If the presence monitoring strategy \"On first message\" is selected, sends messages via rule node connection type Inside or Outside only the first time the geofencing and duration conditions are satisfied; otherwise sends messages via rule node connection type Success. " + + "If the presence monitoring strategy \"On each message\" is selected, sends messages via rule node connection type Inside or Outside every time the geofencing condition is satisfied. " + + "

" + + "Output connections: Entered, Left, Inside, Outside, Success", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeGpsGeofencingConfig" ) public class TbGpsGeofencingActionNode extends AbstractGeofencingNode { - 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 entityStates = new HashMap<>(); private final Gson gson = new Gson(); private final JsonParser parser = new JsonParser(); @@ -95,28 +97,32 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode (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 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); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java index ee375083b4..04bbab01b7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java @@ -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; } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java new file mode 100644 index 0000000000..8444c9cf0d --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java @@ -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 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); + } +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeTest.java index dd7536f4ee..42d69f2593 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeTest.java @@ -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 givenPresenceMonitoringStrategyOnEachMessage_whenOnMsg_thenVerifyOutputMsgTypes() { + private static Stream 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> 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 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 newMsgCaptor, Map 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 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" +