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¶
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/generatewith{"model": name, "keep_alive": 0}unloads that model; the answer saysdone_reason: "unload". The loaded models are the ones/api/pslists, whichtab5_llm.yamlalready reads. - LM Studio (0.4 or later):
POST /api/v1/models/unloadwith{"instance_id": id}; the ids are theloaded_instances[].idofGET /api/v1/models, already read by the package. - llama.cpp:
/models/unloadexists only in router mode (several models), which the package does not support (serveur-ia.md); a single-modelllama-servercannot unload its model without stopping. - Waking a PC: Home Assistant's Wake on LAN integration (
wake_on_lan.send_magic_packet, fieldmac), loaded by awake_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 codesdecharger,reveiller,redemarrerjoined 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 isokand 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 newinput_text.tab5_serveur_ia_mac« Tab5 · adresse MAC du serveur IA · AI server MAC address » (empty by default;AA:BB:CC:DD:EE:FFor with-, checked by the sensor — nopattern:on theinput_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 (okorchargement).redemarrer: a script or a button picked in a new listselect.tab5_serveur_ia_redemarrage« Tab5 · serveur IA, redémarrage · AI server, restart » (memoryinput_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 everyscript.*andbutton.*except the project's scripts (script.tab5_*) and the tablet's own buttons (device modeltab5-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.yamlappends 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 (actionsshown,actives), ignoring unknown codes, empty items, case or space variants and duplicates (tools/test_parse.cpp, fuzzed with an invariantactives ⊆ actions). - Buttons in the band, right to left, as wide as their longest text.
serveur_ia_action.yaml(one!includeper button,n0 to 2) is an icon (ejectF01EA,powerF0425,restartF0709 inmdi_font_32) and a word (« Décharger », « Réveiller », « Redémarrer »). The model and the name are cut to the room left. Colours: activeACCENT/TEXT_SOFT, greyedINACTIVE, waiting for confirmationWARNINGwith a 2 pxWARNINGborder, sentSUCCESS(the other buttons greyed meanwhile). Outside the confirmation the button keeps the theme's border ofstyle_clim_btn(local border removed, aschoix_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_actionwithaction∈ {decharger,reveiller,redemarrer} — a code from its own table, never a model name, an address or an entity.tab5_evenements.yamlchecks the code and startsscript.tab5_serveur_ia_actionwith it; nothing else. - The script re-checks everything.
script.tab5_serveur_ia_action(modesingle,max_exceeded: silent) runs only a code thatsensor.tab5_serveur_ia_actionsoffers 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: onerest_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_onorbutton.presson the picked entity. Every call hascontinue_on_error(a server that is off is not an error of the script; the trace keeps it). Then 1 s, an eventtab5_serveur_ia_releverthat 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 awake_on_lan:already inconfiguration.yamlis 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.logwould 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, thenpackages/tab5_llm.yaml,tab5_serveur_ia.yamlandtab5_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, onerest_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'skeep_alive: 0follow 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.