Skip to content

Configuration Reference

The authoritative reference for every setting OpenCCU-Loom reads at startup: where configuration comes from, how the bootstrap and database tiers interact, and the keys, types, defaults, and environment overrides for each block.

Who this page is for

Operators who run the daemon and need to know exactly which key controls what. If you are setting up for the first time, start with the getting-started guide and come back here for the details.

How configuration is layered

OpenCCU-Loom resolves configuration from three sources, applied in this order (later wins):

  1. Compiled-in defaults — every field has a safe default.
  2. YAML config file (config.yaml) — read at startup.
  3. Environment variables (OPENCCU_LOOM_*) — a curated overlay.

On top of that, most settings are also editable at runtime through the web Config UI; those edits are stored in the SQLite database and win over the YAML on the next start.

Bootstrap tier vs. database tier

A small bootstrap tier is read directly from YAML before the database opens — it has to be, because it tells the daemon where the database lives and how to listen. The bootstrap tier is exactly: data_dir, logging, north.rest.listen (via listen.rest), bootstrap.allow_first_run_setup, and env_file.

Everything else is the database tier — including locale, the callback.* host/ports (they come up after the database opens), centrals, north.mqtt, auth, and Matter. On the first run the daemon seeds the database from your YAML once. From then on the database values win, and the Config UI is the place to edit them — editing the YAML alone will not change a setting that is already seeded. To re-seed from YAML you must clear the corresponding rows (see openccu-loom config import --replace) or start with a fresh data_dir.

This means: put data_dir, the REST listen address, and secrets in YAML/environment; manage centrals, MQTT, callback ports, auth, and Matter through the UI once the daemon is running.

Config file discovery

When you start the daemon with openccu-loom run and omit --config, it probes these locations in order and uses the first file that exists:

Order Location Notes
1 $OPENCCU_LOOM_CONFIG explicit operator override
2 ./config.yaml current working directory (in Docker, /app/config.yaml)
3 $XDG_CONFIG_HOME/openccu-loom/config.yaml or ~/.config/openccu-loom/config.yaml per-user
4 /etc/openccu-loom/config.yaml system-wide

If none exists the daemon runs on built-in defaults and logs no config file found, running with defaults. An explicit --config <path> always wins over discovery.

openccu-loom run --config /etc/openccu-loom/config.yaml

A minimal config.yaml

data_dir: /var/lib/openccu-loom
locale: en

centrals:
  - name: home
    host: 192.0.2.29
    username: Admin
    password: "your-ccu-password"
    interfaces: [HmIP-RF, BidCos-RF]

north:
  rest:
    listen: ":8119"

For the complete annotated set of every key, see example.config.full.yaml in the repository, and example.config.yaml for a shorter starter file.

Environment overlays

A narrow set of environment variables overrides the matching YAML field. They are useful for container deployments where the image carries the YAML and a handful of values are per-deployment. Variables that are unset leave the YAML value intact.

Variable Overrides Type
OPENCCU_LOOM_CONFIG config-file path (discovery) path
OPENCCU_LOOM_LOCALE locale string
OPENCCU_LOOM_DATA_DIR data_dir path
OPENCCU_LOOM_LOG_LEVEL logging.level string
OPENCCU_LOOM_LOG_FORMAT logging.format string
OPENCCU_LOOM_CALLBACK_HOST callback.host string
OPENCCU_LOOM_CALLBACK_PORT callback.port int
OPENCCU_LOOM_CALLBACK_BIN_PORT callback.bin_port int
OPENCCU_LOOM_CALLBACK_PUBLIC_HOST callback.public_host string
OPENCCU_LOOM_REST_LISTEN north.rest.listen string
OPENCCU_LOOM_REST_OPENAPI_VALIDATE OpenAPI request validation bool
OPENCCU_LOOM_REST_OPENAPI_SPEC_PATH external OpenAPI spec path path
OPENCCU_LOOM_MQTT_BROKER_URL north.mqtt.broker_url string
OPENCCU_LOOM_MQTT_TLS_CA_FILE north.mqtt.tls_ca_file path
OPENCCU_LOOM_MQTT_TLS_INSECURE_SKIP_VERIFY north.mqtt.tls_insecure_skip_verify bool
OPENCCU_LOOM_SECRET_KEY at-rest master key (see Secrets) base64
OPENCCU_LOOM_MQTT_PASSWORD MQTT broker password (runtime only) string
OPENCCU_LOOM_OIDC_CLIENT_SECRET OIDC client secret (runtime only) string

The daemon also reads an optional .env file from the working directory (or the path named by env_file: in the config) at startup. Real process-environment values always win over .env entries. See example.env for the full annotated list.

services:
  openccu-loom:
    image: ghcr.io/sukramj/openccu-loom:latest
    environment:
      OPENCCU_LOOM_DATA_DIR: /data
      OPENCCU_LOOM_CALLBACK_HOST: 192.0.2.10
    volumes:
      - openccu-loom-data:/data
export OPENCCU_LOOM_DATA_DIR=/var/lib/openccu-loom
export OPENCCU_LOOM_CALLBACK_HOST=192.0.2.10
openccu-loom run --config /etc/openccu-loom/config.yaml

Secrets

Secret-classed fields are encrypted at rest in the database with AES-256-GCM and stored with the prefix enc:v1:. The master key is resolved hybrid:

  1. OPENCCU_LOOM_SECRET_KEY — a base64-encoded 32-byte key. Wins when present. Generate one with openssl rand -base64 32.
  2. Otherwise an auto-generated key file at <data_dir>/secret.key (created with mode 0600 on first start).

If no key can be resolved or written, the two kinds of secret behave differently. A CCU password is refused rather than written in the clear; security.allow_plaintext_secrets: true lifts that refusal (ADR 0027). Secret fields in the configuration sections (for example the MQTT password or the OIDC client secret) are stored unencrypted instead, so keep a master key in place.

Back up secret.key with your database

Without the master key, encrypted secrets in a restored database cannot be decrypted. See Backup & restore and the security guide.

Fields treated as secrets include centrals[].password, north.rest.auth.users, north.rest.auth.tokens, north.rest.auth.oidc.client_secret, north.mqtt.password, and the Matter commissioning passcode/salt and DAC key path.


Configuration blocks

The field tables — type, class, restart flag and help text of every key — are generated from the code into the configuration reference. The sections below explain what the blocks do and name the compiled-in defaults the reference does not carry. Keys of class secret are encrypted at rest; the environment variables that override a key are listed under Environment overlays.

locale

Fields: see locale in the configuration reference. Defaults not shown in the reference: locale en.

UI/label language. Catalogues ship for en and de.

data_dir

Fields: see data_dir in the configuration reference. Defaults not shown in the reference: data_dir ./var.

Directory for the SQLite database (<data_dir>/openccu-loom.db), the auto-generated secret.key, and other on-disk state. This is a bootstrap-tier field — set it in YAML/environment.

logging

Fields: see logging in the configuration reference. Defaults not shown in the reference: logging.level info; logging.format json.

level is one of debug, info, warn, error. format is one of json, text, text-color. overrides maps a dot-separated subsystem path to a level, for static boot-time overrides; runtime overrides are installed (with a TTL) via the diagnostics API.

callback

The XML-RPC and BIN-RPC callback servers that receive push events from the CCU. The host/ports here must be reachable from the CCU.

Fields: see callback in the configuration reference. Defaults not shown in the reference: callback.host 0.0.0.0; callback.port 8120; callback.bin_port 8129; callback.max_connections 64.

port is the XML-RPC callback (HmIP/BidCos); bin_port is the BIN-RPC callback (CUxD). A value of 0 is not a dynamic port: it is replaced by the defaults 8120 and 8129. port_range (e.g. "30000-30099") applies to the XML-RPC listener and takes precedence over port: when it is set the listener binds the first free port inside the range and port is not used. A malformed range is rejected when the config is saved, not only at the next boot. public_host overrides the host advertised to the CCU when the daemon is behind NAT. The effective port is re-advertised to the CCU on every reconnect, so dynamic ports survive restarts.

max_connections caps the number of concurrent connections each callback listener accepts (0 uses the default of 64) — a guard against a misbehaving or hostile peer exhausting file descriptors. restrict_source_ips, when true, makes the listeners accept callbacks only from the configured CCU IPs (plus loopback); leave it false unless every CCU address is static and known. Both are read once, when the listeners bind, so changing either is restart-required — the config editor marks them accordingly.

north.rest and authentication

The REST + WebSocket server. This is the API surface and the backend for the SPA.

Fields: see north.rest in the configuration reference. Defaults not shown in the reference: north.rest.csrf_enabled true. north.rest.listen (default :8119, environment variable OPENCCU_LOOM_REST_LISTEN) is bootstrap-tier and therefore not in the reference.

north.rest.cors lists the browser origins allowed to call the API cross-origin. Entries are matched case-insensitively and a trailing slash is ignored, so https://ha.example.com/ and https://ha.example.com are the same entry. While csrf_enabled is true the same list — plus the daemon's own origin — also gates the live WebSocket handshake, with one deliberate difference: "*" opens CORS for every origin but is not honoured on the WebSocket, where allowing any origin would remove the socket's CSRF protection. A cross-origin page that needs the live WebSocket must be listed by its concrete origin.

Authentication lives under north.rest.auth:

north.rest.auth.users (secret) is managed through the dedicated user and token surfaces, not the section editor, and therefore not in the reference. north.rest.auth.tokens (secret) is managed through the dedicated user and token surfaces, not the section editor, and therefore not in the reference.

Two further login sources delegate authentication elsewhere. CCU-delegated login (ADR 0043) validates the login form against a CCU's own user database and maps the CCU UserLevel onto a Loom role — so a break-glass local admin always remains a fallback:

Defaults not shown in the reference: north.rest.auth.ccu.enabled add-on: true, else false; north.rest.auth.ccu.primary true (CCU first, local fallback); north.rest.auth.ccu.central first configured central; north.rest.auth.ccu.min_user_level 1 (guest); north.rest.auth.ccu.role_mapping ADR 0043 defaults.

HA Ingress passthrough (ADR 0044) trusts a request proxied by the Home Assistant Supervisor as an admin without a local login. It is a deliberate, supervised-only auth bypass; a genuine token or session always wins over it:

Defaults not shown in the reference: north.rest.auth.ha_ingress.enabled supervised add-on: true, else false; north.rest.auth.ha_ingress.trusted_proxy_cidr 172.30.32.0/23 (Supervisor); north.rest.auth.ha_ingress.role admin.

Authentication setup (Basic, Session, API tokens, OIDC, CCU-delegated login, and HA Ingress passthrough) is covered in detail on the auth admin page (docs/admin/auth.md).

north.ui

north.ui.enabled gates only the minimal no-JS server-rendered diagnostic surface — /health and /about — that stays reachable when the Svelte SPA cannot load. Login, logout, OIDC, and the first-run onboarding wizard all live in the Svelte SPA (ADR 0045), served on the REST listener (:8119 by default); there is no server-rendered HTMX login or /setup template.

north.ui.listen has been removed — the diagnostic surface shares the REST listener, so there is no separate UI bind address. Delete the key (and the OPENCCU_LOOM_UI_LISTEN env var) from existing configs.

Fields: see north.ui in the configuration reference.

north.mqtt

The MQTT bridge (Home Assistant Discovery and/or raw topic planes).

Fields: see north.mqtt in the configuration reference. Defaults not shown in the reference: north.mqtt.topic_base openccu-loom; north.mqtt.protocol_version "5"; north.mqtt.retain_cleanup_window_ms 2000.

broker_url is required when enabled is true; accepted schemes are tcp, mqtt, tls, ssl, mqtts (or a bare host:port). For the topic layout see the MQTT topic schema.

The two topic planes are not independent. raw_enabled publishes the state, availability and command topics; discovery_enabled publishes the Home Assistant discovery payloads, and every entity they declare points at a raw-plane topic. Turning discovery on therefore turns the raw plane on: the daemon corrects raw_enabled to true at startup and logs a warning naming the field. Without that correction Home Assistant would show the full device tree with every entity stuck at unavailable, and nothing would report an error. The reverse is fine — the raw plane alone is the supported setup for non-Home-Assistant MQTT consumers.

discovery_bundles — and what turning it back off costs

discovery_bundles (expert; needs discovery_enabled) switches the discovery plane from one retained config per entity, homeassistant/<component>/<node_id>/<object_id>/config, to one retained device document per node, homeassistant/device/<node_id>/config. It is off by default, and the daemon ignores it while discovery_enabled is false, logging a warning that names the field.

The two forms cannot both be retained for the same unique_id. Home Assistant refuses whichever it sees second, and the refusal is symmetric — measured in both directions on a live instance (ADR 0070, amendment of 2026-09-10). The entire signal is one line in the Home Assistant log:

WARNING [homeassistant.components.mqtt.entity] Received a conflicting MQTT
discovery message for entity sensor.…; the entity was previously discovered
on topic homeassistant/…/config …

Nothing appears on the wire, no entity is created, and openccu-loom cannot see any of it. Its own logs report a successful publish either way.

Turning it on is handled for you: the daemon retracts each superseded per-entity config before it publishes the document.

Turning it back off is the direction to plan for. The device documents from the bundle boot are still retained, and the orphan sweep that clears them runs after the snapshot that publishes the per-entity configs — that ordering is load-bearing for every other case the sweep handles. So on the first boot after the rollback, Home Assistant refuses the per-entity configs against the documents still in place, and the sweep retracts the documents immediately afterwards:

INFO mqtt.discovery_orphan_cleanup central=ccu-haus evicted=37

That line is the confirmation the documents are gone. Restart the daemon once more: the broker is clean, the per-entity configs are published against nothing, and the entities come back. Expect one boot with the affected devices missing from Home Assistant, and plan the rollback as two restarts rather than one.

To skip that window, retract the documents before the rollback restart — list them, then clear each by the exact topic the listing printed:

# 1. List the device documents the bundle boot left behind.
mosquitto_sub -h <broker> --retained-only -W 2 -v -t 'homeassistant/device/+/config'

# 2. Clear each one that belongs to openccu-loom. `-r -n` publishes an
#    empty retained payload, which is how a broker drops a retained message.
mosquitto_pub -h <broker> -r -n -t 'homeassistant/device/<node_id>/config'

script/clean-mqtt-discovery.sh does both discovery forms — per-entity configs and device documents — scoped by origin.name to openccu-loom, and is a dry run unless you pass -y.

The round trip is lossless in both directions: Home Assistant keys its entity registry on unique_id, not on the discovery topic, so a renamed entity_id, a custom name and the device_id all survive.

retain_cleanup_window_ms is how long (500–30000 ms; 0 = 2000) the bridge waits at boot for the broker to deliver retained messages before evicting stale discovery/legacy topics — raise it on high-latency brokers.

north.matter

The native-Go Matter bridge. Disabled by default; opt-in.

Fields: see north.matter in the configuration reference. Defaults not shown in the reference: north.matter.tcp off; north.matter.enable_time_sync false; north.matter.case.node_id 0 (CASE off); north.matter.case.fabric_id 0.

The default vendor/product IDs are development values from the test block — never ship them in production. Matter pairing and commissioning are covered on the Matter user page (docs/user/matter.md).

  • expose_secondary_channels (expert, ADR 0049) controls the one-endpoint-per-device folding. Default false: a physical device projects a single Matter endpoint from its primary channel. Turn it on to also fan out a device's secondary actor / status channels as extra endpoints (they otherwise appear as duplicates in Apple/Google Home). Matter-only — MQTT, HA-Discovery and REST/WS always carry every channel.
  • enable_time_sync (expert) mounts the TimeSynchronization cluster (0x0038) on the Root endpoint. Leave it off unless a controller needs it: Apple Home's HAP service mapper may reject the bridge at pairing when an unexpected RootNode cluster is present. Re-pair after changing it.
  • dev_rotate_unique_ids (expert, dev/test only) mixes a per-boot salt into every endpoint's Matter UniqueID. Enabling it in production breaks accessory recognition — controllers must re-link every device after each restart. Keep the default false.
  • case.* is accepted for compatibility and has no effect since 0.91.0: the operational identity is the Matter stack's own.
  • attestation.dac_path, dac_key_path, pai_path and cd_path point at production Device Attestation material (DAC + key + PAI + CD; PEM or DER, the key PKCS#8 or SEC1). With all four empty the bridge serves the CSA test chain for the configured vendor and product, which Apple Home, Google Home and chip-tool accept without --bypass-attestation-verifier. Once any path is set, all four must load and the key must match the DAC; otherwise the bridge keeps serving every controller already paired but refuses every new pairing, multi-admin included, and reports the cause under the matter.bridge health component. The DAC key path and the commissioning passcode/salt are secrets encrypted at rest.
  • mdns_advertise selects the mDNS advertiser: zeroconf (default, publishes the commissionable records) or noop (in-memory only, no multicast — a hermetic-test opt-out; QR-scan pairing cannot work in this mode).
  • mdns_interfaces (expert) restricts the mDNS responder to the named host interfaces: the bridge announces and answers on them alone, with their addresses. Empty (the default) keeps every multicast-capable interface. Use it on a host attached to two networks behind an mDNS reflector, whose controllers would otherwise cache the far network's address too. Container and virtualisation bridges (docker0, hassio, …) stay filtered even when named. An empty or repeated name is refused at save; a name the host does not have is accepted and reported as a finding on the Diagnostics tab of the Matter view. Restart the daemon after changing it.
  • tcp (expert) selects the Matter TCP transport roles: off (default — UDP only), incoming (accept TCP connections on the same port as the UDP listener), outgoing (the bridge may open TCP connections itself) or both. The roles are advertised in the operational mDNS record's T key and in the session handshakes. A controller that supports Matter TCP then moves large payloads — wildcard reads, OTA transfers — off MRP onto a TCP connection. Turn it on (incoming is the usual choice) when a controller supports Matter TCP and large reads are slow or time out over UDP; with incoming the TCP port must be reachable alongside the UDP one. Restart the daemon after changing it.

north.mcp

The Model Context Protocol server for LLM agents. Disabled by default; read-only even when enabled until allow_writes is also set.

Fields: see north.mcp in the configuration reference.

The MCP transport is mounted on the existing REST listener. See the MCP client guide.

north.webhook

A bidirectional HTTP bridge, both directions disabled by default. Outbound POSTs a signed JSON payload to an operator URL on datapoint / system-status / incident events. Inbound mounts REST routes (POST /api/v1/webhook/value, POST /api/v1/webhook/program) that external systems call to set a datapoint or trigger a program; these are real device writes, so they carry full authorization weight. Both directions are wired once at boot — toggling is restart-required.

Fields: see north.webhook in the configuration reference. Defaults not shown in the reference: north.webhook.events all; north.webhook.centrals all; north.webhook.timeout_ms 10000.

secret is the shared key for the X-OpenCCU-Signature HMAC-SHA256 body signature (empty = no signature header). events and centrals are allowlists (empty = all); parameter_glob further filters datapoint events by parameter name. inbound.token is an optional bearer token accepted in addition to the normal REST auth chain, so a header-only caller (e.g. a doorbell) can POST without a session.

north.discovery.mdns

LAN self-advertisement so zeroconf-aware clients (e.g. Home Assistant) can auto-discover the daemon.

Fields: see north.discovery in the configuration reference. Defaults not shown in the reference: north.discovery.mdns.instance_name OS hostname.

The advertised port mirrors north.rest.listen. Disable for security-sensitive deployments where LAN visibility is unwanted.

north.discovery.ssdp

Active SSDP / UPnP discovery of Homematic / OpenCCU central units on the LAN (ADR 0046). When enabled the daemon periodically multicasts an M-SEARCH, follows each responder's basic_dev.cgi, and surfaces matching CCUs in the UI so the operator can adopt or ignore them.

Defaults not shown in the reference: north.discovery.ssdp.enabled true; north.discovery.ssdp.interval 60s.

This is a read-only LAN scan — a multicast probe only; no data about the daemon leaves the LAN, and it simply finds nothing where multicast is unavailable (some container networks). It is on by default; disable it if unsolicited multicast probes are unwanted.

centrals[]

One entry per configured CCU. OpenCCU-Loom is multi-CCU from day one; see the multi-CCU guide.

Fields: see centrals in the configuration reference. Required: centrals.name (unique), centrals.host, centrals.interfaces (≥1). Defaults not shown in the reference: centrals.system_type ccu; centrals.json_rpc_port 80/443; centrals.primary_interface (HmIP-RF heuristic); centrals.check_connection_interval 30s.

name may contain only letters, digits, - and _. It becomes a path segment of the XML-RPC callback URL the daemon announces to that CCU, so a name outside that set produces a daemon that starts cleanly and never receives a single push event. Two names must also stay distinguishable after north-bound escaping (+, #, / and spaces become _), or the two CCUs would share every state topic and no inbound command could be attributed to either. Both rules are checked at config load, and the two rejections read differently so it is clear which one applies.

interfaces accepts a short list of names or a long form with per- interface overrides:

centrals:
  - name: home
    host: 192.0.2.29
    username: Admin
    password: "your-ccu-password"
    interfaces:
      - HmIP-RF
      - name: BidCos-RF
        port: 2001
        rpc_type: xmlrpc
      - name: CUxD          # CUxD is reached over BIN-RPC

rpc_type is xmlrpc or binrpc and only ever restates what the interface name already implies — CUxD speaks BIN-RPC, every other interface XML-RPC. The transport is not selectable per interface, so a value that contradicts the derived one is rejected at config load instead of being silently ignored; leave the key out unless you want it documented in the file. remote_path overrides the URL path of the XML-RPC endpoint (/RPC2, or /groups for VirtualDevices) for a CCU behind a reverse proxy that exposes it elsewhere; it must be an absolute path on the same host — the bare /, a leading //, a scheme, a query, a fragment and a .. segment are all rejected at config load. json_rpc_port defaults to 80 (plain) or 443 (TLS); set it when the CCU sits behind a non-standard proxy. Set tls_insecure_skip_verify only against a self-signed CCU on a trusted network. check_connection_interval of 0 uses the 30s default; a negative value disables the background connection check.

system_type selects what is behind a central: ccu (a CCU with ReGaHss and the WebUI JSON-RPC — also what the empty value means, so every configuration written before this key existed keeps its meaning), openccu-lite (reached through the occulited HTTP API), or auto (probed at bring-up and then persisted, so later restarts do not probe again). For openccu-lite, json_rpc_port means the HTTP(S) port of the box's occulited API rather than JSON-RPC; the daemon authenticates with api_token instead of username/password, and port, ports and per-interface port/remote_path/rpc_type overrides have no meaning there and are rejected at config load. Set tls_fingerprint (lower-case hex SHA-256 of the box's certificate) to pin a self-signed openccu-lite box's certificate instead of skipping verification; it requires tls: true and is mutually exclusive with tls_insecure_skip_verify. A ccu central must not carry api_token or tls_fingerprint — see Connecting an openccu-lite system for the full onboarding flow, the scopes an api_token needs, and what an openccu-lite system does not offer.

Each central also carries an optional behavior: sub-block of expert per-central toggles that shape how devices and hub entities are modelled. All are hot-reloadable and default to sensible values:

Defaults not shown in the reference: centrals.behavior.light_last_brightness true; centrals.behavior.use_group_channel_for_cover_state true; centrals.behavior.enable_sysvar_scan true; centrals.behavior.enable_program_scan true; centrals.behavior.include_internal_sysvars true; centrals.behavior.include_internal_programs false; centrals.behavior.sysvar_scan_interval 30s; centrals.behavior.enable_device_firmware_check true; centrals.behavior.delay_new_device_creation true.

persistence

Tuning for the SQLite-backed caches. The VALUES cache (persistence.values_cache, ADR 0019) persists wire-DP values so they survive a restart; it is on by default. The measurement-history recorder (persistence.history, ADR 0040) is opt-in: enabling it opens a dedicated history.db, records numeric VALUES samples, drives the SPA charts (GET /history), and can optionally push each sample to an external time-series store.

Fields: see persistence in the configuration reference. Defaults not shown in the reference: persistence.history.enabled false; persistence.history.retention_hourly 390 days (≈13 months); persistence.history.retention_daily 0 (keep forever); persistence.history.include all numeric; persistence.history.energy_price_per_kwh 0 (no costs); persistence.history.energy_currency €; persistence.history.export.enabled false; persistence.history.export.kind influxdb.

Sysvar and program markers

The marker lists do not decide whether a system variable or program is imported — everything the CCU exposes is imported. They decide how an entry arrives in a consumer such as Home Assistant:

Marker Effect
HAHM Makes a system variable writable (switch / select / number / text instead of a read-only sensor). A program has no value to write, so the CCU editor does not offer this marker there.
HX Free marker for your own filtering.
INTERNAL Additionally includes the CCU's internal entries. This matters more than it sounds: the CCU flags most ordinary user programs as internal, so without it a program list can look almost empty.

An entry matching any configured marker arrives enabled; every other entry arrives disabled and is switched on per entity. With no markers configured, everything is imported and everything arrives disabled.

The reference stack also recognises an MQTT marker, which steers a hand-off this daemon does not need — its MQTT bridge publishes every hub entity regardless — so the CCU editor does not offer it. A stored value is dropped when the editor opens; the token is still stripped from the CCU description so it never shows up in an entity name.

include_internal_sysvars / include_internal_programs are the equivalent switch for operators who configure no markers at all; either they or the INTERNAL marker suffice.

history.retention bounds raw samples (clamped up to a 1h floor); retention_hourly and retention_daily bound the two rollup tiers. include/exclude are parameter-name globs (exclude wins). The export token is never inline — the daemon reads it from the env var named by export.token_env.

energy_price_per_kwh makes the energy view show costs next to consumption. Leave it at 0 to show no costs at all — a tariff of zero would render every amount as 0.00, which reads as "free" rather than "not configured". energy_currency only labels those amounts; nothing is ever converted.

reliability

Overrides for the reliability stack (retry / throttle). Both fields default to 0, meaning "use the compiled-in behaviour"; set them only to pin a specific timing (e.g. on heavily-loaded BidCos-RF).

Fields: see reliability in the configuration reference. Defaults not shown in the reference: reliability.command_throttle_inter_command_delay 0 (no pacing).

ccu_data

Optional filesystem overrides for the openccu-data metadata archives (parameter translations and easymode profiles). Both are empty by default — the daemon uses the embedded archives and falls back to raw parameter / model names if a path is set but cannot be read.

Fields: see ccu_data in the configuration reference.

backup

Automatic, scheduled CCU backups. Off by default (a backup touches the CCU and produces files, so it is opt-in); manual backups via the REST/UI surface work regardless. Both fields are read when the scheduled job is registered at boot, so changing either is restart-required — the config editor marks them accordingly.

Fields: see backup in the configuration reference. Defaults not shown in the reference: backup.dir <data_dir>/backups; backup.schedule 0 (disabled); backup.keep_last 0 (keep all). Environment override: backup.dir is overridden by OPENCCU_LOOM_BACKUP_DIR.

keep_last prunes the oldest scheduled backups per central beyond the given count after each successful run.

dir decides where the downloaded .sbk archives are kept. The default is wrong in exactly one situation: as CCU add-on software the data directory lives in the tree the CCU backs up itself, so its own backups would grow with every archive. The add-on's service script sets the env override to the CCU's backup target for that reason; see Backup & restore for the full rule.

security

Key Type Default Env Secret?
security.allow_plaintext_secrets bool false — no

When false (default and recommended) the daemon refuses to persist a central's password it cannot encrypt with the master key (see Secrets). While a master key is available the flag changes nothing — the password is sealed either way. It matters only in the degraded case: if no key can be resolved (secret.key lost after a restore, OPENCCU_LOOM_SECRET_KEY unset, read-only data directory), saving a CCU password is rejected with 400 Password cannot be stored instead of writing the credential to the database in the clear.

Set it true only in a throwaway/dev environment where no at-rest key is available; the password is then stored in plaintext in the database. The better fix is to restore the master key — /health reports config.secrets as degraded while one is missing.

Validating a config file

Use the admin CLI to check a file before deploying it:

hmcli config validate /etc/openccu-loom/config.yaml

See also

  • Backup & restore — and why secret.key must travel with the database.
  • Troubleshooting — callback reachability, lockouts, lost keys.
  • Security guide — at-rest encryption and hardening.
  • Multi-CCU guide — running several CCUs from one daemon.
  • REST + WebSocket API reference (docs/integrations/rest-ws.md).