Receive and interact with a shopping list when at the shops

I just spent a few days going around in circles and finally worked it out so am posting this here in case anyone finds it useful.

The aim here was to use my HA Voice PE device to store a shopping list and then, when I’m the local shops, receive a text message with the shopping list that I can check items off. I originally had an automation that sent me the todo list but whenever I stopped looking at the screen it would mean I’d have to go through the menus again to get back to the todo list. Surely there has to be a better way…

The solution uses Telegram as it is:

  • Always open
  • Persistent
  • Easy to interact with one-handed

So I wanted this:

A single Telegram message that shows my shopping list, where I can tap items to tick them off — and the message updates in place.

After a lot of trial and error this now works reliably.

What this solution does

  • Sends your HA shopping list to Telegram
  • Each item has a :white_check_mark: button
  • Tapping a button:
    • Marks the item completed in HA
    • Updates the same Telegram message
    • Removes that item’s button
  • When the list is empty:
    • Message updates to “All done”
    • Inline keyboard disappears cleanly

Requirements

  • Home Assistant with the To-Do / Shopping List integration
  • A Telegram bot configured in Polling mode
  • At least one allowed chat ID configured for the bot
  • Basic familiarity with HA automations

:warning: Broadcast Telegram bots will NOT work — callbacks require polling.


Automation 1

Send the shopping list to Telegram

This can be triggered however you like (manual, button, NFC, zone, etc.).
Below is a simple example trigger — adjust as needed.

alias: Shopping list - send to Telegram
mode: single

trigger:
  - platform: state
    entity_id: input_boolean.send_shopping_list
    to: "on"

condition:
  - condition: numeric_state
    entity_id: todo.shopping_list
    above: 0

action:
  - service: todo.get_items
    target:
      entity_id: todo.shopping_list
    data:
      status: needs_action
    response_variable: shopping

  - variables:
      items: "{{ shopping['todo.shopping_list']['items'] | default([]) }}"

      inline_kb: >-
        {% set ns = namespace(rows=[]) %}
        {% for i in items[:25] %}
          {% set label = '✅ ' ~ (i.summary | string | trim) %}
          {% set ns.rows = ns.rows + [ label ~ ':/done_' ~ i.uid ] %}
        {% endfor %}
        {{ ns.rows }}

      body: >-
        Shopping list ({{ items | length }}):
        {{ "\n" -}}
        {%- for i in items %}
        • {{ i.summary }}
        {%- endfor %}
        {{ "\n\n" -}}
        Tap an item to tick it off.

  - service: telegram_bot.send_message
    data:
      config_entry_id: <YOUR_TELEGRAM_CONFIG_ENTRY_ID>
      message: "{{ body }}"
      inline_keyboard: "{{ inline_kb }}"

Automation 2

Handle button taps + edit the message in place

This is the core automation that listens for Telegram callbacks.

alias: Shopping list - tick off item from Telegram (edit in place)
mode: queued
max: 10

trigger:
  - platform: state
    entity_id: event.shopping_list_update_event

condition:
  - condition: template
    value_template: >
      {{ trigger.to_state.attributes.event_type == 'telegram_callback'
         and (trigger.to_state.attributes.data | default('') | string).startswith('/done_') }}

action:
  # Extract callback details
  - variables:
      cb_data: "{{ trigger.to_state.attributes.data | string }}"
      callback_id: "{{ trigger.to_state.attributes.id }}"
      uid: "{{ cb_data | replace('/done_', '') }}"
      msg_id: "{{ trigger.to_state.attributes.message.message_id }}"
      chat_id: "{{ trigger.to_state.attributes.chat_id }}"

  # Mark item completed
  - service: todo.update_item
    target:
      entity_id: todo.shopping_list
    data:
      item: "{{ uid }}"
      status: completed

  # Acknowledge callback (removes Telegram spinner)
  - service: telegram_bot.answer_callback_query
    data:
      config_entry_id: <YOUR_TELEGRAM_CONFIG_ENTRY_ID>
      callback_query_id: "{{ callback_id }}"
      message: "Ticked off ✅"

  # Get remaining items
  - service: todo.get_items
    target:
      entity_id: todo.shopping_list
    data:
      status: needs_action
    response_variable: shopping

  - variables:
      items: "{{ shopping['todo.shopping_list']['items'] | default([]) }}"

      inline_kb: >-
        {% set ns = namespace(rows=[]) %}
        {% for i in items[:25] %}
          {% set label = '✅ ' ~ (i.summary | string | trim) %}
          {% set ns.rows = ns.rows + [ label ~ ':/done_' ~ i.uid ] %}
        {% endfor %}
        {{ ns.rows }}

      body: >-
        {% if items | length == 0 %}
        ✅ All done — shopping list cleared!
        {% else %}
        Remaining ({{ items | length }}):
        {{ "\n" -}}
        {%- for i in items %}
        • {{ i.summary }}
        {%- endfor %}
        {{ "\n\n" -}}
        Tap an item to tick it off.
        {% endif %}

  # Edit the original Telegram message
  # IMPORTANT: inline_keyboard must be omitted when list is empty
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ items | length > 0 }}"
        sequence:
          - service: telegram_bot.edit_message
            data:
              config_entry_id: <YOUR_TELEGRAM_CONFIG_ENTRY_ID>
              chat_id: "{{ chat_id }}"
              message_id: "{{ msg_id }}"
              message: "{{ body }}"
              inline_keyboard: "{{ inline_kb }}"
    default:
      - service: telegram_bot.edit_message
        data:
          config_entry_id: <YOUR_TELEGRAM_CONFIG_ENTRY_ID>
          chat_id: "{{ chat_id }}"
          message_id: "{{ msg_id }}"
          message: "{{ body }}"

:warning: Gotchas & Lessons Learned (very important)

These are the things that tripped me up — hopefully they save others a lot of time.

:one: You must use a polling Telegram bot

  • Broadcast bots cannot receive callbacks
  • Polling bots create an event.<name>_update_event entity
  • All callbacks arrive via that entity

:two: Allowed chat IDs are mandatory

If you see log messages like:

Unauthorized update - chat id not in allowed chats

you must re-add the Telegram integration and explicitly set allowed chat IDs.


:three: send_message and edit_message are not equally forgiving

Some keyboard formats work for send_message but fail for edit_message.

:white_check_mark: This format works reliably for both:

"Button label:/callback_data"

:x: Dict-style or nested list buttons caused parse errors or weird labels.


:four: You cannot send an empty inline keyboard

When the list is empty:

  • Do not send inline_keyboard
  • Not []
  • Not an empty template
  • Omit the field entirely

Using a choose: block is the only reliable way — Jinja omit was inconsistent.


:five: The automation will trigger more than once — that’s OK

You may see log entries like:

If template renders false

This is expected — Telegram sends multiple update types, and the condition filters what you care about.


Final thoughts

This ended up being far trickier than expected but the result is genuinely useful in daily life and hopefully this helps.

2 Likes

Pretty neat. I just use a bunch of zone enter triggers to open my relevant Home Assistant ToDo lists.

- id: 118a6c0d-52c3-4538-8784-abaeca360cab
  alias: 'Shopping Notify'
  max_exceeded: 'silent'
  triggers:
  - id: bunnings
    trigger: zone
    entity_id: device_tracker.iphone
    zone: zone.bunnings
    event: enter
  - id: jaycar
    trigger: zone
    entity_id: device_tracker.iphone
    zone: zone.jaycar
    event: enter
  - id: shopping_list
    trigger: zone
    entity_id: device_tracker.iphone
    zone: zone.<redacted>_court
    event: enter
  - id: shopping_list
    trigger: zone
    entity_id: device_tracker.iphone
    zone: zone.<redacted>_town
    event: enter
  - id: <redacted>_street
    trigger: zone
    entity_id: device_tracker.iphone
    zone: zone.<redacted>_shops
    event: enter
  conditions:
  - condition: or
    conditions:
    - condition: and
      conditions:
      - condition: trigger
        id: bunnings
      - condition: numeric_state
        entity_id: todo.bunnings
        above: 0
    - condition: and
      conditions:
      - condition: trigger
        id: shopping_list
      - condition: numeric_state
        entity_id: todo.shopping_list
        above: 0
    - condition: and
      conditions:
      - condition: trigger
        id: jaycar
      - condition: numeric_state
        entity_id: todo.jaycar
        above: 0
    - condition: and
      conditions:
      - condition: trigger
        id: <redacted>_street
      - condition: numeric_state
        entity_id: todo.<redacted>
        above: 0
  actions:
  - action: notify.mobile_app_iphone
    data:
      message: "🛒 Time to shop? (long click to open list)"
      data:
        actions:
          - action: URI
            title: View shopping list
            uri: "/todo?entity_id=todo.{{trigger.id}}"
1 Like

I started out using the notify. action but struggled to keep the list open while doing the shopping. Whenever I stopped looking at my phone the screen lock would come on and I would have to unlock it and navigate back to the todo list in HA. I was already using Telegram for another purpose so it made sense to reuse that as it stays persistent.

That’s weird. Whenever that happens to me it stays on the list.