ADR 0055 — Heating-group administration via the CCU jpages proxy¶
Date: 2026-07-22 Status: accepted Related: ADR 0002 — multi-CCU first-class
Context¶
Homematic heating groups (HmIP and BidCos "Heizungsgruppen") are the last large CCU-WebUI capability OpenCCU-Loom does not expose. A group is a virtual device on the VirtualDevices interface whose members are wired to it through a type-specific direct-link matrix; the CCU keeps the member roster in /etc/config/groups.gson and drives a CONFIG_PENDING settle after every change. Today Loom surfaces the virtual group device as an ordinary device with no member context, and offers no way to create, edit, or delete a group.
The CCU implements group orchestration in two places, and the distinction decides our approach:
- Reading the roster is a plain JSON-RPC call.
CCU.getHeatingGroupList(occu/WebUI/www/api/methods/ccu/getheatinggrouplist.tcl) does nothing but read/etc/config/groups.gsonand return it. Any JSON-RPC client with a valid session can call it. - Mutating a group (create / save / delete / member selection) runs entirely inside HMServer, exposed over the HTTP endpoints
/pages/jpages/group/{list,create,save,delete,suitableGroupMembers, configureDevices,assignedGroupMembers}. HMServer builds the virtual device, computes the per-type direct-link matrix, maintainsgroups.gson, and sequences theCONFIG_PENDINGfollow-up. The shipped, readable page templates areoccu/HMserver/opt/HMServer/pages/GroupListPage.ftlandGroupEditPage.ftl; lighttpd proxies^/pages/jpagesto HMServer on127.0.0.1:9292(WebUI/etc/lighttpd/conf.d/proxy.conf).
Reproducing that matrix natively in Go would mean re-deriving, per group type and per device generation, which link roles wire to which — logic eQ-3 evolves with every firmware. Drift there is silent and only shows up as a mis-wired group on real hardware.
The load-bearing question: which session does jpages accept?¶
The one fact that had to be verified before committing to a proxy was whether /pages/jpages authenticates with the JSON-RPC session Loom already holds, or with a separate WebUI session that Loom would have to establish on its own.
It is the JSON-RPC session. HMServer validates the request's sid by POSTing it to ReGa's JSON-RPC endpoint /api/homematic.cgi — the same endpoint and the same session token that Session.login returns. The WebUI passes exactly this token to jpages as a query parameter (.../group/...?sid=<SessionId>, e.g. occu/WebUI/www/config/easymodes/js/Group.js). There is no second login and no separate WebUI cookie in the path.
Loom already holds that token: the JSON-RPC client keeps it in jsonrpc.Client.SessionID() and renews it on its own cadence. And Loom already has the precedent for session-authenticated raw HTTP to the CCU: CcuBackend.SetDownloadFirmwareTransport(baseURL, hc, sessionIDFn) posts to /config/cp_maintenance.cgi and /config/cp_security.cgi with sid := sessionIDFn(). The jpages proxy is that same mechanism aimed at a different path.
Decision¶
Expose heating-group administration by proxying the CCU's own jpages endpoints with Loom's live JSON-RPC session, not by re-implementing the group-wiring matrix in Go.
-
Reads go through JSON-RPC, not the proxy. The group list (GR01) is served by calling
CCU.getHeatingGroupListthrough the existingCcuBackend/jsonrpc.Clientand joining the roster against Loom's device model. This keeps the read path on the same typed, retried, session-managed transport as every other CCU read and needs no HTTP-proxy machinery. -
Mutations go through the jpages proxy. Create / save / delete / member-selection (GR02–GR05) call
/pages/jpages/group/{create,save,delete,suitableGroupMembers}over HTTP, authenticated with?sid=<sessionIDFn()>, following theSetDownloadFirmwareTransportpattern: a per-central southbound HTTP transport wired with the CCU base URL, a boundedhttp.Client, and asessionIDFn func() stringthat returns the live JSON-RPC session. If the session is empty (never logged in / logged out), the call fails with the sameErrUnsupported-class error the firmware path uses. -
The write path is asynchronous with progress broadcasts. A group save triggers a
CONFIG_PENDINGsettle on the virtual device and its members; the REST/WS surface models this as a job with progress broadcasts (GR02 is sized XL for exactly this reason), mirroring the settle handling already used elsewhere in the wave work. -
Native rebuild is the documented fallback, ADR-gated. If a future CCU firmware ever drops or firewalls the jpages endpoints (e.g. a fully sealed-CCU deployment, cf. the open A2 JSON-RPC-compat item), a native reconstruction of the direct-link matrix becomes necessary. That is a separate, large decision and gets its own ADR; it is explicitly out of scope here.
Consequences¶
- The group read surface (GR01) is available immediately and cheaply and does not depend on the proxy transport landing.
- The group write surface reuses the CCU's certified orchestration verbatim, so it cannot drift from eQ-3's link-matrix logic — the whole point of choosing the proxy.
- Loom takes a dependency on the jpages endpoints and their request / response shapes. These are internal CCU surfaces, not a public contract, so the proxy tolerates unknown fields and treats non-2xx responses as opaque errors surfaced to the operator, rather than parsing HMServer internals.
- Group administration requires a CCU that actually runs HMServer with jpages reachable (the standard OpenCCU / CCU3 case). Homegear and any sealed-CCU setup do not offer it; those backends report the capability as unavailable rather than failing mid-write.
- The proxy inherits the JSON-RPC session's lifecycle for free: renewal, backoff, and logout are already handled by
jsonrpc.Client, so the group transport never manages credentials itself — it only reads the current session id at call time.