Added new integration test
This commit is contained in:
		
							parent
							
								
									d5f78e6db0
								
							
						
					
					
						commit
						40276c6c15
					
				@ -15,6 +15,7 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
package org.thingsboard.server.cf;
 | 
					package org.thingsboard.server.cf;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.databind.JsonNode;
 | 
				
			||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
 | 
					import com.fasterxml.jackson.databind.node.ArrayNode;
 | 
				
			||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
					import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
				
			||||||
import org.junit.Test;
 | 
					import org.junit.Test;
 | 
				
			||||||
@ -29,22 +30,33 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
 | 
				
			|||||||
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
 | 
					import org.thingsboard.server.common.data.cf.CalculatedFieldType;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.configuration.Argument;
 | 
					import org.thingsboard.server.common.data.cf.configuration.Argument;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
 | 
					import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
 | 
				
			||||||
 | 
					import org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType;
 | 
				
			||||||
 | 
					import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration;
 | 
				
			||||||
 | 
					import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent;
 | 
				
			||||||
 | 
					import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.configuration.Output;
 | 
					import org.thingsboard.server.common.data.cf.configuration.Output;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.configuration.OutputType;
 | 
					import org.thingsboard.server.common.data.cf.configuration.OutputType;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
 | 
					import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
 | 
				
			||||||
 | 
					import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration;
 | 
					import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration;
 | 
				
			||||||
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
 | 
					import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
 | 
				
			||||||
import org.thingsboard.server.common.data.debug.DebugSettings;
 | 
					import org.thingsboard.server.common.data.debug.DebugSettings;
 | 
				
			||||||
import org.thingsboard.server.common.data.id.AssetProfileId;
 | 
					import org.thingsboard.server.common.data.id.AssetProfileId;
 | 
				
			||||||
import org.thingsboard.server.common.data.id.EntityId;
 | 
					import org.thingsboard.server.common.data.id.EntityId;
 | 
				
			||||||
 | 
					import org.thingsboard.server.common.data.relation.EntityRelation;
 | 
				
			||||||
 | 
					import org.thingsboard.server.common.data.relation.EntitySearchDirection;
 | 
				
			||||||
import org.thingsboard.server.controller.CalculatedFieldControllerTest;
 | 
					import org.thingsboard.server.controller.CalculatedFieldControllerTest;
 | 
				
			||||||
import org.thingsboard.server.dao.service.DaoSqlTest;
 | 
					import org.thingsboard.server.dao.service.DaoSqlTest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Arrays;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
import java.util.concurrent.TimeUnit;
 | 
					import java.util.concurrent.TimeUnit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
					import static org.assertj.core.api.Assertions.assertThat;
 | 
				
			||||||
import static org.awaitility.Awaitility.await;
 | 
					import static org.awaitility.Awaitility.await;
 | 
				
			||||||
 | 
					import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@DaoSqlTest
 | 
					@DaoSqlTest
 | 
				
			||||||
public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest {
 | 
					public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest {
 | 
				
			||||||
@ -606,6 +618,139 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
 | 
				
			|||||||
                });
 | 
					                });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    public void testGeofencingCalculatedField_SingleZonePerGroup() throws Exception {
 | 
				
			||||||
 | 
					        // --- Arrange entities ---
 | 
				
			||||||
 | 
					        Device device = createDevice("GF Device", "sn-geo-1");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Allowed zone polygon (square)
 | 
				
			||||||
 | 
					        String allowedPolygon = """
 | 
				
			||||||
 | 
					        {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"}
 | 
				
			||||||
 | 
					        """;
 | 
				
			||||||
 | 
					        // Restricted zone polygon (square)
 | 
				
			||||||
 | 
					        String restrictedPolygon = """
 | 
				
			||||||
 | 
					        {"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"}
 | 
				
			||||||
 | 
					        """;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Asset allowedZoneAsset = createAsset("Allowed Zone", null);
 | 
				
			||||||
 | 
					        doPost("/api/plugins/telemetry/ASSET/" + allowedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE,
 | 
				
			||||||
 | 
					                JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygon + "}")).andExpect(status().isOk());;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Asset restrictedZoneAsset = createAsset("Restricted Zone", null);
 | 
				
			||||||
 | 
					        doPost("/api/plugins/telemetry/ASSET/" + restrictedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE,
 | 
				
			||||||
 | 
					                JacksonUtil.toJsonNode("{\"zone\":" + restrictedPolygon + "}")).andExpect(status().isOk());;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Relations from device to zones
 | 
				
			||||||
 | 
					        EntityRelation deviceToAllowedZoneRelation = new EntityRelation();
 | 
				
			||||||
 | 
					        deviceToAllowedZoneRelation.setFrom(device.getId());
 | 
				
			||||||
 | 
					        deviceToAllowedZoneRelation.setTo(allowedZoneAsset.getId());
 | 
				
			||||||
 | 
					        deviceToAllowedZoneRelation.setType("AllowedZone");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        EntityRelation deviceToRestrictedZoneRelation = new EntityRelation();
 | 
				
			||||||
 | 
					        deviceToRestrictedZoneRelation.setFrom(device.getId());
 | 
				
			||||||
 | 
					        deviceToRestrictedZoneRelation.setTo(restrictedZoneAsset.getId());
 | 
				
			||||||
 | 
					        deviceToRestrictedZoneRelation.setType("RestrictedZone");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        doPost("/api/relation", deviceToAllowedZoneRelation).andExpect(status().isOk());
 | 
				
			||||||
 | 
					        doPost("/api/relation", deviceToRestrictedZoneRelation).andExpect(status().isOk());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Initial device coordinates (inside Allowed, outside Restricted)
 | 
				
			||||||
 | 
					        doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope",
 | 
				
			||||||
 | 
					                JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // --- Build CF: GEOFENCING ---
 | 
				
			||||||
 | 
					        CalculatedField cf = new CalculatedField();
 | 
				
			||||||
 | 
					        cf.setEntityId(device.getId());
 | 
				
			||||||
 | 
					        cf.setType(CalculatedFieldType.GEOFENCING);
 | 
				
			||||||
 | 
					        cf.setName("Geofencing CF");
 | 
				
			||||||
 | 
					        cf.setDebugSettings(DebugSettings.off());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Coordinates: TS_LATEST on the device
 | 
				
			||||||
 | 
					        Argument lat = new Argument();
 | 
				
			||||||
 | 
					        lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null));
 | 
				
			||||||
 | 
					        Argument lon = new Argument();
 | 
				
			||||||
 | 
					        lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Zone groups: ATTRIBUTE on specific assets (one zone per group)
 | 
				
			||||||
 | 
					        Argument allowedZones = new Argument();
 | 
				
			||||||
 | 
					        var allowedZonesRefDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
 | 
				
			||||||
 | 
					        allowedZonesRefDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM);
 | 
				
			||||||
 | 
					        allowedZonesRefDynamicSourceConfiguration.setRelationType("AllowedZone");
 | 
				
			||||||
 | 
					        allowedZonesRefDynamicSourceConfiguration.setMaxLevel(1);
 | 
				
			||||||
 | 
					        allowedZonesRefDynamicSourceConfiguration.setFetchLastLevelOnly(true);
 | 
				
			||||||
 | 
					        allowedZones.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE));
 | 
				
			||||||
 | 
					        allowedZones.setRefDynamicSource(CFArgumentDynamicSourceType.RELATION_QUERY);
 | 
				
			||||||
 | 
					        allowedZones.setRefDynamicSourceConfiguration(allowedZonesRefDynamicSourceConfiguration);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Argument restrictedZones = new Argument();
 | 
				
			||||||
 | 
					        var restrictedZonesRefDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
 | 
				
			||||||
 | 
					        restrictedZonesRefDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM);
 | 
				
			||||||
 | 
					        restrictedZonesRefDynamicSourceConfiguration.setRelationType("RestrictedZone");
 | 
				
			||||||
 | 
					        restrictedZonesRefDynamicSourceConfiguration.setMaxLevel(1);
 | 
				
			||||||
 | 
					        restrictedZonesRefDynamicSourceConfiguration.setFetchLastLevelOnly(true);
 | 
				
			||||||
 | 
					        restrictedZones.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE));
 | 
				
			||||||
 | 
					        restrictedZones.setRefDynamicSource(CFArgumentDynamicSourceType.RELATION_QUERY);
 | 
				
			||||||
 | 
					        restrictedZones.setRefDynamicSourceConfiguration(restrictedZonesRefDynamicSourceConfiguration);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cfg.setArguments(Map.of(
 | 
				
			||||||
 | 
					                GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat,
 | 
				
			||||||
 | 
					                GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon,
 | 
				
			||||||
 | 
					                "allowedZones", allowedZones,
 | 
				
			||||||
 | 
					                "restrictedZones", restrictedZones
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Zone group reporting config
 | 
				
			||||||
 | 
					        List<GeofencingEvent> reportEvents = Arrays.stream(GeofencingEvent.values()).toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        GeofencingZoneGroupConfiguration allowedCfg = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents);
 | 
				
			||||||
 | 
					        GeofencingZoneGroupConfiguration restrictedCfg = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cfg.setGeofencingZoneGroupConfigurations(Map.of(
 | 
				
			||||||
 | 
					                "allowedZones", allowedCfg,
 | 
				
			||||||
 | 
					                "restrictedZones", restrictedCfg
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Output to server attributes
 | 
				
			||||||
 | 
					        Output out = new Output();
 | 
				
			||||||
 | 
					        out.setType(OutputType.ATTRIBUTES);
 | 
				
			||||||
 | 
					        out.setScope(AttributeScope.SERVER_SCOPE);
 | 
				
			||||||
 | 
					        cfg.setOutput(out);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cf.setConfiguration(cfg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        doPost("/api/calculatedField", cf, CalculatedField.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // --- Assert initial evaluation (ENTERED / OUTSIDE) ---
 | 
				
			||||||
 | 
					        await().alias("initial geofencing evaluation")
 | 
				
			||||||
 | 
					                .atMost(TIMEOUT, TimeUnit.SECONDS)
 | 
				
			||||||
 | 
					                .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
 | 
				
			||||||
 | 
					                .untilAsserted(() -> {
 | 
				
			||||||
 | 
					                    ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent", "restrictedZoneEvent");
 | 
				
			||||||
 | 
					                    assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
 | 
				
			||||||
 | 
					                    Map<String, String> m = kv(attrs);
 | 
				
			||||||
 | 
					                    assertThat(m).containsEntry("allowedZoneEvent", "ENTERED")
 | 
				
			||||||
 | 
					                            .containsEntry("restrictedZoneEvent", "OUTSIDE");
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // --- Move device into Restricted zone (and outside Allowed) ---
 | 
				
			||||||
 | 
					        doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope",
 | 
				
			||||||
 | 
					                JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // --- Assert transition (LEFT / ENTERED) ---
 | 
				
			||||||
 | 
					        await().alias("transition evaluation after movement")
 | 
				
			||||||
 | 
					                .atMost(TIMEOUT, TimeUnit.SECONDS)
 | 
				
			||||||
 | 
					                .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
 | 
				
			||||||
 | 
					                .untilAsserted(() -> {
 | 
				
			||||||
 | 
					                    ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent", "restrictedZoneEvent");
 | 
				
			||||||
 | 
					                    assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
 | 
				
			||||||
 | 
					                    Map<String, String> m = kv(attrs);
 | 
				
			||||||
 | 
					                    assertThat(m).containsEntry("allowedZoneEvent", "LEFT")
 | 
				
			||||||
 | 
					                            .containsEntry("restrictedZoneEvent", "ENTERED");
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception {
 | 
					    private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception {
 | 
				
			||||||
        return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class);
 | 
					        return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -621,4 +766,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
 | 
				
			|||||||
        return doPost("/api/asset", asset, Asset.class);
 | 
					        return doPost("/api/asset", asset, Asset.class);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static Map<String, String> kv(ArrayNode attrs) {
 | 
				
			||||||
 | 
					        Map<String, String> m = new HashMap<>();
 | 
				
			||||||
 | 
					        for (JsonNode n : attrs) {
 | 
				
			||||||
 | 
					            m.put(n.get("key").asText(), n.get("value").asText());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return m;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user