- Presence Scene Controller: per-room presence-aware time-of-day scenes with vacant/sleep scenes and Motion Light coexistence - Time Of Day Selector: event-driven state machine that sets an input_select when a configured time entity fires - List both blueprints in the root README - Bump version to 2.13.0
10 KiB
Presence Scene Controller Blueprint
A per-room, presence-aware time-of-day scene controller. Maps scenes to time-of-day options by index — like the Day Scene Controller — but adds presence gating, vacant/sleep scenes, and explicit coexistence with the Motion Light blueprint.
Designed to take over the per-room responsibilities that have outgrown a single house-wide scene per time slot. Day Scene Controller can be kept alongside this blueprint for genuinely house-wide scenes (Away, Goodnight, All-Off).
Why this exists
Day Scene Controller triggers one global scene per time-of-day slot. As a home grows, those scenes balloon to dozens of entities and start fighting the user — empty bedrooms light up at sunset, scenes overwrite whatever is happening in the room you're actually in, and a single light requires editing four monolithic scenes.
This blueprint splits the problem per room. Each room gets its own small scenes (3–6 entities), its own presence sensors, its own vacancy behavior, and a clean handoff with motion-driven automations.
Architecture
Time of Day Selector (input_select, 1 instance)
│
├──► Day Scene Controller (house-wide, kept for Away/Goodnight)
└──► Presence Scene Controller × N (one instance per room)
Per room the user provides:
- A small set of scenes (Morning/Day/Evening/Night, plus a Vacant scene).
- Optional presence sensors.
- Optional sleep-mode switch + sleep scene.
- Optional Motion Light state entity for coexistence.
Migration path
The blueprint is built so you can adopt it gradually:
- Day 1. Instantiate the blueprint per room with no presence sensors and small per-room scenes. With no sensors the room is treated as always-occupied and behaves as a per-room TOD mapper — you immediately gain smaller scenes and per-room control without buying any new hardware.
- Day N. Add presence sensors to one room (start with the bedroom — biggest UX win). Append the sensor entity to the input list. Vacancy gating, the sleep-person fix, and TOD-defer all activate for that room. No other config changes.
- Repeat per room as you wire more sensors. Day Scene Controller can stay around for the house-wide scenes that still make sense, or be retired completely.
How it works
Triggers
The automation re-evaluates on any of:
- Time-of-day state change.
- Any presence sensor reporting
on. - All presence sensors reporting
offfor the configured timeout. - Room enable switch toggled.
- Sleep mode switch toggled.
- Motion Light state entity changed (used as an abort signal).
Decision flow
On every run, in order:
- Yield to Motion Light. If the configured Motion Light state entity reports the same room is in
ENABLING/ENABLED/MANUAL, stop immediately. - Motion-Light-state-change abort. If this run was triggered by a Motion Light state change and Motion Light is now released, stop without re-applying anything (avoids fighting a freshly-disabled Motion Light).
- Validate TOD. Stop if the time-of-day state is not in the options list, or if the scenes list is shorter than the options.
- Apply TOD-while-occupied policy (only when the trigger was a TOD flip and the room is currently occupied):
defer— stop, wait for the next vacancy cycle.apply_if_lights_off— stop unless every light/switch in the target scene is currently off.apply— continue.
- Skip if same scene. If a
last_applied_state_entityis configured and the target scene matches what was last applied, stop. - Apply the target scene.
- Persist last-applied scene (if a state entity is configured).
- Post-apply re-check. After 300 ms re-read Motion Light state; if Motion Light has since claimed the room, stop without running callbacks.
- Run callback —
vacant_scene_applied_callbackorscene_applied_callbackdepending on which scene was applied.
Target-scene resolution
Computed at action time (not trigger time), so a TOD flip that fires while a vacancy timeout is still counting down resolves to the new TOD's scene at apply time:
| Condition | Target scene |
|---|---|
| Sleep mode switch ON (and sleep scene configured) | sleep_scene |
| Room enable switch OFF | vacant_scene |
| Occupied and TOD index valid | scenes[tod_index] |
| Vacant | vacant_scene |
"TOD change while occupied" — the headline rule
Three modes for the toggle, default apply_if_lights_off:
| Mode | What it does | Best for |
|---|---|---|
apply_if_lights_off |
Re-apply only if every light/switch in the target scene is currently off | Default — handles "sleeping at 06:00 in a dark room" and "kitchen is dark, refresh it" with a single rule. Doesn't fight a user who is already reading with the lamp on. |
defer |
Never re-apply on TOD flip; wait for the next vacancy cycle | Bedrooms with mmWave that always reports occupancy. |
apply |
Always re-apply on TOD flip | Kitchen, hallway, transit areas. |
The deferred TOD scene is delivered automatically the next time the room transitions vacant→occupied — when you walk back in, you see the new TOD's scene.
Vacancy
When all presence sensors have been off for presence_off_timeout, the vacant_scene is applied. Typical choices:
- An "All Off" scene for hallways and bathrooms.
- A dim night-light scene for the bedroom.
- An empty-room scene that turns off only the discretionary lights.
If no presence sensors are configured, vacancy is never reached and vacant_scene is effectively unused (still required as input, but ignored).
Sleep mode
When sleep_mode_switch is ON (and sleep_scene is configured), every run resolves to sleep_scene regardless of TOD or presence. Use it for:
- Shift workers sleeping during the day.
- Naps where TOD says "Afternoon" but you want the room dark.
- A partner entering the bedroom without lighting it up.
The switch can be wired to a dashboard button, an NFC tag, a voice command, an alarm-clock automation that flips it off in the morning, or a sleep tracker.
Motion Light coexistence
If both Motion Light and Presence Scene Controller target the same lights, point Presence Scene Controller at Motion Light's state input_text plus the matching key (Motion Light's automation_state_placeholder_key, or — by default — the room enable switch entity ID).
Coexistence is enforced two ways:
- Yield gate. Every evaluation reads Motion Light's state JSON. If it reports
ENABLING/ENABLED/MANUALfor this room, we stop. - mode: restart abort. A change on the Motion Light state entity is one of the triggers, so a transition into ENABLING during a scene application cancels the in-flight run via Home Assistant's restart mode.
- Post-apply re-check. A 300 ms delay after
scene.turn_onis followed by a fresh read of Motion Light state. If Motion Light claimed the room during the apply, we yield without calling the success callback.
This is advisory rather than a hard lock — Home Assistant doesn't expose CAS-style locking — but it eliminates the common race in practice.
Presence semantics
- ANY-on logic: the room is occupied if any presence sensor reports on.
- Sensors reporting
unavailableorunknownare treated as occupied — safer than the alternative (briefly plunging a real-occupied room into the vacant scene during a Zigbee hiccup). presence_off_timeoutdebounces motion drop-outs and short absences.
Configuration
| Group | Input | Notes |
|---|---|---|
| States | Time of Day State Selector | Required. The shared input_select. |
| Room Enable Switch | Optional kill switch. OFF → apply vacant scene. | |
| Presence | Presence Sensors | Optional list. Empty = always occupied. |
| Presence Off Timeout | Seconds (default 120). | |
| Scenes | Time-of-Day Scenes | Required ordered list, one per TOD option. |
| Vacant Scene | Required. Applied on vacancy / room disabled. | |
| Sleep Scene | Optional. Required for sleep mode to do anything. | |
| Behavior | TOD Change Behavior While Occupied | apply_if_lights_off (default) / defer / apply. |
| Sleep Mode Switch | Optional. | |
| Motion Light Coexistence | Motion Light State Entity | Optional input_text from a Motion Light instance. |
| Motion Light State Key | Optional override of the JSON key. | |
| Persistence | Last Applied Scene Entity | Optional input_text for skip-if-same-scene. |
| Actions | Scene Applied Callback | Runs after a TOD/sleep scene is applied. |
| Vacant Scene Applied Callback | Runs after the vacant scene is applied. | |
| Debug | Enable Debug Notifications | Posts a persistent notification per evaluation. |
Behavior notes & known limitations
- HA restart while occupied. After a restart, the first vacancy cycle applies the current TOD's scene to the room. If TOD already advanced past several boundaries during the restart, intermediate scenes are not retroactively applied.
- Identical vacant scene and TOD scene. If you accidentally set them to the same entity, the skip-if-same-scene guard suppresses redundant work — but only if you've configured a
last_applied_state_entity. - Motion Light coexistence is advisory. A worst-case race between a scene apply and Motion Light's enable path can still produce a brief flicker before the post-apply re-check yields. If you need stricter mutual exclusion, wire a per-room "scene controller idle"
input_booleaninto Motion Light'scondition_switches. - Settled occupancy isn't modeled. A user who rolls out of bed but stays in the room never triggers a vacancy cycle, so the deferred TOD scene won't apply until they leave and re-enter. Acceptable in practice; the first bathroom trip resets it.
- One blueprint instance per room. If you need separate behavior for two distinct light sets in the same room (e.g., bedroom main lights vs. bedroom fan + reading lamp), instantiate the blueprint twice with non-overlapping scenes.
- Optional state-trigger entities. Triggers reference
room_enable_switch,sleep_mode_switch, andmotion_light_state_entity. Leaving them empty is supported by modern Home Assistant; on older versions you may see a non-fatal warning in the log. The automation continues to work either way.
Author
Alexei Dolgolyov (dolgolyov.alexei@gmail.com)