diff --git a/dashboards/llm_monitor.yaml b/dashboards/llm_monitor.yaml new file mode 100755 index 0000000..c4e41ce --- /dev/null +++ b/dashboards/llm_monitor.yaml @@ -0,0 +1,90 @@ +# Surveillance du modele local sur la Spark. +# +# Tableau volontairement SEPARE des tableaux energie : ce qu'il montre n'a aucun +# rapport avec le photovoltaique, et les melanger rendrait les deux moins +# lisibles. +# +# Rempli par un moniteur externe (cron toutes les 2 min sur 10.10.10.17). Tant +# qu'il n'a pas tourne, la carte le DIT au lieu d'afficher des zeros rassurants. + +title: Modèle local +views: + - title: Spark + path: spark + icon: mdi:robot-outline + cards: + + - type: conditional + conditions: + - entity: binary_sensor.llm_monitor_muet + state: "on" + card: + type: markdown + content: > + ## ⚠️ Le moniteur ne parle plus + + + Aucune nouvelle depuis + {{ states('sensor.llm_monitor_age_du_dernier_passage') }} minutes, + alors qu'il devrait passer toutes les 2 minutes. + + + **Les valeurs ci-dessous sont donc périmées** — elles montrent le + dernier état connu, pas l'état actuel. Un moniteur mort affiche les + mêmes chiffres rassurants qu'un système sain : c'est cette carte, + et elle seule, qui fait la différence. + + - type: entities + title: Le modèle + state_color: true + entities: + - entity: input_text.llm_monitor_status + name: État + - entity: input_text.llm_monitor_model + name: Modèle chargé + - entity: input_datetime.llm_monitor_last_seen + name: Dernière vérification + - entity: sensor.llm_monitor_age_du_dernier_passage + name: Âge de cette vérification + - type: divider + - entity: binary_sensor.llm_monitor_muet + name: Moniteur muet + + - type: entities + title: Mémoire de la Spark + state_color: true + entities: + - entity: input_number.llm_mem_avail_pct + name: RAM disponible + - entity: binary_sensor.spark_ram_critique + name: RAM critique + footer: + type: graph + entity: input_number.llm_mem_avail_pct + hours_to_show: 24 + detail: 2 + + - type: entities + title: Alertes + entities: + - entity: input_boolean.llm_monitor_enabled + name: Alertes WhatsApp + + - type: markdown + content: > + **Comment lire cette carte** + + + `RAM critique` ne se déclenche que si le moniteur a donné de ses + nouvelles récemment. Une valeur jamais reçue vaut zéro, et zéro n'est + pas une mesure basse — c'est une absence de mesure. C'est + `Moniteur muet` qui le dit, et lui seul. + + + Le seuil de RAM est à 8 %. En dessous, `earlyoom` tue le processus le + plus gourmand de la machine, et sur la Spark le plus gourmand est le + modèle lui-même. + + + L'interrupteur ne coupe que les **alertes WhatsApp**. Le moniteur + continue de mesurer et cette carte reste à jour. diff --git a/ha-configuration.yaml b/ha-configuration.yaml index 91e0130..7be414f 100755 --- a/ha-configuration.yaml +++ b/ha-configuration.yaml @@ -288,6 +288,13 @@ sensor: lovelace: dashboards: + modele-local: + mode: yaml + title: Modèle local + icon: mdi:robot-outline + show_in_sidebar: true + filename: dashboards/llm_monitor.yaml + bilan-energetique: mode: yaml title: Bilan énergétique diff --git a/packages/llm_monitor.yaml b/packages/llm_monitor.yaml new file mode 100755 index 0000000..b67391b --- /dev/null +++ b/packages/llm_monitor.yaml @@ -0,0 +1,108 @@ +# Surveillance du modele local sur la Spark (10.10.10.8) +# +# Ce paquet N'A RIEN A VOIR avec le systeme photovoltaique. Il vit ici parce que +# c'est le seul endroit ou des entites Home Assistant peuvent exister, pas parce +# qu'il appartient au domaine energie. Ne pas melanger ses entites a celles de +# `evcs_*` / `victron_*`, et ne rien cabler de l'energie dessus. +# +# Ce que surveille le moniteur (script externe, cron sur 10.10.10.17) : +# - le point d'acces du modele repond-il +# - la RAM disponible de la Spark, qu'earlyoom peut faire tomber en tuant +# le modele autour de 6 % +# +# ⚠️ AUCUN `initial:` DANS CE FICHIER. Declarer `initial:` DESACTIVE la +# restauration d'etat : le helper est force a cette valeur a CHAQUE demarrage de +# Home Assistant. Le 06/09/2026 cinq reglages manuels du projet se +# reinitialisaient ainsi en silence, dont deux toggles de cablage. Le +# kill-switch ci-dessous doit garder la position choisie par le proprietaire. + +input_boolean: + llm_monitor_enabled: + name: "LLM monitor — alertes WhatsApp" + icon: mdi:bell-ring + +input_number: + llm_mem_avail_pct: + name: "Spark — RAM disponible" + min: 0 + max: 100 + step: 0.1 + unit_of_measurement: "%" + mode: box + icon: mdi:memory + +input_text: + # ⚠️ `max` DOIT couvrir le plus long MESSAGE D'ERREUR, pas la valeur nominale. + # Le 08/09/2026, `evcs_balance_forecast_updated` etait declare `max: 32` : ses + # messages d'echec faisaient 43 caracteres, Home Assistant les REJETAIT EN + # SILENCE, et une panne de 4 h 45 n'a laisse aucune trace. 255 est la valeur + # sure pour un champ d'etat. + llm_monitor_status: + name: "LLM monitor — état" + max: 255 + icon: mdi:robot-outline + + llm_monitor_model: + name: "LLM monitor — modèle chargé" + max: 100 + icon: mdi:chip + +input_datetime: + # ⚠️ LE CAPTEUR LE PLUS IMPORTANT DU PAQUET, et celui qui manquait au + # brouillon. Sans horodatage ecrit A CHAQUE PASSAGE, un moniteur mort est + # indiscernable d'un systeme sain : les trois autres entites gardent leur + # derniere valeur, qui dit « tout va bien », indefiniment. + # + # Et il ne faut PAS deduire la fraicheur du `last_updated` des autres + # entites : Home Assistant NE RAFRAICHIT PAS l'horodatage d'un helper reecrit + # a l'identique. Constate le 01/09/2026 — un etat paraissait fige depuis + # 2 440 s alors que le controleur tournait normalement. Une RAM stable a + # 12,3 % pendant une heure produirait exactement la meme illusion. + llm_monitor_last_seen: + name: "LLM monitor — dernière vérification" + has_date: true + has_time: true + +template: + - sensor: + - name: "LLM monitor — âge du dernier passage" + unique_id: llm_monitor_age_min + unit_of_measurement: "min" + state: > + {% set t = states('input_datetime.llm_monitor_last_seen') %} + {% if t not in ['unknown','unavailable',''] %} + {{ ((as_timestamp(now()) - as_timestamp(t, 0)) / 60) | round(0) }} + {% else %}unknown{% endif %} + + - binary_sensor: + # Le moniteur tourne toutes les 2 minutes. Au-dela de 10, il ne tourne + # plus — et c'est une information a part entiere, pas une absence. + - name: "LLM monitor — muet" + unique_id: llm_monitor_muet + device_class: problem + state: > + {% set t = states('input_datetime.llm_monitor_last_seen') %} + {% if t in ['unknown','unavailable',''] %}on + {% else %} + {{ (as_timestamp(now()) - as_timestamp(t, 0)) > 600 }} + {% endif %} + + # earlyoom tue le processus le plus gourmand quand la memoire s'effondre, + # et sur cette machine le plus gourmand est le modele. Le seuil est celui + # que le proprietaire a nomme. + # + # ⚠️ CONDITIONNE A LA FRAICHEUR DU MONITEUR. Une valeur jamais ecrite vaut + # 0 — le minimum du helper — et serait lue comme « 0 % de RAM libre », + # donc comme une alarme permanente avant meme que le moniteur ait tourne + # une fois. Une mesure qu'on n'a pas recue n'est pas une mesure basse : + # c'est `llm_monitor_muet` qui doit le dire, et lui seul. + - name: "Spark — RAM critique" + unique_id: spark_ram_critique + device_class: problem + state: > + {% set t = states('input_datetime.llm_monitor_last_seen') %} + {% if t in ['unknown','unavailable',''] %}off + {% elif (as_timestamp(now()) - as_timestamp(t, 0)) > 600 %}off + {% else %} + {{ states('input_number.llm_mem_avail_pct') | float(100) < 8 }} + {% endif %}