{"record":{"id":"a75bcccf1f851d83","repo":"thingsboard/thingsboard","slug":"invalid-perimeter-data-source-for-zone-with-id","errorCode":null,"errorMessage":"Invalid perimeter data source for zone with id: {}. Perimeter definition must be stored as attribute!","messagePattern":"Invalid perimeter data source for zone with id: (.+?)\\. Perimeter definition must be stored as attribute!","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java","lineNumber":49,"sourceCode":"import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE;\nimport static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE;\n\n@Data\npublic class GeofencingZoneState {\n\n    private final EntityId zoneId;\n\n    private long ts;\n    private Long version;\n    private PerimeterDefinition perimeterDefinition;\n\n    @EqualsAndHashCode.Exclude\n    private GeofencingPresenceStatus lastPresence;\n\n    public GeofencingZoneState(EntityId zoneId, KvEntry entry) {\n        this.zoneId = zoneId;\n        if (!(entry instanceof AttributeKvEntry attributeKvEntry)) {\n            throw new IllegalArgumentException(\"Invalid perimeter data source for zone with id: \" + zoneId + \". Perimeter definition must be stored as attribute!\");\n        }\n        this.ts = attributeKvEntry.getLastUpdateTs();\n        this.version = attributeKvEntry.getVersion();\n        if (entry.getValueAsString() == null) {\n            throw new IllegalArgumentException(\"Perimeter attribute '\" + entry.getKey() + \"' not found for Zone with id: \" + zoneId);\n        }\n        this.perimeterDefinition = JacksonUtil.fromString(entry.getValueAsString(), PerimeterDefinition.class,\n                \"Invalid perimeter definition format for Zone with id: \" + zoneId + \". Failed to parse attribute '\" + entry.getKey() + \"'\");\n    }\n\n    public GeofencingZoneState(GeofencingZoneProto proto) {\n        this.zoneId = ProtoUtils.fromProto(proto.getZoneId());\n        this.ts = proto.getTs();\n        this.version = proto.getVersion();\n        this.perimeterDefinition = JacksonUtil.fromString(proto.getPerimeterDefinition(), PerimeterDefinition.class);\n        if (proto.hasInside()) {\n            this.lastPresence = proto.getInside() ? INSIDE : OUTSIDE;\n        }","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/thingsboard/thingsboard/blob/45c30e83fa57356840d5f25d894002d94222d524/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java#L31-L67","documentation":"GeofencingZoneState is built from a KvEntry that must be an AttributeKvEntry, because the zone perimeter definition is only valid when stored as a server attribute (it needs lastUpdateTs and version for optimistic state tracking). The constructor throws IllegalArgumentException when the supplied KvEntry is any other implementation (typically a telemetry TvKvEntry), i.e. the perimeter was read from latest telemetry or a time series query instead of the attributes service.","triggerScenarios":"A geofencing zone configured to read its perimeter definition from a LATEST_TELEMETRY argument instead of an attribute; a code path that resolves the zone perimeter via telemetryService.findLatest instead of attributesService.find; a calculated field config where the zone argument's refType was changed from attribute to telemetry.","commonSituations":"Users pointing the zone perimeter at a telemetry key because the device publishes its geofence as telemetry; copy-paste of an argument config that uses telemetry scope; custom code feeding a non-attribute KvEntry into new GeofencingZoneState(EntityId, KvEntry).","solutions":["Change the zone's perimeter argument in the calculated field configuration to reference an ATTRIBUTE (server/shared/client scope), not latest telemetry","If the perimeter genuinely arrives as telemetry, add a rule-engine chain that copies it into a server attribute and point the zone at that attribute","If calling this constructor from custom code, pass only entries obtained from attributesService.find(...) so the instance is an AttributeKvEntry"],"exampleFix":"// before: perimeter read from latest telemetry\nTelemetryKvEntry entry = tsService.findLatest(tenantId, zoneId, PERIMETER_KEY).get();\nnew GeofencingZoneState(zoneId, entry);\n// after: perimeter stored as server attribute\nAttributeKvEntry attr = attributesService.find(tenantId, zoneId, AttributeScope.SERVER_SCOPE, PERIMETER_KEY).get().orElseThrow();\nnew GeofencingZoneState(zoneId, attr);","handlingStrategy":"type-guard","validationCode":"// ensure the perimeter comes from the attributes service before constructing state\nOptional<AttributeKvEntry> opt = attributesService.find(tenantId, zoneId, AttributeScope.SERVER_SCOPE, key).get();\nif (opt.isEmpty()) { /* zone unconfigured */ }","typeGuard":"boolean isPerimeterSource(AttributeKvEntry e) { return e instanceof AttributeKvEntry; }\n// Java pattern form:\nif (entry instanceof AttributeKvEntry a) { new GeofencingZoneState(zoneId, a); } else { /* reject telemetry source */ }","tryCatchPattern":"catch (IllegalArgumentException e) if message contains \"must be stored as attribute\": surface a config error naming the zone id; no retry.","preventionTips":["Point zone perimeters only at server attributes","If perimeters arrive as telemetry, bridge them into attributes with a rule chain first","Never call the EntityId/KvEntry constructor with entries from telemetry services"],"tags":["geofencing","attributes","telemetry","calculated-field"],"backgroundTag":null,"analyzedSha":"45c30e83fa57356840d5f25d894002d94222d524","analyzedAt":"2026-08-14T11:45:36.599Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}