motion_detected_lights.yaml 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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. entity:
  26. domain: binary_sensor
  27. multiple: true
  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. entity:
  36. domain: light
  37. illuminance_sensors:
  38. name: Lux sensors
  39. description: >-
  40. Lux (illuminance) sensor to monitor. If the threshold are met, the lights will stay off. The average
  41. illuminance will be calculated if several devices is set.
  42. default: []
  43. selector:
  44. entity:
  45. domain: sensor
  46. device_class: illuminance
  47. illuminance_threshold:
  48. name: Lux threshold
  49. description: >-
  50. Threshold before lights are turned on. Default (-1) means that the lux threshold won't be considered.
  51. default: -1.0
  52. selector:
  53. number:
  54. max: 100.0
  55. min: -1.0
  56. step: 0.1
  57. dim_percentage:
  58. name: Dim-percentage
  59. description: >-
  60. Dim lights to this level (%) of current light level before turning lights off. Current level
  61. will be stored in a scene. Setting this to 0.0 or 1.0 will skip this action.
  62. default: 0.3
  63. selector:
  64. number:
  65. max: 1.0
  66. min: 0.0
  67. step: 0.01
  68. after_wait_actions:
  69. name: Wait actions
  70. description: >-
  71. Actions to run to delay further, e.g wait until media player state is off, housekeeping mode
  72. is off, humidity is back down etc.
  73. default: []
  74. selector:
  75. action:
  76. wait_actions_before_dim:
  77. name: Wait actions before dim
  78. description: >-
  79. Run wait actions before dimming down (not turning off). Set to false/OFF if you want wait
  80. actions to run after dimming instead, but before turning OFF completely.
  81. default: "{{ true }}"
  82. selector:
  83. boolean:
  84. delay:
  85. name: Delay
  86. description: >-
  87. Time (minutes) to wait to turn off after last movement was detected.
  88. selector:
  89. number:
  90. min: 0.0
  91. max: 300.0
  92. step: 5.0
  93. default: 3
  94. mode: restart
  95. trigger_variables:
  96. trigger_sensors_input: !input trigger_sensors
  97. trigger_sensors: >-
  98. {{ trigger_sensors_input|unique|sort }}
  99. scene_name: >-
  100. {{ this.entity_id.replace('.', '_') }}
  101. synced_lights_input: !input synced_lights
  102. synced_lights: >-
  103. {{ synced_lights_input|unique|sort }}
  104. trigger:
  105. - platform: state
  106. # from: "off"
  107. to: "on"
  108. entity_id: !input trigger_sensors
  109. - platform: event
  110. event_type: "motion_detected_lights"
  111. event_data:
  112. end: "{{ scene_name }}"
  113. variables:
  114. illuminance_sensors_input: !input illuminance_sensors
  115. illuminance_sensors: >-
  116. {{ illuminance_sensors_input|unique|sort }}
  117. illuminance_threshold: !input illuminance_threshold
  118. invalid_light: >- # Used to invalidate scene as there's no other way to do it at the moment
  119. {{ states.light|rejectattr('entity_id', 'in', synced_lights)|map(attribute='entity_id')|first }}
  120. log_level: warning
  121. dim_percentage: !input dim_percentage
  122. delay_minutes: !input delay
  123. delay_seconds: "{{ delay_minutes * 60 }}"
  124. wait_actions_before_dim: !input wait_actions_before_dim
  125. exclude: ["off", "unavailable", "unknown"]
  126. action:
  127. - alias: "Triggered on motion or event"
  128. if: "{{ trigger.platform != 'event' }}"
  129. then:
  130. - alias: "Test if threshold is met"
  131. choose:
  132. conditions:
  133. - condition: template
  134. value_template: "{{ illuminance_threshold != -1 and illuminance_sensors|count > 0 }}"
  135. - condition: template
  136. value_template: >- # If any lights are on... Continue so they eventually will turn off
  137. {% set lights = namespace(on=false) %}
  138. {% for light in synced_lights if is_state(light, 'on') %}
  139. {% set lights.on = true %}
  140. {% endfor %}
  141. {{ not lights.on }}
  142. - condition: template
  143. value_template: >-
  144. {% set lux = namespace(values=[]) %}
  145. {% for sensor in illuminance_sensors %}
  146. {% set lux.values = lux.values + [ states(sensor)|float ] %}
  147. {% endfor %}
  148. {{ (lux.values|sum / lux.values|count)|int > illuminance_threshold }}
  149. sequence:
  150. stop: "Illuminance threshold met"
  151. - service: system_log.write
  152. data:
  153. level: warning
  154. logger: "{{ scene_name }}"
  155. message: "Lights: {{ synced_lights }} vs {{ synced_lights_input }}. Invalid light ({{ invalid_light }})."
  156. - alias: "Motion: Turn on exisiting scene or turn on lights."
  157. continue_on_error: true # if scenes are empty (might happen after a scene.reload)
  158. choose:
  159. conditions: "{{ states.scene|selectattr('attributes.friendly_name', 'eq', scene_name)|list|count == 1 and invalid_light not in state_attr('scene.' + scene_name, 'entity_id') }}"
  160. sequence: # Scene do exist and is valid
  161. - service: system_log.write
  162. data:
  163. level: "{{ log_level }}"
  164. logger: "{{ scene_name }}"
  165. message: >-
  166. Scene «{{ scene_name }}» already exists and contains valid lights ({{ state_attr('scene.' + scene_name, 'entity_id') }}). Restore scene.
  167. - alias: "Motion: Turn on existing scene for synced lights"
  168. service: scene.turn_on
  169. target:
  170. entity_id: "scene.{{scene_name}}"
  171. default: # Scene does not exist
  172. - service: system_log.write
  173. continue_on_error: true
  174. data:
  175. level: "{{ log_level }}"
  176. logger: "{{ scene_name }}"
  177. message: >-
  178. Scene «{{ scene_name }} does NOT exist or is INVALID.
  179. Entities to sync: {{ synced_lights }} => Turn ON every light source.
  180. - alias: "Motion: Turn on synced lights"
  181. service: light.turn_on
  182. target:
  183. entity_id: "{{ synced_lights }}"
  184. - alias: >-
  185. Listen for light changes and motion in parallel. Save snapshots to scene on
  186. light changes. We dont restore light every time the motion is triggered without
  187. being OFF. End this part of automation when motion stops.
  188. parallel:
  189. - alias: >-
  190. Listen for light changes for as long as this automation lives,
  191. means: until event is fired and automation is restarted.
  192. repeat:
  193. while: "{{ true }}"
  194. sequence:
  195. - alias: "Store variable with current light values (on or brightness)"
  196. variables:
  197. synced_lights_values: >-
  198. {% set lights = namespace(values=[]) %}
  199. {% for light in synced_lights %}
  200. {%if is_state_attr(light, 'supported_color_modes', 'brightness') %}
  201. {% set lights.values = lights.values + [state_attr(light, 'brightness')] %}
  202. {% else %}
  203. {% set lights.values = lights.values + [ states(light) ] %}
  204. {% endif %}
  205. {% endfor %}
  206. {{ lights.values }}
  207. - alias: "Wait for change in any light source"
  208. wait_template: >-
  209. {% set lights = namespace(changed=false) %}
  210. {% for light in synced_lights %}
  211. {%if is_state_attr(light, 'supported_color_modes', 'brightness') and not state_attr(light, 'brightness') == synced_lights_values[loop.index0] %}
  212. {% set lights.changed = true %}
  213. {% elif not states(light) == synced_lights_values[loop.index0] %}
  214. {% set lights.changed = true %}
  215. {% endif %}
  216. {% endfor %}
  217. {{ lights.changed }}
  218. - alias: "Store variable with new light values (on or brightness), for debuging purposes"
  219. variables:
  220. synced_lights_new_values: >-
  221. {% set lights = namespace(values=[]) %}
  222. {% for light in synced_lights %}
  223. {%if is_state_attr(light, 'supported_color_modes', 'brightness') %}
  224. {% set lights.values = lights.values + [state_attr(light, 'brightness')] %}
  225. {% else %}
  226. {% set lights.values = lights.values + [ states(light) ] %}
  227. {% endif %}
  228. {% endfor %}
  229. {{ lights.values }}
  230. # Check here if values = 0 / OFF
  231. - service: system_log.write
  232. data:
  233. level: "{{ log_level }}"
  234. logger: "{{ scene_name }}"
  235. message: >-
  236. Light levels changed: {{ synced_lights }}\n\nfrom:{{ synced_lights_values }}\n\nto:{{ synced_lights_new_values }}.
  237. Saving snapshot to scene «{{ scene_name }}».
  238. - alias: "Store a variable with ON lights."
  239. variables:
  240. synced_lights_on: >-
  241. {% set lights = namespace(on=[]) %}
  242. {% for light in synced_lights if states(light) not in exclude %}
  243. {% set lights.on = lights.on + [light] %}
  244. {% endfor %}
  245. {{ lights.on }}
  246. - choose:
  247. conditions: "{{ synced_lights_on|count == 0 }}"
  248. sequence:
  249. service: system_log.write
  250. data:
  251. level: "{{ log_level }}"
  252. logger: "{{ scene_name }}"
  253. message: "No lights on: invalidating with {{ invalid_light }} which isn't in {{ synced_lights }}."
  254. default:
  255. service: system_log.write
  256. data:
  257. level: "{{ log_level }}"
  258. logger: "{{ scene_name }}"
  259. message: "Lights on: {{ synced_lights_on }}."
  260. - alias: "Save scene for current change and return to loop. If no lights are ON; invalidate scene."
  261. service: scene.create
  262. data:
  263. scene_id: "{{ scene_name }}"
  264. snapshot_entities: >-
  265. {{ [ invalid_light ] if synced_lights_on|count == 0 else synced_lights_on }}
  266. - sequence: # @ignore: Missing property "condition"
  267. # alias: >-
  268. # Listen for motion change, and wait for it to become 'OFF',
  269. # wait for the delay and actions to finish, and then send event to kill parallelism.
  270. - alias: "Wait for motion to end."
  271. wait_template: >-
  272. {% set sensors = namespace(triggered=false) %}
  273. {% for sensor in trigger_sensors if is_state(sensor, 'on') %}
  274. {% set sensors.triggered = true %}
  275. {% endfor %}
  276. {{ not sensors.triggered }}
  277. - service: system_log.write
  278. data:
  279. level: "{{ log_level }}"
  280. logger: "{{ scene_name }}"
  281. message: >-
  282. No motion detected by {{ trigger_sensors }}. Run wait actions
  283. wait {{ (delay_seconds * 2/3)|int }} seconds,, then end
  284. automation (parallelism) by issuing an event, and let automation «re-trigger».
  285. - alias: "Run and wait for actions to finish"
  286. choose:
  287. conditions: >-
  288. {{ wait_actions_before_dim }}
  289. sequence: !input after_wait_actions
  290. - alias: "Wait for delay to fire event."
  291. delay:
  292. seconds: "{{ (delay_seconds * 2/3)|int }}"
  293. - alias: "Ending: Fire event to restart automation and kill parallel processes"
  294. event: "motion_detected_lights"
  295. event_data:
  296. end: "{{ scene_name }}"
  297. else: # Event was fired
  298. - service: system_log.write
  299. data:
  300. level: "{{ log_level }}"
  301. logger: "{{ scene_name }}"
  302. message: "Ending {{ scene_name }}, dim down and eventually turn off {{ synced_lights }}."
  303. - delay: 5 # Just to make sure parallel threads have ended
  304. - alias: "Calculate average brightness and set dim level"
  305. variables:
  306. brightness: >-
  307. {% set brightness = namespace(levels=[]) %}
  308. {% for light in synced_lights if is_state(light, 'on') %}
  309. {% if is_state_attr(light, 'supported_color_modes', 'brightness') and state_attr(light, 'brightness')|int != 0 %}
  310. {% set brightness.levels = brightness.levels + [ state_attr(light, 'brightness')|float ] %}
  311. {% else %}
  312. {% set brightness.levels = brightness.levels + [ 100 ] %}
  313. {% endif %}
  314. {% endfor %}
  315. {{ 0.0 if brightness.levels|length == 0 else brightness.levels|average }}
  316. dim_level: "{{ (brightness * dim_percentage)|round(2, 'ceil') }}"
  317. - service: system_log.write
  318. data:
  319. level: "{{ log_level }}"
  320. logger: "{{ scene_name }}"
  321. message: >-
  322. Light avg-brightness is: {{ brightness }}, now dimming lights
  323. with {{ (dim_percentage * 100)|int }}% to {{ dim_level }}.
  324. - alias: "Dim lights to lower brightness to alert no motion"
  325. service: light.turn_on
  326. target:
  327. entity_id: >-
  328. {{ synced_lights }}
  329. data:
  330. brightness: "{{ dim_level }}"
  331. - alias: "Run and wait for actions to finish"
  332. choose:
  333. conditions: >-
  334. {{ not wait_actions_before_dim }}
  335. sequence: !input after_wait_actions
  336. - alias: "Wait for dim to be recognized by anyone before turning off lights"
  337. delay:
  338. seconds: "{{ (delay_seconds * 1/3)|int }}"
  339. - alias: "Turn off lights"
  340. service: light.turn_off
  341. target:
  342. entity_id: "{{ synced_lights }}"
  343. - service: system_log.write
  344. data:
  345. level: "{{ log_level }}"
  346. logger: "{{ scene_name }}"
  347. message: >-
  348. Lights {{ synced_lights }} turned off.