Sync your Alexa/Todoist shopping list to the Home Assistant shopping list!

Tags: #<Tag:0x00007fc41dc36818> #<Tag:0x00007fc41dc36728> #<Tag:0x00007fc41dc36638>

What does this setup do?

Sync a list from Todoist to the Home Assistant shopping list component. If you sync Alexa to Todoist, you can have Home Assistant and Alexa share the contents of the shopping list.

This script provides a service to sync the HA shopping list with the Todoist list. The Todoist list will overwrite the one stored by HA. When an item is checked or added to the HA list, it will be updated on the Todoist list. Optionally if you setup webhooks from Todoist, you can have HA sync the shopping list automatically whenever something changes.

Requirements

  1. pyscript custom-components/pyscript: Pyscript adds rich Python scripting to HASS (github.com) - enable the global HASS variable when setting up
  2. Todoist API key (Integrations (todoist.com))
  3. Todoist project ID (when on your Alexa Shopping List project, use the ID at the end of the URL)

Add this file to your config/pyscript/shopping_list_sync.py

import requests
import sys
import time
from importlib import reload
if "/config/pyscript_modules" not in sys.path:
    sys.path.append("/config/pyscript_modules")

import write_file

write_file = reload(write_file)

TODOIST_TOKEN = "<your Todoist API token>"
TODOIST_PROJECT_ID = <your Todoist project ID>

def get_tasks():
    get_tasks_url =f"https://api.todoist.com/rest/v1/tasks?project_id={TODOIST_PROJECT_ID}"
    headers = {"Authorization" : f"Bearer {TODOIST_TOKEN}"}

    status_code = 500

    while status_code is not 200:
        response = task.executor(requests.get, get_tasks_url, headers = headers)
        status_code = response.status_code
        time.sleep(1)
    json = response.json()
    return json

def add_task(item):
    url = "https://api.todoist.com/rest/v1/tasks"
    headers = {"Authorization" : f"Bearer {TODOIST_TOKEN}", "Content-Type" : "application/json"}
    body = {"content" : item, "project_id" : TODOIST_PROJECT_ID}
    status_code = 500

    while status_code is not 200:
        response = task.executor(requests.post, url, headers = headers, json=body)
        status_code = response.status_code
        time.sleep(1)
    if status_code == 200:
        return True
    else:
        return False

def update_task(id, content):
    url = f"https://api.todoist.com/rest/v1/tasks/{id}"
    headers = {"Authorization" : f"Bearer {TODOIST_TOKEN}", "Content-Type" : "application/json"}
    body = {"content" : content}
    status_code = 500

    counter = 0
    while status_code is not 204 and counter < 10:
        response = task.executor(requests.post, url, headers = headers, json=body)
        status_code = response.status_code
        time.sleep(1)
        counter += 1
    if status_code == 204:
        return True
    else:
        return False

def complete_task(id):
    url = f"https://api.todoist.com/rest/v1/tasks/{id}/close"
    headers = {"Authorization" : f"Bearer {TODOIST_TOKEN}"}
    status_code = 500

    counter = 0
    while status_code is not 204 and counter < 10:
        response = task.executor(requests.post, url, headers = headers)
        status_code = response.status_code
        time.sleep(1)
        counter += 1
    if status_code == 204:
        return True
    else:
        return False

@service
def sync_shopping_list():
    tasks = []
    json = get_tasks()
    for item in json:
        tasks.append({"name" : item["content"], "id" : str(item["id"]), "complete" : item["completed"]})

    task.executor(write_file.write_json, filename = "/config/.shopping_list.json", content=tasks)
    hass.data["shopping_list"].async_load()

@event_trigger('shopping_list_updated')
def update_shopping_list(action=None, item=None):
    if action == "add":
        add_task(item["name"])
        sync_shopping_list()
    if action == "update" and item["complete"] == False:
        update_task(item["id"],item["name"])
        sync_shopping_list()
    if action == "update" and item["complete"] == True:
        complete_task(item["id"])
        sync_shopping_list()

Add this file to config/pyscript_modules/write_file.py

import json

def write_json(filename, content):
    with open(filename, 'w') as file:
        json.dump(content, file, indent=4)

Restart pyscript

Run the service pyscript.sync_shopping_list from developer tools.

You should now have a synced list. Note this will remove everything you have in the current shopping list, so add the items to Todoist first.

Setting up webhook to update HA when Todoist is updated (via Alexa etc.)

For this to work, you need to have your HA available on the Internet with SSL enabled.

  1. Set up an automation in HA with a webhook trigger. Set the action to call service and call pyscript.sync_shopping_list

  2. Create a new app in Todoist - https://developer.todoist.com/appconsole.html

  3. Choose a name, and then scroll down to Webhooks.

  4. Under Watched events, select all item:* options (added, deleted etc.)

  5. Enter the webhook URL in the following format: https://<your-HA-URL>/api/webhook/<webhook-id-from-automation>

  6. Scroll down and click the “Create test token” button:

  7. Head to integrations and check your app name is listed.

  8. Restart Home Assistant.

  9. Now when you update anything on Todoist, the webhook should be called to get HA to update the shopping list!

6 Likes

I’m loving the integration but I can’t get the webhooks to work… When I add a new item it doesn’t seem to fire.
Things I’ve done -

  • Configured Todoist as explained in your post
  • When calling the URL in step 7 I get “invalid state” - I assume this is the error referred to? It redirects to my instance of Home Assistant but I don’t get the log-in screen to authenticate…
  • If I call the webhook from curl then it works (so can confirm the webhook is configured properly)

I’m sure I’m doing something stupid, any ideas?

If you go to Integrations (todoist.com), does the app show up?

Nothing there - just Alexa and Google Calendar.

@Dangermoose Please follow the updated steps from number 6.

Brilliant, can confirm thats worked. Great stuff. Thanks for this!

1 Like

Great tools!
One question, if i add something in the HA Shopping list, Would it be deleted by the script ?
Or is it a both-way sync ?

Thanks!

It’s a two way sync. Once you add an item in HA, it adds it to Todoist. Then it downloads the Todoist list and overwrites the HA one. So whatever order you have in Todoist is the same as HA.

Thank you for your quick reply, i’m going to install this right now :slight_smile:

Ok, i setup the thing, it works when i add something in the Alexa/Todoist list, but if i add something in HA list, it’s not sync with Alexa/todoist and it’s deleted with the next sync.
I’m missing something!

Can you check your HA logs?

Try having Todoist website and HA open at same time and see if it appears on Todoist before disappearing.

i’ll try.
For the log, i’m having that:

Logger: homeassistant.components.automation.sync_alexatohashoppinglist
Source: helpers/script.py:1138
Integration: Automation (documentation, issues)
First occurred: 4:31:46 PM (7 occurrences)
Last logged: 4:33:35 PM

sync AlexaToHAShoppingList: Already running

That shouldn’t matter. Are you on the latest version of HA?

Yes 2021.1.5
The update HA to Todoist is managed by the python script ?

Yes, so I am not sure where the error might be, especially if the sync service is working.

ho sorry, i just restart HA and it’s working!
Many thanks, it’s a great tools !

1 Like

Glad it’s working now!

This is still working with the new HA updates.

Is there a way to use this to sync also custom lists from Alexa?

If it’s in Todoist, you might be able to just replace the project ID in the script. Although it will still sync with shopping list in HA.