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:
@@ -7,10 +7,10 @@ This blueprint creates a smart motion-activated light control system. It handles
|
||||
- Multiple motion sensor support (triggers on ANY sensor)
|
||||
- Condition switches (ALL must be ON for automation to work)
|
||||
- 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
|
||||
- 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
|
||||
- Luminance sensor support (only trigger in dark conditions)
|
||||
- Time-based conditions (only active during specified hours)
|
||||
@@ -36,19 +36,29 @@ The automation tracks these states via persistent storage:
|
||||
|
||||
## Behavior Notes
|
||||
|
||||
- Will NOT turn on light if it's already ON (prevents hijacking user control)
|
||||
- If user changes light while automation is active, enters MANUAL mode (after grace period)
|
||||
- Grace period prevents false manual overrides from delayed device state reports (e.g., Zigbee)
|
||||
- MANUAL mode exits when light is turned OFF (by any means)
|
||||
- Timeout delay only applies when turning OFF (motion cleared)
|
||||
- Time conditions support overnight windows (e.g., 22:00 to 06:00)
|
||||
- Day/Night mode uses separate time window from time conditions
|
||||
- Will NOT turn on light if it's already ON (prevents hijacking user control).
|
||||
- If user **changes** the light while automation is active (e.g., dim, color, scene), enters MANUAL mode (after grace period).
|
||||
- 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).
|
||||
- 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.
|
||||
- Grace period prevents false manual overrides from delayed device state reports (e.g., Zigbee) — including stray reports that can follow a turn-off.
|
||||
- MANUAL mode exits when light is turned OFF (by any means).
|
||||
- Timeout delay only applies when turning OFF (motion cleared).
|
||||
- Time conditions support overnight windows (e.g., 22:00 to 06:00).
|
||||
- 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
|
||||
|
||||
- At least one motion sensor
|
||||
- `input_text` entity for persistent state storage
|
||||
- Target light(s), switch(es), group, or area to control
|
||||
- At least one motion sensor.
|
||||
- `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.
|
||||
|
||||
## Author
|
||||
|
||||
|
||||
+189
-101
@@ -92,15 +92,15 @@ blueprint:
|
||||
target_area:
|
||||
name: Target Area (optional)
|
||||
description: >
|
||||
Area ID (e.g., "living_room"). All lights and switches in the
|
||||
area will be discovered and controlled.
|
||||
default: ""
|
||||
Pick an area. All lights and switches in the area will be
|
||||
discovered and controlled.
|
||||
default:
|
||||
selector:
|
||||
text:
|
||||
area: {}
|
||||
|
||||
target_light_data:
|
||||
name: Light Data Dictionary (optional)
|
||||
default: ""
|
||||
default: {}
|
||||
description: >
|
||||
Provide a YAML dictionary of light.turn_on parameters.
|
||||
If not specified, the light's last settings are preserved.
|
||||
@@ -289,14 +289,14 @@ blueprint:
|
||||
day_light_data:
|
||||
name: Day Light Settings
|
||||
description: Light parameters during day mode (YAML dictionary)
|
||||
default: ""
|
||||
default: {}
|
||||
selector:
|
||||
object: {}
|
||||
|
||||
night_light_data:
|
||||
name: Night Light Settings
|
||||
description: Light parameters during night mode (YAML dictionary)
|
||||
default: ""
|
||||
default: {}
|
||||
selector:
|
||||
object: {}
|
||||
|
||||
@@ -385,6 +385,20 @@ blueprint:
|
||||
step: 1
|
||||
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:
|
||||
name: Manual Override Grace Period (seconds)
|
||||
description: >
|
||||
@@ -455,7 +469,9 @@ blueprint:
|
||||
manual_action:
|
||||
name: Manual callback action (optional)
|
||||
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.
|
||||
default: []
|
||||
selector:
|
||||
@@ -479,10 +495,12 @@ trigger:
|
||||
seconds: !input motion_debounce
|
||||
id: "motion_sensor_on"
|
||||
|
||||
# Motion sensors OFF
|
||||
# Motion sensors OFF (with debounce to filter PIR drop-outs)
|
||||
- platform: state
|
||||
entity_id: !input motion_sensors
|
||||
to: "off"
|
||||
for:
|
||||
seconds: !input motion_off_debounce
|
||||
id: "motion_sensor_off"
|
||||
|
||||
# Condition switches ON/OFF
|
||||
@@ -499,20 +517,22 @@ trigger:
|
||||
entity_id: !input target_switches
|
||||
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
|
||||
value_template: >
|
||||
{% if luminance_sensor %}
|
||||
{{ states(luminance_sensor) not in ['unknown','unavailable'] }}
|
||||
{{ states(luminance_sensor) | float(99999) < (luminance_threshold | float(50)) }}
|
||||
{% else %}
|
||||
false
|
||||
{% endif %}
|
||||
|
||||
# Luminance enable switch changed
|
||||
# Luminance enable switch turned ON (re-evaluate)
|
||||
- platform: template
|
||||
value_template: >
|
||||
{% if luminance_enable_switch %}
|
||||
{{ states(luminance_enable_switch) not in ['unknown','unavailable'] }}
|
||||
{{ is_state(luminance_enable_switch, 'on') }}
|
||||
{% else %}
|
||||
false
|
||||
{% endif %}
|
||||
@@ -531,7 +551,6 @@ variables:
|
||||
# State Machine Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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_enabled: '1' # Light is ON and controlled by automation
|
||||
automation_state_enabling: '2' # Turn-on command sent, awaiting confirmation
|
||||
@@ -576,7 +595,7 @@ variables:
|
||||
{% if target_light_group is not none %}
|
||||
{% set result = result + [target_light_group] %}
|
||||
{% 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 result = result + area_lights %}
|
||||
{% endif %}
|
||||
@@ -594,7 +613,7 @@ variables:
|
||||
{% if target_switches | length > 0 %}
|
||||
{% set result = result + target_switches %}
|
||||
{% 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 result = result + area_switches %}
|
||||
{% endif %}
|
||||
@@ -631,13 +650,16 @@ variables:
|
||||
# ---------------------------------------------------------------------------
|
||||
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: >
|
||||
{% set text = states(automation_state_entity) | string %}
|
||||
{% if text in ['unknown','unavailable','none',''] %}
|
||||
{{ dict() }}
|
||||
{% else %}
|
||||
{{ text | from_json }}
|
||||
{% set parsed = text | from_json(default=dict()) %}
|
||||
{{ parsed if parsed is mapping else dict() }}
|
||||
{% endif %}
|
||||
|
||||
automation_state_placeholder_key: !input automation_state_placeholder_key
|
||||
@@ -655,20 +677,11 @@ variables:
|
||||
{% endif %}
|
||||
|
||||
# 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() }}"
|
||||
|
||||
# Current state machine state
|
||||
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_is_none: "{{ (motion_light_state | string) == automation_state_none }}"
|
||||
state_is_enabled: "{{ (motion_light_state | string) == automation_state_enabled }}"
|
||||
@@ -927,28 +940,31 @@ action:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: >
|
||||
{# Check actual on/off state only — do NOT use brightness_threshold here.
|
||||
Brightness threshold is for the enable guard (any_device_on), not for
|
||||
detecting whether the light was actually turned off. During transitions,
|
||||
brightness may temporarily be below threshold while the light is still on. #}
|
||||
{% set res = true %}
|
||||
{% if light_entity is not none %}
|
||||
{% set res = res and is_state(light_entity, 'off') %}
|
||||
{% endif %}
|
||||
{% 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) }}
|
||||
{# Check actual on/off state across ALL resolved devices — do NOT use
|
||||
brightness_threshold here. Threshold is for the enable guard
|
||||
(any_device_on), not for detecting actual off. During transitions,
|
||||
brightness may temporarily dip below threshold while still on. #}
|
||||
{% set has_devices = (resolved_all_lights | length > 0) or (resolved_all_switches | length > 0) %}
|
||||
{% set lights_on = resolved_all_lights | select('is_state', 'on') | list | length > 0 %}
|
||||
{% set switches_on = resolved_all_switches | select('is_state', 'on') | list | length > 0 %}
|
||||
{{ has_devices and not lights_on and not switches_on }}
|
||||
|
||||
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
|
||||
target:
|
||||
entity_id: "{{ automation_state_entity }}"
|
||||
data:
|
||||
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 }}
|
||||
|
||||
# Re-evaluate: if an external source turned off the light while
|
||||
@@ -986,7 +1002,8 @@ action:
|
||||
{% if text in ['unknown','unavailable','none',''] %}
|
||||
{{ dict() }}
|
||||
{% else %}
|
||||
{{ text | from_json }}
|
||||
{% set parsed = text | from_json(default=dict()) %}
|
||||
{{ parsed if parsed is mapping else dict() }}
|
||||
{% 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) }}"
|
||||
@@ -997,7 +1014,18 @@ action:
|
||||
|
||||
# --- 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
|
||||
target:
|
||||
entity_id: "{{ automation_state_entity }}"
|
||||
@@ -1005,12 +1033,33 @@ action:
|
||||
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_action_timestamp: date_time_now,
|
||||
state_motion_light_last_brightness: 0
|
||||
})) %}
|
||||
{{ 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:
|
||||
- conditions:
|
||||
- condition: template
|
||||
@@ -1049,27 +1098,6 @@ action:
|
||||
target:
|
||||
entity_id: "{{ resolved_all_switches }}"
|
||||
|
||||
# Execute enable callback
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ enable_action != [] }}"
|
||||
sequence: !input enable_action
|
||||
|
||||
# Debug notification
|
||||
- choose:
|
||||
- conditions: "{{ enable_debug_notifications }}"
|
||||
sequence:
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
title: "Motion Light Debug"
|
||||
message: >
|
||||
Action: RE-ENABLE (external turn-off recovery)
|
||||
Time: {{ now().strftime('%H:%M:%S') }}
|
||||
Lights: {{ resolved_all_lights }}
|
||||
Switches: {{ resolved_all_switches }}
|
||||
Scene: {{ effective_scene if use_scene_instead else 'N/A' }}
|
||||
|
||||
# ----- Sub-case: Automation just turned on the light -----
|
||||
# Transition from ENABLING to ENABLED, or disable immediately
|
||||
# if motion already cleared during the ENABLING phase
|
||||
@@ -1160,27 +1188,36 @@ action:
|
||||
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
|
||||
|
||||
# ----- Sub-case: User manually changed the light -----
|
||||
# Transition from ENABLED to MANUAL (user took control)
|
||||
# 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),
|
||||
# NOT on attribute-only updates (on→on) which Zigbee devices
|
||||
# commonly send as the light settles after a transition.
|
||||
# Grace period: ignore state changes shortly after the automation
|
||||
# turns on the light to avoid false manual override detection.
|
||||
# In NONE state, only a turn-ON counts as a takeover (a turn-OFF
|
||||
# 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:
|
||||
- condition: template
|
||||
value_template: >
|
||||
{% set meaningful_change = trigger.from_state.state != trigger.to_state.state %}
|
||||
{% if not meaningful_change %}
|
||||
{{ 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 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 %}
|
||||
{{ parsed is none or (now() - parsed).total_seconds() > grace }}
|
||||
{% else %}
|
||||
{{ state_is_enabled }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ false }}
|
||||
{% endif %}
|
||||
sequence:
|
||||
# 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 })) %}
|
||||
{{ 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:
|
||||
- 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
|
||||
|
||||
# Call manual action callback
|
||||
@@ -1212,7 +1252,7 @@ action:
|
||||
message: >
|
||||
Action: MANUAL OVERRIDE
|
||||
Time: {{ now().strftime('%H:%M:%S') }}
|
||||
Previous State: ENABLED
|
||||
Previous State: {{ 'ENABLED' if state_is_enabled else 'NONE (pre-emptive)' }}
|
||||
New State: MANUAL
|
||||
Trigger: {{ trigger_id }}
|
||||
|
||||
@@ -1227,6 +1267,33 @@ action:
|
||||
- condition: template
|
||||
value_template: "{{ (state_is_enabled or state_is_enabling) and must_be_disabled_preview }}"
|
||||
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)
|
||||
- service: input_text.set_value
|
||||
target:
|
||||
@@ -1309,14 +1376,32 @@ action:
|
||||
|
||||
# Enable the light/switch
|
||||
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:
|
||||
last_brightness: >
|
||||
{% if reference_light is none or is_state(reference_light, 'off') %}
|
||||
{{ 0 }}
|
||||
{% 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 %}
|
||||
|
||||
# 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.
|
||||
# This must happen first because mode: restart may cancel
|
||||
@@ -1333,6 +1418,31 @@ action:
|
||||
})) %}
|
||||
{{ 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
|
||||
- choose:
|
||||
- conditions:
|
||||
@@ -1373,28 +1483,6 @@ action:
|
||||
target:
|
||||
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)
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user