Skip to content

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.
  • 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.