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:
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:
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 contractdocker run -d \
-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
make build
./bin/openccu-loom run --config config.yaml
First-run setup:
http://localhost:8081/setup and create the first admin./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:
DELETE /api/v1/config/sections/{section} reverts to the YAML
fallback.The shipped config.example.yaml is intentionally short — see
config.example.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:
--include-secrets is passed; orpassword_env: MY_VAR_NAME in
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.yaml
Then 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-secret
services:
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.yaml
Mount 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.
aiohomematic on the wire — every
CCU interface, every device profile, every reliability invariant
(circuit breaker, retry, throttle, request coalescer, ping/pong)./set topic subscriptions. Pure
Go MQTT 3.1.1 client (no paho dependency).assets/openapi.yaml) and 85 WebSocket
commands, OpenAPI 3.1 contract, RFC 9457 problem+json,
Idempotency-Key middleware.go:embed) as the primary surface; an HTMX + html/template
fallback covers login, setup wizard, and status dashboards without
JavaScript. Locale-aware i18n (de + en).CGO_ENABLED=0) for Linux
amd64 / arm64 / armv7, plus macOS, Windows (best-effort). Docker
image published to ghcr.io/sukramj/openccu-loom.aiohomematicThe 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 + internal/boundary.Execute |
| 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 buildx
Prerequisites: 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.