A standalone Go daemon that talks to Homematic and HomematicIP CCUs (CCU2, CCU3, RaspberryMatic) over XML-RPC, BIN-RPC, and JSON-RPC, and bridges them to MQTT (Home Assistant Discovery + raw topic plane), a REST + WebSocket API, and a web-based configuration UI.
OpenCCU-Loom is a Go port of the Python library
aiohomematic that adds a
standalone-daemon surface (MQTT, REST + WebSocket, Config UI, Matter)
on top of CCU connectivity. The two projects coexist: aiohomematic
remains the library powering the Home Assistant integration
Homematic(IP) Local; OpenCCU-Loom is the self-contained daemon for
users who want MQTT / REST / Web-UI / Matter access without running
Home Assistant.
Two parts, both deliberate:
- OpenCCU is the upstream this project extends: github.com/OpenCCU/OpenCCU (openccu.de) — a Buildroot-based, cloud-free smart-home OS for the HomematicIP CCU (CCU3 / ELV-Charly) that runs on Raspberry Pi, x86/ARM, Proxmox, Docker, LXC, Kubernetes. OpenCCU-Loom is an extension that adds the daemon surface OpenCCU itself doesn't ship: MQTT (HA Discovery + raw plane), a REST + WebSocket API, a Svelte-based Config UI, and a native-Go Matter bridge. The same OpenCCU tree is also the source for the openccu-data catalogue (extracted translations, easymodes, device profiles), which OpenCCU-Loom embeds at build time — so OpenCCU is both the runtime host the daemon ships alongside and the upstream the daemon's metadata is derived from.
- Loom describes what the daemon actually does. A wiring loom is the bundled, ordered cable harness that runs through any car or aircraft — many individual conductors gathered into one routed path. That is the daemon's role exactly: many CCU wire protocols (XML-RPC, BIN-RPC, JSON-RPC, push callbacks across HmIP-RF, BidCos-RF, BidCos-Wired, VirtualDevices, CUxD) come in on one side; multiple north-bound protocols (MQTT, REST, WebSocket, Matter) come out on the other. The mark — four parallel strands held by a bundling band — depicts that cross-section directly.
Together: a loom for OpenCCU that weaves the proprietary wire side into the standard-protocol side.
0.1.0. The daemon ships with all four north-bound bridges
working end-to-end against a real CCU and the godevccu simulator:
- MQTT (HA Discovery + raw plane), REST + WebSocket, Svelte SPA + HTMX bootstrap, and a native-Go Matter bridge with PASE/CASE, IM Read/Write/Invoke/Subscribe/TimedRequest, 12 generic measurement cluster servers, 12 bridge-core clusters, and Custom-DP projections for switch / light / cover / climate / lock / siren / valve.
- Production-grade Matter attestation requires vendor-supplied DAC/PAI/CD bundles via config; the bundled CSA Test PAA chain works for development and Apple-/Google-/chip-tool-driven testing.
- Reliability layer (circuit breaker, retry, throttle, request coalescer, ping/pong) is parity-tested against the upstream reference values.
Canonical references:
SPECIFICATION.md— complete 0.1.0 specificationCHANGELOG.md— release historydocs/user-guide.md— operator install + config walkthroughdocs/SECURITY.md— threat model + audit checklistdocs/caching.md— every cache layer + boot-time radio costdocs/adr/— architecture decisions (Matter bridge in ADR 0012, commissioning bring-up in ADR 0013)assets/openapi.yaml— REST API contract
docker run -d --restart unless-stopped \
-p 8080:8080 -p 8081:8081 -p 8120:8120 -p 8129:8129 \
-v $(pwd)/config.yaml:/app/config.yaml:ro \
-v openccu-loom-data:/app/var \
ghcr.io/sukramj/openccu-loom:latest run --config /app/config.yaml
--restart unless-stopped(already set indocker-compose.yaml) lets the Config UI's Restart action work: the daemon exits on restart and Docker brings the container back. Without a restart policy the container would stay stopped. (The Home Assistant add-on handles this in-container via s6-overlay.)
make build
./bin/openccu-loom run --config config.yamlAdd https://github.com/SukramJ/openccu-loom as a repository under
Settings → Add-ons → Add-on Store → ⋮ → Repositories, then install
OpenCCU-Loom. The Config UI appears as a sidebar panel (Ingress) and
on :8080; state persists in the add-on's /data. See
packaging/ha-addon/README.md. For native
CCU3 / RaspberryMatic installs, see
packaging/ccu-addon/README.md.
First-run setup:
- Start the daemon without a user pre-configured.
- Open
http://localhost:8081/setupand create the first admin. - Sign in at
/login— OIDC is supported when configured.
OpenCCU-Loom splits its configuration into three tiers so the SPA can drive almost everything at runtime without forcing operators to hand-edit YAML for every change.
| Tier | Lives in | What goes there | Edit via |
|---|---|---|---|
| Bootstrap | config.yaml |
data_dir, north.rest.listen, north.ui.listen, logging.{level,format}, bootstrap.allow_first_run_setup, env_file |
Edit the YAML + restart |
| Live | SQLite (<data_dir>/openccu-loom.db) |
Everything else — CCUs, MQTT, Matter, mDNS, CORS, OIDC, rate-limit, reliability tunables, users, API tokens | SPA Settings tab, or REST PUT /api/v1/config/sections/{section} |
| Secrets | environment (process or .env file) |
CCU passwords, MQTT password, OIDC client secret | Operator-owned; daemon never writes them back |
The runtime daemon overlays the live tier on top of whatever the YAML provides, so:
- an empty database starts from the YAML — that's the seed.
- edits in the SPA win on the next restart.
- deleting a section row in the database via
DELETE /api/v1/config/sections/{section}reverts to the YAML fallback.
The shipped example.config.yaml is intentionally short — see
example.config.yaml. Everything in the
"live" tier can still be set declaratively in YAML if you prefer
GitOps + restart over live-edit; the SPA is just an additional
surface, not a replacement.
For the complete field schema (every available knob with its
classification basic / expert / secret), call
GET /api/v1/config/schema once the daemon is up; the SPA reads
the same endpoint to build its editors.
OpenCCU-Loom never persists CCU / MQTT / OIDC passwords to YAML unless you put them there yourself. The recommended path is:
- type the password directly into the SPA's CCU dialog → stored
encrypted-at-rest by the OS (SQLite file is mode 0600), redacted
from backup tarballs unless
--include-secretsis passed; or - keep the password in your shell / systemd / Docker / Kubernetes
secret store and reference it via
password_env: MY_VAR_NAMEin the SPA's CCU dialog (expert mode).
The supported env hooks are:
| Secret | Resolution |
|---|---|
| CCU password (per-CCU) | centrals[].password_env in the SPA / DB. Daemon reads os.Getenv(<that-name>) when it dials the CCU. |
| MQTT broker password | OPENCCU_LOOM_MQTT_PASSWORD |
| OIDC client secret | OPENCCU_LOOM_OIDC_CLIENT_SECRET |
export CCU_HOME_PASSWORD='your-ccu-password'
export OPENCCU_LOOM_MQTT_PASSWORD='your-mqtt-password'
./bin/openccu-loom run --config config.yamlThen in the SPA's CCU dialog, set Password env-var to
CCU_HOME_PASSWORD. The daemon resolves it on every CCU dial; the
password never lands in config.yaml, the SQLite database, or a
backup tarball.
EnvironmentFile= is the cleanest way to pin secrets to a
service unit without exposing them on the command line:
# /etc/systemd/system/openccu-loom.service
[Service]
EnvironmentFile=/etc/openccu-loom/secrets.env
ExecStart=/usr/local/bin/openccu-loom run --config /etc/openccu-loom/config.yaml# /etc/openccu-loom/secrets.env (chmod 0600, owner root)
CCU_HOME_PASSWORD=your-ccu-password
OPENCCU_LOOM_MQTT_PASSWORD=your-mqtt-password
OPENCCU_LOOM_OIDC_CLIENT_SECRET=your-oidc-secretservices:
openccu-loom:
image: ghcr.io/sukramj/openccu-loom:latest
env_file:
- secrets.env
volumes:
- ./config.yaml:/app/config.yaml:ro
- openccu-loom-data:/app/var
command: run --config /app/config.yamlMount the secrets.env file outside the image, never bake it in.
For Kubernetes use a Secret with envFrom: — same shape.
If you cannot use env variables (one-off dev installs), set
security.allow_plaintext_secrets: true via the Settings section
in the SPA. With the toggle on, the per-CCU dialog accepts a
plaintext fallback in addition to password_env. The plaintext
value is then stored in the SQLite centrals table; do not enable
this on a production host.
- Full south-bound parity with
aiohomematicon the wire — every CCU interface, every device profile, every reliability invariant (circuit breaker, retry, throttle, request coalescer, ping/pong). - MQTT bridge with Home Assistant Discovery and a raw topic
plane; bidirectional control via
/settopic subscriptions. Pure Go MQTT 3.1.1 client (nopahodependency). - REST + WebSocket API — ~90 REST endpoints (full catalogue in
assets/openapi.yaml) and 85 WebSocket commands, OpenAPI 3.1 contract, RFC 9457 problem+json, Idempotency-Key middleware. - Configuration UI — Svelte 5 SPA (Tailwind 4 + Vite, embedded
via
go:embed) as the primary surface; an HTMX +html/templatefallback covers login, setup wizard, and status dashboards without JavaScript. Locale-aware i18n (de + en). - Multi-CCU: one daemon, many CCUs, all scoped cleanly.
- Authentication: HTTP Basic, Bearer tokens, session cookies with CSRF, OpenID Connect (PKCE + JWKS-verified RS256 signatures).
- Static single binary (
CGO_ENABLED=0) for Linux amd64 / arm64 / armv7, plus macOS, Windows (best-effort). Docker image published toghcr.io/sukramj/openccu-loom.
The two projects are designed for different consumers and therefore diverge on the north-bound side. Wire-level behaviour and the device profile catalogue are kept in lockstep.
| Area | aiohomematic | OpenCCU-Loom |
|---|---|---|
| Language | Python 3.14 (asyncio) | Go 1.26+ |
| License | MIT | MIT |
| Primary consumer | Home Assistant integration | Standalone daemon (MQTT / REST / UI / Matter) |
| CUxD transport | JSON-RPC via CCU facade + MQTT workaround | Native BIN-RPC + BIN-RPC callback server |
| Multi-CCU | one CentralUnit per process |
many CentralUnits per process |
| Config | programmatic (Pydantic) | YAML (with defaults) |
| Persistence | JSON files | SQLite WAL + filesystem under data_dir/ |
| UI | none (HA provides one) | built-in Svelte 5 SPA (HTMX fallback for no-JS) |
Decorators (@state_property, @inspector) |
Python runtime | Go struct tags; @inspector wrapping handled inline at call sites |
| Device profiles | hand-written Python | generated from the aiohomematic registry, plus hand-written Go wrappers |
OpenCCU-Loom hat vier strukturelle Säulen, die Architektur-Drift verhindern:
| Säule | Tool | Was wird detektiert |
|---|---|---|
| 1. Reachability | make reachability |
Dead-Code (exported ohne Production-Caller) |
| 2. Pin-Tests | tests/contract/wiring_pins/ |
Wiring-Regressionen |
| 3. Wire-Snapshots | make wire-snapshots |
Custom-DP-Wire-Encoding-Drifts |
| 4. E2E-Smoke | make e2e |
Feature-Drifts zur Laufzeit |
Details: docs/parity/structural-approach.md
make build # produces ./bin/openccu-loom
make test # unit + contract tests
make integration # godevccu + Mosquitto integration tests (Mosquitto needs Docker)
make bench # benchmarks
make lint # golangci-lint (null findings required)
make docker # multi-arch Docker image via buildxPrerequisites: Go 1.26+, golangci-lint, gofumpt, goreleaser,
Docker (+ buildx) for the Mosquitto-backed integration tests.
Integration runs use godevccu,
a pure-Go HomeMatic CCU simulator embedded as a regular module
dependency — no Python toolchain required.
The OpenCCU-Loom source code is licensed under the MIT License — aligned with the rest of the aiohomematic ecosystem (aiohomematic, aiohomematic-config, openccu-data).
The binary distribution additionally ships CCU metadata archives
sourced from openccu-data.
Those files (internal/ccudata/embedded/*.json.gz,
internal/ccudata/embedded/profiles/*.json.gz) are governed by the
eQ-3 HomeMatic Software License — free for private and
non-commercial use; commercial redistribution requires written
permission from eQ-3 AG. See
internal/ccudata/embedded/NOTICE
for the full terms and
docs/adr/0003-embed-occu-extracts.md
for the rationale.
Operators with commercial use-cases can swap the embedded archives
out via cfg.CCUData.{translations_path,easymode_path} for
self-licensed equivalents — the daemon degrades gracefully.
SPECIFICATION.md— design intent, hard constraints, resolved decisions.CLAUDE.md— entry point for AI assistants and fresh contributors.docs/adr/— architecture decisions.docs/external-clients/— wire contract for Python / TypeScript / Rust clients. Start with the topic hierarchy for the WebSocket surface and the asks backlog for the closure-index. The contract architecture itself is locked in ADR 0020, ADR 0021 (mDNS auto-discovery), and ADR 0022 (WS resume + envelope kind).
CONTRIBUTING.md covers local setup, PR
expectations, and the release workflow. Before opening a PR, please
open an issue so we agree on scope, especially when the change touches
the wire layer or the device profile catalogue.
- aiohomematic — the reference implementation this Go port follows for wire behaviour and the device profile catalogue.
- The Homematic / HomematicIP community and eQ-3 for the devices and protocol knowledge that make any of this possible.