Browse Source

Merge pull request #9970 from moliboga/master

Updated TbGpsGeofencingActionNode
pull/10185/head
Andrew Shvayka 3 years ago
committed by GitHub
parent
commit
10ef1bc691
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 67
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java
  2. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java
  3. 23
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/GpsGeofencingEvents.java
  4. 39
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java
  5. 259
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeTest.java

67
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.rule.engine.geo;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
@ -28,6 +30,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.Collections;
@ -39,6 +42,11 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
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;
/**
* Created by ashvayka on 19.01.18.
*/
@ -46,15 +54,24 @@ import java.util.concurrent.TimeoutException;
@RuleNode(
type = ComponentType.ACTION,
name = "gps geofencing events",
version = 1,
configClazz = TbGpsGeofencingActionNodeConfiguration.class,
relationTypes = {"Success", "Entered", "Left", "Inside", "Outside"},
nodeDescription = "Produces incoming messages using GPS based geofencing",
nodeDetails = "Extracts latitude and longitude parameters from incoming message and returns different events based on configuration parameters",
nodeDetails = "Extracts latitude and longitude parameters from incoming message and returns different events based on configuration parameters. " +
"<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 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 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();
@ -80,25 +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 (!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;
}
}
ctx.tellNext(msg, matches ? ENTERED : LEFT);
return;
}
if (config.isReportPresenceStatusOnEachMessage()) {
ctx.tellNext(msg, entityState.isInside() ? INSIDE : OUTSIDE);
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) {
@ -127,4 +151,17 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofe
protected Class<TbGpsGeofencingActionNodeConfiguration> getConfigClazz() {
return TbGpsGeofencingActionNodeConfiguration.class;
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
boolean hasChanges = false;
if (fromVersion == 0) {
if (!oldConfiguration.has(REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE)) {
hasChanges = true;
((ObjectNode) oldConfiguration).put(REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE, false);
}
}
return new TbPair<>(hasChanges, oldConfiguration);
}
}

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java

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

23
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/GpsGeofencingEvents.java

@ -0,0 +1,23 @@
/**
* 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.util;
public class GpsGeofencingEvents {
public static final String ENTERED = "Entered";
public static final String INSIDE = "Inside";
public static final String LEFT = "Left";
public static final String OUTSIDE = "Outside";
}

39
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java

@ -0,0 +1,39 @@
/**
* 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);
}
}

259
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeTest.java

@ -0,0 +1,259 @@
/**
* 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 org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.attributes.AttributesService;
import java.time.Duration;
import java.util.UUID;
import java.util.stream.Stream;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
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;
@ExtendWith(MockitoExtension.class)
class TbGpsGeofencingActionNodeTest extends AbstractRuleNodeUpgradeTest {
@Mock
private TbContext ctx;
@Mock
private AttributesService attributesService;
private TbGpsGeofencingActionNode node;
@BeforeEach
void setUp() {
node = spy(new TbGpsGeofencingActionNode());
}
@AfterEach
void tearDown() {
node.destroy();
}
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 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 givenReportPresenceStatusOnEachMessage_whenOnMsg_thenVerifyOutputMsgType(
GpsGeofencingActionTestCase gpsGeofencingActionTestCase,
String expectedOutput
) throws TbNodeException {
// GIVEN
var config = new TbGpsGeofencingActionNodeConfiguration().defaultConfiguration();
config.setReportPresenceStatusOnEachMessage(gpsGeofencingActionTestCase.isReportPresenceStatusOnEachMessage());
node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
TbMsg msg = gpsGeofencingActionTestCase.isMsgInside() ?
getInsideRectangleTbMsg(gpsGeofencingActionTestCase.getEntityId()) :
getOutsideRectangleTbMsg(gpsGeofencingActionTestCase.getEntityId());
when(ctx.getAttributesService()).thenReturn(attributesService);
ReflectionTestUtils.setField(node, "entityStates", gpsGeofencingActionTestCase.getEntityStates());
// WHEN
node.onMsg(ctx, msg);
// THEN
verify(ctx.getAttributesService(), never()).find(any(), any(), any(), anyString());
verify(ctx, never()).tellFailure(any(), any(Throwable.class));
verify(ctx, never()).enqueueForTellNext(any(), eq(expectedOutput), any(), any());
verify(ctx, never()).ack(any());
if (SUCCESS.equals(expectedOutput)) {
verify(ctx).tellSuccess(eq(msg));
} else {
verify(ctx).tellNext(eq(msg), eq(expectedOutput));
}
}
private TbMsg getOutsideRectangleTbMsg(EntityId entityId) {
return getTbMsg(entityId, getMetadataForNewVersionPolygonPerimeter(),
GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLatitude(),
GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLongitude());
}
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) {
String data = "{\"latitude\": " + latitude + ", \"longitude\": " + longitude + "}";
return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, metadata, data);
}
private TbMsgMetaData getMetadataForNewVersionPolygonPerimeter() {
var metadata = new TbMsgMetaData();
metadata.putValue("ss_perimeter", GeoUtilTest.SIMPLE_RECT);
return metadata;
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// default config for version 0
Arguments.of(0,
"{\n" +
" \"minInsideDuration\": 1,\n" +
" \"minOutsideDuration\": 1,\n" +
" \"minInsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"latitudeKeyName\": \"latitude\",\n" +
" \"longitudeKeyName\": \"longitude\",\n" +
" \"perimeterType\": \"POLYGON\",\n" +
" \"fetchPerimeterInfoFromMessageMetadata\": true,\n" +
" \"perimeterKeyName\": \"ss_perimeter\",\n" +
" \"polygonsDefinition\": null,\n" +
" \"centerLatitude\": null,\n" +
" \"centerLongitude\": null,\n" +
" \"range\": null,\n" +
" \"rangeUnit\": null\n" +
"}\n",
true,
"{\n" +
" \"minInsideDuration\": 1,\n" +
" \"minOutsideDuration\": 1,\n" +
" \"minInsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"reportPresenceStatusOnEachMessage\": false,\n" +
" \"latitudeKeyName\": \"latitude\",\n" +
" \"longitudeKeyName\": \"longitude\",\n" +
" \"perimeterType\": \"POLYGON\",\n" +
" \"fetchPerimeterInfoFromMessageMetadata\": true,\n" +
" \"perimeterKeyName\": \"ss_perimeter\",\n" +
" \"polygonsDefinition\": null,\n" +
" \"centerLatitude\": null,\n" +
" \"centerLongitude\": null,\n" +
" \"range\": null,\n" +
" \"rangeUnit\": null\n" +
"}\n"),
// default config for version 1 with upgrade from version 0
Arguments.of(0,
"{\n" +
" \"minInsideDuration\": 1,\n" +
" \"minOutsideDuration\": 1,\n" +
" \"minInsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"reportPresenceStatusOnEachMessage\": false,\n" +
" \"latitudeKeyName\": \"latitude\",\n" +
" \"longitudeKeyName\": \"longitude\",\n" +
" \"perimeterType\": \"POLYGON\",\n" +
" \"fetchPerimeterInfoFromMessageMetadata\": true,\n" +
" \"perimeterKeyName\": \"ss_perimeter\",\n" +
" \"polygonsDefinition\": null,\n" +
" \"centerLatitude\": null,\n" +
" \"centerLongitude\": null,\n" +
" \"range\": null,\n" +
" \"rangeUnit\": null\n" +
"}\n",
false,
"{\n" +
" \"minInsideDuration\": 1,\n" +
" \"minOutsideDuration\": 1,\n" +
" \"minInsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"reportPresenceStatusOnEachMessage\": false,\n" +
" \"latitudeKeyName\": \"latitude\",\n" +
" \"longitudeKeyName\": \"longitude\",\n" +
" \"perimeterType\": \"POLYGON\",\n" +
" \"fetchPerimeterInfoFromMessageMetadata\": true,\n" +
" \"perimeterKeyName\": \"ss_perimeter\",\n" +
" \"polygonsDefinition\": null,\n" +
" \"centerLatitude\": null,\n" +
" \"centerLongitude\": null,\n" +
" \"range\": null,\n" +
" \"rangeUnit\": null\n" +
"}\n")
);
}
@Override
protected TbNode getTestNode() {
return node;
}
}
Loading…
Cancel
Save