notify_user.yaml 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. blueprint:
  2. domain: script
  3. name: Notify user
  4. description: >-
  5. A script blueprint that takes context into consideration where you
  6. dont have to think about the parallelizm.
  7. However, you should take into account that this script might live
  8. «forever», if you're not cautious. It will spawn into three services,
  9. where 2 of them might live until a script reload or a HA restart.
  10. However 2, this script is perfect if you know what you're doing and dont
  11. want to deal with the hassle of «contexting». E.g if you want to push the
  12. same thing to multiple devices, and let them do actions based on the data.
  13. Here's an example of a «happy birthday» script that will notify every device
  14. and let each device action on it (send an sms or dismiss it) without affecting
  15. other users.
  16. Example usage
  17. ``` # (create a script with this )
  18. script:
  19. notify_device:
  20. alias: Notify device
  21. fields:
  22. [add fields from blueprint here]
  23. max: 200
  24. use_blueprint:
  25. path: notify_user.yaml
  26. input:
  27. notify_device: "{{ notify_device }}"
  28. timeout: "{{ timeout if timeout is defined else 0 }}"
  29. data: "{{ data }}"
  30. action_scripts: "{{ action_scripts if action_scripts is defined else {} }}"
  31. ```
  32. and call it:
  33. ```
  34. service: script.turn_on
  35. target:
  36. entity_id: script.notify_device
  37. data:
  38. variables:
  39. notify_device: "{{ notify.mobile_app_device }}"
  40. data: (data as a normal "notify"-service)
  41. message: "..."
  42. data:
  43. actions:
  44. - action: DO_IT_NOW_DISMISS_ALL #Ending action with DISMISS_NOW will remove it from other devices.
  45. title: Do it now
  46. action_scripts:
  47. DO_IT_NOW_DISMISS_ALL:
  48. script: "script.my_script" #script to run if user clicks "Do it now"
  49. variables:
  50. var1: "Variables my script need"
  51. ```
  52. Feel free to use it within a loop too:
  53. ```
  54. repeat:
  55. for_each: >-
  56. - device_1
  57. - device_2
  58. sequence:
  59. - service: script.turn_on
  60. [...]
  61. data:
  62. variables:
  63. notify_device: "{{ repeat.item }}"
  64. ```
  65. input:
  66. notify_device:
  67. name: "Device to notify"
  68. description: >-
  69. The name of the notify service, e.g service.mobile_app_x
  70. selector:
  71. object:
  72. timeout:
  73. name: "Timeout"
  74. description: >-
  75. Timeout before clearing notification and stop waiting. 0 equals to never.
  76. Respects «Days» too in YAML mode. Timeout set here will override `data.data.timeout`
  77. and visa-versa.
  78. selector:
  79. duration:
  80. # enable_days: true (HA fire an error on this.)
  81. data:
  82. name: "Message data"
  83. description: "Equal to data field in the notify service"
  84. selector:
  85. object:
  86. # Scripts to run based on `data->actions` set in notify-service. E.g:
  87. # ```
  88. # data:
  89. # actions:
  90. # - action: ACTION_ANCHOR
  91. # title: This test
  92. # ```
  93. # with matching:
  94. # ```
  95. # action_scripts:
  96. # ACTION_ANCHOR: script...
  97. # ```
  98. # The format for `ACTION_ANCHOR` can be just a string, referencing a script,
  99. # or an object with the following format:
  100. # ```
  101. # ACTION_ANCHOR:
  102. # script: script..
  103. # variables:
  104. # var1: ....
  105. # ```
  106. action_scripts:
  107. name: Action scripts
  108. description: >-
  109. Read `action_scripts` comments within blueprint.
  110. selector:
  111. object:
  112. default: {}
  113. tts:
  114. name: "TTS"
  115. description: "Not used yet"
  116. selector:
  117. boolean:
  118. default: "{{ false }}"
  119. mode: parallel
  120. variables:
  121. input_notify_device: !input notify_device
  122. input_timeout: !input timeout
  123. input_data: !input data
  124. action_scripts: !input action_scripts
  125. sequence:
  126. - variables:
  127. device_name: "{{ input_notify_device.replace('notify.mobile_app_', '') }}"
  128. ctx: "{{ context.id ~ '_' ~ device_name }}"
  129. device_id: >-
  130. {{ dict.from_keys(device_attr(device_id('device_tracker.' ~ device_name), 'identifiers')).mobile_app }}
  131. update_timeout: >-
  132. {% if 'data' in input_data and 'timeout' in input_data.data %}
  133. {{ input_data.data.timeout|int }}
  134. {% else %}
  135. {% set seconds = 0 %}
  136. {% if input_timeout is iterable %}
  137. {% if 'seconds' in input_timeout %}
  138. {% set seconds = seconds + input_timeout.seconds|int %}
  139. {% endif %}
  140. {% if 'minutes' in input_timeout %}
  141. {% set seconds = seconds + (input_timeout.minutes|int * 60 * 60) %}
  142. {% endif %}
  143. {% if 'hours' in input_timeout %}
  144. {% set seconds = seconds + ((input_timeout.hours|int) * 60 * 60) %}
  145. {% endif %}
  146. {% if 'days' in input_timeout %}
  147. {% set seconds = seconds + ((input_timeout.days|int) * 60 * 60 * 24) %}
  148. {% endif %}
  149. {% elif input_timeout is not iterable %}
  150. {% set seconds = input_timeout|int %}
  151. {% endif %}
  152. {{ false if seconds <= 0 else (now().timestamp() + seconds) }}
  153. {% endif %}
  154. update_data: >-
  155. {% set data_data = (input_data.data.items() | list) if 'data' in input_data else [] %}
  156. {% if 'data' in input_data %}
  157. {% if 'actions' in input_data.data %}
  158. {% set action = namespace(entities=[]) %}
  159. {% for a in input_data.data.actions %}
  160. {% set update = {"action": ctx ~ '_' ~ a.action}.items() | list %}
  161. {% set current = a.items() | list | rejectattr(
  162. '0', 'eq', update | map(attribute='0') | list
  163. ) | list %}
  164. {% set action.entities = action.entities + [
  165. dict.from_keys(current + update)
  166. ] %}
  167. {% endfor %}
  168. {% set actions = { "actions": action.entities }.items() | list %}
  169. {% set data_data = dict.from_keys(data_data | rejectattr(
  170. '0', 'eq', actions | map(attribute='0') | list
  171. ) | list + actions).items() | list
  172. %}
  173. {% endif %}
  174. {% if 'timeout' not in input_data and update_timeout is not false %}
  175. {% set add = { "timeout": update_timeout|int }.items() | list %}
  176. {% set data_data = dict.from_keys(data_data | rejectattr(
  177. '0', 'eq', add | map(attribute='0') | list
  178. ) | list + add).items() | list
  179. %}
  180. {% endif %}
  181. {% if 'group' not in input_data.data %}
  182. {% set add = { "group": "default-group" }.items() | list %}
  183. {% set data_data = dict.from_keys(data_data | rejectattr(
  184. '0', 'eq', add | map(attribute='0') | list
  185. ) | list + add).items() | list
  186. %}
  187. {% endif %}
  188. {% if (
  189. (
  190. 'alert_once' in input_data.data or
  191. 'actions' in input_data.data or
  192. 'persistent' in input_data.data
  193. ) and 'tag' not in input_data.data
  194. ) or (
  195. update_timeout is not false and 'tag' not in input_data.data
  196. )
  197. %}
  198. {% set add = { "tag": "tag_" + ctx }.items() | list %}
  199. {% set data_data = dict.from_keys(data_data | rejectattr(
  200. '0', 'eq', add | map(attribute='0') | list
  201. ) | list + add).items() | list
  202. %}
  203. {% endif %}
  204. {% else %}
  205. {% if update_timeout is not false %}
  206. {% set add = { "tag": "tag_" + ctx, 'timeout': update_timeout|int }.items() | list %}
  207. {% set data_data = dict.from_keys(data_data | rejectattr(
  208. '0', 'eq', add | map(attribute='0') | list
  209. ) | list + add).items() | list
  210. %}
  211. {% endif %}
  212. {% endif %}
  213. {% if data_data|length != 0 %}
  214. {% set data_data = {"data": dict.from_keys(data_data)}.items() | list %}
  215. {{ dict.from_keys((input_data.items() | list | rejectattr(
  216. '0', 'eq', data_data | map(attribute='0') | list
  217. ) | list) + data_data) }}
  218. {% else %}
  219. {{ input_data }}
  220. {% endif %}
  221. action_handlers: >-
  222. {% set actions = namespace(handlers=[]) %}
  223. {% for ask in action_scripts.keys() %}
  224. {% set askc = ctx ~ '_' ~ ask %}
  225. {% for action in update_data.data.actions if askc == action.action %}
  226. {% set actions.handlers = actions.handlers + [(
  227. askc, action_scripts[ask]
  228. )] %}
  229. {% endfor %}
  230. {% endfor %}
  231. {{ dict.from_keys(actions.handlers) }}
  232. - alias: "Parallize event and action listeners"
  233. parallel:
  234. - alias: "Listen for event if actions are given"
  235. if: "{{ action_handlers|length > 0 or update_timeout is not false }}"
  236. then:
  237. - alias: "Loop for events until criterias are met"
  238. repeat:
  239. while: "{{ (update_timeout - now().timestamp() > 0) or update_timeout == 0 }}"
  240. sequence:
  241. - if: "{{ update_timeout == 0 }}"
  242. then:
  243. alias: "Wait for app event, without timeout"
  244. wait_for_trigger:
  245. - platform: event
  246. event_type:
  247. - mobile_app_notification_action
  248. - mobile_app_notification_cleared
  249. event_data:
  250. device_id: "{{ device_id }}"
  251. - platform: event
  252. event_type: 'context_notification_clear'
  253. event_data:
  254. context: "{{ context.id }}"
  255. else:
  256. alias: "Wait for app event, with timeout"
  257. wait_for_trigger:
  258. - platform: event
  259. event_type:
  260. - mobile_app_notification_action
  261. - mobile_app_notification_cleared
  262. event_data:
  263. device_id: "{{ device_id }}"
  264. - platform: event
  265. event_type: 'context_notification_clear'
  266. event_data:
  267. context: "{{ context.id }}"
  268. timeout: >-
  269. {{ update_timeout - now().timestamp() }}
  270. - if: "{{ wait.trigger is none }}"
  271. then:
  272. alias: "Reached timeout, ending."
  273. stop: "Reached timeout, ending."
  274. - alias: "Check if the notification was cleared (completed) by another device."
  275. if: "{{ wait.trigger.event.event_type == 'context_notification_clear' }}"
  276. then:
  277. - alias: "Clear notification on other devices, task was completed"
  278. service: "{{ input_notify_device }}"
  279. data:
  280. message: "clear_notification"
  281. data:
  282. tag: "{{ update_data.data.tag }}"
  283. - alias: "And give a 20 seconds notification on other devices that it was cleared."
  284. service: "{{ input_notify_device }}"
  285. data:
  286. title: "Notification completed"
  287. message: >-
  288. {{ wait.trigger.event.data.user.split(' ')[0] }} completed «{{
  289. (update_data.title if 'title' else update_data.message)[0:20]
  290. }}...» ✔
  291. data:
  292. visibility: public
  293. importance: low
  294. color: "#688840"
  295. timeout: 20
  296. notification_icon: >-
  297. {{ update_data.data.notification_icon if 'data' in update_data and 'notification_icon' in update_data.data else 'mdi:checkbox-marked-circle-plus-outline' }}
  298. - alias: "Notification cleared by another user, ending."
  299. stop: "Notification cleared by another user, ending."
  300. - alias: "Check that the notification is within this context"
  301. condition: >-
  302. {% set in_ctx = namespace(bool=false) %}
  303. {% for key in wait.trigger.event.data.keys() if key.startswith('action') and key.endswith('key') %}
  304. {% if wait.trigger.event.data[key].startswith(ctx) %}
  305. {% set in_ctx.bool = true %}
  306. {% endif %}
  307. {% endfor %}
  308. {{ in_ctx.bool }}
  309. - if: "{{ wait.trigger.event.event_type.endswith('_cleared') }}"
  310. then:
  311. alias: "User cleared notification, stop listening for events."
  312. stop: "User cleared notification, stop listening for events."
  313. - if: "{{ wait.trigger.event.data.action.endswith('_DISMISS_ALL') }}"
  314. then:
  315. event: context_notification_clear
  316. event_data:
  317. context: "{{ context.id }}"
  318. user: '{{ states.person|selectattr("attributes.user_id", "==", wait.trigger.event.context.user_id)|map(attribute="attributes.friendly_name")|first }}'
  319. - variables:
  320. action: >-
  321. {{ action_handlers[ wait.trigger.event.data.action ]|default(false) }}
  322. - alias: "Check if the action is associated with a script"
  323. if: "{{ not action }}"
  324. then:
  325. - stop: "Action is not associated with any scripts, ending."
  326. - if: "{{ action is string or 'variables' not in action }}"
  327. then:
  328. service: script.turn_on
  329. target:
  330. entity_id: "{{ script }}"
  331. else:
  332. service: script.turn_on
  333. target:
  334. entity_id: "{{ action.script }}"
  335. data:
  336. variables: "{{ action.variables }}"
  337. - alias: Send message to device
  338. service: "{{ input_notify_device }}"
  339. # @ignore: Incorrect type. Expected "object"
  340. data: "{{ update_data }}"