Browse Source

Re-wrote script, to listen on fewer events and therefore made script simpler - reducing parallel sequences.

Joachim M. Giæver 1 year ago
parent
commit
6049169bf7
1 changed files with 303 additions and 296 deletions
  1. 303 296
      script/notify_user.yaml

+ 303 - 296
script/notify_user.yaml

@@ -15,107 +15,101 @@ blueprint:
 
     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:
+    other users. 
 
+    Example usage:
     ```
     script:
-      birthday_wishes:
-        mode: parallel
-        sequence:
-          - variables:
-              blang: "{{ lang if lang is defined else none }}"
-              bname: "{{ name|lower|title }}"
-              bage: "{{ age|string + {1: 'st', 2: 'nd', 3: 'rd'}.get(4 if 10 <= age|int % 100 < 20 else age|int % 10, 'th') }}"
-          - service: "{{ notify_device }}"
-            data:
-              message: command_activity
-              data:
-                intent_action: "android.intent.action.SENDTO"
-                intent_uri: >-
-                  sms:{{ number }}?body={{-
-                    "🎼 «Hurra for deg som fyller ditt år, ja - deg vil jeg gratulere» 🎶 🎉 \n\n"
-                  }}{%- if blang == 'no_nb' -%}
-                    Gratulere med {{ age|default('') }}årsdagen, {{ bname }}! 🎂🎁 Håpe du får en fin dag 🥳
-                    {%- else -%}
-                    Happy {{ bage ~ ' ' if age else '' }}birthday, {{ bname }}! 🎂🎁 Hope'll get a beautiful day 🥳
-                  {%- endif -%}
+      notify_device:
+        alias: Notify device
+        fields:
+          notify_device:
+            name: "Device notify service"
+            description: "Device service, e.g notify.mobile_app_x"
+            selector:
+              object:
+          data:
+            name: Notification data
+            description: "Notification data, same as for notify-services"
+            selector:
+              object:
+          action_scripts:
+            name: Action scripts
+            description: >-
+              See blueprint
+            selector:
+              object:
+            default: {}
+          timeout:
+            name: "Timeout"
+            description: >-
+              Timeout before clearing event and stop waiting. 0 equals to never. 
+              Respects «Days» too in YAML mode.
+            selector:
+              duration:
+        max: 200
+        use_blueprint:
+          path: notify_user.yaml
+          input:
+            notify_device: "{{ notify_device }}"
+            index: "{{ index if index is defined else 0 }}"
+            timeout: "{{ timeout if timeout is defined else 0 }}"
+            data: "{{ data }}"
+            action_scripts: "{{ action_scripts if action_scripts is defined else {} }}"
+    ```
 
-    automation:
-      - alias: "Birthday: Notify on birthday!"
-        trigger: 
-          - platform: calendar
-            event: start
-            entity_id: calendar.ical_birthdays
-            offset:
-              hours: 10
-        action: 
-          - repeat:
-              for_each: "{{ birthdays }}"
-              sequence:
-                # Set variables here
-                - repeat:
-                    for_each: "{{ notify_devices }}"
-                    sequence:
-                      service: script.turn_on
-                      target:
-                        entity_id: script.notify_device
-                      data:
-                        variables:
-                          notify_device: "{{ repeat.item }}"
-                          index: "{{ index }}-{{ repeat.index }}"
-                          timeout:
-                            hours: 14
-                          data:
-                              title: "It is {{ name }}'s birthday. 🎁"
-                              message: >-
-                                {% if age is not false %}
-                                  {{ name }} is turning {{ age }} today.
-                                {% else %}
-                                  {{ name }} is getting one year older today.
-                                {% endif %}
-                                Send your congratulations 🎉 to let him/her
-                                know that you appreciate them. 🥳
-                              data:
-                                actions:
-                                  - action: SEND_SMS
-                                    title: Send SMS
-                                  - action: DISMISS
-                                    title: Dismiss
-                          action_scripts:
-                            SEND_SMS:
-                              script: script.birthday_wishes
-                              variables:
-                                lang: "{{ lang }}"
-                                name: "{{ name }}"
-                                age: "{{ age }}"
-                                notify_device: "{{ repeat.item }}"
-                                number: "{{ phones|join(',') }}"
+    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: "Devices to notify"
+      name: "Device to notify"
       description: >-
         The name of the notify service, e.g service.mobile_app_x
       selector:
-        object:        
-    index:
-      name: Device index
-      description: >-
-        If called several times within same context, increase this for each.
-      default:
-        seconds: 0
-      selector:
-        number:
-          min: 1
-          max: 1000
-          step: 1
+        object:
     timeout:
       name: "Timeout"
       description: >-
-        Timeout before notifications dissapear. 
+        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
+          # enable_days: true (HA fire an error on this.)
     data:
       name: "Message data"
       description: "Equal to data field in the notify service"
@@ -148,7 +142,7 @@ blueprint:
       selector:
         object:
       default: {}
-    
+
     tts:
       name: "TTS"
       description: "Not used yet"
@@ -160,149 +154,190 @@ mode: parallel
 
 variables:
   input_notify_device: !input notify_device
-  index: !input index
-  ctx: "{{ this.context.id + '_' + input_notify_device ~ '_' ~ index }}" # 
-  
   input_timeout: !input timeout
-  update_timeout: >-
-    {% 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 %}
-    {{ 0 if seconds == 0 else (now().timestamp() + seconds) }}
-
   input_data: !input data
-  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 '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 (
-          timeout != 0 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 timeout != 0 %}
-        {% 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 %}
-    {% 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_scripts: !input action_scripts
-  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) }}
 
-sequence: 
-  # - service: system_log.write
-  #   data:
-  #     level: warning
-  #     message: >-
-  #       Action handlers: {{ action_handlers }}
-  #       Timeout: {{ update_timeout }} seconds
-  #       Data: {{ update_data }}
-  
-  - alias: "Parallize event listeners and notification"
+sequence:
+  - variables:
+      device_name: "{{ input_notify_device.replace('notify.mobile_app_', '') }}"
+      ctx: "{{ context.id ~ '_' ~ device_name }}"
+      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) }}
+
+  - alias: "Parallize event and action listeners"
     parallel:
-    - alias: "Listen for event if actions are given"
-      if: "{{ action_handlers|length > 0 }}"
-      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
-                else:
-                  alias: "Wait for app event, with timeout"
-                  wait_for_trigger:
-                    - platform: event
-                      event_type: 
-                        - mobile_app_notification_action
-                        - mobile_app_notification_cleared
-                  timeout: >-
-                    {{ update_timeout - now().timestamp() }}
+      - sequence: []
+
+      - 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 }}"
+                  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() }}
+
+                - if: "{{ wait.trigger is none }}"
+                  then:
+                    alias: "Reached timeout, ending."
+                    stop: "Reached timeout, ending."
 
-              - if: "{{ wait.trigger is none }}"
-                then:
-                  alias: "Reached timeout, stopping."
-                  stop: "Reached timeout, stopping."
-              
-              - variables: 
-                  in_ctx: >-
+                - 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 the notification «{{
+                            (update_data.title if 'title' else update_data.message)[0:20]
+                          }}...»
+                        data:
+                          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) %}
@@ -311,83 +346,55 @@ sequence:
                     {% endfor %}
                     {{ in_ctx.bool }}
 
-              - alias: "Check if context matches or restart from top"
-                condition: "{{ in_ctx }}"
+                - 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.event_type.endswith('_cleared') or
-                    wait.trigger.event.data.action not in action_handlers.keys()
-                  }}
-                then:
-                  - alias: "Cleared event within user context"
-                    event: custom_mobile_app_notification_action
-                    event_data:
-                      context: "{{ ctx }}"
-                      action: "cleared"
-                  - stop: "User cleared notification or choose an invalid option, stop listening for events."
+                - service: system_log.write
+                  data:
+                    level: warning
+                    message: >-
+                      Context: {{ ctx }}\n
+                      Event-type: {{ wait.trigger.event.event_type }}\n
+                      Event-action: {{ wait.trigger.event.data.action }}
 
-              - event: custom_mobile_app_notification_action
-                event_data:
-                  context: "{{ ctx }}"
-                  action: "{{ wait.trigger.event.data.action }}"
-              - stop: "User acted within context, stop listening for further 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: "Listen for forwarded events"
-      if: "{{ action_handlers|length > 0 or update_timeout != 0 }}"
-      then:
-        - if: "{{ update_timeout == 0 }}"
-          then:
-            alias: "Wait for context event, without timeout"
-            wait_for_trigger:
-              - platform: event
-                event_type: custom_mobile_app_notification_action
-                event_data: 
-                  context: "{{ ctx }}"
-          else:
-            alias: "Wait for context event, with timeout"
-            wait_for_trigger:
-              - platform: event
-                event_type: custom_mobile_app_notification_action
-                event_data: 
-                  context: "{{ ctx }}"
-            timeout: >-
-              {{ update_timeout - now().timestamp() }}
+                - variables:
+                    action: >-
+                      {{ action_handlers[ wait.trigger.event.data.action ]|default(false) }}
 
-        - alias: "Check if timed out and remove notification"
-          if: "{{ wait.trigger == none }}"
-          then:
-            alias: "Clear notification, timeout reached."
-            service: "{{ input_notify_device }}"
-            data:
-              message: "clear_notification"
-              data:
-                tag: "{{ data.data.tag }}"
-          else:
-            - variables:
-                action: >-
-                  {% set action = wait.trigger.event.data.action %}
-                  {{ action_handlers[action] if action in action_handlers else false }}
+                - service: system_log.write
+                  data:
+                    level: warning
+                    message: "Action: {{ action }}"
 
-            - alias: "Action is known or stop executing"
-              condition: "{{ action is not false }}"
+                - alias: "Check if the action is associated with a script"
+                  if: "{{ not action }}"
+                  then:
+                    - stop: "Action is not associated with any scripts, ending."
 
-            - if: "{{ action is string or 'variables' not in action }}"
-              then:
-                service: script.turn_on
-                target:
-                  entity_id: "{{ script }}"
-              else:
-                service: script.turn_on
-                target:
-                  entity_id: "{{ action.script }}"
-                data:
-                  variables: "{{ action.variables }}"
+                - if: "{{ action is string or 'variables' not in action }}"
+                  then:
+                    service: script.turn_on
+                    target:
+                      entity_id: "{{ script }}"
+                  else:
+                    service: script.turn_on
+                    target:
+                      entity_id: "{{ action.script }}"
+                    data:
+                      variables: "{{ action.variables }}"
 
+      - alias: Send message to device
+        service: "{{ input_notify_device }}"
 
-    - alias: Send message to device
-      service: "{{ input_notify_device }}"
-      
-      data: >- 
-        {{ update_data }}
-      # @ignore: Incorrect type. Expected "object"
+        data: >-
+          {{ update_data }}
+        # @ignore: Incorrect type. Expected "object"