Skip to content

Sitting Lamp → Hue circadian renderer (design)

Ticket: #93 · Date: 2026-07-21

Goal

Migrate the master-bedroom sitting-area lamp from the on/off ZigBee switch (switch.master_bedroom_sitting_lamp) to the already-paired Philips Hue bulb light.master_bedroom_sitting_room_lamp_bulb (friendly name "Sitting Room Lamp Bulb"), and rewrite script.master_bedroom_sitting_lamp_render so that when the lamp is ON it renders a circadian colour-temperature + brightness that tracks the sun and is sensitive to room darkness (motion + sun-below-horizon, plus a storm-darkened daytime room via a lux sensor).

Verified facts

  • Bulb: light.master_bedroom_sitting_room_lamp_bulb, platform hue. supported_color_modes: [color_temp, xy], min_color_temp_kelvin: 2000, max_color_temp_kelvin: 6535, brightness. Full tunable-white + colour.
  • Power independence: the bulb reports off but available while switch.master_bedroom_sitting_lamp is off → the bulb has its own always-on power. No smart-bulb-behind-a-smart-switch trap. The old ZigBee plug is freed; leave it paired but drop it from the renderer/scripts (may be repurposed).
  • Lux sensor: sensor.night_light_7_illuminance — the master-bedroom night light (NL7) illuminance.
  • Sun tracker: reuse sensor.ambiance_scene (configuration_ambiance_addition.yaml) — the 10-phase sun-elevation template the Govee ambiance lamps already key off (night/dawn/first_light/ sunrise/morning/midday/afternoon/golden_hour/sunset/dusk).

The "dark" test (replaces bare sun below_horizon)

dark = is_state('sun.sun','below_horizon')
       or (states('sensor.night_light_7_illuminance') | float(999)
           < states('input_number.sitting_lamp_dark_lux') | float(20))
  • New helper input_number.sitting_lamp_dark_lux (configuration.yaml): min 0, max 200, step 1, unit_of_measurement: lx, icon: mdi:brightness-5. No initial: (persistent via restore_state); set to ~20 once after first deploy. Dashboard-tunable.
  • The float(999) default means a missing/unknown lux reading is treated as bright (fails the dark test) — fail-safe: a dead sensor won't spuriously light the lamp during the day.

Circadian output

When the priority stack decides ON, call light.turn_on on the bulb with a Kelvin + brightness picked by:

  • Sun above horizon (only reachable via the daytime-storm branch) → warm/cozy override: 2700 K, 65%. Storm = cozy, not office-bright.
  • Sun below horizon → the circadian phase map keyed on sensor.ambiance_scene:
phase Kelvin brightness %
night 2200 40
dawn 2700 35
first_light 3000 45
sunrise 3500 55
morning 4500 80
midday 5500 100
afternoon 4500 85
golden_hour 2900 75
sunset 2500 65
dusk 2200 55

(brightness bumped 2026-07-21 — the evening/night end was too dim for reading; night 20→40, golden_hour 60→75, sunset 45→65, dusk 30→55, cozy 50→65.)

(midday/morning/afternoon/sunrise phases only occur with the sun up, so in practice the sun-below path uses the warm end — golden_hour/sunset/dusk/night/first_light/dawn. The full table is kept so the map is total and future-proof.) Values chosen for a bedroom; tune live later.

brightness sent as brightness_pct; Kelvin as color_temp_kelvin. All within the bulb's 2000–6535 K.

Priority stack (rewritten script.master_bedroom_sitting_lamp_render)

Same 5 branches; branch 2/5 gates use the new dark, ON actions render circadian:

  1. Away (zone.home | int == 0) → stop (away presence-sim owns the lamp).
  2. Waking (morning_armed on AND dark) → ON circadian.
  3. Sleeping (input_boolean.sleeping on) → OFF.
  4. Movie (media_player.master_bedroom_apple_tv playing OR media_player.bedroom_tv on) → OFF.
  5. Occupied (binary_sensor.bedroom_occupancy on) AND ON-window → ON circadian, where ON-window = (sun below_horizon AND time 16:00–02:00) ← evening, as before (the wrap-around gate that stops a 5 am relight when a partner's still in bed after Good Morning clears sleeping) OR (sun above_horizon AND dark-by-lux) ← daytime storm (renders the warm/cozy override).
  6. default → OFF.

Rendering ON is a shared step: compute (kelvin, pct) per the rule above, light.turn_on the bulb. Mode stays restart.

Trigger automation (master_bedroom_sitting_lamp_render_triggers)

Keep existing (morning_armed, sleeping, bedroom_occupancy on / off-for-20m, sun.sun below/above, the two media players, zone.home). Add: - sensor.ambiance_scene (state change) → re-render so Kelvin/brightness drift through the evening. - sensor.night_light_7_illuminance — trigger on crossing the threshold. Since the threshold is a tunable helper, use a template trigger on the dark-by-lux boolean (or a numeric trigger with the helper as the below:) so storm onset/clear re-renders. Also add input_number.sitting_lamp_dark_lux as a trigger so changing the threshold re-renders.

Away simulation / away-off (_away_sim, _away_off)

Move from switch.turn_on/off to light.turn_on/off on the bulb. Away-sim's random "on" turns the bulb to a fixed cozy 2500 K, 40% (a lived-in look); "off" turns it off. Away-off at 23:00 → light.turn_off.

Downstream reference updates

Change every switch.master_bedroom_sitting_lamp reference that means "the sitting lamp" to the bulb: - script.good_night (scripts.yaml) and script.we_are_leaving (lists it explicitly ~L169). - homekit.yaml (expose the bulb instead of the switch, if it was exposed). - CLAUDE.md §23 (Master Bedroom Sitting Lamp) + the group.lights note (still not a member — the renderer owns sleep-off), and automations.yaml §50 header comment. - Docs: docs/lighting.md (schedule + description), docs/automations.md (§50 reference), projects/smart_lighting_scenes.md. - The old switch.master_bedroom_sitting_lamp is left paired (not removed) but no longer driven.

Testing / deploy

  • New entity refs for Tier-3: add to tests/fixtures/entities_allow.txt with comments — light.master_bedroom_sitting_room_lamp_bulb (Hue, already live but not yet snapshotted) and input_number.sitting_lamp_dark_lux (created on next restart). sensor.night_light_7_illuminance is already in entities.txt. Fold all into the next make snapshot.
  • Deploy path: touches scripts.yaml + automations.yaml + configuration.yaml → CI runs check_config then full ha core restart (configuration.yaml changed). Automations hot-reload only applies when just automations change; here it's a restart.
  • Post-deploy: set input_number.sitting_lamp_dark_lux to 20; verify the bulb renders circadian in the evening and warm/cozy on a simulated dark-lux (or lower the threshold to test). Render-test the dark template live via /api/template.

Addendum (2026-07-21) — manual override + Alexa on/off

Follow-up to the same ticket: the dark test means the lamp never comes on when the room is bright, so add a way to force it on (e.g. a daytime reading light).

  • New helper input_boolean.sitting_lamp_manual (friendly name "Sitting Room Lamp", persistent).
  • Renderer: new manual branch below sleeping/movie, above the dark-gated occupied branchmanual on → ON at the circadian setting, ignoring the dark test. cozy is tightened to the storm case only (sun above horizon AND lux-dark AND occupied), so manual-on by day uses the true cool/bright midday circadian value rather than the warm override.
  • Auto-clear: automation.master_bedroom_sitting_lamp_manual_clear — bedroom empty for 2 min AND manual on → turn the override off (→ renderer mode). script.wake_house also clears it each morning (alongside the corner/office manuals). The manual boolean is added as a render trigger.
  • Alexa: expose input_boolean.sitting_lamp_manual as the switch "Sitting Room Lamp" (a switch supports both on and off, unlike a script-scene). "On" = force on; "off" = back to renderer mode (dark by day / when you leave; the evening ambiance rule may still light it — accepted, per the "off = renderer mode" decision). The raw bulb stays unexposed to Alexa (direct control would fight the renderer). First non-script entity on the Alexa allowlist — update the CLAUDE.md index.
  • Decisions (user-confirmed): manual only ignores the dark test (sleeping/movie still win); Alexa "off" returns to auto (no separate force-off state). A hard force-off is a possible later follow-up.

Out of scope / follow-ups

  • No colour (xy) scenes — circadian is colour-temperature only (bedroom, not a party lamp).
  • Adaptive-Lighting HACS not introduced; we reuse the home-grown ambiance_scene sun tracker for consistency with the Govee lamps.