ADR-0059: An « AI server » popup — one local LLM server, sensors picked in ten « Tab5 · … » lists, pushed by tab5_maj_serveur_ia on change (two seconds apart at least), the tokens-per-second curve kept by the tablet¶
Status: Accepted (2026-10-10, lot 2 of the local-LLM audit; not tried on a tablet when written). Extended by ADR-0060: a 14th field, the action buttons. Date: 2026-10-10
Context¶
The local-LLM audit of 2026-10-10 (audits 2 and 4) asks for a dashboard of the home's LLM server on the tablet: online or not, the loaded model, tokens per second, requests running and queued, VRAM, GPU temperature, RAM, power. Lot 0 (packages/tab5_llm.yaml, another PR) creates sensors for Ollama, llama.cpp and LM Studio; the machine's own numbers (GPU temperature, RAM, power) come from other integrations (Glances, System Monitor, a metered plug). Lot 3 will add action buttons.
Constraints: push-only and events-only (ADR-0001, ADR-0025); no entity ID in the firmware or in the public HA files (ADR-0024); colours from the palette (ADR-0029); lv_chart is not compiled; instant transitions.
Decision¶
- One server. The payload is one record of thirteen
|fields —name|state|model|tps|running|queued|vram|vram_total|vram_pct|temp|level|ram|power— and the format allows more records after a;, which this firmware ignores. Why one: ten lists per server would make the settings unreadable, the author has one GPU machine, and the page brick (ADR-0046) is there if a second server ever comes (one page each, no new action). - Sensors picked, never guessed.
packages/tab5_serveur_ia.yamlhas ten single-choice lists « Tab5 · serveur IA, … · AI server, … » (select.tab5_ia_*, memoriesinput_text.tab5_choix_ia_*, « Aucun » by default), each offering only what fits: state (anybinary_sensoror text sensor), model (text sensor), tokens/s and requests (state_class: measurement), VRAM (a data unit, or % for the used one), GPU temperature (device_class: temperature), RAM (%), power (device_class: power). The package talks to no server: the sensors can come from anywhere. - Home Assistant assembles and decides. A template sensor
sensor.tab5_poussee_serveur_ia(state = roles chosen) holds the payload in itspayloadattribute: units converted (data units to GiB written « Go », °F to °C, kW to W), rounded (tokens/s to 0.1, GiB to 0.1, °C, %, W and requests to integers) so that measurement noise does not push, the VRAM percentage computed from used / total. The GPU temperature level (0 normal, 1 high, 2 critical) uses two thresholds in one place, 80 and 90 °C — cautious values, not verified per GPU. Name = the device of the first chosen sensor; the queue falls back on anen_attenteattribute of the running-requests sensor (lot 0's shape). - Push on change, two seconds apart at least. An automation in mode
singletriggered by thepayloadattribute pushes, waits two seconds, and pushes again while the sensor changed during the wait (last_updatedless than two seconds old). A second one pushes at once when a list changes, when the tablet connects, on « MAJ Écran » and at HA start (tab5_origineguard); the push script starts withtab5_connectee. - The tablet keeps the curve.
ServeurIaCourbe(Tab5/socle/tab5_parse.h, section 14, tested on a PC) keeps the tokens/s of the last 24 pushes; an unknown value is a gap. A curve from Home Assistant would need either the recorder's statistics (5-minute steps: too coarse for a server that answers in seconds) or a trigger-based sensor sampling every few seconds (database writes all the time). The cost: the curve starts again after a restart of the tablet, and its time axis is « pushes », not seconds — about the last minute while the server works, longer when it idles (nothing changes, nothing is pushed). - Nothing repainted while the popup is closed.
serveur_ia_recu()reads the payload (serveur_ia_lire(), cases intools/test_parse.cpp, fuzzed), copies the name and model, adds the curve point, and paints only if the popup is open. Opening it paints the last state. No timer. - The popup. Full-screen modal with the shared chrome (ADR-0009), one
ModalRegistry::addline (ADR-0013, 27 of 32),Ecran::SERVEUR_IA(option « Serveur IA » at the end of « Aller à l'écran », gesture codeserveur_iaat the end ofkCodesGestes, index 28, offered by the blueprint), and in the navigation wheel (ADR-0042) at the end of Agenda (6 of 8), only once Home Assistant has pushed a server: the package is optional, a home without it never pushes anything. Layout: a band (dotSUCCESSonline /INACTIVEoffline /TEXT_DIMunknown — notBAR_INACTIVE, equal toINACTIVEin the dark palette, state, name, model cut with « … »), the tokens/s card on the left (largeroboto_55_bnumber,ui_courbe_lisse()curve over 24 places, requests below), four cards on the right (serveur_ia_carte.yaml): VRAM with a bar, GPU temperature coloured by level (SUCCESS/WARNING/ERROR, unknown temperature or unknown levelTEXT_DIMwith no status line: an empty or unreadable level is never shown as « normal »), RAM with a bar, power. Unknown values show « — » (serveur_ia_nombre_texte()), never a made-up 0. Lot 3's buttons go in the band, right of the model, which is cut to the room left.
Consequences¶
- Contract stays 1.2.0: one action added since the last release (v3.8.0-rc.4, contract 1.1.0), already a minor step. Update order: firmware first, then
packages/tab5_serveur_ia.yamland the blueprint (gesture option). New HA files with an older firmware: the push fails without stopping anything else (continue_on_error); new firmware without the package: the popup says « En attente de Home Assistant » and the wheel does not offer it. - Cost of the cadence — counted, not measured on the tablet. Traffic: one call of 67 to 72 bytes of payload in the demo (171 at most with a 40-character name and an 80-character model), at most one every two seconds: under 90 bytes/s of payload plus the API framing (framing not measured). Popup closed: no LVGL write at all (by construction:
serveur_ia_recu()only reads the popup's hidden flag, and calls noui_*and no otherlv_*unless the popup is shown). Popup open: one push rewrites the curve (lv_line_set_points()always invalidates the line, about 550 × 300 px) and the labels whose text changed (ui_text()compares first), so at most one partial frame every two seconds. Its duration was not measured; for scale, a changed value measured under 10 ms and opening a full-screen popup 126 to 197 ms on 3.2.0 (performance). Home Assistant side: one recorder row per change of the payload attribute — every 30 s withtab5_llm.yaml's polling — unless the sensor is excluded from the recorder. - 15 new screen texts in six languages (« VRAM », « GPU », « RAM », « tok/s » stay as they are); MDI glyphs
server(F048B: 26, 32, 36, 70),speedometer(F04C5),memory(F035B) andexpansion-card(F08AE) inmdi_font_32; « — » added toroboto_55_b. - Demo: one server per scene (
tab5_maj_serveur_ia, 16 services). Render: three captures (serveur-iawith 24 pushes,serveur-ia-hors-ligne,serveur-ia-vide); the navigation wheel's Agenda family now shows « Serveur IA » after the scenes.