Aller au contenu

ADR-0060: Three actions in the « AI server » popup — unload the model, wake the PC, restart the service — offered by Home Assistant in a 14th field, asked as one esphome.tab5_serveur_ia_action event, done by a guarded script

Page en anglais

Cette page n'existe qu'en anglais.

Status: Accepted (2026-10-10, lot 3 of the local-LLM audit; not tried on a tablet nor against a real server when written). Date: 2026-10-10

Context

ADR-0059 put a dashboard of the home's LLM server on the tablet and kept room in its band for lot 3: buttons that act on the server. The audit asks for three: unload the loaded model (free the VRAM), wake the PC that runs the server (Wake-on-LAN), restart the AI service.

Constraints: events-only (ADR-0025) — the firmware never calls a Home Assistant action, it emits an esphome.tab5_* event that packages/tab5_evenements.yaml maps to a whitelist; no entity ID, address or command in the firmware or in the public HA files (ADR-0024); no free text from the tablet; a button that cannot do anything must not look as if it could.

What the servers offer (official documentation, read on 2026-10-10):

  • Ollama: POST /api/generate with {"model": name, "keep_alive": 0} unloads that model; the answer says done_reason: "unload". The loaded models are the ones /api/ps lists, which tab5_llm.yaml already reads.
  • LM Studio (0.4 or later): POST /api/v1/models/unload with {"instance_id": id}; the ids are the loaded_instances[].id of GET /api/v1/models, already read by the package.
  • llama.cpp: /models/unload exists only in router mode (several models), which the package does not support (serveur-ia.md); a single-model llama-server cannot unload its model without stopping.
  • Waking a PC: Home Assistant's Wake on LAN integration (wake_on_lan.send_magic_packet, field mac), loaded by a wake_on_lan: key.
  • Restarting a service: no server has an endpoint for it. It belongs to whatever runs the server (a systemd unit over SSH, a Docker container, an add-on, a smart plug), which Home Assistant reaches by a script or a button the user already has.

Decision

  • Home Assistant decides what is offered, in one place. A template sensor sensor.tab5_serveur_ia_actions (packages/tab5_llm.yaml) lists the codes decharger, reveiller, redemarrer joined by « , »: code = button active, -code = button shown greyed (set up, pointless right now), code absent = no button, « aucune » when none. Nothing with the type « Aucun ».
    • decharger: Ollama or LM Studio with an address; active when the last reading is ok and a model (Ollama) or an instance (LM Studio) is loaded. llama.cpp: never (see Context — the documentation says so).
    • reveiller: a valid MAC address in a new input_text.tab5_serveur_ia_mac « Tab5 · adresse MAC du serveur IA · AI server MAC address » (empty by default; AA:BB:CC:DD:EE:FF or with -, checked by the sensor — no pattern: on the input_text: Home Assistant applies it to the « unknown » state of a new one, refuses the entity and stops the YAML reload, found by the « Intégration HACS » CI job); greyed while the server answers (ok or chargement).
    • redemarrer: a script or a button picked in a new list select.tab5_serveur_ia_redemarrage « Tab5 · serveur IA, redémarrage · AI server, restart » (memory input_text.tab5_choix_ia_redemarrage, « Aucun » by default, same trigger-based pattern and registry filter as the other lists), that still exists. The list offers every script.* and button.* except the project's scripts (script.tab5_*) and the tablet's own buttons (device model tab5-ha-hmi). Always active: whether a restart makes sense is the user's call.
  • A 14th field of tab5_maj_serveur_ia, not a new service. packages/tab5_serveur_ia.yaml appends the sensor's state, filtered to the three codes, to its record. Why a field: the popup already receives a record each time the server changes, the buttons depend on the same reading, and the firmware's reader ignores fields past its maximum (champs_decouper). Both mismatches work: an older firmware drops the 14th field (no buttons), an older package sends 13 fields (no buttons). A new service would have needed a second push automation and a second contract entry for three bits.
  • The tablet reads codes, never text. serveur_ia_lire() turns the field into two bit masks (actions shown, actives), ignoring unknown codes, empty items, case or space variants and duplicates (tools/test_parse.cpp, fuzzed with an invariant actives ⊆ actions).
  • Buttons in the band, right to left, as wide as their longest text. serveur_ia_action.yaml (one !include per button, n 0 to 2) is an icon (eject F01EA, power F0425, restart F0709 in mdi_font_32) and a word (« Décharger », « Réveiller », « Redémarrer »). The model and the name are cut to the room left. Colours: active ACCENT / TEXT_SOFT, greyed INACTIVE, waiting for confirmation WARNING with a 2 px WARNING border, sent SUCCESS (the other buttons greyed meanwhile). Outside the confirmation the button keeps the theme's border of style_clim_btn (local border removed, as choix_peindre() does). With no name, the model gets the whole room left; with a name, half of it.
  • A second tap for the two that interrupt. Unload and restart cut an answer in progress: the first tap turns the button into « Confirmer ? » for 4 s (one-shot LVGL timer), the second sends. Wake sends at once: it cannot harm a running server (and is greyed when the server answers). Any received record that no longer offers the action as active cancels the confirmation.
  • One event, one field. The tablet emits esphome.tab5_serveur_ia_action with action ∈ {decharger, reveiller, redemarrer} — a code from its own table, never a model name, an address or an entity. tab5_evenements.yaml checks the code and starts script.tab5_serveur_ia_action with it; nothing else.
  • The script re-checks everything. script.tab5_serveur_ia_action (mode single, max_exceeded: silent) runs only a code that sensor.tab5_serveur_ia_actions offers as active at that moment, and takes the model names, instance ids, address, MAC and target from Home Assistant's own state, never from the request. Unload: one rest_command.tab5_serveur_ia_envoyer (POST JSON, 10 s timeout, the body built by Home Assistant from a dict) per loaded model or instance, eight at most; wake: wake_on_lan.send_magic_packet; restart: script.turn_on or button.press on the picked entity. Every call has continue_on_error (a server that is off is not an error of the script; the trace keeps it). Then 1 s, an event tab5_serveur_ia_relever that triggers a reading at once (the unloaded model leaves the screen without waiting 30 s), and 4 s more: one action every 5 s at most, the requests during the wait dropped silently. The tablet mirrors it: after a send, « Envoyé » for 5 s and no other send.
  • wake_on_lan: in the package. An empty key loads the integration. Home Assistant merges packages into the configuration; an empty package value adds nothing, so a wake_on_lan: already in configuration.yaml is no duplicate. The « Tab5 » integration loads it at runtime like the package's other domains.
  • No logbook entry. The script's trace says what was asked and what each call answered; a logbook.log would add a dependency for nothing more.

Consequences

  • Contract stays 1.2.0 (already a minor step since v3.8.0-rc.4): one event added (esphome.tab5_serveur_ia_action: [action]), one field appended to an existing variable's format. Update order: firmware first, then packages/tab5_llm.yaml, tab5_serveur_ia.yaml and tab5_evenements.yaml. Older firmware with the new packages: 14 fields, the last one ignored, no buttons. New firmware with the older packages: no 14th field, no buttons, nothing sent.
  • Home Assistant side: one sensor, one list, two input_text, one rest_command, one script, one event branch, the Wake on LAN integration. Recorder: the actions sensor changes only when a model loads or unloads, the server goes on or off, or a setting changes.
  • Firmware: 3 buttons (9 widgets), two one-shot timers alive at most 4 and 5 s, 4 new screen texts in six languages (« Confirmer ? » existed), two MDI glyphs in mdi_font_32. Flash and RAM: see the PR (measured by the compile, not on the device).
  • Not verified when written: no action was sent to a real server (the author's Ollama was read only), no magic packet was sent, the popup was seen only in the off-device render (serveur-ia-actions, serveur-ia-confirmer). LM Studio's unload and Ollama's keep_alive: 0 follow their documentation.
  • Another action = one code in kServeurIaActionCodes (tab5_parse.h) and in the actions sensor, one branch of the script, one button include, one icon, one word.