eTilbudsavis i Home Assistant — Sorteret efter Butik med Notifikation ved Ankomst

Hvordan jeg fik eTilbudsavis i Home Assistant: varer sorteret efter butik, pris synlig i beskrivelsen og push-notifikation ved ankomst til en butik.

#home-assistant #indkøb #automation #hacs #todo #zoner
9. juni 2026
eTilbudsavis i Home Assistant — Sorteret efter Butik med Notifikation ved Ankomst

Jeg stod i Netto og vidste ikke om der var noget på listen herfra. Det var der — men det var begravet midt i femten varer fra tre andre butikker. Jeg scrollede. Fandt dem til sidst. Det sker hver gang.

Ugen forinden havde nogen i en dansk Facebook-gruppe nævnt at Home Assistant kunne sende en notifikation når man trådte ind i en butik med varer på listen. Jeg tænkte over det hele vejen hjem fra Netto.

Integrationen — CagosDk på GitHub

Udgangspunktet er en HACS-integration lavet af CagosDk. Tilføj den via HACS → Integrationer → Brugerdefinerede repositories, indsæt GitHub-URL’en og tilføj integrationen. Den opretter en todo-entity for hvert af dine eTilbudsavis-lister — min hedder todo.min_indkobsliste. Derfra optræder den som et normalt HA-todo-kort og i companion-appen på telefonen.

Home Assistant dashboard med et indkøbskurv-chip der viser antal varer på listen

Et-app-problemet var løst. Men standardvisningen viser kun varenavne — ingen butik, ingen pris, ingen dato. Med femten varer fra fire butikker er det ikke nyttigt når man har travlt.

Ændringer i todo.py: description og sortering

Integrationen gemmer alle data fra eTilbudsavis API’en i coordinatoren — herunder business.name (butik), offer.price og offer.validUntil. Som standard sætter todo.py kun butiksnavnet i description-feltet. For at få pris og dato med tilføjede jeg to hjælpefunktioner og ændrede sorteringsnøglen.

Description-builderen:

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

Både price og validUntil hentes med .get() — hvis API’en ikke returnerer dem for shopping list-items, springes de bare over. Resultatet er altid gyldigt: som minimum kun butiksnavnet.

Den anden hjælpefunktion håndterer udløbne tilbud. Hvis validUntil er i fortiden, får varens summary et præfix så det er tydeligt:

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 ""

I todo_items-propertyen kaldes begge funktioner per vare, og sorteringsnøglen får butiksdimensionen:

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(),   # butik — samler varer fra samme butik
    x[1].summary.lower(),
))

Resultatet: ufuldførte varer før afkrydsede, derefter alfabetisk efter butik, derefter alfabetisk inden for butikken.

eTilbudsavis indkøbsliste i Home Assistant companion-appen, sorteret efter butik med pris og dato synlig

To ting er værd at vide inden du redigerer filen. For det første ligger todo.py/config/custom_components/etilbudsavis/todo.py på HA-maskinen — HACS-opdateringer overskriver den. Ændringerne er små nok til at det tager et par minutter at genoprette dem. For det andet kræver ændringer i Python-filer i custom components en fuld HA-genstartreload_config_entry tømmer ikke Pythons module cache.

Zoner og proximity-notifikationer

Det er den del Facebook-kommentaren handlede om. Home Assistant har en zone-entity-type — man tegner en cirkel rundt om en adresse og companion-appen tracker hvornår telefonen befinder sig inden for den radius. Jeg oprettede zoner for de butikker vi faktisk handler i: Min Kobmand, ABC Lavpris, Bilka, Lidl, Føtex og to Netto-adresser.

En automation triggerer når enten min Pixel 10 eller min kones Pixel 9a ankommer til en af zonerne:

trigger:
  - platform: zone
    entity_id:
      - device_tracker.pixel_10
      - device_tracker.pixel_9a
    zone: zone.bilka
    event: enter
  # ... gentages for hvert butiks-zone

Automationen kobler hvert zone-entity til et butiksnavn i et variables-blok. To Netto-adresser peger på det samme navn — de giver den samme notifikation uanset hvilken butik man ankommer til:

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('') }}"

Handlingen henter den aktuelle liste via todo.get_items, filtrerer varer hvis description starter med butiksnavnet, og sender en push hvis der er matches:

- 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 }} vare(r) på listen"
    message: "{{ varer | join('\n') }}"
  action: notify.alle_enheder

regex_match matcher fra starten af strengen, så “Netto” matcher “Netto – 12,95 kr. · til 12/6” uden at bruge contains. Erstat notify.alle_enheder med din egen notify-service — det er den gruppe jeg bruger til at ramme begge telefoner på én gang.

Første gang det virkede stod jeg foran Netto på Hylkedalvej. Telefonen vibrerede. To varer. Jeg vidste godt hvad der stod på listen — jeg havde selv skrevet den — men det er noget andet at have den på skærmen i det øjeblik man træder ind ad døren.