Sync Alexa Shopping List with Home Assistant

Sometimes I develop something and I’m really proud of the way I implemented a solution, this is not one of those times… :slight_smile:

I’m happy with the solution –
The solution allows me to -

  1. Use voice command with Alexa, voice commands that are simple. Such as “Alexa, add eggs to the shopping list” (and NOT a skill that currently requires command such as "Alexa, ask Anylist to add eggs to the shopping list). Additionally, to be able to go food shopping with the Alexa app and to check off products as I’m buying them.
  2. See the shopping list on a Home Assistant dashboard, and have it sync in real time with the Alexa shopping list, either after a voice command was issued or if the Alexa app was used to add/remove items.

To those of you who don’t quite understand the issue here - Amazon decided a few months ago to discontinue the API that allows 3rd party apps to communicate with their shopping list, breaking apps such as Anylist and Grocy and breaking the connection that those allowed into Home Assistant.

Although it allows me to do exactly what I want, and to also show the shopping list exactly in the UI I wanted (preview below), the solution is very convoluted, requires the integration of NodeRed and a few “ugly” hacks for everything to work. But once you get everything done, it does work, and works very well.

The screenshot of my card:

The full tutorial:

If someone finds a simpler solution that’s not dependent on so many different “moving parts”, I’ll be happy to hear…

2 Likes

Reading your tutorial, I’m in the same “Anylist” boat. I’d like to use your process to have the Alexa Media Player integration implement “Alexa, ask Anylist to add <mumble> to the shopping list” once I say “Alexa add <mumble>”.

The issue with this, is that then you use Anylist to go grocery shopping, and whatever you’ll cross off in Anylist will not sync back to Alexa. So then I was forced to eventually give up on Anylist, unfortunately. Setting up a 3 way sync between Home Assistant, Alexa and Anylist was a little too much work for me, it was difficult enough to hook up Alexa and Home Assistant. So I’m slowly getting used to the Alexa shopping list, maybe I’ll end up writing some app that’ll just show the Home Assistant shopping list, that will be the most efficient thing I think.

Good feedback. I really miss Anylist’s online shopping integration support so I may just delete the items from Alexa’s list manually. I’m sure Amazon’s decision hurt Anylist a lot, which was the point to begin with I suppose.

Thanks for the dev and walkthrough it’s really great.

I have made a few change:

  • Use of a file instead of a input text because the maximum length of 255 was too limiting.
  • Adding category to each item and coloring the text of the item depending on the category (the category assignement by Chatgpt is far from perfect :frowning: need proper prompt engineering.
  • Making each item clickable in order to check it off when you completed the purchase of the item.

Here are the different code:

Node Red

[{"id":"a05564b7cf7ef451","type":"inject","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"0 7-21 * * *","once":false,"onceDelay":"10","topic":"","payload":"","payloadType":"date","x":630,"y":1860,"wires":[["804b50e3d1730656"]]},{"id":"369bb2070d279d0f","type":"alexa-remote-event","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"","account":"7e07d994aaa8568c","event":"ws-device-activity","x":250,"y":1900,"wires":[["8f6552836a7b1a0d","f9ad5951b230d8b9"]]},{"id":"8f6552836a7b1a0d","type":"switch","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"Success?","property":"payload.data.utteranceType","propertyType":"msg","rules":[{"t":"eq","v":"GENERAL","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":440,"y":1900,"wires":[["4e529c4e3e2ec9d6"]]},{"id":"4e529c4e3e2ec9d6","type":"switch","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"Shopping list?","property":"payload.description.summary","propertyType":"msg","rules":[{"t":"cont","v":"achats","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":620,"y":1900,"wires":[["804b50e3d1730656"]]},{"id":"804b50e3d1730656","type":"alexa-remote-list","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"","account":"7e07d994aaa8568c","config":{"option":"getListItems","value":{"list":{"type":"str","value":"YW16bjEuYWNjb3VudC5BR0g0MkY1MkgzQVhWVVBGR0ZTQk8zSEFRQjJRLVNIT1BQSU5HX0lURU0="}}},"x":820,"y":1900,"wires":[["97a28f2f7a335291","b3326780c90e8168"]]},{"id":"97a28f2f7a335291","type":"debug","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"debug 5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":820,"y":1860,"wires":[]},{"id":"9ab9efc95be86e20","type":"ha-button","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"Update Shopping List","version":0,"debugenabled":false,"outputs":1,"entityConfig":"493ff69ce7a88be3","outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"}],"x":460,"y":1940,"wires":[["c5d2b0b8e68369ac"]]},{"id":"b3326780c90e8168","type":"function","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"function 1","func":"// Extract the array from the input payload\nlet items = msg.payload;\n\n// Map through the items to process the value based on the \"completed\" field\nlet values = items.map(item => {\n    // Check if the item is completed, and prepend \"!\" if true\n    return item.completed ? '!' + item.value : item.value;\n});\n\n// Concatenate the values into a single string\nlet concatenatedValues = values.join(',');\n\n// Set the concatenated string as the new payload\nmsg.payload = {\n    model: 'gpt-4o-mini',\n    messages: [\n        {\n            content: 'Voici une liste d articles d une liste de courses. Les 10 catégories suivantes sont définies pour minimiser le chevauchement : 1) Fruits et légumes frais (tous les fruits et légumes frais). 2) Viandes, poissons et fruits de mer (toutes les viandes, poissons, fruits de mer). 3) Produits laitiers et œufs (lait, yaourt, fromage, beurre, œufs). 4) Boulangerie et pâtisserie (pain, viennoiseries, gâteaux, pâtisseries). 5) Épicerie salée (pâtes, riz, conserves salées, sauces, épices, huiles, condiments). 6) Épicerie sucrée (biscuits, confitures, chocolat, sucre, miel, céréales sucrées, desserts). 7) Boissons (eau, jus, soda, café, thé, alcool). 8) Produits d hygiène et de beauté(shampoing, savon, dentifrice, maquillage, produits de soin). 9) Produits d entretien et de nettoyage (lessive, produit vaisselle, nettoyants, désinfectants, éponges). 10) Articles pour la maison et divers (ustensiles de cuisine, piles, papeterie, sacs poubelle, ampoules). Pour chaque article de la liste, réalisez les étapes suivantes : assignez-le à une catégorie parmi celles fournies précédemment puis ajoutez le numéro de cette catégorie avant le nom de l article, séparé par un tiret \"-\". Renvoie moi la liste avec tous les articles (avec son numéro de catégorie) séparé par des virgules comme dans la liste que vous avez reçue, sans espaces supplémentaires. Voici des règles supplémentaires à appliquer : - Si un article commence par un \"!\", gardez ce caractère, c est un caractère spécial utilisé pour autre chose. - Ne modifiez pas les noms des articles de la liste ou les numéros des catégories fournies. - Ne rajoutez aucun texte dans votre réponse avant la liste réordonnée. - Suivez ces consignes à la lettre.Voici la liste: ' + concatenatedValues,\n            role: 'user'\n        }\n    ]\n};\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1000,"y":1900,"wires":[["9884e8e8a18fa8b0","a695509aa4d396a3"]]},{"id":"9884e8e8a18fa8b0","type":"debug","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"debug 7","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1000,"y":1860,"wires":[]},{"id":"a695509aa4d396a3","type":"OpenAI API","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"","property":"payload","propertyType":"msg","service":"b9c49ceb7f25e4b8","method":"createChatCompletion","x":1200,"y":1900,"wires":[["3f5b56ab7fc208dc","5fe44d7ad9efb6ca"]]},{"id":"3f5b56ab7fc208dc","type":"debug","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"debug 8","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1200,"y":1860,"wires":[]},{"id":"f9ad5951b230d8b9","type":"debug","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"debug 9","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":240,"y":1860,"wires":[]},{"id":"c5d2b0b8e68369ac","type":"alexa-remote-init","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"","account":"7e07d994aaa8568c","option":"initialise","x":640,"y":1940,"wires":[["804b50e3d1730656"]]},{"id":"c36f3801e55b7ecc","type":"file","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"Ecrire fichier liste","filename":"/homeassistant/www/shopping_list.txt","filenameType":"str","appendNewline":true,"createDir":true,"overwriteFile":"true","encoding":"utf8","x":1690,"y":1900,"wires":[["1bed472b9b1d91b0"]]},{"id":"1bed472b9b1d91b0","type":"debug","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"debug 12","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1920,"y":1900,"wires":[]},{"id":"5fe44d7ad9efb6ca","type":"change","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.choices[0].message.content","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1430,"y":1900,"wires":[["c36f3801e55b7ecc"]]},{"id":"e2ce153ea3ef1e69","type":"junction","z":"90b28db9888b0e0a","g":"8bfb5eaeacf230b5","x":180,"y":1860,"wires":[[]]},{"id":"7e07d994aaa8568c","type":"alexa-remote-account","name":"Amazon David","authMethod":"proxy","proxyOwnIp":"192.168.1.103","proxyPort":"3456","cookieFile":"/config/alexacookie.txt","refreshInterval":"3","alexaServiceHost":"layla.amazon.com","pushDispatchHost":"","amazonPage":"amazon.fr","acceptLanguage":"fr-fr","onKeywordInLanguage":"on","userAgent":"","usePushConnection":"on","autoInit":"on","autoQueryActivityOnTrigger":"on"},{"id":"493ff69ce7a88be3","type":"ha-entity-config","server":"cd0af06f.2d645","deviceConfig":"e89ac03c8f552ff3","name":"Update Shopping List","version":"6","entityType":"button","haConfig":[{"property":"name","value":"Update Shopping List"},{"property":"icon","value":""},{"property":"entity_picture","value":""},{"property":"entity_category","value":"config"},{"property":"device_class","value":"update"}],"resend":false,"debugEnabled":false},{"id":"b9c49ceb7f25e4b8","type":"Service Host","apiBase":"https://api.openai.com/v1","secureApiKeyHeaderOrQueryName":"Authorization","organizationId":"","name":"Tompouce Open AI API"},{"id":"cd0af06f.2d645","type":"server","name":"Home Assistant","addon":false,"rejectUnauthorizedCerts":false,"ha_boolean":"","connectionDelay":false,"cacheJson":true,"heartbeat":true,"heartbeatInterval":"30","areaSelector":"friendlyName","deviceSelector":"friendlyName","entitySelector":"friendlyName","statusSeparator":"","statusYear":"numeric","statusMonth":"numeric","statusDay":"numeric","statusHourCycle":"h23","statusTimeFormat":"h:m:s","enableGlobalContextStore":true},{"id":"e89ac03c8f552ff3","type":"ha-device-config","name":"Node Red Buttons","hwVersion":"","manufacturer":"Node-RED","model":"","swVersion":""}]

Html code in Home assistant tailwindcss template:

entity: ''
content: |-
  <div class="flex flex-wrap gap-2 justify-center p-2">
      {% set items = states('sensor.liste_de_courses_alexa').split(',') %}
      {% set num_items = items | count %}
      {% set status_string = states('input_text.clicked_items') %}
      {% set status_length = status_string | length %}

      <!-- Ajustement de la longueur de status_string -->
      {% if status_length < num_items %}
          {% set status_string = status_string + ('0' * (num_items - status_length)) %}
      {% elif status_length > num_items %}
          {% set status_string = status_string[0:num_items] %}
      {% endif %}

      <!-- Bouton de mise à jour de la liste -->
      <div class="bg-accent rounded-lg p-3">
          <span style="font-size:14px">
              <div onClick="
                  hass.callService('button', 'press', {entity_id: 'button.update_shopping_list'});
                  hass.callService('input_text', 'set_value', {entity_id: 'input_text.clicked_items', value: ''});
              " class="hover:scale-105 transition-all cursor-pointer">
                  🛒📋🔃 
              </div>
          </span>
      </div>

      <!-- Affichage des éléments triés par catégorie -->
      {% for cat in range(1, 11) %}
          {% for i in range(0, num_items) %}
              {% set item = items[i] %}
              {% if item.startswith(cat|string + '-') %}
                  {% set status = status_string[i]|int(0) %}
                  <!-- Détermination de la couleur en fonction de la catégorie -->
                  {% set color = "" %}
                  {% if cat == 1 %}
                      {% set color = "#FF0000" %}  {# Rouge #}
                  {% elif cat == 2 %}
                      {% set color = "#FF7F00" %}  {# Orange #}
                  {% elif cat == 3 %}
                      {% set color = "#FFFF00" %}  {# Jaune #}
                  {% elif cat == 4 %}
                      {% set color = "#00FF00" %}  {# Vert #}
                  {% elif cat == 5 %}
                      {% set color = "#0000FF" %}  {# Bleu #}
                  {% elif cat == 6 %}
                      {% set color = "#4B0082" %}  {# Indigo #}
                  {% elif cat == 7 %}
                      {% set color = "#8A2BE2" %}  {# Violet #}
                  {% elif cat == 8 %}
                      {% set color = "#00FFFF" %}  {# Cyan #}
                  {% elif cat == 9 %}
                      {% set color = "#FF69B4" %}  {# Rose #}
                  {% elif cat == 10 %}
                      {% set color = "#800000" %}  {# Marron #}
                  {% endif %}

                  <!-- Détermination de la couleur à afficher et du style -->
                  {% if status == 1 %}
                      {% set display_color = 'gray' %}
                      {% set text_decoration = 'line-through' %}
                  {% else %}
                      {% set display_color = color %}
                      {% set text_decoration = 'none' %}
                  {% endif %}

                  <!-- Création de new_status_string pour le clic -->
                  {% set new_status_string = status_string[:i] + ('0' if status == 1 else '1') + status_string[i+1:] %}

                  <!-- Suppression du numéro de catégorie et du tiret de l'article -->
                  {% set item_name = item.split('-', 1)[1] %}

                  <!-- Affichage de l'élément cliquable sans le préfixe -->
                  <div class="bg-accent rounded-lg p-3">
                      <span id="item-{{ i }}" data-initial-color="{{ color }}" style="font-size:14px; color: {{ display_color }}; text-decoration: {{ text_decoration }}; cursor: pointer;"
                            onClick="hass.callService('input_text', 'set_value', {entity_id: 'input_text.clicked_items', value: '{{ new_status_string }}'})">
                          {{ item_name }}
                      </span>
                  </div>
              {% endif %}
          {% endfor %}
      {% endfor %}
  </div>
ignore_line_breaks: true
always_update: false
parse_jinja: true
code_editor: Ace
entities: []
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
type: custom:tailwindcss-template-card

I was able to find what I believe is an even better solution, without using NodeRed at all:

What’s the repo? There is no link in your message.

I linked to the other forum post where this was discussed. The direct link to the GitHub repo is:

For now, I’m using both solutions, until I decide which one is more reliable.

1 Like