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:
@@ -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 (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:
|
||||
|
||||
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
|
||||
@@ -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)
|
||||
@@ -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 }}"
|
||||
@@ -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
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.9.1"
|
||||
"version": "2.13.0"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user