eTilbudsavis in Home Assistant — Nach Geschäft sortiert mit Benachrichtigung beim Betreten
eTilbudsavis in Home Assistant: nach Geschäft sortierte Liste mit Preis in der Beschreibung und Push-Benachrichtigung beim Betreten relevanter Geschäfte.
Acht Artikel. Vier Geschäfte. Vor dem Eingang zum Bilka — ich wusste nicht mehr, welche davon von hier waren. Ich öffnete die eTilbudsavis-App, scrollte durch die gesamte Liste, suchte die zwei Bilka-Artikel heraus. Der ganze Vorgang dauerte länger als das Einkaufen selbst.
Einige Tage zuvor hatte jemand in einer dänischen Facebook-Gruppe erwähnt, dass Home Assistant eine Push-Benachrichtigung senden kann, wenn man ein Geschäft betritt, in dem Artikel auf der Einkaufsliste angeboten werden. Das hatte ich nicht gewusst. Jetzt schon.
Die Integration — CagosDk auf GitHub
Der Ausgangspunkt ist eine HACS-Integration von CagosDk. Sie wird über HACS → Integrationen → Benutzerdefinierte Repositories hinzugefügt. Die Integration erzeugt eine todo-Entität für jede eTilbudsavis-Einkaufsliste — bei mir todo.min_indkobsliste. Sie erscheint dann als natives HA-Todo-Karte und in der Companion-App.
Das Zwei-App-Problem war damit gelöst. Die Standardansicht zeigt aber nur Artikelnamen — kein Geschäft, kein Preis, kein Datum. Bei fünfzehn Artikeln aus vier verschiedenen Geschäften hilft das wenig, wenn es schnell gehen muss.
Anpassungen in todo.py: Beschreibung und Sortierung
Die Integration speichert alle API-Daten im Coordinator — darunter business.name (Geschäft), offer.price und offer.validUntil. Standardmäßig schreibt todo.py nur den Geschäftsnamen ins description-Feld. Um Preis und Datum hinzuzubekommen, habe ich zwei Hilfsfunktionen ergänzt und den Sortierschlüssel geändert.
Der Description-Builder:
def _build_description(store: str, offer: dict | None, now: datetime) -> str | None:
if not store:
return None
offer = offer or {}
price = offer.get("price")
valid_until_str = offer.get("validUntil")
desc = store
if price is not None:
price_str = f"{price:.2f}".replace(".", ",")
desc += f" - {price_str} kr."
if valid_until_str:
try:
valid_until = datetime.fromisoformat(valid_until_str.replace("Z", "+00:00"))
if (valid_until - now).total_seconds() > 0:
desc += f" til {valid_until.day}/{valid_until.month}"
except ValueError:
pass
return desc
Sowohl price als auch validUntil werden per .get() abgerufen — fehlen sie in der API-Antwort, werden sie einfach übersprungen. Das Ergebnis ist immer gültig: mindestens der Geschäftsname.
Die zweite Hilfsfunktion behandelt abgelaufene Angebote. Liegt validUntil in der Vergangenheit, bekommt die Artikelzusammenfassung ein Präfix:
def _expiry_prefix(offer: dict | None, now: datetime) -> str:
if not offer:
return ""
valid_until_str = offer.get("validUntil")
if not valid_until_str:
return ""
try:
valid_until = datetime.fromisoformat(valid_until_str.replace("Z", "+00:00"))
except ValueError:
return ""
if (valid_until - now).days < 0:
return "UDLOBET!!! "
return ""
In der todo_items-Property werden beide Funktionen pro Artikel aufgerufen, und der Sortierschlüssel bekommt die Geschäftsdimension:
store = (item.get("business") or {}).get("name") or ""
prefix = _expiry_prefix(item.get("offer"), now)
summary = f"{prefix}{count}x {name}"
description = _build_description(store, item.get("offer"), now)
result.append((store, TodoItem(...)))
result.sort(key=lambda x: (
x[1].status == TodoItemStatus.COMPLETED,
x[0].lower(), # Geschäft — gruppiert Artikel zusammen
x[1].summary.lower(),
))
Das Ergebnis: unerledigte Artikel vor erledigten, dann alphabetisch nach Geschäft, dann alphabetisch innerhalb eines Geschäfts.
Zwei Dinge sind vor der Bearbeitung wichtig. Erstens liegt todo.py unter /config/custom_components/etilbudsavis/todo.py auf der HA-Maschine — HACS-Updates überschreiben die Datei. Die Änderungen sind klein genug, um sie in wenigen Minuten erneut einzupflegen. Zweitens erfordert jede Änderung an Python-Dateien in Custom Components einen vollständigen HA-Neustart — reload_config_entry leert den Python-Modulcache nicht.
Zonen und Proximity-Benachrichtigungen
Das ist der Teil aus dem Facebook-Kommentar. Home Assistant hat einen zone-Entitätstyp — man legt einen Radius um eine Adresse fest und die Companion-App trackt, wann das Telefon diese Zone betritt oder verlässt. Ich habe Zonen für die Geschäfte angelegt, in denen wir tatsächlich einkaufen: Min Kobmand, ABC Lavpris, Bilka, Lidl, Føtex und zwei Netto-Filialen.
Eine Automation löst aus, wenn mein Pixel 10 oder das Pixel 9a meiner Frau eine dieser Zonen betritt:
trigger:
- platform: zone
entity_id:
- device_tracker.pixel_10
- device_tracker.pixel_9a
zone: zone.bilka
event: enter
# ... für jede weitere Geschäftszone wiederholen
Die Automation ordnet jede Zone einem Geschäftsnamen in einem variables-Block zu. Zwei Netto-Standorte zeigen auf denselben Namen — sie lösen dieselbe Benachrichtigung aus:
variables:
butik_lookup:
zone.min_kobmand: "Min Kobmand"
zone.abc_lavpris: "ABC Lavpris"
zone.bilka: "Bilka"
zone.lidl_vejlevej: "Lidl"
zone.lidl_mosevej: "Lidl"
zone.fotex: "Føtex"
zone.netto_hylkedalvej: "Netto"
zone.netto_stadionvej: "Netto"
butik_navn: "{{ butik_lookup[trigger.zone] | default('') }}"
Die Aktion ruft dann todo.get_items ab, filtert Artikel deren description mit dem Geschäftsnamen beginnt, und sendet eine Push-Benachrichtigung wenn Treffer vorhanden sind:
- target:
entity_id: todo.min_indkobsliste
data:
status: needs_action
response_variable: todo_svar
action: todo.get_items
- variables:
varer: >
{% set alle = todo_svar['todo.min_indkobsliste']['items'] | default([]) %}
{% set ns = namespace(liste=[]) %}
{% for vare in alle %}
{% if vare.description | default('') | regex_match(butik_navn) %}
{% set ns.liste = ns.liste + [vare.summary] %}
{% endif %}
{% endfor %}
{{ ns.liste }}
- condition: template
value_template: "{{ varer | count > 0 }}"
- data:
title: "🛒 {{ butik_navn }} – {{ varer | count }} Artikel auf der Liste"
message: "{{ varer | join('\n') }}"
action: notify.alle_enheder
regex_match matcht vom Anfang des Strings, sodass „Netto” auf „Netto – 12,95 kr. · til 12/6” passt, ohne einen contains-Check zu brauchen. notify.alle_enheder durch den eigenen Notify-Service ersetzen — das ist die Gruppe, die bei mir beide Telefone gleichzeitig erreicht.
Das erste Mal funktionierte es vor dem Netto auf dem Hylkedalvej. Telefon vibriert. Zwei Artikel auf der Liste. Ich wusste es eigentlich — ich hatte die Liste selbst geschrieben. Aber es auf dem Display zu sehen genau in dem Moment, in dem man das Geschäft betritt, ist etwas anderes als daran zu denken, die App zu öffnen.