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