Batch Multiple Shopping List Updates into One Notification

Hi everyone!

I want to share simple HA automation for shopping list notifications that buffers multiple item updates and sends a single combined notification instead of one notification per item. This is especially useful when adding items via Assist, which often send multiple rapid updates.

Key features:

  • Buffers all shopping list changes for 15 seconds, then sends one notification with all changes.
  • Tracks the following actions:
    • :new: Added
    • :white_check_mark: Completed
    • :recycle: Restored (unchecked)
    • :no_entry: Removed (only if not completed)
  • Capitalizes item names for better readability.
  • Includes a notification button “VIEW FULL LIST” that opens the shopping list in the HA app.
  • MDI notification_icon
  • Adds custom notification channel to set a specific sound or behavior on Android devices.

Important note:

Currently, the shopping_list_updated event does not support user IDs, so this notification cannot be filtered per user.

alias: Shopping List Update Notification
description: >-
  Groups multiple shopping list updates into a single notification by buffering
  changes over a short period.
triggers:
  - alias: On shopping list change
    event_type: shopping_list_updated
    trigger: event
    variables:
      action: "{{ trigger.event.data.action }}"
      completed: "{{ trigger.event.data.item.complete }}"
      item: "{{ trigger.event.data.item.name | capitalize }}"
conditions:
  - alias: Valid action check
    condition: template
    value_template: "{{ valid_actions.get((action, completed), false) }}"
actions:
  - alias: Append to buffer
    action: input_text.set_value
    data:
      value: >
        {{ action_label ~ ": " ~ item if buffer in [none, ''] else buffer ~ "; "
        ~ action_label ~ ": " ~ item }}
    target:
      entity_id: input_text.shopping_list_buffer
  - alias: Wait for additional input or timeout
    continue_on_timeout: true
    timeout: "00:00:15"
    wait_for_trigger:
      - alias: On next shopping list event
        event_type: shopping_list_updated
        trigger: event
  - alias: Send mobile notification
    action: notify.mobile_app_your_phone
    data:
      title: đź›’ {{ list_name }} updated
      message: |
        {{ states('input_text.shopping_list_buffer').replace(';','\n') }}
      data:
        clickAction: /todo
        notification_icon: mdi:cart
        url: /todo
        actions:
          - action: URI
            title: VIEW FULL LIST
            uri: /todo
  - alias: Clear buffer
    action: input_text.set_value
    data:
      value: ""
    target:
      entity_id: input_text.shopping_list_buffer
mode: restart
variables:
  valid_actions: |
    {{ {
      ('add', false): true,
      ('update', true): true,
      ('update', false): true,
      ('remove', false): true
    } }}
  action_label: |
    {{ {
      ('add', false): '🆕 Added',
      ('update', true): 'âś… Completed',
      ('update', false): '♻️ Restored',
      ('remove', false): 'â›” Removed'
    }.get((action, completed)) }}
  buffer: "{{ states('input_text.shopping_list_buffer') }}"
  list_name: "{{ state_attr('todo.shopping_list', 'friendly_name') }}"

Dependencies:

  • An input_text helper named shopping_list_buffer (with max length 255) to store buffered item updates.
1 Like

:arrows_counterclockwise: Update: Per-User Notification Workaround (Accurate for All Inputs)

I previously mentioned that per-user notifications aren’t possible because the shopping_list_updated event doesn’t include a user_id. While that’s still true, I’ve now added a helper automation that accurately tracks who made the last shopping list change, even for manual edits via the UI, Assist, or service calls.

:brain: How it works:

  • This automation listens for:
    • Google AI Assist conversations (e.g. when using voice)
    • All service calls to the todo domain: add_item, update_item, remove_item
  • It extracts the user_id from the trigger context
  • It then maps the user to a person and stores their name (id) in the input_text.todo_last_user helper

This allows my main notification automation to use that user ID to:

  • Send the notification to the right person (notify.mobile_app_{{ notify_user }}_phone)
  • Mention who updated the list (Updated {{ display_name }})

:brick: Requirements:

  • input_text.shopping_list_buffer – stores buffered updates
  • input_text.todo_last_user – holds the user ID (like name1 or name2)
  • automation.store_last_todo_list_user – helper automation that tracks who last edited the shopping list — whether by voice (Assist), service call, or UI
  • The user_map dictionary should reflect your user setup

Automation: Store Last Todo List User

alias: Notify Shopping List Updates
description: >-
  Groups multiple shopping list updates into a single notification by buffering
  changes over a short period.
triggers:
  - alias: On shopping list change
    trigger: event
    event_type: shopping_list_updated
    variables:
      action: "{{ trigger.event.data.action }}"
      item: "{{ trigger.event.data.item.name | capitalize }}"
      completed: "{{ trigger.event.data.item.complete }}"
conditions:
  - alias: Valid action filter
    condition: template
    value_template: "{{ valid_actions.get((action, completed), false) }}"
actions:
  - alias: Append to buffer
    action: input_text.set_value
    target:
      entity_id: input_text.shopping_list_buffer
    data:
      value: >
        {{ action_label ~ ": " ~ item if buffer in [none, ''] else buffer ~ "; "
        ~ action_label ~ ": " ~ item }}
  - alias: Wait for more input or timeout
    wait_for_trigger:
      - alias: Another shopping list change
        trigger: event
        event_type: shopping_list_updated
    timeout: "00:00:30"
    continue_on_timeout: true
  - alias: Send notification
    action: notify.mobile_app_{{ notify_user }}_phone
    data:
      title: đź›’ {{ list_name }} Updated by {{ display_name }}
      message: |
        {{ states('input_text.shopping_list_buffer').replace(';','\n') }}
      data:
        notification_icon: mdi:cart
        channel: ShoppingList
        clickAction: /todo
        url: /todo
        actions:
          - action: URI
            title: SHOW ALL LIST
            uri: /todo
  - alias: Clear buffer
    action: input_text.set_value
    target:
      entity_id: input_text.shopping_list_buffer
    data:
      value: ""
mode: restart
variables:
  valid_actions: |
    {{ 
      {
        ('add', false): true,
        ('update', true): true,
        ('update', false): true,
        ('remove', false): true
      }
    }}
  action_label: |
    {{ 
:new: Added
:white_check_mark: Completed
:recycle: Restored (unchecked)
:no_entry: Removed (only if not completed)
      {
        ('add', false): '🆕 Added',
        ('update', true): 'âś… Purchased',
        ('update', false): '♻️ Restored',
        ('remove', false): 'â›” Removed'
      }.get((action, completed))
    }}
  buffer: "{{ states('input_text.shopping_list_buffer') }}"
  list_name: "{{ state_attr('todo.shopping_list', 'friendly_name') }}"
  user_map: |
    {{
      {
        'name1': ['name2', 'name1_instr'],
        'name2': ['name1', 'name2_instr'],
        'unknown': ['oleksandr', 'Unknown']
      }
    }}
  last_user: "{{ states('input_text.todo_last_user') }}"
  notify_user: "{{ user_map.get(last_user)[0] }}"
  display_name: "{{ user_map.get(last_user)[1] }}"

The second value in user_map (e.g., 'name1_instr') is the user’s name in instrumental case, which is used in Ukrainian grammar to indicate “by someone”.

Automation (Helper): Store Last Todo List User

alias: Store Last Todo List User
description: >
  Tracks and saves the user who last updated the shopping list or triggered the
  LLM Assist. This helper automation supports shopping list notifications by
  identifying the most recent user who performed an action or voice command.
triggers:
  - alias: On Google AI conversation state change
    entity_id: conversation.example
    variables:
      user_id: "{{ trigger.to_state.context.user_id }}"
    trigger: state
  - alias: On todo service call
    event_type: call_service
    event_data:
      domain: todo
    variables:
      user_id: "{{ trigger.event.context.user_id }}"
      action: "{{ trigger.event.data.service }}"
    trigger: event
conditions:
  - alias: Only if relevant todo action or non-event trigger
    condition: or
    conditions:
      - condition: template
        value_template: "{{ trigger.platform != 'event' }}"
      - condition: template
        value_template: "{{ action in ['add_item', 'update_item', 'remove_item'] }}"
actions:
  - alias: Store user's name in input text
    action: input_text.set_value
    target:
      entity_id: input_text.todo_last_user
    data:
      value: |
        {{ states.person 
          | selectattr('attributes.user_id', 'equalto', user_id) 
          | map(attribute='attributes.id') 
          | first
          or 'unknown' 
        }}
mode: single