A lighting engine is a state machine, not a remote control
Tying Hue, Govee, audio gear, and projectors into one coherent scene system means treating the living room as a publish-subscribe device graph with a recovery contract.
- problem
- A pile of smart bulbs, LED strips, projectors, and audio gear controlled through vendor apps becomes unmaintainable: scenes drift, devices go offline silently, and "movie mode" stops working when one bulb updates its firmware.
- scope
- A home lighting engine built on Home Assistant: device graph, event-driven scenes, availability-aware recovery, and an offline-safe control surface that does not hide controls when a device is unreachable.
- environment
- Home Assistant on a Raspberry Pi 4, with Philips Hue, Govee, Wiz, an XGIMI projector, Samsung soundbar, and Argon fan controllers exposed over LAN and Zigbee.
Assumptions
- Devices expose a local API or a Zigbee profile; cloud-only devices are excluded from the engine.
- The hub runs continuously and can store scene state outside individual device memory.
- Scene transitions are idempotent: re-applying a scene should not flicker or stack transitions.
Limitations
- Vendor firmware updates can break local APIs without warning; the engine treats API failure as a recoverable event, not a permanent fix.
- This is a personal build log, not a packaged integration; device specifics (IPs, entity IDs) are omitted.
- Color-temperature and gamut mapping across vendors is approximate; perceptual matching is not guaranteed.
Table of contents 4 sections
Model the room as a device graph
The first mistake is treating each light as an independent endpoint. A living room is a graph: the projector affects perceived brightness, the soundbar affects whether lights should dim, and an Argon fan controller ties thermal state to the scene. The scene engine does not set bulbs; it publishes a target state to the graph and lets each node resolve it.
Each device is a node with a capability profile (on/off, brightness, color, color-temperature) and an availability state. A scene is a target vector across the graph, not a script of sequential API calls. This means "movie mode" can include the projector power state and the soundbar input, and a single device failure does not abort the whole scene.
Keep controls visible during outages
A common UX failure is hiding a light control when the entity is unavailable. The operator should see the control, an outage warning, and the last known target. Hiding the control removes the ability to retry or override, which is exactly when the operator needs it most.
The engine stores the last commanded target per device. When a device returns from unavailable, a state listener compares the device current state against the stored target and re-applies if they differ. This turns a 30-second Wi-Fi blip into a self-healing event instead of a manual scene re-trigger.
Wire audio reactivity without a dedicated controller
A DDJ-FLX10 controller maps MIDI to Home Assistant actions. A fader raises brightness, a pad triggers a scene, and a knob sweeps color temperature. The mapping is a thin bridge: MIDI input event to HA service call. The interesting part is the feedback: the controller LEDs reflect the actual device state, not the commanded state, so the physical surface stays honest.
This avoids a dedicated lighting controller box. The DJ controller is already on the desk, already USB-connected, and already has enough physical controls for a room. The bridge is a small script that subscribes to MIDI and to HA state changes, keeping both directions in sync.
# fader 7 -> living room brightness (0-127 -> 0-255)def on_midi(msg): if msg.type == 'pitchwheel' and msg.channel == 0: brightness = int(msg.pitch / 8191 * 255) ha.call_service('light', 'turn_on', entity_id='light.living_room', brightness=brightness) # HA state -> controller LED feedbackdef on_ha_state(entity, new_state): if entity == 'light.living_room': bri = new_state.attributes.get('brightness', 0) send_midi('pitchwheel', channel=0, value=int(bri / 255 * 8191))Make scenes idempotent and declarative
A scene is a declarative target, not an imperative script. "Set living room to 40 percent warm white with projector on" is a contract: the engine resolves each device to the target, applies transitions, and reports which devices reached the target, which are pending, and which failed. Re-running the same scene is a no-op if devices already match.
This is what separates a lighting engine from a remote control. The remote fires commands and hopes. The engine publishes a target, measures convergence, and exposes the delta. When the projector takes 8 seconds to warm up, the scene is not "done" when the command returns; it is done when the projector reports the target input and the bulbs report target brightness.
- implemented Multi-vendor scene engine
Hue, Govee, Wiz, projector, and soundbar states are composed into named scenes with availability checks and per-device fallback targets.
- field-observed Recovery under partial outage
When a bulb drops offline, the scene engine marks it unavailable, keeps the remaining devices in scene, and re-applies target state when the bulb returns instead of requiring a manual re-trigger.
- reference Home Assistant documentationopen ↗
Reference for automations, scenes, template sensors, and device availability.