# Affald fra mail til stemmebesked – sådan husker vi skraldespanden

- Link: https://www.vogh.dk/affald-fra-mail-til-stemmebesked-saadan-husker-vi-skraldespanden/
- Udgivet: 2026-10-09T20:37:32+00:00
- Forfatter: ha-upload

Hvem har ikke stået en morgen og hørt skraldebilen dreje om hjørnet – med spanden
stadig inde i carporten? Hos os er det et løst problem. Forsyningen sender en mail
dagen før tømning, og Home Assistant gør resten: push-besked, kalender, kort på 
tabletten, en stemmebesked om aftenen og til sidst et kamera, der tjekker, om spanden
faktisk står ude.

## Overblikket

 1. Ca. kl. 16 dagen før tømning kommer en mail fra forsyningen (via affaldonline.dk).
 2. Home Assistant læser mailen og sender en push-besked, gemmer teksten og sikrer,
    at tømningen står i affaldskalenderen.
 3. Tabletten i køkkenet viser et kort: “Mad/Rest – tømmes i morgen”.
 4. Kl. 19 siger Home Assistant Voice beskeden højt – medmindre nogen allerede har 
    trykket “Kørt ud” på tabletten.
 5. Kl. 18 aftenen før – og når der sker noget ved spanden – tæller et kamera med AI
    beholderne på deres faste plads. Mangler der en, er den kørt ud, og stemmebeskeden
    kl. 19 springes over.

## 1. En mailkonto kun til Home Assistant

Mailen fra forsyningen går til en separat mailkonto, som kun Home Assistant læser.
Den er koblet på med den indbyggede **IMAP-integration**, som affyrer et `imap_content`-
event, hver gang der kommer en ny mail. Kontoen er kun til notifikationer – den 
kan ikke bruges til at styre noget.

Det lyder enkelt, men der var en del fælder undervejs:

 * **Afsenderen er ikke forsyningen.** Mailen kommer fra `noreply@affaldonline.dk`,
   mens Return-Path er `no_reply@affaldonline.dk`. Derfor matcher automationen på“
   affaldonline” som en del af afsenderen i stedet for en præcis adresse.
 * **Emnefeltet kommer forkert ud.** “Tømning” bliver til “T?mning”, mens selve 
   mailteksten har korrekte danske tegn. Al tekst hentes derfor fra mailens indhold.
 * **Teksten er HTML.** Feltet `text` indeholder HTML-udgaven, ikke den rene tekst.
   Fjerner man bare HTML-tags, står hele CSS-blokken tilbage – så `<style>` skal
   fjernes først.
 * **Standardgrænsen på 2048 tegn er for lille.** Mailen bliver klippet af, før 
   den egentlige besked kommer. Sæt _max message size_ i integrationens indstillinger
   op – her er den 16384.
 * **Datoen er ikke en tekst.** `trigger.event.data.date` er et datetime-objekt.
   Prøver man at lave hele eventet om til JSON, fejler automationen stille: Den 
   står som kørt, men der sker ingenting.
 * **Marker mailen som læst.** Ingen læser kontoen, så uden et kald til `imap.seen`
   bliver mailen ved med at være ulæst – og hver genstart af Home Assistant giver
   en ny notifikation.

### Fra HTML-mail til en sætning man kan sige højt

Mailen ser sådan ud: _“Du får tømt din mad- og restaffaldsbeholder – [adresse] –
i morgen.”_ Automationen renser den i tre trin med variabler:

    ```nord
    variables:
      # 1. Fjern <style>, så HTML-tags, og skær hilsenen fra
      body_clean: >-
        {% set c = trigger.event.data.text | default('') | string
           | regex_replace('(?s)<style.*?</style>', ' ')
           | regex_replace('<[^>]+>', ' ')
           | replace(' ', ' ')
           | regex_replace('\s+', ' ') | trim %}
        {{ c.split('Mvh.')[0] | trim }}
      # 2. Brug en standardtekst, hvis der ikke kom noget brugbart ud
      msg: "{{ body_clean if body_clean | length > 5 else 'Ny besked om affaldstømning' }}"
      # 3. Fjern adressen (det midterste led) til stemmebeskeden
      spoken: >-
        {% set p = msg.split(' - ') %}{% if p | length >= 3 %}{{ (p[0] ~ ' ' ~ (p[2:] | join(' '))) | regex_replace('\s+', ' ') | trim }}{% else %}{{ msg }}{% endif %}
    ```

Push-beskeden får hele teksten, men stemme beskeden får udgaven uden adresse. Det
lyder meget mere naturligt at høre _“Du får tømt din mad- og restaffaldsbeholder
i morgen”_ end at få læst sin egen adresse op. Teksten gemmes i en `input_text` (
klippet til 250 tegn, da en `input_text` højst kan rumme 255), og en `input_boolean`
slås til som flag for, at der venter en besked.

## 2. Kalenderen er sandheden

Affaldskalenderen fyldes normalt udefra med alle årets tømninger. Mail-automationen
bruger den som sikkerhedsnet: Hvis mailen siger “i morgen”, slår den op i kalenderen
med `calendar.get_events`, og _kun_ hvis datoen er tom, oprettes en heldagsbegivenhed
med den rigtige affaldstype. Så bliver der aldrig dubletter, og der bliver aldrig
gættet på en dato, hvis forsyningen ændrer ordlyden.

Tabletkortet læser fra kalenderen – ikke fra mailen – så det virker også, hvis en
mail skulle gå tabt. Her lå endnu en fælde: **En kalenders attributter beskriver
den begivenhed, der er i gang nu**, ikke den næste. På en tømningsdag kører heldagsbegivenheden
fra 00 til 24 og skygger for morgendagens tømning. Da vi havde to tømninger i træk,
forsvandt kortet præcis når det skulle bruges. Løsningen er en lille automation,
der hvert kvarter kalder `calendar.get_events` for i dag og i morgen og skriver 
resultatet til en hjælper, f.eks. “🌿 Haveaffald — tømmes i morgen”.

![](https://www.vogh.dk/wp-content/uploads/2026/10/image-3.png)

## 3. Stemmebesked kl. 19

Mailen kommer om eftermiddagen, så kl. 19 samme aften er et godt tidspunkt – alle
er hjemme, og der er stadig tid til at køre spanden ud. Beskeden siges kun, hvis
flaget er sat, og flaget slås fra bagefter, så den kun siges én gang:

    ```nord
    alias: "Affald: stemmeannoncering kl. 19"
    mode: single
    triggers:
      - trigger: time
        at: "19:00:00"
    conditions:
      - condition: state
        entity_id: input_boolean.affald_afventer
        state: "on"
    actions:
      - action: assist_satellite.announce
        target:
          entity_id: assist_satellite.home_assistant_voice_092cb5_assist_satellite
        data:
          message: "{{ states('input_text.affald_besked') }}"
          preannounce: true
        continue_on_error: true
      - action: input_boolean.turn_off
        target:
          entity_id: input_boolean.affald_afventer
    ```

`preannounce` spiller en lille lyd først, så man når at lytte efter. `continue_on_error`
sørger for, at flaget stadig bliver slået fra, selv hvis højttaleren er offline –
ellers ville den gamle besked blive læst op aftenen efter.

## 4. “Kørt ud”-knappen

Er spanden allerede kørt ud, er der ingen grund til at blive mindet om det. På tabletkortet
er der derfor en knap, “Kørt ud”. Trykker man på den, slås flaget fra, og stemmebeskeden
springes over. Når påmindelsen slukker efter tømningen, nulstilles knappen igen:

    ```nord
    alias: "Affald: kvittering for kørt ud"
    mode: queued
    triggers:
      - trigger: state
        entity_id: input_boolean.skraldespand_kort_ud
        from: "off"
        to: "on"
        id: kvitteret
      - trigger: state
        entity_id: binary_sensor.affald_snart
        from: "on"
        to: "off"
        id: toemt
    actions:
      - choose:
          - conditions:
              - condition: trigger
                id: kvitteret
            sequence:
              - action: input_boolean.turn_off
                target:
                  entity_id: input_boolean.affald_afventer
          - conditions:
              - condition: trigger
                id: toemt
            sequence:
              - action: input_boolean.turn_off
                target:
                  entity_id: input_boolean.skraldespand_kort_ud
    ```

## 5. Kameraet tjekker efter

Kl. 18 aftenen før hver tømning – seks timer før kalenderens heldagsbegivenhed starter–
tager et af UniFi-kameraerne ved indkørslen et billede, og HACS-integrationen **
LLM Vision** spørger en AI-model, om affaldsbeholderne er kørt ud. Det er også derfor,
kalenderen skal være komplet – mangler en tømning, bliver tjekket aldrig kørt. Og
tidspunktet er ikke tilfældigt: kl. 18 er en time før stemmebeskeden.

### Tre fejl der gjorde kameraet blindt

Da jeg gravede i det for at skrive dette indlæg, viste det sig, at analysen **aldrig
var blevet kørt**:

 1. **Et tomt trin stoppede alt.** Første trin var et `ai_task.generate_data` uden 
    data – en rest fra en tidligere udgave. Home Assistant afviser kaldet, og automationen
    stopper, før analysen nås. Udefra så alt normalt ud: “sidst udløst kl. 18:00”, 
    ingen fejlbesked.
 2. **Udbyderen var slået fra.** AI-udbyderen i LLM Vision var deaktiveret, og LLM 
    Vision-hukommelsen pegede på et billede, der var slettet. Løsningen blev at oprette**
    Google Gemini** som ny udbyder og rydde op i hukommelsen.
 3. **Kameraet der både virker og ikke virker.** Det valgte kamera stod som `unavailable`,
    så mit første gæt var, at det var forkert. Men da jeg hentede billeder fra begge
    kameraer, var det netop det “utilgængelige”, der kunne se beholderne. Entiteten
    skifter til `recording` et splitsekund hvert tiende minut – men snapshot virker
    fint.

### Tæl i stedet for at spørge

Første udgave spurgte modellen direkte: Er beholderne kørt ud? Svaret skulle starte
med **JA**, **NEJ** eller **USIKKER**. Det gik galt på to måder. Modellen skrev _“
NEJ. Affaldsbeholderne står …”_ på én linje, så svaret måtte læses som første ord
i stedet for første linje. Og værre: Dagen efter svarede den **JA** til præcis samme
scene – beholderne stod på deres faste plads. Det var ikke kun modellens skyld. 
Spandene stilles ud ved vejen, og **den kan kameraet slet ikke se**. Spørgsmålet
var umuligt at besvare.

Nu spørger automationen om noget, kameraet _kan_ se: **Hvor mange beholdere står
på deres faste plads?** Normalt er der tre – to ved garagegavlen og én ved buskene.
Første linje skal kun være antallet med cifre, eller USIKKER, hvis det er for mørkt,
eller noget dækker. At tælle er en langt enklere opgave end at gætte på noget uden
for billedet. Svaret læses som første ord eller tal, efter at alt andet end bogstaver
og cifre er fjernet:

| Svar fra modellen | Antal | Betyder | 
| `3` + beskrivelse på ny linje | 3 | Alle står hjemme – intet sker | 
| `**2.** To ved gavlen` | 2 | En er kørt ud – “Kørt ud” slås til | 
| `USIKKER, mørkt` | – | Intet sker | 
| `tre` eller `Antal: 2` | – | Ikke et rent tal – intet sker | 
| (fejl / intet svar) | – | Intet sker |

### Koblet til “Kørt ud”

Mangler der en beholder, mens der er tømning på vej, slår automationen den samme“
Kørt ud”-hjælper til som knappen på tabletten – og stemmebeskeden kl. 19 springes
over. Alt andet ændrer ingenting. Det er bevidst asymmetrisk: Det værste, der kan
ske, er en påmindelse for meget – aldrig en for lidt.

Kravet om, at der skal være tømning på vej, kom efter en testkørsel. Uden det kunne
et forkert svar mellem to tømninger slå “Kørt ud” til – og så ville stemmebeskeden
ved _næste_ tømning blive sprunget over, fordi hjælperen først nulstilles efter 
en tømning.

### Tjek når der sker noget ved spanden

Kl. 18 er sikkerhedsnettet. Derudover kalder en **webhook** Home Assistant, når 
der sker noget ved skraldespanden. Er der tømning på vej, og er der ikke allerede
kvitteret, venter automationen 30 sekunder, så spanden er rullet væk, og starter
så den samme optælling. Bagefter holder den 10 minutters pause, hvor nye webhooks
ignoreres, så travlhed ved spanden ikke giver en strøm af AI-kald. Logikken findes
kun ét sted – webhook-automationen kalder bare kameraautomationen.

    ```nord
    alias: Analyze Trashcan Image
    mode: single
    variables:
      normalt_antal: 3   # beholdere på deres faste plads
    triggers:
      # Heldagsbegivenheder starter 00:00, så -6 t = kl. 18 aftenen før
      - trigger: calendar
        entity_id: calendar.affalds_kalender
        event: start
        offset: "-6:0:0"
    actions:
      - action: llmvision.image_analyzer
        continue_on_error: true
        response_variable: analyse
        data:
          provider: DIN_LLM_VISION_UDBYDER   # f.eks. Google Gemini
          image_entity:
            - camera.g6_turret_high_resolution_channel_2
          message: >-
            Billedet er fra et overvågningskamera ved en indkørsel med garager.
            Tæl affaldsbeholderne (sorte/grå containere på hjul) i billedet.
            Normalt står der {{ normalt_antal }}: to ved gavlen af garagelængen
            og én længere til venstre ved buskene.
            Første linje skal KUN være antallet som et tal med cifre.
            Kan du ikke se området tydeligt, så skriv USIKKER i stedet.
            Beskriv derefter kort på dansk, hvor beholderne står.
          store_in_timeline: true
          use_memory: false
          target_width: 1280
          max_tokens: 300
          response_format: text
      # Første ord/tal i svaret; antal = -1, hvis det ikke er et rent tal
      - variables:
          svar: >-
            {{ ((analyse.response_text if analyse is defined and analyse is mapping else '')
                | regex_replace('[^A-Za-zÆØÅæøå0-9]+', ' ') | trim | upper).split(' ')[0] }}
          antal: "{{ svar | int(-1) if svar is match('^[0-9]+$') else -1 }}"
      # Kun når der er tømning på vej, og en beholder mangler
      - if:
          - condition: state
            entity_id: binary_sensor.affald_snart
            state: "on"
          - condition: template
            value_template: "{{ 0 <= antal | int(-1) < normalt_antal }}"
        then:
          - action: input_boolean.turn_on
            target:
              entity_id: input_boolean.skraldespand_kort_ud
    ```

`continue_on_error` på analysen betyder, at automationen fortsætter, hvis kameraet
eller AI-tjenesten er nede – `antal` bliver -1, og den normale påmindelse kører.
Tjekket på `trigger.platform` gør, at automationen også kan testkøres manuelt, hvor
der ingen kalenderbegivenhed er.

## Det jeg har lært

 * **Stol ikke på mailens overflade.** Afsender, emnefelt og tekstformat var alle
   anderledes end forventet. Kig på det rå event i Udviklerværktøjer → Events, før
   du skriver automationen.
 * **Brug én kilde til sandheden.** Mailen er triggeren, men kalenderen er det, 
   alt andet læser fra.
 * **Spørg kun om det, kameraet kan se.** “Er spanden kørt ud?” kunne ikke besvares,
   fordi vejen er uden for billedet. “Hvor mange står på deres plads?” kan.
 * **Tænk på de stille fejl.** Et JSON-kald der fejler, en højttaler der er offline,
   en mail der aldrig markeres som læst – det er dem, der giver mærkelig opførsel
   uger senere.

Resultatet er, at ingen i familien behøver at holde styr på tømmekalenderen længere.
Huset siger til.
