Skip to content

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.yaml has ten single-choice lists « Tab5 · serveur IA, … · AI server, … » (select.tab5_ia_*, memories input_text.tab5_choix_ia_*, « Aucun » by default), each offering only what fits: state (any binary_sensor or 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 its payload attribute: 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 an en_attente attribute of the running-requests sensor (lot 0's shape).
  • Push on change, two seconds apart at least. An automation in mode single triggered by the payload attribute pushes, waits two seconds, and pushes again while the sensor changed during the wait (last_updated less 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_origine guard); the push script starts with tab5_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 in tools/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::add line (ADR-0013, 27 of 32), Ecran::SERVEUR_IA (option « Serveur IA » at the end of « Aller à l'écran », gesture code serveur_ia at the end of kCodesGestes, 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 (dot SUCCESS online / INACTIVE offline / TEXT_DIM unknown — not BAR_INACTIVE, equal to INACTIVE in the dark palette, state, name, model cut with « … »), the tokens/s card on the left (large roboto_55_b number, 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 level TEXT_DIM with 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.yaml and 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 no ui_* and no other lv_* 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 with tab5_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) and expansion-card (F08AE) in mdi_font_32; « — » added to roboto_55_b.
  • Demo: one server per scene (tab5_maj_serveur_ia, 16 services). Render: three captures (serveur-ia with 24 pushes, serveur-ia-hors-ligne, serveur-ia-vide); the navigation wheel's Agenda family now shows « Serveur IA » after the scenes.