# ============================================================================= # Motion Light Automation Blueprint for Home Assistant # ============================================================================= # This blueprint creates a smart motion-activated light control system. # It handles motion detection, luminance-based triggering, and manual override # detection to provide intelligent lighting automation. # # Features: # - Multiple motion sensor support (triggers on ANY sensor) # - Condition switches (ALL must be ON for automation to work) # - Light and/or switch control # - Configurable timeout delay before turning off # - Luminance sensor support (only trigger in dark conditions) # - Manual override detection (stops automation if user changes light) # - Brightness threshold (only trigger if light is dim) # - Custom light parameters (brightness, color, etc.) # - Callback actions for enable/disable/manual events # # State Machine: # The automation tracks these states via persistent storage: # - NONE (0): Idle, waiting for motion # - ENABLING (2): Light turn-on command sent, waiting for state change # - ENABLED (1): Light is ON and controlled by automation # - MANUAL (3): User took control, automation paused until light turns off # # 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 # - MANUAL mode exits when light is turned OFF (by any means) # - Timeout delay only applies when turning OFF (motion cleared) # # Requirements: # - At least one motion sensor # - input_text entity for persistent state storage # - Target light and/or switch to control # # Author: Alexei Dolgolyov (dolgolyov.alexei@gmail.com) # ============================================================================= blueprint: name: "Custom: Motion Light" description: > Smart motion sensor automation blueprint. Note: By default will not run if light was already ON. If light was turned ON during automation running, the automation will enter manual state and will not trigger until the light is turned off. Note: Not tested when motion sensors and state sensors are used at the same time. domain: automation # =========================================================================== # INPUT CONFIGURATION # =========================================================================== input: # ------------------------------------------------------------------------- # Motion & Condition Sensors # ------------------------------------------------------------------------- controls: name: "Controls" collapsed: false input: motion_sensors: name: Motion sensors description: > Select one or more motion sensors. Light turns ON if ANY sensor detects motion (OR logic). default: [] selector: entity: domain: - binary_sensor - switch - group - light multiple: true condition_switches: name: Condition switches description: > Automation will not trigger if ANY of these switches is OFF. All must be ON for the automation to work (AND logic). default: [] selector: entity: domain: - input_boolean - switch - group - light - binary_sensor multiple: true # ------------------------------------------------------------------------- # Target Devices # ------------------------------------------------------------------------- devices_group: name: "Devices" collapsed: false input: target_light: name: Target Light (optional) description: "Light to control. Supports single light only." default: [] selector: entity: domain: light multiple: true target_light_data: name: Light Data Dictionary (optional) default: "" description: > Provide a YAML dictionary of light.turn_on parameters. If not specified, the light's last settings are preserved. Example: brightness: 200 color_temp: 350 rgb_color: [255, 0, 0] effect: rainbow selector: object: {} brightness_threshold: name: Brightness Threshold description: > Automation only triggers if the light's current brightness is below this value. Set to 0 to disable this check. default: 0 selector: number: min: 0 max: 255 step: 1 target_switch: name: Target Switch (optional) description: "Switch to control. Supports single switch only." default: [] selector: entity: domain: switch multiple: true timeout_delay: name: Timeout delay (seconds) description: > Delay before turning off the light after all motion sensors clear. Set to 0 for immediate turn off. default: 0 selector: number: min: 0 max: 3600 step: 1 unit_of_measurement: seconds # ------------------------------------------------------------------------- # Persistent State Configuration # ------------------------------------------------------------------------- persistent_state: name: "Persistent State" collapsed: true input: automation_state_entity: name: Automation state entity description: > `input_text` entity that stores the automation state in JSON format. Required for manual override detection and state tracking. Doesn't require specific initial state - values can be empty. Each automation instance must have its own entity. selector: entity: domain: input_text automation_state_placeholder_key: name: Automation state placeholder key description: > Overrides key for persistent storage if not empty. By default uses the target light/switch entity ID. Don't override if you don't understand the meaning. default: '' selector: text: # ------------------------------------------------------------------------- # Luminance Control # ------------------------------------------------------------------------- luminance: name: "Luminance" collapsed: true input: luminance_sensor: name: Luminance sensor (optional) description: Sensor reporting ambient light level (lux) default: null selector: entity: domain: sensor luminance_threshold: name: Luminance threshold (optional) description: > Light will only turn on if luminance sensor value is below this threshold (darker than this level). default: 100 selector: number: min: 0 max: 10000 step: 1 unit_of_measurement: "lux" luminance_enable_switch: name: Luminance control enable switch (optional) description: > Switch or input_boolean to enable/disable luminance-based control. When OFF, luminance check is skipped. default: null selector: entity: domain: - switch - input_boolean # ------------------------------------------------------------------------- # Callback Actions # ------------------------------------------------------------------------- actions: name: "Actions" collapsed: true input: user_condition: name: Condition block description: Optional condition(s) that must pass for actions to run default: [] selector: condition: {} enable_action: name: Enable callback action (optional) description: Runs when light is turned ON by this automation default: [] selector: action: {} disable_action: name: Disable callback action (optional) description: Runs when light is turned OFF by this automation default: [] selector: action: {} manual_action_runs_disable_action: name: Manual also runs disable action description: > If checked, entering manual mode will also execute the disable callback action. default: false selector: boolean: {} manual_action: name: Manual callback action (optional) description: > Runs when user manually changes the light while automation is active. Requires 'Automation state entity' to be configured. default: [] selector: action: {} # ============================================================================= # AUTOMATION MODE # ============================================================================= # Restart mode ensures rapid motion events don't queue up mode: restart # ============================================================================= # TRIGGERS # ============================================================================= trigger: # Motion sensors ON/OFF - platform: state entity_id: !input motion_sensors id: "motion_sensor" # Condition switches ON/OFF - platform: state entity_id: !input condition_switches # Light state changed (for manual override detection) - platform: state entity_id: !input target_light id: "light_state_changed" # Switch state changed (for manual override detection) - platform: state entity_id: !input target_switch id: "switch_state_changed" # Luminance sensor value changed - platform: template value_template: > {% if luminance_sensor %} {{ states(luminance_sensor) not in ['unknown','unavailable'] }} {% else %} false {% endif %} # Luminance enable switch changed - platform: template value_template: > {% if luminance_enable_switch %} {{ states(luminance_enable_switch) not in ['unknown','unavailable'] }} {% else %} false {% endif %} # ============================================================================= # CONDITIONS # ============================================================================= 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 # --------------------------------------------------------------------------- # 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 automation_state_manual: '3' # User took control, automation paused # Persistent state JSON keys state_motion_light_state: 'mls' # Current state machine state state_motion_light_last_action_timestamp: 'mllat' # Last action timestamp state_motion_light_last_brightness: 'mllb' # Brightness before automation # --------------------------------------------------------------------------- # Trigger Context # --------------------------------------------------------------------------- date_time_now: "{{ now() }}" trigger_id: "{{ trigger.id }}" # --------------------------------------------------------------------------- # Input Variables # --------------------------------------------------------------------------- sensors: !input motion_sensors condition_switches: !input condition_switches timeout: !input timeout_delay brightness_threshold: !input brightness_threshold # Light configuration light_entities: !input target_light light_entity: "{{ light_entities[0] if light_entities | length != 0 else none }}" # Switch configuration switch_entities: !input target_switch switch_entity: "{{ switch_entities[0] if switch_entities | length != 0 else none }}" # --------------------------------------------------------------------------- # Persistent State Management # --------------------------------------------------------------------------- automation_state_entity: !input automation_state_entity # Parse global state JSON from input_text entity automation_state_global: > {% set text = states(automation_state_entity) | string %} {% if text in ['unknown','unavailable','none',''] %} {{ dict() }} {% else %} {{ text | from_json }} {% endif %} automation_state_placeholder_key: !input automation_state_placeholder_key # Determine the key for this automation's state automation_state_key: > {% if automation_state_placeholder_key != '' %} {{ automation_state_placeholder_key }} {% elif switch_entity is not none %} {{ switch_entity }} {% elif light_entity is not none %} {{ light_entity }} {% else %} {{ 'default_motion_light_placeholder' }} {% 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 }}" state_is_enabling: "{{ (motion_light_state | string) == automation_state_enabling }}" state_is_manual: "{{ (motion_light_state | string) == automation_state_manual }}" # --------------------------------------------------------------------------- # Callback Actions # --------------------------------------------------------------------------- manual_action: !input manual_action disable_action: !input disable_action enable_action: !input enable_action manual_action_runs_disable_action: !input manual_action_runs_disable_action light_data: !input target_light_data # --------------------------------------------------------------------------- # Luminance Configuration # --------------------------------------------------------------------------- luminance_sensor: !input luminance_sensor luminance_threshold: !input luminance_threshold luminance_enable_switch: !input luminance_enable_switch # Check if luminance conditions allow triggering luminance_ok: > {% if luminance_sensor is not none and luminance_threshold is not none %} {% set val = states(luminance_sensor) | float(0) %} {% set enabled = true %} {% if luminance_enable_switch %} {% set enabled = is_state(luminance_enable_switch, 'on') %} {% endif %} {{ enabled and val < luminance_threshold }} {% else %} {{ true }} {% endif %} # --------------------------------------------------------------------------- # Trigger Evaluation # --------------------------------------------------------------------------- # Check if ALL condition switches are ON (AND logic) all_of_condition_switches_on: > {% set e = condition_switches if condition_switches is iterable else [condition_switches] %} {% if e | length == 0 %} {{ true }} {% else %} {{ (e | select('is_state', 'on') | list | length) == (e | length) }} {% endif %} # Count how many motion sensors are currently detecting motion count_of_enabled_sensor: > {% set e = sensors if sensors is iterable else [sensors] %} {{ e | select('is_state', 'on') | list | length }} # Motion state checks motion_on: "{{ count_of_enabled_sensor > 0 }}" motion_all_off: "{{ count_of_enabled_sensor == 0 }}" # --------------------------------------------------------------------------- # Enable/Disable Decision Logic # --------------------------------------------------------------------------- # Should we enable the light? (All conditions must be met) must_be_enabled_preview: > {{ (all_of_condition_switches_on and luminance_ok and motion_on) | bool }} must_be_enabled_guard: "{{ state_is_none }}" must_be_enabled: > {{ must_be_enabled_preview and must_be_enabled_guard }} # Should we disable the light? (Motion cleared OR condition switch turned off) must_be_disabled_preview: > {{ ((not all_of_condition_switches_on) or motion_all_off) | bool }} must_be_disabled_guard: "{{ state_is_enabled }}" must_be_disabled: > {{ must_be_disabled_preview and must_be_disabled_guard }} # ============================================================================= # ACTIONS # ============================================================================= action: # --------------------------------------------------------------------------- # DEBUG: Log basic info (enable by setting is_base_debug: true) # --------------------------------------------------------------------------- - choose: - conditions: - condition: template value_template: "{{ is_base_debug }}" sequence: - service: persistent_notification.create data: title: "Debug Info - Motion Light" message: > must_be_enabled_preview: {{ must_be_enabled_preview }}, must_be_disabled_preview: {{ must_be_disabled_preview }}, must_be_disabled: {{ must_be_disabled }}, must_be_disabled_guard: {{ must_be_disabled_guard }}, trigger_id: {{ trigger.id }} # --------------------------------------------------------------------------- # GUARDS: Validate prerequisites # --------------------------------------------------------------------------- # Guard: Only one light supported - choose: - conditions: - condition: template value_template: "{{ light_entities | length > 1 }}" sequence: - stop: "Only one light is supported currently" # Guard: Only one switch supported - choose: - conditions: - condition: template value_template: "{{ switch_entities | length > 1 }}" sequence: - stop: "Only one switch is supported currently" # =========================================================================== # MAIN STATE MACHINE # =========================================================================== - choose: # ----------------------------------------------------------------------- # CASE 1: Light/Switch State Changed (Manual Override Detection) # ----------------------------------------------------------------------- # Handles state changes from the light/switch itself to detect # when automation turned it on vs when user manually changed it - conditions: - condition: template value_template: "{{ trigger_id == 'light_state_changed' or trigger_id == 'switch_state_changed' }}" sequence: - choose: # ----- Sub-case: Light/Switch turned OFF ----- # Reset to NONE state so automation can work again - conditions: - condition: template value_template: > {# BUG FIX: Changed from 'res = false' to 'res = true' for AND logic #} {% set res = true %} {% if light_entity is not none %} {% set brightness = state_attr(light_entity, 'brightness') | int(0) %} {% set light_off = is_state(light_entity, 'off') or brightness < brightness_threshold %} {% set res = res and light_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) }} 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 }} # ----- Sub-case: Automation just turned on the light ----- # Transition from ENABLING to ENABLED - conditions: - condition: template value_template: "{{ state_is_enabling }}" sequence: - service: input_text.set_value target: entity_id: "{{ automation_state_entity }}" data: value: > {% set new_automation_state = (automation_state | combine({ state_motion_light_state: automation_state_enabled })) %} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} # ----- Sub-case: User manually changed the light ----- # Transition from ENABLED to MANUAL (user took control) - conditions: - condition: template value_template: "{{ state_is_enabled }}" sequence: # BUG FIX: Fixed YAML structure - was 'data: >' instead of 'data:' with 'value: >' - 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_manual })) %} {{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }} # Call disable action if configured - choose: - conditions: "{{ manual_action_runs_disable_action and disable_action != [] }}" sequence: !input disable_action # Call manual action callback - choose: - conditions: "{{ manual_action != [] }}" sequence: !input manual_action # ----------------------------------------------------------------------- # CASE 2: Enable Path (Motion Detected, Should Turn On) # ----------------------------------------------------------------------- - conditions: - condition: template value_template: "{{ must_be_enabled }}" sequence: - choose: # Guard: Stop if light is already ON # (Don't hijack user-controlled light) - conditions: - condition: template value_template: > {% set res = false %} {% if light_entity is not none %} {# BUG FIX: Added proper null check for brightness #} {% set brightness = state_attr(light_entity, 'brightness') | int(0) %} {% set res = res or (is_state(light_entity, 'on') and brightness > brightness_threshold) %} {% endif %} {% if switch_entity is not none %} {% set res = res or is_state(switch_entity, 'on') %} {% endif %} {{ res }} sequence: - stop: "Light is already ON when sensors were triggered" # Enable the light/switch default: # Debug info - choose: - conditions: - condition: template value_template: "{{ is_debug }}" sequence: - service: persistent_notification.create data: title: "Debug Info (Enable Path)" message: > Enabling light. light_entity: {{ light_entity }} # Store current brightness (to restore later if configured) - variables: last_brightness: > {% if light_entity is none or is_state(light_entity, 'off') %} {{ 0 }} {% else %} {{ state_attr(light_entity, 'brightness') | int(0) }} {% endif %} # Turn ON the light - choose: - conditions: - condition: template value_template: "{{ light_entity is not none }}" sequence: - service: light.turn_on target: entity_id: "{{ light_entity }}" data: "{{ light_data if light_data else {} }}" # Turn ON the switch - choose: - conditions: - condition: template value_template: "{{ switch_entity is not none }}" sequence: - service: switch.turn_on target: entity_id: "{{ switch_entity }}" # 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 - choose: - conditions: - condition: template value_template: "{{ enable_action != [] }}" sequence: !input enable_action # ----------------------------------------------------------------------- # CASE 3: Disable Path (Motion Cleared, Should Turn Off) # ----------------------------------------------------------------------- - conditions: - condition: template value_template: "{{ must_be_disabled }}" sequence: # Debug info - choose: - conditions: - condition: template value_template: "{{ is_debug }}" sequence: - service: persistent_notification.create data: title: "Debug Info (Disable Path)" message: > Disabling light. light_entity: {{ light_entity }} # Wait for timeout before turning off - delay: seconds: "{{ timeout }}" # Turn OFF or restore the light - choose: - conditions: - condition: template value_template: "{{ light_entity is not none }}" sequence: - variables: last_brightness: "{{ automation_state.get(state_motion_light_last_brightness, 0) | int }}" - choose: # Restore previous brightness if it was set - conditions: - condition: template value_template: "{{ last_brightness > 0 }}" sequence: - service: light.turn_on target: entity_id: "{{ light_entity }}" data: brightness: "{{ last_brightness }}" # Otherwise turn off completely default: - service: light.turn_off target: entity_id: "{{ light_entity }}" # Turn OFF the switch - choose: - conditions: - condition: template value_template: "{{ switch_entity is not none }}" sequence: - service: switch.turn_off target: entity_id: "{{ switch_entity }}" # 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