Sync Alexa Shopping List with Home Assistant – REVISITED

Sync Alexa Shopping List with Home Assistant – REVISITED

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.

Updated shopping list card grouped by supermarket category with separators between aisles.

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.

Shopping list refresh button showing a loading spinner while the list is updating.

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.

Shopping list item checked off directly from the Home Assistant dashboard.

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

  1. 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.
  2. 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.
  3. Node-RED sends the cleaned list to OpenAI. OpenAI receives the current items and returns them grouped into ten predefined supermarket categories.
  4. Node-RED saves the categorized list to a file. The file is saved as /config/www/shopping_list.txt.
  5. Home Assistant reads that file with a file-based sensor. The card then renders the sensor state.
  6. 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.

[{โ€œidโ€:โ€œ3141360816c8e7b4โ€,โ€œtypeโ€:โ€œtabโ€,โ€œlabelโ€:โ€œFlow 1โ€,โ€œdisabledโ€:false,โ€œinfoโ€:โ€œโ€,โ€œenvโ€:},{โ€œidโ€:โ€œe43b77dd4550b44aโ€,โ€œtypeโ€:โ€œha-buttonโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œUpdate Shopping Listโ€,โ€œversionโ€:0,โ€œdebugenabledโ€:false,โ€œoutputsโ€:1,โ€œentityConfigโ€:โ€œxxxxxxxxxxxxxxxxxโ€,โ€œoutputPropertiesโ€:[{โ€œpropertyโ€:โ€œpayloadโ€,โ€œpropertyTypeโ€:โ€œmsgโ€,โ€œvalueโ€:โ€œstringโ€,โ€œvalueTypeโ€:โ€œentityStateโ€},{โ€œpropertyโ€:โ€œtopicโ€,โ€œpropertyTypeโ€:โ€œmsgโ€,โ€œvalueโ€:โ€œโ€,โ€œvalueTypeโ€:โ€œtriggerIdโ€},{โ€œpropertyโ€:โ€œdataโ€,โ€œpropertyTypeโ€:โ€œmsgโ€,โ€œvalueโ€:โ€œโ€,โ€œvalueTypeโ€:โ€œentityโ€}],โ€œxโ€:520,โ€œyโ€:200,โ€œwiresโ€:[[โ€œ9090b4260abf7e93โ€,โ€œ1220666b30e15dabโ€]]},{โ€œidโ€:โ€œf209edffca5b827aโ€,โ€œtypeโ€:โ€œalexa-remote-eventโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œโ€,โ€œaccountโ€:โ€œput here your dataโ€,โ€œeventโ€:โ€œws-device-activityโ€,โ€œxโ€:210,โ€œyโ€:200,โ€œwiresโ€:[[โ€œ1880922c1bc91fa5โ€,โ€œa538251f7c35f6fcโ€]]},{โ€œidโ€:โ€œ67c372ab5edd5da2โ€,โ€œtypeโ€:โ€œinjectโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œโ€,โ€œpropsโ€:[{โ€œpโ€:โ€œpayloadโ€},{โ€œpโ€:โ€œtopicโ€,โ€œvtโ€:โ€œstrโ€}],โ€œrepeatโ€:โ€œโ€,โ€œcrontabโ€:โ€œ0 7-21 * * *โ€,โ€œonceโ€:false,โ€œonceDelayโ€:โ€œ10โ€,โ€œtopicโ€:โ€œโ€,โ€œpayloadโ€:โ€œโ€,โ€œpayloadTypeโ€:โ€œdateโ€,โ€œxโ€:610,โ€œyโ€:100,โ€œwiresโ€:[[โ€œ9090b4260abf7e93โ€]]},{โ€œidโ€:โ€œ1880922c1bc91fa5โ€,โ€œtypeโ€:โ€œswitchโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œSuccess?โ€,โ€œpropertyโ€:โ€œpayload.data.utteranceTypeโ€,โ€œpropertyTypeโ€:โ€œmsgโ€,โ€œrulesโ€:[{โ€œtโ€:โ€œeqโ€,โ€œvโ€:โ€œGENERALโ€,โ€œvtโ€:โ€œstrโ€}],โ€œcheckallโ€:โ€œtrueโ€,โ€œrepairโ€:false,โ€œoutputsโ€:1,โ€œxโ€:420,โ€œyโ€:140,โ€œwiresโ€:[[โ€œ3fb7de8493caf86eโ€]]},{โ€œidโ€:โ€œ3fb7de8493caf86eโ€,โ€œtypeโ€:โ€œswitchโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œShopping list?โ€,โ€œpropertyโ€:โ€œpayload.description.summaryโ€,โ€œpropertyTypeโ€:โ€œmsgโ€,โ€œrulesโ€:[{โ€œtโ€:โ€œcontโ€,โ€œvโ€:โ€œshopping listโ€,โ€œvtโ€:โ€œstrโ€}],โ€œcheckallโ€:โ€œtrueโ€,โ€œrepairโ€:false,โ€œoutputsโ€:1,โ€œxโ€:600,โ€œyโ€:140,โ€œwiresโ€:[[โ€œ9090b4260abf7e93โ€]]},{โ€œidโ€:โ€œ9090b4260abf7e93โ€,โ€œtypeโ€:โ€œalexa-remote-listโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œโ€,โ€œaccountโ€:โ€œput here your dataโ€,โ€œconfigโ€:{โ€œoptionโ€:โ€œgetListItemsโ€,โ€œvalueโ€:{โ€œlistโ€:{โ€œtypeโ€:โ€œstrโ€,โ€œvalueโ€:โ€œput here your dataโ€}}},โ€œxโ€:800,โ€œyโ€:140,โ€œwiresโ€:[[โ€œdd354411878a7aadโ€,โ€œbbaba99cd9e5caf2โ€]]},{โ€œidโ€:โ€œdd354411878a7aadโ€,โ€œtypeโ€:โ€œdebugโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œdebug 5โ€,โ€œactiveโ€:false,โ€œtosidebarโ€:true,โ€œconsoleโ€:false,โ€œtostatusโ€:false,โ€œcompleteโ€:โ€œpayloadโ€,โ€œtargetTypeโ€:โ€œmsgโ€,โ€œstatusValโ€:โ€œโ€,โ€œstatusTypeโ€:โ€œautoโ€,โ€œxโ€:800,โ€œyโ€:100,โ€œwiresโ€:},{โ€œidโ€:โ€œbbaba99cd9e5caf2โ€,โ€œtypeโ€:โ€œfunctionโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œfunction 1โ€,โ€œfuncโ€:โ€œlet items = msg.payload;\n\n// 1. Create the string for Home Assistant\nlet displayValues = items.map(item => item.value);\nlet hstMsg = { payload: displayValues.join(โ€˜,โ€™) };\n\n// 2. Prepare ALL items for deletion (No filter)\nlet itemsToDelete = items.map(i => {\n return {\n payload: {\n item: String(i.id),\n version: parseInt(i.version)\n }\n };\n});\n\nreturn [hstMsg, itemsToDelete];โ€,โ€œoutputsโ€:2,โ€œtimeoutโ€:0,โ€œnoerrโ€:0,โ€œinitializeโ€:โ€œโ€,โ€œfinalizeโ€:โ€œโ€,โ€œlibsโ€:,โ€œxโ€:980,โ€œyโ€:140,โ€œwiresโ€:[[โ€œ0de6aa72dc8b9498โ€,โ€œf28f388d41228b77โ€],[โ€œ1db7fbe78770d13aโ€]]},{โ€œidโ€:โ€œ0de6aa72dc8b9498โ€,โ€œtypeโ€:โ€œdebugโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œdebug 7โ€,โ€œactiveโ€:false,โ€œtosidebarโ€:true,โ€œconsoleโ€:false,โ€œtostatusโ€:false,โ€œcompleteโ€:โ€œtrueโ€,โ€œtargetTypeโ€:โ€œfullโ€,โ€œstatusValโ€:โ€œโ€,โ€œstatusTypeโ€:โ€œautoโ€,โ€œxโ€:980,โ€œyโ€:80,โ€œwiresโ€:},{โ€œidโ€:โ€œf28f388d41228b77โ€,โ€œtypeโ€:โ€œapi-call-serviceโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œTransfer value to HASSโ€,โ€œserverโ€:โ€œput here your dataโ€,โ€œversionโ€:7,โ€œdebugenabledโ€:false,โ€œactionโ€:โ€œinput_text.set_valueโ€,โ€œfloorIdโ€:,โ€œareaIdโ€:,โ€œdeviceIdโ€:,โ€œentityIdโ€:[โ€œinput_text.shoppingliststringโ€],โ€œlabelIdโ€:,โ€œdataโ€:โ€œ{“value”:”{{payload}}”}โ€,โ€œdataTypeโ€:โ€œjsonโ€,โ€œmergeContextโ€:โ€œโ€,โ€œmustacheAltTagsโ€:false,โ€œoutputPropertiesโ€:,โ€œqueueโ€:โ€œnoneโ€,โ€œblockInputOverridesโ€:false,โ€œdomainโ€:โ€œinput_textโ€,โ€œserviceโ€:โ€œset_valueโ€,โ€œxโ€:1290,โ€œyโ€:80,โ€œwiresโ€:[]},{โ€œidโ€:โ€œa538251f7c35f6fcโ€,โ€œtypeโ€:โ€œdebugโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œdebug 9โ€,โ€œactiveโ€:true,โ€œtosidebarโ€:true,โ€œconsoleโ€:false,โ€œtostatusโ€:false,โ€œcompleteโ€:โ€œtrueโ€,โ€œtargetTypeโ€:โ€œfullโ€,โ€œstatusValโ€:โ€œโ€,โ€œstatusTypeโ€:โ€œautoโ€,โ€œxโ€:220,โ€œyโ€:100,โ€œwiresโ€:},{โ€œidโ€:โ€œ1220666b30e15dabโ€,โ€œtypeโ€:โ€œalexa-remote-listโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œโ€,โ€œaccountโ€:โ€œput here your dataโ€,โ€œconfigโ€:{โ€œoptionโ€:โ€œgetListItemsโ€,โ€œvalueโ€:{โ€œlistโ€:{โ€œtypeโ€:โ€œstrโ€,โ€œvalueโ€:โ€œput here your dataโ€}}},โ€œxโ€:800,โ€œyโ€:280,โ€œwiresโ€:[[โ€œ444d0218fd3d5332โ€]]},{โ€œidโ€:โ€œ444d0218fd3d5332โ€,โ€œtypeโ€:โ€œfunctionโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œfunction 2โ€,โ€œfuncโ€:โ€œlet items = msg.payload;\n\n// 1. Create the string for Home Assistant\nlet displayValues = items.map(item => item.value);\nlet hstMsg = { payload: displayValues.join(โ€˜,โ€™) };\n\n// 2. Prepare ALL items for deletion (No filter)\nlet itemsToDelete = items.map(i => {\n return {\n payload: {\n item: String(i.id),\n version: parseInt(i.version)\n }\n };\n});\n\nreturn [hstMsg, itemsToDelete];โ€,โ€œoutputsโ€:2,โ€œtimeoutโ€:0,โ€œnoerrโ€:0,โ€œinitializeโ€:โ€œโ€,โ€œfinalizeโ€:โ€œโ€,โ€œlibsโ€:,โ€œxโ€:980,โ€œyโ€:280,โ€œwiresโ€:[[โ€œcf1da4149b38235aโ€],[โ€œe181a591ab85639bโ€]]},{โ€œidโ€:โ€œcf1da4149b38235aโ€,โ€œtypeโ€:โ€œapi-call-serviceโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œTransfer value to HASSโ€,โ€œserverโ€:โ€œput here your dataโ€,โ€œversionโ€:7,โ€œdebugenabledโ€:false,โ€œactionโ€:โ€œinput_text.set_valueโ€,โ€œfloorIdโ€:,โ€œareaIdโ€:,โ€œdeviceIdโ€:,โ€œentityIdโ€:[โ€œinput_text.shoppingliststringfarmaciaโ€],โ€œlabelIdโ€:,โ€œdataโ€:โ€œ{“value”:”{{payload}}”}โ€,โ€œdataTypeโ€:โ€œjsonโ€,โ€œmergeContextโ€:โ€œโ€,โ€œmustacheAltTagsโ€:false,โ€œoutputPropertiesโ€:,โ€œqueueโ€:โ€œnoneโ€,โ€œblockInputOverridesโ€:false,โ€œdomainโ€:โ€œinput_textโ€,โ€œserviceโ€:โ€œset_valueโ€,โ€œxโ€:1270,โ€œyโ€:280,โ€œwiresโ€:[]},{โ€œidโ€:โ€œde3c5fa27fb7a997โ€,โ€œtypeโ€:โ€œalexa-remote-listโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œโ€,โ€œaccountโ€:โ€œput here your dataโ€,โ€œconfigโ€:{โ€œoptionโ€:โ€œremoveItemโ€,โ€œvalueโ€:{โ€œlistโ€:{โ€œtypeโ€:โ€œstrโ€,โ€œvalueโ€:โ€œput here your dataโ€},โ€œitemโ€:{โ€œtypeโ€:โ€œmsgโ€,โ€œvalueโ€:โ€œpayload.itemโ€},โ€œversionโ€:{โ€œtypeโ€:โ€œmsgโ€,โ€œvalueโ€:โ€œpayload.versionโ€}}},โ€œxโ€:1190,โ€œyโ€:400,โ€œwiresโ€:[[โ€œ86c031919ee649b3โ€]]},{โ€œidโ€:โ€œ86c031919ee649b3โ€,โ€œtypeโ€:โ€œdebugโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œdebug 2โ€,โ€œactiveโ€:false,โ€œtosidebarโ€:true,โ€œconsoleโ€:false,โ€œtostatusโ€:false,โ€œcompleteโ€:โ€œtrueโ€,โ€œtargetTypeโ€:โ€œfullโ€,โ€œstatusValโ€:โ€œโ€,โ€œstatusTypeโ€:โ€œautoโ€,โ€œxโ€:1220,โ€œyโ€:540,โ€œwiresโ€:},{โ€œidโ€:โ€œe181a591ab85639bโ€,โ€œtypeโ€:โ€œdelayโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œโ€,โ€œpauseTypeโ€:โ€œdelayโ€,โ€œtimeoutโ€:โ€œ1โ€,โ€œtimeoutUnitsโ€:โ€œsecondsโ€,โ€œrateโ€:โ€œ1โ€,โ€œnbRateUnitsโ€:โ€œ1โ€,โ€œrateUnitsโ€:โ€œsecondโ€,โ€œrandomFirstโ€:โ€œ1โ€,โ€œrandomLastโ€:โ€œ5โ€,โ€œrandomUnitsโ€:โ€œsecondsโ€,โ€œdropโ€:false,โ€œallowrateโ€:false,โ€œoutputsโ€:1,โ€œxโ€:980,โ€œyโ€:400,โ€œwiresโ€:[[โ€œde3c5fa27fb7a997โ€]]},{โ€œidโ€:โ€œ1db7fbe78770d13aโ€,โ€œtypeโ€:โ€œdelayโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œโ€,โ€œpauseTypeโ€:โ€œdelayโ€,โ€œtimeoutโ€:โ€œ1โ€,โ€œtimeoutUnitsโ€:โ€œsecondsโ€,โ€œrateโ€:โ€œ1โ€,โ€œnbRateUnitsโ€:โ€œ1โ€,โ€œrateUnitsโ€:โ€œsecondโ€,โ€œrandomFirstโ€:โ€œ1โ€,โ€œrandomLastโ€:โ€œ5โ€,โ€œrandomUnitsโ€:โ€œsecondsโ€,โ€œdropโ€:false,โ€œallowrateโ€:false,โ€œoutputsโ€:1,โ€œxโ€:980,โ€œyโ€:200,โ€œwiresโ€:[[โ€œ32463a6c032c0ba6โ€]]},{โ€œidโ€:โ€œ32463a6c032c0ba6โ€,โ€œtypeโ€:โ€œalexa-remote-listโ€,โ€œzโ€:โ€œ3141360816c8e7b4โ€,โ€œnameโ€:โ€œโ€,โ€œaccountโ€:โ€œput here your dataโ€,โ€œconfigโ€:{โ€œoptionโ€:โ€œremoveItemโ€,โ€œvalueโ€:{โ€œlistโ€:{โ€œtypeโ€:โ€œstrโ€,โ€œvalueโ€:โ€œput here your dataโ€},โ€œitemโ€:{โ€œtypeโ€:โ€œmsgโ€,โ€œvalueโ€:โ€œpayload.itemโ€},โ€œversionโ€:{โ€œtypeโ€:โ€œmsgโ€,โ€œvalueโ€:โ€œpayload.versionโ€}}},โ€œxโ€:1170,โ€œyโ€:200,โ€œwiresโ€:[]},{โ€œidโ€:โ€œxxxxxxxxxxxxxxxxxโ€,โ€œtypeโ€:โ€œha-entity-configโ€,โ€œserverโ€:โ€œxxxxxxxxxxxxxxxxxโ€,โ€œdeviceConfigโ€:โ€œxxxxxxxxxxxxxxxxxโ€,โ€œnameโ€:โ€œUpdate Shopping Listโ€,โ€œversionโ€:6,โ€œentityTypeโ€:โ€œbuttonโ€,โ€œhaConfigโ€:[{โ€œpropertyโ€:โ€œnameโ€,โ€œvalueโ€:โ€œUpdat Shopping Listโ€},{โ€œpropertyโ€:โ€œiconโ€,โ€œvalueโ€:โ€œโ€},{โ€œpropertyโ€:โ€œentity_pictureโ€,โ€œvalueโ€:โ€œโ€},{โ€œpropertyโ€:โ€œentity_categoryโ€,โ€œvalueโ€:โ€œconfigโ€},{โ€œpropertyโ€:โ€œdevice_classโ€,โ€œvalueโ€:โ€œupdateโ€}],โ€œresendโ€:false,โ€œdebugEnabledโ€:false},{โ€œidโ€:โ€œxxxxxxxxxxxxxxxxxโ€,โ€œtypeโ€:โ€œserverโ€,โ€œnameโ€:โ€œHome Assistantโ€,โ€œaddonโ€:true},{โ€œidโ€:โ€œxxxxxxxxxxxxxxxxxโ€,โ€œtypeโ€:โ€œha-device-configโ€,โ€œnameโ€:โ€œNode Red Buttonsโ€,โ€œhwVersionโ€:โ€œโ€,โ€œmanufacturerโ€:โ€œNode-REDโ€,โ€œmodelโ€:โ€œโ€,โ€œswVersionโ€:โ€œโ€},{โ€œidโ€:โ€œ7bdc5fa7eae53679โ€,โ€œtypeโ€:โ€œglobal-configโ€,โ€œenvโ€:,โ€œmodulesโ€:{โ€œnode-red-contrib-home-assistant-websocketโ€:โ€œ0.80.3โ€,โ€œnode-red-contrib-alexa-remote2-applestrudelโ€:โ€œ5.0.55โ€}}]

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.


Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply