Een Home Assistant-dashboard dat zijn eigen welkomsttekst schrijft

Ons Home Assistant-dashboard had negen tabbladen: één per kamer, plus een paar voor energie, het weer en een kaart. Niemand kwam ooit verder dan het eerste. Ik wilde één pagina die laat zien wat er in huis speelt zodra je hem opent, met een snelle manier om naar een kamer te springen als je een lichtknop nodig hebt.

De bovenkant van een Home Assistant-dashboard op een telefoon: badges voor stroomverbruik, buitentemperatuur, afvalophaling en de wasmachine, een korte welkomsttekst en een tabel met afspraken en pakketten.
De bovenkant van de pagina op een telefoon. Eerst de badges, dan een korte tekst van een lokaal model, dan een tabel met wat eraan komt, die zonder dat model wordt gemaakt. De afspraken, pakketten en de keer dat er werd aangebeld zijn in deze screenshot verzonnen. Thuis is het dashboard Nederlands; voor deze post is het naar het Engels vertaald.

Ik heb dit in een middag en een avond gebouwd, samen met Claude. Claude schreef de YAML, een kleine custom card en een testopstelling; ik probeerde elke versie op mijn telefoon en zei wat me stoorde. Een flink deel van wat volgt gaat over de dingen die de eerste keer niet werkten.

Eén pagina met een menu onderin

De pagina gebruikt de sections-weergave van Home Assistant, met elke kamer als eigen sectie. Het eerste idee was een rij kamerknoppen die bovenaan het scherm bleef staan terwijl je scrolde. Op mijn telefoon belandde die rij steeds over de badges heen. We hebben drie versies geprobeerd, en elke versie zag er in de debug-output goed uit en op de telefoon fout, omdat we code aanpasten zonder de pagina ooit te zien.

Dat veranderde toen er een manier was om te kijken. Een headless Chromium in een van mijn containers logt nu in op Home Assistant, opent het dashboard op de breedte van een iPhone, een iPad en een desktop, klikt waar je het zegt en bewaart screenshots en metingen. Daarna waren de meeste fixes al gecontroleerd voordat ik ze zag.

De zwevende rij is geschrapt. In de plaats kwam een kaart in de footer van de weergave, die Home Assistant zelf al onderaan het scherm houdt. Het is één pil met links een home-knop en rechts de naam van de sectie waar je naar kijkt. Tik daarop en er opent een lijst met alle secties. Secties die voor een bepaalde gebruiker verborgen zijn, staan ook niet in diens lijst.

Het sectiemenu geopend over het dashboard, met een knop voor elke kamer en sectie.
De lijst met secties, opgebouwd uit de koppen op de pagina. Voor deze post vanuit het Nederlands naar het Engels vertaald, en twee kamernamen zijn veranderd.

Badges die alleen verschijnen als het ertoe doet

Stroomverbruik en de buitentemperatuur staan er altijd. De rest verschijnt als er een reden voor is: de voordeur als het slot niet op slot is, afvalophaling vandaag of morgen, pakketten die vandaag komen, en de wasmachine of vaatwasser terwijl er een programma loopt (resterende tijd) of als hij klaar is (groen).

Zichtbaarheidsregels voor badges kunnen een status of een getal vergelijken, maar niet naar attributen kijken. Het aantal pakketten dat vandaag komt bestaat alleen als attribuut van de pakketintegratie, dus dat kreeg een eigen kleine template-sensor. Afvalophaling kreeg er ook een, die vandaag en morgen samenvoegt tot één waarde. De stroombadge wordt oranje boven 2000 W. Een badge heeft één vaste kleur, dus dat zijn twee badges met tegengestelde voorwaarden.

Een welkomsttekst van een model in huis

Onder de begroeting staat een korte tekst over de dag. Een script verzamelt de gezinsagenda, de weersverwachting, de gedeelde takenlijst, de afvalkalender en alles in huis wat aandacht nodig heeft: een waterlek, een rookmelder, hoge CO2, een balkondeur die openstaat, een batterij die bijna leeg is, een wasmachine die klaar is. Het zoekt ook naar keren dat er werd aangebeld zonder dat iemand opendeed. Dat gaat allemaal naar Qwen3.6-35B-A3B op llama.cpp, dat draait op de Arc Pro B70 waar ik eerder over schreef. De tekst wordt zes keer per dag ververst, en ook als de agenda of een pakket verandert of als iemand thuiskomt, hooguit eens per tien minuten. Niets ervan verlaat het huis.

Home Assistant had al een conversation agent die via LiteLLM naar hetzelfde model wees, dus dat probeerde ik als eerste. Op die route staat thinking aan. Voor deze klus dacht het model 43 seconden na, liep na ongeveer 10.000 tekens denkwerk tegen de tokenlimiet aan en gaf helemaal geen tekst terug. /no_think in de prompt zetten maakte geen verschil. Als je llama.cpp rechtstreeks aanroept met thinking uitgeschakeld in de argumenten voor de chat template, krijg je binnen een tot tweeënhalve seconde antwoord, en een rest_command in Home Assistant kan precies dat verzoek sturen:

rest_command:
  welcome_model:
    url: http://<model-server>:8000/v1/chat/completions
    method: POST
    content_type: application/json
    timeout: 60
    payload: >-
      {{ {"model": "hermes-local", "max_tokens": 700, "temperature": 0.3,
          "chat_template_kwargs": {"enable_thinking": false},
          "messages": [{"role": "system", "content": system},
                       {"role": "user", "content": "Data:\n" ~ data}]} | to_json }}

Zorgen dat het minder zegt

Zonder denkstap neemt het model de data heel letterlijk, ook de delen die je niet als inhoud bedoelde. Bijna elke fix belandde in de template die de data klaarzet, en maar heel weinig in de prompt.

In de eerste versie gingen de afspraken erin als een kale lijst met tijden. Een afspraak van die ochtend werd beschreven als gisteren. Toen de template ze indeelde in groepen als “al geweest”, “later vandaag” en “morgen”, hield dat op.

Lege secties waren erger. Een regel die zei dat er niets bijzonders in huis was, werd een openingszin die zei dat er niets bijzonders in huis was, elke keer weer. Een vaste genummerde volgorde van onderwerpen zorgde ervoor dat het een zin schreef over een onderwerp dat ontbrak. Nu staat een sectie zonder inhoud gewoon niet in de data, en zegt de prompt dat het de onderwerpen moet behandelen in de volgorde waarin ze staan.

Op een rustige dag met alleen het weer om te melden begon het ruimte te vullen. Het vertelde ons wie er in huis woont en heette ons welkom op het dashboard. Een regel daartegen in de prompt hielp voor het grootste deel, en een reguliere expressie haalt nu de paar zinspatronen weg die er nog doorheen glippen.

Het zei ook dat er drie afspraken waren terwijl de lijst er vier had. Zeggen dat het geen aantallen moest noemen deed niets; het bleef drie zeggen. Het juiste aantal in de data zetten loste het op. En een pakketnaam die met een beletselteken was ingekort, kwam terug in de tekst met het beletselteken er nog in, dus namen worden nu op een logisch punt afgekapt, zonder.

Later die avond vroeg ik om een kortere tekst zonder de agenda en de pakketten. Die staan nu in een tabel onder de tekst, gemaakt door de template, en het model schrijft twee tot vier zinnen over het huis, gemist aanbellen, de containers en het weer.

Aanbellen waar niemand op reageerde

Als de deurbelcamera een bezoeker ziet, wacht een automatisering tot tien minuten op een melding van het slimme slot op de voordeur dat het open is gegaan. Gebeurt dat niet, dan noteert het de tijd, en vermeldt de volgende tekst dat er iemand aanbelde en de deur dicht bleef.

Hier zit een gat in. Een andere automatisering ontgrendelt de deur al als er iemand aanbelt terwijl we thuis zijn. Trek je de deur dan met de hand open, dan meldt het slot nooit dat het open is gegaan, en telt het aanbellen als gemist. Een contactsensor op de deur zou dat gat dichten. Die heb ik nog niet.

Eén tabel zonder randen

Afspraken en pakketten staan in een tabel onder de tekst. Markdown-tabellen maken hun kolommen zo breed als de inhoud, dus twee onder elkaar lijnen nooit uit, en de sanitizer van Home Assistant laat bij HTML-tabellen wel een width-attribuut door, maar geen style. De eerste versie gebruikte een kolomgroep in pixels. Daarmee lijnden de tabellen uit, met een rand om elke cel.

De markdown-kaart blijkt role="presentation" op een tabel te herkennen en laat dan de celranden en padding weg. Daarna probeerde ik een lijn onder elke kop. Een horizontale lijn brengt 16 pixels ruimte boven en onder mee, en een dunne rand op een lege tabel in een cel tekent een dubbele lijn, dus de lijnen gingen eruit. Wat overblijft is één tabel met kleine koppen: de afspraken, een lege rij, dan de pakketten. In de linkerkolom staat de dag vetgedrukt, gevolgd door de tijd of de status van het pakket; in de rechterkolom een kalender- of pakketemoji en de naam. De naam van een pakket linkt naar de trackingpagina van de vervoerder, opgebouwd uit de vervoerderscode en het trackingnummer die Parcel doorgeeft; de integratie zelf geeft geen link. Een afspraak met een locatie in de agenda linkt naar een Google Maps-zoekopdracht voor die plek. Niets wat verder dan drie dagen vooruit ligt wordt getoond, een bezorgd pakket blijft staan tot het einde van de dag waarop het binnenkwam, pakketten die de vervoerder niet kan vinden worden weggelaten, en namen worden ingekort zodat een rij op een telefoon op één regel blijft.

De welkomsttekst in de screenshots is door hetzelfde model geschreven op basis van verzonnen data: een keer aanbellen, morgen oud papier en een wasmachine die nog draait. Thuis schrijft het in het Nederlands; voor deze screenshots kreeg het dezelfde instructies in het Engels.

← Alle berichten