# Husopgaver som GitHub-issues – fra stemmekommando til ticket

- Link: https://www.vogh.dk/husopgaver-som-github-issues-fra-stemmekommando-til-ticket/
- Udgivet: 2026-10-11T17:50:44+00:00
- Forfatter: ha-upload

“Skuret skal ryddes.” “Pæren i bryggerset er sprunget.” “Husk at skifte filter i
emhætten.” Den slags opgaver plejede at ende på en seddel på køleskabet eller i 
hovedet på én person, hvor de så blev glemt. Nu siger man det højt til Home Assistant,
og et øjeblik efter ligger opgaven som et **issue på GitHub** og venter på at blive
lukket.

Til daglig arbejder jeg med ServiceNow, så en opgave er for mig en ticket. Den har
et nummer, en historik, og nogen lukker den, når den er løst. GitHub Issues giver
præcis det, uden at jeg skal drive et helt ticketsystem derhjemme. Telefon-appen
er god, labels er gratis, og alt er der stadig om tre år.

## Sådan hænger det sammen

 * **Indbakke:** En helt almindelig lokal to-do-liste i Home Assistant, `todo.husopgaver`.
   Den kan alle i huset skrive på via stemmen (“tilføj skift filter til husopgaver”),
   via HA-appen eller via en lille formular på vægtabletten.
 * **Automation:** Hver gang listen ændrer sig, laves alle åbne punkter om til GitHub-
   issues i et privat repository. Bagefter fjernes de fra listen.
 * **Visning:** En template-sensor henter de åbne og de senest lukkede issues hvert
   5. minut, så de kan vises på dashboardet.
 * **Lukning:** Det sker på GitHub, typisk fra telefonen, når opgaven er løst.

To-do-listen er altså _kun_ en indbakke. Opgaverne bor ét sted, nemlig på GitHub,
så der aldrig er to lister, der er uenige om, hvad der mangler.

## Adgang til GitHub

Lav et **fine-grained personal access token** på GitHub, der _kun_ har adgang til
det ene repository og _kun_ har rettigheden “Issues: Read and write”. Slipper tokenet
ud, kan det ikke andet end at rode med husopgaverne. Det lægges i `secrets.yaml`
sammen med ordet Bearer:

    ```nord
    github_husopgaver_token: "Bearer github_pat_xxxxxxxxxxxx"
    ```

## REST-kommandoerne

Det hele ligger i én pakkefil, `packages/husopgaver.yaml`. Først kommandoen, der
opretter et issue:

    ```nord
    rest_command:
      github_create_issue:
        url: https://api.github.com/repos/DIN-BRUGER/husopgaver/issues
        method: POST
        headers:
          Authorization: !secret github_husopgaver_token
          Accept: application/vnd.github+json
          User-Agent: HomeAssistant
        content_type: application/json
        payload: '{{ {"title": title, "body": body} | to_json }}'

      github_list_issues:
        url: https://api.github.com/repos/DIN-BRUGER/husopgaver/issues?state=open&per_page=100
        method: GET
        headers:
          Authorization: !secret github_husopgaver_token
          Accept: application/vnd.github+json
          User-Agent: HomeAssistant
    ```

To detaljer, der er værd at kende:

 * **`| to_json` på hele payloaden.** Så bliver anførselstegn og linjeskift i titlen
   escaped korrekt. Skriver man JSON’en i hånden, går den i stykker første gang 
   nogen skriver _“Ret ‘den’ skuffe”_.
 * **User-Agent.** GitHubs API afviser kald uden en User-Agent. Den kan i øvrigt
   også give problemer den anden vej: Min egen webhost blokerede Home Assistants
   standard-User-Agent, så det er en god vane altid at sætte den selv.

## Automationen: fra liste til issue

    ```nord
    alias: Husopgaver → GitHub issues
    mode: queued
    max: 10
    triggers:
      - trigger: state
        entity_id: todo.husopgaver
    actions:
      - action: todo.get_items
        target:
          entity_id: todo.husopgaver
        data:
          status: needs_action
        response_variable: result
      - variables:
          oprettet_af: >-
            {% set uid = trigger.to_state.context.user_id if trigger.to_state is not none else none %}
            {{ states.person | selectattr('attributes.user_id', 'eq', uid)
               | map(attribute='name') | first
               | default('stemme/ukendt' if uid is none else 'ukendt bruger', true) }}
      - repeat:
          for_each: "{{ result['todo.husopgaver']['items'] }}"
          sequence:
            - action: rest_command.github_create_issue
              data:
                title: "{{ repeat.item.summary }}"
                body: >-
                  {{ repeat.item.description ~ '\n\n' if repeat.item.description is defined
                     and repeat.item.description else '' }}Oprettet af {{ oprettet_af }} via Home Assistant
              response_variable: svar
              continue_on_error: true
            - if:
                - condition: template
                  value_template: "{{ svar is defined and svar.status == 201 }}"
              then:
                - action: todo.remove_item
                  target:
                    entity_id: todo.husopgaver
                  data:
                    item: "{{ repeat.item.uid }}"
                - event: husopgaver_opdater
                  event_data:
                    number: "{{ svar.content.number }}"
    ```

Det vigtigste her er rækkefølgen. **Punktet fjernes først fra listen, når GitHub
har svaret 201 Created.** Er nettet nede, eller udløber tokenet, bliver opgaven 
bare stående på listen og prøves igen ved næste ændring. Intet bliver væk på vejen.
Det er det samme princip som i en god integration på arbejdet: Bekræft modtagelsen,
før du sletter kilden.

Et par andre greb:

 * **Hvem oprettede den?** State-ændringens `context.user_id` slås op i `person`-
   entiteterne. Kommer opgaven fra stemmeassistenten, er der ingen bruger, og så
   står der “stemme/ukendt”. Teksten “Oprettet af …” havner nederst i issuet.
 * **`mode: queued`.** Fjerner automationen et punkt, ændrer listen sig, og automationen
   trigges igen. Med `queued` kører de efter hinanden i stedet for at falde over
   hinanden. Den anden kørsel finder bare en tom liste.
 * **Eventet `husopgaver_opdater`** får sensoren til at hente listen med det samme,
   så det nye issue står på dashboardet med det samme og ikke først om fem minutter.

## Sensoren: issues tilbage på dashboardet

    ```nord
    template:
      - triggers:
          - trigger: homeassistant
            event: start
          - trigger: time_pattern
            minutes: "/5"
          - trigger: event
            event_type: husopgaver_opdater
        actions:
          - action: rest_command.github_list_issues
            response_variable: svar
            continue_on_error: true
        sensor:
          - name: Husopgaver
            unique_id: husopgaver_github_open_issues
            icon: mdi:hammer-wrench
            state: >
              {% if svar is defined and svar.status == 200 %}
                {{ svar.content | rejectattr('pull_request', 'defined') | list | count }}
              {% else %}
                {{ this.state }}
              {% endif %}
            attributes:
              issues: >
                {% if svar is defined and svar.status == 200 %}
                  {% set ns = namespace(l=[]) %}
                  {% for i in svar.content if i.pull_request is not defined %}
                    {% set ns.l = ns.l + [{'number': i.number, 'title': i.title,
                                           'url': i.html_url, 'created': i.created_at,
                                           'labels': i.labels | map(attribute='name') | list}] %}
                  {% endfor %}
                  {{ ns.l }}
                {% else %}
                  {{ this.attributes.issues | default([]) }}
                {% endif %}
    ```

 * **Pull requests filtreres fra.** GitHubs issues-endpoint returnerer også pull
   requests. Dem kan man kende på feltet `pull_request`.
 * **`this.state` som fallback.** Fejler kaldet, beholder sensoren sin sidste værdi
   i stedet for at hoppe til 0 eller “unavailable”. Et kort GitHub-udfald får altså
   ikke det til at se ud, som om alle opgaver er løst.
 * Hos mig henter sensoren også de 10 senest lukkede issues til en liste over klarede
   opgaver, for det er rart at se, hvad der faktisk bliver lavet.

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

## Formularen på vægtabletten

Ikke alle vil tale til huset. Derfor er der en lille popup på tabletten med to `
input_text`-felter (titel og beskrivelse) og en knap, der kører et script. Scriptet
lægger opgaven på `todo.husopgaver` og tømmer felterne. Resten klarer automationen.
Det er en pointe i sig selv: Uanset om opgaven kommer fra stemmen, appen eller formularen,
går den _samme_ vej ind. Der er kun én integration at vedligeholde.

## Hvad har jeg lært?

 * **Ét sted for sandheden.** Det er fristende at synkronisere begge veje mellem
   to-do-listen og GitHub, men så skal man håndtere konflikter, sletninger og dubletter.
   Som ren indbakke er listen langt simplere, og der er aldrig tvivl om, hvor opgaven
   bor.
 * **Templates genindlæses ikke altid.** Efter en ændring i template-sensoren skete
   der ingenting, før jeg fyrede eventet manuelt. Tjek altid sensoren bagefter, 
   og genstart HA, hvis den ikke ændrer sig.
 * **Labels er gratis.** Sæt labels på issues på GitHub, så kommer de automatisk
   med i sensorens attributter og kan bruges til at filtrere eller farve listen 
   på dashboardet. Det kræver ingen ekstra kode i Home Assistant.

Vil du se resten af huset? [Her er oversigten over mit Home Assistant-setup](https://www.vogh.dk/mit-home-assistant-setup-et-hus-der-naesten-passer-sig-selv/).
