blueprint: domain: automation name: Motion detected lights description: >- Activate lights based on binary_sensors. This is typically a motion sensor, but can also be a door sensor. The automation features: - dim light(s) before turning them off to alert present people that the sensors have become "off" and the automation will turn off the lights. The current states of each lights is stored in a scene, so if the ones in that room then move - the lights will dim back to the current state by applying the stored scene. - a parallel thread that will listen for light changes, e.g: The automation is triggered multiple times whenever anyone is in the room. If you have adjusted the light level or turned off any lights while the automation is running, these changes are stored to the scene, so the lights will stay in same state. - «wait for action» either before the lights are dimmed or while the lights are dimmed down. This is perfect to use when the house state is in «cleaning mode», while someone is showering (high humdity) and the motion sensor can't register motion because of the steam etc. input: trigger_sensors: name: Trigger sensors description: >- Sensors that will trigger this automation. It can be a motion sensor, door sensor etc. Any areas or devices that is given in this input will be crawled for binary_sensors, so it's recommended to only list entities explicitly with their entity_id. selector: entity: domain: binary_sensor multiple: true synced_lights: name: Synced lights description: >- Lights that will syncronize the trigger sensors. Any areas or devices that is given in this input will be crawled for light entities, so it's recommended to only list entities explicitly with their entity_id. selector: entity: domain: light illuminance_sensors: name: Lux sensors description: >- Lux (illuminance) sensor to monitor. If the threshold are met, the lights will stay off. The average illuminance will be calculated if several devices is set. default: [] selector: entity: domain: sensor device_class: illuminance illuminance_threshold: name: Lux threshold description: >- Threshold before lights are turned on. Default (-1) means that the lux threshold won't be considered. default: -1.0 selector: number: max: 100.0 min: -1.0 step: 0.1 dim_percentage: name: Dim-percentage description: >- Dim lights to this level (%) of current light level before turning lights off. Current level will be stored in a scene. Setting this to 0.0 or 1.0 will skip this action. default: 0.3 selector: number: max: 1.0 min: 0.0 step: 0.01 after_wait_actions: name: Wait actions description: >- Actions to run to delay further, e.g wait until media player state is off, housekeeping mode is off, humidity is back down etc. default: [] selector: action: wait_actions_before_dim: name: Wait actions before dim description: >- Run wait actions before dimming down (not turning off). Set to false/OFF if you want wait actions to run after dimming instead, but before turning OFF completely. default: "{{ true }}" selector: boolean: delay: name: Delay description: >- Time (minutes) to wait to turn off after last movement was detected. selector: number: min: 0.0 max: 300.0 step: 5.0 default: 3 mode: restart trigger_variables: trigger_sensors_input: !input trigger_sensors trigger_sensors: >- {{ trigger_sensors_input|unique|sort }} scene_name: >- {{ this.entity_id.replace('.', '_') }} synced_lights_input: !input synced_lights synced_lights: >- {{ synced_lights_input|unique|sort }} trigger: - platform: state # from: "off" to: "on" entity_id: !input trigger_sensors - platform: event event_type: "motion_detected_lights" event_data: end: "{{ scene_name }}" variables: illuminance_sensors_input: !input illuminance_sensors illuminance_sensors: >- {{ illuminance_sensors_input|unique|sort }} # {% set sensors = namespace(entities=[]) %} # {% if 'entity_id' in illuminance_sensors_input %} # {% for entity in ([ illuminance_sensors_input['entity_id'] ] if illuminance_sensors_input['entity_id'] is string else illuminance_sensors_input['entity_id']) %} # {% set sensors.entities = sensors.entities + [entity] %} # {% endfor %} # {% endif %} # {% if 'area_id' in illuminance_sensors_input %} # {% for area in ([ illuminance_sensors_input['area_id'] ] if illuminance_sensors_input['area_id'] is string else illuminance_sensors_input['area_id']) %} # {% for entity in area_entities(area) if state_attr(entity, 'device_class') == 'illuminance' %} # {% set sensors.entities = sensors.entities + [entity] %} # {% endfor %} # {% endfor %} # {% endif %} # {% if 'device_id' in trigger_sensors_input %} # {% for device in ([ illuminance_sensors_input['device_id'] ] if illuminance_sensors_input['device_id'] is string else illuminance_sensors_input['device_id']) %} # {% for entity in device_entities(device) if state_attr(entity, 'device_class') == 'illuminance' %} # {% set sensors.entities = sensors.entities + [entity] %} # {% endfor %} # {% endfor %} # {% endif %} # {{ sensors.entities|unique|list }} illuminance_threshold: !input illuminance_threshold invalid_light: >- # Used to invalidate scene as there's no other way to do it at the moment {{ states.light|rejectattr('entity_id', 'in', synced_lights)|map(attribute='entity_id')|first }} log_level: warning dim_percentage: !input dim_percentage delay_minutes: !input delay delay_seconds: "{{ delay_minutes * 60 }}" wait_actions_before_dim: !input wait_actions_before_dim action: - alias: "Triggered on motion or event" if: "{{ trigger.platform != 'event' }}" then: - alias: "Test if threshold is met" choose: conditions: - condition: template value_template: "{{ illuminance_threshold != -1 and illuminance_sensors|count > 0 }}" - condition: template value_template: >- # If any lights are on... Continue so they eventually will turn off {% set lights = namespace(on=false) %} {% for light in synced_lights if is_state(light, 'on') %} {% set lights.on = true %} {% endfor %} {{ not lights.on }} - condition: template value_template: >- {% set lux = namespace(values=[]) %} {% for sensor in illuminance_sensors %} {% set lux.values = lux.values + [ states(sensor)|float ] %} {% endfor %} {{ (lux.values|sum / lux.values|count)|int > illuminance_threshold }} sequence: stop: "Illuminance threshold met" - alias: "Motion: Turn on exisiting scene or turn on lights." continue_on_error: true # if scenes are empty (might happen after a scene.reload) choose: conditions: "{{ states.scene|selectattr('attributes.friendly_name', 'eq', scene_name)|list|count == 1 and synced_lights == state_attr('scene.' + scene_name, 'entity_id')|sort }}" sequence: # Scene do exist and is valid - service: system_log.write data: level: "{{ log_level }}" logger: "{{ scene_name }}" message: >- Scene «{{ scene_name }}» already exists and contains all lights. Restore scene. - alias: "Motion: Turn on existing scene for synced lights" service: scene.turn_on target: entity_id: "scene.{{scene_name}}" default: # Scene does not exist - service: system_log.write continue_on_error: true data: level: "{{ log_level }}" logger: "{{ scene_name }}" message: >- Scene «{{ scene_name }} does NOT exist or is INVALID. Entities to sync: {{ synced_lights }} => Turn ON every light sourcce. - alias: "Motion: Turn on synced lights" service: light.turn_on target: entity_id: "{{ synced_lights }}" - alias: >- Listen for light changes and motion in parallel. Save snapshots to scene on light changes. we dont restore light every time the motion is triggered without being OFF. End this part of automation when motion stops. parallel: - alias: >- Listen for light changes for as long as this automation lives, means: until event is fired and automation is restarted. repeat: while: "{{ true }}" sequence: - alias: "Store variable with current light values (on or brightness)" variables: synced_lights_values: >- {% set lights = namespace(values=[]) %} {% for light in synced_lights %} {%if is_state_attr(light, 'color_mode', 'brightness') %} {% set lights.values = lights.values + [state_attr(light, 'brightness')] %} {% else %} {% set lights.values = lights.values + [states(light)] %} {% endif %} {% endfor %} {{ lights.values }} - alias: "Wait for change in any light source" wait_template: >- {% set lights = namespace(changed=false) %} {% for light in synced_lights %} {%if is_state_attr(light, 'color_mode', 'brightness') and state_attr(light, 'brightness') != synced_lights_values[loop.index - 1] %} {% set lights.changed = true %} {% elif not is_state_attr(light, 'color_mode', 'brightness') and states(light) != synced_lights_values[loop.index - 1] %} {% set lights.changed = true %} {% endif %} {% endfor %} {{ lights.changed }} # Check here if values = 0 / OFF - service: system_log.write data: level: "{{ log_level }}" logger: "{{ scene_name }}" message: >- Light levels changed: {{ synced_lights }} from {{ synced_lights_values }}. Saving snapshot to scene «{{ scene_name }}». - alias: "Store a variable with ON lights." variables: synced_lights_on: >- {% set lights = namespace(on=[]) %} {% for light in synced_lights if not is_state(light, 'off') %} {% set lights.on = lights.on + [light] %} {% endfor %} {{ lights.on }} - choose: conditions: "{{ synced_lights_on|count == 0 }}" sequence: service: system_log.write data: level: "{{ log_level }}" logger: "{{ scene_name }}" message: "No lights on: invalidating with {{ invalid_light }} which isn't in {{ synced_lights }}." - alias: "Save scene for current change and return to loop. If no lights are ON; invalidate scene." service: scene.create data: scene_id: "{{ scene_name }}" snapshot_entities: >- {{ [ invalid_light ] if synced_lights_on|count == 0 else synced_lights }} - sequence: # @ignore: Missing property "condition" # alias: >- # Listen for motion change, and wait for it to become 'OFF', # wait for the delay and actions to finish, and then send event to kill parallelism. - alias: "Wait for motion to end." wait_template: >- {% set sensors = namespace(triggered=false) %} {% for sensor in trigger_sensors if is_state(sensor, 'on') %} {% set sensors.triggered = true %} {% endfor %} {{ not sensors.triggered }} - service: system_log.write data: level: "{{ log_level }}" logger: "{{ scene_name }}" message: >- No motion detected by {{ trigger_sensors }}. Run wait actions wait {{ (delay_seconds * 2/3)|int }} seconds,, then end automation (parallelism) by issuing an event, and let automation «re-trigger». - alias: "Run and wait for actions to finish" choose: conditions: >- {{ wait_actions_before_dim }} sequence: !input after_wait_actions - alias: "Wait for delay to fire event." delay: seconds: "{{ (delay_seconds * 2/3)|int }}" - alias: "Ending: Fire event to restart automation and kill parallel processes" event: "motion_detected_lights" event_data: end: "{{ scene_name }}" else: # Event was fired - service: system_log.write data: level: "{{ log_level }}" logger: "{{ scene_name }}" message: "Ending {{ scene_name }}, dim down and eventually turn off {{ synced_lights }}." - alias: "Calculate average brightness and set dim level" variables: brightness: >- {% set brightness = namespace(levels=[]) %} {% for light in synced_lights if is_state(light, 'on') %} {% if is_state_attr(light, 'color_mode', 'brightness') and state_attr(light, 'brightness')|int != 0 %} {% set brightness.levels = brightness.levels + [ state_attr(light, 'brightness') ] %} {% else %} {% set brightness.levels = brightness.levels + [ 100 ] %} {% endif %} {% endfor %} {{ 0.0 if brightness.levels|length == 0 else brightness.levels|sum / brightness.levels|length }} dim_level: "{{ (brightness * dim_percentage)|int }}" - service: system_log.write data: level: "{{ log_level }}" logger: "{{ scene_name }}" message: >- Light avg-brightness is: {{ brightness }}, now dimming lights with {{ (dim_percentage * 100)|int }}% to {{ dim_level }}. - alias: "Dim lights to a low brightness to alert no motion" service: light.turn_on target: entity_id: >- {{ synced_lights }} data: brightness: "{{ dim_level }}" - alias: "Run and wait for actions to finish" choose: conditions: >- {{ not wait_actions_before_dim }} sequence: !input after_wait_actions - alias: "Wait for dim to be recognized by anyone before turning off lights" delay: seconds: "{{ (delay_seconds * 1/3)|int }}" - alias: "Turn off lights" service: light.turn_off target: entity_id: "{{ synced_lights }}" - service: system_log.write data: level: "{{ log_level }}" logger: "{{ scene_name }}" message: >- Lights {{ synced_lights }} turned off.