Files
celebright/docs/PROTOCOL.md
T
Hank MuellerandClaude Opus 5 cb99d9f941 Add Celebright holiday-lights integration
Home Assistant custom integration for Celebright (CLC-03) holiday-lights
controllers, plus a standalone reference client. Local-only, reverse-engineered
from the device's WebSocket protocol — talks directly to the controller on the
LAN with no vendor cloud.

- select entity exposes the device's saved scenes plus an off option
- firmware-v2 protocol (savedScenes); getSystemState drives availability and
  the active scene, scene library fetched best-effort and cached
- WS connect/recv/send wrapped in asyncio timeouts so a silent device can't
  blow past HA's setup deadline
- config flow prompts for the controller IP; no credentials involved
- docs/PROTOCOL.md: full v2 WebSocket protocol and device behaviors

Migrated from the private dfritz/celebright with history dropped and internal
site references removed. MIT-licensed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6B8b3iYNv6QUftK2FDfYb
2026-09-01 15:37:23 +00:00

115 lines
5.3 KiB
Markdown

# Celebright protocol & operations reference
Canonical technical reference for the Celebright controller, reverse-engineered
from device WebSocket captures and live probing. The maintained implementation
is the Home Assistant integration in [`custom_components/celebright/`](../custom_components/celebright).
## Device
| Field | Value (reference unit) |
|---|---|
| Model | `CLC-03` |
| Hardware | `hwVer 4` |
| Firmware | `fwVer 2.04` (protocol **v2**) |
| LAN address | device DHCP/static IP on your LAN (HTTP/WS on port 80) |
| HTTP/WS port | `80` (config portal at `/`, WebSocket at `/ws`) |
| Discovery | UDP broadcast port `49999`, payload `App Broadcast Message` |
## Firmware versions
Firmware **2.x renamed the v1 "presets" concept to "savedScenes"** and bumped
the wire protocol. v2 replies are tagged `"v":2`. The v1 topics
(`getPresetsAndEventsPaginated`, `loadPreset`, response `presetsPage`) are gone;
sending them now returns `logDeviceError "Unrecognized topic [X]"`. The
standalone `celebright_controller.py` is the original v1 reverse-engineering
reference and does **not** work against 2.x — the integration is v2.
## WebSocket protocol (v2)
`ws://<device>/ws`, JSON frames `{"topic": <str>, "message": <obj>}`. Negotiates
`permessage-deflate`. Client→server frames are masked (standard WS); the device's
own frames are unmasked and tagged `"v":2`.
### Topics
| Request topic | `message` | Response topic | Notes |
|---|---|---|---|
| `getSystemState` | `{}` | `systemState` | **Always answered** (lights on or off). |
| `getSavedScenesAndEventsPaginated` | `{}` | `savedScenesPage` | **Only served while idle** — see gating below. |
| `getZones` | `{"v":2}` | `systemZones` | Light zones / per-light map. |
| `getInfo` | `{}` | `getInfoResponse` | Device info (model, fw, IP, RSSI, storage). |
| `loadSavedScene` | `{"savedSceneUuid": <uuid>}` | `systemState` | Activate a scene. |
| `setTurnOffAndDisableSchedule` | `{}` | `systemState` | Turn off + disable schedule. |
| unknown | — | `logDeviceError` | `"Unrecognized topic [X] No action taken"`. |
### `systemState` message
```json
{"userDisplay": 1, "scheduleEnabled": 0, "sleepTimer": 223,
"activeSavedScene": "<uuid>", "currentScene": [ ... ],
"md5": "8400de4ae50038cee347364b840e6328"}
```
- `userDisplay` 0 = off, 1 = a scene is showing.
- `activeSavedScene` = uuid of the showing scene (absent when off).
- `md5` is a **library-level** hash — constant across on/off, changes when the
saved-scene set changes. Use it to invalidate a cached scene list.
- `loadSavedScene` sets a default `sleepTimer` (~minutes) itself; no separate
sleep-timer call is needed.
### `savedScenesPage` message
```json
{"savedScenes": [
{"uuid": "...", "name": "Starry Night", "desc": "...", "md5": "...",
"displays": [ {"uuid": "...", "zones": ["..."], "lookType": 2,
"lookData": { ... }} ]}
], "offset": 0, "limit": 10, "total": 7}
```
`displays`/`lookData` (patterns, palettes) are opaque to the integration — it
only needs `uuid` + `name`.
## Device behaviors that shape the integration
- **Scene library is gated on idle.** `getSavedScenesAndEventsPaginated` returns
`savedScenesPage` immediately when the lights are **off**, but returns
**nothing** (silent, not an error) while a scene is actively rendering. A
local-only client therefore cannot fetch the library on demand while lights
are on. The vendor app sidesteps this by reading the library from the
encrypted cloud relay.
- **One WebSocket client at a time.** Overlapping connections (e.g. a leftover
test client) make a fresh connection's reads return nothing — close the old
one and let the slot free before reconnecting.
- **Unknown topics don't close the socket** — they emit `logDeviceError`, so a
read loop waiting for a specific reply must give up on a timeout, not hang.
### How the integration copes
- Availability + current scene are driven by `getSystemState` (always answered),
so the entry stays `loaded` even with lights on.
- The scene library is fetched **best-effort and cached**, re-fetched only when
the `systemState` `md5` changes. A failed fetch keeps the cached list instead
of failing the coordinator update.
- All WS connect/recv/send calls are wrapped in `asyncio.wait_for`
(`WS_CONNECT_TIMEOUT`/`WS_RECV_TIMEOUT`/`WS_SEND_TIMEOUT` in `const.py`) so a
silent device cannot blow past Home Assistant's 60s setup deadline.
## Deploy
Copy `custom_components/celebright/` into your Home Assistant
`config/custom_components/` (or use HACS as a custom repository), then restart
Home Assistant. The on-disk copy is not git-managed — after updating the files
call `homeassistant.restart`; a config-entry reload does **not** re-import changed
Python. Verify the entry reaches `loaded` and the preset `select` entity lists the
scenes. A `make deploy` target is provided for rsync-over-SSH deployment; set
`REMOTE_HOST` to your Home Assistant host.
## Re-deriving the protocol from a capture
A `.pcapng` of the vendor app talking to the device (port 80) yields the wire
protocol. The cloud relay is encrypted and not capturable, so only the local
device exchange is visible. Parse the capture per TCP connection; **client→server
frames are WS-masked** (XOR the 4-byte key) to read the request topics, while the
device's responses are plaintext.