motion_detected_lights.yaml 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. blueprint:
  2. domain: automation
  3. name: Motion detected lights
  4. description: >-
  5. Activate lights based on binary_sensors. This is typically a motion sensor, but can also be a door sensor.
  6. The automation features:
  7. - dim light(s) before turning them off to alert present people that the sensors have become "off" and
  8. the automation will turn off the lights. The current states of each lights is stored in a scene, so
  9. if the ones in that room then move - the lights will dim back to the current state by applying the
  10. stored scene.
  11. - a parallel thread that will listen for light changes, e.g: The automation is triggered multiple times
  12. whenever anyone is in the room. If you have adjusted the light level or turned off any lights while
  13. the automation is running, these changes are stored to the scene, so the lights will stay in same state.
  14. - «wait for action» either before the lights are dimmed or while the lights are dimmed down. This is
  15. perfect to use when the house state is in «cleaning mode», while someone is showering (high humdity)
  16. and the motion sensor can't register motion because of the steam etc.
  17. input:
  18. trigger_sensors:
  19. name: Trigger sensors
  20. description: >-
  21. Sensors that will trigger this automation. It can be a motion sensor, door sensor etc. Any areas
  22. or devices that is given in this input will be crawled for binary_sensors, so it's recommended
  23. to only list entities explicitly with their entity_id.
  24. selector:
  25. target:
  26. entity:
  27. domain: binary_sensor
  28. synced_lights:
  29. name: Synced lights
  30. description: >-
  31. Lights that will syncronize the trigger sensors. Any areas or devices that is given in this input
  32. will be crawled for light entities, so it's recommended to only list entities explicitly with their
  33. entity_id.
  34. selector:
  35. target:
  36. entity:
  37. domain: light
  38. illuminance_sensors:
  39. name: Lux sensors
  40. description: >-
  41. Lux (illuminance) sensor to monitor. If the threshold are met, the lights will stay off. The average
  42. illuminance will be calculated if several devices is set.
  43. default: []
  44. selector:
  45. target:
  46. entity:
  47. domain: sensor
  48. device_class: illuminance
  49. illuminance_threshold:
  50. name: Lux threshold
  51. description: >-
  52. Threshold before lights are turned on. Default (-1) means that the lux threshold won't be considered.
  53. default: -1.0
  54. selector:
  55. number:
  56. max: 100.0
  57. min: -1.0
  58. step: 0.1
  59. invalidate_scene:
  60. name: Invalidate scene
  61. description: >-
  62. Invalidate scene after lights is turned completely off. This is great if you want to be able
  63. to turn off certain lights while you're in the room, but when no-one is there for the «delay»
  64. and the lights will be turned OFF it won't restore the scene but restore every light to the
  65. state stored in their internal memory.
  66. default: "{{ true }}"
  67. selector:
  68. boolean:
  69. dim_percentage:
  70. name: Dim-percentage
  71. description: >-
  72. Dim lights to this level (%) of current light level before turning lights off. Current level
  73. will be stored in a scene. Setting this to 0.0 or 1.0 will skip this action.
  74. default: 0.3
  75. selector:
  76. number:
  77. max: 1.0
  78. min: 0.0
  79. step: 0.01
  80. after_wait_actions:
  81. name: Wait actions
  82. description: >-
  83. Actions to run to delay further, e.g wait until media player state is off, housekeeping mode
  84. is off, humidity is back down etc.
  85. default: []
  86. selector:
  87. action:
  88. wait_actions_before_dim:
  89. name: Wait actions before dim
  90. description: >-
  91. Run wait actions before dimming down (not turning off). Set to false/OFF if you want wait
  92. actions to run after dimming instead, but before turning OFF completely.
  93. default: "{{ true }}"
  94. selector:
  95. boolean:
  96. delay:
  97. name: Delay
  98. description: >-
  99. Time (minutes) to wait to turn off after last movement was detected.
  100. selector:
  101. number:
  102. min: 0.0
  103. max: 300.0
  104. step: 5.0
  105. default: 3
  106. mode: restart
  107. trigger_variables:
  108. trigger_sensors_input: !input trigger_sensors
  109. trigger_sensors: >-
  110. {% set sensors = namespace(entities=[]) %}
  111. {% if 'entity_id' in trigger_sensors_input %}
  112. {% for entity in ([ trigger_sensors_input['entity_id'] ] if trigger_sensors_input['entity_id'] is string else trigger_sensors_input['entity_id']) %}
  113. {% set sensors.entities = sensors.entities + [entity] %}
  114. {% endfor %}
  115. {% endif %}
  116. {% if 'area_id' in trigger_sensors_input %}
  117. {% for area in ([ trigger_sensors_input['area_id'] ] if trigger_sensors_input['area_id'] is string else trigger_sensors_input['area_id']) %}
  118. {% for entity in area_entities(area) if entity.startswith('binary_sensor.') %}
  119. {% set sensors.entities = sensors.entities + [entity] %}
  120. {% endfor %}
  121. {% endfor %}
  122. {% endif %}
  123. {% if 'device_id' in trigger_sensors_input %}
  124. {% for device in ([ trigger_sensors_input['device_id'] ] if trigger_sensors_input['device_id'] is string else trigger_sensors_input['device_id']) %}
  125. {% for entity in device_entities(device) if entity.startswith('binary_sensor.') %}
  126. {% set sensors.entities = sensors.entities + [entity] %}
  127. {% endfor %}
  128. {% endfor %}
  129. {% endif %}
  130. {{ sensors.entities|unique|list }}
  131. scene_name: >-
  132. {{ this.entity_id.replace('.', '_') }}
  133. synced_lights_inputs: !input synced_lights
  134. synced_lights: >-
  135. {% set lights = namespace(entities=[]) %}
  136. {% if 'entity_id' in synced_lights_inputs %}
  137. {% for entity in ([ synced_lights_inputs['entity_id'] ] if synced_lights_inputs['entity_id'] is string else synced_lights_inputs['entity_id']) %}
  138. {% set lights.entities = lights.entities + [entity] %}
  139. {% endfor %}
  140. {% endif %}
  141. {% if 'area_id' in synced_lights_inputs %}
  142. {% for area in ([ synced_lights_inputs['area_id'] ] if synced_lights_inputs['area_id'] is string else synced_lights_inputs['area_id']) %}
  143. {% for entity in area_entities(area) if entity.startswith('light.') %}
  144. {% set lights.entities = lights.entities + [entity] %}
  145. {% endfor %}
  146. {% endfor %}
  147. {% endif %}
  148. {% if 'device_id' in synced_lights_inputs %}
  149. {% for device in ([ synced_lights_inputs['device_id'] ] if synced_lights_inputs['device_id'] is string else synced_lights_inputs['device_id']) %}
  150. {% for entity in device_entities(device) if entity.startswith('light.') %}
  151. {% set lights.entities = lights.entities + [entity] %}
  152. {% endfor %}
  153. {% endfor %}
  154. {% endif %}
  155. {{ lights.entities|unique|list|sort }}
  156. trigger:
  157. ############ Hack for ############
  158. # - platform: state #
  159. # entity_id: "{{ sensors }}" #
  160. ##################################
  161. - platform: template
  162. value_template: >-
  163. {% set sensors = namespace(triggered=false) %}
  164. {% for sensor in trigger_sensors if is_state(sensor, 'on') %}
  165. {% set sensors.triggered = true %}
  166. {% endfor %}
  167. {{ sensors.triggered }}
  168. - platform: event
  169. event_type: "motion_detected_lights"
  170. event_data:
  171. end: "{{ scene_name }}"
  172. variables:
  173. illuminance_sensors_input: !input illuminance_sensors
  174. illuminance_sensors: >-
  175. {% set sensors = namespace(entities=[]) %}
  176. {% if 'entity_id' in illuminance_sensors_input %}
  177. {% for entity in ([ illuminance_sensors_input['entity_id'] ] if illuminance_sensors_input['entity_id'] is string else illuminance_sensors_input['entity_id']) %}
  178. {% set sensors.entities = sensors.entities + [entity] %}
  179. {% endfor %}
  180. {% endif %}
  181. {% if 'area_id' in illuminance_sensors_input %}
  182. {% for area in ([ illuminance_sensors_input['area_id'] ] if illuminance_sensors_input['area_id'] is string else illuminance_sensors_input['area_id']) %}
  183. {% for entity in area_entities(area) if state_attr(entity, 'device_class') == 'illuminance' %}
  184. {% set sensors.entities = sensors.entities + [entity] %}
  185. {% endfor %}
  186. {% endfor %}
  187. {% endif %}
  188. {% if 'device_id' in trigger_sensors_input %}
  189. {% for device in ([ illuminance_sensors_input['device_id'] ] if illuminance_sensors_input['device_id'] is string else illuminance_sensors_input['device_id']) %}
  190. {% for entity in device_entities(device) if state_attr(entity, 'device_class') == 'illuminance' %}
  191. {% set sensors.entities = sensors.entities + [entity] %}
  192. {% endfor %}
  193. {% endfor %}
  194. {% endif %}
  195. {{ sensors.entities|unique|list }}
  196. illuminance_threshold: !input illuminance_threshold
  197. invalidate_scene: !input invalidate_scene
  198. invalid_light: >- # Used to invalidate scene as there's no other way to do it at the moment
  199. {{ states.light|rejectattr('entity_id', 'in', synced_lights)|map(attribute='entity_id')|first }}
  200. log_level: warning
  201. dim_percentage: !input dim_percentage
  202. delay_minutes: !input delay
  203. delay_seconds: "{{ delay_minutes * 60 }}"
  204. wait_actions_before_dim: !input wait_actions_before_dim
  205. action:
  206. - alias: "Triggered on motion or event"
  207. if: "{{ trigger.platform != 'event' }}"
  208. then:
  209. - alias: "Test if threshold is met"
  210. choose:
  211. conditions:
  212. - condition: template
  213. value_template: "{{ illuminance_threshold != -1 and illuminance_sensors|count > 0 }}"
  214. - condition: template
  215. value_template: >- # If any lights are on... Continue so they eventually will turn off
  216. {% set lights = namespace(on=false) %}
  217. {% for light in synced_lights if is_state(light, 'on') %}
  218. {% set lights.on = true %}
  219. {% endfor %}
  220. {{ not lights.on }}
  221. - condition: template
  222. value_template: >-
  223. {% set lux = namespace(values=[]) %}
  224. {% for sensor in illuminance_sensors %}
  225. {% set lux.values = lux.values + [ states(sensor)|float ] %}
  226. {% endfor %}
  227. {{ (lux.values|sum / lux.values|count)|int > illuminance_threshold }}
  228. sequence:
  229. stop: "Illuminance threshold met"
  230. - alias: "Motion: Turn on exisiting scene or turn on lights."
  231. continue_on_error: true # if scenes are empty (might happen after a scene.reload)
  232. choose:
  233. conditions: "{{ states.scene | selectattr('attributes.friendly_name', 'eq', scene_name) | list | count == 1 and synced_lights == state_attr('scene.' + scene_name, 'entity_id')|sort }}"
  234. sequence: # Scene do exist and is valid
  235. - service: system_log.write
  236. data:
  237. level: "{{ log_level }}"
  238. logger: "{{ scene_name }}"
  239. message: >-
  240. Scene «{{ scene_name }}» already exists and contains all lights. Restore scene.
  241. - alias: "Motion: Turn on existing scene for synced lights"
  242. service: scene.turn_on
  243. target:
  244. entity_id: "scene.{{scene_name}}"
  245. default: # Scene does not exist
  246. - service: system_log.write
  247. continue_on_error: true
  248. data:
  249. level: "{{ log_level }}"
  250. logger: "{{ scene_name }}"
  251. message: >-
  252. Scene «{{ scene_name }} does NOT exist or is INVALID.
  253. Entities to sync: {{ synced_lights }} => Turn ON every light sourcce.
  254. - alias: "Motion: Turn on synced lights"
  255. service: light.turn_on
  256. target:
  257. entity_id: "{{ synced_lights }}"
  258. - alias: >-
  259. Listen for light changes and motion in parallel. Save snapshots to scene on
  260. light changes. we dont restore light every time the motion is triggered without
  261. being OFF. End this part of automation when motion stops.
  262. parallel:
  263. - alias: >-
  264. Listen for light changes for as long as this automation lives,
  265. means: until event is fired and automation is restarted.
  266. repeat:
  267. while: "{{ true }}"
  268. sequence:
  269. - alias: "Store variable with current light values (on or brightness)"
  270. variables:
  271. synced_lights_values: >-
  272. {% set lights = namespace(values=[]) %}
  273. {% for light in synced_lights %}
  274. {%if is_state_attr(light, 'color_mode', 'brightness') %}
  275. {% set lights.values = lights.values + [state_attr(light, 'brightness')] %}
  276. {% else %}
  277. {% set lights.values = lights.values + [states(light)] %}
  278. {% endif %}
  279. {% endfor %}
  280. {{ lights.values }}
  281. - alias: "Wait for change in any light source"
  282. wait_template: >-
  283. {% set lights = namespace(changed=false) %}
  284. {% for light in synced_lights %}
  285. {%if is_state_attr(light, 'color_mode', 'brightness') and state_attr(light, 'brightness') != synced_lights_values[loop.index - 1] %}
  286. {% set lights.changed = true %}
  287. {% elif not is_state_attr(light, 'color_mode', 'brightness') and states(light) != synced_lights_values[loop.index - 1] %}
  288. {% set lights.changed = true %}
  289. {% endif %}
  290. {% endfor %}
  291. {{ lights.changed }}
  292. # Check here if values = 0 / OFF
  293. - service: system_log.write
  294. data:
  295. level: "{{ log_level }}"
  296. logger: "{{ scene_name }}"
  297. message: >-
  298. Light levels changed: {{ synced_lights }} from {{ synced_lights_values }}.
  299. Saving snapshot to scene «{{ scene_name }}».
  300. - alias: "Store variable a variable with ON lights."
  301. variables:
  302. synced_lights_on: >-
  303. {% set lights = namespace(on=[]) %}
  304. {% for light in synced_lights if not is_state(light, 'off') %}
  305. {% set lights.on = lights.on + [light] %}
  306. {% endfor %}
  307. {{ lights.on }}
  308. - choose:
  309. conditions: "{{ synced_lights_on|count == 0 }}"
  310. sequence:
  311. service: system_log.write
  312. data:
  313. level: "{{ log_level }}"
  314. logger: "{{ scene_name }}"
  315. message: "No lights on: invalidating with {{ invalid_light }} which isn't in {{ synced_lights }}."
  316. - alias: "Save scene for current change and return to loop. If no lights are ON; invalidate scene."
  317. service: scene.create
  318. data:
  319. scene_id: "{{ scene_name }}"
  320. snapshot_entities: >-
  321. {{ synced_lights if synced_lights_on|count != 0 else [ invalid_light ] }}
  322. - alias: >-
  323. Listen for motion change, and wait for it to become 'OFF',
  324. wait for the delay and actions to finish, and then send event to kill parallelism.
  325. sequence: # @ignore: Missing property "condition"
  326. - alias: "Wait for motion to end."
  327. wait_template: >-
  328. {% set sensors = namespace(triggered=false) %}
  329. {% for sensor in trigger_sensors if is_state(sensor, 'on') %}
  330. {% set sensors.triggered = true %}
  331. {% endfor %}
  332. {{ not sensors.triggered }}
  333. - service: system_log.write
  334. data:
  335. level: "{{ log_level }}"
  336. logger: "{{ scene_name }}"
  337. message: >-
  338. No motion detected by {{ trigger_sensors }}. Run wait actions
  339. wait {{ (delay_seconds * 1/3)|int }} seconds,, then end
  340. automation (parallelism) by issuing an event, and let automation «re-trigger».
  341. - alias: "Run and wait for actions to finish"
  342. choose:
  343. conditions: >-
  344. {{ wait_actions_before_dim }}
  345. sequence: !input after_wait_actions
  346. - alias: "Wait for delay to fire event."
  347. delay:
  348. seconds: "{{ (delay_seconds * 2/3)|int }}"
  349. - alias: "Ending: Fire event to restart automation and kill parallel processes"
  350. event: "motion_detected_lights"
  351. event_data:
  352. end: "{{ scene_name }}"
  353. else: # Event was fired
  354. - service: system_log.write
  355. data:
  356. level: "{{ log_level }}"
  357. logger: "{{ scene_name }}"
  358. message: "Ending {{ scene_name }}, dim down and eventually turn off {{ synced_lights }}."
  359. - alias: "Calculate average brightness and set dim level"
  360. variables:
  361. brightness: >-
  362. {% set brightness = namespace(levels=[]) %}
  363. {% for light in synced_lights if is_state(light, 'on') %}
  364. {% if is_state_attr(light, 'color_mode', 'brightness') and state_attr(light, 'brightness')|int != 0 %}
  365. {% set brightness.levels = brightness.levels + [ state_attr(light, 'brightness') ] %}
  366. {% else %}
  367. {% set brightness.levels = brightness.levels + [ 100 ] %}
  368. {% endif %}
  369. {% endfor %}
  370. {{ 0.0 if brightness.levels|length == 0 else brightness.levels|sum / brightness.levels|length }}
  371. dim_level: "{{ (brightness * dim_percentage)|int }}"
  372. - service: system_log.write
  373. data:
  374. level: "{{ log_level }}"
  375. logger: "{{ scene_name }}"
  376. message: >-
  377. Light avg-brightness is: {{ brightness }}, now dimming lights
  378. with {{ (dim_percentage * 100)|int }}% to {{ dim_level }}".
  379. - alias: "Dim lights to a low brightness to alert no motion"
  380. service: light.turn_on
  381. target:
  382. entity_id: >-
  383. {{ synced_lights }}
  384. data:
  385. brightness: "{{ dim_level }}"
  386. - alias: "Run and wait for actions to finish"
  387. choose:
  388. conditions: >-
  389. {{ not wait_actions_before_dim }}
  390. sequence: !input after_wait_actions
  391. - alias: "Wait for dim to be recognized by anyone before turning off lights"
  392. delay:
  393. seconds: "{{ (delay_seconds * 1/3)|int }}"
  394. - alias: "Turn off lights"
  395. service: light.turn_off
  396. target:
  397. entity_id: "{{ synced_lights }}"
  398. - alias: "Invalidate scene if TRUE"
  399. condition: template
  400. value_template: "{{ invalidate_scene is true }}"
  401. - alias: "Invalidate scene."
  402. service: scene.create
  403. data:
  404. scene_id: "{{ scene_name }}"
  405. snapshot_entities: "{{ [ invalid_light ] }}"