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:
- Authoring — consult these conventions when you model a new adapter.
- 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 |
Output (± Supervised) |
8/9 |
reader |
Reader (± SecurityKeypad) |
6/7 |
keypad |
SecurityKeypad and/or Reader |
— |
camera / channel |
video functions / StatusPoll |
— |
module |
Comm (± Tamper/Power) |
— see MODEL-009 |
- Detection: for each device type, check the category's canonical function is present.
- Violation:
category: detectorwith noIntrusionDetector; or a category carrying functions from a different domain (areadercarryingOutput+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: doorThing MUST setaccess_point: true— authorization targets the door. (5/6.) - Any OTHER category the panel authorizes as a unit —
partition/area,floor, occasionallyzone— MUST also setaccess_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 anaccess_point: trueThing (the wire bitmask is derived fromperson.Devices, not operator-entered).
Violation:
- a door with
access_pointabsent/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_flagsrelationship 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_readerand a REX (Door.rexdeviceRef or the Door function's REX handling); egress is not a second reader. - entry + exit readers — the door has both
entry_readerandexit_readerdeviceRefs.
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.openand DoorUnsecured* - permanent strike release / hold-open →
pq.command.access.open.permanentand DoorHoldUnsecured(lock.held) - cancel hold-open ("close door") →
pq.command.access.close(display "Close") and DoorSecured*(lock.secured) - entry denied — deadbolt engage, panel lockout, or vendor "disable/block door" →
pq.command.access.lockand DoorLocked*(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.unlockand DoorUnlocked*(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.