Compare commits

4 Commits

Author SHA1 Message Date
e98df855d9 Fix false manual override detection and brightness threshold guard in Motion Light
- Add configurable grace period (default 10s) to prevent false manual
  override detection from delayed device state reports (e.g., Zigbee)
- Fix any_device_on guard to respect brightness_threshold so lights
  below the threshold don't block the automation from triggering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:28:16 +03:00
29b1044d63 Add Light Color Mapper blueprint
Maps color sensor states to lights in real-time with index-based pairing.
Supports FPS throttling, brightness override, configurable behavior when
sensors are unavailable, and a post-update callback action.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:28:07 +03:00
006f9e532c Add {year} variable to asset templates in Immich Album Watcher
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:27:59 +03:00
9383f957b6 Add Dreame Vacuum Notifications blueprint
Sends customizable notifications for Dreame vacuum events (cleaning
status, consumable alerts, warnings, errors, and informational messages).
Requires the Dreame Vacuum integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:27:51 +03:00
9 changed files with 973 additions and 14 deletions

View 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>)

View File

@@ -0,0 +1,419 @@
# 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
- condition: template
value_template: "{{ event_entity_id == 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 }}"

View File

@@ -107,6 +107,7 @@ These variables can be used in the image and video asset templates. Also used fo
| `{type}` | Asset type (IMAGE or VIDEO) | | `{type}` | Asset type (IMAGE or VIDEO) |
| `{created}` | Creation date/time (always shown) | | `{created}` | Creation date/time (always shown) |
| `{created_if_unique}` | Creation date/time formatted with template if dates differ, empty if all same | | `{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 | | `{owner}` | Owner display name |
| `{url}` | Public URL to view the asset (empty if no shared link) | | `{url}` | Public URL to view the asset (empty if no shared link) |
| `{download_url}` | Direct download URL for the original file | | `{download_url}` | Direct download URL for the original file |

View File

@@ -236,7 +236,7 @@ blueprint:
name: "Image Asset Template" name: "Image Asset Template"
description: > description: >
Template for IMAGE assets in the list. Also used for Telegram media captions. 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}" default: "\n • 🖼️ {filename}"
selector: selector:
text: text:
@@ -246,7 +246,7 @@ blueprint:
name: "Video Asset Template" name: "Video Asset Template"
description: > description: >
Template for VIDEO assets in the list. Also used for Telegram media captions. 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}" default: "\n • 🎬 {filename}"
selector: selector:
text: text:
@@ -1160,6 +1160,7 @@ variables:
{% set raw_date = asset.created_at | default('', true) %} {% 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 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 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 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 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 '' %} {% set rating = asset.rating | default('') | string if asset.rating not in [none, ''] else '' %}
@@ -1190,7 +1191,8 @@ variables:
| replace('{location_if_unique}', location_if_unique) | replace('{location_if_unique}', location_if_unique)
| replace('{city}', city) | replace('{city}', city)
| replace('{state}', state) | replace('{state}', state)
| replace('{country}', country) %} | replace('{country}', country)
| replace('{year}', year) %}
{% set ns.items = ns.items ~ item %} {% set ns.items = ns.items ~ item %}
{% endfor %} {% endfor %}
{% set more_count = sorted_assets | length - max_items %} {% set more_count = sorted_assets | length - max_items %}
@@ -1565,6 +1567,7 @@ action:
{% set raw_date = asset.created_at | default('', true) %} {% 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 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 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 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 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 '' %} {% set rating = asset.rating | default('') | string if asset.rating not in [none, ''] else '' %}
@@ -1595,7 +1598,8 @@ action:
| replace('{location_if_unique}', location_if_unique) | replace('{location_if_unique}', location_if_unique)
| replace('{city}', city) | replace('{city}', city)
| replace('{state}', state) | replace('{state}', state)
| replace('{country}', country) %} | replace('{country}', country)
| replace('{year}', year) %}
{% set ns.items = ns.items ~ item %} {% set ns.items = ns.items ~ item %}
{% endfor %} {% endfor %}
{{ ns.items }} {{ ns.items }}
@@ -1824,6 +1828,7 @@ action:
{% set raw_date = asset.created_at | default('', true) %} {% 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 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 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 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 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 '' %} {% set rating = asset.rating | default('') | string if asset.rating not in [none, ''] else '' %}
@@ -1854,7 +1859,8 @@ action:
| replace('{location_if_unique}', location_if_unique) | replace('{location_if_unique}', location_if_unique)
| replace('{city}', city) | replace('{city}', city)
| replace('{state}', state) | replace('{state}', state)
| replace('{country}', country) %} | replace('{country}', country)
| replace('{year}', year) %}
{% set ns.items = ns.items ~ item %} {% set ns.items = ns.items ~ item %}
{% endfor %} {% endfor %}
{{ ns.items }} {{ ns.items }}
@@ -2087,6 +2093,7 @@ action:
{% set raw_date = asset.created_at | default('', true) %} {% 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 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 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 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 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 '' %} {% set rating = asset.rating | default('') | string if asset.rating not in [none, ''] else '' %}
@@ -2117,7 +2124,8 @@ action:
| replace('{location_if_unique}', location_if_unique) | replace('{location_if_unique}', location_if_unique)
| replace('{city}', city) | replace('{city}', city)
| replace('{state}', state) | replace('{state}', state)
| replace('{country}', country) %} | replace('{country}', country)
| replace('{year}', year) %}
{% set ns.items = ns.items ~ item %} {% set ns.items = ns.items ~ item %}
{% endfor %} {% endfor %}
{{ ns.items }} {{ ns.items }}
@@ -2336,6 +2344,7 @@ action:
{% set raw_date = asset.created_at | default('', true) %} {% 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 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 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 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 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 '' %} {% set rating = asset.rating | default('') | string if asset.rating not in [none, ''] else '' %}
@@ -2366,7 +2375,8 @@ action:
| replace('{location_if_unique}', location_if_unique) | replace('{location_if_unique}', location_if_unique)
| replace('{city}', city) | replace('{city}', city)
| replace('{state}', state) | replace('{state}', state)
| replace('{country}', country) %} | replace('{country}', country)
| replace('{year}', year) %}
{% set ns.items = ns.items ~ item %} {% set ns.items = ns.items ~ item %}
{% endfor %} {% endfor %}
{{ ns.items }} {{ ns.items }}

View 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 (060 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 0255 (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>)

View File

@@ -0,0 +1,327 @@
# 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 (0255).
Set to 0 to use each sensor's brightness attribute instead.
default: 0
selector:
number:
min: 0
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
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 }}
**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 }}"
# 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 }}"
- 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 }}"

View File

@@ -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) - Day/Night mode (different light settings based on time)
- Scene support (activate scenes instead of light parameters) - Scene support (activate scenes instead of light parameters)
- Dim before off (visual warning before turning off) - 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) - Brightness threshold (only trigger if light is dim)
- Custom light parameters (brightness, color, etc.) - Custom light parameters (brightness, color, etc.)
- Callback actions for enable/disable/manual events - Callback actions for enable/disable/manual events
@@ -37,7 +37,8 @@ The automation tracks these states via persistent storage:
## Behavior Notes ## Behavior Notes
- Will NOT turn on light if it's already ON (prevents hijacking user control) - Will NOT turn on light if it's already ON (prevents hijacking user control)
- If user changes 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) - MANUAL mode exits when light is turned OFF (by any means)
- Timeout delay only applies when turning OFF (motion cleared) - Timeout delay only applies when turning OFF (motion cleared)
- Time conditions support overnight windows (e.g., 22:00 to 06:00) - Time conditions support overnight windows (e.g., 22:00 to 06:00)

View File

@@ -37,6 +37,7 @@ blueprint:
- switch - switch
- group - group
- light - light
- input_boolean
multiple: true multiple: true
condition_switches: condition_switches:
@@ -384,6 +385,22 @@ blueprint:
step: 1 step: 1
unit_of_measurement: "seconds" 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: 10
selector:
number:
min: 0
max: 30
step: 1
unit_of_measurement: "seconds"
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Debug # Debug
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -546,6 +563,7 @@ variables:
min_on_duration: !input min_on_duration min_on_duration: !input min_on_duration
brightness_threshold: !input brightness_threshold brightness_threshold: !input brightness_threshold
transition_duration: !input transition_duration transition_duration: !input transition_duration
manual_override_grace_period: !input manual_override_grace_period
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Target Device Resolution # Target Device Resolution
@@ -591,11 +609,22 @@ variables:
# Reference light for state checks (first available) # Reference light for state checks (first available)
reference_light: "{{ resolved_all_lights[0] if resolved_all_lights | length > 0 else none }}" 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: > 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 %} {% 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 }}" all_devices_off: "{{ not any_device_on }}"
@@ -884,9 +913,20 @@ action:
# ----- Sub-case: User manually changed the light ----- # ----- Sub-case: User manually changed the light -----
# Transition from ENABLED to MANUAL (user took control) # Transition from ENABLED to MANUAL (user took control)
# Grace period: ignore state changes shortly after the automation
# turns on the light to avoid false manual override detection.
# Some devices (especially Zigbee) report delayed state updates.
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ state_is_enabled }}" value_template: >
{% set last_ts = automation_state.get(state_motion_light_last_action_timestamp, none) %}
{% set grace = (transition_duration | float(0)) + (manual_override_grace_period | float(10)) %}
{% 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 %}
sequence: sequence:
# BUG FIX: Fixed YAML structure - was 'data: >' instead of 'data:' with 'value: >' # BUG FIX: Fixed YAML structure - was 'data: >' instead of 'data:' with 'value: >'
- service: input_text.set_value - service: input_text.set_value

View File

@@ -1,3 +1,3 @@
{ {
"version": "2.0.0" "version": "2.2.3"
} }