Compare commits
10 Commits
6848c3f903
...
feature/im
| Author | SHA1 | Date | |
|---|---|---|---|
| 13132323ea | |||
| a6ac4f4257 | |||
| 584c970044 | |||
| 4c03bc849e | |||
| eefc8993e3 | |||
| 9d19dfa8d3 | |||
| e98df855d9 | |||
| 29b1044d63 | |||
| 006f9e532c | |||
| 9383f957b6 |
@@ -237,8 +237,11 @@ blueprint:
|
||||
collapsed: false
|
||||
input:
|
||||
power_sensor:
|
||||
name: Power Sensor
|
||||
description: "Sensor reporting device power consumption (W)"
|
||||
name: Power Sensor (optional)
|
||||
description: >
|
||||
Sensor reporting device power consumption (W).
|
||||
Leave empty to disable power monitoring.
|
||||
default:
|
||||
selector:
|
||||
entity:
|
||||
domain: sensor
|
||||
@@ -277,6 +280,22 @@ blueprint:
|
||||
entity:
|
||||
domain: input_boolean
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Debug
|
||||
# -------------------------------------------------------------------------
|
||||
debug_group:
|
||||
name: "Debug"
|
||||
collapsed: true
|
||||
input:
|
||||
enable_debug_notifications:
|
||||
name: Enable Debug Notifications
|
||||
description: >
|
||||
Send persistent notifications for debugging automation behavior.
|
||||
Shows current state of all variables and filtering decisions.
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
# =============================================================================
|
||||
# AUTOMATION MODE
|
||||
# =============================================================================
|
||||
@@ -321,13 +340,6 @@ trigger:
|
||||
- platform: state
|
||||
entity_id: !input env_sensors
|
||||
|
||||
# Power sensor dropped below threshold (potential malfunction)
|
||||
- platform: numeric_state
|
||||
entity_id: !input power_sensor
|
||||
below: !input power_threshold
|
||||
for:
|
||||
seconds: !input power_decay_duration
|
||||
|
||||
# =============================================================================
|
||||
# CONDITIONS
|
||||
# =============================================================================
|
||||
@@ -349,6 +361,11 @@ action:
|
||||
force_on_entity: !input force_on_entity
|
||||
hysteresis_window: !input hysteresis_window
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Device State
|
||||
# -----------------------------------------------------------------------
|
||||
is_device_on: "{{ states(device_entity) not in ['off', 'unavailable', 'unknown'] }}"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Force ON Check
|
||||
# -----------------------------------------------------------------------
|
||||
@@ -406,13 +423,13 @@ action:
|
||||
# -----------------------------------------------------------------------
|
||||
# Power Monitoring
|
||||
# -----------------------------------------------------------------------
|
||||
power_threshold: !input power_threshold
|
||||
power_sensor: !input power_sensor
|
||||
power_threshold: !input power_threshold
|
||||
power_problematic_indicator_entity: !input power_problematic_indicator_entity
|
||||
power_decay_duration: !input power_decay_duration
|
||||
power: "{{ states(power_sensor) | float(0) }}"
|
||||
power: "{{ states(power_sensor) | float(0) if power_sensor is not none else 0 }}"
|
||||
# Device is problematic if it's consuming power but below threshold
|
||||
is_power_not_ok: "{{ (power > 0 and power < power_threshold) if power_threshold != 0 else false }}"
|
||||
is_power_not_ok: "{{ (power > 0 and power < power_threshold) if power_sensor is not none and power_threshold != 0 else false }}"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Window/Door State
|
||||
@@ -472,7 +489,7 @@ action:
|
||||
# -----------------------------------------------------------------------
|
||||
# Debug Flag
|
||||
# -----------------------------------------------------------------------
|
||||
is_debug: false
|
||||
is_debug: !input enable_debug_notifications
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DEBUG: Log current state
|
||||
@@ -482,18 +499,29 @@ action:
|
||||
sequence:
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
title: "Climate Controller (debug)"
|
||||
title: "Climate Device Controller Debug"
|
||||
message: >
|
||||
is_force_on = {{ is_force_on }},
|
||||
room_closed = {{ room_closed }},
|
||||
house_closed = {{ house_closed }},
|
||||
is_value_below_threshold = {{ is_value_below_threshold }},
|
||||
is_value_below_turn_on_threshold = {{ is_value_below_turn_on_threshold }},
|
||||
is_value_at_or_above_target = {{ is_value_at_or_above_target }},
|
||||
target_value = {{ target_value }},
|
||||
turn_on_threshold = {{ turn_on_threshold }},
|
||||
schedule_active = {{ schedule_active }},
|
||||
control_switch = {{ states(control_switch) }}
|
||||
**Device State:**
|
||||
- Device: {{ states(device_entity) }} (on: {{ is_device_on }})
|
||||
- Control Switch: {{ states(control_switch) }}
|
||||
- Force ON: {{ is_force_on }}
|
||||
- Schedule Active: {{ schedule_active }}
|
||||
|
||||
**Environment:**
|
||||
- Target Value: {{ target_value }}
|
||||
- Turn-On Threshold: {{ turn_on_threshold }} (hysteresis: {{ hysteresis_window }})
|
||||
- Below Emergency Threshold: {{ is_value_below_threshold }}
|
||||
- Below Turn-On Threshold: {{ is_value_below_turn_on_threshold }}
|
||||
- At/Above Target: {{ is_value_at_or_above_target }}
|
||||
|
||||
**Doors & Windows:**
|
||||
- House Closed: {{ house_closed }}
|
||||
- Room Closed: {{ room_closed }}
|
||||
|
||||
**Power Monitoring:**
|
||||
- Power Sensor: {{ power_sensor | default('not configured') }}
|
||||
- Power: {{ power }} W
|
||||
- Power OK: {{ not is_power_not_ok }}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POWER MONITORING: Flag device if malfunctioning
|
||||
@@ -503,7 +531,7 @@ action:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ states(device_entity) not in ['off', 'unavailable', 'unknown'] and power_problematic_indicator_entity is not none }}"
|
||||
value_template: "{{ power_sensor is not none and states(device_entity) not in ['off', 'unavailable', 'unknown'] and power_problematic_indicator_entity is not none }}"
|
||||
sequence:
|
||||
- variables:
|
||||
# Check if enough time has passed since last power reading
|
||||
@@ -562,7 +590,11 @@ action:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ is_force_on }}"
|
||||
sequence: !input turn_on_action
|
||||
sequence:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ not is_device_on }}"
|
||||
then: !input turn_on_action
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# PRIORITY 2: Emergency Override (Safety)
|
||||
@@ -571,7 +603,11 @@ action:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ is_value_below_threshold }}"
|
||||
sequence: !input turn_on_action
|
||||
sequence:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ not is_device_on }}"
|
||||
then: !input turn_on_action
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# PRIORITY 3: Control Switch Off
|
||||
@@ -580,7 +616,11 @@ action:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ is_state(control_switch, 'off') }}"
|
||||
sequence: !input turn_off_action
|
||||
sequence:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ is_device_on }}"
|
||||
then: !input turn_off_action
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# PRIORITY 4: Environment Not Ready
|
||||
@@ -589,7 +629,11 @@ action:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ not (house_closed or room_closed) or not schedule_active }}"
|
||||
sequence: !input turn_off_action
|
||||
sequence:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ is_device_on }}"
|
||||
then: !input turn_off_action
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# PRIORITY 5: Target Reached
|
||||
@@ -598,7 +642,11 @@ action:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ is_value_at_or_above_target }}"
|
||||
sequence: !input turn_off_action
|
||||
sequence:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ is_device_on }}"
|
||||
then: !input turn_off_action
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# PRIORITY 6: Below Turn-On Threshold
|
||||
@@ -607,7 +655,11 @@ action:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ is_value_below_turn_on_threshold }}"
|
||||
sequence: !input turn_on_action
|
||||
sequence:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ not is_device_on }}"
|
||||
then: !input turn_on_action
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# DEFAULT: Maintain Current State (Hysteresis Zone)
|
||||
|
||||
94
Common/Dreame Vacuum/README.md
Normal file
94
Common/Dreame Vacuum/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Dreame Vacuum Notifications
|
||||
|
||||
Sends customizable notifications for Dreame vacuum events. Requires the [Dreame Vacuum](https://github.com/Tasshack/dreame-vacuum) integration.
|
||||
|
||||
## Features
|
||||
|
||||
- Notifications for cleaning started and completed
|
||||
- Consumable end-of-life alerts (brush, filter, mop pad, sensor, etc.)
|
||||
- Device warning and error notifications
|
||||
- Informational alerts (e.g., action blocked by Do Not Disturb)
|
||||
- Individual toggle for each event type
|
||||
- Customizable message templates with variable substitution
|
||||
- Multiple notification targets
|
||||
|
||||
## How It Works
|
||||
|
||||
The blueprint listens to events fired by the Dreame Vacuum integration:
|
||||
|
||||
| Event | Description |
|
||||
| --- | --- |
|
||||
| `dreame_vacuum_task_status` | Cleaning job started or completed |
|
||||
| `dreame_vacuum_consumable` | Consumable reached end-of-life |
|
||||
| `dreame_vacuum_warning` | Dismissible device warning |
|
||||
| `dreame_vacuum_error` | Device fault |
|
||||
| `dreame_vacuum_information` | Action blocked by user settings |
|
||||
|
||||
Events are filtered by the configured vacuum entity, so only the selected vacuum triggers notifications.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Input | Description |
|
||||
| --- | --- |
|
||||
| **Vacuum Entity** | The Dreame vacuum entity to monitor |
|
||||
| **Notification Targets** | One or more `notify` entities |
|
||||
| **Event Toggles** | Enable/disable each event type independently |
|
||||
| **Message Templates** | Customizable message for each event type |
|
||||
|
||||
## Message Template Variables
|
||||
|
||||
### Cleaning Started
|
||||
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| `{vacuum_name}` | Friendly name of the vacuum |
|
||||
| `{cleaning_mode}` | Cleaning mode (e.g., sweeping, mopping) |
|
||||
| `{status}` | Current status |
|
||||
|
||||
### Cleaning Completed
|
||||
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| `{vacuum_name}` | Friendly name of the vacuum |
|
||||
| `{cleaning_mode}` | Cleaning mode used |
|
||||
| `{status}` | Final status |
|
||||
| `{cleaned_area}` | Area cleaned (m²) |
|
||||
| `{cleaning_time}` | Cleaning duration (minutes) |
|
||||
|
||||
### Consumable Depleted
|
||||
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| `{vacuum_name}` | Friendly name of the vacuum |
|
||||
| `{consumable}` | Consumable name (e.g., Main Brush, Side Brush, Filter, Mop Pad) |
|
||||
|
||||
### Warning
|
||||
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| `{vacuum_name}` | Friendly name of the vacuum |
|
||||
| `{warning}` | Warning description |
|
||||
| `{code}` | Warning code |
|
||||
|
||||
### Error
|
||||
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| `{vacuum_name}` | Friendly name of the vacuum |
|
||||
| `{error}` | Error description |
|
||||
| `{code}` | Error code |
|
||||
|
||||
### Information
|
||||
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| `{vacuum_name}` | Friendly name of the vacuum |
|
||||
| `{information}` | Information message (e.g., Dust Collection, Cleaning Paused) |
|
||||
|
||||
## Debug Mode
|
||||
|
||||
Enable **Debug Notifications** in the Debug section to send persistent notifications with raw event data for troubleshooting.
|
||||
|
||||
## Author
|
||||
|
||||
Alexei Dolgolyov (<dolgolyov.alexei@gmail.com>)
|
||||
424
Common/Dreame Vacuum/blueprint.yaml
Normal file
424
Common/Dreame Vacuum/blueprint.yaml
Normal file
@@ -0,0 +1,424 @@
|
||||
# Dreame Vacuum Notifications
|
||||
# Sends notifications for Dreame vacuum events (cleaning, errors, consumables).
|
||||
# See README.md for detailed documentation.
|
||||
#
|
||||
# Author: Alexei Dolgolyov (dolgolyov.alexei@gmail.com)
|
||||
|
||||
blueprint:
|
||||
name: "Custom: Dreame Vacuum Notifications"
|
||||
description: >
|
||||
Sends customizable notifications for Dreame vacuum events including
|
||||
cleaning status, consumable alerts, warnings, and errors.
|
||||
Requires the Dreame Vacuum integration (github.com/Tasshack/dreame-vacuum).
|
||||
domain: automation
|
||||
|
||||
# ===========================================================================
|
||||
# INPUT CONFIGURATION
|
||||
# ===========================================================================
|
||||
input:
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Vacuum Configuration
|
||||
# -------------------------------------------------------------------------
|
||||
vacuum_group:
|
||||
name: "Vacuum"
|
||||
collapsed: false
|
||||
input:
|
||||
vacuum_entity:
|
||||
name: Vacuum Entity
|
||||
description: "The Dreame vacuum entity to monitor."
|
||||
selector:
|
||||
entity:
|
||||
integration: dreame_vacuum
|
||||
domain: vacuum
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Notification Configuration
|
||||
# -------------------------------------------------------------------------
|
||||
notification_group:
|
||||
name: "Notification"
|
||||
collapsed: false
|
||||
input:
|
||||
notify_targets:
|
||||
name: Notification Targets
|
||||
description: "Notification service entities to send messages to (select one or more)."
|
||||
selector:
|
||||
entity:
|
||||
domain: notify
|
||||
multiple: true
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Event Toggles
|
||||
# -------------------------------------------------------------------------
|
||||
toggles_group:
|
||||
name: "Event Toggles"
|
||||
collapsed: false
|
||||
input:
|
||||
enable_cleaning_started:
|
||||
name: Cleaning Started
|
||||
description: "Send notification when the vacuum starts cleaning."
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
enable_cleaning_completed:
|
||||
name: Cleaning Completed
|
||||
description: "Send notification when the vacuum finishes cleaning."
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
enable_consumable:
|
||||
name: Consumable Depleted
|
||||
description: "Send notification when a consumable reaches end-of-life."
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
enable_warning:
|
||||
name: Warning
|
||||
description: "Send notification for device warnings."
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
enable_error:
|
||||
name: Error
|
||||
description: "Send notification for device errors."
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
enable_information:
|
||||
name: Information
|
||||
description: "Send notification for informational alerts (e.g., blocked by DND)."
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Message Templates
|
||||
# -------------------------------------------------------------------------
|
||||
messages_group:
|
||||
name: "Message Templates"
|
||||
collapsed: true
|
||||
input:
|
||||
message_cleaning_started:
|
||||
name: "Cleaning Started Message"
|
||||
description: >
|
||||
Message sent when the vacuum starts cleaning.
|
||||
Variables: `{vacuum_name}`, `{cleaning_mode}`, `{status}`
|
||||
default: "🧹 {vacuum_name} started cleaning ({cleaning_mode})."
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
|
||||
message_cleaning_completed:
|
||||
name: "Cleaning Completed Message"
|
||||
description: >
|
||||
Message sent when the vacuum finishes cleaning.
|
||||
Variables: `{vacuum_name}`, `{cleaning_mode}`, `{status}`,
|
||||
`{cleaned_area}`, `{cleaning_time}`
|
||||
default: "✅ {vacuum_name} finished cleaning. Area: {cleaned_area} m², time: {cleaning_time} min."
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
|
||||
message_consumable:
|
||||
name: "Consumable Depleted Message"
|
||||
description: >
|
||||
Message sent when a consumable reaches end-of-life.
|
||||
Variables: `{vacuum_name}`, `{consumable}`
|
||||
default: "🔧 {vacuum_name}: {consumable} needs replacement."
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
|
||||
message_warning:
|
||||
name: "Warning Message"
|
||||
description: >
|
||||
Message sent for device warnings.
|
||||
Variables: `{vacuum_name}`, `{warning}`, `{code}`
|
||||
default: "⚠️ {vacuum_name} warning: {warning} (code: {code})."
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
|
||||
message_error:
|
||||
name: "Error Message"
|
||||
description: >
|
||||
Message sent for device errors.
|
||||
Variables: `{vacuum_name}`, `{error}`, `{code}`
|
||||
default: "❌ {vacuum_name} error: {error} (code: {code})."
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
|
||||
message_information:
|
||||
name: "Information Message"
|
||||
description: >
|
||||
Message sent for informational alerts.
|
||||
Variables: `{vacuum_name}`, `{information}`
|
||||
default: "ℹ️ {vacuum_name}: {information}."
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Debug
|
||||
# -------------------------------------------------------------------------
|
||||
debug:
|
||||
name: "Debug"
|
||||
collapsed: true
|
||||
input:
|
||||
enable_debug_notifications:
|
||||
name: Enable Debug Notifications
|
||||
description: >
|
||||
Send persistent notifications for debugging automation behavior.
|
||||
Shows raw event data and filtering decisions.
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
# Queued mode to avoid dropping rapid events
|
||||
mode: queued
|
||||
max: 5
|
||||
|
||||
# =============================================================================
|
||||
# TRIGGERS
|
||||
# =============================================================================
|
||||
trigger:
|
||||
# Cleaning job started or completed
|
||||
- platform: event
|
||||
event_type: dreame_vacuum_task_status
|
||||
id: "task_status"
|
||||
|
||||
# Consumable reached end-of-life
|
||||
- platform: event
|
||||
event_type: dreame_vacuum_consumable
|
||||
id: "consumable"
|
||||
|
||||
# Informational alert (job blocked by user settings)
|
||||
- platform: event
|
||||
event_type: dreame_vacuum_information
|
||||
id: "information"
|
||||
|
||||
# Dismissible device warning
|
||||
- platform: event
|
||||
event_type: dreame_vacuum_warning
|
||||
id: "warning"
|
||||
|
||||
# Device fault
|
||||
- platform: event
|
||||
event_type: dreame_vacuum_error
|
||||
id: "error"
|
||||
|
||||
# =============================================================================
|
||||
# VARIABLES
|
||||
# =============================================================================
|
||||
variables:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input References
|
||||
# ---------------------------------------------------------------------------
|
||||
vacuum_entity: !input vacuum_entity
|
||||
notify_targets: !input notify_targets
|
||||
enable_debug_notifications: !input enable_debug_notifications
|
||||
|
||||
# Event toggles
|
||||
enable_cleaning_started: !input enable_cleaning_started
|
||||
enable_cleaning_completed: !input enable_cleaning_completed
|
||||
enable_consumable: !input enable_consumable
|
||||
enable_warning: !input enable_warning
|
||||
enable_error: !input enable_error
|
||||
enable_information: !input enable_information
|
||||
|
||||
# Message templates
|
||||
message_cleaning_started_template: !input message_cleaning_started
|
||||
message_cleaning_completed_template: !input message_cleaning_completed
|
||||
message_consumable_template: !input message_consumable
|
||||
message_warning_template: !input message_warning
|
||||
message_error_template: !input message_error
|
||||
message_information_template: !input message_information
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vacuum Info
|
||||
# ---------------------------------------------------------------------------
|
||||
vacuum_name: "{{ state_attr(vacuum_entity, 'friendly_name') | default(vacuum_entity) }}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Event Data (flat structure — fields are directly on trigger.event.data)
|
||||
# ---------------------------------------------------------------------------
|
||||
event_entity_id: "{{ trigger.event.data.entity_id | default('') }}"
|
||||
|
||||
# Task status fields
|
||||
task_cleaning_mode: "{{ trigger.event.data.cleaning_mode | default('unknown') }}"
|
||||
task_status_value: "{{ trigger.event.data.status | default('unknown') }}"
|
||||
task_completed: "{{ trigger.event.data.completed | default(false) }}"
|
||||
task_cleaned_area: "{{ trigger.event.data.cleaned_area | default(0) }}"
|
||||
task_cleaning_time: "{{ trigger.event.data.cleaning_time | default(0) }}"
|
||||
|
||||
# Consumable fields
|
||||
consumable_name: "{{ (trigger.event.data.consumable | default('unknown')) | replace('_', ' ') | title }}"
|
||||
|
||||
# Warning fields
|
||||
warning_description: "{{ trigger.event.data.warning | default('unknown') }}"
|
||||
warning_code: "{{ trigger.event.data.code | default('') }}"
|
||||
|
||||
# Error fields
|
||||
error_description: "{{ trigger.event.data.error | default('unknown') }}"
|
||||
error_code: "{{ trigger.event.data.code | default('') }}"
|
||||
|
||||
# Information fields
|
||||
information_description: "{{ (trigger.event.data.information | default('unknown')) | replace('_', ' ') | title }}"
|
||||
|
||||
# =============================================================================
|
||||
# CONDITIONS
|
||||
# =============================================================================
|
||||
condition:
|
||||
# Only process events from the configured vacuum entity.
|
||||
# The Dreame Vacuum integration uses generate_entity_id() for the entity_id
|
||||
# in event data, which may append a numeric suffix (e.g., _2) since the
|
||||
# actual vacuum entity already occupies the base entity_id.
|
||||
- condition: template
|
||||
value_template: >
|
||||
{{ event_entity_id == vacuum_entity
|
||||
or event_entity_id.startswith(vacuum_entity ~ '_') }}
|
||||
|
||||
# =============================================================================
|
||||
# ACTIONS
|
||||
# =============================================================================
|
||||
action:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Debug Logging
|
||||
# ---------------------------------------------------------------------------
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ enable_debug_notifications }}"
|
||||
sequence:
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
title: "Dreame Vacuum Debug"
|
||||
message: >
|
||||
**Trigger:** {{ trigger.id }}
|
||||
**Entity:** {{ event_entity_id }}
|
||||
**Vacuum:** {{ vacuum_name }}
|
||||
**Event Data:** {{ trigger.event.data }}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Send Notification Based on Event Type
|
||||
# ---------------------------------------------------------------------------
|
||||
- choose:
|
||||
|
||||
# CASE 1: Cleaning Started
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: >
|
||||
{{ trigger.id == 'task_status'
|
||||
and not task_completed
|
||||
and enable_cleaning_started }}
|
||||
sequence:
|
||||
- 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 }}"
|
||||
@@ -4,8 +4,7 @@ This blueprint monitors Immich album changes and sends notifications when assets
|
||||
|
||||
## Features
|
||||
|
||||
- Filter by hub/instance name (for multi-hub setups)
|
||||
- Monitor specific albums by name (whitelist)
|
||||
- Monitor specific albums by ID (whitelist)
|
||||
- Filter by asset type (track images only, videos only, or both)
|
||||
- Filter by favorites only (only notify about favorite assets)
|
||||
- Sort assets by date, rating, name, or keep original order
|
||||
@@ -107,6 +106,7 @@ These variables can be used in the image and video asset templates. Also used fo
|
||||
| `{type}` | Asset type (IMAGE or VIDEO) |
|
||||
| `{created}` | Creation date/time (always shown) |
|
||||
| `{created_if_unique}` | Creation date/time formatted with template if dates differ, empty if all same |
|
||||
| `{year}` | Year the asset was created (4-digit, e.g., 2024) |
|
||||
| `{owner}` | Owner display name |
|
||||
| `{url}` | Public URL to view the asset (empty if no shared link) |
|
||||
| `{download_url}` | Direct download URL for the original file |
|
||||
@@ -146,6 +146,7 @@ Select input_text entities containing Telegram chat IDs. Can be user IDs (positi
|
||||
- Large media lists are automatically split into multiple groups (2-10 items per group)
|
||||
- Optional chat action indicator (typing, uploading photo/video) while processing
|
||||
- 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
|
||||
|
||||
@@ -158,6 +159,8 @@ Select input_text entities containing Telegram chat IDs. Can be user IDs (positi
|
||||
|
||||
Sends a summary notification of tracked albums at configured times. Album names and share URLs are automatically read from the Album ID Entity's `album_name` and `share_url` attributes (if available). You can configure multiple notification times (e.g., "12:00, 18:00") using comma-separated 24-hour format with leading zeros.
|
||||
|
||||
Use **Summary Interval** to control how often summaries are sent (default: every day). Set it to `7` for weekly, `14` for bi-weekly, etc. The **Summary Start Date** anchors the interval — summaries are sent on that date and every N days after. For example, setting the start date to a Monday with an interval of 7 sends summaries every Monday.
|
||||
|
||||
When Telegram media is enabled, an optional image can be attached to the summary message. By default, the official Immich logo is used. Set the **Summary Image URL** to empty to send text-only notifications.
|
||||
|
||||
### Summary Message Template Variables
|
||||
|
||||
@@ -19,20 +19,9 @@ blueprint:
|
||||
# Hub & Album Configuration
|
||||
# -------------------------------------------------------------------------
|
||||
albums_group:
|
||||
name: "Hub & Albums"
|
||||
name: "Albums"
|
||||
collapsed: false
|
||||
input:
|
||||
hub_names:
|
||||
name: Hub Names to Track
|
||||
description: >
|
||||
List of Immich hub/instance names to monitor.
|
||||
Only events from matching hubs will trigger notifications.
|
||||
Leave empty to track all hubs.
|
||||
default: []
|
||||
selector:
|
||||
text:
|
||||
multiple: true
|
||||
|
||||
album_id_entities:
|
||||
name: Album ID Entities
|
||||
description: >
|
||||
@@ -236,7 +225,7 @@ blueprint:
|
||||
name: "Image Asset Template"
|
||||
description: >
|
||||
Template for IMAGE assets in the list. Also used for Telegram media captions.
|
||||
Variables: `{filename}`, `{description}`, `{type}`, `{created}`, `{created_if_unique}`, `{owner}`, `{url}`, `{download_url}`, `{photo_url}`, `{playback_url}`, `{people}`, `{album_name}`, `{album_created}`, `{album_updated}`, `{is_favorite}`, `{rating}`, `{location}`, `{location_if_unique}`, `{city}`, `{state}`, `{country}`
|
||||
Variables: `{filename}`, `{description}`, `{type}`, `{created}`, `{created_if_unique}`, `{year}`, `{owner}`, `{url}`, `{download_url}`, `{photo_url}`, `{playback_url}`, `{people}`, `{album_name}`, `{album_created}`, `{album_updated}`, `{is_favorite}`, `{rating}`, `{location}`, `{location_if_unique}`, `{city}`, `{state}`, `{country}`
|
||||
default: "\n • 🖼️ {filename}"
|
||||
selector:
|
||||
text:
|
||||
@@ -246,7 +235,7 @@ blueprint:
|
||||
name: "Video Asset Template"
|
||||
description: >
|
||||
Template for VIDEO assets in the list. Also used for Telegram media captions.
|
||||
Variables: `{filename}`, `{description}`, `{type}`, `{created}`, `{created_if_unique}`, `{owner}`, `{url}`, `{download_url}`, `{photo_url}`, `{playback_url}`, `{people}`, `{album_name}`, `{album_created}`, `{album_updated}`, `{is_favorite}`, `{rating}`, `{location}`, `{location_if_unique}`, `{city}`, `{state}`, `{country}`
|
||||
Variables: `{filename}`, `{description}`, `{type}`, `{created}`, `{created_if_unique}`, `{year}`, `{owner}`, `{url}`, `{download_url}`, `{photo_url}`, `{playback_url}`, `{people}`, `{album_name}`, `{album_created}`, `{album_updated}`, `{is_favorite}`, `{rating}`, `{location}`, `{location_if_unique}`, `{city}`, `{state}`, `{country}`
|
||||
default: "\n • 🎬 {filename}"
|
||||
selector:
|
||||
text:
|
||||
@@ -472,6 +461,23 @@ blueprint:
|
||||
- label: "Uploading 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
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -489,6 +495,28 @@ blueprint:
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
periodic_summary_interval:
|
||||
name: Summary Interval (Days)
|
||||
description: >
|
||||
Number of days between periodic summaries.
|
||||
Set to 1 for daily, 7 for weekly, etc.
|
||||
default: 1
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 365
|
||||
step: 1
|
||||
mode: box
|
||||
|
||||
periodic_summary_start_date:
|
||||
name: Summary Start Date
|
||||
description: >
|
||||
The date from which to start counting the summary interval.
|
||||
Summaries are sent on this date and every N days after.
|
||||
default: "2025-01-01"
|
||||
selector:
|
||||
date:
|
||||
|
||||
periodic_notification_times:
|
||||
name: Summary Notification Times
|
||||
description: >
|
||||
@@ -886,7 +914,7 @@ variables:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input Variables
|
||||
# ---------------------------------------------------------------------------
|
||||
hub_names: !input hub_names
|
||||
|
||||
album_id_entities: !input album_id_entities
|
||||
automation_id: !input automation_id
|
||||
|
||||
@@ -926,9 +954,13 @@ variables:
|
||||
telegram_disable_url_preview: !input telegram_disable_url_preview
|
||||
telegram_chat_action: !input telegram_chat_action
|
||||
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
|
||||
enable_periodic_summary: !input enable_periodic_summary
|
||||
periodic_summary_interval: !input periodic_summary_interval
|
||||
periodic_summary_start_date: !input periodic_summary_start_date
|
||||
periodic_summary_message_template: !input periodic_summary_message
|
||||
periodic_album_template: !input periodic_album_template
|
||||
periodic_summary_image_url: !input periodic_summary_image_url
|
||||
@@ -1033,10 +1065,6 @@ variables:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Computed Values
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check if this hub should be tracked (empty list = track all)
|
||||
is_hub_tracked: >
|
||||
{{ hub_names | length == 0 or event_hub_name in hub_names }}
|
||||
|
||||
# Check if this album should be tracked (empty list = track all)
|
||||
is_album_tracked: >
|
||||
{{ album_ids | length == 0 or event_album_id in album_ids }}
|
||||
@@ -1160,6 +1188,7 @@ variables:
|
||||
{% set raw_date = asset.created_at | default('', true) %}
|
||||
{% set dt = raw_date | as_datetime(none) if raw_date is string and raw_date | length > 0 else none %}
|
||||
{% set formatted_date = dt.strftime(date_format) if dt else '' %}
|
||||
{% set year = dt.strftime('%Y') if dt else '' %}
|
||||
{% set created_if_unique = '' if unique_dates | length == 1 else (date_if_unique_template | replace('{date}', formatted_date)) %}
|
||||
{% set is_favorite = favorite_indicator_template if asset.is_favorite | default(false) else '' %}
|
||||
{% set rating = asset.rating | default('') | string if asset.rating not in [none, ''] else '' %}
|
||||
@@ -1190,7 +1219,8 @@ variables:
|
||||
| replace('{location_if_unique}', location_if_unique)
|
||||
| replace('{city}', city)
|
||||
| replace('{state}', state)
|
||||
| replace('{country}', country) %}
|
||||
| replace('{country}', country)
|
||||
| replace('{year}', year) %}
|
||||
{% set ns.items = ns.items ~ item %}
|
||||
{% endfor %}
|
||||
{% set more_count = sorted_assets | length - max_items %}
|
||||
@@ -1247,13 +1277,15 @@ variables:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Periodic Summary Variables
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check if periodic summary should run (at configured times)
|
||||
# Manual test event (immich_album_watcher_test_periodic_summary) bypasses time check
|
||||
# Check if periodic summary should run (at configured times and interval)
|
||||
# Manual test event (immich_album_watcher_test_periodic_summary) bypasses interval check
|
||||
# If event data contains automation_id, only matching automations respond
|
||||
should_send_periodic_summary: >
|
||||
{% if enable_periodic_summary %}
|
||||
{% if trigger.id == 'periodic_summary_timer' %}
|
||||
{{ true }}
|
||||
{% set start = periodic_summary_start_date | as_datetime %}
|
||||
{% set days_elapsed = (now().date() - start.date()).days %}
|
||||
{{ days_elapsed >= 0 and days_elapsed % periodic_summary_interval == 0 }}
|
||||
{% elif trigger.platform == 'event' and trigger.event.event_type == 'immich_album_watcher_test_periodic_summary' %}
|
||||
{% set event_automation_id = trigger.event.data.automation_id | default('') %}
|
||||
{% if event_automation_id | length > 0 %}
|
||||
@@ -1382,7 +1414,7 @@ condition:
|
||||
{% elif trigger.platform == 'event' and trigger.event.event_type in ['immich_album_watcher_test_periodic_summary', 'immich_album_watcher_test_scheduled_assets', 'immich_album_watcher_test_memory_mode'] %}
|
||||
{{ should_send_periodic_summary or should_send_scheduled_assets or should_send_memory_mode }}
|
||||
{% else %}
|
||||
{{ is_hub_tracked and is_album_tracked and should_notify }}
|
||||
{{ is_album_tracked and should_notify }}
|
||||
{% endif %}
|
||||
|
||||
# =============================================================================
|
||||
@@ -1424,6 +1456,25 @@ action:
|
||||
assets: "{{ [{'url': periodic_summary_image_url, 'type': 'photo'}] if periodic_summary_image_url | length > 0 else [] }}"
|
||||
disable_web_page_preview: "{{ telegram_disable_url_preview }}"
|
||||
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
|
||||
- if:
|
||||
@@ -1565,6 +1616,7 @@ action:
|
||||
{% set raw_date = asset.created_at | default('', true) %}
|
||||
{% set dt = raw_date | as_datetime(none) if raw_date is string and raw_date | length > 0 else none %}
|
||||
{% set formatted_date = dt.strftime(date_format) if dt else '' %}
|
||||
{% set year = dt.strftime('%Y') if dt else '' %}
|
||||
{% set created_if_unique = '' if scheduled_unique_dates | length == 1 else (date_if_unique_template | replace('{date}', formatted_date) if formatted_date | length > 0 else '') %}
|
||||
{% set is_favorite = favorite_indicator_template if asset.is_favorite | default(false) else '' %}
|
||||
{% set rating = asset.rating | default('') | string if asset.rating not in [none, ''] else '' %}
|
||||
@@ -1595,7 +1647,8 @@ action:
|
||||
| replace('{location_if_unique}', location_if_unique)
|
||||
| replace('{city}', city)
|
||||
| replace('{state}', state)
|
||||
| replace('{country}', country) %}
|
||||
| replace('{country}', country)
|
||||
| replace('{year}', year) %}
|
||||
{% set ns.items = ns.items ~ item %}
|
||||
{% endfor %}
|
||||
{{ ns.items }}
|
||||
@@ -1666,6 +1719,24 @@ action:
|
||||
caption: "{{ scheduled_message }}"
|
||||
disable_web_page_preview: "{{ telegram_disable_url_preview }}"
|
||||
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
|
||||
- variables:
|
||||
@@ -1695,6 +1766,25 @@ action:
|
||||
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
|
||||
wait_for_response: false
|
||||
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
|
||||
# Distributes the limit evenly across albums (e.g., limit=10 with 2 albums = 5 each)
|
||||
@@ -1824,6 +1914,7 @@ action:
|
||||
{% set raw_date = asset.created_at | default('', true) %}
|
||||
{% set dt = raw_date | as_datetime(none) if raw_date is string and raw_date | length > 0 else none %}
|
||||
{% set formatted_date = dt.strftime(date_format) if dt else '' %}
|
||||
{% set year = dt.strftime('%Y') if dt else '' %}
|
||||
{% set created_if_unique = '' if combined_unique_dates | length == 1 else (date_if_unique_template | replace('{date}', formatted_date) if formatted_date | length > 0 else '') %}
|
||||
{% set is_favorite = favorite_indicator_template if asset.is_favorite | default(false) else '' %}
|
||||
{% set rating = asset.rating | default('') | string if asset.rating not in [none, ''] else '' %}
|
||||
@@ -1854,7 +1945,8 @@ action:
|
||||
| replace('{location_if_unique}', location_if_unique)
|
||||
| replace('{city}', city)
|
||||
| replace('{state}', state)
|
||||
| replace('{country}', country) %}
|
||||
| replace('{country}', country)
|
||||
| replace('{year}', year) %}
|
||||
{% set ns.items = ns.items ~ item %}
|
||||
{% endfor %}
|
||||
{{ ns.items }}
|
||||
@@ -1924,6 +2016,24 @@ action:
|
||||
caption: "{{ combined_message }}"
|
||||
disable_web_page_preview: "{{ telegram_disable_url_preview }}"
|
||||
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:
|
||||
combined_reply_to_id: "{{ telegram_combined_text_response[album_id_entities[0]].message_id | default(0) | int }}"
|
||||
@@ -1951,6 +2061,25 @@ action:
|
||||
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
|
||||
wait_for_response: false
|
||||
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
|
||||
- if:
|
||||
@@ -2087,6 +2216,7 @@ action:
|
||||
{% set raw_date = asset.created_at | default('', true) %}
|
||||
{% set dt = raw_date | as_datetime(none) if raw_date is string and raw_date | length > 0 else none %}
|
||||
{% set formatted_date = dt.strftime(date_format) if dt else '' %}
|
||||
{% set year = dt.strftime('%Y') if dt else '' %}
|
||||
{% set created_if_unique = '' if memory_unique_dates | length == 1 else (date_if_unique_template | replace('{date}', formatted_date) if formatted_date | length > 0 else '') %}
|
||||
{% set is_favorite = favorite_indicator_template if asset.is_favorite | default(false) else '' %}
|
||||
{% set rating = asset.rating | default('') | string if asset.rating not in [none, ''] else '' %}
|
||||
@@ -2117,7 +2247,8 @@ action:
|
||||
| replace('{location_if_unique}', location_if_unique)
|
||||
| replace('{city}', city)
|
||||
| replace('{state}', state)
|
||||
| replace('{country}', country) %}
|
||||
| replace('{country}', country)
|
||||
| replace('{year}', year) %}
|
||||
{% set ns.items = ns.items ~ item %}
|
||||
{% endfor %}
|
||||
{{ ns.items }}
|
||||
@@ -2188,6 +2319,24 @@ action:
|
||||
caption: "{{ memory_message }}"
|
||||
disable_web_page_preview: "{{ telegram_disable_url_preview }}"
|
||||
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
|
||||
- variables:
|
||||
@@ -2217,6 +2366,25 @@ action:
|
||||
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
|
||||
wait_for_response: false
|
||||
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
|
||||
- conditions:
|
||||
@@ -2336,6 +2504,7 @@ action:
|
||||
{% set raw_date = asset.created_at | default('', true) %}
|
||||
{% set dt = raw_date | as_datetime(none) if raw_date is string and raw_date | length > 0 else none %}
|
||||
{% set formatted_date = dt.strftime(date_format) if dt else '' %}
|
||||
{% set year = dt.strftime('%Y') if dt else '' %}
|
||||
{% set created_if_unique = '' if memory_comb_unique_dates | length == 1 else (date_if_unique_template | replace('{date}', formatted_date) if formatted_date | length > 0 else '') %}
|
||||
{% set is_favorite = favorite_indicator_template if asset.is_favorite | default(false) else '' %}
|
||||
{% set rating = asset.rating | default('') | string if asset.rating not in [none, ''] else '' %}
|
||||
@@ -2366,7 +2535,8 @@ action:
|
||||
| replace('{location_if_unique}', location_if_unique)
|
||||
| replace('{city}', city)
|
||||
| replace('{state}', state)
|
||||
| replace('{country}', country) %}
|
||||
| replace('{country}', country)
|
||||
| replace('{year}', year) %}
|
||||
{% set ns.items = ns.items ~ item %}
|
||||
{% endfor %}
|
||||
{{ ns.items }}
|
||||
@@ -2436,6 +2606,24 @@ action:
|
||||
caption: "{{ memory_comb_message }}"
|
||||
disable_web_page_preview: "{{ telegram_disable_url_preview }}"
|
||||
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:
|
||||
memory_comb_reply_to_id: "{{ telegram_memory_comb_text_response[album_id_entities[0]].message_id | default(0) | int }}"
|
||||
@@ -2463,6 +2651,25 @@ action:
|
||||
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
|
||||
wait_for_response: false
|
||||
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
|
||||
- choose:
|
||||
@@ -2502,7 +2709,6 @@ action:
|
||||
- {{ event_people | join(', ') if event_people | length > 0 else '(none)' }}
|
||||
|
||||
**Filtering:**
|
||||
- Is Hub Tracked: {{ is_hub_tracked }}
|
||||
- Is Album Tracked: {{ is_album_tracked }}
|
||||
- Should Notify: {{ should_notify }}
|
||||
- Track Images: {{ track_images }}
|
||||
@@ -2666,6 +2872,24 @@ action:
|
||||
caption: "{{ message }}"
|
||||
disable_web_page_preview: "{{ telegram_disable_url_preview }}"
|
||||
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
|
||||
@@ -2703,6 +2927,24 @@ action:
|
||||
caption: "{{ message }}"
|
||||
disable_web_page_preview: "{{ telegram_disable_url_preview }}"
|
||||
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)
|
||||
@@ -2829,6 +3071,24 @@ action:
|
||||
caption: "{{ telegram_message }}"
|
||||
disable_web_page_preview: "{{ telegram_disable_url_preview }}"
|
||||
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
|
||||
- variables:
|
||||
@@ -2871,3 +3131,23 @@ action:
|
||||
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
|
||||
wait_for_response: false
|
||||
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 }}
|
||||
|
||||
67
Common/Light Color Mapper/README.md
Normal file
67
Common/Light Color Mapper/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Light Color Mapper
|
||||
|
||||
Syncs light colors in real-time from color sensor entities. Sensors and lights are paired by list index (sensor 0 → light 0, sensor 1 → light 1, etc.).
|
||||
|
||||
## Features
|
||||
|
||||
- Real-time color sync from sensor entities to lights
|
||||
- Index-based sensor-to-light mapping (add in matching order)
|
||||
- FPS throttling for performance control (0–60 fps)
|
||||
- Brightness override or per-sensor brightness from attributes
|
||||
- Configurable behavior when sensors are unavailable (turn off, keep last, default color)
|
||||
- Control entity (switch/input_boolean) to enable/disable
|
||||
- Optional auto-off when the control entity is disabled
|
||||
- Custom post-update callback action (runs after each update cycle)
|
||||
|
||||
## How It Works
|
||||
|
||||
The automation triggers on any state change from the configured color sensors. Each sensor is paired with a light by list position:
|
||||
|
||||
| Sensor Index | Light Index |
|
||||
| --- | --- |
|
||||
| Sensor 0 | Light 0 |
|
||||
| Sensor 1 | Light 1 |
|
||||
| Sensor 2 | Light 2 |
|
||||
| ... | ... |
|
||||
|
||||
When a sensor updates, **all** sensor-light pairs are re-evaluated to keep lights in sync.
|
||||
|
||||
### FPS Throttling
|
||||
|
||||
The automation uses `mode: restart` with a trailing delay to throttle updates:
|
||||
|
||||
- **FPS = 0**: No throttle — every state change triggers an immediate update.
|
||||
- **FPS > 0**: After processing all lights, a delay of `1000 / FPS` ms is added. If a new sensor state arrives during the delay, the automation restarts with the latest data. This naturally caps updates to the configured FPS.
|
||||
|
||||
### Sensor Requirements
|
||||
|
||||
Each color sensor should expose:
|
||||
|
||||
| Attribute | Description |
|
||||
| --- | --- |
|
||||
| **State** | Hex color string (e.g., `#FF8800`) or `None` when not processing |
|
||||
| `rgb_color` | Color as `[r, g, b]` list (used to set light color) |
|
||||
| `brightness` | Brightness value 0–255 (used when no override is set) |
|
||||
|
||||
## Configuration
|
||||
|
||||
| Input | Description |
|
||||
| --- | --- |
|
||||
| **Control Entity** | Switch or input_boolean that enables/disables the automation |
|
||||
| **Turn Off Lights on Disable** | Turn off all mapped lights when the control entity is switched off (default: true) |
|
||||
| **Color Sensors** | List of color sensor entities (index-mapped to lights) |
|
||||
| **Lights** | List of light entities (index-mapped to sensors) |
|
||||
| **Maximum FPS** | Update rate limit, 0 = unlimited (default: 0) |
|
||||
| **Brightness Override** | Fixed brightness for all lights, 0 = use sensor brightness (default: 0) |
|
||||
| **When Sensor is Unavailable** | Action when sensor is None/unavailable: Turn Off, Keep Last, or Set Default Color |
|
||||
| **Default Color** | Color to apply when sensor is unavailable and action is "Set Default Color" |
|
||||
| **Post-Update Action** | Custom action to run after all lights are updated (e.g., fire an event, call a service) |
|
||||
|
||||
## Notes
|
||||
|
||||
- If the sensor and light lists have different lengths, only the shorter count of pairs is used (extra entities are ignored).
|
||||
- When the control entity is turned off and "Turn Off Lights on Disable" is enabled, only the paired lights (up to `pair_count`) are turned off.
|
||||
|
||||
## Author
|
||||
|
||||
Alexei Dolgolyov (<dolgolyov.alexei@gmail.com>)
|
||||
342
Common/Light Color Mapper/blueprint.yaml
Normal file
342
Common/Light Color Mapper/blueprint.yaml
Normal file
@@ -0,0 +1,342 @@
|
||||
# Light Color Mapper
|
||||
# Syncs light colors in real-time from color sensor entities.
|
||||
# See README.md for detailed documentation.
|
||||
#
|
||||
# Author: Alexei Dolgolyov (dolgolyov.alexei@gmail.com)
|
||||
|
||||
blueprint:
|
||||
name: "Custom: Light Color Mapper"
|
||||
description: >
|
||||
Maps color sensor states to lights in real-time.
|
||||
Sensors and lights are paired by list index (sensor 0 → light 0, etc.).
|
||||
Supports FPS throttling, brightness override, and configurable behavior
|
||||
when sensors are unavailable.
|
||||
domain: automation
|
||||
|
||||
# ===========================================================================
|
||||
# INPUT CONFIGURATION
|
||||
# ===========================================================================
|
||||
input:
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Control
|
||||
# -------------------------------------------------------------------------
|
||||
control_group:
|
||||
name: "Control"
|
||||
collapsed: false
|
||||
input:
|
||||
control_entity:
|
||||
name: Control Entity
|
||||
description: "Switch or input_boolean that enables/disables this automation."
|
||||
selector:
|
||||
entity:
|
||||
domain:
|
||||
- input_boolean
|
||||
- switch
|
||||
|
||||
turn_off_on_disable:
|
||||
name: Turn Off Lights on Disable
|
||||
description: "Turn off all mapped lights when the control entity is switched off."
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Devices
|
||||
# -------------------------------------------------------------------------
|
||||
devices_group:
|
||||
name: "Devices"
|
||||
collapsed: false
|
||||
input:
|
||||
color_sensors:
|
||||
name: Color Sensors
|
||||
description: >
|
||||
List of color sensor entities. Each sensor's state is a hex color
|
||||
string (e.g., #FF8800) with attributes: r, g, b, brightness,
|
||||
rgb_color. Sensors are mapped to lights by list index.
|
||||
selector:
|
||||
entity:
|
||||
domain: sensor
|
||||
multiple: true
|
||||
|
||||
lights:
|
||||
name: Lights
|
||||
description: >
|
||||
List of light entities. Each light is paired with the sensor at the
|
||||
same list index (sensor 0 → light 0, sensor 1 → light 1, etc.).
|
||||
selector:
|
||||
entity:
|
||||
domain: light
|
||||
multiple: true
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Settings
|
||||
# -------------------------------------------------------------------------
|
||||
settings_group:
|
||||
name: "Settings"
|
||||
collapsed: false
|
||||
input:
|
||||
max_fps:
|
||||
name: Maximum FPS
|
||||
description: >
|
||||
Maximum updates per second. With mode: restart, a trailing delay
|
||||
throttles updates naturally — new state changes during the delay
|
||||
restart the automation with fresh data. Set to 0 for unlimited.
|
||||
default: 0
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 60
|
||||
mode: slider
|
||||
|
||||
brightness_override:
|
||||
name: Brightness Override
|
||||
description: >
|
||||
Override brightness for all lights (0–255).
|
||||
Set to 0 to use each sensor's brightness attribute instead.
|
||||
default: 0
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
mode: slider
|
||||
|
||||
min_brightness:
|
||||
name: Minimum Brightness
|
||||
description: >
|
||||
Minimum brightness for all lights (1–255). If the sensor or
|
||||
override brightness is below this value, this minimum is used
|
||||
instead. Prevents lights from becoming too dim to see.
|
||||
default: 5
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 255
|
||||
mode: slider
|
||||
|
||||
none_action:
|
||||
name: When Sensor is Unavailable
|
||||
description: "Action to take when a sensor state is None, unknown, or unavailable."
|
||||
default: "turn_off"
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: "Turn Off Light"
|
||||
value: "turn_off"
|
||||
- label: "Keep Last Color"
|
||||
value: "keep_last"
|
||||
- label: "Set Default Color"
|
||||
value: "default_color"
|
||||
|
||||
default_color:
|
||||
name: Default Color
|
||||
description: >
|
||||
Color to apply when a sensor is unavailable.
|
||||
Only used when "When Sensor is Unavailable" is set to "Set Default Color".
|
||||
default: [255, 255, 255]
|
||||
selector:
|
||||
color_rgb:
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Callback Actions
|
||||
# -------------------------------------------------------------------------
|
||||
actions_group:
|
||||
name: "Callback Actions"
|
||||
collapsed: true
|
||||
input:
|
||||
post_update_action:
|
||||
name: Post-Update Action
|
||||
description: >
|
||||
Custom action to run after all sensor-light pairs have been processed.
|
||||
Runs once per update cycle, after all lights are set.
|
||||
Leave empty to do nothing.
|
||||
default: []
|
||||
selector:
|
||||
action:
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Debug
|
||||
# -------------------------------------------------------------------------
|
||||
debug:
|
||||
name: "Debug"
|
||||
collapsed: true
|
||||
input:
|
||||
enable_debug_notifications:
|
||||
name: Enable Debug Notifications
|
||||
description: >
|
||||
Send persistent notifications showing sensor states and light
|
||||
updates for troubleshooting.
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
# Restart mode ensures the latest sensor state always wins.
|
||||
# Combined with a trailing delay (FPS throttle), incoming state changes
|
||||
# during the delay cancel the current run and restart with fresh data.
|
||||
mode: restart
|
||||
|
||||
# =============================================================================
|
||||
# TRIGGERS
|
||||
# =============================================================================
|
||||
trigger:
|
||||
# Any color sensor changes state
|
||||
- platform: state
|
||||
entity_id: !input color_sensors
|
||||
id: "sensor_changed"
|
||||
|
||||
# Control entity toggled on/off
|
||||
- platform: state
|
||||
entity_id: !input control_entity
|
||||
id: "control_changed"
|
||||
|
||||
# =============================================================================
|
||||
# VARIABLES
|
||||
# =============================================================================
|
||||
variables:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input References
|
||||
# ---------------------------------------------------------------------------
|
||||
control_entity: !input control_entity
|
||||
turn_off_on_disable: !input turn_off_on_disable
|
||||
color_sensors: !input color_sensors
|
||||
lights: !input lights
|
||||
max_fps: !input max_fps
|
||||
brightness_override: !input brightness_override
|
||||
min_brightness: !input min_brightness
|
||||
none_action: !input none_action
|
||||
default_color: !input default_color
|
||||
enable_debug_notifications: !input enable_debug_notifications
|
||||
post_update_action: !input post_update_action
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Computed Values
|
||||
# ---------------------------------------------------------------------------
|
||||
# Use the shorter list length to avoid index-out-of-range errors
|
||||
pair_count: "{{ [color_sensors | length, lights | length] | min }}"
|
||||
|
||||
# =============================================================================
|
||||
# ACTIONS
|
||||
# =============================================================================
|
||||
action:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Debug Logging
|
||||
# ---------------------------------------------------------------------------
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ enable_debug_notifications }}"
|
||||
sequence:
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
title: "Light Color Mapper Debug"
|
||||
message: >
|
||||
**Trigger:** {{ trigger.id }}
|
||||
**Control:** {{ states(control_entity) }}
|
||||
**Pairs:** {{ pair_count }}
|
||||
**FPS:** {{ max_fps }}
|
||||
**Brightness Override:** {{ brightness_override }}
|
||||
**Min Brightness:** {{ min_brightness }}
|
||||
|
||||
**Sensor States:**
|
||||
{% for i in range(pair_count | int) %}
|
||||
{{ i }}: {{ color_sensors[i] }} = {{ states(color_sensors[i]) }} (rgb: {{ state_attr(color_sensors[i], 'rgb_color') }}, brightness: {{ state_attr(color_sensors[i], 'brightness') }})
|
||||
{% endfor %}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check Control Entity
|
||||
# ---------------------------------------------------------------------------
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ not is_state(control_entity, 'on') }}"
|
||||
sequence:
|
||||
# Turn off all mapped lights if configured
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ turn_off_on_disable }}"
|
||||
sequence:
|
||||
- service: light.turn_off
|
||||
target:
|
||||
entity_id: "{{ lights[:pair_count | int] }}"
|
||||
- stop: "Control entity is off"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Process Each Sensor-Light Pair
|
||||
# ---------------------------------------------------------------------------
|
||||
- repeat:
|
||||
count: "{{ pair_count | int }}"
|
||||
sequence:
|
||||
- variables:
|
||||
idx: "{{ repeat.index - 1 }}"
|
||||
current_sensor: "{{ color_sensors[idx] }}"
|
||||
current_light: "{{ lights[idx] }}"
|
||||
sensor_state: "{{ states(current_sensor) }}"
|
||||
is_unavailable: "{{ sensor_state in ['None', 'none', 'unknown', 'unavailable', 'undefined'] or sensor_state | length == 0 }}"
|
||||
|
||||
- choose:
|
||||
# Sensor is unavailable — apply configured action
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ is_unavailable }}"
|
||||
sequence:
|
||||
- choose:
|
||||
# Turn off the light
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ none_action == 'turn_off' }}"
|
||||
sequence:
|
||||
- service: light.turn_off
|
||||
target:
|
||||
entity_id: "{{ current_light }}"
|
||||
|
||||
# Set default color
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ none_action == 'default_color' }}"
|
||||
sequence:
|
||||
- service: light.turn_on
|
||||
target:
|
||||
entity_id: "{{ current_light }}"
|
||||
data:
|
||||
rgb_color: "{{ default_color }}"
|
||||
brightness: "{{ [brightness_override | int if brightness_override | int > 0 else 255, min_brightness | int] | max }}"
|
||||
|
||||
# keep_last — do nothing
|
||||
|
||||
# Sensor has valid state — apply color to light
|
||||
default:
|
||||
- variables:
|
||||
sensor_rgb: "{{ state_attr(current_sensor, 'rgb_color') | default([255, 255, 255]) }}"
|
||||
sensor_brightness: "{{ state_attr(current_sensor, 'brightness') | default(255) }}"
|
||||
target_brightness: "{{ [brightness_override | int if brightness_override | int > 0 else sensor_brightness | int, min_brightness | int] | max }}"
|
||||
|
||||
- service: light.turn_on
|
||||
target:
|
||||
entity_id: "{{ current_light }}"
|
||||
data:
|
||||
rgb_color: "{{ sensor_rgb }}"
|
||||
brightness: "{{ target_brightness }}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Post-Update Callback
|
||||
# ---------------------------------------------------------------------------
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ post_update_action | length > 0 }}"
|
||||
sequence: !input post_update_action
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FPS Throttle
|
||||
# ---------------------------------------------------------------------------
|
||||
# With mode: restart, new triggers during this delay cancel and restart
|
||||
# the automation, effectively capping updates to max_fps per second.
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ max_fps | int > 0 }}"
|
||||
sequence:
|
||||
- delay:
|
||||
milliseconds: "{{ (1000 / (max_fps | int)) | int }}"
|
||||
@@ -17,7 +17,7 @@ This blueprint creates a smart motion-activated light control system. It handles
|
||||
- Day/Night mode (different light settings based on time)
|
||||
- Scene support (activate scenes instead of light parameters)
|
||||
- Dim before off (visual warning before turning off)
|
||||
- Manual override detection (stops automation if user changes light)
|
||||
- Manual override detection (stops automation if user changes light) with configurable grace period
|
||||
- Brightness threshold (only trigger if light is dim)
|
||||
- Custom light parameters (brightness, color, etc.)
|
||||
- Callback actions for enable/disable/manual events
|
||||
@@ -37,7 +37,8 @@ The automation tracks these states via persistent storage:
|
||||
## Behavior Notes
|
||||
|
||||
- Will NOT turn on light if it's already ON (prevents hijacking user control)
|
||||
- If user changes light while automation is active, enters MANUAL mode
|
||||
- If user changes light while automation is active, enters MANUAL mode (after grace period)
|
||||
- Grace period prevents false manual overrides from delayed device state reports (e.g., Zigbee)
|
||||
- MANUAL mode exits when light is turned OFF (by any means)
|
||||
- Timeout delay only applies when turning OFF (motion cleared)
|
||||
- Time conditions support overnight windows (e.g., 22:00 to 06:00)
|
||||
|
||||
@@ -37,6 +37,7 @@ blueprint:
|
||||
- switch
|
||||
- group
|
||||
- light
|
||||
- input_boolean
|
||||
multiple: true
|
||||
|
||||
condition_switches:
|
||||
@@ -140,7 +141,7 @@ blueprint:
|
||||
description: >
|
||||
Delay before turning off the light after all motion sensors
|
||||
clear. Set to 0 for immediate turn off.
|
||||
default: 120
|
||||
default: 0
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
@@ -384,6 +385,22 @@ blueprint:
|
||||
step: 1
|
||||
unit_of_measurement: "seconds"
|
||||
|
||||
manual_override_grace_period:
|
||||
name: Manual Override Grace Period (seconds)
|
||||
description: >
|
||||
After the automation turns on a light, ignore state changes for
|
||||
this many seconds to avoid false manual override detection.
|
||||
Some devices (especially Zigbee) report delayed state updates
|
||||
that can be mistaken for manual control. Increase this value
|
||||
if you see false manual overrides in the debug log.
|
||||
default: 2
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 30
|
||||
step: 1
|
||||
unit_of_measurement: "seconds"
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Debug
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -510,12 +527,6 @@ condition: !input user_condition
|
||||
# =============================================================================
|
||||
variables:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Debug Flags
|
||||
# ---------------------------------------------------------------------------
|
||||
is_debug: false # Detailed debug for specific actions
|
||||
is_base_debug: false # Basic debug info at start
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State Machine Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -546,6 +557,7 @@ variables:
|
||||
min_on_duration: !input min_on_duration
|
||||
brightness_threshold: !input brightness_threshold
|
||||
transition_duration: !input transition_duration
|
||||
manual_override_grace_period: !input manual_override_grace_period
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Target Device Resolution
|
||||
@@ -591,11 +603,22 @@ variables:
|
||||
# Reference light for state checks (first available)
|
||||
reference_light: "{{ resolved_all_lights[0] if resolved_all_lights | length > 0 else none }}"
|
||||
|
||||
# Check if any device is on
|
||||
# Check if any device is on (respects brightness_threshold)
|
||||
any_device_on: >
|
||||
{% set lights_on = resolved_all_lights | select('is_state', 'on') | list | length > 0 %}
|
||||
{% set ns = namespace(lights_on=false) %}
|
||||
{% for light in resolved_all_lights %}
|
||||
{% if is_state(light, 'on') %}
|
||||
{% if brightness_threshold | int(0) > 0 %}
|
||||
{% if state_attr(light, 'brightness') | int(0) >= brightness_threshold | int(0) %}
|
||||
{% set ns.lights_on = true %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set ns.lights_on = true %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set switches_on = resolved_all_switches | select('is_state', 'on') | list | length > 0 %}
|
||||
{{ lights_on or switches_on }}
|
||||
{{ ns.lights_on or switches_on }}
|
||||
|
||||
all_devices_off: "{{ not any_device_on }}"
|
||||
|
||||
@@ -797,7 +820,7 @@ variables:
|
||||
# Should we disable the light? (Motion cleared OR condition switch turned off)
|
||||
must_be_disabled_preview: >
|
||||
{{ ((not all_of_condition_switches_on) or motion_all_off) | bool }}
|
||||
must_be_disabled_guard: "{{ state_is_enabled }}"
|
||||
must_be_disabled_guard: "{{ state_is_enabled or state_is_enabling }}"
|
||||
must_be_disabled: >
|
||||
{{ must_be_disabled_preview and must_be_disabled_guard }}
|
||||
|
||||
@@ -807,22 +830,61 @@ variables:
|
||||
action:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DEBUG: Log basic info (enable by setting is_base_debug: true)
|
||||
# DEBUG: Log entry state on every trigger (helps trace mode: restart issues)
|
||||
# ---------------------------------------------------------------------------
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ is_base_debug }}"
|
||||
- conditions: "{{ enable_debug_notifications }}"
|
||||
sequence:
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
title: "Debug Info - Motion Light"
|
||||
title: "Motion Light Debug - ENTRY"
|
||||
message: >
|
||||
must_be_enabled_preview: {{ must_be_enabled_preview }},
|
||||
must_be_disabled_preview: {{ must_be_disabled_preview }},
|
||||
must_be_disabled: {{ must_be_disabled }},
|
||||
must_be_disabled_guard: {{ must_be_disabled_guard }},
|
||||
trigger_id: {{ trigger.id }}
|
||||
=== Automation Triggered ===
|
||||
Time: {{ now().strftime('%H:%M:%S.%f')[:12] }}
|
||||
Trigger ID: {{ trigger_id }}
|
||||
Trigger Entity: {{ trigger.entity_id | default('N/A') }}
|
||||
Trigger From: {{ trigger.from_state.state | default('N/A') }}
|
||||
Trigger To: {{ trigger.to_state.state | default('N/A') }}
|
||||
|
||||
=== State Machine ===
|
||||
State: {{ motion_light_state }} (NONE=0, ENABLED=1, ENABLING=2, MANUAL=3)
|
||||
state_is_none: {{ state_is_none }}
|
||||
state_is_enabling: {{ state_is_enabling }}
|
||||
state_is_enabled: {{ state_is_enabled }}
|
||||
state_is_manual: {{ state_is_manual }}
|
||||
|
||||
=== Decision Variables ===
|
||||
motion_on: {{ motion_on }} ({{ count_of_enabled_sensor }} sensors)
|
||||
all_of_condition_switches_on: {{ all_of_condition_switches_on }}
|
||||
luminance_ok: {{ luminance_ok }}
|
||||
time_condition_ok: {{ time_condition_ok }}
|
||||
any_device_on: {{ any_device_on }}
|
||||
|
||||
=== Enable/Disable Logic ===
|
||||
must_be_enabled_preview: {{ must_be_enabled_preview }}
|
||||
must_be_enabled_guard: {{ must_be_enabled_guard }}
|
||||
must_be_enabled: {{ must_be_enabled }}
|
||||
must_be_disabled_preview: {{ must_be_disabled_preview }}
|
||||
must_be_disabled_guard: {{ must_be_disabled_guard }}
|
||||
must_be_disabled: {{ must_be_disabled }}
|
||||
|
||||
=== Which CASE will match ===
|
||||
CASE 1 (state changed): {{ trigger_id == 'light_state_changed' or trigger_id == 'switch_state_changed' }}
|
||||
CASE 2 (enable): {{ must_be_enabled }}
|
||||
CASE 3 (disable): {{ must_be_disabled }}
|
||||
|
||||
=== Grace Period ===
|
||||
{%- 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)) -%}
|
||||
last_action_ts: {{ last_ts | default('not set') }}
|
||||
grace_period: {{ grace }}s
|
||||
{%- if last_ts is not none -%}
|
||||
{%- set parsed = last_ts | as_datetime -%}
|
||||
{%- if parsed is not none %}
|
||||
time_since_action: {{ (now() - parsed).total_seconds() | round(2) }}s
|
||||
grace_expired: {{ (now() - parsed).total_seconds() > grace }}
|
||||
{%- endif -%}
|
||||
{%- endif %}
|
||||
|
||||
# ===========================================================================
|
||||
# MAIN STATE MACHINE
|
||||
@@ -838,6 +900,26 @@ action:
|
||||
- condition: template
|
||||
value_template: "{{ trigger_id == 'light_state_changed' or trigger_id == 'switch_state_changed' }}"
|
||||
sequence:
|
||||
# Debug: log which CASE 1 sub-case will fire
|
||||
- choose:
|
||||
- conditions: "{{ enable_debug_notifications }}"
|
||||
sequence:
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
title: "Motion Light Debug - CASE 1"
|
||||
message: >
|
||||
CASE 1: light/switch state changed
|
||||
Time: {{ now().strftime('%H:%M:%S.%f')[:12] }}
|
||||
Trigger: {{ trigger_id }}
|
||||
Entity: {{ trigger.entity_id | default('N/A') }}
|
||||
From: {{ trigger.from_state.state | default('N/A') }} → To: {{ trigger.to_state.state | default('N/A') }}
|
||||
Meaningful change: {{ trigger.from_state.state != trigger.to_state.state }}
|
||||
Light brightness: {{ state_attr(reference_light, 'brightness') | default('N/A') }}
|
||||
State: {{ motion_light_state }}
|
||||
state_is_enabling: {{ state_is_enabling }}
|
||||
state_is_enabled: {{ state_is_enabled }}
|
||||
must_be_disabled_preview: {{ must_be_disabled_preview }}
|
||||
|
||||
- choose:
|
||||
|
||||
# ----- Sub-case: Light/Switch turned OFF -----
|
||||
@@ -845,12 +927,13 @@ action:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: >
|
||||
{# BUG FIX: Changed from 'res = false' to 'res = true' for AND logic #}
|
||||
{# Check actual on/off state only — do NOT use brightness_threshold here.
|
||||
Brightness threshold is for the enable guard (any_device_on), not for
|
||||
detecting whether the light was actually turned off. During transitions,
|
||||
brightness may temporarily be below threshold while the light is still on. #}
|
||||
{% set res = true %}
|
||||
{% if light_entity is not none %}
|
||||
{% set brightness = state_attr(light_entity, 'brightness') | int(0) %}
|
||||
{% set light_off = is_state(light_entity, 'off') or brightness < brightness_threshold %}
|
||||
{% set res = res and light_off %}
|
||||
{% set res = res and is_state(light_entity, 'off') %}
|
||||
{% endif %}
|
||||
{% if switch_entity is not none %}
|
||||
{% set res = res and is_state(switch_entity, 'off') %}
|
||||
@@ -868,25 +951,237 @@ action:
|
||||
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_none })) %}
|
||||
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
|
||||
|
||||
# Re-evaluate: if an external source turned off the light while
|
||||
# the automation was actively controlling it, wait briefly and
|
||||
# re-enable if conditions are still met.
|
||||
# Safety: state_is_enabled/state_is_enabling reflect the state at
|
||||
# run start (before reset). When CASE 3 turns off the light, it
|
||||
# sets state to NONE first, so on restart state_is_none=true and
|
||||
# this block is skipped — preventing unwanted re-enable.
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ state_is_enabled or state_is_enabling }}"
|
||||
sequence:
|
||||
# Wait for the off-transition to finish
|
||||
- delay:
|
||||
seconds: "{{ transition_duration }}"
|
||||
|
||||
# Fresh condition check (evaluates current entity states)
|
||||
- condition: template
|
||||
value_template: >
|
||||
{% set e = sensors if sensors is iterable else [sensors] %}
|
||||
{% set motion_active = e | select('is_state', 'on') | list | length > 0 %}
|
||||
{% set cond_ok = true %}
|
||||
{% set cs = condition_switches if condition_switches is iterable else [condition_switches] %}
|
||||
{% if cs | length > 0 %}
|
||||
{% set cond_ok = (cs | select('is_state', 'on') | list | length) == (cs | length) %}
|
||||
{% endif %}
|
||||
{{ motion_active and cond_ok }}
|
||||
|
||||
# Re-read state from input_text (may have changed during delay)
|
||||
- variables:
|
||||
re_eval_state_global: >
|
||||
{% set text = states(automation_state_entity) | string %}
|
||||
{% if text in ['unknown','unavailable','none',''] %}
|
||||
{{ dict() }}
|
||||
{% else %}
|
||||
{{ text | from_json }}
|
||||
{% endif %}
|
||||
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) }}"
|
||||
|
||||
# Only proceed if state is still NONE (no other run claimed it)
|
||||
- condition: template
|
||||
value_template: "{{ (re_eval_motion_light_state | string) == automation_state_none }}"
|
||||
|
||||
# --- Re-enable path (mirrors CASE 2) ---
|
||||
|
||||
# Set state to ENABLING before turning on
|
||||
- service: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ automation_state_entity }}"
|
||||
data:
|
||||
value: >
|
||||
{% set new_automation_state = (re_eval_state | combine({
|
||||
state_motion_light_state: automation_state_enabling,
|
||||
state_motion_light_last_action_timestamp: now(),
|
||||
state_motion_light_last_brightness: 0
|
||||
})) %}
|
||||
{{ re_eval_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
|
||||
|
||||
# Scene or light activation
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ use_scene_instead and effective_scene is not none }}"
|
||||
sequence:
|
||||
- service: scene.turn_on
|
||||
target:
|
||||
entity_id: "{{ effective_scene }}"
|
||||
data:
|
||||
transition: "{{ transition_duration }}"
|
||||
|
||||
default:
|
||||
# Turn ON lights
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ resolved_all_lights | length > 0 }}"
|
||||
sequence:
|
||||
- service: light.turn_on
|
||||
target:
|
||||
entity_id: "{{ resolved_all_lights }}"
|
||||
data: >
|
||||
{% set d = effective_light_data if effective_light_data else {} %}
|
||||
{% if transition_duration > 0 %}
|
||||
{% set d = d | combine({'transition': transition_duration}) %}
|
||||
{% endif %}
|
||||
{{ d }}
|
||||
|
||||
# Turn ON switches
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ resolved_all_switches | length > 0 }}"
|
||||
sequence:
|
||||
- service: switch.turn_on
|
||||
target:
|
||||
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 -----
|
||||
# Transition from ENABLING to ENABLED
|
||||
# Transition from ENABLING to ENABLED, or disable immediately
|
||||
# if motion already cleared during the ENABLING phase
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ state_is_enabling }}"
|
||||
sequence:
|
||||
- service: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ automation_state_entity }}"
|
||||
data:
|
||||
value: >
|
||||
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_enabled })) %}
|
||||
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
|
||||
- choose:
|
||||
# If disable conditions are already met (motion cleared
|
||||
# while light was still in ENABLING state), skip ENABLED
|
||||
# and go straight to disable
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ must_be_disabled_preview }}"
|
||||
sequence:
|
||||
# Reset state to NONE
|
||||
- service: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ automation_state_entity }}"
|
||||
data:
|
||||
value: >
|
||||
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_none })) %}
|
||||
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
|
||||
|
||||
# Turn OFF or restore lights
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ resolved_all_lights | length > 0 }}"
|
||||
sequence:
|
||||
- variables:
|
||||
last_brightness: "{{ automation_state.get(state_motion_light_last_brightness, 0) | int }}"
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ last_brightness > 0 }}"
|
||||
sequence:
|
||||
- service: light.turn_on
|
||||
target:
|
||||
entity_id: "{{ resolved_all_lights }}"
|
||||
data:
|
||||
brightness: "{{ last_brightness }}"
|
||||
transition: "{{ transition_duration }}"
|
||||
default:
|
||||
- service: light.turn_off
|
||||
target:
|
||||
entity_id: "{{ resolved_all_lights }}"
|
||||
data:
|
||||
transition: "{{ transition_duration }}"
|
||||
|
||||
# Turn OFF switches
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ resolved_all_switches | length > 0 }}"
|
||||
sequence:
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: "{{ resolved_all_switches }}"
|
||||
|
||||
# Execute disable callback
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ disable_action != [] }}"
|
||||
sequence: !input disable_action
|
||||
|
||||
# Debug notification
|
||||
- choose:
|
||||
- conditions: "{{ enable_debug_notifications }}"
|
||||
sequence:
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
title: "Motion Light Debug"
|
||||
message: >
|
||||
Action: DISABLE (enabling interrupted)
|
||||
Time: {{ now().strftime('%H:%M:%S') }}
|
||||
Trigger: {{ trigger_id }}
|
||||
|
||||
# Normal case: motion still active, transition to ENABLED
|
||||
default:
|
||||
- service: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ automation_state_entity }}"
|
||||
data:
|
||||
value: >
|
||||
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_enabled })) %}
|
||||
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
|
||||
|
||||
# ----- Sub-case: User manually changed the light -----
|
||||
# Transition from ENABLED to MANUAL (user took control)
|
||||
# Only triggers on meaningful state changes (on→off or off→on),
|
||||
# NOT on attribute-only updates (on→on) which Zigbee devices
|
||||
# commonly send as the light settles after a transition.
|
||||
# Grace period: ignore state changes shortly after the automation
|
||||
# turns on the light to avoid false manual override detection.
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ state_is_enabled }}"
|
||||
value_template: >
|
||||
{% set meaningful_change = trigger.from_state.state != trigger.to_state.state %}
|
||||
{% if not meaningful_change %}
|
||||
{{ false }}
|
||||
{% else %}
|
||||
{% 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)) %}
|
||||
{% if state_is_enabled and last_ts is not none %}
|
||||
{% set parsed = last_ts | as_datetime %}
|
||||
{{ parsed is none or (now() - parsed).total_seconds() > grace }}
|
||||
{% else %}
|
||||
{{ state_is_enabled }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
sequence:
|
||||
# BUG FIX: Fixed YAML structure - was 'data: >' instead of 'data:' with 'value: >'
|
||||
- service: input_text.set_value
|
||||
@@ -921,6 +1216,81 @@ action:
|
||||
New State: MANUAL
|
||||
Trigger: {{ trigger_id }}
|
||||
|
||||
# ----- Default: No sub-case matched -----
|
||||
# This handles the case where a light_state_changed trigger fires
|
||||
# during the grace period (e.g., Zigbee delayed state reports) while
|
||||
# the disable path was already in progress but got cancelled by
|
||||
# mode: restart. If disable conditions are met, turn off directly.
|
||||
default:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ (state_is_enabled or state_is_enabling) and must_be_disabled_preview }}"
|
||||
sequence:
|
||||
# Reset state to NONE first (before turn-off triggers another restart)
|
||||
- service: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ automation_state_entity }}"
|
||||
data:
|
||||
value: >
|
||||
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_none })) %}
|
||||
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
|
||||
|
||||
# Turn OFF or restore lights
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ resolved_all_lights | length > 0 }}"
|
||||
sequence:
|
||||
- variables:
|
||||
last_brightness: "{{ automation_state.get(state_motion_light_last_brightness, 0) | int }}"
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ last_brightness > 0 }}"
|
||||
sequence:
|
||||
- service: light.turn_on
|
||||
target:
|
||||
entity_id: "{{ resolved_all_lights }}"
|
||||
data:
|
||||
brightness: "{{ last_brightness }}"
|
||||
transition: "{{ transition_duration }}"
|
||||
default:
|
||||
- service: light.turn_off
|
||||
target:
|
||||
entity_id: "{{ resolved_all_lights }}"
|
||||
data:
|
||||
transition: "{{ transition_duration }}"
|
||||
|
||||
# Turn OFF switches
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ resolved_all_switches | length > 0 }}"
|
||||
sequence:
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: "{{ resolved_all_switches }}"
|
||||
|
||||
# Execute disable callback
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ disable_action != [] }}"
|
||||
sequence: !input disable_action
|
||||
|
||||
# Debug notification
|
||||
- choose:
|
||||
- conditions: "{{ enable_debug_notifications }}"
|
||||
sequence:
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
title: "Motion Light Debug"
|
||||
message: >
|
||||
Action: DISABLE (restart recovery)
|
||||
Time: {{ now().strftime('%H:%M:%S') }}
|
||||
Trigger: {{ trigger_id }}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# CASE 2: Enable Path (Motion Detected, Should Turn On)
|
||||
# -----------------------------------------------------------------------
|
||||
@@ -948,6 +1318,21 @@ action:
|
||||
{{ state_attr(reference_light, 'brightness') | int(0) }}
|
||||
{% endif %}
|
||||
|
||||
# Update state to ENABLING BEFORE turning on the light.
|
||||
# This must happen first because mode: restart may cancel
|
||||
# subsequent steps if the light state change fires immediately.
|
||||
- service: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ automation_state_entity }}"
|
||||
data:
|
||||
value: >
|
||||
{% set new_automation_state = (automation_state | combine({
|
||||
state_motion_light_state: automation_state_enabling,
|
||||
state_motion_light_last_action_timestamp: date_time_now,
|
||||
state_motion_light_last_brightness: last_brightness
|
||||
})) %}
|
||||
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
|
||||
|
||||
# Scene activation path
|
||||
- choose:
|
||||
- conditions:
|
||||
@@ -988,19 +1373,6 @@ action:
|
||||
target:
|
||||
entity_id: "{{ resolved_all_switches }}"
|
||||
|
||||
# Update state to ENABLING (waiting for light state change confirmation)
|
||||
- service: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ automation_state_entity }}"
|
||||
data:
|
||||
value: >
|
||||
{% set new_automation_state = (automation_state | combine({
|
||||
state_motion_light_state: automation_state_enabling,
|
||||
state_motion_light_last_action_timestamp: date_time_now,
|
||||
state_motion_light_last_brightness: last_brightness
|
||||
})) %}
|
||||
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
|
||||
|
||||
# Execute enable callback action
|
||||
- choose:
|
||||
- conditions:
|
||||
@@ -1074,15 +1446,49 @@ action:
|
||||
- delay:
|
||||
seconds: "{{ dim_duration }}"
|
||||
|
||||
# Read last_brightness before resetting state
|
||||
- variables:
|
||||
last_brightness: "{{ automation_state.get(state_motion_light_last_brightness, 0) | int }}"
|
||||
|
||||
# Update state to NONE BEFORE turning off the light.
|
||||
# This must happen first because mode: restart may cancel
|
||||
# subsequent steps if the light state change fires during
|
||||
# turn_off transition, which could falsely trigger manual override.
|
||||
- service: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ automation_state_entity }}"
|
||||
data:
|
||||
value: >
|
||||
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_none })) %}
|
||||
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
|
||||
|
||||
# Execute disable callback action (before turn-off to avoid restart cancellation)
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ disable_action != [] }}"
|
||||
sequence: !input disable_action
|
||||
|
||||
# Debug notification (before turn-off to avoid restart cancellation)
|
||||
- choose:
|
||||
- conditions: "{{ enable_debug_notifications }}"
|
||||
sequence:
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
title: "Motion Light Debug"
|
||||
message: >
|
||||
Action: DISABLE
|
||||
Time: {{ now().strftime('%H:%M:%S') }}
|
||||
Timeout: {{ timeout }}s
|
||||
Min On Duration: {{ min_on_duration }}s
|
||||
Dim Before Off: {{ enable_dim_before_off }}
|
||||
|
||||
# Turn OFF or restore the lights
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ resolved_all_lights | length > 0 }}"
|
||||
sequence:
|
||||
- variables:
|
||||
last_brightness: "{{ automation_state.get(state_motion_light_last_brightness, 0) | int }}"
|
||||
|
||||
- choose:
|
||||
# Restore previous brightness if it was set
|
||||
- conditions:
|
||||
@@ -1113,33 +1519,3 @@ action:
|
||||
- service: switch.turn_off
|
||||
target:
|
||||
entity_id: "{{ resolved_all_switches }}"
|
||||
|
||||
# Update state to NONE (ready for next motion event)
|
||||
- service: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ automation_state_entity }}"
|
||||
data:
|
||||
value: >
|
||||
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_none })) %}
|
||||
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
|
||||
|
||||
# Execute disable callback action
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ disable_action != [] }}"
|
||||
sequence: !input disable_action
|
||||
|
||||
# Debug notification
|
||||
- choose:
|
||||
- conditions: "{{ enable_debug_notifications }}"
|
||||
sequence:
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
title: "Motion Light Debug"
|
||||
message: >
|
||||
Action: DISABLE
|
||||
Time: {{ now().strftime('%H:%M:%S') }}
|
||||
Timeout: {{ timeout }}s
|
||||
Min On Duration: {{ min_on_duration }}s
|
||||
Dim Before Off: {{ enable_dim_before_off }}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.0.0"
|
||||
"version": "2.5.2"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user