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

5.3 KiB

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/.

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

{"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

{"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.