Architecture Decision Records¶
Architecture Decision Records (ADRs) capture the consequential design choices behind OpenCCU-Loom — the decision, its context, and its consequences — so future readers can understand why the code looks the way it does.
Who this page is for
Contributors and developers who need the rationale behind a design choice. ADRs are immutable once landed: a superseded decision gets a new ADR rather than an edit to the old one.
The table below catalogues every ADR. Each entry links to the record itself — except the Matter wire-stack decisions, which moved to go-fabric with the code they describe. Their numbers are kept here as forwarding pages so existing links and bookmarks keep resolving, and those numbers are never reused: the next ADR continues from the highest number in this table.
| # | Title | Summary |
|---|---|---|
| 0001 | License: MIT | The source is licensed MIT. |
| 0002 | Multi-CCU as a first-class feature | One daemon serves many CCUs, scoped by central_name, from 0.1.0. |
| 0003 | Embed openccu-data metadata artifacts | Translations, easymodes, and profiles are embedded from openccu-data. |
| 0004 | Python decorators ↔ Go cross-cutting | How decorator-based concerns from the Python reference map to Go. |
| 0005 | Visibility as outbound filter | Visibility is applied as an outbound filter on REST and MQTT. |
| 0006 | Naming conventions for REST + MQTT | Shared naming rules across the REST and MQTT surfaces. |
| 0007 | Strong model: Source interface | A single Source interface backs both reads and writes. |
| 0008 | AggregatedState default-flip | Flips the AggregatedState default and removes the legacy path. |
| 0009 | Service-method command topics | Service-method command topics in HA Discovery. |
| 0010 | Discovery payload from the model | HA-Discovery payload construction moves into the model. |
| 0011 | MQTT topic & payload architecture | The topic and payload structure for the MQTT plane. |
| 0012 | Matter bridge: pure-Go | The Matter bridge is implemented in pure Go, no CGo SDK. |
| 0013 | Matter wire-protocol design rules | Moved to go-fabric — the record is maintained as go-fabric ADR 0001. This page forwards. |
| 0014 | Parameter ignore / un-ignore mechanics | How parameters are ignored and un-ignored. |
| 0015 | Split Ignored from NoCreate | Separates Ignored from NoCreate in DataPointUsage. |
| 0016 | Custom-DP-aware UI rendering | UI rendering is aware of custom data points. |
| 0017 | Logging and diagnostics | The logging and diagnostics model. |
| 0018 | Health tracker parity | The health tracker mirrors aiohomematic. |
| 0019 | Persistent VALUES cache | A persistent VALUES cache with a wire-DP lifecycle. |
| 0020 | External-client wire contract | The wire contract external clients depend on. |
| 0021 | mDNS self-advertisement | mDNS self-advertisement for LAN auto-discovery. |
| 0022 | WebSocket resume cursor + kind | The WebSocket resume cursor and envelope kind discriminator. |
| 0023 | HmIP-Wired is a ProductGroup | HmIP-Wired is modeled as a ProductGroup, not an interface. |
| 0024 | Instance vs CCU identity | Daemon-instance vs CCU identity, and the two interface ids. |
| 0025 | MCP server as a north-bound adapter | The MCP server is a north-bound adapter. |
| 0026 | MCP dev-mode | A build-tag-gated MCP introspection surface. |
| 0027 | Encrypt config secrets at rest | Config secrets are encrypted at rest. |
| 0028 | Contract digest & version guard | A contract digest and version guard couple API-schema changes to the types-repo release. |
| 0029 | Tier-model teardown for Unit.Stop | Unit.Stop tears down subsystems in tiered order. |
| 0030 | Event-bus dispatch striping: rejected | Per-central isolation already meets the goal; dispatch striping is rejected. |
| 0031 | IM opcode dispatch seam | Moved to go-fabric — the record is maintained as go-fabric ADR 0002. This page forwards. |
| 0032 | Sigma resumption extraction | Moved to go-fabric — the record is maintained as go-fabric ADR 0003. This page forwards. |
| 0033 | Groups cluster stays a stub | Moved to go-fabric — the record is maintained as go-fabric ADR 0004. This page forwards. |
| 0034 | Adapter package taxonomy | internal/central/adapter stays one package with a documented taxonomy. |
| 0035 | Hub refresh-set extraction | The refresh-coordination sub-component is extracted from HubCoordinator. |
| 0036 | Matter Bridge decomposition | Moved to go-fabric — the record is maintained as go-fabric ADR 0005. This page forwards. |
| 0037 | OTLP span exporter | A pluggable span exporter ships a lean OTLP/HTTP exporter, not the OTel-gRPC SDK. |
| 0038 | Cross-stack CI gate | The cross-stack model-snapshot parity gate runs in nightly CI. |
| 0039 | Subscribe dispatch seam | Moved to go-fabric — the record is maintained as go-fabric ADR 0006. This page forwards. |
| 0040 | Measurement history | Measurement history is stored in embedded SQLite with an opt-in push exporter. |
| 0041 | Persist auth sessions | Auth sessions persist in SQLite as a save-through cache. |
| 0042 | Clear CCU cache and re-pull | Clearing CCU-derivable caches and re-pulling is a first-class operation. |
| 0043 | CCU as an authentication provider | Login can be delegated to a CCU's own user database. |
| 0044 | Single-port onboarding + HA Ingress auth | Onboarding runs on a single port with HA Ingress auth passthrough. |
| 0045 | Login + onboarding into the SPA | Login and first-run onboarding live in the Svelte SPA. |
| 0046 | SSDP CCU discovery | CCUs are discovered on the LAN via active SSDP/UPnP. |
| 0047 | North-bound bridge registry | North-bound bridges are Services owned by a Registry. |
| 0048 | chip-tool ↔ godevccu send/receive matrix | Moved to go-fabric — the record is maintained as go-fabric ADR 0007. This page forwards. |
| 0049 | Matter one endpoint per device | Matter exposes one endpoint per physical device by default. |
| 0050 | MQTT transport → shared go-mqtt module | The in-tree MQTT transport is extracted into the external shared go-mqtt module. |
| 0051 | North-bound authorization model | Role-based MinRole gating unified across REST + WS, plus backup-at-rest sealing. |
| 0052 | Daemon-level alarm MQTT topics | The alarm engine publishes panel state/commands on daemon-level topics, not per-central ones. |
| 0053 | CCU metadata via go-openccu-data | Embedded OCCU extracts come from the versioned go-openccu-data module instead of a hand-synced copy. |
| 0054 | Remote ingress proxy add-on | OpenCCU-Loom Remote: an HA Ingress panel for remote instances via token injection, instead of the local-only Ingress passthrough. |
| 0055 | Heating groups via the CCU jpages proxy | Heating-group administration goes through the CCU's own jpages endpoints, the only interface that drives the member roster. |
| 0056 | Room areas, and the zone/area naming split | Operator-defined areas group CCU rooms one level up; the alarm partition is renamed "zone" to free the word. |
| 0057 | CCU add-on self-update | The CCU/OpenCCU add-on updates itself through the firmware's own install_addon path, with no WebUI round trip. |
| 0058 | mDNS TXT carries the configured CCU serials | Partially supersedes ADR 0021: the TXT record advertises the configured serials so clients can match before authenticating. |
| 0059 | The Security & Safety MQTT plane | Extends ADR 0052: hazard classes aggregate across centrals, so they get daemon-level topics too. |
| 0060 | loom-prefixed CCU-facing interface ids | Partially supersedes ADR 0024: the wire-boundary InitInterfaceID carries a loom prefix and drops the repeated central name. |
| 0061 | The migration Down path is unsupported | goose Down blocks exist to satisfy the tool; they are destructive and must never run in production. |
| 0062 | Suppression reasons are recomputed, not recorded | Extends ADR 0015: the Ignored mark says that a parameter is hidden, never which rule hid it, so the reason is re-derived from the same rule sets. |
| 0063 | Device profiles are maintained here, not generated | script/generate_profiles.py is removed and the catalogue becomes ordinary hand-maintained source; every deviation from the fork point is recorded in notes/parity/by_design.md. |
| 0064 | A garage drive projects as a Matter Closure, not a WindowCovering | Refines ADR 0049: the drive carries ClosureControl with named stops, so the WindowCovering projection and its lift-percentage machinery are removed. |
| 0065 | The composition root states its wiring, so a machine can check it | Each wire* function declares its seam into a manifest the daemon builds at start-up, turning "is X wired, and does it run before Y" from an approximate question into an exact one. |
| 0066 | Relicense to AGPL-3.0-only with a commercial exception | Proposed. MIT gives commercial reuse of the code away; AGPL plus a written exception makes monetisation ask first, while the licence stays OSI-approved. Supersedes ADR 0001 from v0.66.0. |
| 0067 | The north surface is a model API, not an HA entity projection | The REST/WS contract stays Home-Assistant-agnostic and the entity projection stays in the Python consumer; the MQTT plane keeps its own projection and is explicitly not a precedent. Additive model fields remain welcome — the line is drawn at descriptors. |
| 0068 | What unique_id promises, and it differs per plane | Changeable on REST/WS with a documented transition and an old key derivable from the payload; changeable on MQTT discovery too, but only as a documented break with a swept orphan, since Home Assistant has no migration path there. A generic previous_unique_id was designed and rejected. Amended 2026-08-30. |
| 0069 | The master-profile surface reads a link-paramset archive | The profiles/ archive is link-paramset data: the generator reads the firmware's LINK easymode tree and skips its MASTER tree, and the reference writes it with the peer channel address as the paramset key. master_profiles.* is a link surface under a wrong name, and apply writes link-derived values into MASTER — inert only because the lookup never matches. masterprofile is deleted in favour of linkprofile. |
| 0070 | Extract the HA discovery model into a shared module | Six projects carry their own copy of the MQTT/HA-discovery layer and the copies have drifted into real defects. Two modules are extracted: go-hamqtt (model, device-based discovery bundle, publisher runtime) and go-ha-catalog (the HA vocabulary, generated from the core checkout). This daemon migrates first, before any bridge — it is both the architectural source and the hardest consumer. Extends 0050. An amendment withdraws the unique-id re-key for this daemon, so 0068's guarantee holds: no entity is re-created. A second amendment records the bundle migration measured against a live instance: the registry entry survives, but only if the per-entity config is retracted before the bundle is published — the other order is refused with nothing but a log line. Closed 2026-09-14: all nine phases shipped — the closing section carries a per-phase outcome table, phase 9's deliberate decline of the device bundle (two UniFi consoles cannot be told apart), an erratum showing the LOC table understates every bridge row by 1.6x to 2.3x, and the two findings the fan-out produced that belong to no single repository. |
Related reading¶
- Architecture — how these decisions show up in the package layout.
- Matter parity contract — the host-side boundary. The projection decisions that stayed here are 0012 (cluster subset), 0021 (the daemon's own mDNS), 0049 (one endpoint per device) and 0064 (garage as Closure); the wire-side rules live in go-fabric.