blueprint: domain: script name: Notify user description: >- A script blueprint that takes context into consideration where you dont have to think about the parallelizm. However, you should take into account that this script might live «forever», if you're not cautious. It will spawn into three services, where 2 of them might live until a script reload or a HA restart. However 2, this script is perfect if you know what you're doing and dont want to deal with the hassle of «contexting». E.g if you want to push the same thing to multiple devices, and let them do actions based on the data. Here's an example of a «happy birthday» script that will notify every device and let each device action on it (send an sms or dismiss it) without affecting other users. Example usage ``` # (create a script with this ) script: notify_device: alias: Notify device fields: [add fields from blueprint here] max: 200 use_blueprint: path: notify_user.yaml input: notify_device: "{{ notify_device }}" timeout: "{{ timeout if timeout is defined else 0 }}" data: "{{ data }}" action_scripts: "{{ action_scripts if action_scripts is defined else {} }}" ``` and call it: ``` service: script.turn_on target: entity_id: script.notify_device data: variables: notify_device: "{{ notify.mobile_app_device }}" data: (data as a normal "notify"-service) message: "..." data: actions: - action: DO_IT_NOW_DISMISS_ALL #Ending action with DISMISS_NOW will remove it from other devices. title: Do it now action_scripts: DO_IT_NOW_DISMISS_ALL: script: "script.my_script" #script to run if user clicks "Do it now" variables: var1: "Variables my script need" ``` Feel free to use it within a loop too: ``` repeat: for_each: >- - device_1 - device_2 sequence: - service: script.turn_on [...] data: variables: notify_device: "{{ repeat.item }}" ``` input: notify_device: name: "Device to notify" description: >- The name of the notify service, e.g service.mobile_app_x selector: object: timeout: name: "Timeout" description: >- Timeout before clearing notification and stop waiting. 0 equals to never. Respects «Days» too in YAML mode. Timeout set here will override `data.data.timeout` and visa-versa. selector: duration: # enable_days: true (HA fire an error on this.) data: name: "Message data" description: "Equal to data field in the notify service" selector: object: # Scripts to run based on `data->actions` set in notify-service. E.g: # ``` # data: # actions: # - action: ACTION_ANCHOR # title: This test # ``` # with matching: # ``` # action_scripts: # ACTION_ANCHOR: script... # ``` # The format for `ACTION_ANCHOR` can be just a string, referencing a script, # or an object with the following format: # ``` # ACTION_ANCHOR: # script: script.. # variables: # var1: .... # ``` action_scripts: name: Action scripts description: >- Read `action_scripts` comments within blueprint. selector: object: default: {} tts: name: "TTS" description: "Not used yet" selector: boolean: default: "{{ false }}" mode: parallel variables: input_notify_device: !input notify_device input_timeout: !input timeout input_data: !input data action_scripts: !input action_scripts sequence: - variables: device_name: "{{ input_notify_device.replace('notify.mobile_app_', '') }}" ctx_idx: "{{ state_attr(this.entity_id, 'current')|default(1, true) }}" ctx: "{{ context.id ~ '_' ~ device_name ~ '_' ~ ctx_idx }}" device_id: >- {{ dict.from_keys(device_attr(device_id('device_tracker.' ~ device_name), 'identifiers')).mobile_app }} update_timeout: >- {% if 'data' in input_data and 'timeout' in input_data.data %} {{ input_data.data.timeout|int }} {% else %} {% set seconds = 0 %} {% if input_timeout is iterable %} {% if 'seconds' in input_timeout %} {% set seconds = seconds + input_timeout.seconds|int %} {% endif %} {% if 'minutes' in input_timeout %} {% set seconds = seconds + (input_timeout.minutes|int * 60 * 60) %} {% endif %} {% if 'hours' in input_timeout %} {% set seconds = seconds + ((input_timeout.hours|int) * 60 * 60) %} {% endif %} {% if 'days' in input_timeout %} {% set seconds = seconds + ((input_timeout.days|int) * 60 * 60 * 24) %} {% endif %} {% elif input_timeout is not iterable %} {% set seconds = input_timeout|int %} {% endif %} {{ false if seconds <= 0 else (now().timestamp() + seconds) }} {% endif %} update_data: >- {% set data_data = (input_data.data.items() | list) if 'data' in input_data else [] %} {% if 'data' in input_data %} {% if 'actions' in input_data.data %} {% set action = namespace(entities=[]) %} {% for a in input_data.data.actions %} {% set update = {"action": ctx ~ '_' ~ a.action}.items() | list %} {% set current = a.items() | list | rejectattr( '0', 'eq', update | map(attribute='0') | list ) | list %} {% set action.entities = action.entities + [ dict.from_keys(current + update) ] %} {% endfor %} {% set actions = { "actions": action.entities }.items() | list %} {% set data_data = dict.from_keys(data_data | rejectattr( '0', 'eq', actions | map(attribute='0') | list ) | list + actions).items() | list %} {% endif %} {% if 'timeout' not in input_data and update_timeout is not false %} {% set add = { "timeout": update_timeout|int }.items() | list %} {% set data_data = dict.from_keys(data_data | rejectattr( '0', 'eq', add | map(attribute='0') | list ) | list + add).items() | list %} {% endif %} {% if 'group' not in input_data.data %} {% set add = { "group": "default-group" }.items() | list %} {% set data_data = dict.from_keys(data_data | rejectattr( '0', 'eq', add | map(attribute='0') | list ) | list + add).items() | list %} {% endif %} {% if ( ( 'alert_once' in input_data.data or 'actions' in input_data.data or 'persistent' in input_data.data ) and 'tag' not in input_data.data ) or ( update_timeout is not false and 'tag' not in input_data.data ) %} {% set add = { "tag": "tag_" + ctx }.items() | list %} {% set data_data = dict.from_keys(data_data | rejectattr( '0', 'eq', add | map(attribute='0') | list ) | list + add).items() | list %} {% endif %} {% else %} {% if update_timeout is not false %} {% set add = { "tag": "tag_" + ctx, 'timeout': update_timeout|int }.items() | list %} {% set data_data = dict.from_keys(data_data | rejectattr( '0', 'eq', add | map(attribute='0') | list ) | list + add).items() | list %} {% endif %} {% endif %} {% if data_data|length != 0 %} {% set data_data = {"data": dict.from_keys(data_data)}.items() | list %} {{ dict.from_keys((input_data.items() | list | rejectattr( '0', 'eq', data_data | map(attribute='0') | list ) | list) + data_data) }} {% else %} {{ input_data }} {% endif %} action_handlers: >- {% set actions = namespace(handlers=[]) %} {% for ask in action_scripts.keys() %} {% set askc = ctx ~ '_' ~ ask %} {% for action in update_data.data.actions if askc == action.action %} {% set actions.handlers = actions.handlers + [( askc, action_scripts[ask] )] %} {% endfor %} {% endfor %} {{ dict.from_keys(actions.handlers) }} - service: system_log.write data: level: warning message: "CONTEXT:{{ ctx }} \n\nID: {{ context.id }} == {{ this.context.id }} == {{ context.id == this.context.id }}\n\nDATA: {{update_data}}\n\n{{this}}\n\nDevice ID: {{device_id}}" - alias: "Parallize event and action listeners" parallel: - alias: "Listen for event if actions are given" if: "{{ action_handlers|length > 0 or update_timeout is not false }}" then: - alias: "Loop for events until criterias are met" repeat: while: "{{ (update_timeout - now().timestamp() > 0) or update_timeout == 0 }}" sequence: - if: "{{ update_timeout == 0 }}" then: alias: "Wait for app event, without timeout" wait_for_trigger: - platform: event event_type: - mobile_app_notification_action - mobile_app_notification_cleared event_data: device_id: "{{ device_id }}" - platform: event event_type: 'context_notification_clear' event_data: context: "{{ context.id }}" else: alias: "Wait for app event, with timeout" wait_for_trigger: - platform: event event_type: - mobile_app_notification_action - mobile_app_notification_cleared event_data: device_id: "{{ device_id }}" - platform: event event_type: 'context_notification_clear' event_data: context: "{{ context.id }}" timeout: >- {{ update_timeout - now().timestamp() }} - alias: "Check if trigger is none" if: "{{ wait.trigger is none }}" then: alias: "Reached timeout, ending." stop: "Reached timeout, ending." - alias: "Check if the notification was cleared (completed) by another device." if: "{{ wait.trigger.event.event_type == 'context_notification_clear' }}" then: - alias: "Clear notification on other devices, task was completed" service: "{{ input_notify_device }}" data: message: "clear_notification" data: tag: "{{ update_data.data.tag }}" - alias: "And give a 20 seconds notification on other devices that it was cleared." service: "{{ input_notify_device }}" data: title: "Notification completed" message: >- {{ wait.trigger.event.data.user.split(' ')[0] }} completed «{{ (update_data.title if 'title' else update_data.message)[0:20] }}...» ✔ data: visibility: public importance: low color: "#688840" timeout: 20 notification_icon: >- {{ update_data.data.notification_icon if 'data' in update_data and 'notification_icon' in update_data.data else 'mdi:checkbox-marked-circle-plus-outline' }} - alias: "Notification cleared by another user, ending." stop: "Notification cleared by another user, ending." - alias: "Check that the notification is within this context" condition: >- {% set in_ctx = namespace(bool=false) %} {% for key in wait.trigger.event.data.keys() if key.startswith('action') and key.endswith('key') %} {% if wait.trigger.event.data[key].startswith(ctx) %} {% set in_ctx.bool = true %} {% endif %} {% endfor %} {{ in_ctx.bool }} - if: "{{ wait.trigger.event.event_type.endswith('_cleared') }}" then: alias: "User cleared notification, stop listening for events." stop: "User cleared notification, stop listening for events." - if: "{{ wait.trigger.event.data.action.endswith('_DISMISS_ALL') }}" then: event: context_notification_clear event_data: context: "{{ context.id }}" user: '{{ states.person|selectattr("attributes.user_id", "==", wait.trigger.event.context.user_id)|map(attribute="attributes.friendly_name")|first }}' - alias: "Set action variable" variables: action: >- {{ action_handlers[ wait.trigger.event.data.action ]|default(false) }} - alias: "Check if the action is associated with a script" if: "{{ not action }}" then: - stop: "Action is not associated with any scripts, ending." - service: system_log.write data: level: warning message: "ACTION: {{ action }} " - if: "{{ action is string or 'variables' not in action }}" then: service: script.turn_on target: entity_id: "{{ action.script }}" else: service: script.turn_on target: entity_id: "{{ action.script }}" data: variables: "{{ action.variables }}" - alias: Send message to device service: "{{ input_notify_device }}" # @ignore: Incorrect type. Expected "object" data: "{{ update_data }}"