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

  1. Change the zone's perimeter argument in the calculated field configuration to reference an ATTRIBUTE (server/shared/client scope), not latest telemetry
  2. 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
  3. 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

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


AI-assisted analysis of thingsboard/thingsboard@45c30e83fa (2026-08-14). Data as JSON: /api/errors/a75bcccf1f851d83. Report an issue: GitHub.