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.

#home-assistant #einkaufen #automation #hacs #todo #zonen
9. Juni 2026
eTilbudsavis in Home Assistant — Nach Geschäft sortiert mit Benachrichtigung beim Betreten

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.

Home Assistant Dashboard mit einem Einkaufswagen-Chip, der die Anzahl der ausstehenden Artikel anzeigt

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.

eTilbudsavis Einkaufsliste in der Home Assistant Companion-App, nach Geschäft sortiert mit Preis und Datum

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-Neustartreload_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.