Small python tool to forward stock of you favourite stores to Home Assistant via MQTT.
Stores are automatically created via MQTT Discovery and contain some addition attributes like price and more.
Easiest way is to use the Home Assistant Add-on. I created one in my Home Assistant Add-on repository.
Docker image is created automatically and available at dockerhub: maxwinterstein/toogoodtogo-ha-mqtt-bridge
This project uses some awesome tools for better developer experience:
None of this is required to run it natively, but makes the job a lot easier.
There are multiples ways to get going, some of the fastest might be using pkgx and direnv.
Having both pre-installed on your machine (see related docs) will get you going this quick:
- Adjust
settings.local.json- see below - Start a pkgx dev environment
dev # provided by pkgx - will read pkgx.yaml and install deps - Run it though Taskfile (the better Makefile INHO)
task run
Not needed when using HA Add-On
Create some settings file called settings.local.json (see settings.example.json):
{
"mqtt": {
"host": "homeassistant.local",
"port": 1883,
"username": "mqtt",
"password": "mqtt"
},
"tgtg": {
"email": "me@example.ocm",
"language": "en-US",
"polling_schedule": "*/10 * * * *",
"intense_fetch": { "interval": 30, "period_of_time": 5 }
},
"timezone": "Europe/Berlin",
"locale": "en_us"
}sets the polling interval in cron notation. For more Information have a look here: https://crontab.guru/
Is meant query your favourites for a short amount of time with a higher frequency.
Ideal for those boxes you always miss!
With the interval, the time between the queries can be controlled.
With the setting period_of_time the duration of the intense fetch can be defined.
The smallest interval is 10 seconds, and the maximum duration of the intense_fetch is 60 minutes.
Attention: This is meant for experienced users as you might get blocked for a certain amount of time by toogoodtogo.
When enabled, above mentioned intense_fetch will be started automatically when a shops sales window (automatically created portions) starts.
We add some jitter on the fetch interval, so not everyone hits the poor API at the same second.
as TooGoodToGo provides its times as UTC we format it to local time. See Wikipedia for valid values.
to format pickup times like in 2 hours. E.g. de for german, en_us for american english.
remove items from Home Assistant if they are no longer in the fetched result.
A more thorough version of cleanup. Instead of relying on the locally tracked store list
(which is empty after a fresh install or a wiped data dir), it reconciles against the MQTT
broker itself: it discovers every store entity the broker still holds and removes any that are
no longer in your favourites. Runs once shortly after startup and then daily. Enabled by
default — set to false to disable.
folder to store persistent data. Needed e.g. for cleanup feature.
By default all topics live under homeassistant/ so Home Assistant auto-discovers everything.
These optional settings let you decouple the data topics from the discovery prefix or use the
bridge without Home Assistant at all. All default to the historical topics, so existing setups
are unaffected.
{
"mqtt": { "base": "homeassistant/sensor" },
"homeassistant": { "enabled": true, "discovery_prefix": "homeassistant" },
"raw": false
}mqtt.base— base topic for the sensor state/attribute (andraw) data, e.g. set totgtgto publish state undertgtg/toogoodtogo_<id>/state. Defaulthomeassistant/sensor.homeassistant.enabled— set tofalseto stop publishing Home Assistant discovery configs (and the intense-fetch switch). State/attribute/raw data is still published, for non-HA MQTT consumers. Defaulttrue.homeassistant.discovery_prefix— Home Assistant's MQTT discovery prefix, if you've customised it in HA. Defaulthomeassistant.raw— also publish the full, unprocessed store payload to<mqtt.base>/toogoodtogo_<id>/raw. Defaultfalse.
And start with the mounted settings file, e.g. for macOS:
docker run --rm -ti --pull always -v $PWD/settings.local.json:/app/settings.local.json -v $PWD/data/:/data -v /etc/localtime:/etc/localtime:ro maxwinterstein/toogoodtogo-ha-mqtt-bridgeOr using docker-compose:
version: "3"
services:
toogoodtogo-bridge:
image: maxwinterstein/toogoodtogo-ha-mqtt-bridge
container_name: toogoodtogo-bridge
volumes:
- ./settings.local.json:/app/settings.local.json
- ./data:/data
- /etc/localtime:/etc/localtime:ro
restart: unless-stoppedAttributes are used to keep the amount of sensors small. If you want some specific sensor you can create it as template sensor.
sensor:
- platform: template
sensors:
toogoodtogo_eilles_frankfurt_price:
unit_of_measurement: "€"
icon_template: mdi:currency-eur
friendly_name: "Eilles Frankfurt Price"
value_template: "{{ state_attr('sensor.toogoodtogo_123456', 'price') }}"Note
Entity IDs changed. Newly discovered sensors now get a short, stable ID based on the
store's immutable TooGoodToGo item ID — e.g. sensor.toogoodtogo_123456 — instead of one
derived from the (changeable) store name. Existing sensors keep their current IDs: Home
Assistant never renames an entity once created, so only stores favourited after updating use
the new scheme. Older installs may therefore show a mix of sensor.toogoodtogo_… and the
legacy sensor.too_good_to_go_toogoodtogo_… forms.
To migrate existing sensors to the new IDs, open the Too Good To Go device in Home Assistant, select the three-dots menu and choose Recreate entity IDs (see the Home Assistant docs). Heads-up: this does not update references in your automations, scripts or dashboards — you have to fix those manually.
Add the following piece of code into /developer-tools/template in HomeAssistant. (Remove the last comma)
{%- for state in states -%}
{%- if (state.entity_id.startswith('sensor.toogoodtogo_'))-%}
{{state.entity_id}},
{%- endif -%}
{%- endfor -%}alias: TooGoodToGo Notification
description: "Sends a notification when a toogoodtogo offer becomes available"
trigger:
- platform: state
entity_id:
>- # This is your list of toogoodoto sensors which are generated (Copy paste the list from above)
sensor.toogoodtogo_1,sensor.toogoodtogo_2,sensor.toogoodtogo_3
attribute: stock_available
from: false
to: true
condition: []
action:
- service: notify.mobile_app_android
data:
message: >-
Available: {{trigger.to_state.state}}, For:
{{trigger.to_state.attributes.price}} in
{{trigger.to_state.attributes.pickup_start_human}}
title: "{{trigger.to_state.attributes.friendly_name}}"
data:
clickAction: "{{trigger.to_state.attributes.url}}"
image: "{{trigger.to_state.attributes.picture}}"
group: tgtg
tag: "{{trigger.entity_id}}"
- service: notify.mobile_app_iphone
data:
message: >-
Available: {{trigger.to_state.state}}, For:
{{trigger.to_state.attributes.price}} in
{{trigger.to_state.attributes.pickup_start_human}},
{{trigger.to_state.attributes.friendly_name}}
title: "{{trigger.to_state.attributes.friendly_name}}"
data:
url: "{{trigger.to_state.attributes.url}}"
image: "{{trigger.to_state.attributes.picture}}"
group: tgtg
tag: "{{trigger.entity_id}}"
mode: parallel
max: 10alias: TooGoodToGo UnNotification
description: ""
trigger:
- platform: state
entity_id:
>- # This is your list of toogoodoto sensors which are generated (Copy paste the list from above)
sensor.toogoodtogo_1,sensor.toogoodtogo_2,sensor.toogoodtogo_3
attribute: stock_available
from: true
to: false
condition: []
action:
- service: notify.mobile_app_android
data:
message: clear_notification
data:
tag: "{{trigger.entity_id}}"
- service: notify.mobile_app_iphone
data:
message: clear_notification
data:
tag: "{{trigger.entity_id}}"
mode: parallel
max: 10alias: TGTG Pickup Reminder
description: ""
triggers:
- trigger: time
at:
entity_id: sensor.toogoodtogo_next_collection
offset: "-00:30:00"
actions:
- variables:
next_collection_entity: sensor.toogoodtogo_next_collection
notification_message: |-
🛍️ <b>Too Good To Go Pickup Reminder</b>
<b>Item:</b> {{ state_attr(next_collection_entity, 'item_name') }}
<b>Store:</b> {{ state_attr(next_collection_entity, 'store_name') }}
<b>Time:</b> {{ as_timestamp(state_attr(next_collection_entity, 'pickup_start')) | timestamp_custom('%H:%M') }} - {{ as_timestamp(state_attr(next_collection_entity, 'pickup_end')) | timestamp_custom('%H:%M') }} <i>({{ state_attr(next_collection_entity, 'pickup_start_human') }})</i>
<b>Address:</b> {{ state_attr(next_collection_entity, 'address') }}
<b>Maps:</b> <a href="https://www.google.com/maps/search/?api=1&query={{ state_attr(next_collection_entity, 'address') | urlencode }}">Open in Google Maps</a>
- action: telegram_bot.send_message
metadata: {}
data:
disable_notification: true
target: <TELEGRAM USERID>
parse_mode: html
message: "{{ notification_message }}"This project uses pre-commit to make sure the code keeps clean and similar. Usage is highly advised.
See CONTRIBUTING.md for the full development setup and the checks to run before opening a pull request.
Big thanks to everyone contributing <3
Made with contrib.rocks.
Copyright (c) 2026 Max Winterstein
This project is licensed under the GNU General Public License v3.0 — see the
LICENSE file for the full text.

