6 Commits

Author SHA1 Message Date
24f89e57b8 Replace deprecated target with chat_id in Telegram bot service calls
Fixes deprecation warning for HA 2026.9.0 where telegram_bot.send_message
`target` parameter is being removed in favor of `chat_id`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 00:46:17 +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
9 changed files with 566 additions and 147 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
@@ -159,6 +158,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: >
@@ -489,6 +478,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 +897,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
@@ -929,6 +940,8 @@ variables:
# 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 +1046,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 +1258,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 +1395,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 %}
# ============================================================================= # =============================================================================
@@ -2512,7 +2523,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 }}

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,12 +951,206 @@ 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:
- choose:
# If disable conditions are already met (motion cleared
# while light was still in ENABLING state), skip ENABLED
# and go straight to disable
- conditions:
- condition: template
value_template: "{{ must_be_disabled_preview }}"
sequence:
# Reset state to NONE
- service: input_text.set_value
target:
entity_id: "{{ automation_state_entity }}"
data:
value: >
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_none })) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Turn OFF or restore lights
- choose:
- conditions:
- condition: template
value_template: "{{ resolved_all_lights | length > 0 }}"
sequence:
- variables:
last_brightness: "{{ automation_state.get(state_motion_light_last_brightness, 0) | int }}"
- choose:
- conditions:
- condition: template
value_template: "{{ last_brightness > 0 }}"
sequence:
- service: light.turn_on
target:
entity_id: "{{ resolved_all_lights }}"
data:
brightness: "{{ last_brightness }}"
transition: "{{ transition_duration }}"
default:
- service: light.turn_off
target:
entity_id: "{{ resolved_all_lights }}"
data:
transition: "{{ transition_duration }}"
# Turn OFF switches
- choose:
- conditions:
- condition: template
value_template: "{{ resolved_all_switches | length > 0 }}"
sequence:
- service: switch.turn_off
target:
entity_id: "{{ resolved_all_switches }}"
# Execute disable callback
- choose:
- conditions:
- condition: template
value_template: "{{ disable_action != [] }}"
sequence: !input disable_action
# Debug notification
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Motion Light Debug"
message: >
Action: DISABLE (enabling interrupted)
Time: {{ now().strftime('%H:%M:%S') }}
Trigger: {{ trigger_id }}
# Normal case: motion still active, transition to ENABLED
default:
- service: input_text.set_value - service: input_text.set_value
target: target:
entity_id: "{{ automation_state_entity }}" entity_id: "{{ automation_state_entity }}"
@@ -913,20 +1161,27 @@ 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)
# 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 meaningful_change = trigger.from_state.state != trigger.to_state.state %}
{% if not meaningful_change %}
{{ false }}
{% else %}
{% set last_ts = automation_state.get(state_motion_light_last_action_timestamp, none) %} {% set last_ts = automation_state.get(state_motion_light_last_action_timestamp, none) %}
{% set grace = (transition_duration | float(0)) + (manual_override_grace_period | float(10)) %} {% set grace = (transition_duration | float(0)) + (manual_override_grace_period | float(2)) %}
{% if state_is_enabled and last_ts is not none %} {% if state_is_enabled and last_ts is not none %}
{% set parsed = last_ts | as_datetime %} {% set parsed = last_ts | as_datetime %}
{{ parsed is none or (now() - parsed).total_seconds() > grace }} {{ parsed is none or (now() - parsed).total_seconds() > grace }}
{% else %} {% else %}
{{ state_is_enabled }} {{ 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: >'
- service: input_text.set_value - service: input_text.set_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

@@ -242,5 +242,5 @@ action:
sequence: sequence:
- service: telegram_bot.send_message - service: telegram_bot.send_message
data: data:
target: "{{ chat_id }}" chat_id: "{{ chat_id }}"
message: "{{ reply_message }}" message: "{{ reply_message }}"

View File

@@ -286,7 +286,7 @@ action:
sequence: sequence:
- service: telegram_bot.send_message - service: telegram_bot.send_message
data: data:
target: "{{ chat_id }}" chat_id: "{{ chat_id }}"
message: "{{ answers[button_index] }}" message: "{{ answers[button_index] }}"
# Reply to original message unless we're deleting it # Reply to original message unless we're deleting it
reply_to_message_id: > reply_to_message_id: >
@@ -397,7 +397,7 @@ action:
# Send the message with keyboard # Send the message with keyboard
- service: telegram_bot.send_message - service: telegram_bot.send_message
data: data:
target: "{{ result_chat_ids }}" chat_id: "{{ result_chat_ids }}"
message: "{{ message_text }}" message: "{{ message_text }}"
inline_keyboard: "{{ inline_keyboard }}" inline_keyboard: "{{ inline_keyboard }}"
config_entry_id: > config_entry_id: >

View File

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