Compare commits

..

6 Commits

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 00:42:26 +03:00
a6ac4f4257 Add debug logging input and skip redundant on/off commands in Climate Device Controller
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:42:02 +03:00
584c970044 Make power_sensor optional in Climate Device Controller
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:27:48 +03:00
4c03bc849e Several minor updates to the existing blueprints 2026-02-28 22:10:11 +03:00
eefc8993e3 Add min_brightness setting to Light Color Mapper
Adds a minimum brightness floor (default: 5) that prevents lights from
becoming too dim. Applied to both sensor brightness and default color paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 00:51:03 +03:00
9d19dfa8d3 Fix multiple mode:restart state machine bugs in Motion Light
- Fix brightness threshold falsely resetting state to NONE during light
  transition ramp-up (brightness temporarily below threshold != light off)
- Fix false manual override from Zigbee attribute-only updates (on→on)
  by requiring meaningful state change (on→off or off→on)
- Fix disable guard to also accept ENABLING state (not just ENABLED)
- Move state updates before service calls to survive mode:restart cancellation
- Add ENABLING sub-case handler for motion-cleared-during-enable scenario
- Add CASE 1 default handler for restart recovery disable path
- Add comprehensive debug logging at automation entry and CASE 1 entry
- Change default timeout_delay to 0 and grace_period to 2s
- Remove unused is_debug/is_base_debug variables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 01:57:45 +03:00
7 changed files with 824 additions and 144 deletions

View File

@@ -237,8 +237,11 @@ blueprint:
collapsed: false collapsed: false
input: input:
power_sensor: power_sensor:
name: Power Sensor name: Power Sensor (optional)
description: "Sensor reporting device power consumption (W)" description: >
Sensor reporting device power consumption (W).
Leave empty to disable power monitoring.
default:
selector: selector:
entity: entity:
domain: sensor domain: sensor
@@ -277,6 +280,22 @@ blueprint:
entity: entity:
domain: input_boolean 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 # AUTOMATION MODE
# ============================================================================= # =============================================================================
@@ -321,13 +340,6 @@ trigger:
- platform: state - platform: state
entity_id: !input env_sensors 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 # CONDITIONS
# ============================================================================= # =============================================================================
@@ -349,6 +361,11 @@ action:
force_on_entity: !input force_on_entity force_on_entity: !input force_on_entity
hysteresis_window: !input hysteresis_window hysteresis_window: !input hysteresis_window
# -----------------------------------------------------------------------
# Device State
# -----------------------------------------------------------------------
is_device_on: "{{ states(device_entity) not in ['off', 'unavailable', 'unknown'] }}"
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Force ON Check # Force ON Check
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
@@ -406,13 +423,13 @@ action:
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Power Monitoring # Power Monitoring
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
power_threshold: !input power_threshold
power_sensor: !input power_sensor power_sensor: !input power_sensor
power_threshold: !input power_threshold
power_problematic_indicator_entity: !input power_problematic_indicator_entity power_problematic_indicator_entity: !input power_problematic_indicator_entity
power_decay_duration: !input power_decay_duration 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 # 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 # Window/Door State
@@ -472,7 +489,7 @@ action:
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Debug Flag # Debug Flag
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
is_debug: false is_debug: !input enable_debug_notifications
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# DEBUG: Log current state # DEBUG: Log current state
@@ -482,18 +499,29 @@ action:
sequence: sequence:
- service: persistent_notification.create - service: persistent_notification.create
data: data:
title: "Climate Controller (debug)" title: "Climate Device Controller Debug"
message: > message: >
is_force_on = {{ is_force_on }}, **Device State:**
room_closed = {{ room_closed }}, - Device: {{ states(device_entity) }} (on: {{ is_device_on }})
house_closed = {{ house_closed }}, - Control Switch: {{ states(control_switch) }}
is_value_below_threshold = {{ is_value_below_threshold }}, - Force ON: {{ is_force_on }}
is_value_below_turn_on_threshold = {{ is_value_below_turn_on_threshold }}, - Schedule Active: {{ schedule_active }}
is_value_at_or_above_target = {{ is_value_at_or_above_target }},
target_value = {{ target_value }}, **Environment:**
turn_on_threshold = {{ turn_on_threshold }}, - Target Value: {{ target_value }}
schedule_active = {{ schedule_active }}, - Turn-On Threshold: {{ turn_on_threshold }} (hysteresis: {{ hysteresis_window }})
control_switch = {{ states(control_switch) }} - 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 # POWER MONITORING: Flag device if malfunctioning
@@ -503,7 +531,7 @@ action:
- choose: - choose:
- conditions: - conditions:
- condition: template - 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: sequence:
- variables: - variables:
# Check if enough time has passed since last power reading # Check if enough time has passed since last power reading
@@ -562,7 +590,11 @@ action:
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ is_force_on }}" 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) # PRIORITY 2: Emergency Override (Safety)
@@ -571,7 +603,11 @@ action:
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ is_value_below_threshold }}" 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 # PRIORITY 3: Control Switch Off
@@ -580,7 +616,11 @@ action:
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ is_state(control_switch, 'off') }}" 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 # PRIORITY 4: Environment Not Ready
@@ -589,7 +629,11 @@ action:
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ not (house_closed or room_closed) or not schedule_active }}" 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 # PRIORITY 5: Target Reached
@@ -598,7 +642,11 @@ action:
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ is_value_at_or_above_target }}" 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 # PRIORITY 6: Below Turn-On Threshold
@@ -607,7 +655,11 @@ action:
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ is_value_below_turn_on_threshold }}" 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) # DEFAULT: Maintain Current State (Hysteresis Zone)

View File

@@ -275,9 +275,14 @@ variables:
# CONDITIONS # CONDITIONS
# ============================================================================= # =============================================================================
condition: condition:
# Only process events from the configured vacuum entity # 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 - condition: template
value_template: "{{ event_entity_id == vacuum_entity }}" value_template: >
{{ event_entity_id == vacuum_entity
or event_entity_id.startswith(vacuum_entity ~ '_') }}
# ============================================================================= # =============================================================================
# ACTIONS # ACTIONS

View File

@@ -4,8 +4,7 @@ This blueprint monitors Immich album changes and sends notifications when assets
## Features ## Features
- Filter by hub/instance name (for multi-hub setups) - Monitor specific albums by ID (whitelist)
- Monitor specific albums by name (whitelist)
- Filter by asset type (track images only, videos only, or both) - Filter by asset type (track images only, videos only, or both)
- Filter by favorites only (only notify about favorite assets) - Filter by favorites only (only notify about favorite assets)
- Sort assets by date, rating, name, or keep original order - Sort assets by date, rating, name, or keep original order
@@ -147,6 +146,7 @@ Select input_text entities containing Telegram chat IDs. Can be user IDs (positi
- Large media lists are automatically split into multiple groups (2-10 items per group) - Large media lists are automatically split into multiple groups (2-10 items per group)
- Optional chat action indicator (typing, uploading photo/video) while processing - Optional chat action indicator (typing, uploading photo/video) while processing
- Optional maximum asset size filter to skip large files - Optional maximum asset size filter to skip large files
- Respects integration quiet hours — notifications are queued and sent when quiet hours end (configurable bypass per blueprint instance)
### Limitations ### Limitations
@@ -159,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. 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. 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 ### Summary Message Template Variables

View File

@@ -19,20 +19,9 @@ blueprint:
# Hub & Album Configuration # Hub & Album Configuration
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
albums_group: albums_group:
name: "Hub & Albums" name: "Albums"
collapsed: false collapsed: false
input: 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: album_id_entities:
name: Album ID Entities name: Album ID Entities
description: > description: >
@@ -472,6 +461,23 @@ blueprint:
- label: "Uploading Document..." - label: "Uploading Document..."
value: "upload_document" value: "upload_document"
telegram_quiet_hours_start:
name: Quiet Hours Start
description: >
Start time for quiet hours. During quiet hours, Telegram notifications
are queued and sent when quiet hours end.
default: "23:00"
selector:
time:
telegram_quiet_hours_end:
name: Quiet Hours End
description: >
End time for quiet hours. Queued notifications are sent after this time.
default: "07:00"
selector:
time:
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Periodic Summary # Periodic Summary
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -489,6 +495,28 @@ blueprint:
selector: selector:
boolean: 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: periodic_notification_times:
name: Summary Notification Times name: Summary Notification Times
description: > description: >
@@ -886,7 +914,7 @@ variables:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Input Variables # Input Variables
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
hub_names: !input hub_names
album_id_entities: !input album_id_entities album_id_entities: !input album_id_entities
automation_id: !input automation_id automation_id: !input automation_id
@@ -926,9 +954,13 @@ variables:
telegram_disable_url_preview: !input telegram_disable_url_preview telegram_disable_url_preview: !input telegram_disable_url_preview
telegram_chat_action: !input telegram_chat_action telegram_chat_action: !input telegram_chat_action
telegram_max_asset_size: !input telegram_max_asset_size telegram_max_asset_size: !input telegram_max_asset_size
telegram_quiet_hours_start: !input telegram_quiet_hours_start
telegram_quiet_hours_end: !input telegram_quiet_hours_end
# Periodic Summary Settings # Periodic Summary Settings
enable_periodic_summary: !input enable_periodic_summary enable_periodic_summary: !input enable_periodic_summary
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_summary_message_template: !input periodic_summary_message
periodic_album_template: !input periodic_album_template periodic_album_template: !input periodic_album_template
periodic_summary_image_url: !input periodic_summary_image_url periodic_summary_image_url: !input periodic_summary_image_url
@@ -1033,10 +1065,6 @@ variables:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Computed Values # 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) # Check if this album should be tracked (empty list = track all)
is_album_tracked: > is_album_tracked: >
{{ album_ids | length == 0 or event_album_id in album_ids }} {{ album_ids | length == 0 or event_album_id in album_ids }}
@@ -1249,13 +1277,15 @@ variables:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Periodic Summary Variables # Periodic Summary Variables
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Check if periodic summary should run (at configured times) # Check if periodic summary should run (at configured times and interval)
# Manual test event (immich_album_watcher_test_periodic_summary) bypasses time check # Manual test event (immich_album_watcher_test_periodic_summary) bypasses interval check
# If event data contains automation_id, only matching automations respond # If event data contains automation_id, only matching automations respond
should_send_periodic_summary: > should_send_periodic_summary: >
{% if enable_periodic_summary %} {% if enable_periodic_summary %}
{% if trigger.id == 'periodic_summary_timer' %} {% 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' %} {% 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('') %} {% set event_automation_id = trigger.event.data.automation_id | default('') %}
{% if event_automation_id | length > 0 %} {% if event_automation_id | length > 0 %}
@@ -1384,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'] %} {% 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 }} {{ should_send_periodic_summary or should_send_scheduled_assets or should_send_memory_mode }}
{% else %} {% else %}
{{ is_hub_tracked and is_album_tracked and should_notify }} {{ is_album_tracked and should_notify }}
{% endif %} {% endif %}
# ============================================================================= # =============================================================================
@@ -1426,6 +1456,25 @@ action:
assets: "{{ [{'url': periodic_summary_image_url, 'type': 'photo'}] if periodic_summary_image_url | length > 0 else [] }}" assets: "{{ [{'url': periodic_summary_image_url, 'type': 'photo'}] if periodic_summary_image_url | length > 0 else [] }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Periodic Summary - Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ periodic_summary_formatted[:200] }}...
- Has Image: {{ 'Yes' if periodic_summary_image_url | length > 0 else 'No' }}
- Response: {{ telegram_periodic_response }}
# Delay between periodic summary and scheduled assets if both trigger at the same hour # Delay between periodic summary and scheduled assets if both trigger at the same hour
- if: - if:
@@ -1670,6 +1719,24 @@ action:
caption: "{{ scheduled_message }}" caption: "{{ scheduled_message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Scheduled Per-Album - Text Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ scheduled_message[:200] }}...
- Response: {{ telegram_scheduled_text_response }}
# Extract message ID for reply # Extract message ID for reply
- variables: - variables:
@@ -1699,6 +1766,25 @@ action:
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}" max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
wait_for_response: false wait_for_response: false
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log media send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Scheduled Per-Album - Media Send:**
- Chat ID: {{ repeat.item }}
- Assets: {{ scheduled_media_urls | length }}
- Reply To: {{ scheduled_reply_to_id }}
- Response: {{ telegram_scheduled_media_response }}
# Combined Mode: Fetch from all albums and combine into one notification # Combined Mode: Fetch from all albums and combine into one notification
# Distributes the limit evenly across albums (e.g., limit=10 with 2 albums = 5 each) # Distributes the limit evenly across albums (e.g., limit=10 with 2 albums = 5 each)
@@ -1930,6 +2016,24 @@ action:
caption: "{{ combined_message }}" caption: "{{ combined_message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Scheduled Combined - Text Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ combined_message[:200] }}...
- Response: {{ telegram_combined_text_response }}
- variables: - variables:
combined_reply_to_id: "{{ telegram_combined_text_response[album_id_entities[0]].message_id | default(0) | int }}" combined_reply_to_id: "{{ telegram_combined_text_response[album_id_entities[0]].message_id | default(0) | int }}"
@@ -1957,6 +2061,25 @@ action:
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}" max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
wait_for_response: false wait_for_response: false
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log media send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Scheduled Combined - Media Send:**
- Chat ID: {{ repeat.item }}
- Assets: {{ combined_media_urls | length }}
- Reply To: {{ combined_reply_to_id }}
- Response: {{ telegram_combined_media_response }}
# Delay before memory mode if another scheduled notification was sent at the same hour # Delay before memory mode if another scheduled notification was sent at the same hour
- if: - if:
@@ -2196,6 +2319,24 @@ action:
caption: "{{ memory_message }}" caption: "{{ memory_message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Memory Per-Album - Text Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ memory_message[:200] }}...
- Response: {{ telegram_memory_text_response }}
# Extract message ID for reply # Extract message ID for reply
- variables: - variables:
@@ -2225,6 +2366,25 @@ action:
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}" max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
wait_for_response: false wait_for_response: false
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log media send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Memory Per-Album - Media Send:**
- Chat ID: {{ repeat.item }}
- Assets: {{ memory_media_urls | length }}
- Reply To: {{ memory_reply_to_id }}
- Response: {{ telegram_memory_media_response }}
# Combined Mode: Fetch from all albums and combine into one notification # Combined Mode: Fetch from all albums and combine into one notification
- conditions: - conditions:
@@ -2446,6 +2606,24 @@ action:
caption: "{{ memory_comb_message }}" caption: "{{ memory_comb_message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Memory Combined - Text Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ memory_comb_message[:200] }}...
- Response: {{ telegram_memory_comb_text_response }}
- variables: - variables:
memory_comb_reply_to_id: "{{ telegram_memory_comb_text_response[album_id_entities[0]].message_id | default(0) | int }}" memory_comb_reply_to_id: "{{ telegram_memory_comb_text_response[album_id_entities[0]].message_id | default(0) | int }}"
@@ -2473,6 +2651,25 @@ action:
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}" max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
wait_for_response: false wait_for_response: false
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log media send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Memory Combined - Media Send:**
- Chat ID: {{ repeat.item }}
- Assets: {{ memory_comb_media_urls | length }}
- Reply To: {{ memory_comb_reply_to_id }}
- Response: {{ telegram_memory_comb_media_response }}
# Stop here if this was a scheduled trigger - don't continue to event-based actions # Stop here if this was a scheduled trigger - don't continue to event-based actions
- choose: - choose:
@@ -2512,7 +2709,6 @@ action:
- {{ event_people | join(', ') if event_people | length > 0 else '(none)' }} - {{ event_people | join(', ') if event_people | length > 0 else '(none)' }}
**Filtering:** **Filtering:**
- Is Hub Tracked: {{ is_hub_tracked }}
- Is Album Tracked: {{ is_album_tracked }} - Is Album Tracked: {{ is_album_tracked }}
- Should Notify: {{ should_notify }} - Should Notify: {{ should_notify }}
- Track Images: {{ track_images }} - Track Images: {{ track_images }}
@@ -2676,6 +2872,24 @@ action:
caption: "{{ message }}" caption: "{{ message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Album Renamed - Text Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ message[:200] }}...
- Response: {{ telegram_renamed_response }}
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# CASE 5: Album Deleted # CASE 5: Album Deleted
@@ -2713,6 +2927,24 @@ action:
caption: "{{ message }}" caption: "{{ message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Album Deleted - Text Send:**
- Chat ID: {{ repeat.item }}
- Caption: {{ message[:200] }}...
- Response: {{ telegram_deleted_response }}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Send Media to Telegram (if enabled) # Send Media to Telegram (if enabled)
@@ -2839,6 +3071,24 @@ action:
caption: "{{ telegram_message }}" caption: "{{ telegram_message }}"
disable_web_page_preview: "{{ telegram_disable_url_preview }}" disable_web_page_preview: "{{ telegram_disable_url_preview }}"
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log text send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Assets Added - Text Send:**
- Chat ID: {{ current_chat_id }}
- Caption: {{ telegram_message[:200] }}...
- Response: {{ telegram_text_response }}
# Extract message ID for replies # Extract message ID for replies
- variables: - variables:
@@ -2881,3 +3131,23 @@ action:
max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}" max_asset_data_size: "{{ telegram_max_asset_size | int * 1048576 }}"
wait_for_response: false wait_for_response: false
chat_action: "{{ telegram_chat_action }}" chat_action: "{{ telegram_chat_action }}"
quiet_hours_start: "{{ telegram_quiet_hours_start }}"
quiet_hours_end: "{{ telegram_quiet_hours_end }}"
# Debug: Log media send result
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Immich Album Watcher - Telegram Send Debug"
message: >
**Assets Added - Media Send:**
- Chat ID: {{ current_chat_id }}
- Assets: {{ media_urls | length }}
- Reply To: {{ reply_to_message_id }}
- Max Group Size: {{ max_media_per_group }}
- Response: {{ telegram_media_response }}

View File

@@ -101,6 +101,19 @@ blueprint:
max: 255 max: 255
mode: slider mode: slider
min_brightness:
name: Minimum Brightness
description: >
Minimum brightness for all lights (1255). 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: none_action:
name: When Sensor is Unavailable name: When Sensor is Unavailable
description: "Action to take when a sensor state is None, unknown, or unavailable." description: "Action to take when a sensor state is None, unknown, or unavailable."
@@ -189,6 +202,7 @@ variables:
lights: !input lights lights: !input lights
max_fps: !input max_fps max_fps: !input max_fps
brightness_override: !input brightness_override brightness_override: !input brightness_override
min_brightness: !input min_brightness
none_action: !input none_action none_action: !input none_action
default_color: !input default_color default_color: !input default_color
enable_debug_notifications: !input enable_debug_notifications enable_debug_notifications: !input enable_debug_notifications
@@ -222,6 +236,7 @@ action:
**Pairs:** {{ pair_count }} **Pairs:** {{ pair_count }}
**FPS:** {{ max_fps }} **FPS:** {{ max_fps }}
**Brightness Override:** {{ brightness_override }} **Brightness Override:** {{ brightness_override }}
**Min Brightness:** {{ min_brightness }}
**Sensor States:** **Sensor States:**
{% for i in range(pair_count | int) %} {% for i in range(pair_count | int) %}
@@ -286,7 +301,7 @@ action:
entity_id: "{{ current_light }}" entity_id: "{{ current_light }}"
data: data:
rgb_color: "{{ default_color }}" rgb_color: "{{ default_color }}"
brightness: "{{ brightness_override | int if brightness_override | int > 0 else 255 }}" brightness: "{{ [brightness_override | int if brightness_override | int > 0 else 255, min_brightness | int] | max }}"
# keep_last — do nothing # keep_last — do nothing
@@ -295,7 +310,7 @@ action:
- variables: - variables:
sensor_rgb: "{{ state_attr(current_sensor, 'rgb_color') | default([255, 255, 255]) }}" sensor_rgb: "{{ state_attr(current_sensor, 'rgb_color') | default([255, 255, 255]) }}"
sensor_brightness: "{{ state_attr(current_sensor, 'brightness') | default(255) }}" sensor_brightness: "{{ state_attr(current_sensor, 'brightness') | default(255) }}"
target_brightness: "{{ brightness_override | int if brightness_override | int > 0 else sensor_brightness | int }}" target_brightness: "{{ [brightness_override | int if brightness_override | int > 0 else sensor_brightness | int, min_brightness | int] | max }}"
- service: light.turn_on - service: light.turn_on
target: target:

View File

@@ -141,7 +141,7 @@ blueprint:
description: > description: >
Delay before turning off the light after all motion sensors Delay before turning off the light after all motion sensors
clear. Set to 0 for immediate turn off. clear. Set to 0 for immediate turn off.
default: 120 default: 0
selector: selector:
number: number:
min: 0 min: 0
@@ -393,7 +393,7 @@ blueprint:
Some devices (especially Zigbee) report delayed state updates Some devices (especially Zigbee) report delayed state updates
that can be mistaken for manual control. Increase this value that can be mistaken for manual control. Increase this value
if you see false manual overrides in the debug log. if you see false manual overrides in the debug log.
default: 10 default: 2
selector: selector:
number: number:
min: 0 min: 0
@@ -527,12 +527,6 @@ condition: !input user_condition
# ============================================================================= # =============================================================================
variables: variables:
# ---------------------------------------------------------------------------
# Debug Flags
# ---------------------------------------------------------------------------
is_debug: false # Detailed debug for specific actions
is_base_debug: false # Basic debug info at start
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# State Machine Constants # State Machine Constants
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -826,7 +820,7 @@ variables:
# Should we disable the light? (Motion cleared OR condition switch turned off) # Should we disable the light? (Motion cleared OR condition switch turned off)
must_be_disabled_preview: > must_be_disabled_preview: >
{{ ((not all_of_condition_switches_on) or motion_all_off) | bool }} {{ ((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: >
{{ must_be_disabled_preview and must_be_disabled_guard }} {{ must_be_disabled_preview and must_be_disabled_guard }}
@@ -836,22 +830,61 @@ variables:
action: 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: - choose:
- conditions: - conditions: "{{ enable_debug_notifications }}"
- condition: template
value_template: "{{ is_base_debug }}"
sequence: sequence:
- service: persistent_notification.create - service: persistent_notification.create
data: data:
title: "Debug Info - Motion Light" title: "Motion Light Debug - ENTRY"
message: > message: >
must_be_enabled_preview: {{ must_be_enabled_preview }}, === Automation Triggered ===
must_be_disabled_preview: {{ must_be_disabled_preview }}, Time: {{ now().strftime('%H:%M:%S.%f')[:12] }}
must_be_disabled: {{ must_be_disabled }}, Trigger ID: {{ trigger_id }}
must_be_disabled_guard: {{ must_be_disabled_guard }}, Trigger Entity: {{ trigger.entity_id | default('N/A') }}
trigger_id: {{ trigger.id }} 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 # MAIN STATE MACHINE
@@ -867,6 +900,26 @@ action:
- condition: template - condition: template
value_template: "{{ trigger_id == 'light_state_changed' or trigger_id == 'switch_state_changed' }}" value_template: "{{ trigger_id == 'light_state_changed' or trigger_id == 'switch_state_changed' }}"
sequence: 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: - choose:
# ----- Sub-case: Light/Switch turned OFF ----- # ----- Sub-case: Light/Switch turned OFF -----
@@ -874,12 +927,13 @@ action:
- conditions: - conditions:
- condition: template - condition: template
value_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 %} {% set res = true %}
{% if light_entity is not none %} {% if light_entity is not none %}
{% set brightness = state_attr(light_entity, 'brightness') | int(0) %} {% set res = res and is_state(light_entity, 'off') %}
{% set light_off = is_state(light_entity, 'off') or brightness < brightness_threshold %}
{% set res = res and light_off %}
{% endif %} {% endif %}
{% if switch_entity is not none %} {% if switch_entity is not none %}
{% set res = res and is_state(switch_entity, 'off') %} {% set res = res and is_state(switch_entity, 'off') %}
@@ -897,35 +951,236 @@ action:
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_none })) %} {% 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 }} {{ 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 ----- # ----- 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: - conditions:
- condition: template - condition: template
value_template: "{{ state_is_enabling }}" value_template: "{{ state_is_enabling }}"
sequence: sequence:
- service: input_text.set_value - choose:
target: # If disable conditions are already met (motion cleared
entity_id: "{{ automation_state_entity }}" # while light was still in ENABLING state), skip ENABLED
data: # and go straight to disable
value: > - conditions:
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_enabled })) %} - condition: template
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} 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 ----- # ----- Sub-case: User manually changed the light -----
# Transition from ENABLED to MANUAL (user took control) # 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 # Grace period: ignore state changes shortly after the automation
# turns on the light to avoid false manual override detection. # 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: > value_template: >
{% set last_ts = automation_state.get(state_motion_light_last_action_timestamp, none) %} {% set meaningful_change = trigger.from_state.state != trigger.to_state.state %}
{% set grace = (transition_duration | float(0)) + (manual_override_grace_period | float(10)) %} {% if not meaningful_change %}
{% if state_is_enabled and last_ts is not none %} {{ false }}
{% set parsed = last_ts | as_datetime %}
{{ parsed is none or (now() - parsed).total_seconds() > grace }}
{% else %} {% else %}
{{ state_is_enabled }} {% 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 %} {% 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: >'
@@ -961,6 +1216,81 @@ action:
New State: MANUAL New State: MANUAL
Trigger: {{ trigger_id }} 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) # CASE 2: Enable Path (Motion Detected, Should Turn On)
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
@@ -988,6 +1318,21 @@ action:
{{ state_attr(reference_light, 'brightness') | int(0) }} {{ state_attr(reference_light, 'brightness') | int(0) }}
{% endif %} {% 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 # Scene activation path
- choose: - choose:
- conditions: - conditions:
@@ -1028,19 +1373,6 @@ action:
target: target:
entity_id: "{{ resolved_all_switches }}" 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 # Execute enable callback action
- choose: - choose:
- conditions: - conditions:
@@ -1114,15 +1446,49 @@ action:
- delay: - delay:
seconds: "{{ dim_duration }}" 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 # Turn OFF or restore the lights
- choose: - choose:
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ resolved_all_lights | length > 0 }}" value_template: "{{ resolved_all_lights | length > 0 }}"
sequence: sequence:
- variables:
last_brightness: "{{ automation_state.get(state_motion_light_last_brightness, 0) | int }}"
- choose: - choose:
# Restore previous brightness if it was set # Restore previous brightness if it was set
- conditions: - conditions:
@@ -1153,33 +1519,3 @@ action:
- service: switch.turn_off - service: switch.turn_off
target: target:
entity_id: "{{ resolved_all_switches }}" 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 }}

View File

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