Architecture¶
A map of how OpenCCU-Loom is put together: the hexagonal layering, the key packages, the multi-CCU model, the south-bound transports, and where to read deeper.
Who this page is for
Contributors and developers who need a mental model of the daemon before changing it. For the authoritative design intent, read SPECIFICATION.md; for implementation detail, read the code.
Hexagonal layering¶
OpenCCU-Loom follows a ports-and-adapters (hexagonal) design. North-bound adapters expose the domain to the outside world; the domain core holds all the logic; a south-bound adapter speaks the Homematic wire protocols to the CCU.
flowchart TB
subgraph North["North-bound adapters (internal/north)"]
mqtt[mqtt]
rest[rest]
ws[rest/ws]
ui[ui · Svelte SPA + no-JS /health,/about]
matteradapter[matteradapter · Matter glue]
mcp[mcp]
end
subgraph Core["Domain core"]
central[central · CentralRegistry / CentralUnit]
model[model]
client[client]
store[store · SQLite]
health[health]
end
subgraph South["South-bound transports (internal/client/transport)"]
xmlrpc[xmlrpc]
binrpc[binrpc]
jsonrpc[jsonrpc]
occulited[occulited]
end
fabric[[go-fabric module · Matter stack]]
ccu[(CCU / openccu-lite)]
North --> Core
matteradapter --> fabric
Core --> South
South --> ccu
ccu -. callbacks / event stream .-> Core The dependency rule points inward: adapters depend on the core, never the reverse. Cross-cutting protocol interfaces (the DI contracts shared by central, client, and north-bound adapters) live in pkg/interfaces.
Key packages¶
The public, thin library surface lives under pkg/; everything daemon-internal lives under internal/.
| Package | Role |
|---|---|
pkg/hmtypes, pkg/hmenum, pkg/hmerr | Primitive types, enums, error sentinels. |
pkg/hmevent | Domain event types carried on the bus. |
pkg/hmapi, pkg/hmproto, pkg/interfaces | External DTOs, Homematic wire shapes, DI contracts. |
internal/central | CentralUnit, coordinators, registries, the callback servers, scheduler. |
internal/client | InterfaceClient, transports, reliability (circuit breaker, retry, throttle), ReGa runner. |
internal/model | Domain model: devices, channels, data points, custom/calculated/combined profiles, schedules, hub. |
internal/north | REST, WS, UI, MQTT, MCP and webhook adapters, plus matteradapter — the host side of the Matter bridge, whose stack is the external go-fabric module. |
internal/store/matterendpoint | Matter endpoint identity and the exposure allowlist, keyed by the Homematic source identity. |
internal/wiring | The wiring manifest the composition root declares its seams into (ADR 0065). |
cmd/openccu-loom | The composition root — see Composition root and wiring. |
internal/store | SQLite persistence (migrations, sessions, paramsets, devices, audit) plus in-memory caches. |
internal/auth, internal/config, internal/health | Auth backends, config loading, the unified health tracker. |
internal/secret, internal/configstore | Secret handling and config-at-rest. |
internal/audit, internal/diagnostics, internal/metrics, internal/observability | Change-log, diagnostics, Prometheus collectors, tracing helpers. |
The full tree is described in CLAUDE.md.
Multi-CCU model¶
Multi-CCU is a first-class feature. One daemon serves many CCUs:
- One
CentralUnitis created per configured CCU. - A
CentralRegistryholds them all and is shared with the north-bound adapters. central_nameis the scoping dimension threaded through every cross-cutting call.
No code path assumes a single central. The rationale is in ADR 0002; the user-facing view is Multi-CCU.
South-bound transports and callback servers¶
Every interface pushes — callbacks on a CCU, the occulited event stream on an openccu-lite box. There is no polling-only path.
| Transport | Interfaces | Callback server |
|---|---|---|
| XML-RPC + JSON-RPC | HmIP-RF, BidCos-RF, BidCos-Wired, VirtualDevices | XML-RPC over HTTP on :8120 |
| BIN-RPC | CUxD | BIN-RPC over raw TCP on :8129 |
| occulited HTTP API (openccu-lite) | HmIP-RF, BidCos-RF, BidCos-Wired, VirtualDevices | none — one event stream per central (ADR 0072) |
HmIP-Wired is not a separate south-bound interface — ADR 0023 models it as a ProductGroup carried on the HmIP-RF interface.
For CCU centrals, two listeners run, one per protocol, shared across all centrals:
- XML-RPC over HTTP on
callback.port(default:8120), routed by URL path/RPC2/<central_name>. - BIN-RPC over raw TCP on
callback.bin_port(default:8129), routed byinterface_idin the envelope.
The XML-RPC listener binds a fixed port or the first free port of callback.port_range; the BIN-RPC listener binds a fixed port only. A port of 0 is not dynamic: it is replaced by the defaults 8120 and 8129. The effective port is re-advertised to the CCU on every init() and every reconnect. CUxD always speaks BIN-RPC — never treat it as JSON-RPC.
South profiles¶
A central picks its south-bound strategy once, at construction, based on its system_type: the CCU profile (ReGa, JSON-RPC, init callbacks) or the openccu-lite profile (the box's occulited HTTP API — an XML-RPC proxy, a push event stream, a metadata store instead of ReGa). Every downstream consumer — the description/paramset pipeline, the callback handlers, the model, every north-bound adapter — is shared between the two; the only place a central's system type is inspected is the selection itself.
flowchart LR
cfg["central.system_type"] --> select["southProfileFor()"]
select --> ccu["CCU profile\nReGa · JSON-RPC · init callbacks"]
select --> lite["openccu-lite profile\nocculited HTTP API · event stream · metadata store"]
ccu --> shared["shared: pipeline, model, north-bound adapters"]
lite --> shared A profile answers four questions the same way regardless of which central it belongs to: where readiness comes from, where events arrive from, which system services (power, backup, firmware) are wired, and what the hub bring-up step (identity, names, rooms) reads before devices load. What a central cannot do on its openccu-lite system — no ReGa, so no system variables, programs or the inbox — is expressed as an absent feature, never as a special case sprinkled through the shared code. The decision is recorded in ADR 0071.
Event bus¶
Cross-domain communication inside the core runs over an internal, generic, typed, priority-aware event bus (internal/central/events). It has no re-entrancy: handlers subscribe with a priority and an unsubscribe handle.
unsubscribe := bus.Subscribe(func(e hmevent.DataPointValueChanged) {
// handle
}, events.WithPriority(events.PriorityHigh))
defer unsubscribe()
Persistence¶
Persistence is SQLite via modernc.org/sqlite — pure Go, no CGo. The default build runs CGO_ENABLED=0 and ships as a single static binary. Schema migrations live under internal/store/sqlite/migrations/ and are applied with goose. Cache layers and boot-time CCU radio cost are documented in Caching.
Matter bridge¶
The Matter wire stack is not part of this repository. It lives in the go-fabric module — a semantic port of matter.js HEAD covering the TLV codec, the Interaction Model, PASE / CASE, MRP over IPv6 UDP, DNS-SD advertisement, the cluster servers and the endpoint assembler — which the daemon embeds as a library. What stays here is the host side: internal/north/matteradapter/ walks the device model and produces the flat endpoint specs go-fabric turns into a topology, internal/store/matterendpoint/ backs go-fabric's endpoint store with the persisted source → endpoint-id mapping and the operator's exposure allowlist, and each device profile projects itself in internal/model/custom/<dp>/matter.go. The binding contract and standing parity guards are described in the Matter parity contract; the boundary itself, and how a go-fabric release is taken, in The Matter boundary; the user-facing view is Matter.
Where to read deeper¶
SPECIFICATION.md— goals, non-goals, constraints, resolved decisions.- ADR index — every architectural decision, with rationale.
- Composition root and wiring — how subsystems are wired and which guards pin the wiring.
- The Matter boundary — go-fabric and the Matter glue that stays here.
- Release process — version carriers, changelogs, tag and follow-ups.
- Security guide — trust boundaries and the threat model.
- Source interface — the strong-model read/write contract.