notify_user.yaml 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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_idx: "{{ state_attr(this.entity_id, 'current')|default(1, true) }}"
  129. ctx: "{{ context.id ~ '_' ~ device_name ~ '_' ~ ctx_idx }}"
  130. device_id: >-
  131. {{ dict.from_keys(device_attr(device_id('device_tracker.' ~ device_name), 'identifiers')).mobile_app }}
  132. update_timeout: >-
  133. {% if 'data' in input_data and 'timeout' in input_data.data %}
  134. {{ input_data.data.timeout|int }}
  135. {% else %}
  136. {% set seconds = 0 %}
  137. {% if input_timeout is iterable %}
  138. {% if 'seconds' in input_timeout %}
  139. {% set seconds = seconds + input_timeout.seconds|int %}
  140. {% endif %}
  141. {% if 'minutes' in input_timeout %}
  142. {% set seconds = seconds + (input_timeout.minutes|int * 60 * 60) %}
  143. {% endif %}
  144. {% if 'hours' in input_timeout %}
  145. {% set seconds = seconds + ((input_timeout.hours|int) * 60 * 60) %}
  146. {% endif %}
  147. {% if 'days' in input_timeout %}
  148. {% set seconds = seconds + ((input_timeout.days|int) * 60 * 60 * 24) %}
  149. {% endif %}
  150. {% elif input_timeout is not iterable %}
  151. {% set seconds = input_timeout|int %}
  152. {% endif %}
  153. {{ false if seconds <= 0 else (now().timestamp() + seconds) }}
  154. {% endif %}
  155. update_data: >-
  156. {% set data_data = (input_data.data.items() | list) if 'data' in input_data else [] %}
  157. {% if 'data' in input_data %}
  158. {% if 'actions' in input_data.data %}
  159. {% set action = namespace(entities=[]) %}
  160. {% for a in input_data.data.actions %}
  161. {% set update = {"action": ctx ~ '_' ~ a.action}.items() | list %}
  162. {% set current = a.items() | list | rejectattr(
  163. '0', 'eq', update | map(attribute='0') | list
  164. ) | list %}
  165. {% set action.entities = action.entities + [
  166. dict.from_keys(current + update)
  167. ] %}
  168. {% endfor %}
  169. {% set actions = { "actions": action.entities }.items() | list %}
  170. {% set data_data = dict.from_keys(data_data | rejectattr(
  171. '0', 'eq', actions | map(attribute='0') | list
  172. ) | list + actions).items() | list
  173. %}
  174. {% endif %}
  175. {% if 'timeout' not in input_data and update_timeout is not false %}
  176. {% set add = { "timeout": update_timeout|int }.items() | list %}
  177. {% set data_data = dict.from_keys(data_data | rejectattr(
  178. '0', 'eq', add | map(attribute='0') | list
  179. ) | list + add).items() | list
  180. %}
  181. {% endif %}
  182. {% if 'group' not in input_data.data %}
  183. {% set add = { "group": "default-group" }.items() | list %}
  184. {% set data_data = dict.from_keys(data_data | rejectattr(
  185. '0', 'eq', add | map(attribute='0') | list
  186. ) | list + add).items() | list
  187. %}
  188. {% endif %}
  189. {% if (
  190. (
  191. 'alert_once' in input_data.data or
  192. 'actions' in input_data.data or
  193. 'persistent' in input_data.data
  194. ) and 'tag' not in input_data.data
  195. ) or (
  196. update_timeout is not false and 'tag' not in input_data.data
  197. )
  198. %}
  199. {% set add = { "tag": "tag_" + ctx }.items() | list %}
  200. {% set data_data = dict.from_keys(data_data | rejectattr(
  201. '0', 'eq', add | map(attribute='0') | list
  202. ) | list + add).items() | list
  203. %}
  204. {% endif %}
  205. {% else %}
  206. {% if update_timeout is not false %}
  207. {% set add = { "tag": "tag_" + ctx, 'timeout': update_timeout|int }.items() | list %}
  208. {% set data_data = dict.from_keys(data_data | rejectattr(
  209. '0', 'eq', add | map(attribute='0') | list
  210. ) | list + add).items() | list
  211. %}
  212. {% endif %}
  213. {% endif %}
  214. {% if data_data|length != 0 %}
  215. {% set data_data = {"data": dict.from_keys(data_data)}.items() | list %}
  216. {{ dict.from_keys((input_data.items() | list | rejectattr(
  217. '0', 'eq', data_data | map(attribute='0') | list
  218. ) | list) + data_data) }}
  219. {% else %}
  220. {{ input_data }}
  221. {% endif %}
  222. action_handlers: >-
  223. {% set actions = namespace(handlers=[]) %}
  224. {% for ask in action_scripts.keys() %}
  225. {% set askc = ctx ~ '_' ~ ask %}
  226. {% for action in update_data.data.actions if askc == action.action %}
  227. {% set actions.handlers = actions.handlers + [(
  228. askc, action_scripts[ask]
  229. )] %}
  230. {% endfor %}
  231. {% endfor %}
  232. {{ dict.from_keys(actions.handlers) }}
  233. - service: system_log.write
  234. data:
  235. level: warning
  236. message: "CONTEXT:{{ ctx }} \n\nID: {{ context.id }} == {{ this.context.id }} == {{ context.id == this.context.id }}\n\nDATA: {{update_data}}\n\n{{this}}"
  237. - alias: "Parallize event and action listeners"
  238. parallel:
  239. - alias: "Listen for event if actions are given"
  240. if: "{{ action_handlers|length > 0 or update_timeout is not false }}"
  241. then:
  242. - alias: "Loop for events until criterias are met"
  243. repeat:
  244. while: "{{ (update_timeout - now().timestamp() > 0) or update_timeout == 0 }}"
  245. sequence:
  246. - if: "{{ update_timeout == 0 }}"
  247. then:
  248. alias: "Wait for app event, without timeout"
  249. wait_for_trigger:
  250. - platform: event
  251. event_type:
  252. - mobile_app_notification_action
  253. - mobile_app_notification_cleared
  254. event_data:
  255. device_id: "{{ device_id }}"
  256. - platform: event
  257. event_type: 'context_notification_clear'
  258. event_data:
  259. context: "{{ context.id }}"
  260. else:
  261. alias: "Wait for app event, with timeout"
  262. wait_for_trigger:
  263. - platform: event
  264. event_type:
  265. - mobile_app_notification_action
  266. - mobile_app_notification_cleared
  267. event_data:
  268. device_id: "{{ device_id }}"
  269. - platform: event
  270. event_type: 'context_notification_clear'
  271. event_data:
  272. context: "{{ context.id }}"
  273. timeout: >-
  274. {{ update_timeout - now().timestamp() }}
  275. - if: "{{ wait.trigger is none }}"
  276. then:
  277. alias: "Reached timeout, ending."
  278. stop: "Reached timeout, ending."
  279. - alias: "Check if the notification was cleared (completed) by another device."
  280. if: "{{ wait.trigger.event.event_type == 'context_notification_clear' }}"
  281. then:
  282. - alias: "Clear notification on other devices, task was completed"
  283. service: "{{ input_notify_device }}"
  284. data:
  285. message: "clear_notification"
  286. data:
  287. tag: "{{ update_data.data.tag }}"
  288. - alias: "And give a 20 seconds notification on other devices that it was cleared."
  289. service: "{{ input_notify_device }}"
  290. data:
  291. title: "Notification completed"
  292. message: >-
  293. {{ wait.trigger.event.data.user.split(' ')[0] }} completed «{{
  294. (update_data.title if 'title' else update_data.message)[0:20]
  295. }}...» ✔
  296. data:
  297. visibility: public
  298. importance: low
  299. color: "#688840"
  300. timeout: 20
  301. notification_icon: >-
  302. {{ update_data.data.notification_icon if 'data' in update_data and 'notification_icon' in update_data.data else 'mdi:checkbox-marked-circle-plus-outline' }}
  303. - alias: "Notification cleared by another user, ending."
  304. stop: "Notification cleared by another user, ending."
  305. - alias: "Check that the notification is within this context"
  306. condition: >-
  307. {% set in_ctx = namespace(bool=false) %}
  308. {% for key in wait.trigger.event.data.keys() if key.startswith('action') and key.endswith('key') %}
  309. {% if wait.trigger.event.data[key].startswith(ctx) %}
  310. {% set in_ctx.bool = true %}
  311. {% endif %}
  312. {% endfor %}
  313. {{ in_ctx.bool }}
  314. - if: "{{ wait.trigger.event.event_type.endswith('_cleared') }}"
  315. then:
  316. alias: "User cleared notification, stop listening for events."
  317. stop: "User cleared notification, stop listening for events."
  318. - if: "{{ wait.trigger.event.data.action.endswith('_DISMISS_ALL') }}"
  319. then:
  320. event: context_notification_clear
  321. event_data:
  322. context: "{{ context.id }}"
  323. user: '{{ states.person|selectattr("attributes.user_id", "==", wait.trigger.event.context.user_id)|map(attribute="attributes.friendly_name")|first }}'
  324. - variables:
  325. action: >-
  326. {{ action_handlers[ wait.trigger.event.data.action ]|default(false) }}
  327. - alias: "Check if the action is associated with a script"
  328. if: "{{ not action }}"
  329. then:
  330. - stop: "Action is not associated with any scripts, ending."
  331. - if: "{{ action is string or 'variables' not in action }}"
  332. then:
  333. service: script.turn_on
  334. target:
  335. entity_id: "{{ script }}"
  336. else:
  337. service: script.turn_on
  338. target:
  339. entity_id: "{{ action.script }}"
  340. data:
  341. variables: "{{ action.variables }}"
  342. - alias: Send message to device
  343. service: "{{ input_notify_device }}"
  344. # @ignore: Incorrect type. Expected "object"
  345. data: "{{ update_data }}"