feat: add Presence Scene Controller and Time Of Day Selector blueprints

- 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
This commit is contained in:
2026-05-27 13:10:03 +03:00
parent 3d15f481a0
commit fafcf116be
6 changed files with 985 additions and 1 deletions
+158
View File
@@ -0,0 +1,158 @@
# 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](../Day%20Scene%20Controller/README.md) — but adds presence gating, vacant/sleep scenes, and explicit coexistence with the [Motion Light](../Motion%20Light/README.md) 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 (36 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:
1. **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.
2. **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.
3. 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 `off` for 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:
1. **Yield to Motion Light.** If the configured Motion Light state entity reports the same room is in `ENABLING`/`ENABLED`/`MANUAL`, stop immediately.
2. **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).
3. **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.
4. **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.
5. **Skip if same scene.** If a `last_applied_state_entity` is configured and the target scene matches what was last applied, stop.
6. **Apply the target scene.**
7. **Persist last-applied scene** (if a state entity is configured).
8. **Post-apply re-check.** After 300 ms re-read Motion Light state; if Motion Light has since claimed the room, stop without running callbacks.
9. **Run callback**`vacant_scene_applied_callback` or `scene_applied_callback` depending 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/MANUAL` for 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_on` is 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 `unavailable` or `unknown` are treated as **occupied** — safer than the alternative (briefly plunging a real-occupied room into the vacant scene during a Zigbee hiccup).
- `presence_off_timeout` debounces 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_boolean` into Motion Light's `condition_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`, and `motion_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)
@@ -0,0 +1,626 @@
# Presence Scene Controller Blueprint
# Per-room presence-aware time-of-day scene controller.
# See README.md for detailed documentation.
#
# Author: Alexei Dolgolyov (dolgolyov.alexei@gmail.com)
blueprint:
name: "Custom: Presence Scene Controller"
description: >
Per-room presence-aware time-of-day scene controller. Maps scenes to
time-of-day options by index (like Day Scene Controller), but adds
presence gating, vacant/sleep scenes, and Motion Light coexistence.
Designed to take over the per-room responsibilities of Day Scene
Controller — keep Day Scene Controller for genuinely house-wide
scenes (Away, Goodnight, All-Off).
domain: automation
input:
# -------------------------------------------------------------------------
# State Sources
# -------------------------------------------------------------------------
states_group:
name: States
collapsed: false
input:
time_of_day_state:
name: Time of Day State Selector
description: >
input_select entity holding the current time-of-day state.
Typically managed by Time of Day Selector or Time of Day
Controller.
selector:
entity:
domain: input_select
room_enable_switch:
name: Room Enable Switch (optional)
description: >
When OFF, the vacant scene is applied and TOD/presence are
ignored. Useful as a per-room kill switch (movie night, guest
bedroom). Leave empty to skip this gate.
default: ""
selector:
entity:
domain:
- binary_sensor
- input_boolean
- switch
# -------------------------------------------------------------------------
# Presence
# -------------------------------------------------------------------------
presence_group:
name: Presence
collapsed: false
input:
presence_sensors:
name: Presence Sensors (optional)
description: >
Any presence-related entities — motion sensors, mmWave,
occupancy groups, input_booleans. ANY-on logic: the room is
occupied if any sensor reports on. Sensors reporting
unavailable/unknown are treated as on (safe default).
Leave empty to disable presence gating; the room is then
always treated as occupied and the vacant scene is never
applied.
default: []
selector:
entity:
domain:
- binary_sensor
- input_boolean
- group
multiple: true
presence_off_timeout:
name: Presence Off Timeout (seconds)
description: >
How long all presence sensors must report off before the
room is considered vacant. Filters PIR drop-outs and short
absences.
default: 120
selector:
number:
min: 0
max: 3600
step: 5
unit_of_measurement: seconds
# -------------------------------------------------------------------------
# Scenes
# -------------------------------------------------------------------------
scenes_group:
name: Scenes
collapsed: false
input:
scenes:
name: Time-of-Day Scenes
description: >
Ordered list of scenes matching time-of-day options by index.
Length must equal the number of time-of-day options.
selector:
entity:
domain: scene
multiple: true
vacant_scene:
name: Vacant Scene
description: >
Scene applied when the room becomes vacant (after the
presence-off timeout) or when the room enable switch is OFF.
Typically a dim or all-off scene.
selector:
entity:
domain: scene
sleep_scene:
name: Sleep Scene (optional)
description: >
Scene applied when the sleep mode switch is ON. Bypasses TOD
and presence entirely. Typically a very dim "do not disturb"
scene or all-off. Leave empty if sleep mode is not used.
default: ""
selector:
entity:
domain: scene
# -------------------------------------------------------------------------
# Behavior
# -------------------------------------------------------------------------
behavior_group:
name: Behavior
collapsed: false
input:
tod_behavior_while_occupied:
name: TOD Change Behavior While Occupied
description: >
What to do when time-of-day flips while the room is occupied:
- apply_if_lights_off (default): re-apply only if every
targeted light/switch in the new scene is currently off.
Doesn't disturb a sleeping/relaxing user, refreshes a
dark-but-occupied room.
- defer: never re-apply on TOD flip; wait for the next
vacancy cycle. Best for bedrooms with mmWave that always
reports occupancy.
- apply: always re-apply on TOD flip. Best for kitchens,
hallways, transit areas.
default: apply_if_lights_off
selector:
select:
options:
- apply_if_lights_off
- defer
- apply
sleep_mode_switch:
name: Sleep Mode Switch (optional)
description: >
input_boolean / binary_sensor that, when ON, forces the
sleep scene and ignores TOD/presence. Useful for shift
workers, daytime naps, or letting a partner enter without
lighting up the room. Requires sleep_scene to be configured
(otherwise the switch is silently ignored).
default: ""
selector:
entity:
domain:
- binary_sensor
- input_boolean
# -------------------------------------------------------------------------
# Motion Light Coexistence (advanced)
# -------------------------------------------------------------------------
motion_light_group:
name: Motion Light Coexistence
collapsed: true
input:
motion_light_state_entity:
name: Motion Light State Entity (optional)
description: >
input_text used by a Motion Light blueprint instance that
targets the same lights as this room. While Motion Light is
actively controlling the lights (state ENABLING/ENABLED/
MANUAL), this blueprint stays out of its way. State changes
on this entity also abort any in-flight scene application
via mode: restart.
default: ""
selector:
entity:
domain: input_text
motion_light_state_key:
name: Motion Light State Key (optional)
description: >
JSON key used inside Motion Light's state entity. Leave
empty to fall back to the room enable switch entity ID; set
explicitly to match Motion Light's
automation_state_placeholder_key.
default: ""
selector:
text:
# -------------------------------------------------------------------------
# Persistence
# -------------------------------------------------------------------------
persistence_group:
name: Persistence
collapsed: true
input:
last_applied_state_entity:
name: Last Applied Scene Entity (optional)
description: >
input_text used to remember the last scene this automation
applied, so identical re-applications can be skipped. Each
instance must have its own entity. Default max_length 100
is usually enough; ensure it fits the longest scene entity
ID. Leave empty to disable the skip-if-same-scene guard.
default: ""
selector:
entity:
domain: input_text
# -------------------------------------------------------------------------
# Callbacks
# -------------------------------------------------------------------------
actions_group:
name: Actions
collapsed: true
input:
scene_applied_callback:
name: Scene Applied Callback
description: >
Runs after a TOD or sleep scene is applied. Useful for
notifications, dependent automations.
default: []
selector:
action: {}
vacant_scene_applied_callback:
name: Vacant Scene Applied Callback
description: Runs after the vacant scene is applied.
default: []
selector:
action: {}
# -------------------------------------------------------------------------
# Debug
# -------------------------------------------------------------------------
debug_group:
name: Debug
collapsed: true
input:
enable_debug_notifications:
name: Enable Debug Notifications
description: >
Send a persistent notification on every evaluation with the
decision inputs and the resolved target scene.
default: false
selector:
boolean:
# Restart mode: rapid TOD/presence changes always use latest values, and
# Motion Light state changes can abort an in-flight scene application.
mode: restart
# =============================================================================
# Triggers
# =============================================================================
trigger:
# Time-of-day state changed
- platform: state
entity_id: !input time_of_day_state
id: tod_changed
# Any presence sensor reports on (immediate)
- platform: state
entity_id: !input presence_sensors
to: "on"
id: presence_on
# All presence sensors have been off for the configured timeout
- platform: state
entity_id: !input presence_sensors
to: "off"
for:
seconds: !input presence_off_timeout
id: presence_off
# Room enable switch toggled
- platform: state
entity_id: !input room_enable_switch
id: room_enable_changed
# Sleep mode switch toggled
- platform: state
entity_id: !input sleep_mode_switch
id: sleep_mode_changed
# Motion Light state entity changed (abort signal via mode: restart)
- platform: state
entity_id: !input motion_light_state_entity
id: motion_light_state_changed
# =============================================================================
# Variables
# =============================================================================
variables:
# ---------------------------------------------------------------------------
# Inputs
# ---------------------------------------------------------------------------
time_of_day_state: !input time_of_day_state
presence_sensors: !input presence_sensors
room_enable_switch: !input room_enable_switch
scenes_list: !input scenes
vacant_scene: !input vacant_scene
sleep_scene: !input sleep_scene
sleep_mode_switch: !input sleep_mode_switch
tod_behavior_while_occupied: !input tod_behavior_while_occupied
motion_light_state_entity: !input motion_light_state_entity
motion_light_state_key_input: !input motion_light_state_key
last_applied_state_entity: !input last_applied_state_entity
enable_debug_notifications: !input enable_debug_notifications
# ---------------------------------------------------------------------------
# Trigger context
# ---------------------------------------------------------------------------
trigger_id: "{{ trigger.id | default('') }}"
# ---------------------------------------------------------------------------
# TOD resolution
# ---------------------------------------------------------------------------
tod_state: "{{ states(time_of_day_state) }}"
tod_options: "{{ state_attr(time_of_day_state, 'options') or [] }}"
tod_index: "{{ tod_options.index(tod_state) if tod_state in tod_options else -1 }}"
tod_valid: "{{ tod_index >= 0 and tod_index < (scenes_list | length) }}"
tod_scene: "{{ scenes_list[tod_index] if tod_valid else '' }}"
# ---------------------------------------------------------------------------
# Presence resolution (ANY-on logic)
# Sensors reporting unavailable/unknown are treated as occupied — safer than
# the alternative of plunging an actually-occupied room into vacant scene.
# ---------------------------------------------------------------------------
has_presence_sensors: "{{ (presence_sensors | length) > 0 }}"
any_presence_on: >
{% set sensors = presence_sensors if presence_sensors is iterable else [presence_sensors] %}
{% set ns = namespace(on=false) %}
{% for s in sensors %}
{% set v = states(s) %}
{% if v in ['on', 'home', 'true', '1', 'unavailable', 'unknown'] %}
{% set ns.on = true %}
{% endif %}
{% endfor %}
{{ ns.on }}
is_occupied: "{{ (not has_presence_sensors) or any_presence_on }}"
# ---------------------------------------------------------------------------
# Room enable / sleep mode
# ---------------------------------------------------------------------------
room_enabled: >
{% if room_enable_switch in [none, ''] %}
true
{% else %}
{{ states(room_enable_switch) in ['on', 'home', 'true', '1'] }}
{% endif %}
sleep_active: >
{% if sleep_mode_switch in [none, ''] or sleep_scene in [none, ''] %}
false
{% else %}
{{ states(sleep_mode_switch) in ['on', 'home', 'true', '1'] }}
{% endif %}
# ---------------------------------------------------------------------------
# Motion Light coexistence
# State JSON shape (from Motion Light):
# { "<key>": { "mls": "0|1|2|3", "mllat": "...", "mllb": ... } }
# mls: 0=NONE, 1=ENABLED, 2=ENABLING, 3=MANUAL.
# ---------------------------------------------------------------------------
motion_light_state_global: >
{% if motion_light_state_entity in [none, ''] %}
{{ dict() }}
{% else %}
{% set text = states(motion_light_state_entity) | string %}
{% if text in ['unknown', 'unavailable', 'none', ''] %}
{{ dict() }}
{% else %}
{% set parsed = text | from_json(default=dict()) %}
{{ parsed if parsed is mapping else dict() }}
{% endif %}
{% endif %}
motion_light_resolved_key: >
{% if motion_light_state_key_input not in [none, ''] %}
{{ motion_light_state_key_input }}
{% else %}
{{ room_enable_switch | default('', true) }}
{% endif %}
motion_light_room_state: >
{% set inner = motion_light_state_global.get(motion_light_resolved_key, dict()) %}
{{ (inner.get('mls', '0') | string) if inner is mapping else '0' }}
motion_light_claimed: "{{ motion_light_room_state in ['1', '2', '3'] }}"
# ---------------------------------------------------------------------------
# Target scene (recomputed at apply time, never cached at trigger time)
# ---------------------------------------------------------------------------
target_scene: >
{% if sleep_active %}
{{ sleep_scene }}
{% elif not room_enabled %}
{{ vacant_scene }}
{% elif is_occupied and tod_valid %}
{{ tod_scene }}
{% else %}
{{ vacant_scene }}
{% endif %}
is_vacant_target: "{{ target_scene == vacant_scene }}"
# ---------------------------------------------------------------------------
# Skip-if-same-scene guard
# ---------------------------------------------------------------------------
last_applied_scene: >
{% if last_applied_state_entity in [none, ''] %}
''
{% else %}
{{ states(last_applied_state_entity) | string }}
{% endif %}
# ---------------------------------------------------------------------------
# `apply_if_lights_off` evaluation: are all entities in the target scene
# currently off? Looks at the scene's entity_id attribute.
# ---------------------------------------------------------------------------
scene_lights_off: >
{% set ns = namespace(any_on=false) %}
{% if target_scene not in [none, ''] %}
{% set entities = state_attr(target_scene, 'entity_id') or [] %}
{% for e in entities %}
{% if (e.startswith('light.') or e.startswith('switch.')) and is_state(e, 'on') %}
{% set ns.any_on = true %}
{% endif %}
{% endfor %}
{% endif %}
{{ not ns.any_on }}
# =============================================================================
# Actions
# =============================================================================
action:
# ---------------------------------------------------------------------------
# Debug: log entry state
# ---------------------------------------------------------------------------
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Presence Scene Controller — entry"
message: >
Trigger: {{ trigger_id }}
TOD: {{ tod_state }} (idx {{ tod_index }})
Occupied: {{ is_occupied }} | Enabled: {{ room_enabled }} | Sleep: {{ sleep_active }}
ML claimed: {{ motion_light_claimed }} (mls={{ motion_light_room_state }}, key={{ motion_light_resolved_key }})
Target: {{ target_scene }}
Last applied: {{ last_applied_scene }}
# ---------------------------------------------------------------------------
# Yield to Motion Light if it's currently controlling the room
# ---------------------------------------------------------------------------
- choose:
- conditions:
- condition: template
value_template: "{{ motion_light_claimed }}"
sequence:
- stop: "Motion Light is controlling this room — yielding."
# ---------------------------------------------------------------------------
# Motion Light state-change is purely an abort signal. If we reach here and
# ML is NOT claimed, do nothing — ML may have just released the room and
# turned the lights off intentionally; we should not immediately re-apply.
# ---------------------------------------------------------------------------
- choose:
- conditions:
- condition: template
value_template: "{{ trigger_id == 'motion_light_state_changed' }}"
sequence:
- stop: "Motion Light state changed — abort signal only."
# ---------------------------------------------------------------------------
# Validate TOD configuration
# ---------------------------------------------------------------------------
- choose:
- conditions:
- condition: template
value_template: "{{ tod_index == -1 }}"
sequence:
- stop: "Invalid time-of-day state — current state not in options list."
- conditions:
- condition: template
value_template: "{{ is_occupied and room_enabled and not sleep_active and not tod_valid }}"
sequence:
- stop: "Scene index out of range — scenes list shorter than TOD options."
# ---------------------------------------------------------------------------
# TOD-while-occupied policy. Only kicks in when this run was triggered by a
# TOD flip AND the room is occupied AND no override mode is in effect.
# ---------------------------------------------------------------------------
- choose:
- conditions:
- condition: template
value_template: >
{{ trigger_id == 'tod_changed'
and is_occupied
and room_enabled
and not sleep_active }}
sequence:
- choose:
- conditions:
- condition: template
value_template: "{{ tod_behavior_while_occupied == 'defer' }}"
sequence:
- stop: "TOD changed while occupied — deferring per policy."
- conditions:
- condition: template
value_template: >
{{ tod_behavior_while_occupied == 'apply_if_lights_off' and not scene_lights_off }}
sequence:
- stop: "TOD changed while occupied with lights on — deferring."
# ---------------------------------------------------------------------------
# Skip if the target scene is already what we last applied
# ---------------------------------------------------------------------------
- choose:
- conditions:
- condition: template
value_template: >
{{ last_applied_scene != ''
and target_scene not in [none, '']
and last_applied_scene == target_scene }}
sequence:
- stop: "Scene already applied — no-op."
# ---------------------------------------------------------------------------
# Apply the target scene
# ---------------------------------------------------------------------------
- choose:
- conditions:
- condition: template
value_template: "{{ target_scene not in [none, ''] }}"
sequence:
- service: scene.turn_on
target:
entity_id: "{{ target_scene }}"
# ---------------------------------------------------------------------------
# Persist last-applied scene
# ---------------------------------------------------------------------------
- choose:
- conditions:
- condition: template
value_template: >
{{ last_applied_state_entity not in [none, '']
and target_scene not in [none, ''] }}
sequence:
- service: input_text.set_value
target:
entity_id: "{{ last_applied_state_entity }}"
data:
value: "{{ target_scene }}"
# ---------------------------------------------------------------------------
# Post-apply Motion Light re-check. Brief delay then re-read ML state — if
# Motion Light claimed the room while we were applying, do not contest.
# ---------------------------------------------------------------------------
- delay:
milliseconds: 300
- variables:
ml_state_after: >
{% if motion_light_state_entity in [none, ''] %}
0
{% else %}
{% set text = states(motion_light_state_entity) | string %}
{% if text in ['unknown', 'unavailable', 'none', ''] %}
0
{% else %}
{% set parsed = text | from_json(default=dict()) %}
{% if parsed is mapping %}
{% set inner = parsed.get(motion_light_resolved_key, dict()) %}
{{ (inner.get('mls', '0') | string) if inner is mapping else '0' }}
{% else %}
0
{% endif %}
{% endif %}
{% endif %}
- choose:
- conditions:
- condition: template
value_template: "{{ (ml_state_after | string) in ['1', '2', '3'] }}"
sequence:
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Presence Scene Controller"
message: "Motion Light claimed room post-apply — yielding without callback."
- stop: "Motion Light took over after scene apply."
# ---------------------------------------------------------------------------
# Run the appropriate callback
# ---------------------------------------------------------------------------
- choose:
- conditions:
- condition: template
value_template: "{{ is_vacant_target }}"
sequence: !input vacant_scene_applied_callback
default: !input scene_applied_callback
+51
View File
@@ -0,0 +1,51 @@
# Time of Day Selector Blueprint
Event-driven blueprint that updates an `input_select` based on a list of time triggers. Each trigger entity is mapped to a state name; when the trigger fires, the matching state becomes active.
This is a sibling to the [Time Of Day Controller](../Time%20Of%20Day%20Controller/README.md) blueprint, which uses minute-by-minute polling and threshold comparison. Use this one when you want exact event-based firing and the ability to attach multiple trigger sources (e.g. a sun event **or** a manual forced time) to the same state.
## How It Works
- Define a list of **time triggers**`input_datetime` helpers and/or timestamp sensors such as `sensor.sun_next_rising` / `sensor.sun_next_dusk` / `sensor.sun_next_midnight`.
- Define a parallel list of **state names** — one per trigger, in the same order.
- When any trigger fires, the blueprint looks up the firing entity in the trigger list and selects the matching state name on the target `input_select`.
- Duplicate state names produce **OR-override** behavior: whichever associated trigger fires first wins.
## Index Mapping
The two lists are matched by position:
| Index | Trigger entity | State name |
|-------|---------------|------------|
| 0 | `sensor.sun_next_rising` | `Утро` |
| 1 | `input_datetime.dnevnoe_vremia` | `День` |
| 2 | `sensor.sun_next_dusk` | `Вечер` |
| 3 | `input_datetime.vechernee_vremia_prinuditelno` | `Вечер` |
| 4 | `sensor.sun_next_midnight` | `Ночь` |
| 5 | `input_datetime.polnochnoe_vremia_prinuditelno` | `Ночь` |
In this example, `Вечер` is set when either the natural dusk time or the forced datetime fires — whichever comes first.
## Supported Time Sources
- `input_datetime` entities (time-only: `HH:MM:SS`).
- Timestamp sensors that report an ISO datetime (e.g. `sensor.sun_next_rising`).
## Configuration
| Input | Description |
|-------|-------------|
| **Time of Day Selector** | The `input_select` entity to update. Its options must include every state name in the list below. |
| **Time of Day Triggers** | Ordered list of trigger entities. |
| **Time of Day States** | Parallel list of state names, same length as triggers. |
| **Debug mode** | When enabled, posts a persistent notification on each fire with the firing entity, resolved state, and previous state. |
## Behavioral Notes
- `input_select.select_option` is idempotent — repeated triggers for the same state cause no extra state changes.
- A guard condition skips the action if the resolved state already matches the current `input_select` value.
- On Home Assistant restart, the state is **not** retroactively corrected — it updates on the next trigger. If you need self-correcting state on restart, use the [Time Of Day Controller](../Time%20Of%20Day%20Controller/README.md) (polling) blueprint instead.
## Author
Alexei Dolgolyov (dolgolyov.alexei@gmail.com)
+147
View File
@@ -0,0 +1,147 @@
# Time of Day Selector Blueprint
# Event-driven: sets an input_select when any configured time entity fires.
# See README.md for detailed documentation.
#
# Author: Alexei Dolgolyov (dolgolyov.alexei@gmail.com)
blueprint:
name: "Custom: Time of Day Selector"
description: >
Event-driven time-of-day state machine.
Maps a list of time entities to a parallel list of state names.
When any time entity fires, the corresponding state is selected on the
target input_select. Multiple entities mapping to the same state name
produce OR-override behavior (e.g. sun_next_dusk OR a forced
input_datetime — whichever fires first wins).
domain: automation
input:
# -------------------------------------------------------------------------
# Output Configuration
# -------------------------------------------------------------------------
tod_select:
name: Time of Day Selector
description: >
The input_select entity whose option will be updated.
Its options must include every state name listed below.
selector:
entity:
domain: input_select
# -------------------------------------------------------------------------
# Trigger / State Mapping
# -------------------------------------------------------------------------
tod_times:
name: Time of Day Triggers
description: >
Ordered list of time entities. Each fires when its configured time
is reached. Accepts input_datetime (time-only) and timestamp sensors
(e.g. sensor.sun_next_rising, sensor.sun_next_dusk).
selector:
entity:
domain:
- input_datetime
- sensor
multiple: true
tod_states:
name: Time of Day States
description: >
Parallel list of state names — one entry for each trigger above, in
the same order. Each name must match an option of the target
input_select exactly. Repeat a name to give that state multiple
trigger sources (override / "whichever fires first" pattern).
selector:
text:
multiple: true
# -------------------------------------------------------------------------
# Debug
# -------------------------------------------------------------------------
is_debug:
name: Debug mode
description: >
When enabled, posts a persistent notification on each fire showing
the firing entity, resolved state, and previous state.
default: false
selector:
boolean:
# Single mode is sufficient: select_option is idempotent and trigger collisions
# are extremely rare (different time entities reaching the same instant).
mode: single
# =============================================================================
# Triggers
# =============================================================================
trigger:
- platform: time
at: !input tod_times
# =============================================================================
# Variables
# =============================================================================
variables:
tod_select: !input tod_select
tod_times: !input tod_times
tod_states: !input tod_states
is_debug: !input is_debug
# Index of the entity that fired, within tod_times. -1 if not found.
fired_index: >
{% set ns = namespace(idx=-1) %}
{% for e in tod_times %}
{% if e == trigger.entity_id %}
{% set ns.idx = loop.index0 %}
{% endif %}
{% endfor %}
{{ ns.idx }}
# State name resolved from the firing entity. Empty if lookup failed
# (e.g. mismatched list lengths or unknown trigger entity).
target_state: >
{%- if fired_index >= 0 and fired_index < tod_states | length -%}
{{ tod_states[fired_index] }}
{%- endif -%}
previous_state: "{{ states(tod_select) }}"
# =============================================================================
# Condition - Only proceed if a valid state was resolved and it differs
# =============================================================================
condition:
- condition: template
value_template: "{{ target_state | length > 0 and target_state != previous_state }}"
# =============================================================================
# Actions
# =============================================================================
action:
# ---------------------------------------------------------------------------
# Debug Logging (optional)
# ---------------------------------------------------------------------------
- choose:
- conditions:
- condition: template
value_template: "{{ is_debug }}"
sequence:
- service: persistent_notification.create
data:
title: "Time of Day Selector"
message: >
Fired entity: {{ trigger.entity_id }}
Index in triggers list: {{ fired_index }}
Previous state: {{ previous_state }}
Target state: {{ target_state }}
# ---------------------------------------------------------------------------
# Update Time of Day State
# ---------------------------------------------------------------------------
- service: input_select.select_option
target:
entity_id: "{{ tod_select }}"
data:
option: "{{ target_state }}"
+2
View File
@@ -14,11 +14,13 @@ A collection of automation blueprints for Home Assistant.
| Home Presence | Determines home presence from multiple signals |
| Immich Album Watcher | Sends notifications when photos are added to Immich albums |
| Motion Light | Smart motion sensor light control |
| Presence Scene Controller | Per-room presence-aware time-of-day scene controller |
| Refrigerator | Monitors refrigerator temperature and triggers express mode |
| Telegram Commands | Responds to Telegram bot commands with callback actions |
| Telegram Question | Sends Telegram messages with inline keyboard buttons |
| Thermostat Controller | Controls thermostat based on schedules, presence, and window sensors |
| Time Of Day Controller | Sets input_select based on time-of-day thresholds |
| Time Of Day Selector | Sets an input_select state when a configured time entity fires |
| Track Abnormal Plug Activity | Monitors power sensors for sustained overconsumption |
| Washing Machine | Sends notifications for washing machine events |
+1 -1
View File
@@ -1,3 +1,3 @@
{
"version": "2.9.1"
"version": "2.13.0"
}