Skip to content

Adapter Modeling Conventions

Recurring modeling conventions for adapter-registration.yaml (and access-model.yaml) — how Things, categories, functions, and relationships are shaped. These are distinct from compliance rules, which check implementation/runtime correctness; modeling conventions check structural shape.

Each convention has a strength:

  • RULE — near-universal across shipping adapters; a deviation is a finding (review flags it).
  • RECOMMENDATION — common best practice; a deviation is advisory (review notes it, does not fail).

This split is deliberate: PQ recommends shapes, it does not mandate them. Don't treat a RECOMMENDATION deviation as an error.

Inheritance: a derived device type (extends: Base) inherits the base's category, functions, properties, commands, and events, and may omit them. Resolve extends before applying any convention below — a derived type that omits an inherited category or function is correct, not a deviation. (Repeating an inherited value is allowed but redundant.)

Dual use:

  1. Authoring — consult these conventions when you model a new adapter.
  2. Review — use the machine-checkable conventions as a checklist when reviewing an existing model.

Evidence: derived from 12 shipping adapters / 72 device types.

Convention index

# Name Strength Checkable
MODEL-001 Category implies its canonical function RULE yes
MODEL-002 Doors are access points RULE yes
MODEL-003 apb_point belongs on readers RULE yes
MODEL-004 Readers must be linked to a door RULE yes
MODEL-005 Category must be a schema enum value RULE yes
MODEL-006 Relationships use parent-child or deviceRef only RULE (principle) partial
MODEL-007 Door assembly uses conventional deviceRef names RECOMMENDATION yes
MODEL-008 Detectors reference their partitions via partitions[] RULE (intrusion) yes
MODEL-009 Bus modules carry Comm (+ Tamper/Power) RECOMMENDATION yes
MODEL-010 Reader-to-door direction (entry/exit) is explicit RECOMMENDATION partial
MODEL-011 Documented hardware variants are represented or scoped RECOMMENDATION no
MODEL-012 Access readers follow physical topology RULE (access) partial
MODEL-013 Door command taxonomy distinguishes strike release from deadbolt lock/unlock RULE (access) partial
MODEL-014 Enumerable panels are discovered, not hand-authored RECOMMENDATION no
MODEL-015 Framework state must not compete with device-owned state; every property earns its place RULE partial

MODEL-001: Category implies its canonical function

Strength: RULE · Checkable: yes

A device type's category determines the function it is expected to carry. Deviations almost always mean a mis-modelled Thing (e.g. a single box with every function, or a wrong category).

category canonical function(s) evidence
detector IntrusionDetector 8/8
partition Partition 7/7
door Door 5/6
output OutputSupervised) 8/9
reader ReaderSecurityKeypad) 6/7
keypad SecurityKeypad and/or Reader
camera / channel video functions / StatusPoll
module CommTamper/Power) — see MODEL-009
  • Detection: for each device type, check the category's canonical function is present.
  • Violation: category: detector with no IntrusionDetector; or a category carrying functions from a different domain (a reader carrying Output+Input+Door).

MODEL-002: Access points — doors always, areas/partitions/floors when the panel authorizes them

Strength: RULE · Checkable: yes

access_point: true marks a Thing whose access a person's grant can target; the adapter then derives the panel's native form from person.Devices. It is a per-device-type flag, not a door-only concept.

  • A category: door Thing MUST set access_point: true — authorization targets the door. (5/6.)
  • Any OTHER category the panel authorizes as a unit — partition/area, floor, occasionally zone — MUST also set access_point: true, so the grant flows through the normal access path, NOT a static profile/config mask. Precedent: intrusion panels that authorize users per partition model that partition as an access_point: true Thing (the wire bitmask is derived from person.Devices, not operator-entered).

Violation:

  • a door with access_point absent/false — verify the exception is intentional (e.g. a door modelled only for monitoring, not access); OR
  • a panel that authorizes users per area/partition/floor but models it as an operator-entered bitmask profile field instead of an access-point Thing — this also breaks the entity-typed access convention (no area_mask / door_flags relationship bitmasks).

MODEL-003: apb_point belongs on readers

Strength: RULE · Checkable: yes

Antipassback resolution happens at the reader. apb_point: true should appear only on reader (occasionally keypad) categories, never on doors/modules/outputs. (3/7 readers; 1/5 keypads.)

Violation: apb_point: true on a non-reader/keypad category.


MODEL-004: Readers must be linked to a door

Strength: RULE · Checkable: yes

In an access-control adapter, every reader must be resolvably linked to a door — via deviceRef (door.entry_reader: ReaderType, door.exit_reader: ReaderType, or reader-to-door deviceRef) or the framework IThingRelationResolver relation. Parent-child is valid only when it also represents physical containment/topology; do not make a reader a child of a door just to express the logical access relationship. A reader with no path to a door cannot grant access.

See concepts/access-control-assembly.md.

  • Detection: for each reader, look for (a) a deviceRef from a door to it, (b) a deviceRef from it to a door, or (c) a documented topology relation that the resolver maps to a door.
  • Violation: readers and doors both present but no link between them.

MODEL-005: Category must be a schema enum value

Strength: RULE · Checkable: yes

category must be one of: door, reader, panel, module, input, output, partition, keypad, detector, nvr, channel, camera. (Also enforced by adapter-registration.schema.json, but the review checks it without running schema validation.)

Violation example: category: access-control or category: acme-access-panel (found in a non-shipping/test adapter).


MODEL-006: Relationships use parent-child or deviceRef only

Strength: RULE (principle) · Checkable: partial

The only two ways to express a relationship between Things are parent-child (parents) and deviceRef (a property typed as a device type). Do not invent other linkage (string id fields, free-text references). Parent-child = containment/addressing; deviceRef = peer/assembly/shared links.

Detection (partial): flag properties that look like cross-references but are typed as string/int and named like a device type (door_id, reader_ref) instead of using a deviceRef type.

Access-model analogue: the same principle applies to relationships between access-model entities (access-model.yaml) — every cross-entity reference must be an entity-typed property, never an integer, id/address scalar, or bit mask (door_flags, area_mask). This one is a hard runtime-correctness rule, not just a structural preference, because the framework derives dependency ordering from typed references; see RULE-040.


MODEL-007: Door assembly uses conventional deviceRef names

Strength: RECOMMENDATION · Checkable: yes

When wiring a door via deviceRef, use the established names so the assembly is recognisable: entry_reader, exit_reader (→ reader), strike / relay_output (→ output), door_contact (→ input or detector), rex (→ input).

Advisory: a door deviceRef with an ad-hoc name (reader1, lock_relay) — works, but prefer the convention.


MODEL-008: Detectors reference their partitions via partitions[]

Strength: RULE (intrusion adapters) · Checkable: yes

Zone-to-partition assignment is a fundamental EZS mechanism. For any adapter with security_domains: ["intrusion"], detectors must express partition membership via a partitions: PartitionType[] deviceRef property.

Violation: a detector in an intrusion adapter with no partitions deviceRef.

Opt-out: if the protocol genuinely cannot provide zone-partition mapping (not exposed via any command, query, or configuration dump), the adapter must document this limitation explicitly in CHECKLIST.md under "Known limitations" with:

  • which protocol commands/queries were checked
  • confirmation that the mapping is not available at runtime or config-time
  • whether manual zone-partition assignment in PQ config is a viable workaround

Without this documentation, missing partitions[] is a violation, not an advisory.

Review note: security_domains names the adapter's primary technical domain, not every domain whose Thing functions appear in the model. Do not require additional security_domains entries solely because an intrusion adapter exposes access, fire, safety, or I/O functions where the hardware naturally overlaps those domains.


MODEL-009: Bus modules carry Comm (+ Tamper/Power)

Strength: RECOMMENDATION · Checkable: yes

category: module (bus expanders, door-control modules) typically expose Comm for link supervision plus Tamper/Power where the hardware reports them. (10/10 modules under a panel.)

Advisory: a module with no supervision functions — confirm the hardware truly reports nothing.


MODEL-010: Reader-to-door direction (entry/exit) is explicit

Strength: RECOMMENDATION · Checkable: partial

A door's reader topology must be unambiguous. Two valid shapes:

  • single reader + REX egress — the door has an entry_reader and a REX (Door.rex deviceRef or the Door function's REX handling); egress is not a second reader.
  • entry + exit readers — the door has both entry_reader and exit_reader deviceRefs.

In/out is the reader's direction access-event parameter, not a separate access-control path. If the protocol or hardware documents a two-reader or antipassback-capable variant (APB implies directional in/out readers), model exit_reader — or record a note/gap that only the single-reader variant is in scope. Don't silently assume one reader.

Advisory when: a door has only entry_reader (no exit_reader, no rex) while the device is APB-capable or has a documented two-reader variant, and there is no explaining note. See concepts/access-control-assembly.md.


MODEL-011: Documented hardware variants are represented or explicitly scoped

Strength: RECOMMENDATION · Checkable: no (needs documentation cross-reference)

When the protocol documents multiple hardware variants of one device that differ in capability — reader count, I/O count, antipassback, credential technology — the model must either express the differences (variant types via extends, capability properties, max_siblings, or optional deviceRefs) or document the collapse and which variant(s) are in scope. Collapsing all variants into one type_id silently hides capabilities (e.g. a 2-reader or APB-capable variant) from the developer.

Advisory when: the survey/bridge lists several hardware variants but the device tree has a single undifferentiated type with no scope note.


MODEL-012: Access readers follow physical topology

Strength: RULE (access adapters) · Checkable: partial

If the protocol or hardware exposes access events, credential reads, or enrollment at a reader/head, model that reader/head as a category: reader Thing. Access granted/denied/unknown and enrollment events are reader-scoped because occupancy and direction are computed from the reader, not from the door.

Place reader Things under the physical device where they are connected: panel, controller, expander, door module, or keypad. Link the reader to its logical door with a deviceRef (entry_reader, exit_reader, or a reader-side door reference) rather than moving the reader under the door just to show the access relationship.

Violation: access events are emitted by a protocol reader/head but the model has no reader Thing; or a reader is parented under a door even though the vendor topology says it is physically hosted by a panel/expander/module and the door link could be represented with deviceRef.


MODEL-013: Door command taxonomy distinguishes strike release from deadbolt lock/unlock

Strength: RULE (access adapters) · Checkable: partial

Door/strike terminology is easy to conflate. Model vendor commands by physical effect:

  • momentary strike release, relay pulse, or "open door" → pq.command.access.open and Door Unsecured*
  • permanent strike release / hold-open → pq.command.access.open.permanent and Door HoldUnsecured (lock.held)
  • cancel hold-open ("close door") → pq.command.access.close (display "Close") and Door Secured* (lock.secured)
  • entry denied — deadbolt engage, panel lockout, or vendor "disable/block door" → pq.command.access.lock and Door Locked* (lock.deadbolt); the mechanism is a wire detail, a PQ door has ONE denied state; register ONLY when the vendor verifiably denies a valid card
  • undo the lock — deadbolt retract, lockout end → pq.command.access.unlock and Door Unlocked* (lock.secured); unlock is strictly the inverse of lock, it does NOT cancel hold-open

Violation: a vendor "open door" / strike release command is exposed as pq.command.access.unlock, or a strike release confirmation calls Unlocked/UnlockedRemote instead of Unsecured/UnsecuredRemote, or a door registers pq.command.access.disable/.enable (those are non-door device commands — readers, modules, terminals; a denied door is access.lock).


MODEL-014: Enumerable panels are discovered, not hand-authored

Strength: RECOMMENDATION · Checkable: no (needs protocol-capability knowledge)

If the protocol exposes a way to enumerate the panel's configured objects — read back which zones/partitions/outputs/readers/doors/modules exist, plus their names and relationships — the adapter should implement device discovery rather than requiring the whole device tree to be authored by hand. Discovery populates only the facts the protocol actually exposes; bindings the protocol cannot determine (e.g. which reader serves which door) are left for the operator to configure. Re-discovery preserves those unset bindings — see the reimport reconciliation contract in patterns/14-device-config-import.md.

Rationale: hand-authoring a full device tree is error-prone and does not scale; when the protocol can report the inventory it is the correct source of truth. Leaving operator-only bindings (like reader↔door wiring) untouched is safe because a deviceRef the adapter never assigns defaults to preserve on re-import — the operator's wiring survives even though discovery is otherwise authoritative (scalars it omits are removed).

Advisory when: the protocol is known to support enumeration but the adapter ships no discovery capability — flag it as an improvement recommendation, not a hard failure (some protocols genuinely cannot enumerate). You should attempt discovery when the protocol supports it. Conversely, if a protocol genuinely cannot enumerate its configuration, document that limitation with citation rather than silently omitting discovery.


MODEL-015: Framework state must not compete with device-owned state; every property earns its place

Strength: RULE · Checkable: partial (needs protocol/status-page knowledge)

Two failures that both surface only on hardware, so the model must be checked for them up front:

(a) No duplicate owner of a state. When the device reports a state on a status page the adapter polls (lock/relock, output on/off, armed, tamper), the framework must not also simulate that state from a timer or derived counter. Enabling use_framework_timers: true on a door whose panel reports its own lock state means two owners race: the framework's access-grant timer says "unsecured" while the panel's poll says "secured", and the operator sees a door stuck open after the panel already relocked it. Set use_framework_timers: false (and the equivalent for any framework-derived state) when the device owns and reports that state; let status polling be the sole owner and keep async events as the audit source. use_framework_timers: true is correct only when the device does not report the post-action state (no lock monitor, no relock event).

(b) Every property earns its place. An operator-facing property is valid only if the adapter programs it onto the device, derives behaviour from it, or the operator must set it for correct operation. A property that is displayed but never read (e.g. an lcd_type the adapter ignores) is dead UI — remove it, or hide it with private_when if the framework surfaces it by default.

Violation: use_framework_timers: true (or equivalent framework-simulated state) on a Thing whose status poll already reports that same state; or a declared property the adapter neither programs, derives behaviour from, nor requires the operator to set.

Rationale: both were caught only by hardware testing (a panel's door lock state, and an lcd_type and door timers on another) and required manual correction. A framework state machine running in parallel with an authoritative device status page will desync on any missed or reordered update, and there is no self-correction; a property with no function is cognitive load with no payoff.