Husopgaver som GitHub-issues – fra stemmekommando til ticket

“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:

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:

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

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

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.

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.