This is an updated version of my earlier Alexa shopping list method for Home Assistant. The basic idea is still the same: Node-RED fetches the Alexa shopping list, optionally sends it through OpenAI to sort it by supermarket aisles, saves the result into a local text file, and Home Assistant displays that file on a dashboard card. What changed in this version is the usability: the list is now visually separated into categories or aisles, items can be checked on and off directly from the dashboard, duplicate Alexa items are cleaned up before the list is sent to OpenAI, and the refresh button now shows a loading spinner while the list is being rebuilt.
Just as important, there are no Home Assistant helpers you need to create manually for the button, the clickable item text, or the loading state. Those entities are exposed from Node-RED Companion. Home Assistant only needs the file-based sensor that reads /config/www/shopping_list.txt, plus the dashboard card itself.

In the screenshot above, notice the vertical separators between groups of items. OpenAI still assigns each product to one of ten supermarket categories, but instead of coloring each item differently, the card now uses the categories to create subtle aisle breaks. In day-to-day use, I find this much easier on the eye for my kitchen Home Assistant dashboard.

The refresh button now turns into a spinning animated loader while the flow is running. That matters because the OpenAI step is not instantaneous, and without visible feedback it is hard to know whether the button press actually did anything. In this version, Node-RED exposes a switch entity to Home Assistant and the card uses that switch to decide whether to show the normal refresh icon or an activity indicator.

Items can now also be checked on and off directly from the Home Assistant dashboard. Tapping an unchecked item marks it as completed. Tapping a completed item removes the checkmark. Internally, the current card writes a temporary text value back into a Node-RED text entity, Node-RED updates the Alexa list item itself, and it also updates the local file by adding or removing a leading ! in front of the product name. The card then uses that ! marker to draw the strikethrough.
What Changed From My Previous Version
- Category separators instead of category colors or simple sorting . OpenAI still sorts by aisles, but the card now displays clean separators between category groups instead of using a different background color per category.
- Direct check on/off from the dashboard. The list is now interactive. You can tap an item to mark it complete and tap it again to uncheck it.
- A visible loading state. While Node-RED is rebuilding the file, the refresh button turns into a spinner.
- No manually created Home Assistant helpers. In the previous approach, helper entities in Home Assistant were part of the flow. In this version, the extra entities are all exposed from Node-RED Companion instead.
- Duplicate handling for Alexa list items. Alexa shopping lists can contain the same item more than once. Before sending the list to OpenAI, this version deduplicates intelligently. If all copies are completed, it keeps one. If both completed and incomplete versions exist, it keeps only one incomplete version. If multiple incomplete copies exist, it keeps one and removes the rest.
How This Version Works
- Node-RED fetches the Alexa shopping list. I still use the Alexa Remote node for this because there is no direct Home Assistant equivalent for shopping lists.
- Node-RED deduplicates the list. This happens before OpenAI sees the items. Removed duplicates are emitted one-by-one on a second output and can then be removed from Alexa asynchronously.
- Node-RED sends the cleaned list to OpenAI. OpenAI receives the current items and returns them grouped into ten predefined supermarket categories.
- Node-RED saves the categorized list to a file. The file is saved as
/config/www/shopping_list.txt. - Home Assistant reads that file with a file-based sensor. The card then renders the sensor state.
- When you tap an item on the card, Home Assistant writes the item name into a Node-RED text entity. Node-RED listens to that entity, updates the matching Alexa item, updates the local file, and then forces the file sensor to refresh with
homeassistant.update_entity.
A Quick Note About Home Assistant Entities in This Version
The only Home Assistant-side entity you need to create manually for the list itself is the file-based sensor that reads /config/www/shopping_list.txt. The button, text, and switch entities used by the dashboard are created by Node-RED Companion when you deploy the flow.
If Home Assistant OS blocks access to the file path, add the following to configuration.yaml and restart Home Assistant:
homeassistant:
allowlist_external_dirs:
- /config/www
Then add a File-based sensor in the Home Assistant UI that points to /config/www/shopping_list.txt. In my setup the entity is named sensor.shopping_list.
Node-RED Setup Notes
- Install node-red-contrib-alexa-remote2-applestrudel.
- Optional: install an OpenAI node. I used @inductiv/node-red-openai-api.
- Install Node-RED Companion from HACS and set up the Node-RED Home Assistant server connection.
- Import the flow below and then edit the account, server, service host, entity config, and file path details so they match your own environment.
- Do not assume my Alexa account IDs, list IDs, OpenAI service config, Home Assistant server config, or local IP addresses will match yours. They will not. You must open the relevant nodes and adapt them to your own system.
The Updated TailwindCSS Template Card
This is the current card I am using. It reads the file sensor, shows a spinner while the list is refreshing, inserts separators between aisle groups, and lets you tap items to check or uncheck them. Paste it into a TailwindCSS Template Card in Home Assistant.
type: custom:tailwindcss-template-card
content: |-
<div class="flex flex-wrap gap-2 justify-center p-2">
{% set raw_list = states('sensor.shopping_list') %}
{% set invalid_states = ['unknown', 'unavailable', 'none', ''] %}
{% set is_updating = is_state('switch.node_red_buttons_updating_shopping_list', 'on') %}
<div class="bg-accent rounded-lg p-3">
{% if is_updating %}
<div
class="flex items-center justify-center"
style="width: 24px; height: 24px;"
>
<div
style="
width: 18px;
height: 18px;
border: 3px solid rgba(255,255,255,0.35);
border-top-color: rgba(255,255,255,1);
border-radius: 50%;
animation: spin 0.8s linear infinite;
"
></div>
</div>
{% else %}
<div
onclick="hass.callService('button', 'press', {entity_id: 'button.update_shopping_list_nodered'});"
class="hover:scale-105 transition-all cursor-pointer"
style="font-size:14px;"
>
๐๐๐
</div>
{% endif %}
</div>
{% if raw_list not in invalid_states %}
<div class="self-center px-1" style="font-size:28px; line-height:1; opacity:0.35;">|</div>
{% endif %}
{% if raw_list in invalid_states %}
<div class="bg-accent rounded-lg p-3">
<span style="font-size:14px;">Shopping list is empty</span>
</div>
{% else %}
{% set items = raw_list.split(',') %}
{% set first_group = namespace(done=false) %}
{% for cat in range(1, 11) %}
{% set has_items = namespace(value=false) %}
{% for i in range(0, items | count) %}
{% set item = items[i].strip() %}
{% if item.startswith(cat|string + '-') %}
{% set has_items.value = true %}
{% endif %}
{% endfor %}
{% if has_items.value %}
{% if first_group.done %}
<div class="self-center px-1" style="font-size:28px; line-height:1; opacity:0.35;">|</div>
{% else %}
{% set first_group.done = true %}
{% endif %}
{% for i in range(0, items | count) %}
{% set item = items[i].strip() %}
{% if item.startswith(cat|string + '-') %}
{% set item_name_raw = item.split('-', 1)[1] %}
{% if item_name_raw.startswith('!') %}
{% set item_name_display = item_name_raw[1:] %}
{% set is_source_checked = true %}
{% set click_value = '!' + item_name_display %}
{% else %}
{% set item_name_display = item_name_raw %}
{% set is_source_checked = false %}
{% set click_value = item_name_display %}
{% endif %}
{% if is_source_checked %}
{% set text_decoration = 'line-through' %}
{% set opacity = '0.65' %}
{% else %}
{% set text_decoration = 'none' %}
{% set opacity = '1' %}
{% endif %}
<div
class="bg-accent rounded-lg p-3 cursor-pointer hover:scale-105 transition-all"
style="opacity: {{ opacity }};"
onclick="hass.callService('text', 'set_value', {entity_id: 'text.node_red_buttons_shopping_list_item_removed', value: '{{ click_value }}'});"
>
<span
id="item-{{ i }}"
style="font-size:14px; text-decoration: {{ text_decoration }};"
>
{{ item_name_display }}
</span>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
<style>
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</div>
ignore_line_breaks: true
always_update: false
parse_jinja: true
code_editor: Ace
entities:
- sensor.shopping_list
- switch.node_red_buttons_updating_shopping_list
- text.node_red_buttons_shopping_list_item_removed
bindings: []
actions: []
debounceChangePeriod: 100
plugins:
daisyui:
enabled: true
url: https://fastly.jsdelivr.net/npm/daisyui@latest/dist/full.css
theme: dark - dark
overrideCardBackground: false
tailwindElements:
enabled: false
The Updated Node-RED Flow
Below is the current full flow. Import it into Node-RED, then go through the nodes and update the parts that are environment-specific. That includes your Alexa account node, your Alexa shopping list ID, your Home Assistant server config, your Node-RED Companion entity configs, your OpenAI API node, and your file paths if they differ from mine.
Why The Duplicate Cleanup Matters
Alexa shopping lists can contain duplicates surprisingly easily. You can add the same item multiple times by voice, or end up with both a completed and an incomplete version of the same item. If I sent that directly to OpenAI, the dashboard could become messy, and the list would not reflect how I actually want to shop.
So in this version I clean duplicates before the AI step. The logic is:
- If all copies of an item are completed, keep only one completed copy.
- If at least one copy is incomplete, keep one incomplete copy and remove all completed copies.
- If multiple incomplete copies exist, keep one and remove the rest.
The removed duplicates are emitted from a second function output one at a time so they can be removed from Alexa asynchronously. This keeps the list cleaner and avoids showing repeated items on the dashboard.
One Important Omission
There is still one limitation here: if you update the list directly from the Alexa mobile app, there is no special event exposed to Node-RED that tells this flow to start immediately. Voice activity from an Echo device can trigger the flow, and the manual dashboard button can also trigger it, but app-only edits are different.
A practical workaround would be to add a periodic refresh, for example every hour. That is a perfectly reasonable compromise if you care a lot about app-side edits. I did not build that into the version I am describing here as a core requirement, because I wanted the manual button and voice-driven path to stay front and center. If app-side edits are important for your household, adding an Inject node on a timer is the first thing I would suggest.
Bottom Line
This version feels much closer to what I originally wanted. The list is still built on top of a workaround, because Amazon does not provide a friendly official path for this use case, but the experience is much nicer now. The dashboard is compact, the aisle grouping is easy to scan, duplicates are handled, the refresh flow gives visual feedback, and the items can be checked on or off directly from the wall tablet while shopping.
It is still not perfect. The mobile app does not provide a clean trigger into Node-RED, and the whole thing still depends on a community Alexa node continuing to work. But as a practical Home Assistant dashboard for an Alexa shopping list, this version has been a big improvement for me.
If you adapt this for your own setup and find a cleaner way to detect mobile-app updates, or a nicer way to display the aisles on the card, I would love to hear about it.



