feat: harden Motion Light manual-override and turn-off recovery

- Add motion-off debounce to filter PIR drop-outs
- Treat pre-emptive manual turn-on (from idle) as MANUAL mode
- Recover from external turn-off while motion still active
- Run enable/disable callbacks before turn-on/off for mode:restart safety
- Document brightness-threshold interaction and input_text max_length
This commit is contained in:
2026-05-27 13:09:50 +03:00
parent a52cffa062
commit 34cf5b1f7a
2 changed files with 211 additions and 113 deletions
+22 -12
View File
@@ -7,10 +7,10 @@ This blueprint creates a smart motion-activated light control system. It handles
- Multiple motion sensor support (triggers on ANY sensor) - Multiple motion sensor support (triggers on ANY sensor)
- Condition switches (ALL must be ON for automation to work) - Condition switches (ALL must be ON for automation to work)
- Multiple lights and/or switches control - Multiple lights and/or switches control
- Light groups and area-based targeting - Light groups and area-based targeting (native area picker)
- Configurable timeout delay before turning off - Configurable timeout delay before turning off
- Minimum on duration (prevents rapid on/off cycling) - Minimum on duration (prevents rapid on/off cycling)
- Motion sensor debounce (filter false triggers) - Motion sensor on/off debounce (filter false triggers and PIR drop-outs)
- Smooth light transitions with configurable duration - Smooth light transitions with configurable duration
- Luminance sensor support (only trigger in dark conditions) - Luminance sensor support (only trigger in dark conditions)
- Time-based conditions (only active during specified hours) - Time-based conditions (only active during specified hours)
@@ -36,19 +36,29 @@ The automation tracks these states via persistent storage:
## Behavior Notes ## Behavior Notes
- Will NOT turn on light if it's already ON (prevents hijacking user control) - Will NOT turn on light if it's already ON (prevents hijacking user control).
- If user changes light while automation is active, enters MANUAL mode (after grace period) - If user **changes** the light while automation is active (e.g., dim, color, scene), enters MANUAL mode (after grace period).
- Grace period prevents false manual overrides from delayed device state reports (e.g., Zigbee) - If the user **turns the light ON** while the automation is idle (pre-emptive manual control), it enters MANUAL mode and fires the `manual_action` callback. The `disable_action` callback is **not** run in this case (nothing was enabled to disable).
- MANUAL mode exits when light is turned OFF (by any means) - If the light is **turned off** by any external source while motion is still active, the automation re-enables it (external turn-off recovery). To intentionally exit the automation, also turn off the corresponding **condition switch** — this routes through the disable path and respects the configured timeouts.
- Timeout delay only applies when turning OFF (motion cleared) - Grace period prevents false manual overrides from delayed device state reports (e.g., Zigbee) — including stray reports that can follow a turn-off.
- Time conditions support overnight windows (e.g., 22:00 to 06:00) - MANUAL mode exits when light is turned OFF (by any means).
- Day/Night mode uses separate time window from time conditions - Timeout delay only applies when turning OFF (motion cleared).
- Time conditions support overnight windows (e.g., 22:00 to 06:00).
- Day/Night mode uses separate time window from time conditions.
### Brightness threshold interaction
`brightness_threshold` is checked only when the automation decides whether to **enable**. A light that is on but dimmer than the threshold is treated as "available to take over" — so the automation will run with that light. When the disable path runs, it will turn the light **off** (it does not restore a below-threshold dim state).
### Callback ordering and `mode: restart`
Light state changes feed back into the automation, which uses `mode: restart` to handle rapid bursts. To make sure callbacks aren't silently dropped when the automation restarts mid-action, both `enable_action` and `disable_action` run **before** the corresponding turn-on/turn-off command.
## Requirements ## Requirements
- At least one motion sensor - At least one motion sensor.
- `input_text` entity for persistent state storage - `input_text` entity for persistent state storage. Each automation instance must have its own entity. Increase the entity's `max_length` (default 100) to **at least 255** to leave room for the JSON state — short values can cause silent truncation.
- Target light(s), switch(es), group, or area to control - Target light(s), switch(es), group, or area to control.
## Author ## Author
+189 -101
View File
@@ -92,15 +92,15 @@ blueprint:
target_area: target_area:
name: Target Area (optional) name: Target Area (optional)
description: > description: >
Area ID (e.g., "living_room"). All lights and switches in the Pick an area. All lights and switches in the area will be
area will be discovered and controlled. discovered and controlled.
default: "" default:
selector: selector:
text: area: {}
target_light_data: target_light_data:
name: Light Data Dictionary (optional) name: Light Data Dictionary (optional)
default: "" default: {}
description: > description: >
Provide a YAML dictionary of light.turn_on parameters. Provide a YAML dictionary of light.turn_on parameters.
If not specified, the light's last settings are preserved. If not specified, the light's last settings are preserved.
@@ -289,14 +289,14 @@ blueprint:
day_light_data: day_light_data:
name: Day Light Settings name: Day Light Settings
description: Light parameters during day mode (YAML dictionary) description: Light parameters during day mode (YAML dictionary)
default: "" default: {}
selector: selector:
object: {} object: {}
night_light_data: night_light_data:
name: Night Light Settings name: Night Light Settings
description: Light parameters during night mode (YAML dictionary) description: Light parameters during night mode (YAML dictionary)
default: "" default: {}
selector: selector:
object: {} object: {}
@@ -385,6 +385,20 @@ blueprint:
step: 1 step: 1
unit_of_measurement: "seconds" unit_of_measurement: "seconds"
motion_off_debounce:
name: Motion Off Debounce (seconds)
description: >
Motion sensor must report 'off' for this duration before the
disable path starts evaluating. Filters out brief sensor
drop-outs (common with PIR sensors). Set to 0 to disable.
default: 0
selector:
number:
min: 0
max: 30
step: 1
unit_of_measurement: "seconds"
manual_override_grace_period: manual_override_grace_period:
name: Manual Override Grace Period (seconds) name: Manual Override Grace Period (seconds)
description: > description: >
@@ -455,7 +469,9 @@ blueprint:
manual_action: manual_action:
name: Manual callback action (optional) name: Manual callback action (optional)
description: > description: >
Runs when user manually changes the light while automation is active. Runs when the user takes manual control of the light: either by
changing it while the automation is active, or by turning it ON
while the automation is idle (pre-emptive manual control).
Requires 'Automation state entity' to be configured. Requires 'Automation state entity' to be configured.
default: [] default: []
selector: selector:
@@ -479,10 +495,12 @@ trigger:
seconds: !input motion_debounce seconds: !input motion_debounce
id: "motion_sensor_on" id: "motion_sensor_on"
# Motion sensors OFF # Motion sensors OFF (with debounce to filter PIR drop-outs)
- platform: state - platform: state
entity_id: !input motion_sensors entity_id: !input motion_sensors
to: "off" to: "off"
for:
seconds: !input motion_off_debounce
id: "motion_sensor_off" id: "motion_sensor_off"
# Condition switches ON/OFF # Condition switches ON/OFF
@@ -499,20 +517,22 @@ trigger:
entity_id: !input target_switches entity_id: !input target_switches
id: "switch_state_changed" id: "switch_state_changed"
# Luminance sensor value changed # Luminance dropped below threshold (re-evaluate, may now enable)
# Template triggers fire on false→true transition, so this only fires when
# the sensor *crosses* the threshold downward — exactly when we care.
- platform: template - platform: template
value_template: > value_template: >
{% if luminance_sensor %} {% if luminance_sensor %}
{{ states(luminance_sensor) not in ['unknown','unavailable'] }} {{ states(luminance_sensor) | float(99999) < (luminance_threshold | float(50)) }}
{% else %} {% else %}
false false
{% endif %} {% endif %}
# Luminance enable switch changed # Luminance enable switch turned ON (re-evaluate)
- platform: template - platform: template
value_template: > value_template: >
{% if luminance_enable_switch %} {% if luminance_enable_switch %}
{{ states(luminance_enable_switch) not in ['unknown','unavailable'] }} {{ is_state(luminance_enable_switch, 'on') }}
{% else %} {% else %}
false false
{% endif %} {% endif %}
@@ -531,7 +551,6 @@ variables:
# State Machine Constants # State Machine Constants
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# These define the possible automation states stored in persistent storage # These define the possible automation states stored in persistent storage
automation_state_invalid: '-1' # Error state
automation_state_none: '0' # Idle, waiting for motion automation_state_none: '0' # Idle, waiting for motion
automation_state_enabled: '1' # Light is ON and controlled by automation automation_state_enabled: '1' # Light is ON and controlled by automation
automation_state_enabling: '2' # Turn-on command sent, awaiting confirmation automation_state_enabling: '2' # Turn-on command sent, awaiting confirmation
@@ -576,7 +595,7 @@ variables:
{% if target_light_group is not none %} {% if target_light_group is not none %}
{% set result = result + [target_light_group] %} {% set result = result + [target_light_group] %}
{% endif %} {% endif %}
{% if target_area != '' %} {% if target_area is not none and target_area != '' %}
{% set area_lights = area_entities(target_area) | select('match', '^light\\.') | list %} {% set area_lights = area_entities(target_area) | select('match', '^light\\.') | list %}
{% set result = result + area_lights %} {% set result = result + area_lights %}
{% endif %} {% endif %}
@@ -594,7 +613,7 @@ variables:
{% if target_switches | length > 0 %} {% if target_switches | length > 0 %}
{% set result = result + target_switches %} {% set result = result + target_switches %}
{% endif %} {% endif %}
{% if target_area != '' %} {% if target_area is not none and target_area != '' %}
{% set area_switches = area_entities(target_area) | select('match', '^switch\\.') | list %} {% set area_switches = area_entities(target_area) | select('match', '^switch\\.') | list %}
{% set result = result + area_switches %} {% set result = result + area_switches %}
{% endif %} {% endif %}
@@ -631,13 +650,16 @@ variables:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
automation_state_entity: !input automation_state_entity automation_state_entity: !input automation_state_entity
# Parse global state JSON from input_text entity # Parse global state JSON from input_text entity.
# Tolerant of empty/unknown/unavailable values and corrupt JSON
# (truncation can occur if input_text max_length is too short).
automation_state_global: > automation_state_global: >
{% set text = states(automation_state_entity) | string %} {% set text = states(automation_state_entity) | string %}
{% if text in ['unknown','unavailable','none',''] %} {% if text in ['unknown','unavailable','none',''] %}
{{ dict() }} {{ dict() }}
{% else %} {% else %}
{{ text | from_json }} {% set parsed = text | from_json(default=dict()) %}
{{ parsed if parsed is mapping else dict() }}
{% endif %} {% endif %}
automation_state_placeholder_key: !input automation_state_placeholder_key automation_state_placeholder_key: !input automation_state_placeholder_key
@@ -655,20 +677,11 @@ variables:
{% endif %} {% endif %}
# Get this automation's state from global state # Get this automation's state from global state
# BUG FIX: Changed from 'light_entity != ""' to proper none check
automation_state: "{{ automation_state_global.get(automation_state_key, dict()) if (light_entity is not none or switch_entity is not none) else dict() }}" automation_state: "{{ automation_state_global.get(automation_state_key, dict()) if (light_entity is not none or switch_entity is not none) else dict() }}"
# Current state machine state # Current state machine state
motion_light_state: "{{ automation_state.get(state_motion_light_state, automation_state_none) }}" motion_light_state: "{{ automation_state.get(state_motion_light_state, automation_state_none) }}"
# Track last action timestamp
motion_light_last_action_timestamp: >
{% if trigger_id == 'state_motion' %}
{{ date_time_now }}
{% else %}
{{ automation_state.get(state_motion_light_last_action_timestamp, none) }}
{% endif %}
# State machine state checks (for readability) # State machine state checks (for readability)
state_is_none: "{{ (motion_light_state | string) == automation_state_none }}" state_is_none: "{{ (motion_light_state | string) == automation_state_none }}"
state_is_enabled: "{{ (motion_light_state | string) == automation_state_enabled }}" state_is_enabled: "{{ (motion_light_state | string) == automation_state_enabled }}"
@@ -927,28 +940,31 @@ action:
- conditions: - conditions:
- condition: template - condition: template
value_template: > value_template: >
{# Check actual on/off state only — do NOT use brightness_threshold here. {# Check actual on/off state across ALL resolved devices — do NOT use
Brightness threshold is for the enable guard (any_device_on), not for brightness_threshold here. Threshold is for the enable guard
detecting whether the light was actually turned off. During transitions, (any_device_on), not for detecting actual off. During transitions,
brightness may temporarily be below threshold while the light is still on. #} brightness may temporarily dip below threshold while still on. #}
{% set res = true %} {% set has_devices = (resolved_all_lights | length > 0) or (resolved_all_switches | length > 0) %}
{% if light_entity is not none %} {% set lights_on = resolved_all_lights | select('is_state', 'on') | list | length > 0 %}
{% set res = res and is_state(light_entity, 'off') %} {% set switches_on = resolved_all_switches | select('is_state', 'on') | list | length > 0 %}
{% endif %} {{ has_devices and not lights_on and not switches_on }}
{% if switch_entity is not none %}
{% set res = res and is_state(switch_entity, 'off') %}
{% endif %}
{# Only true if we have at least one device and all are off #}
{{ res and (light_entity is not none or switch_entity is not none) }}
sequence: sequence:
# Reset state to NONE # Reset state to NONE and stamp the action timestamp.
# Every turn-off (automation OR user) passes through here, so
# stamping now starts the grace window that protects the
# pre-emptive manual-ON detection (NONE -> MANUAL) below from
# stray off->on device reports that can follow a turn-off
# (common with Zigbee bulbs + transitions).
- service: input_text.set_value - service: input_text.set_value
target: target:
entity_id: "{{ automation_state_entity }}" entity_id: "{{ automation_state_entity }}"
data: data:
value: > value: >
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_none })) %} {% set new_automation_state = (automation_state | combine({
state_motion_light_state: automation_state_none,
state_motion_light_last_action_timestamp: date_time_now
})) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Re-evaluate: if an external source turned off the light while # Re-evaluate: if an external source turned off the light while
@@ -986,7 +1002,8 @@ action:
{% if text in ['unknown','unavailable','none',''] %} {% if text in ['unknown','unavailable','none',''] %}
{{ dict() }} {{ dict() }}
{% else %} {% else %}
{{ text | from_json }} {% set parsed = text | from_json(default=dict()) %}
{{ parsed if parsed is mapping else dict() }}
{% endif %} {% endif %}
re_eval_state: "{{ re_eval_state_global.get(automation_state_key, dict()) }}" re_eval_state: "{{ re_eval_state_global.get(automation_state_key, dict()) }}"
re_eval_motion_light_state: "{{ re_eval_state.get(state_motion_light_state, automation_state_none) }}" re_eval_motion_light_state: "{{ re_eval_state.get(state_motion_light_state, automation_state_none) }}"
@@ -997,7 +1014,18 @@ action:
# --- Re-enable path (mirrors CASE 2) --- # --- Re-enable path (mirrors CASE 2) ---
# Set state to ENABLING before turning on # Guard: scene mode without resolved scene
- choose:
- conditions:
- condition: template
value_template: "{{ use_scene_instead and effective_scene is none }}"
sequence:
- stop: "Scene mode enabled but no scene configured for current mode"
# Set state to ENABLING before turning on.
# last_brightness=0 is correct here: this path runs after the
# light was turned off externally, so there's no meaningful
# brightness to capture.
- service: input_text.set_value - service: input_text.set_value
target: target:
entity_id: "{{ automation_state_entity }}" entity_id: "{{ automation_state_entity }}"
@@ -1005,12 +1033,33 @@ action:
value: > value: >
{% set new_automation_state = (re_eval_state | combine({ {% set new_automation_state = (re_eval_state | combine({
state_motion_light_state: automation_state_enabling, state_motion_light_state: automation_state_enabling,
state_motion_light_last_action_timestamp: now(), state_motion_light_last_action_timestamp: date_time_now,
state_motion_light_last_brightness: 0 state_motion_light_last_brightness: 0
})) %} })) %}
{{ re_eval_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ re_eval_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Scene or light activation # Run enable callback BEFORE turning on (mode:restart safety)
- choose:
- conditions:
- condition: template
value_template: "{{ enable_action != [] }}"
sequence: !input enable_action
# Debug notification (also before turn-on for restart safety)
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Motion Light Debug"
message: >
Action: RE-ENABLE (external turn-off recovery)
Time: {{ now().strftime('%H:%M:%S') }}
Lights: {{ resolved_all_lights }}
Switches: {{ resolved_all_switches }}
Scene: {{ effective_scene if use_scene_instead else 'N/A' }}
# Scene or light activation (after callback/debug for restart safety)
- choose: - choose:
- conditions: - conditions:
- condition: template - condition: template
@@ -1049,27 +1098,6 @@ action:
target: target:
entity_id: "{{ resolved_all_switches }}" entity_id: "{{ resolved_all_switches }}"
# Execute enable callback
- choose:
- conditions:
- condition: template
value_template: "{{ enable_action != [] }}"
sequence: !input enable_action
# Debug notification
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Motion Light Debug"
message: >
Action: RE-ENABLE (external turn-off recovery)
Time: {{ now().strftime('%H:%M:%S') }}
Lights: {{ resolved_all_lights }}
Switches: {{ resolved_all_switches }}
Scene: {{ effective_scene if use_scene_instead else 'N/A' }}
# ----- Sub-case: Automation just turned on the light ----- # ----- Sub-case: Automation just turned on the light -----
# Transition from ENABLING to ENABLED, or disable immediately # Transition from ENABLING to ENABLED, or disable immediately
# if motion already cleared during the ENABLING phase # if motion already cleared during the ENABLING phase
@@ -1160,27 +1188,36 @@ action:
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# ----- Sub-case: User manually changed the light ----- # ----- Sub-case: User manually changed the light -----
# Transition from ENABLED to MANUAL (user took control) # Transition to MANUAL (user took control). Fires when EITHER:
# * state is ENABLED and the user changes the light, OR
# * state is NONE and the user turns the light ON while the
# automation is idle (pre-emptive manual control).
# Only triggers on meaningful state changes (on→off or off→on), # Only triggers on meaningful state changes (on→off or off→on),
# NOT on attribute-only updates (on→on) which Zigbee devices # NOT on attribute-only updates (on→on) which Zigbee devices
# commonly send as the light settles after a transition. # commonly send as the light settles after a transition.
# Grace period: ignore state changes shortly after the automation # In NONE state, only a turn-ON counts as a takeover (a turn-OFF
# turns on the light to avoid false manual override detection. # is handled by the "Light turned OFF" sub-case above).
# Grace period: ignore state changes shortly after the automation's
# last action to avoid false manual override detection.
- conditions: - conditions:
- condition: template - condition: template
value_template: > value_template: >
{% set meaningful_change = trigger.from_state.state != trigger.to_state.state %} {% set meaningful_change = trigger.from_state.state != trigger.to_state.state %}
{% if not meaningful_change %} {% if not meaningful_change %}
{{ false }} {{ false }}
{% else %} {% elif state_is_none and trigger.to_state.state != 'on' %}
{{ false }}
{% elif state_is_enabled or state_is_none %}
{% set last_ts = automation_state.get(state_motion_light_last_action_timestamp, none) %} {% set last_ts = automation_state.get(state_motion_light_last_action_timestamp, none) %}
{% set grace = (transition_duration | float(0)) + (manual_override_grace_period | float(2)) %} {% set grace = (transition_duration | float(0)) + (manual_override_grace_period | float(2)) %}
{% if state_is_enabled and last_ts is not none %} {% if last_ts is none %}
{{ true }}
{% else %}
{% set parsed = last_ts | as_datetime %} {% set parsed = last_ts | as_datetime %}
{{ parsed is none or (now() - parsed).total_seconds() > grace }} {{ parsed is none or (now() - parsed).total_seconds() > grace }}
{% else %}
{{ state_is_enabled }}
{% endif %} {% endif %}
{% else %}
{{ false }}
{% endif %} {% endif %}
sequence: sequence:
# BUG FIX: Fixed YAML structure - was 'data: >' instead of 'data:' with 'value: >' # BUG FIX: Fixed YAML structure - was 'data: >' instead of 'data:' with 'value: >'
@@ -1192,9 +1229,12 @@ action:
{% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_manual })) %} {% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_manual })) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Call disable action if configured # Call disable action if configured.
# Only when we were actively ENABLED — a pre-emptive manual ON
# (from NONE) never enabled anything, so there is nothing to
# "disable" and running it could touch unrelated devices.
- choose: - choose:
- conditions: "{{ manual_action_runs_disable_action and disable_action != [] }}" - conditions: "{{ state_is_enabled and manual_action_runs_disable_action and disable_action != [] }}"
sequence: !input disable_action sequence: !input disable_action
# Call manual action callback # Call manual action callback
@@ -1212,7 +1252,7 @@ action:
message: > message: >
Action: MANUAL OVERRIDE Action: MANUAL OVERRIDE
Time: {{ now().strftime('%H:%M:%S') }} Time: {{ now().strftime('%H:%M:%S') }}
Previous State: ENABLED Previous State: {{ 'ENABLED' if state_is_enabled else 'NONE (pre-emptive)' }}
New State: MANUAL New State: MANUAL
Trigger: {{ trigger_id }} Trigger: {{ trigger_id }}
@@ -1227,6 +1267,33 @@ action:
- condition: template - condition: template
value_template: "{{ (state_is_enabled or state_is_enabling) and must_be_disabled_preview }}" value_template: "{{ (state_is_enabled or state_is_enabling) and must_be_disabled_preview }}"
sequence: sequence:
# Honor min_on_duration even on the recovery path so a
# mistimed Zigbee report can't chop the on-time short.
# We skip timeout_delay here intentionally — this path
# is the recovery for a cancelled CASE 3, and the user
# already paid that delay before the restart.
- variables:
time_since_enabled: >
{% set last_ts = automation_state.get(state_motion_light_last_action_timestamp, none) %}
{% if last_ts is none %}
{{ 9999 }}
{% else %}
{% set parsed = last_ts | as_datetime %}
{% if parsed is none %}
{{ 9999 }}
{% else %}
{{ (now() - parsed).total_seconds() | int }}
{% endif %}
{% endif %}
remaining_min_on: "{{ [0, min_on_duration - time_since_enabled] | max }}"
- choose:
- conditions:
- condition: template
value_template: "{{ remaining_min_on > 0 }}"
sequence:
- delay:
seconds: "{{ remaining_min_on }}"
# Reset state to NONE first (before turn-off triggers another restart) # Reset state to NONE first (before turn-off triggers another restart)
- service: input_text.set_value - service: input_text.set_value
target: target:
@@ -1309,15 +1376,33 @@ action:
# Enable the light/switch # Enable the light/switch
default: default:
# Store current brightness (to restore later if configured) # Store current brightness (to restore later if configured).
# If light is on but below brightness_threshold, treat as "off"
# for restore purposes — otherwise the disable path would
# restore that dim state instead of actually turning off.
- variables: - variables:
last_brightness: > last_brightness: >
{% if reference_light is none or is_state(reference_light, 'off') %} {% if reference_light is none or is_state(reference_light, 'off') %}
{{ 0 }} {{ 0 }}
{% else %} {% else %}
{{ state_attr(reference_light, 'brightness') | int(0) }} {% set br = state_attr(reference_light, 'brightness') | int(0) %}
{% set thr = brightness_threshold | int(0) %}
{% if thr > 0 and br < thr %}
{{ 0 }}
{% else %}
{{ br }}
{% endif %}
{% endif %} {% endif %}
# Guard: scene mode is on but no scene resolved — stop loudly
# rather than silently falling back to lights with default data.
- choose:
- conditions:
- condition: template
value_template: "{{ use_scene_instead and effective_scene is none }}"
sequence:
- stop: "Scene mode enabled but no scene configured for current mode"
# Update state to ENABLING BEFORE turning on the light. # Update state to ENABLING BEFORE turning on the light.
# This must happen first because mode: restart may cancel # This must happen first because mode: restart may cancel
# subsequent steps if the light state change fires immediately. # subsequent steps if the light state change fires immediately.
@@ -1333,6 +1418,31 @@ action:
})) %} })) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Run enable callback BEFORE turning on the light.
# Light state change fires mode:restart which cancels remaining
# steps — running the callback first ensures it isn't silently
# dropped (mirrors the CASE 3 disable_action ordering).
- choose:
- conditions:
- condition: template
value_template: "{{ enable_action != [] }}"
sequence: !input enable_action
# Debug notification (also before turn-on for restart safety)
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Motion Light Debug"
message: >
Action: ENABLE
Time: {{ now().strftime('%H:%M:%S') }}
Lights: {{ resolved_all_lights }}
Switches: {{ resolved_all_switches }}
Scene: {{ effective_scene if use_scene_instead else 'N/A' }}
Night Mode: {{ is_night_mode }}
# Scene activation path # Scene activation path
- choose: - choose:
- conditions: - conditions:
@@ -1373,28 +1483,6 @@ action:
target: target:
entity_id: "{{ resolved_all_switches }}" entity_id: "{{ resolved_all_switches }}"
# Execute enable callback action
- choose:
- conditions:
- condition: template
value_template: "{{ enable_action != [] }}"
sequence: !input enable_action
# Debug notification
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "Motion Light Debug"
message: >
Action: ENABLE
Time: {{ now().strftime('%H:%M:%S') }}
Lights: {{ resolved_all_lights }}
Switches: {{ resolved_all_switches }}
Scene: {{ effective_scene if use_scene_instead else 'N/A' }}
Night Mode: {{ is_night_mode }}
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# CASE 3: Disable Path (Motion Cleared, Should Turn Off) # CASE 3: Disable Path (Motion Cleared, Should Turn Off)
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------