committed by
GitHub
5 changed files with 376 additions and 15 deletions
@ -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"; |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue