thingsboard/thingsboard · error · IllegalArgumentException
Invalid perimeter data source for zone with id: {}. Perimete
Error message
Invalid perimeter data source for zone with id: {}. Perimeter definition must be stored as attribute! What it means
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.
Source
Thrown at application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java:49
import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE;
@Data
public class GeofencingZoneState {
private final EntityId zoneId;
private long ts;
private Long version;
private PerimeterDefinition perimeterDefinition;
@EqualsAndHashCode.Exclude
private GeofencingPresenceStatus lastPresence;
public GeofencingZoneState(EntityId zoneId, KvEntry entry) {
this.zoneId = zoneId;
if (!(entry instanceof AttributeKvEntry attributeKvEntry)) {
throw new IllegalArgumentException("Invalid perimeter data source for zone with id: " + zoneId + ". Perimeter definition must be stored as attribute!");
}
this.ts = attributeKvEntry.getLastUpdateTs();
this.version = attributeKvEntry.getVersion();
if (entry.getValueAsString() == null) {
throw new IllegalArgumentException("Perimeter attribute '" + entry.getKey() + "' not found for Zone with id: " + zoneId);
}
this.perimeterDefinition = JacksonUtil.fromString(entry.getValueAsString(), PerimeterDefinition.class,
"Invalid perimeter definition format for Zone with id: " + zoneId + ". Failed to parse attribute '" + entry.getKey() + "'");
}
public GeofencingZoneState(GeofencingZoneProto proto) {
this.zoneId = ProtoUtils.fromProto(proto.getZoneId());
this.ts = proto.getTs();
this.version = proto.getVersion();
this.perimeterDefinition = JacksonUtil.fromString(proto.getPerimeterDefinition(), PerimeterDefinition.class);
if (proto.hasInside()) {
this.lastPresence = proto.getInside() ? INSIDE : OUTSIDE;
}View on GitHub (pinned to 45c30e83fa)
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
Example fix
// before: perimeter read from latest telemetry TelemetryKvEntry entry = tsService.findLatest(tenantId, zoneId, PERIMETER_KEY).get(); new GeofencingZoneState(zoneId, entry); // after: perimeter stored as server attribute AttributeKvEntry attr = attributesService.find(tenantId, zoneId, AttributeScope.SERVER_SCOPE, PERIMETER_KEY).get().orElseThrow(); new GeofencingZoneState(zoneId, attr);
Defensive patterns
Strategy: type-guard
Validate before calling
// ensure the perimeter comes from the attributes service before constructing state
Optional<AttributeKvEntry> opt = attributesService.find(tenantId, zoneId, AttributeScope.SERVER_SCOPE, key).get();
if (opt.isEmpty()) { /* zone unconfigured */ } Type guard
boolean isPerimeterSource(AttributeKvEntry e) { return e instanceof AttributeKvEntry; }
// Java pattern form:
if (entry instanceof AttributeKvEntry a) { new GeofencingZoneState(zoneId, a); } else { /* reject telemetry source */ } Try / catch
catch (IllegalArgumentException e) if message contains "must be stored as attribute": surface a config error naming the zone id; no retry.
Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- Zone group configuration is missing for the: {}
- Perimeter attribute '{}' not found for Zone with id: {}
- ITEM_NOT_FOUND
- Unsupported referenced entity type: '{}'.
- Single value size exceeds the maximum allowed limit. The arg
AI-assisted analysis of thingsboard/thingsboard@45c30e83fa (2026-08-14).
Data as JSON: /api/errors/a75bcccf1f851d83.
Report an issue: GitHub.