Browse Source

Merge pull request #14647 from thingsboard/geofencing-cf-eval-logic-change

Updated geofencing CF evaluation logic
pull/14650/head
Viacheslav Klimov 8 months ago
committed by GitHub
parent
commit
90f0eecdc9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
  2. 6
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java
  3. 33
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  4. 5
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
  5. 8
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java
  6. 7
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java

10
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java

@ -110,10 +110,14 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState imp
return;
}
GeofencingTransitionEvent transitionEvent = eval.transition();
if (transitionEvent == null && !firstEval) {
return;
if (transitionEvent == null) {
if (!firstEval) {
return;
}
transitionEvent = eval.status() == INSIDE ?
GeofencingTransitionEvent.ENTERED :
GeofencingTransitionEvent.LEFT;
}
transitionEvent = transitionEvent == null ? GeofencingTransitionEvent.LEFT : transitionEvent;
EntityRelation relation = switch (zoneGroupCfg.getDirection()) {
case TO -> new EntityRelation(zoneId, entityId, zoneGroupCfg.getRelationType());
case FROM -> new EntityRelation(entityId, zoneId, zoneGroupCfg.getRelationType());

6
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java

@ -89,11 +89,7 @@ public class GeofencingZoneState {
// first evaluation
if (this.lastPresence == null) {
this.lastPresence = status;
GeofencingTransitionEvent transition = null;
if (status == GeofencingPresenceStatus.INSIDE) {
transition = GeofencingTransitionEvent.ENTERED;
}
return new GeofencingEvalResult(transition, status);
return new GeofencingEvalResult(null, status);
}
// State changed
if (this.lastPresence != status) {

33
application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java

@ -799,23 +799,22 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/calculatedField", cf, CalculatedField.class);
// --- Assert initial evaluation (ENTERED / OUTSIDE) ---
// --- Assert initial evaluation ---
await().alias("initial geofencing evaluation")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode attrs = getServerAttributes(device.getId(),
"allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent");
// --- no restrictedZonesEvent as no transition happened yet
assertThat(attrs).isNotNull().isNotEmpty().hasSize(3);
// --- no transition events as no transitions happened yet
assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
.containsEntry("allowedZonesStatus", "INSIDE")
assertThat(m).containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("restrictedZonesStatus", "OUTSIDE");
});
// --- delete attributes reported in previous evaluation
doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/SERVER_SCOPE?keys=allowedZonesEvent,allowedZonesStatus,restrictedZonesStatus", String.class);
doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/SERVER_SCOPE?keys=allowedZonesStatus,restrictedZonesStatus", String.class);
// --- Update restrictedZone by 'restrictedZone' attribute update
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE,
@ -909,17 +908,16 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/calculatedField", cf, CalculatedField.class);
// --- Assert initial evaluation (ENTERED / OUTSIDE) ---
// --- Assert initial evaluation ---
await().alias("initial geofencing evaluation")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode attrs = getServerAttributes(device.getId(),
"allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus");
assertThat(attrs).isNotNull().isNotEmpty().hasSize(3);
"allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent");
assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
.containsEntry("allowedZonesStatus", "INSIDE")
assertThat(m).containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("restrictedZonesStatus", "OUTSIDE");
});
@ -1014,16 +1012,15 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
var geofencingConfiguration = (GeofencingCalculatedFieldConfiguration) configuration;
assertThat(geofencingConfiguration.isScheduledUpdateEnabled()).isTrue();
// --- Assert initial evaluation (ENTERED) ---
// --- Assert initial evaluation ---
await().alias("initial geofencing evaluation")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus");
assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
assertThat(attrs).isNotNull().isNotEmpty().hasSize(1);
Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
.containsEntry("allowedZonesStatus", "INSIDE");
assertThat(m).containsEntry("allowedZonesStatus", "INSIDE");
});
// --- Move device OUTSIDE Zone A (expect LEFT) ---
@ -1057,11 +1054,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(device.getId(), savedCalculatedField.getId(), minAllowedScheduledUpdateIntervalInSecForCF);
// --- Same coordinates as before, but now we expect ENTERED since a new zone is registered ---
// --- Same coordinates as before, but now we expect INSIDE group status since a new zone is registered ---
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope",
JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")).andExpect(status().isOk());
// --- Assert dynamic refresh picks up new relation and flips event back to ENTERED on the next telemetry update ---
// --- Assert dynamic refresh picks up a new relation and flips status back to INSIDE on the next telemetry update ---
await().alias("dynamic refresh rebinds allowedZones")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(1, TimeUnit.SECONDS)
@ -1069,7 +1066,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus");
assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
assertThat(m).containsEntry("allowedZonesEvent", "LEFT") // attribute from previous eval with outdated ts.
.containsEntry("allowedZonesStatus", "INSIDE");
});
}

5
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java

@ -249,7 +249,6 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode()
.put("allowedZonesEvent", "ENTERED")
.put("allowedZonesStatus", "INSIDE")
.put("restrictedZonesStatus", "OUTSIDE")
);
@ -329,9 +328,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result).isNotNull();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode().put("allowedZonesEvent", "ENTERED")
);
assertThat(result.getResult().get("values")).isEmpty();
SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L);
SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L);

8
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java

@ -47,8 +47,8 @@ public class GeofencingZoneStateTest {
@Test
void evaluate_initialInside_thenInsideAgain() {
var inside = new Coordinates(50.4730, 30.5050);
// first evaluation: no prior state -> ENTERED
assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE));
// first evaluation: no prior state -> INSIDE
assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE));
// same position again -> INSIDE (steady state)
assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE));
}
@ -66,8 +66,8 @@ public class GeofencingZoneStateTest {
void evaluate_inside_thenLeave() {
var inside = new Coordinates(50.4730, 30.5050);
var outside = new Coordinates(50.4760, 30.5110);
// enter
assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE));
// initial eval
assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE));
// leave -> LEFT
assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(LEFT, OUTSIDE));
// still outside -> OUTSIDE

7
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java

@ -409,11 +409,10 @@ public class CalculatedFieldTest extends AbstractContainerTest {
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode attrs = testRestClient.getAttributes(device.getId(), SERVER_SCOPE,
"allowedZonesEvent,allowedZonesStatus,restrictedZonesStatus");
assertThat(attrs).isNotNull().hasSize(3);
"allowedZonesEvent,allowedZonesStatus,restrictedZonesEvent,restrictedZonesStatus");
assertThat(attrs).isNotNull().hasSize(2);
Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
.containsEntry("allowedZonesStatus", "INSIDE")
assertThat(m).containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("restrictedZonesStatus", "OUTSIDE");
});

Loading…
Cancel
Save