1 Commits

Author SHA1 Message Date
alexei.dolgolyov 13132323ea Add debug logging and quiet hours support to Immich Album Watcher Telegram notifications
- Add debug persistent notifications after all 13 send_telegram_notification calls
  (periodic summary, scheduled per-album/combined, memory per-album/combined,
  album renamed/deleted, assets added text/media) logging chat ID, caption,
  assets count, reply-to ID, and service response when debug mode is enabled
- Replace removed ignore_quiet_hours parameter with new quiet_hours_start and
  quiet_hours_end time inputs (defaults: 23:00-07:00) on all Telegram service calls
- Update README with quiet hours documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 00:42:26 +03:00
22 changed files with 645 additions and 2540 deletions
-57
View File
@@ -1,57 +0,0 @@
# vex configuration — https://github.com/tenatarika/vex
#
# Place this file in your project root as .vex.toml
# Glob patterns to exclude from indexing (gitignore syntax, on top of .gitignore)
# exclude = [
# "vendor/**",
# "node_modules/**",
# "*.generated.go",
# "dist/**",
# ]
# Default output format: "text", "json", or "compact"
# format = "text"
# Enable semantic embeddings by default (slower indexing, enables meaning-based search)
semantic = true
# Automatically run `vex update` before search if the index is stale
auto_update = true
# Embedder used for semantic indexing. Known IDs: minilm-l6-v2 (default).
# Changing the embedder requires a full reindex.
# embedder = "minilm-l6-v2"
# Cache directory override. Defaults to the platform cache location.
# macOS: ~/Library/Caches/vex
# Linux: $XDG_CACHE_HOME/vex (fallback: ~/.cache/vex)
# Windows: %LOCALAPPDATA%\vex (fallback: %USERPROFILE%\AppData\Local\vex)
# Accepts absolute paths, "~/..." or paths relative to this file (e.g. "./.vex/cache").
# Can also be overridden per-invocation with --cache-dir or $VEX_CACHE_DIR.
# cache_dir = "./.vex/cache"
# Store the index inside the project as `<project>/.vex_cache/`. Useful when
# the cache should travel with the project (e.g. on a moved or renamed
# directory). vex writes a `.gitignore` inside it so contents are not
# committed. Overridden by `cache_dir`, `--cache-dir`, or $VEX_CACHE_DIR.
# local_cache = false
# Thread count for parallel indexing (index/update/watch).
# * unset — 80% of available cores, rounded up (default, leaves headroom)
# * 0 — use all cores (explicit opt-in to max throughput)
# * N — exactly N workers
# Overridable per-invocation with `-j/--jobs` or $VEX_JOBS.
# jobs = 4
# Build the persistent call-graph section. Disabling falls back to live-scan
# for `vex callers`/`vex callees` (slower per-query, but saves indexing
# time on large monorepos). The opt-out is persisted in the manifest so
# `vex update` does not silently re-add the section.
# Per-invocation override: `vex index --no-call-graph`.
# call_graph = true
# Build the BM25 channel. Disabling drops the third RRF channel and keeps
# only structural (+ semantic). Same persistence rules as `call_graph`.
# Per-invocation override: `vex index --no-bm25`.
# bm25 = true
+1 -44
View File
@@ -8,50 +8,7 @@ This blueprint monitors multiple binary sensors and triggers push notifications
- Debounce timer to prevent false alarms from brief sensor triggers - Debounce timer to prevent false alarms from brief sensor triggers
- Optional melody and volume selection for alarm devices - Optional melody and volume selection for alarm devices
- Automatic alarm turn-off when all sensors clear - Automatic alarm turn-off when all sensors clear
- Notifications are only sent on sensor ON (not on clear)
- Built-in debug mode that creates persistent notifications at each stage
## Debug Mode
Enable the **Debug****Enable Debug Logging** toggle to create persistent
notifications at each execution stage. Use this when notifications are not
reaching the target device:
1. **[1/4] Trigger Received** — shows trigger ID, entity, state transition,
which sensors are currently on, the resolved notify target, and whether
it is considered "usable" by the condition check.
2. **[2/4] Sending Notification** — shows the target entity, sensor index,
and the exact message that will be sent.
3. **[3/4] Notification Sent** — shown after the `notify.send_message` call.
Contains a troubleshooting checklist if the notification never arrives.
4. **[4/4] Alarm Control Done** — shows which alarm switch action was taken.
Additionally, stage info is written to the HA system log under the
`blueprint.alarm_notification` logger.
### Common causes of "no notification delivered"
- The notify integration is offline or the target device has notifications
disabled.
- `notify_target` is not actually a notify **entity**. Test from Developer
Tools → Actions → `notify.send_message` with the same target and
message; if that fails there, it will fail in the blueprint too.
- The sensor state change was shorter than the debounce duration and the
`sensor_on` trigger never fired. Temporarily set debounce to `0` to rule
this out.
- The notify integration rejects the message (e.g., Telegram bot with an
invalid `chat_id` in its configuration). Check the HA log for the
failing service call.
### Implementation note: notify via `notify.send_message`
The blueprint uses `action: notify.send_message` with
`target.entity_id: "{{ notify_target }}"`. This is the canonical modern HA
pattern and requires `notify_target` to be a notify **entity** (visible in
Developer Tools → States). Integrations that only register a `notify.<name>`
service but no entity are not supported by this blueprint directly — create
a notify entity for them or adapt the blueprint.
## Author ## Author
Alexei Dolgolyov (<dolgolyov.alexei@gmail.com>) Alexei Dolgolyov (dolgolyov.alexei@gmail.com)
+31 -255
View File
@@ -1,5 +1,5 @@
# Multi-Sensor Alarm & Notification Blueprint # Multi-Sensor Alarm & Notification Blueprint
# Monitors sensors and triggers notifications and alarm actions when activated. # Monitors sensors and triggers notifications and alarms when activated.
# See README.md for detailed documentation. # See README.md for detailed documentation.
blueprint: blueprint:
@@ -119,23 +119,6 @@ blueprint:
selector: selector:
text: text:
# -------------------------------------------------------------------------
# Debug Configuration
# -------------------------------------------------------------------------
debug_group:
name: "Debug"
collapsed: true
input:
debug_enabled:
name: Enable Debug Logging
description: >
When enabled, creates persistent notifications at each stage of execution:
trigger received, notification attempt, notification result, alarm control.
Use to troubleshoot why notifications aren't reaching the target.
default: false
selector:
boolean:
# Restart mode ensures rapid sensor changes are handled cleanly # Restart mode ensures rapid sensor changes are handled cleanly
mode: restart mode: restart
@@ -170,31 +153,23 @@ variables:
melody_id: !input melody_id melody_id: !input melody_id
volume_select: !input volume_select volume_select: !input volume_select
volume_id: !input volume_id volume_id: !input volume_id
is_debug: !input debug_enabled
# Computed state values # Computed state values
enabled_sensors: "{{ binary_sensors | select('is_state', 'on') | list }}" enabled_sensors: "{{ binary_sensors | select('is_state', 'on') | list }}"
is_any_sensor_on: "{{ (binary_sensors | select('is_state', 'on') | list | length) > 0 }}" is_any_sensor_on: "{{ enabled_sensors | length > 0 }}"
# Whether a usable notify target is configured
has_notify_target: "{{ notify_target is not none and (notify_target | string | trim) not in ['', '[]', 'None'] }}"
# Whether alarm control is configured
has_alarm_switch: "{{ alarm_switch is not none and (alarm_switch | string | trim) not in ['', '[]', 'None'] }}"
# Small delay between setting melody/volume to ensure device processes each command # Small delay between setting melody/volume to ensure device processes each command
delay_between_commands_ms: 100 delay_between_commands_ms: 100
# Version stamp so debug output can confirm which blueprint revision is live. # Debug flag - set to true to enable persistent notifications for troubleshooting
# Bump this whenever debug/flow logic changes. is_debug: false
blueprint_version: "2.6.4 (notify.send_message + entity_id)"
# ============================================================================= # =============================================================================
# Actions # Actions
# ============================================================================= # =============================================================================
action: action:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Debug Stage 1: Trigger Received # Debug Logging (optional)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
- choose: - choose:
- conditions: - conditions:
@@ -203,222 +178,45 @@ action:
sequence: sequence:
- service: persistent_notification.create - service: persistent_notification.create
data: data:
notification_id: "alarm_debug_trigger" title: "Alarm Notification Debug"
title: "Alarm Debug [1/4]: Trigger Received"
message: > message: >
Blueprint version: {{ blueprint_version }} Trigger: {{ trigger.id }}
Trigger ID: {{ trigger.id }}
Entity: {{ trigger.entity_id }} Entity: {{ trigger.entity_id }}
From: {{ trigger.from_state.state if trigger.from_state is not none else 'n/a' }} Sensors: {{ binary_sensors }}
→ To: {{ trigger.to_state.state if trigger.to_state is not none else 'n/a' }} Active: {{ enabled_sensors }}
Any On: {{ is_any_sensor_on }}
All monitored sensors: {{ binary_sensors }}
Currently active (on): {{ enabled_sensors }}
Any sensor on: {{ is_any_sensor_on }}
Notify target: {{ notify_target }}
Has notify target: {{ has_notify_target }}
Alarm switch: {{ alarm_switch }}
Has alarm switch: {{ has_alarm_switch }}
- service: system_log.write
data:
level: info
logger: blueprint.alarm_notification
message: >-
Trigger '{{ trigger.id }}' from {{ trigger.entity_id }};
active={{ enabled_sensors }};
notify_target={{ notify_target }};
has_notify_target={{ has_notify_target }}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Debug Stage 1.5: What the notification-branch conditions evaluate to # Send Notification (when sensor turns ON)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
- choose: - choose:
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ is_debug }}" # Only notify if a sensor is on and notification target is configured
sequence: value_template: "{{ is_any_sensor_on and notify_target is not none }}"
- service: persistent_notification.create
data:
notification_id: "alarm_debug_pre_notify"
title: "Alarm Debug [1.5/4]: Evaluating notify conditions"
message: >
trigger.id = {{ trigger.id }}
trigger.id == 'sensor_on': {{ trigger.id == 'sensor_on' }}
has_notify_target: {{ has_notify_target }}
Combined (should be True to enter notify branch):
{{ trigger.id == 'sensor_on' and has_notify_target }}
# ---------------------------------------------------------------------------
# Send Notification (only on sensor_on, when target configured)
# ---------------------------------------------------------------------------
- choose:
- conditions:
# Single template condition avoids any quirks with condition: trigger
# inside a choose block.
- condition: template
value_template: "{{ trigger.id == 'sensor_on' and has_notify_target }}"
sequence: sequence:
- variables: - variables:
# The sensor that fired this trigger # Get the sensor that triggered this automation
sensor: "{{ trigger.entity_id }}" sensor: "{{ trigger.entity_id }}"
# Namespace-loop avoids Jinja sandbox restrictions on list.index() # Find index of this sensor in the list
sensor_index: >- sensor_index: >
{%- set ns = namespace(idx=-1) -%} {% set idx = binary_sensors | list %}
{%- for s in binary_sensors -%} {{ idx.index(sensor) if sensor in idx else -1 }}
{%- if s == sensor -%}{%- set ns.idx = loop.index0 -%}{%- endif -%} # Get custom message or use default
{%- endfor -%} message: >
{{ ns.idx }} {% set messages = notify_texts | list %}
# Resolve message (strip whitespace to keep notifications clean) {% if sensor_index >= 0 and sensor_index < messages | length %}
message: >- {{ messages[sensor_index] }}
{%- set messages = notify_texts | list -%} {% else %}
{%- set idx = sensor_index | int(-1) -%} Alarm: {{ state_attr(sensor, 'friendly_name') | default(sensor) }} triggered
{%- if idx >= 0 and idx < messages | length -%} {% endif %}
{{ messages[idx] }}
{%- else -%}
Alarm: {{ state_attr(sensor, 'friendly_name') or sensor }} triggered
{%- endif -%}
# Debug Stage 2: Variables computed, about to send - service: notify.send_message
- choose:
- conditions:
- condition: template
value_template: "{{ is_debug }}"
sequence:
- service: persistent_notification.create
data:
notification_id: "alarm_debug_notify_attempt"
title: "Alarm Debug [2/4]: Sending Notification"
message: >
Target: {{ notify_target }}
Sensor: {{ sensor }}
Sensor index (raw): "{{ sensor_index }}"
Messages defined: {{ notify_texts | length }}
Resolved message: "{{ message }}"
# Debug Stage 2.25: About to invoke service
- choose:
- conditions:
- condition: template
value_template: "{{ is_debug }}"
sequence:
- service: persistent_notification.create
data:
notification_id: "alarm_debug_pre_service"
title: "Alarm Debug [2.25/4]: Before notify.send_message"
message: >
Blueprint version: {{ blueprint_version }}
About to call: action notify.send_message
with target.entity_id: {{ notify_target }}
and data.message: "{{ message }}"
# Send via notify.send_message targeting the notify entity.
# This matches the canonical HA pattern used by working automations.
# continue_on_error ensures the alarm-control stage still runs
# if the notify integration is misconfigured.
- action: notify.send_message
metadata: {}
target: target:
entity_id: "{{ notify_target }}" entity_id: "{{ notify_target }}"
data: data:
message: "{{ message }}" message: "{{ message }}"
continue_on_error: true
# Debug Stage 2.75: Service call returned (success or caught error)
- choose:
- conditions:
- condition: template
value_template: "{{ is_debug }}"
sequence:
- service: persistent_notification.create
data:
notification_id: "alarm_debug_post_service"
title: "Alarm Debug [2.75/4]: After notify.send_message"
message: >
notify.send_message to {{ notify_target }} returned
(either success or continue_on_error caught it).
If this notification is missing but [2.25/4] is
present, the call raised an error that
continue_on_error did NOT catch. Check the HA log
and verify {{ notify_target }} is a notify entity
(Developer Tools → States should list it).
# Debug Stage 3: Notification result
- choose:
- conditions:
- condition: template
value_template: "{{ is_debug }}"
sequence:
- service: persistent_notification.create
data:
notification_id: "alarm_debug_notify_result"
title: "Alarm Debug [3/4]: Notification Sent"
message: >
notify.send_message dispatched to {{ notify_target }}
with message: "{{ message }}".
If nothing arrived on the device, check:
1) HA log for errors from the notify integration
2) That the underlying integration (Telegram bot,
mobile app, etc.) is online
3) Test from Developer Tools → Actions:
notify.send_message with target {{ notify_target }}
- service: system_log.write
data:
level: info
logger: blueprint.alarm_notification
message: >-
Notification attempted to {{ notify_target }};
sensor={{ sensor }}; message='{{ message }}'
# ---------------------------------------------------------------------------
# Debug Stage 3.5: Unconditional checkpoint so we can tell whether the
# notification branch silently aborted the script above. If this appears
# but [2/4] or [3/4] don't, the variables block or notify.send_message
# errored silently.
# ---------------------------------------------------------------------------
- choose:
- conditions:
- condition: template
value_template: "{{ is_debug }}"
sequence:
- service: persistent_notification.create
data:
notification_id: "alarm_debug_after_notify"
title: "Alarm Debug [3.5/4]: After notify branch"
message: >
Execution reached this point. trigger.id = {{ trigger.id }}.
If [2/4] and [3/4] did NOT appear above, the notify branch
either did not enter (check [1.5/4]), or errored silently
in the variables / notify.send_message step.
Check Home Assistant log for Jinja or service errors.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Alarm Device Control # Alarm Device Control
@@ -426,20 +224,20 @@ action:
- choose: - choose:
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ has_alarm_switch }}" # Only control alarm if alarm switch is configured
value_template: "{{ alarm_switch | length > 0 }}"
sequence: sequence:
# Set melody (if configured) # Set melody (if configured)
- choose: - choose:
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ melody_select is not none and (melody_select | string | trim) not in ['', '[]', 'None'] and (melody_id | string | length) > 0 }}" value_template: "{{ melody_select | length > 0 and melody_id | length > 0 }}"
sequence: sequence:
- service: select.select_option - service: select.select_option
target: target:
entity_id: "{{ melody_select }}" entity_id: "{{ melody_select }}"
data: data:
option: "{{ melody_id }}" option: "{{ melody_id }}"
continue_on_error: true
- delay: - delay:
milliseconds: "{{ delay_between_commands_ms }}" milliseconds: "{{ delay_between_commands_ms }}"
@@ -447,14 +245,13 @@ action:
- choose: - choose:
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ volume_select is not none and (volume_select | string | trim) not in ['', '[]', 'None'] and (volume_id | string | length) > 0 }}" value_template: "{{ volume_select | length > 0 and volume_id | length > 0 }}"
sequence: sequence:
- service: select.select_option - service: select.select_option
target: target:
entity_id: "{{ volume_select }}" entity_id: "{{ volume_select }}"
data: data:
option: "{{ volume_id }}" option: "{{ volume_id }}"
continue_on_error: true
- delay: - delay:
milliseconds: "{{ delay_between_commands_ms }}" milliseconds: "{{ delay_between_commands_ms }}"
@@ -468,30 +265,9 @@ action:
- service: switch.turn_on - service: switch.turn_on
target: target:
entity_id: "{{ alarm_switch }}" entity_id: "{{ alarm_switch }}"
continue_on_error: true
# All sensors clear -> turn alarm OFF # All sensors clear -> turn alarm OFF
default: default:
- service: switch.turn_off - service: switch.turn_off
target: target:
entity_id: "{{ alarm_switch }}" entity_id: "{{ alarm_switch }}"
continue_on_error: true
# Debug Stage 4: Alarm control done
- choose:
- conditions:
- condition: template
value_template: "{{ is_debug }}"
sequence:
- service: persistent_notification.create
data:
notification_id: "alarm_debug_alarm_control"
title: "Alarm Debug [4/4]: Alarm Control Done"
message: >
Alarm switch: {{ alarm_switch }}
Action: {{ 'turn_on' if is_any_sensor_on else 'turn_off' }}
Melody: {{ melody_id }} → {{ melody_select }}
Volume: {{ volume_id }} → {{ volume_select }}
+12 -136
View File
@@ -1,6 +1,6 @@
# Dreame Vacuum Notifications # Dreame Vacuum Notifications
Sends customizable notifications for Dreame vacuum events. Requires the [Dreame Vacuum](https://github.com/Tasshack/dreame-vacuum) integration and Home Assistant 2024.7+ (`notify.send_message` action). Sends customizable notifications for Dreame vacuum events. Requires the [Dreame Vacuum](https://github.com/Tasshack/dreame-vacuum) integration.
## Features ## Features
@@ -8,11 +8,8 @@ Sends customizable notifications for Dreame vacuum events. Requires the [Dreame
- Consumable end-of-life alerts (brush, filter, mop pad, sensor, etc.) - Consumable end-of-life alerts (brush, filter, mop pad, sensor, etc.)
- Device warning and error notifications - Device warning and error notifications
- Informational alerts (e.g., action blocked by Do Not Disturb) - Informational alerts (e.g., action blocked by Do Not Disturb)
- Friendly, readable labels for cleaning mode and status (raw enum names are translated)
- Optional localization: override any label, and map numeric codes to your own text
- Individual toggle for each event type - Individual toggle for each event type
- Per-code/per-type filter lists for silencing routine warnings, errors, or info messages - Customizable message templates with variable substitution
- Customizable message templates
- Multiple notification targets - Multiple notification targets
## How It Works ## How It Works
@@ -27,16 +24,7 @@ The blueprint listens to events fired by the Dreame Vacuum integration:
| `dreame_vacuum_error` | Device fault | | `dreame_vacuum_error` | Device fault |
| `dreame_vacuum_information` | Action blocked by user settings | | `dreame_vacuum_information` | Action blocked by user settings |
### Matching the configured vacuum Events are filtered by the configured vacuum entity, so only the selected vacuum triggers notifications.
The integration fires every event with an `entity_id` that it derives from the vacuum's **device name** (via `generate_entity_id()`); events do **not** carry a `device_id`. Matching therefore accepts:
1. The event `entity_id` exactly equal to the configured entity.
2. The configured entity followed by a purely numeric suffix (e.g. `vacuum.dreame_x10_2`) — `generate_entity_id()` adds such a suffix when the base id is already taken.
Non-numeric suffixes (e.g. `_pro`) are rejected, so `vacuum.dreame_x10` will not match `vacuum.dreame_x10_pro`.
> **Note:** because the fired `entity_id` is derived from the device name, if you rename the vacuum's entity_id in Home Assistant so it no longer matches the auto-generated id, matching can fail and notifications stop. Keep the integration-generated entity_id (or rename the *device* instead of the entity) for reliable matching.
## Configuration ## Configuration
@@ -45,15 +33,7 @@ Non-numeric suffixes (e.g. `_pro`) are rejected, so `vacuum.dreame_x10` will not
| **Vacuum Entity** | The Dreame vacuum entity to monitor | | **Vacuum Entity** | The Dreame vacuum entity to monitor |
| **Notification Targets** | One or more `notify` entities | | **Notification Targets** | One or more `notify` entities |
| **Event Toggles** | Enable/disable each event type independently | | **Event Toggles** | Enable/disable each event type independently |
| **Filtering** | Lists of warning/error codes and information types to silence |
| **Message Templates** | Customizable message for each event type | | **Message Templates** | Customizable message for each event type |
| **Localization** | Optional label and code translation tables (see [Localization](#localization)) |
### Filtering
- **Warning Codes / Error Codes to Ignore** — compared against the event's numeric `code`. Enter the code as text (e.g. `68`).
- **Information Types to Ignore** — information events carry a *type id*, not a numeric code; enter the id (`dust_collection`, `cleaning_paused`).
- The two temporary-map warnings (new-map / replace-temporary-map) carry **no** code. The spurious "map cleared" warning is suppressed automatically; the genuine "new map generated" warning can be filtered by its text if needed.
## Message Template Variables ## Message Template Variables
@@ -62,16 +42,16 @@ Non-numeric suffixes (e.g. `_pro`) are rejected, so `vacuum.dreame_x10` will not
| Variable | Description | | Variable | Description |
| --- | --- | | --- | --- |
| `{vacuum_name}` | Friendly name of the vacuum | | `{vacuum_name}` | Friendly name of the vacuum |
| `{cleaning_mode}` | Cleaning mode, friendly-formatted (e.g., Sweeping, Mopping, Sweeping & Mopping) | | `{cleaning_mode}` | Cleaning mode (e.g., sweeping, mopping) |
| `{status}` | Current status, friendly-formatted (e.g., Cleaning, Room cleaning) | | `{status}` | Current status |
### Cleaning Completed ### Cleaning Completed
| Variable | Description | | Variable | Description |
| --- | --- | | --- | --- |
| `{vacuum_name}` | Friendly name of the vacuum | | `{vacuum_name}` | Friendly name of the vacuum |
| `{cleaning_mode}` | Cleaning mode used, friendly-formatted | | `{cleaning_mode}` | Cleaning mode used |
| `{status}` | Final status, friendly-formatted | | `{status}` | Final status |
| `{cleaned_area}` | Area cleaned (m²) | | `{cleaned_area}` | Area cleaned (m²) |
| `{cleaning_time}` | Cleaning duration (minutes) | | `{cleaning_time}` | Cleaning duration (minutes) |
@@ -87,18 +67,16 @@ Non-numeric suffixes (e.g. `_pro`) are rejected, so `vacuum.dreame_x10` will not
| Variable | Description | | Variable | Description |
| --- | --- | | --- | --- |
| `{vacuum_name}` | Friendly name of the vacuum | | `{vacuum_name}` | Friendly name of the vacuum |
| `{warning}` | Warning description (already human-readable English from the integration) | | `{warning}` | Warning description |
| `{code}` | Numeric warning code (empty for the temporary-map warning) | | `{code}` | Warning code |
| `{code_name}` | Optional label for the code — empty unless you fill the code table (see [Localization](#localization)) |
### Error ### Error
| Variable | Description | | Variable | Description |
| --- | --- | | --- | --- |
| `{vacuum_name}` | Friendly name of the vacuum | | `{vacuum_name}` | Friendly name of the vacuum |
| `{error}` | Error description (already human-readable English from the integration) | | `{error}` | Error description |
| `{code}` | Numeric error code | | `{code}` | Error code |
| `{code_name}` | Optional label for the code — empty unless you fill the code table (see [Localization](#localization)) |
### Information ### Information
@@ -107,111 +85,9 @@ Non-numeric suffixes (e.g. `_pro`) are rejected, so `vacuum.dreame_x10` will not
| `{vacuum_name}` | Friendly name of the vacuum | | `{vacuum_name}` | Friendly name of the vacuum |
| `{information}` | Information message (e.g., Dust Collection, Cleaning Paused) | | `{information}` | Information message (e.g., Dust Collection, Cleaning Paused) |
> The default warning/error templates show `(code: {code})`. When an event has no code, the blueprint automatically strips the empty `(code: )` parenthetical.
## Friendly Labels
`{cleaning_mode}` and `{status}` arrive from the integration as raw `UPPER_SNAKE_CASE` enum names; the blueprint translates them to readable text. Any value not in the tables below falls back to a generic humanizer (underscores → spaces, capitalized). All labels can be overridden — see [Localization](#localization).
### Cleaning Mode (`{cleaning_mode}`)
| Raw enum | Friendly label |
| --- | --- |
| `UNKNOWN` | Unknown |
| `SWEEPING` | Sweeping |
| `MOPPING` | Mopping |
| `SWEEPING_AND_MOPPING` | Sweeping & Mopping |
### Status (`{status}`)
| Raw enum | Friendly label |
| --- | --- |
| `IDLE` | Idle |
| `PAUSED` | Paused |
| `CLEANING` | Cleaning |
| `BACK_HOME` | Returning to dock |
| `PART_CLEANING` | Spot cleaning |
| `FOLLOW_WALL` | Following wall |
| `CHARGING` | Charging |
| `OTA` | Updating firmware |
| `WIFI_SET` | Wi-Fi setup |
| `POWER_OFF` | Powered off |
| `ERROR` | Error |
| `REMOTE_CONTROL` | Remote control |
| `SLEEPING` | Sleeping |
| `STANDBY` | Standby |
| `SEGMENT_CLEANING` | Room cleaning |
| `ZONE_CLEANING` | Zone cleaning |
| `SPOT_CLEANING` | Spot cleaning |
| `FAST_MAPPING` | Mapping |
| `MONITOR_CRUISE` | Patrolling |
| `MONITOR_SPOT` | Spot monitoring |
| `SUMMON_CLEAN` | Summon clean |
(Also mapped: `FCT`, `FACTORY`, `SELF_TEST`, `FACTORY_FUNCION_TEST` — diagnostic states.)
### Consumable (`{consumable}`) and Information (`{information}`)
| Raw id | Friendly label |
| --- | --- |
| `main_brush` | Main Brush |
| `side_brush` | Side Brush |
| `filter` | Filter |
| `sensor` | Sensor |
| `mop_pad` | Mop Pad |
| `silver_ion` | Silver Ion |
| `detergent` | Detergent |
| `dust_collection` | Dust Collection |
| `cleaning_paused` | Cleaning Paused |
## Localization
Two optional inputs (under **Localization**) let you relabel or translate any value without editing the blueprint. Both are entered as YAML key/value pairs (the object selector).
**Label Overrides** — keys are the raw values the integration emits (cleaning-mode/status are `UPPER_SNAKE` enum names; consumable/information are lower-case ids). Applied to `{cleaning_mode}`, `{status}`, `{consumable}`, and `{information}`:
```yaml
SWEEPING_AND_MOPPING: Подметание и мытьё
CLEANING: Уборка
BACK_HOME: Возврат на базу
dust_collection: Очистка контейнера
```
**Warning/Error Code Labels** — keys are numeric codes (unquoted); the value is exposed as `{code_name}`. Leave empty to omit `{code_name}`:
```yaml
68: Снять швабру
47: Робот застрял
```
Lookup order for every value is: your override → built-in friendly label → generic humanizer.
A ready-to-paste **Russian** starter table (all statuses, modes, consumables, information types, and warning/error codes) is provided in [localization.ru.yaml](localization.ru.yaml) — copy each section into the matching Localization input. To show fully Russian warning/error text, reference `{code_name}` in your warning/error templates instead of the English `{warning}`/`{error}`.
> No built-in numeric-code → name table is shipped, because code meanings can differ between integration versions. The `{warning}` / `{error}` text already comes from your installed integration (so it is always correct); use `{code_name}` only if you want a short, localized tag for specific codes.
### Warning vs. Error Codes
The integration classifies a fault as a dismissible **warning** only for the codes below; every other fault with a positive code (except battery-low) is reported as an **error** and carries a numeric `{code}` you can paste into **Error Codes to Ignore**.
| Code | Name |
| --- | --- |
| 47 | Blocked |
| 68 | Remove mop |
| 70 | Mop removed |
| 71 | Mop pad stopped rotating |
| 72 | Mop pad stopped rotating |
| 107 | Water tank dry |
| 114 | Clean mop pad |
## Notes
- The default messages contain emojis. Most modern notify integrations (Mobile App, Telegram, Discord) render them correctly; legacy SMS-based integrations may not.
- `notify.send_message` requires Home Assistant 2024.7 or newer. It accepts only a `message` (no title/tag); put any emphasis directly in the message text.
## Debug Mode ## Debug Mode
Enable **Debug Notifications** in the Debug section to send a persistent notification for every trigger. Each debug notification includes the blueprint version, dispatched event kind, and the enable decision — useful for confirming filters are doing what you expect. The notification is keyed per vacuum so debug entries from multiple vacuums do not overwrite each other. Enable **Debug Notifications** in the Debug section to send persistent notifications with raw event data for troubleshooting.
## Author ## Author
+146 -294
View File
@@ -96,43 +96,6 @@ blueprint:
selector: selector:
boolean: boolean:
# -------------------------------------------------------------------------
# Filtering
# -------------------------------------------------------------------------
filters_group:
name: "Filtering"
collapsed: true
input:
warning_codes_ignore:
name: Warning Codes to Ignore
description: >
List of warning codes to silence (one entry per code, as text).
Useful for suppressing routine warnings while keeping critical ones.
default: []
selector:
text:
multiple: true
error_codes_ignore:
name: Error Codes to Ignore
description: >
List of error codes to silence (one entry per code, as text).
default: []
selector:
text:
multiple: true
information_codes_ignore:
name: Information Types to Ignore
description: >
List of information message types to silence (one entry per type).
Information events carry a type id, not a numeric code.
Valid values: dust_collection, cleaning_paused.
default: []
selector:
text:
multiple: true
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Message Templates # Message Templates
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -175,7 +138,7 @@ blueprint:
name: "Warning Message" name: "Warning Message"
description: > description: >
Message sent for device warnings. Message sent for device warnings.
Variables: `{vacuum_name}`, `{warning}`, `{code}`, `{code_name}` Variables: `{vacuum_name}`, `{warning}`, `{code}`
default: "⚠️ {vacuum_name} warning: {warning} (code: {code})." default: "⚠️ {vacuum_name} warning: {warning} (code: {code})."
selector: selector:
text: text:
@@ -185,7 +148,7 @@ blueprint:
name: "Error Message" name: "Error Message"
description: > description: >
Message sent for device errors. Message sent for device errors.
Variables: `{vacuum_name}`, `{error}`, `{code}`, `{code_name}` Variables: `{vacuum_name}`, `{error}`, `{code}`
default: "❌ {vacuum_name} error: {error} (code: {code})." default: "❌ {vacuum_name} error: {error} (code: {code})."
selector: selector:
text: text:
@@ -201,42 +164,6 @@ blueprint:
text: text:
multiline: true multiline: true
# -------------------------------------------------------------------------
# Localization / Label Overrides
# -------------------------------------------------------------------------
localization_group:
name: "Localization"
collapsed: true
input:
label_overrides:
name: "Label Overrides"
description: >
Optional translation/relabel table for dynamic values, as YAML
key/value pairs. Keys are the raw values the integration emits
(cleaning mode / status are UPPER_SNAKE enum names; consumable /
information are lower-case ids); values are your preferred text in
any language. Applied to {cleaning_mode}, {status}, {consumable}
and {information}. Example:
SWEEPING_AND_MOPPING: Подметание и мытьё
CLEANING: Уборка
BACK_HOME: Возврат на базу
dust_collection: Очистка контейнера
default: {}
selector:
object:
code_label_overrides:
name: "Warning/Error Code Labels"
description: >
Optional table mapping a numeric warning/error code to a short
label, exposed as the {code_name} placeholder. Leave empty to omit
{code_name}. Use unquoted numbers as keys. Example:
68: Снять швабру
47: Робот застрял
default: {}
selector:
object:
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Debug # Debug
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -248,15 +175,14 @@ blueprint:
name: Enable Debug Notifications name: Enable Debug Notifications
description: > description: >
Send persistent notifications for debugging automation behavior. Send persistent notifications for debugging automation behavior.
Shows raw event data, dispatched event kind, and the enable decision. Shows raw event data and filtering decisions.
default: false default: false
selector: selector:
boolean: boolean:
# Queued mode to avoid dropping rapid events. A single cleanup-completion pass # Queued mode to avoid dropping rapid events
# can fire up to 8 events (1 task_status + 7 consumables), so queue up to 10.
mode: queued mode: queued
max: 10 max: 5
# ============================================================================= # =============================================================================
# TRIGGERS # TRIGGERS
@@ -291,14 +217,11 @@ trigger:
# VARIABLES # VARIABLES
# ============================================================================= # =============================================================================
variables: variables:
# Bumped whenever event-handling logic changes; surfaced in debug output
# so users can confirm which revision is running.
blueprint_version: "1.2.1"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Input References # Input References
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
vacuum_entity: !input vacuum_entity vacuum_entity: !input vacuum_entity
notify_targets: !input notify_targets
enable_debug_notifications: !input enable_debug_notifications enable_debug_notifications: !input enable_debug_notifications
# Event toggles # Event toggles
@@ -309,11 +232,6 @@ variables:
enable_error: !input enable_error enable_error: !input enable_error
enable_information: !input enable_information enable_information: !input enable_information
# Filter lists (lists of strings)
warning_codes_ignore: !input warning_codes_ignore
error_codes_ignore: !input error_codes_ignore
information_codes_ignore: !input information_codes_ignore
# Message templates # Message templates
message_cleaning_started_template: !input message_cleaning_started message_cleaning_started_template: !input message_cleaning_started
message_cleaning_completed_template: !input message_cleaning_completed message_cleaning_completed_template: !input message_cleaning_completed
@@ -322,214 +240,49 @@ variables:
message_error_template: !input message_error message_error_template: !input message_error
message_information_template: !input message_information message_information_template: !input message_information
# Localization overrides (dicts; default to empty mappings)
label_overrides: !input label_overrides
code_label_overrides: !input code_label_overrides
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Vacuum Info # Vacuum Info
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Prefer the device name (clean, single) over the entity's friendly_name. vacuum_name: "{{ state_attr(vacuum_entity, 'friendly_name') | default(vacuum_entity) }}"
# The Dreame integration names the vacuum entity " <device name>" (leading
# space) which Home Assistant composes into the friendly_name, so
# friendly_name renders the device name twice (e.g. "Z10 Pro Z10 Pro").
# Use the device's user-set/original name; fall back to friendly_name, then
# the entity_id.
vacuum_name: >
{%- set did = device_id(vacuum_entity) -%}
{%- set dname = (device_attr(did, 'name_by_user') or device_attr(did, 'name')) if did else none -%}
{{ dname if dname else (state_attr(vacuum_entity, 'friendly_name') | default(vacuum_entity, true)) }}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Event Data (flat structure — fields are directly on trigger.event.data) # Event Data (flat structure — fields are directly on trigger.event.data)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
event_entity_id: "{{ trigger.event.data.entity_id | default('') }}" event_entity_id: "{{ trigger.event.data.entity_id | default('') }}"
# ---------------------------------------------------------------------------
# Friendly label maps
# ---------------------------------------------------------------------------
# The integration emits cleaning_mode and status as raw UPPER_SNAKE enum
# names (e.g. SWEEPING_AND_MOPPING, SEGMENT_CLEANING). These maps translate
# them to readable text. Lookup order at the call site is:
# user label_overrides -> built-in map below -> generic humanizer.
cleaning_mode_labels:
UNKNOWN: "Unknown"
SWEEPING: "Sweeping"
MOPPING: "Mopping"
SWEEPING_AND_MOPPING: "Sweeping & Mopping"
status_labels:
UNKNOWN: "Unknown"
IDLE: "Idle"
PAUSED: "Paused"
CLEANING: "Cleaning"
BACK_HOME: "Returning to dock"
PART_CLEANING: "Spot cleaning"
FOLLOW_WALL: "Following wall"
CHARGING: "Charging"
OTA: "Updating firmware"
FCT: "Factory check"
WIFI_SET: "Wi-Fi setup"
POWER_OFF: "Powered off"
FACTORY: "Factory mode"
ERROR: "Error"
REMOTE_CONTROL: "Remote control"
SLEEPING: "Sleeping"
SELF_TEST: "Self test"
FACTORY_FUNCION_TEST: "Factory function test"
STANDBY: "Standby"
SEGMENT_CLEANING: "Room cleaning"
ZONE_CLEANING: "Zone cleaning"
SPOT_CLEANING: "Spot cleaning"
FAST_MAPPING: "Mapping"
MONITOR_CRUISE: "Patrolling"
MONITOR_SPOT: "Spot monitoring"
SUMMON_CLEAN: "Summon clean"
# ---------------------------------------------------------------------------
# Task status fields # Task status fields
# --------------------------------------------------------------------------- task_cleaning_mode: "{{ trigger.event.data.cleaning_mode | default('unknown') }}"
# Raw enum names as fired by the integration. task_status_value: "{{ trigger.event.data.status | default('unknown') }}"
task_cleaning_mode_raw: "{{ trigger.event.data.cleaning_mode | default('UNKNOWN') }}" task_completed: "{{ trigger.event.data.completed | default(false) }}"
task_status_raw: "{{ trigger.event.data.status | default('UNKNOWN') }}"
# Friendly values: override -> built-in map -> generic humanizer fallback.
task_cleaning_mode: "{{ (label_overrides or {}).get(task_cleaning_mode_raw, cleaning_mode_labels.get(task_cleaning_mode_raw, task_cleaning_mode_raw | replace('_', ' ') | title)) }}"
task_status_value: "{{ (label_overrides or {}).get(task_status_raw, status_labels.get(task_status_raw, task_status_raw | replace('_', ' ') | title)) }}"
# Coerce to a real bool so the started/completed dispatch stays correct.
task_completed: "{{ trigger.event.data.completed | default(false) | bool(false) }}"
task_cleaned_area: "{{ trigger.event.data.cleaned_area | default(0) }}" task_cleaned_area: "{{ trigger.event.data.cleaned_area | default(0) }}"
task_cleaning_time: "{{ trigger.event.data.cleaning_time | default(0) }}" task_cleaning_time: "{{ trigger.event.data.cleaning_time | default(0) }}"
# ---------------------------------------------------------------------------
# Consumable fields # Consumable fields
# --------------------------------------------------------------------------- consumable_name: "{{ (trigger.event.data.consumable | default('unknown')) | replace('_', ' ') | title }}"
consumable_name: "{{ (label_overrides or {}).get(trigger.event.data.consumable | default('unknown'), (trigger.event.data.consumable | default('unknown')) | replace('_', ' ') | title) }}"
# ---------------------------------------------------------------------------
# Warning fields # Warning fields
# --------------------------------------------------------------------------- warning_description: "{{ trigger.event.data.warning | default('unknown') }}"
# Most warnings carry a short English description; the temporary-map events
# instead carry a bare snake id or a markdown blob. Humanize a bare id (and
# honor overrides) while leaving descriptive text / markdown untouched.
warning_description: >
{%- set w = trigger.event.data.warning | default('unknown') -%}
{%- if w and ' ' not in w and '#' not in w -%}{{ (label_overrides or {}).get(w, w | replace('_', ' ') | title) }}{%- else -%}{{ w }}{%- endif -%}
warning_code: "{{ trigger.event.data.code | default('') }}" warning_code: "{{ trigger.event.data.code | default('') }}"
# ---------------------------------------------------------------------------
# Error fields # Error fields
# ---------------------------------------------------------------------------
error_description: "{{ trigger.event.data.error | default('unknown') }}" error_description: "{{ trigger.event.data.error | default('unknown') }}"
error_code: "{{ trigger.event.data.code | default('') }}" error_code: "{{ trigger.event.data.code | default('') }}"
# ---------------------------------------------------------------------------
# Code label (optional, localized)
# ---------------------------------------------------------------------------
# Numeric warning/error code as fired (empty for code-less events). code_name
# is the localized label for that code, exposed as {code_name}: a user override
# if present, otherwise the integration's own English description, otherwise
# empty. No built-in numeric code table is shipped, so there is no risk of
# stale labels across integration versions.
event_code: "{{ trigger.event.data.code | default('') }}"
code_name: >
{%- set lbl = (code_label_overrides or {}).get(event_code, (code_label_overrides or {}).get(event_code | string, '')) -%}
{%- if lbl -%}{{ lbl }}
{%- elif trigger.id == 'warning' -%}{{ warning_description }}
{%- elif trigger.id == 'error' -%}{{ error_description }}
{%- endif -%}
# ---------------------------------------------------------------------------
# Information fields # Information fields
# --------------------------------------------------------------------------- information_description: "{{ (trigger.event.data.information | default('unknown')) | replace('_', ' ') | title }}"
information_description: "{{ (label_overrides or {}).get(trigger.event.data.information | default('unknown'), (trigger.event.data.information | default('unknown')) | replace('_', ' ') | title) }}"
# Information events carry a type id (e.g. dust_collection), never a numeric
# code — this raw id drives the Information ignore filter.
information_code: "{{ trigger.event.data.information | default('') }}"
# ---------------------------------------------------------------------------
# Event Dispatch
# ---------------------------------------------------------------------------
# Map the raw trigger to a single logical event kind. Keeping this in one
# place avoids the duplicated condition logic that the old multi-branch
# `choose` had.
event_kind: >
{%- if trigger.id == 'task_status' and not task_completed -%}cleaning_started
{%- elif trigger.id == 'task_status' and task_completed -%}cleaning_completed
{%- elif trigger.id == 'consumable' -%}consumable
{%- elif trigger.id == 'warning' -%}warning
{%- elif trigger.id == 'error' -%}error
{%- elif trigger.id == 'information' -%}information
{%- else -%}none
{%- endif -%}
# Whether this event should produce a notification, given user toggles
# and any per-code filter lists. Coerce with `| bool(false)` at the
# consumer because folded scalars can render as the string "True"/"False".
# The warning branch also drops the integration's spurious "replace_temporary_map"
# clear event (fired with no code, including on every restart).
event_enabled: >
{%- if event_kind == 'cleaning_started' -%}{{ enable_cleaning_started }}
{%- elif event_kind == 'cleaning_completed' -%}{{ enable_cleaning_completed }}
{%- elif event_kind == 'consumable' -%}{{ enable_consumable }}
{%- elif event_kind == 'warning' -%}{{ enable_warning and (trigger.event.data.warning | default('')) != 'replace_temporary_map' and (warning_code | string) not in warning_codes_ignore }}
{%- elif event_kind == 'error' -%}{{ enable_error and (error_code | string) not in error_codes_ignore }}
{%- elif event_kind == 'information' -%}{{ enable_information and (information_code | string) not in information_codes_ignore }}
{%- else -%}False
{%- endif -%}
# Pick the per-event message template.
message_template: >
{%- if event_kind == 'cleaning_started' -%}{{ message_cleaning_started_template }}
{%- elif event_kind == 'cleaning_completed' -%}{{ message_cleaning_completed_template }}
{%- elif event_kind == 'consumable' -%}{{ message_consumable_template }}
{%- elif event_kind == 'warning' -%}{{ message_warning_template }}
{%- elif event_kind == 'error' -%}{{ message_error_template }}
{%- elif event_kind == 'information' -%}{{ message_information_template }}
{%- else -%}{%- endif -%}
# Render the message. Placeholders that don't apply to this event are
# absent from the chosen template, so `replace()` is a no-op for them.
# {vacuum_name} is substituted last (it is the only user-controlled value)
# so a friendly name containing a literal token cannot be re-expanded. The
# final regex strips an empty "(label: )" parenthetical in any language left
# by a code-less event, e.g. "(code: )" or "(код предупреждения: )".
message: >
{%- set code_for_event = warning_code if event_kind == 'warning'
else (error_code if event_kind == 'error' else '') -%}
{%- set rendered = message_template
| replace('{cleaning_mode}', task_cleaning_mode)
| replace('{status}', task_status_value)
| replace('{cleaned_area}', task_cleaned_area | string)
| replace('{cleaning_time}', task_cleaning_time | string)
| replace('{consumable}', consumable_name)
| replace('{warning}', warning_description)
| replace('{error}', error_description)
| replace('{information}', information_description)
| replace('{code_name}', code_name)
| replace('{code}', code_for_event | string)
| replace('{vacuum_name}', vacuum_name) -%}
{{ rendered | regex_replace('\\s*\\(\\s*[^():]*:\\s*\\)', '') }}
# ============================================================================= # =============================================================================
# CONDITIONS # CONDITIONS
# ============================================================================= # =============================================================================
condition: condition:
# Only process events from the configured vacuum. The integration fires every # Only process events from the configured vacuum entity.
# event with an `entity_id` derived from the device NAME via generate_entity_id() # The Dreame Vacuum integration uses generate_entity_id() for the entity_id
# events never carry a `device_id`, so matching is entity_id based. # in event data, which may append a numeric suffix (e.g., _2) since the
# generate_entity_id() may append a purely numeric suffix (e.g. `_2`) when the # actual vacuum entity already occupies the base entity_id.
# base id is already taken, so we accept the configured entity_id exactly, or
# followed by a purely-numeric suffix, and reject non-numeric suffixes (e.g. `_pro`).
- condition: template - condition: template
value_template: > value_template: >
{%- if event_entity_id == '' -%} {{ event_entity_id == vacuum_entity
false or event_entity_id.startswith(vacuum_entity ~ '_') }}
{%- elif event_entity_id == vacuum_entity -%}
true
{%- elif event_entity_id.startswith(vacuum_entity ~ '_') -%}
{{ event_entity_id[(vacuum_entity | length) + 1:].isdigit() }}
{%- else -%}
false
{%- endif -%}
# ============================================================================= # =============================================================================
# ACTIONS # ACTIONS
@@ -539,34 +292,133 @@ action:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Debug Logging # Debug Logging
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
- if: - choose:
- condition: template - conditions:
value_template: "{{ enable_debug_notifications }}" - condition: template
then: value_template: "{{ enable_debug_notifications }}"
- action: persistent_notification.create sequence:
data: - service: persistent_notification.create
notification_id: "dreame_vacuum_debug_{{ vacuum_entity }}" data:
title: "Dreame Vacuum Debug — {{ vacuum_name }}" title: "Dreame Vacuum Debug"
message: > message: >
**Blueprint version:** {{ blueprint_version }} **Trigger:** {{ trigger.id }}
**Trigger:** {{ trigger.id }} **Entity:** {{ event_entity_id }}
**Event kind:** {{ event_kind }} **Vacuum:** {{ vacuum_name }}
**Enabled:** {{ event_enabled }} **Event Data:** {{ trigger.event.data }}
**Entity:** {{ event_entity_id }}
**Vacuum:** {{ vacuum_name }}
**Event Data:** {{ trigger.event.data }}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Send Notification # Send Notification Based on Event Type
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Single dispatch — message and the enable decision are computed in the - choose:
# variables block above. We just gate on the resolved flags here.
- if: # CASE 1: Cleaning Started
- condition: template - conditions:
value_template: "{{ event_kind != 'none' and (event_enabled | bool(false)) }}" - condition: template
then: value_template: >
- action: notify.send_message {{ trigger.id == 'task_status'
target: and not task_completed
entity_id: !input notify_targets and enable_cleaning_started }}
data: sequence:
message: "{{ message }}" - variables:
message: >
{% set tpl = message_cleaning_started_template %}
{{ tpl | replace('{vacuum_name}', vacuum_name)
| replace('{cleaning_mode}', task_cleaning_mode)
| replace('{status}', task_status_value) }}
- service: notify.send_message
target:
entity_id: "{{ notify_targets }}"
data:
message: "{{ message }}"
# CASE 2: Cleaning Completed
- conditions:
- condition: template
value_template: >
{{ trigger.id == 'task_status'
and task_completed
and enable_cleaning_completed }}
sequence:
- variables:
message: >
{% set tpl = message_cleaning_completed_template %}
{{ tpl | replace('{vacuum_name}', vacuum_name)
| replace('{cleaning_mode}', task_cleaning_mode)
| replace('{status}', task_status_value)
| replace('{cleaned_area}', task_cleaned_area | string)
| replace('{cleaning_time}', task_cleaning_time | string) }}
- service: notify.send_message
target:
entity_id: "{{ notify_targets }}"
data:
message: "{{ message }}"
# CASE 3: Consumable Depleted
- conditions:
- condition: template
value_template: >
{{ trigger.id == 'consumable' and enable_consumable }}
sequence:
- variables:
message: >
{% set tpl = message_consumable_template %}
{{ tpl | replace('{vacuum_name}', vacuum_name)
| replace('{consumable}', consumable_name) }}
- service: notify.send_message
target:
entity_id: "{{ notify_targets }}"
data:
message: "{{ message }}"
# CASE 4: Warning
- conditions:
- condition: template
value_template: >
{{ trigger.id == 'warning' and enable_warning }}
sequence:
- variables:
message: >
{% set tpl = message_warning_template %}
{{ tpl | replace('{vacuum_name}', vacuum_name)
| replace('{warning}', warning_description)
| replace('{code}', warning_code | string) }}
- service: notify.send_message
target:
entity_id: "{{ notify_targets }}"
data:
message: "{{ message }}"
# CASE 5: Error
- conditions:
- condition: template
value_template: >
{{ trigger.id == 'error' and enable_error }}
sequence:
- variables:
message: >
{% set tpl = message_error_template %}
{{ tpl | replace('{vacuum_name}', vacuum_name)
| replace('{error}', error_description)
| replace('{code}', error_code | string) }}
- service: notify.send_message
target:
entity_id: "{{ notify_targets }}"
data:
message: "{{ message }}"
# CASE 6: Information
- conditions:
- condition: template
value_template: >
{{ trigger.id == 'information' and enable_information }}
sequence:
- variables:
message: >
{% set tpl = message_information_template %}
{{ tpl | replace('{vacuum_name}', vacuum_name)
| replace('{information}', information_description) }}
- service: notify.send_message
target:
entity_id: "{{ notify_targets }}"
data:
message: "{{ message }}"
-152
View File
@@ -1,152 +0,0 @@
# Russian (русский) starter localization for the Dreame Vacuum Notifications blueprint.
#
# HOW TO USE
# When configuring the automation from the blueprint, open the "Localization"
# section and paste:
# * the mapping under `label_overrides:` -> into the "Label Overrides" input
# * the mapping under `code_label_overrides:` -> into the "Warning/Error Code Labels" input
# (Paste the key/value lines themselves; the object selector takes the mapping.)
#
# {cleaning_mode} / {status} / {consumable} / {information} use label_overrides.
# {code_name} uses code_label_overrides. To show fully Russian warning/error
# text, use {code_name} in your warning/error templates instead of the
# integration-supplied English {warning}/{error}, e.g.:
# message_warning: "⚠️ {vacuum_name}: {code_name} (код {code})."
#
# Tweak any wording to taste. Anything you leave out falls back to the built-in
# English label, then to a generic humanizer.
label_overrides:
# --- Cleaning mode ({cleaning_mode}) ---
UNKNOWN: Неизвестно
SWEEPING: Подметание
MOPPING: Мытьё
SWEEPING_AND_MOPPING: Подметание и мытьё
# --- Status ({status}) ---
IDLE: Ожидание
PAUSED: Пауза
CLEANING: Уборка
BACK_HOME: Возврат на базу
PART_CLEANING: Точечная уборка
FOLLOW_WALL: Уборка вдоль стен
CHARGING: Зарядка
OTA: Обновление прошивки
FCT: Заводская проверка
WIFI_SET: Настройка Wi-Fi
POWER_OFF: Выключен
FACTORY: Заводской режим
ERROR: Ошибка
REMOTE_CONTROL: Ручное управление
SLEEPING: Сон
SELF_TEST: Самодиагностика
FACTORY_FUNCION_TEST: Заводской тест функций
STANDBY: Режим ожидания
SEGMENT_CLEANING: Уборка комнат
ZONE_CLEANING: Уборка зон
SPOT_CLEANING: Точечная уборка
FAST_MAPPING: Построение карты
MONITOR_CRUISE: Патрулирование
MONITOR_SPOT: Видеонаблюдение
SUMMON_CLEAN: Уборка по вызову
# --- Consumables ({consumable}) ---
main_brush: Основная щётка
side_brush: Боковая щётка
filter: Фильтр
secondary_filter: Вторичный фильтр
sensor: Датчики
mop_pad: Насадка для мытья
silver_ion: Серебряный ионизатор
detergent: Моющее средство
# --- Information ({information}) ---
dust_collection: Очистка пылесборника
cleaning_paused: Уборка приостановлена
code_label_overrides:
# Numeric warning/error code -> short Russian label ({code_name}).
# Warnings: 47, 68, 70, 71, 72, 107, 114. Everything else is an error.
1: Колёса вывешены
2: Ошибка датчика обрыва
3: Заклинило датчик столкновения
4: Робот наклонён
5: Заклинило датчик столкновения
6: Колёса вывешены
7: Ошибка оптического датчика
8: Не установлен пылесборник
9: Не установлен бак для воды
10: Бак для воды пуст
11: Фильтр влажный или забит
12: Намотка на основную щётку
13: Намотка на боковую щётку
14: Фильтр влажный или забит
15: Заблокировано левое колесо
16: Заблокировано правое колесо
17: Робот застрял (не может повернуть)
18: Робот застрял (не может ехать)
19: Не найдена база
21: Ошибка зарядки
23: Внутренняя ошибка
24: Ошибка датчика навигации
25: Ошибка датчика перемещения
26: Ошибка оптического датчика
27: Помеха ИК-датчику
28: На базу не подаётся питание
29: Ошибка температуры батареи
30: Ошибка датчика вентилятора
31: Заблокировано левое колесо
32: Заблокировано правое колесо
33: Ошибка акселерометра
34: Ошибка гироскопа
35: Ошибка гироскопа
36: Ошибка левого магнитного датчика
37: Ошибка правого магнитного датчика
38: Ошибка датчика потока
39: Ошибка ИК-датчика
40: Ошибка камеры
41: Сильное магнитное поле
42: Ошибка водяного насоса
43: Ошибка часов (RTC)
44: Внутренняя ошибка
45: Внутренняя ошибка
46: Внутренняя ошибка
47: Маршрут заблокирован
48: Ошибка лазерного дальномера
49: Заклинило бампер лидара
50: Ошибка водяного насоса
51: Фильтр влажный или забит
54: Ошибка краевого датчика
55: Под роботом обнаружен ковёр
56: Ошибка датчика обхода препятствий
57: Ошибка краевого датчика
58: Ошибка ультразвукового датчика
59: Запретная зона или виртуальная стена
61: Не удаётся достичь зоны
62: Не удаётся достичь зоны
63: Маршрут заблокирован
64: Маршрут заблокирован
65: Робот в запретной зоне
66: Робот в запретной зоне
67: Робот в запретной зоне
68: Снимите и промойте швабру
69: Отсоединилась насадка для мытья
70: Отсоединилась насадка для мытья
71: Насадка для мытья не вращается
72: Насадка для мытья не вращается
101: Мешок для пыли заполнен или забит воздуховод
102: Крышка базы открыта или нет мешка
103: Крышка базы открыта или нет мешка
104: Мешок для пыли заполнен или забит воздуховод
105: Не установлен бак чистой воды
106: Бак грязной воды полон или не установлен
107: Мало чистой воды
108: Бак грязной воды полон или не установлен
109: Засор бака грязной воды
110: Ошибка насоса грязной воды
111: Неправильно установлен лоток мойки
112: Очистите лоток мойки
114: Очистите лоток мойки швабры
116: Долейте чистую воду
118: Бак грязной воды переполнен
119: Высокий уровень воды в лотке мойки
+1
View File
@@ -146,6 +146,7 @@ Select input_text entities containing Telegram chat IDs. Can be user IDs (positi
- Large media lists are automatically split into multiple groups (2-10 items per group) - Large media lists are automatically split into multiple groups (2-10 items per group)
- Optional chat action indicator (typing, uploading photo/video) while processing - Optional chat action indicator (typing, uploading photo/video) while processing
- Optional maximum asset size filter to skip large files - Optional maximum asset size filter to skip large files
- Respects integration quiet hours — notifications are queued and sent when quiet hours end (configurable bypass per blueprint instance)
### Limitations ### Limitations
+260
View File
@@ -461,6 +461,23 @@ blueprint:
- label: "Uploading Document..." - label: "Uploading Document..."
value: "upload_document" value: "upload_document"
telegram_quiet_hours_start:
name: Quiet Hours Start
description: >
Start time for quiet hours. During quiet hours, Telegram notifications
are queued and sent when quiet hours end.
default: "23:00"
selector:
time:
telegram_quiet_hours_end:
name: Quiet Hours End
description: >
End time for quiet hours. Queued notifications are sent after this time.
default: "07:00"
selector:
time:
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Periodic Summary # Periodic Summary
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -937,6 +954,8 @@ variables:
telegram_disable_url_preview: !input telegram_disable_url_preview telegram_disable_url_preview: !input telegram_disable_url_preview
telegram_chat_action: !input telegram_chat_action telegram_chat_action: !input telegram_chat_action
telegram_max_asset_size: !input telegram_max_asset_size telegram_max_asset_size: !input telegram_max_asset_size
telegram_quiet_hours_start: !input telegram_quiet_hours_start
telegram_quiet_hours_end: !input telegram_quiet_hours_end
# Periodic Summary Settings # Periodic Summary Settings
enable_periodic_summary: !input enable_periodic_summary enable_periodic_summary: !input enable_periodic_summary
@@ -1437,6 +1456,25 @@ action:
assets: "{{ [{'url': periodic_summary_image_url, 'type': 'photo'}] if periodic_summary_image_url | length > 0 else [] }}" assets: "{{ [{'url': periodic_summary_image_url, 'type': 'photo'}] if periodic_summary_image_url | length > 0 else [] }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Periodic Summary - Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ periodic_summary_formatted[:200] }}...
- Has Image: {{ 'Yes' if periodic_summary_image_url | length > 0 else 'No' }}
- Response: {{ telegram_periodic_response }}
# Delay between periodic summary and scheduled assets if both trigger at the same hour # Delay between periodic summary and scheduled assets if both trigger at the same hour
- if: - if:
@@ -1681,6 +1719,24 @@ action:
caption: "{{ scheduled_message }}" caption: "{{ scheduled_message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Scheduled Per-Album - Text Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ scheduled_message[:200] }}...
- Response: {{ telegram_scheduled_text_response }}
# Extract message ID for reply # Extract message ID for reply
- variables: - variables:
@@ -1710,6 +1766,25 @@ action:
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}" max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
wait_for_response: false wait_for_response: false
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log media send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Scheduled Per-Album - Media Send:**
- Chat ID: {{ repeat.item }}
- Assets: {{ scheduled_media_urls | length }}
- Reply To: {{ scheduled_reply_to_id }}
- Response: {{ telegram_scheduled_media_response }}
# Combined Mode: Fetch from all albums and combine into one notification # Combined Mode: Fetch from all albums and combine into one notification
# Distributes the limit evenly across albums (e.g., limit=10 with 2 albums = 5 each) # Distributes the limit evenly across albums (e.g., limit=10 with 2 albums = 5 each)
@@ -1941,6 +2016,24 @@ action:
caption: "{{ combined_message }}" caption: "{{ combined_message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Scheduled Combined - Text Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ combined_message[:200] }}...
- Response: {{ telegram_combined_text_response }}
- variables: - variables:
combined_reply_to_id: "{{ telegram_combined_text_response[album_id_entities[0]].message_id | default(0) | int }}" combined_reply_to_id: "{{ telegram_combined_text_response[album_id_entities[0]].message_id | default(0) | int }}"
@@ -1968,6 +2061,25 @@ action:
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}" max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
wait_for_response: false wait_for_response: false
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log media send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Scheduled Combined - Media Send:**
- Chat ID: {{ repeat.item }}
- Assets: {{ combined_media_urls | length }}
- Reply To: {{ combined_reply_to_id }}
- Response: {{ telegram_combined_media_response }}
# Delay before memory mode if another scheduled notification was sent at the same hour # Delay before memory mode if another scheduled notification was sent at the same hour
- if: - if:
@@ -2207,6 +2319,24 @@ action:
caption: "{{ memory_message }}" caption: "{{ memory_message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Memory Per-Album - Text Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ memory_message[:200] }}...
- Response: {{ telegram_memory_text_response }}
# Extract message ID for reply # Extract message ID for reply
- variables: - variables:
@@ -2236,6 +2366,25 @@ action:
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}" max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
wait_for_response: false wait_for_response: false
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log media send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Memory Per-Album - Media Send:**
- Chat ID: {{ repeat.item }}
- Assets: {{ memory_media_urls | length }}
- Reply To: {{ memory_reply_to_id }}
- Response: {{ telegram_memory_media_response }}
# Combined Mode: Fetch from all albums and combine into one notification # Combined Mode: Fetch from all albums and combine into one notification
- conditions: - conditions:
@@ -2457,6 +2606,24 @@ action:
caption: "{{ memory_comb_message }}" caption: "{{ memory_comb_message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Memory Combined - Text Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ memory_comb_message[:200] }}...
- Response: {{ telegram_memory_comb_text_response }}
- variables: - variables:
memory_comb_reply_to_id: "{{ telegram_memory_comb_text_response[album_id_entities[0]].message_id | default(0) | int }}" memory_comb_reply_to_id: "{{ telegram_memory_comb_text_response[album_id_entities[0]].message_id | default(0) | int }}"
@@ -2484,6 +2651,25 @@ action:
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}" max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
wait_for_response: false wait_for_response: false
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log media send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Memory Combined - Media Send:**
- Chat ID: {{ repeat.item }}
- Assets: {{ memory_comb_media_urls | length }}
- Reply To: {{ memory_comb_reply_to_id }}
- Response: {{ telegram_memory_comb_media_response }}
# Stop here if this was a scheduled trigger - don't continue to event-based actions # Stop here if this was a scheduled trigger - don't continue to event-based actions
- choose: - choose:
@@ -2686,6 +2872,24 @@ action:
caption: "{{ message }}" caption: "{{ message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Album Renamed - Text Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ message[:200] }}...
- Response: {{ telegram_renamed_response }}
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# CASE 5: Album Deleted # CASE 5: Album Deleted
@@ -2723,6 +2927,24 @@ action:
caption: "{{ message }}" caption: "{{ message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Album Deleted - Text Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ message[:200] }}...
- Response: {{ telegram_deleted_response }}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Send Media to Telegram (if enabled) # Send Media to Telegram (if enabled)
@@ -2849,6 +3071,24 @@ action:
caption: "{{ telegram_message }}" caption: "{{ telegram_message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Assets Added - Text Send:**
- Chat ID: {{ current_chat_id }}
- Caption: {{ telegram_message[:200] }}...
- Response: {{ telegram_text_response }}
# Extract message ID for replies # Extract message ID for replies
- variables: - variables:
@@ -2891,3 +3131,23 @@ action:
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}" max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
wait_for_response: false wait_for_response: false
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log media send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Assets Added - Media Send:**
- Chat ID: {{ current_chat_id }}
- Assets: {{ media_urls | length }}
- Reply To: {{ reply_to_message_id }}
- Max Group Size: {{ max_media_per_group }}
- Response: {{ telegram_media_response }}
+12 -22
View File
@@ -7,10 +7,10 @@ This blueprint creates a smart motion-activated light control system. It handles
- Multiple motion sensor support (triggers on ANY sensor) - Multiple motion sensor support (triggers on ANY sensor)
- Condition switches (ALL must be ON for automation to work) - Condition switches (ALL must be ON for automation to work)
- Multiple lights and/or switches control - Multiple lights and/or switches control
- Light groups and area-based targeting (native area picker) - Light groups and area-based targeting
- Configurable timeout delay before turning off - Configurable timeout delay before turning off
- Minimum on duration (prevents rapid on/off cycling) - Minimum on duration (prevents rapid on/off cycling)
- Motion sensor on/off debounce (filter false triggers and PIR drop-outs) - Motion sensor debounce (filter false triggers)
- Smooth light transitions with configurable duration - Smooth light transitions with configurable duration
- Luminance sensor support (only trigger in dark conditions) - Luminance sensor support (only trigger in dark conditions)
- Time-based conditions (only active during specified hours) - Time-based conditions (only active during specified hours)
@@ -36,29 +36,19 @@ The automation tracks these states via persistent storage:
## Behavior Notes ## Behavior Notes
- Will NOT turn on light if it's already ON (prevents hijacking user control). - Will NOT turn on light if it's already ON (prevents hijacking user control)
- If user **changes** the light while automation is active (e.g., dim, color, scene), enters MANUAL mode (after grace period). - If user changes light while automation is active, enters MANUAL mode (after grace period)
- If the user **turns the light ON** while the automation is idle (pre-emptive manual control), it enters MANUAL mode and fires the `manual_action` callback. The `disable_action` callback is **not** run in this case (nothing was enabled to disable). - Grace period prevents false manual overrides from delayed device state reports (e.g., Zigbee)
- If the light is **turned off** by any external source while motion is still active, the automation re-enables it (external turn-off recovery). To intentionally exit the automation, also turn off the corresponding **condition switch** — this routes through the disable path and respects the configured timeouts. - MANUAL mode exits when light is turned OFF (by any means)
- Grace period prevents false manual overrides from delayed device state reports (e.g., Zigbee) — including stray reports that can follow a turn-off. - Timeout delay only applies when turning OFF (motion cleared)
- MANUAL mode exits when light is turned OFF (by any means). - Time conditions support overnight windows (e.g., 22:00 to 06:00)
- Timeout delay only applies when turning OFF (motion cleared). - Day/Night mode uses separate time window from time conditions
- Time conditions support overnight windows (e.g., 22:00 to 06:00).
- Day/Night mode uses separate time window from time conditions.
### Brightness threshold interaction
`brightness_threshold` is checked only when the automation decides whether to **enable**. A light that is on but dimmer than the threshold is treated as "available to take over" — so the automation will run with that light. When the disable path runs, it will turn the light **off** (it does not restore a below-threshold dim state).
### Callback ordering and `mode: restart`
Light state changes feed back into the automation, which uses `mode: restart` to handle rapid bursts. To make sure callbacks aren't silently dropped when the automation restarts mid-action, both `enable_action` and `disable_action` run **before** the corresponding turn-on/turn-off command.
## Requirements ## Requirements
- At least one motion sensor. - At least one motion sensor
- `input_text` entity for persistent state storage. Each automation instance must have its own entity. Increase the entity's `max_length` (default 100) to **at least 255** to leave room for the JSON state — short values can cause silent truncation. - `input_text` entity for persistent state storage
- Target light(s), switch(es), group, or area to control. - Target light(s), switch(es), group, or area to control
## Author ## Author
+101 -189
View File
@@ -92,15 +92,15 @@ blueprint:
target_area: target_area:
name: Target Area (optional) name: Target Area (optional)
description: > description: >
Pick an area. All lights and switches in the area will be Area ID (e.g., "living_room"). All lights and switches in the
discovered and controlled. area will be discovered and controlled.
default: default: ""
selector: selector:
area: {} text:
target_light_data: target_light_data:
name: Light Data Dictionary (optional) name: Light Data Dictionary (optional)
default: {} default: ""
description: > description: >
Provide a YAML dictionary of light.turn_on parameters. Provide a YAML dictionary of light.turn_on parameters.
If not specified, the light's last settings are preserved. If not specified, the light's last settings are preserved.
@@ -289,14 +289,14 @@ blueprint:
day_light_data: day_light_data:
name: Day Light Settings name: Day Light Settings
description: Light parameters during day mode (YAML dictionary) description: Light parameters during day mode (YAML dictionary)
default: {} default: ""
selector: selector:
object: {} object: {}
night_light_data: night_light_data:
name: Night Light Settings name: Night Light Settings
description: Light parameters during night mode (YAML dictionary) description: Light parameters during night mode (YAML dictionary)
default: {} default: ""
selector: selector:
object: {} object: {}
@@ -385,20 +385,6 @@ blueprint:
step: 1 step: 1
unit_of_measurement: "seconds" unit_of_measurement: "seconds"
motion_off_debounce:
name: Motion Off Debounce (seconds)
description: >
Motion sensor must report 'off' for this duration before the
disable path starts evaluating. Filters out brief sensor
drop-outs (common with PIR sensors). Set to 0 to disable.
default: 0
selector:
number:
min: 0
max: 30
step: 1
unit_of_measurement: "seconds"
manual_override_grace_period: manual_override_grace_period:
name: Manual Override Grace Period (seconds) name: Manual Override Grace Period (seconds)
description: > description: >
@@ -469,9 +455,7 @@ blueprint:
manual_action: manual_action:
name: Manual callback action (optional) name: Manual callback action (optional)
description: > description: >
Runs when the user takes manual control of the light: either by Runs when user manually changes the light while automation is active.
changing it while the automation is active, or by turning it ON
while the automation is idle (pre-emptive manual control).
Requires 'Automation state entity' to be configured. Requires 'Automation state entity' to be configured.
default: [] default: []
selector: selector:
@@ -495,12 +479,10 @@ trigger:
seconds: !input motion_debounce seconds: !input motion_debounce
id: "motion_sensor_on" id: "motion_sensor_on"
# Motion sensors OFF (with debounce to filter PIR drop-outs) # Motion sensors OFF
- platform: state - platform: state
entity_id: !input motion_sensors entity_id: !input motion_sensors
to: "off" to: "off"
for:
seconds: !input motion_off_debounce
id: "motion_sensor_off" id: "motion_sensor_off"
# Condition switches ON/OFF # Condition switches ON/OFF
@@ -517,22 +499,20 @@ trigger:
entity_id: !input target_switches entity_id: !input target_switches
id: "switch_state_changed" id: "switch_state_changed"
# Luminance dropped below threshold (re-evaluate, may now enable) # Luminance sensor value changed
# Template triggers fire on false→true transition, so this only fires when
# the sensor *crosses* the threshold downward — exactly when we care.
- platform: template - platform: template
value_template: > value_template: >
{% if luminance_sensor %} {% if luminance_sensor %}
{{ states(luminance_sensor) | float(99999) < (luminance_threshold | float(50)) }} {{ states(luminance_sensor) not in ['unknown','unavailable'] }}
{% else %} {% else %}
false false
{% endif %} {% endif %}
# Luminance enable switch turned ON (re-evaluate) # Luminance enable switch changed
- platform: template - platform: template
value_template: > value_template: >
{% if luminance_enable_switch %} {% if luminance_enable_switch %}
{{ is_state(luminance_enable_switch, 'on') }} {{ states(luminance_enable_switch) not in ['unknown','unavailable'] }}
{% else %} {% else %}
false false
{% endif %} {% endif %}
@@ -551,6 +531,7 @@ variables:
# State Machine Constants # State Machine Constants
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# These define the possible automation states stored in persistent storage # These define the possible automation states stored in persistent storage
automation_state_invalid: '-1' # Error state
automation_state_none: '0' # Idle, waiting for motion automation_state_none: '0' # Idle, waiting for motion
automation_state_enabled: '1' # Light is ON and controlled by automation automation_state_enabled: '1' # Light is ON and controlled by automation
automation_state_enabling: '2' # Turn-on command sent, awaiting confirmation automation_state_enabling: '2' # Turn-on command sent, awaiting confirmation
@@ -595,7 +576,7 @@ variables:
{% if target_light_group is not none %} {% if target_light_group is not none %}
{% set result = result + [target_light_group] %} {% set result = result + [target_light_group] %}
{% endif %} {% endif %}
{% if target_area is not none and target_area != '' %} {% if target_area != '' %}
{% set area_lights = area_entities(target_area) | select('match', '^light\\.') | list %} {% set area_lights = area_entities(target_area) | select('match', '^light\\.') | list %}
{% set result = result + area_lights %} {% set result = result + area_lights %}
{% endif %} {% endif %}
@@ -613,7 +594,7 @@ variables:
{% if target_switches | length > 0 %} {% if target_switches | length > 0 %}
{% set result = result + target_switches %} {% set result = result + target_switches %}
{% endif %} {% endif %}
{% if target_area is not none and target_area != '' %} {% if target_area != '' %}
{% set area_switches = area_entities(target_area) | select('match', '^switch\\.') | list %} {% set area_switches = area_entities(target_area) | select('match', '^switch\\.') | list %}
{% set result = result + area_switches %} {% set result = result + area_switches %}
{% endif %} {% endif %}
@@ -650,16 +631,13 @@ variables:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
automation_state_entity: !input automation_state_entity automation_state_entity: !input automation_state_entity
# Parse global state JSON from input_text entity. # Parse global state JSON from input_text entity
# Tolerant of empty/unknown/unavailable values and corrupt JSON
# (truncation can occur if input_text max_length is too short).
automation_state_global: > automation_state_global: >
{% set text = states(automation_state_entity) | string %} {% set text = states(automation_state_entity) | string %}
{% if text in ['unknown','unavailable','none',''] %} {% if text in ['unknown','unavailable','none',''] %}
{{ dict() }} {{ dict() }}
{% else %} {% else %}
{% set parsed = text | from_json(default=dict()) %} {{ text | from_json }}
{{ parsed if parsed is mapping else dict() }}
{% endif %} {% endif %}
automation_state_placeholder_key: !input automation_state_placeholder_key automation_state_placeholder_key: !input automation_state_placeholder_key
@@ -677,11 +655,20 @@ variables:
{% endif %} {% endif %}
# Get this automation's state from global state # Get this automation's state from global state
# BUG FIX: Changed from 'light_entity != ""' to proper none check
automation_state: "{{ automation_state_global.get(automation_state_key, dict()) if (light_entity is not none or switch_entity is not none) else dict() }}" automation_state: "{{ automation_state_global.get(automation_state_key, dict()) if (light_entity is not none or switch_entity is not none) else dict() }}"
# Current state machine state # Current state machine state
motion_light_state: "{{ automation_state.get(state_motion_light_state, automation_state_none) }}" motion_light_state: "{{ automation_state.get(state_motion_light_state, automation_state_none) }}"
# Track last action timestamp
motion_light_last_action_timestamp: >
{% if trigger_id == 'state_motion' %}
{{ date_time_now }}
{% else %}
{{ automation_state.get(state_motion_light_last_action_timestamp, none) }}
{% endif %}
# State machine state checks (for readability) # State machine state checks (for readability)
state_is_none: "{{ (motion_light_state | string) == automation_state_none }}" state_is_none: "{{ (motion_light_state | string) == automation_state_none }}"
state_is_enabled: "{{ (motion_light_state | string) == automation_state_enabled }}" state_is_enabled: "{{ (motion_light_state | string) == automation_state_enabled }}"
@@ -940,31 +927,28 @@ action:
- conditions: - conditions:
- condition: template - condition: template
value_template: > value_template: >
{# Check actual on/off state across ALL resolved devices — do NOT use {# Check actual on/off state only — do NOT use brightness_threshold here.
brightness_threshold here. Threshold is for the enable guard Brightness threshold is for the enable guard (any_device_on), not for
(any_device_on), not for detecting actual off. During transitions, detecting whether the light was actually turned off. During transitions,
brightness may temporarily dip below threshold while still on. #} brightness may temporarily be below threshold while the light is still on. #}
{% set has_devices = (resolved_all_lights | length > 0) or (resolved_all_switches | length > 0) %} {% set res = true %}
{% set lights_on = resolved_all_lights | select('is_state', 'on') | list | length > 0 %} {% if light_entity is not none %}
{% set switches_on = resolved_all_switches | select('is_state', 'on') | list | length > 0 %} {% set res = res and is_state(light_entity, 'off') %}
{{ has_devices and not lights_on and not switches_on }} {% endif %}
{% if switch_entity is not none %}
{% set res = res and is_state(switch_entity, 'off') %}
{% endif %}
{# Only true if we have at least one device and all are off #}
{{ res and (light_entity is not none or switch_entity is not none) }}
sequence: sequence:
# Reset state to NONE and stamp the action timestamp. # Reset state to NONE
# Every turn-off (automation OR user) passes through here, so
# stamping now starts the grace window that protects the
# pre-emptive manual-ON detection (NONE -> MANUAL) below from
# stray off->on device reports that can follow a turn-off
# (common with Zigbee bulbs + transitions).
- service: input_text.set_value - service: input_text.set_value
target: target:
entity_id: "{{ automation_state_entity }}" entity_id: "{{ automation_state_entity }}"
data: data:
value: > value: >
{% set new_automation_state = (automation_state | combine({ {% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_none })) %}
state_motion_light_state: automation_state_none,
state_motion_light_last_action_timestamp: date_time_now
})) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Re-evaluate: if an external source turned off the light while # Re-evaluate: if an external source turned off the light while
@@ -1002,8 +986,7 @@ action:
{% if text in ['unknown','unavailable','none',''] %} {% if text in ['unknown','unavailable','none',''] %}
{{ dict() }} {{ dict() }}
{% else %} {% else %}
{% set parsed = text | from_json(default=dict()) %} {{ text | from_json }}
{{ parsed if parsed is mapping else dict() }}
{% endif %} {% endif %}
re_eval_state: "{{ re_eval_state_global.get(automation_state_key, dict()) }}" re_eval_state: "{{ re_eval_state_global.get(automation_state_key, dict()) }}"
re_eval_motion_light_state: "{{ re_eval_state.get(state_motion_light_state, automation_state_none) }}" re_eval_motion_light_state: "{{ re_eval_state.get(state_motion_light_state, automation_state_none) }}"
@@ -1014,18 +997,7 @@ action:
# --- Re-enable path (mirrors CASE 2) --- # --- Re-enable path (mirrors CASE 2) ---
# Guard: scene mode without resolved scene # Set state to ENABLING before turning on
- choose:
- conditions:
- condition: template
value_template: "{{ use_scene_instead and effective_scene is none }}"
sequence:
- stop: "Scene mode enabled but no scene configured for current mode"
# Set state to ENABLING before turning on.
# last_brightness=0 is correct here: this path runs after the
# light was turned off externally, so there's no meaningful
# brightness to capture.
- service: input_text.set_value - service: input_text.set_value
target: target:
entity_id: "{{ automation_state_entity }}" entity_id: "{{ automation_state_entity }}"
@@ -1033,33 +1005,12 @@ action:
value: > value: >
{% set new_automation_state = (re_eval_state | combine({ {% set new_automation_state = (re_eval_state | combine({
state_motion_light_state: automation_state_enabling, state_motion_light_state: automation_state_enabling,
state_motion_light_last_action_timestamp: date_time_now, state_motion_light_last_action_timestamp: now(),
state_motion_light_last_brightness: 0 state_motion_light_last_brightness: 0
})) %} })) %}
{{ re_eval_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ re_eval_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Run enable callback BEFORE turning on (mode:restart safety) # Scene or light activation
- choose:
- conditions:
- condition: template
value_template: "{{ enable_action != [] }}"
sequence: !input enable_action
# Debug notification (also before turn-on for restart safety)
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Motion Light Debug"
message: >
Action: RE-ENABLE (external turn-off recovery)
Time: {{ now().strftime('%H:%M:%S') }}
Lights: {{ resolved_all_lights }}
Switches: {{ resolved_all_switches }}
Scene: {{ effective_scene if use_scene_instead else 'N/A' }}
# Scene or light activation (after callback/debug for restart safety)
- choose: - choose:
- conditions: - conditions:
- condition: template - condition: template
@@ -1098,6 +1049,27 @@ action:
target: target:
entity_id: "{{ resolved_all_switches }}" entity_id: "{{ resolved_all_switches }}"
# Execute enable callback
- choose:
- conditions:
- condition: template
value_template: "{{ enable_action != [] }}"
sequence: !input enable_action
# Debug notification
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Motion Light Debug"
message: >
Action: RE-ENABLE (external turn-off recovery)
Time: {{ now().strftime('%H:%M:%S') }}
Lights: {{ resolved_all_lights }}
Switches: {{ resolved_all_switches }}
Scene: {{ effective_scene if use_scene_instead else 'N/A' }}
# ----- Sub-case: Automation just turned on the light ----- # ----- Sub-case: Automation just turned on the light -----
# Transition from ENABLING to ENABLED, or disable immediately # Transition from ENABLING to ENABLED, or disable immediately
# if motion already cleared during the ENABLING phase # if motion already cleared during the ENABLING phase
@@ -1188,36 +1160,27 @@ action:
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# ----- Sub-case: User manually changed the light ----- # ----- Sub-case: User manually changed the light -----
# Transition to MANUAL (user took control). Fires when EITHER: # Transition from ENABLED to MANUAL (user took control)
# * state is ENABLED and the user changes the light, OR
# * state is NONE and the user turns the light ON while the
# automation is idle (pre-emptive manual control).
# Only triggers on meaningful state changes (on→off or off→on), # Only triggers on meaningful state changes (on→off or off→on),
# NOT on attribute-only updates (on→on) which Zigbee devices # NOT on attribute-only updates (on→on) which Zigbee devices
# commonly send as the light settles after a transition. # commonly send as the light settles after a transition.
# In NONE state, only a turn-ON counts as a takeover (a turn-OFF # Grace period: ignore state changes shortly after the automation
# is handled by the "Light turned OFF" sub-case above). # turns on the light to avoid false manual override detection.
# Grace period: ignore state changes shortly after the automation's
# last action to avoid false manual override detection.
- conditions: - conditions:
- condition: template - condition: template
value_template: > value_template: >
{% set meaningful_change = trigger.from_state.state != trigger.to_state.state %} {% set meaningful_change = trigger.from_state.state != trigger.to_state.state %}
{% if not meaningful_change %} {% if not meaningful_change %}
{{ false }} {{ false }}
{% elif state_is_none and trigger.to_state.state != 'on' %} {% else %}
{{ false }}
{% elif state_is_enabled or state_is_none %}
{% set last_ts = automation_state.get(state_motion_light_last_action_timestamp, none) %} {% set last_ts = automation_state.get(state_motion_light_last_action_timestamp, none) %}
{% set grace = (transition_duration | float(0)) + (manual_override_grace_period | float(2)) %} {% set grace = (transition_duration | float(0)) + (manual_override_grace_period | float(2)) %}
{% if last_ts is none %} {% if state_is_enabled and last_ts is not none %}
{{ true }}
{% else %}
{% set parsed = last_ts | as_datetime %} {% set parsed = last_ts | as_datetime %}
{{ parsed is none or (now() - parsed).total_seconds() > grace }} {{ parsed is none or (now() - parsed).total_seconds() > grace }}
{% else %}
{{ state_is_enabled }}
{% endif %} {% endif %}
{% else %}
{{ false }}
{% endif %} {% endif %}
sequence: sequence:
# BUG FIX: Fixed YAML structure - was 'data: >' instead of 'data:' with 'value: >' # BUG FIX: Fixed YAML structure - was 'data: >' instead of 'data:' with 'value: >'
@@ -1229,12 +1192,9 @@ action:
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_manual })) %} {% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_manual })) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Call disable action if configured. # Call disable action if configured
# Only when we were actively ENABLED — a pre-emptive manual ON
# (from NONE) never enabled anything, so there is nothing to
# "disable" and running it could touch unrelated devices.
- choose: - choose:
- conditions: "{{ state_is_enabled and manual_action_runs_disable_action and disable_action != [] }}" - conditions: "{{ manual_action_runs_disable_action and disable_action != [] }}"
sequence: !input disable_action sequence: !input disable_action
# Call manual action callback # Call manual action callback
@@ -1252,7 +1212,7 @@ action:
message: > message: >
Action: MANUAL OVERRIDE Action: MANUAL OVERRIDE
Time: {{ now().strftime('%H:%M:%S') }} Time: {{ now().strftime('%H:%M:%S') }}
Previous State: {{ 'ENABLED' if state_is_enabled else 'NONE (pre-emptive)' }} Previous State: ENABLED
New State: MANUAL New State: MANUAL
Trigger: {{ trigger_id }} Trigger: {{ trigger_id }}
@@ -1267,33 +1227,6 @@ action:
- condition: template - condition: template
value_template: "{{ (state_is_enabled or state_is_enabling) and must_be_disabled_preview }}" value_template: "{{ (state_is_enabled or state_is_enabling) and must_be_disabled_preview }}"
sequence: sequence:
# Honor min_on_duration even on the recovery path so a
# mistimed Zigbee report can't chop the on-time short.
# We skip timeout_delay here intentionally — this path
# is the recovery for a cancelled CASE 3, and the user
# already paid that delay before the restart.
- variables:
time_since_enabled: >
{% set last_ts = automation_state.get(state_motion_light_last_action_timestamp, none) %}
{% if last_ts is none %}
{{ 9999 }}
{% else %}
{% set parsed = last_ts | as_datetime %}
{% if parsed is none %}
{{ 9999 }}
{% else %}
{{ (now() - parsed).total_seconds() | int }}
{% endif %}
{% endif %}
remaining_min_on: "{{ [0, min_on_duration - time_since_enabled] | max }}"
- choose:
- conditions:
- condition: template
value_template: "{{ remaining_min_on > 0 }}"
sequence:
- delay:
seconds: "{{ remaining_min_on }}"
# Reset state to NONE first (before turn-off triggers another restart) # Reset state to NONE first (before turn-off triggers another restart)
- service: input_text.set_value - service: input_text.set_value
target: target:
@@ -1376,33 +1309,15 @@ action:
# Enable the light/switch # Enable the light/switch
default: default:
# Store current brightness (to restore later if configured). # Store current brightness (to restore later if configured)
# If light is on but below brightness_threshold, treat as "off"
# for restore purposes — otherwise the disable path would
# restore that dim state instead of actually turning off.
- variables: - variables:
last_brightness: > last_brightness: >
{% if reference_light is none or is_state(reference_light, 'off') %} {% if reference_light is none or is_state(reference_light, 'off') %}
{{ 0 }} {{ 0 }}
{% else %} {% else %}
{% set br = state_attr(reference_light, 'brightness') | int(0) %} {{ state_attr(reference_light, 'brightness') | int(0) }}
{% set thr = brightness_threshold | int(0) %}
{% if thr > 0 and br < thr %}
{{ 0 }}
{% else %}
{{ br }}
{% endif %}
{% endif %} {% endif %}
# Guard: scene mode is on but no scene resolved — stop loudly
# rather than silently falling back to lights with default data.
- choose:
- conditions:
- condition: template
value_template: "{{ use_scene_instead and effective_scene is none }}"
sequence:
- stop: "Scene mode enabled but no scene configured for current mode"
# Update state to ENABLING BEFORE turning on the light. # Update state to ENABLING BEFORE turning on the light.
# This must happen first because mode: restart may cancel # This must happen first because mode: restart may cancel
# subsequent steps if the light state change fires immediately. # subsequent steps if the light state change fires immediately.
@@ -1418,31 +1333,6 @@ action:
})) %} })) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Run enable callback BEFORE turning on the light.
# Light state change fires mode:restart which cancels remaining
# steps — running the callback first ensures it isn't silently
# dropped (mirrors the CASE 3 disable_action ordering).
- choose:
- conditions:
- condition: template
value_template: "{{ enable_action != [] }}"
sequence: !input enable_action
# Debug notification (also before turn-on for restart safety)
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Motion Light Debug"
message: >
Action: ENABLE
Time: {{ now().strftime('%H:%M:%S') }}
Lights: {{ resolved_all_lights }}
Switches: {{ resolved_all_switches }}
Scene: {{ effective_scene if use_scene_instead else 'N/A' }}
Night Mode: {{ is_night_mode }}
# Scene activation path # Scene activation path
- choose: - choose:
- conditions: - conditions:
@@ -1483,6 +1373,28 @@ action:
target: target:
entity_id: "{{ resolved_all_switches }}" entity_id: "{{ resolved_all_switches }}"
# Execute enable callback action
- choose:
- conditions:
- condition: template
value_template: "{{ enable_action != [] }}"
sequence: !input enable_action
# Debug notification
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Motion Light Debug"
message: >
Action: ENABLE
Time: {{ now().strftime('%H:%M:%S') }}
Lights: {{ resolved_all_lights }}
Switches: {{ resolved_all_switches }}
Scene: {{ effective_scene if use_scene_instead else 'N/A' }}
Night Mode: {{ is_night_mode }}
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# CASE 3: Disable Path (Motion Cleared, Should Turn Off) # CASE 3: Disable Path (Motion Cleared, Should Turn Off)
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
-158
View File
@@ -1,158 +0,0 @@
# 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)
@@ -1,626 +0,0 @@
# 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
+1 -1
View File
@@ -242,5 +242,5 @@ action:
sequence: sequence:
- service: telegram_bot.send_message - service: telegram_bot.send_message
data: data:
chat_id: "{{ chat_id }}" target: "{{ chat_id }}"
message: "{{ reply_message }}" message: "{{ reply_message }}"
+2 -2
View File
@@ -286,7 +286,7 @@ action:
sequence: sequence:
- service: telegram_bot.send_message - service: telegram_bot.send_message
data: data:
chat_id: "{{ chat_id }}" target: "{{ chat_id }}"
message: "{{ answers[button_index] }}" message: "{{ answers[button_index] }}"
# Reply to original message unless we're deleting it # Reply to original message unless we're deleting it
reply_to_message_id: > reply_to_message_id: >
@@ -397,7 +397,7 @@ action:
# Send the message with keyboard # Send the message with keyboard
- service: telegram_bot.send_message - service: telegram_bot.send_message
data: data:
chat_id: "{{ result_chat_ids }}" target: "{{ result_chat_ids }}"
message: "{{ message_text }}" message: "{{ message_text }}"
inline_keyboard: "{{ inline_keyboard }}" inline_keyboard: "{{ inline_keyboard }}"
config_entry_id: > config_entry_id: >
-51
View File
@@ -1,51 +0,0 @@
# 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
@@ -1,147 +0,0 @@
# 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 }}"
+1 -16
View File
@@ -7,7 +7,6 @@ This blueprint monitors washing machine or dryer appliances and sends notificati
- Start notification with cycle duration, estimated end time, and mode details - Start notification with cycle duration, estimated end time, and mode details
- Completion notification (reminder to unload clothes) with energy report - Completion notification (reminder to unload clothes) with energy report
- "Almost done" notification (configurable minutes before end) - "Almost done" notification (configurable minutes before end)
- Unload reminder (configurable delay, optional repeats) — auto-cancelled when a new cycle starts or a configured door/lid sensor opens
- Pause/Resume notifications (detect when cycle is paused or resumed) - Pause/Resume notifications (detect when cycle is paused or resumed)
- Error message notifications - Error message notifications
- Preparation mode notification (e.g., for dryer prep) - Preparation mode notification (e.g., for dryer prep)
@@ -40,8 +39,6 @@ The automation tracks the appliance through these states:
| `cst` | Cycle Start Time (ISO timestamp) | | `cst` | Cycle Start Time (ISO timestamp) |
| `esmp` | Energy Samples accumulator (Wh) | | `esmp` | Energy Samples accumulator (Wh) |
| `lst` | Last Sample Time (ISO timestamp) | | `lst` | Last Sample Time (ISO timestamp) |
| `cct` | Cycle Completion Time (ISO timestamp, drives unload reminder) |
| `urc` | Unload Reminder Count (number of reminders already sent) |
## Message Template Variables ## Message Template Variables
@@ -52,7 +49,7 @@ All message templates support these placeholder variables (use single braces):
| `{appliance_name}` | Device name (e.g., "Washing Machine") | | `{appliance_name}` | Device name (e.g., "Washing Machine") |
| `{remaining}` | Remaining time as string (e.g., "01:30:00") | | `{remaining}` | Remaining time as string (e.g., "01:30:00") |
| `{estimated_end}` | Estimated completion time (e.g., "14:30") | | `{estimated_end}` | Estimated completion time (e.g., "14:30") |
| `{minutes}` | Remaining minutes (almost-done) or elapsed minutes since completion (unload reminder) | | `{minutes}` | Remaining minutes as number (e.g., 90) |
| `{error}` | Error message text (only in error notification) | | `{error}` | Error message text (only in error notification) |
| `{tub_count}` | Tub clean counter value (only in tub clean notification) | | `{tub_count}` | Tub clean counter value (only in tub clean notification) |
| `{tub_threshold}` | Tub clean threshold (only in tub clean notification) | | `{tub_threshold}` | Tub clean threshold (only in tub clean notification) |
@@ -67,18 +64,6 @@ All message templates support these placeholder variables (use single braces):
- Notification service entity - Notification service entity
- (Optional) Power sensor for energy tracking - (Optional) Power sensor for energy tracking
## Unload Reminder
If `Unload Reminder Delay` is greater than 0, the automation:
1. Records the completion timestamp (`cct`) when the cycle finishes — but **only if** every door sensor in `Unload Reminder Door Sensors` is currently closed (or the list is empty).
2. Sends a reminder N minutes later (where N = delay), then optionally repeats every `Unload Reminder Repeat Interval` minutes up to `Unload Reminder Repeat Count` total.
3. Cancels all pending reminders when:
- A new cycle starts (start handler clears `cct` and `urc`), **or**
- Any of the configured door / lid sensors transitions to `on` (treated as "user has unloaded").
The check runs once per minute via a `time_pattern` trigger. The door sensor list is optional — leave it empty to keep reminders purely time-based.
## Note ## Note
Default messages are in Russian for LG ThinQ integration. Customize messages in the "Messages" section for your language. Default messages are in Russian for LG ThinQ integration. Customize messages in the "Messages" section for your language.
+14 -211
View File
@@ -65,67 +65,6 @@ blueprint:
selector: selector:
boolean: boolean:
# -------------------------------------------------------------------------
# Unload Reminder
# -------------------------------------------------------------------------
# Optional reminder to unload the appliance after the cycle completes.
# Purely time-based: no door/state tracking required.
unload_reminder_group:
name: "Unload Reminder"
collapsed: true
input:
unload_reminder_delay:
name: Unload Reminder Delay (minutes)
description: >
Send a reminder this many minutes after the cycle completes
if the appliance has not been emptied yet.
Set to 0 to disable unload reminders entirely.
default: 0
selector:
number:
min: 0
max: 180
unit_of_measurement: minutes
mode: slider
unload_reminder_repeat_count:
name: Unload Reminder Repeat Count
description: >
Total number of unload reminders to send (including the first).
Set to 1 for a single reminder.
default: 1
selector:
number:
min: 1
max: 10
mode: slider
unload_reminder_repeat_interval:
name: Unload Reminder Repeat Interval (minutes)
description: >
Time between repeated unload reminders.
Only used when Repeat Count > 1.
default: 15
selector:
number:
min: 5
max: 120
unit_of_measurement: minutes
mode: slider
unload_reminder_door_sensors:
name: Unload Reminder Door Sensors (optional)
description: >
Optional list of door / lid binary sensors. When any of them
opens after the cycle completes, pending unload reminders are
cancelled (treated as "user has unloaded").
Leave empty to keep reminders purely time-based.
default: []
selector:
entity:
domain: binary_sensor
multiple: true
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Persistent State Configuration # Persistent State Configuration
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -204,17 +143,6 @@ blueprint:
text: text:
multiline: true multiline: true
message_unload_reminder:
name: "Unload Reminder Message"
description: >
Reminder sent after the cycle completes if the appliance has not
been unloaded within the configured delay.
Variables: `{appliance_name}`, `{minutes}` (elapsed since completion)
default: "🧺 {appliance_name}: прошло {minutes} мин. — пора достать вещи!"
selector:
text:
multiline: true
message_almost_done: message_almost_done:
name: "Almost Done Message" name: "Almost Done Message"
description: > description: >
@@ -461,18 +389,6 @@ trigger:
entity_id: !input power_sensor entity_id: !input power_sensor
id: "power_update" id: "power_update"
# Periodic tick for time-based checks (e.g., unload reminder)
- platform: time_pattern
minutes: "/1"
id: "reminder_tick"
# Door / lid opened after completion (cancels unload reminder)
# Note: Uses multiple selector, so empty list means trigger is skipped
- platform: state
entity_id: !input unload_reminder_door_sensors
to: 'on'
id: "door_opened"
# ============================================================================= # =============================================================================
# CONDITIONS # CONDITIONS
# ============================================================================= # =============================================================================
@@ -496,8 +412,6 @@ variables:
state_cycle_start_time: 'cst' # Cycle start timestamp state_cycle_start_time: 'cst' # Cycle start timestamp
state_energy_samples: 'esmp' # Energy sample accumulator (Wh) state_energy_samples: 'esmp' # Energy sample accumulator (Wh)
state_last_sample_time: 'lst' # Last power sample timestamp state_last_sample_time: 'lst' # Last power sample timestamp
state_cycle_completion_time: 'cct' # Cycle completion timestamp (for unload reminder)
state_unload_reminder_count: 'urc' # Number of unload reminders already sent
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Input Variables # Input Variables
@@ -517,10 +431,6 @@ variables:
show_estimated_end_time: !input show_estimated_end_time show_estimated_end_time: !input show_estimated_end_time
power_sensor: !input power_sensor power_sensor: !input power_sensor
energy_cost_per_kwh: !input energy_cost_per_kwh energy_cost_per_kwh: !input energy_cost_per_kwh
unload_reminder_delay: !input unload_reminder_delay
unload_reminder_repeat_count: !input unload_reminder_repeat_count
unload_reminder_repeat_interval: !input unload_reminder_repeat_interval
unload_reminder_door_sensors: !input unload_reminder_door_sensors
enable_debug_notifications: !input enable_debug_notifications enable_debug_notifications: !input enable_debug_notifications
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -528,7 +438,6 @@ variables:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
message_start_template: !input message_start message_start_template: !input message_start
message_completed_template: !input message_completed message_completed_template: !input message_completed
message_unload_reminder_template: !input message_unload_reminder
message_almost_done_template: !input message_almost_done message_almost_done_template: !input message_almost_done
message_preparation_template: !input message_preparation message_preparation_template: !input message_preparation
message_error_template: !input message_error message_error_template: !input message_error
@@ -553,17 +462,15 @@ variables:
and run_state not in non_running_state_ids and run_state not in non_running_state_ids
and run_state != preparation_state_id and run_state != preparation_state_id
and run_state != run_state_completion_id and run_state != run_state_completion_id
and (remaining | string | trim) not in ['unknown', 'unavailable', 'none', '-', ''] }} and remaining not in ['unknown', 'unavailable'] }}
# Parse remaining time string (hh:mm:ss) to total minutes. # Parse remaining time string (hh:mm:ss) to total minutes
# Use int(0) defaults so a partially invalid sensor value (e.g.
# 'unavailable:00:00') degrades to 0 instead of raising.
remaining_time_in_minutes: > remaining_time_in_minutes: >
{% set r = remaining | string | trim %} {% if remaining not in ['unknown', 'unavailable', '-'] %}
{% if r not in ['unknown', 'unavailable', 'none', '-', ''] %} {% set parts = remaining.split(':') %}
{% set parts = r.split(':') %}
{% if parts | length >= 2 %} {% if parts | length >= 2 %}
{{ parts[0] | int(0) * 60 + parts[1] | int(0) }} {% set total = parts[0]|int * 60 + parts[1]|int %}
{{ total }}
{% else %} {% else %}
{{ 0 }} {{ 0 }}
{% endif %} {% endif %}
@@ -573,12 +480,11 @@ variables:
# Calculate estimated end time from remaining time # Calculate estimated end time from remaining time
estimated_end_time: > estimated_end_time: >
{% set r = remaining | string | trim %} {% if remaining not in ['unknown', 'unavailable', '-'] %}
{% if r not in ['unknown', 'unavailable', 'none', '-', ''] %} {% set parts = remaining.split(':') %}
{% set parts = r.split(':') %}
{% if parts | length >= 2 %} {% if parts | length >= 2 %}
{% set hours = parts[0] | int(0) %} {% set hours = parts[0] | int %}
{% set minutes = parts[1] | int(0) %} {% set minutes = parts[1] | int %}
{% set end_time = now() + timedelta(hours=hours, minutes=minutes) %} {% set end_time = now() + timedelta(hours=hours, minutes=minutes) %}
{{ end_time.strftime('%H:%M') }} {{ end_time.strftime('%H:%M') }}
{% else %} {% else %}
@@ -713,9 +619,7 @@ action:
state_notification_about_start_sent: true, state_notification_about_start_sent: true,
state_cycle_start_time: now().isoformat(), state_cycle_start_time: now().isoformat(),
state_energy_samples: 0, state_energy_samples: 0,
state_last_sample_time: now().isoformat(), state_last_sample_time: now().isoformat()
state_cycle_completion_time: '',
state_unload_reminder_count: 0
})) %} })) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
@@ -742,7 +646,7 @@ action:
value_template: > value_template: >
{{ not is_running {{ not is_running
and automation_state.get(state_notification_about_start_sent, false) and automation_state.get(state_notification_about_start_sent, false)
and (states(run_state_sensor) in [run_state_completion_id, 'unknown', 'unavailable', '-'] or states(remaining_time_sensor) in ['unknown', 'unavailable', '-']) }} and (states(run_state_sensor) in [run_state_completion_id, 'unknown', '-'] or states(remaining_time_sensor) in ['unknown', '-']) }}
sequence: sequence:
- variables: - variables:
# Calculate energy consumption # Calculate energy consumption
@@ -782,17 +686,13 @@ action:
entity_id: "{{ automation_state_entity }}" entity_id: "{{ automation_state_entity }}"
data: data:
value: > value: >
{% set any_door_open = (unload_reminder_door_sensors | select('is_state', 'on') | list | length) > 0 %}
{% set arm_reminder = (unload_reminder_delay | int(0)) > 0 and not any_door_open %}
{% set new_automation_state = (automation_state | combine({ {% set new_automation_state = (automation_state | combine({
state_notification_about_remaining_time_sent: false, state_notification_about_remaining_time_sent: false,
state_notification_about_start_sent: false, state_notification_about_start_sent: false,
state_notification_about_preparation_sent: false, state_notification_about_preparation_sent: false,
state_notification_about_pause_sent: false, state_notification_about_pause_sent: false,
state_was_paused: false, state_was_paused: false,
state_energy_samples: 0, state_energy_samples: 0
state_cycle_completion_time: (now().isoformat() if arm_reminder else ''),
state_unload_reminder_count: 0
})) %} })) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
@@ -894,7 +794,7 @@ action:
- variables: - variables:
# Calculate minutes for the template # Calculate minutes for the template
minutes: "{{ remaining.split(':')[1] | int(0) if ':' in (remaining | string) else 0 }}" minutes: "{{ remaining.split(':')[1] | int }}"
# Render the message template with available variables # Render the message template with available variables
message: > message: >
{% set tpl = message_almost_done_template %} {% set tpl = message_almost_done_template %}
@@ -1073,100 +973,3 @@ action:
state_last_sample_time: now().isoformat() state_last_sample_time: now().isoformat()
})) %} })) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# -----------------------------------------------------------------------
# CASE 10: Unload Reminder
# -----------------------------------------------------------------------
# Triggered when: Cycle completion timestamp is recorded and the
# configured delay (plus any repeat intervals) has elapsed without a
# new cycle starting. CASE 1 clears the timestamp on a new cycle.
# Action: Remind the user to unload the appliance.
- conditions:
- condition: template
value_template: >
{%- set cct_val = automation_state.get(state_cycle_completion_time, '') -%}
{%- set count_val = automation_state.get(state_unload_reminder_count, 0) | int(0) -%}
{{ (unload_reminder_delay | int(0)) > 0
and cct_val not in ['', 'unknown', 'unavailable', 'none']
and count_val < (unload_reminder_repeat_count | int(1))
and ((now() - (cct_val | as_datetime)).total_seconds() / 60)
>= ((unload_reminder_delay | int(0)) + count_val * (unload_reminder_repeat_interval | int(0))) }}
sequence:
- variables:
cct_val: "{{ automation_state.get(state_cycle_completion_time, '') }}"
count_val: "{{ automation_state.get(state_unload_reminder_count, 0) | int(0) }}"
new_reminder_count: "{{ (count_val | int(0)) + 1 }}"
elapsed_minutes: >
{{ ((now() - (cct_val | as_datetime)).total_seconds() / 60) | round(0) | int }}
# Render the message template with available variables
message: >
{% set tpl = message_unload_reminder_template %}
{{ tpl | replace('{appliance_name}', appliance_name)
| replace('{minutes}', elapsed_minutes | string) }}
# Send unload reminder notification
- service: notify.send_message
target:
entity_id: !input notify_target
data:
message: "{{ message }}"
# Increment reminder count; clear completion time when last reminder is sent
- service: input_text.set_value
target:
entity_id: "{{ automation_state_entity }}"
data:
value: >
{% set new_automation_state = (automation_state | combine({
state_unload_reminder_count: new_reminder_count | int(0),
state_cycle_completion_time: ('' if (new_reminder_count | int(0)) >= (unload_reminder_repeat_count | int(1)) else cct_val)
})) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Debug notification for unload reminder
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "{appliance_name} - UNLOAD REMINDER"
message: >
Action: UNLOAD REMINDER
Time: {{ now().strftime('%H:%M:%S') }}
Reminder #: {{ new_reminder_count }} of {{ unload_reminder_repeat_count }}
Elapsed: {{ elapsed_minutes }} min
# -----------------------------------------------------------------------
# CASE 11: Door Opened After Completion (cancel unload reminder)
# -----------------------------------------------------------------------
# Triggered when: A configured door / lid sensor opens while a cycle
# completion timestamp is pending. Treated as "user has unloaded".
# Action: Clear cct and urc so no further reminders fire.
- conditions:
- condition: template
value_template: >
{{ trigger.id == 'door_opened'
and automation_state.get(state_cycle_completion_time, '') not in ['', 'unknown', 'unavailable', 'none'] }}
sequence:
- service: input_text.set_value
target:
entity_id: "{{ automation_state_entity }}"
data:
value: >
{% set new_automation_state = (automation_state | combine({
state_cycle_completion_time: '',
state_unload_reminder_count: 0
})) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Debug notification for door-cancelled reminder
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "{appliance_name} - UNLOAD DETECTED"
message: >
Action: UNLOAD REMINDER CANCELLED
Time: {{ now().strftime('%H:%M:%S') }}
Door: {{ trigger.entity_id }}
-2
View File
@@ -14,13 +14,11 @@ A collection of automation blueprints for Home Assistant.
| Home Presence | Determines home presence from multiple signals | | Home Presence | Determines home presence from multiple signals |
| Immich Album Watcher | Sends notifications when photos are added to Immich albums | | Immich Album Watcher | Sends notifications when photos are added to Immich albums |
| Motion Light | Smart motion sensor light control | | 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 | | Refrigerator | Monitors refrigerator temperature and triggers express mode |
| Telegram Commands | Responds to Telegram bot commands with callback actions | | Telegram Commands | Responds to Telegram bot commands with callback actions |
| Telegram Question | Sends Telegram messages with inline keyboard buttons | | Telegram Question | Sends Telegram messages with inline keyboard buttons |
| Thermostat Controller | Controls thermostat based on schedules, presence, and window sensors | | 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 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 | | Track Abnormal Plug Activity | Monitors power sensors for sustained overconsumption |
| Washing Machine | Sends notifications for washing machine events | | Washing Machine | Sends notifications for washing machine events |
+10 -33
View File
@@ -4,42 +4,19 @@ Controls lights and switches using Zigbee2MQTT button devices with multiple acti
## Features ## Features
- Map multiple action IDs to different lights/switches by index position - Map multiple action IDs to different lights/switches
- Supports `light`, `switch`, and `input_boolean` entities - Supports light, switch, and input_boolean entities
- Visual feedback via blink indication when pressing an already-active light after an idle timeout - Visual feedback via blink indication when pressing already-active light
- Optionally tracks last interacted entity in an `input_text` helper - Tracks last interacted entity in an input_text helper
- Supports two MQTT topics for two physical buttons - Supports multiple MQTT topics (multiple buttons)
- Optional debug notifications for troubleshooting
## How It Works ## How It Works
1. Receives MQTT messages from one or two Zigbee buttons. 1. Receives MQTT messages with action IDs from Zigbee buttons
2. Reads `action` from the payload and looks it up in the configured **Action IDs** list. 2. Maps action ID to corresponding entity by index position
3. The matched index is used to pick the corresponding entity from the **Switches** list. 3. Toggles the matched entity (light/switch/input_boolean)
4. Toggles the matched entity (`light` / `switch` / `input_boolean`). 4. Optionally blinks light if it's already on (idle timeout feature)
5. For lights, if the light is already on and has been on for longer than the configured idle timeout, the light is briefly blinked instead of toggled, to indicate it is already active.
6. If an `input_text` helper is configured, the matched entity ID is written to it.
## Configuration
| Input | Description |
| --- | --- |
| **MQTT Topic** | Primary topic (e.g. `zigbee2mqtt/my_button1`). |
| **MQTT Topic 2 (Optional)** | Second topic. Leave the placeholder to disable the second trigger. |
| **Action IDs** | Ordered list of action strings emitted by the button (e.g. `single`, `double`, `hold`). |
| **Switches** | Entities matched to **Action IDs** by index — index `0` maps to action `0`, and so on. |
| **Last Interacted Entity Text Helper (optional)** | `input_text` that receives the last toggled entity ID. Leave empty to skip. |
| **Timeout for indicator blink** | Idle seconds after which a press on an already-on light blinks instead of toggling. `0` disables blinking. |
| **Count of blinks** | Number of blink cycles. `0` also disables blinking (every press just toggles). |
| **Interval between blinks** | Delay between turn off / turn on in milliseconds. |
| **Enable Debug Notifications** | When on, posts a persistent notification per trigger and per light-branch decision. |
The number of **Action IDs** should match the number of **Switches**. Out-of-range or unknown actions are ignored safely.
## Debug Mode
Enable **Enable Debug Notifications** to surface the resolved action, target index, target entity, and (for lights) the elapsed-time / blink decision in Home Assistant's persistent notifications panel. Disable when not troubleshooting to avoid notification noise.
## Author ## Author
Alexei Dolgolyov (<dolgolyov.alexei@gmail.com>) Alexei Dolgolyov (dolgolyov.alexei@gmail.com)
+52 -143
View File
@@ -6,13 +6,10 @@
blueprint: blueprint:
name: "Custom: MQTT Button Control" name: "Custom: MQTT Button Control"
description: > description: Control a Zigbee2MQTT device with multiple actions, that allows to toggle lights and switches and store entity identifier of last interacted switch/light.
Control a Zigbee2MQTT device with multiple actions: toggle lights,
switches and input_booleans, and optionally remember the entity that
was last interacted with in an input_text helper.
domain: automation domain: automation
input: input:
mqtt_group: mqtt_group:
name: "MQTT" name: "MQTT"
collapsed: false collapsed: false
@@ -22,7 +19,7 @@ blueprint:
description: The MQTT topic for your Zigbee button (e.g., zigbee2mqtt/my_button1). description: The MQTT topic for your Zigbee button (e.g., zigbee2mqtt/my_button1).
selector: selector:
text: text:
mqtt_topic2: mqtt_topic2:
name: MQTT Topic 2 (Optional) name: MQTT Topic 2 (Optional)
description: > description: >
@@ -31,7 +28,7 @@ blueprint:
default: "blueprint/disabled/mqtt_button_control" default: "blueprint/disabled/mqtt_button_control"
selector: selector:
text: text:
devices: devices:
name: "Primary" name: "Primary"
collapsed: false collapsed: false
@@ -43,19 +40,19 @@ blueprint:
selector: selector:
text: text:
multiple: true multiple: true
switches: switches:
name: Switches name: Switches
description: "The list of switches to control. Next types are supported: `light`, `switch`, `input_boolean`" description: "The list of switches to control. Next types are supported: `light`, `switch`, `input_boolean`"
default: [] default: []
selector: selector:
entity: entity:
domain: domain:
- light - light
- switch - switch
- input_boolean - input_boolean
multiple: true multiple: true
outputs: outputs:
name: "Outputs" name: "Outputs"
collapsed: false collapsed: false
@@ -67,7 +64,7 @@ blueprint:
selector: selector:
entity: entity:
domain: input_text domain: input_text
common: common:
name: "Common" name: "Common"
collapsed: false collapsed: false
@@ -81,8 +78,8 @@ blueprint:
min: 0 min: 0
max: 100 max: 100
step: 1 step: 1
unit_of_measurement: "s" unit_of_measurement: "s"
blink_count: blink_count:
name: Count of blinks name: Count of blinks
description: "Count of blinks to indicate active light" description: "Count of blinks to indicate active light"
@@ -91,8 +88,8 @@ blueprint:
number: number:
min: 0 min: 0
max: 5 max: 5
step: 1 step: 1
blink_interval: blink_interval:
name: Interval between blinks name: Interval between blinks
description: "Interval between indicator blinks (in ms)" description: "Interval between indicator blinks (in ms)"
@@ -101,36 +98,18 @@ blueprint:
number: number:
min: 0 min: 0
max: 1000 max: 1000
step: 50 step: 50
unit_of_measurement: "ms" unit_of_measurement: "ms"
# -------------------------------------------------------------------------
# Debug
# -------------------------------------------------------------------------
debug_group:
name: "Debug"
collapsed: true
input:
enable_debug_notifications:
name: Enable Debug Notifications
description: >
Send persistent notifications for debugging automation behavior.
Shows received action, resolved target entity and blink decision.
default: false
selector:
boolean:
trigger: trigger:
- platform: mqtt - platform: mqtt
topic: !input mqtt_topic topic: !input mqtt_topic
id: button_1
- platform: mqtt - platform: mqtt
topic: !input mqtt_topic2 topic: !input mqtt_topic2
id: button_2 enabled: "{{ mqtt_topic2 != '' }}"
enabled: "{{ mqtt_topic2 not in ['', 'blueprint/disabled/mqtt_button_control'] }}"
mode: restart mode: restart
condition: condition:
- condition: template - condition: template
value_template: "{{ 'action' in trigger.payload_json }}" value_template: "{{ 'action' in trigger.payload_json }}"
@@ -138,59 +117,21 @@ condition:
action: action:
- variables: - variables:
action_id: "{{ trigger.payload_json.action }}" action_id: "{{ trigger.payload_json.action }}"
switches: !input switches switches: !input switches
action_ids: !input action_ids action_ids: !input action_ids
last_interacted_text: !input last_interacted_text
is_debug: !input enable_debug_notifications target_index: >
{% if action_id in action_ids %}
# Resolve action_id → switch index. Whitespace-controlled and coerced
# to int so subscripting `switches[target_index]` is safe.
target_index: >-
{%- if action_id in action_ids -%}
{{ action_ids.index(action_id) }} {{ action_ids.index(action_id) }}
{%- else -%} {% else %}
-1 -1
{%- endif -%} {% endif %}
# Bounds-check the index: action_ids may have more entries than switches. target_entity: "{{ switches[target_index] if target_index != -1 else none }}"
target_entity: >- last_interacted_text: !input last_interacted_text
{%- set idx = target_index | int(-1) -%}
{%- if idx >= 0 and idx < (switches | length) -%} is_debug: false
{{ switches[idx] }}
{%- else -%}
{{ none }}
{%- endif -%}
has_last_interacted_text: "{{ last_interacted_text is string and last_interacted_text | trim != '' }}"
# ---------------------------------------------------------------------------
# Debug: trigger received
# ---------------------------------------------------------------------------
- choose:
- conditions:
- condition: template
value_template: "{{ is_debug }}"
sequence:
- service: persistent_notification.create
data:
notification_id: "mqtt_button_control_debug"
title: "MQTT Button Control Debug"
message: >
Trigger: {{ trigger.id }}
Topic: {{ trigger.topic }}
action_id: {{ action_id }}
action_ids: {{ action_ids }}
switches: {{ switches }}
target_index: {{ target_index | int(-1) }}
target_entity: {{ target_entity if target_entity is not none else 'none' }}
has_last_interacted_text: {{ has_last_interacted_text }}
- choose: - choose:
- conditions: - conditions:
- condition: template - condition: template
@@ -198,18 +139,12 @@ action:
sequence: sequence:
- variables: - variables:
entity_type: "{{ target_entity.split('.')[0] }}" entity_type: "{{ target_entity.split('.')[0] }}"
# Persist last interacted entity only when a helper is configured. - service: input_text.set_value
- choose: data:
- conditions: entity_id: "{{ last_interacted_text }}"
- condition: template value: "{{ target_entity }}"
value_template: "{{ has_last_interacted_text }}"
sequence:
- service: input_text.set_value
data:
entity_id: "{{ last_interacted_text }}"
value: "{{ target_entity }}"
- choose: - choose:
# Light # Light
- conditions: - conditions:
@@ -218,29 +153,14 @@ action:
sequence: sequence:
- variables: - variables:
timeout_for_indication_blink: !input timeout_for_indication_blink timeout_for_indication_blink: !input timeout_for_indication_blink
seconds_elapsed: >
{{ (as_timestamp(now()) - as_timestamp(states[target_entity].last_changed)) | int }}
should_blink: "{{ timeout_for_indication_blink != 0 and seconds_elapsed > timeout_for_indication_blink }}"
blink_count: !input blink_count blink_count: !input blink_count
blink_timeout: !input blink_interval blink_timeout: !input blink_interval
is_light_on: "{{ is_state(target_entity, 'on') }}" is_light_on: "{{ is_state(target_entity, 'on') }}"
# Inline state access — HA stringifies State objects when
# stored in `variables:`, so `.last_changed` must be read # Debug
# within a single template render.
seconds_elapsed: >-
{%- set s = states[target_entity] -%}
{%- if s is not none -%}
{{ (as_timestamp(now()) - as_timestamp(s.last_changed)) | int(0) }}
{%- else -%}
0
{%- endif -%}
# Blink only when both the idle timeout AND a positive
# blink count are configured; otherwise fall through to a
# plain toggle (a 0 count would otherwise run an empty
# repeat and leave the light untouched).
should_blink: >-
{{ timeout_for_indication_blink != 0
and (blink_count | int(0)) > 0
and (seconds_elapsed | int(0)) > timeout_for_indication_blink }}
# Debug: light branch state
- choose: - choose:
- conditions: - conditions:
- condition: template - condition: template
@@ -248,25 +168,13 @@ action:
sequence: sequence:
- service: persistent_notification.create - service: persistent_notification.create
data: data:
notification_id: "mqtt_button_control_debug_light" title: "Debug Info"
title: "MQTT Button Control Debug (light)"
message: > message: >
target_entity: {{ target_entity }} seconds_elapsed = {{ seconds_elapsed }},
should_blink = {{ should_blink }}
is_light_on: {{ is_light_on }}
seconds_elapsed: {{ seconds_elapsed }}
timeout_for_indication_blink: {{ timeout_for_indication_blink }}
should_blink: {{ should_blink }}
blink_count: {{ blink_count }}
blink_timeout (ms): {{ blink_timeout }}
- choose: - choose:
# Blink (indicate light is already on after idle timeout) # Blink
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ should_blink and is_light_on }}" value_template: "{{ should_blink and is_light_on }}"
@@ -274,6 +182,7 @@ action:
- repeat: - repeat:
count: "{{ blink_count }}" count: "{{ blink_count }}"
sequence: sequence:
- service: light.turn_off - service: light.turn_off
target: target:
entity_id: "{{ target_entity }}" entity_id: "{{ target_entity }}"
@@ -281,7 +190,7 @@ action:
transition: 0 transition: 0
- delay: - delay:
milliseconds: "{{ blink_timeout }}" milliseconds: "{{ blink_timeout }}"
- service: light.turn_on - service: light.turn_on
target: target:
entity_id: "{{ target_entity }}" entity_id: "{{ target_entity }}"
@@ -289,13 +198,13 @@ action:
transition: 0 transition: 0
- delay: - delay:
milliseconds: "{{ blink_timeout }}" milliseconds: "{{ blink_timeout }}"
# Actually toggle # Actually toggle
default: default:
- service: light.toggle - service: light.toggle
target: target:
entity_id: "{{ target_entity }}" entity_id: "{{ target_entity }}"
# Switch # Switch
- conditions: - conditions:
- condition: template - condition: template
@@ -304,7 +213,7 @@ action:
- service: switch.toggle - service: switch.toggle
target: target:
entity_id: "{{ target_entity }}" entity_id: "{{ target_entity }}"
# Input Boolean # Input Boolean
- conditions: - conditions:
- condition: template - condition: template
@@ -312,4 +221,4 @@ action:
sequence: sequence:
- service: input_boolean.toggle - service: input_boolean.toggle
target: target:
entity_id: "{{ target_entity }}" entity_id: "{{ target_entity }}"
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"version": "2.14.2" "version": "2.5.2"
} }