Files
OPENCLAW_ARCHIVE3_17/workspace/airtria-concierge-vehicule-SPEC.md
Thierry Tallagrand b834e3bf75 Concierge au branchement + moniteur LLM/RAM + docs
Concierge: declenche au BRANCHEMENT (transition on de evenement) au lieu de la session ;
helper porte 'branchement' (ts::text verbatim) ; fraicheur [on-10, min(now, on+30)].
Moniteur LLM/RAM (llm-monitor.js) : endpoint spark + RAM, alerte WhatsApp sur changement
d'etat, publie l'etat dans HA (last_seen a chaque passage) + interrupteur de pilotage.
Docs: SPEC a jour, MONITORING-LLM.md, brouillons de config HA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZsbhyGmRwZogdoBwT6C6y
2026-09-15 04:59:15 +02:00

22 KiB
Raw Blame History

Concierge véhicule Airtria — spécification (convenue OpenClaw ↔ EVCS, 2026-09-13)

🔁 MISE À JOUR 2026-09-15 : déclenchement AU BRANCHEMENT (plus à la session)

Le propriétaire veut la question dès le BRANCHEMENT (au branchement du soir, la borne ne tire rien avant les HC de 01:24 ; attendre une session laissait la 1re mal étiquetée). concierge-live.js détecte désormais sur les transitions 'on' de evenement (plus sur session_charge). Le helper porte branchement = ts::text VERBATIM de la transition 'on' (PAS horodatage_ha, antérieur de 3-51 s → EVCS raterait le branchement en cours), qui remplace sessions. Fraîcheur = sélecteur changé dans [on−10 min, min(now, on+30 min)] (EVCS). Règle de puissance abandonnée au branchement (pas de session/p_max) → on demande pour TOUT branchement périmé, invité géré par la réponse. Division : Airtria détecte le branchement + demande + publie ; EVCS résout, mémorise, applique aux sessions du câble au fur et à mesure (HC 01:24 comprises), refuse les contradictions. Validé contre l'historique (15 branchements, distribution de fraîcheur inchangée) ; déployé, test de portée vert.

🟢 LIVE EN PRODUCTION depuis 2026-09-14 02:22

Kill-switch airtria_concierge_enabled = ON, lecteur EVCS déployé (commit d8ef69b). Preuve end-to-end réelle : session 12/09 11:55 → JEEP, vehicule_sur=t, evenement origine=airtria/source_airtria=whatsapp ; cycle suivant idempotent (etiquette=0). Colonne : 6 confirmées (arbitrages voix du 13/09) / 13 à confirmer. Défaut vehicule_sur basculé à false, 17 lignes rattrapées. Bug de format clef (PG …+02 vs ISO …Z, même instant) attrapé au dry-run EVCS et corrigé côté service (canonisation ISO avant dédup) — côté Airtria rien à changer, on envoie la clef verbatim. Véhicule débranché 14/09 01:31 : prochaine question sur un branchement neuf. Le concierge tourne en régime réel.

VALIDÉ END-TO-END (2026-09-13, essai réel, kill-switch OFF)

Simulation sur le branchement 12/09 11:55 : détection → question WhatsApp → réponse « JEEP » du propriétaire → routage → helper publié {"sessions":["2026-09-12 11:55:54.493+02"],"label":"JEEP","confirmed_at":…,"source":"whatsapp"} → confirmation « ✅ Noté ». Base non touchée (kill-switch OFF). ⚠️ LEÇON ROUTAGE : le modèle local (qwen) IGNORE une consigne de réponse discrète (a répondu « Bonjour Thierry » au lieu de router). Il faut un RÉFLEXE PRIORITAIRE en tête du traitement WhatsApp (AGENTS.md §5.0) : vérifier concierge-pending.json et lancer answer AVANT toute autre réponse. Avec ça, le routage passe de façon fiable. Cron concierge-vehicule ACTIVÉ (enabled) → mode essai : une vraie charge à sélecteur périmé déclenchera une vraie question. Reste pour appliquer en base : kill-switch ON + lecteur EVCS (après validation de son dry-run).

État d'avancement (2026-09-13)

Fichiers dans le workspace : concierge-detection.js (détection + calibration), concierge-write.js (écriture helper, entité unique + relecture), concierge-write.test.js (test de portée, VERT), concierge-live.js (sous-commandes detect et answer). Dépendance pg installée dans le workspace. TESTÉ : calibration 2-3-2-1-1-1 / 5 questions / 0 mixte ; aller-retour écriture sur le vrai helper ; detect bootstrap ; chemin question (état en attente, clef debut verbatim) ; chemin réponse (reconnaissance + publication + confirmation). CÂBLÉ : cron concierge-vehicule (id d113b997-0942-4d71-9e9b-d64249949540, payload COMMAND node concierge-live.js detect, /10, announce whatsapp) — testé : exitCode 0, sortie vide → delivery suppressReason "empty" (aucun WhatsApp vide). Routage des réponses ajouté à AGENTS.md §5bis (sauvegarde AGENTS.md.bak-concierge-). pg installé dans le workspace. ACTIVATION PAR ÉTAPES (toutes décisions du propriétaire) : (1) openclaw cron enable d113b997-… → boucle Q/R en réel, le helper se publie sur une vraie réponse. La BASE n'est pas touchée. (2) EVCS déploie son lecteur en DRY-RUN (redémarre son service) : il journalise ce qu'il écrirait (jointure ±1 s sur les clefs verbatim, extension au groupe) SANS écrire. Valide la moitié EVCS avant que ça compte. (3) kill-switch input_boolean.airtria_concierge_enabled → ON + EVCS commit la bascule → les étiquettes corrigent session_charge. ⚠️ La bascule vehicule_sur n'est PAS neutralisée par le kill-switch : le kill-switch gate l'APPLICATION d'un label, pas le DEFAULT de la colonne. Passer DEFAULT false trop tôt afficherait 100 % de sessions « douteuses » (motif faux) sur le Grafana « Les véhicules ». Donc la bascule = TROIS pièces soudées au commit du ON : DEFAULT false + true-sur- confirmation-humaine + libellé Grafana « selecteur change » → honnête. Décision COMMAND vs agentTurn : command choisi car déterministe (pas de LLM, sortie exacte, suppression auto sur vide). Cron existants modèles : daily-email-report (Gmail 8h30), orange-cloture (17h40).

Objectif : combler le seul trou qu'aucun capteur ne comble — savoir QUEL véhicule (VW / JEEP / invité / bientôt moto ZERO) est branché sur la borne EVCS, pour que session_charge cesse d'attribuer l'énergie au mauvais véhicule.

Contexte déclencheur : le sélecteur input_select.victron_evcs_vehicle est resté figé sur « VW » du 07/09 au 13/09 → 11 sessions d'affilée mal étiquetées, sans aucun signal. VW et JEEP sont électriquement indiscernables l'une de l'autre (~3 680 vs ~3 655 W à 20 A).

Principe d'architecture (non négociable)

  • Le cœur déterministe EVCS reste seul juge. Airtria intervient APRÈS, jamais dans la boucle 30 s.
  • Une seule main sur chaque variable. Airtria ne touche AUCUN actionneur.
  • Toute valeur porte son propre horodatage (confirmed_at), pas celui du run.
  • Producteur qui s'arrête : invalidation STRUCTURELLE (le label est lié à une session précise, il ne peut pas contaminer une autre session). On NE vide PAS le helper (fenêtre de perte inutile) ; on laisse la dernière confirmation.

Décisions du propriétaire (2026-09-13)

  • Lancement v0 + v1 ENSEMBLE.
  • Canal de correction = écriture HA restreinte input_text.airtria_* (variante PostgreSQL INSERT-only écartée).

Détection d'une session (v0)

  • Source = table PostgreSQL session_charge, PAS le binary_sensor HA (il flappe : disconnected x2 câble branché le 07/09).
  • ⚠️ NE PAS utiliser evcs_lecture : il a INSERT sur evenement (registre d'audit partagé) — inadapté à un agent. Rôle DÉDIÉ airtria_ro, LOGIN + SELECT sur session_charge et evenement uniquement, aucune écriture. Mot de passe déposé protégé dans le workspace (.pg-airtria). Base evcs sur 10.10.10.22:5433.
  • Colonnes utiles (session_charge, 17 col) : debut (clef, MS), fin, terminee, vehicule, vehicule_sur (DEFAULT true → à basculer côté service), p_max_w + consigne_max_a + courant_max_a (règle invité), debranche_a (frontière de groupe), energie_compteur_kwh (l'énergie qui FAIT FOI, pas energie_kwh qui rate si le service démarre après le début de charge).
  • On hérite du débounce du service d'historisation : une ligne session_charge ne s'ouvre qu'au-delà de 300 W soutenus, se ferme après 10 min sous le seuil.
  • Cadence : cron 10 min. La latence n'a aucun effet (session de plusieurs heures).
  • Airtria garde son propre état (dernière debut traitée) dans son workspace.

Test d'ambiguïté (quand poser la question) — RÈGLE FINALE

  • Le sélecteur est FRAIS (ne pas demander) SEULEMENT s'il a changé de valeur dans la fenêtre [début du branchement − 10 min, début + 30 min]. Sinon → DEMANDER. Source : evenement (genre='intervention', entite=input_select.victron_evcs_vehicle), JAMAIS le last_changed HA. Implémenté : selectorFreshAt() dans concierge-live.js.
  • Pourquoi la fenêtre serrée (EVCS, données) : un changement de sélecteur EN COURS de branchement ne peut pas étiqueter ce branchement (câble non bougé) — c'est le producteur mécanique des groupes mixtes. Ma 1ère règle (« bougé depuis le débranchement précédent ») déclarait « frais » EXACTEMENT les 2 branchements mixtes de l'historique.
  • Validé sur 11 branchements 04-12/09 : 1 seul FRAIS (05/09, réglé à +13 min), 10 à DEMANDER dont les 2 mixtes. Stat marquante : sélecteur réglé au bon moment 1 fois / 12.
  • Mauvais test (rejeté) : « sélecteur inchangé depuis le début de CETTE session » — vrai aussi quand le propriétaire a bien réglé au branchement.
  • Avant de conclure « pas de geste humain », vérifier la table evenement (un redémarrage HA a pu changer la valeur ; l'auteur y est tracé depuis le 12/09).
  • ✅ CAVEAT last_changed (2026-09-13) CORRIGÉ : le last_changed HA est remis à zéro par un passage unavailable (blip MQTT) sans vrai changement → il ferait croire « frais » un sélecteur périmé. La règle ci-dessus lit donc evenement (qui saute les lectures unavailable), pas last_changed. Limites de la source : le journal commence le 04/09 ; la mémoire du détecteur est en-processus (un changement pendant un redémarrage du service n'est pas rattrapé) → l'absence d'événement ne prouve pas tout, mais dans le doute la règle DEMANDE = le bon côté.
  • En cas de doute : poser la question. Un WhatsApp de trop coûte moins qu'une ligne fausse pour toujours.

Pré-filtre puissance (réduit les questions) — règle À SENS UNIQUE

  • Le bon critère n'est pas la puissance brute mais la puissance RELATIVE À LA CONSIGNE. VW/JEEP saturent vers 16 A embarqués quelle que soit la consigne (~3 606 à 3 784 W à 20 A) ; la Megane a suivi jusqu'à 28 A (6 360 W).
  • Règle conclusive : consigne_max_a >= 20 ET p_max_w > 4200 ⟹ invité, Airtria pré-étiquette « invité » SANS déranger le propriétaire.
  • consigne_max_a < 20 ⟹ AUCUNE information (6 A plafonne ~1 700 W pour tout le monde) → demander.
  • ⚠️ La réciproque est FAUSSE : p_max_w <= 4200 ne prouve PAS que c'est un véhicule du foyer (un invité à 16 A embarqués est indiscernable de la VW). Au-dessus du seuil on sait, en dessous on demande. NE JAMAIS inverser.
  • La consigne vient du contrôleur EVCS : plafond 20 A hors BOOST (input_number.evcs_plafond_courant_edf_on, 28 en BOOST). Le cas conclusif est donc fréquent ; le cas muet = sessions à courant réduit (fin de charge solaire à 6 A).
  • Moto ZERO (fin sept. 2026) : tombera sous 3,6 kW → côté foyer, distinguée par le sélecteur comme les autres. Seuil à réévaluer à son arrivée.

Regroupement = frontière FACTUELLE via evenement (pas le temps)

  • ⚠️ Ne PAS regrouper par écart de temps : le creux soir→heures creuses fait ~5 h 20 (fin solaire ~20:00 → HC ~01:24), indistinguable d'un vrai débranchement/rebranchement par un seuil de durée.
  • La frontière est un FAIT : les transitions de binary_sensor.victron_evcs_vehicle_connected sont écrites dans evenement (off→on au branchement, on→off au débranchement). Un branchement = intervalle [off→on … on→off suivant]. Toutes les lignes session_charge dont debut tombe dans l'intervalle = même véhicule = UNE seule question. Intervalle ouvert (encore branché) = groupe en cours.
  • Exemple semaine 08-12/09 : 6 branchements = 2-3-2-1-1-1 sessions (le mercredi 15:55+19:35+01:25 est un TRIPLET, même câble). Un regroupement qui rend des paires soir/nuit est FAUX.
  • Le groupe reste la notion d'Airtria (propriétaire unique sur la notion dérivée) ; le service EVCS ne fait qu'itérer la liste du helper.
  • ⚠️ Ne PAS émettre une écriture helper par session (elles écraseraient le même helper ; le service qui poll toutes les 60 s ne verrait que la dernière → perte silencieuse des autres). Le helper porte donc une LISTE de session_debut (voir ci-dessous), publiée en UNE écriture atomique. Le service itère la liste.
  • Fallback coupure : les transitions ne sont écrites que si le service tourne. Frontière absente/incomplète → groupe INCERTAIN → poser la question PAR session, jamais sur une frontière reconstruite. Un WhatsApp de trop > une étiquette fausse.

Sources de données (requêtes, lecture seule via airtria_ro)

  • Frontière des branchements — table evenement (colonnes : ts, genre, source, resume, detail jsonb, id). Filtre robuste (pas de parsing de texte) : detail->>'entite' = 'binary_sensor.victron_evcs_vehicle_connected', detail->>'apres' = 'on' (branchement) / 'off' (débranchement). Ordonner sur ts, borner les intervalles sur detail->>'horodatage_ha' (vérité physique après rattrapage). Le champ entite n'existe que sur les transitions → les fins de session sont exclues naturellement.
  • Bonus qualité : les événements de fin de session portent detail->>'debut' (= session_charge.debut exact, ms comprises) et les trois énergies côte à côte (kwh_compteur fait foi, kwh_integre, kwh_borne). Un écart entre elles est un signal de qualité à remonter (pas une question au propriétaire).
  • Accès : Node + node-postgres (pur JS, à ajouter au workspace) ; PG:5433 joignable depuis la machine spark → calibration exécutable en lecture seule.
  • Implémentation : concierge-detection.js (mode calibration, aucune écriture).

La question WhatsApp

  • Un seul message, au démarrage d'une session ambiguë (ou d'un groupe).
  • Ouverte, pas binaire : « Quel véhicule est branché ? » avec options lues dynamiquement dans le sélecteur + « aucune des deux / invité ».
  • Porte la clef debut de la session pour permettre l'application a posteriori.
  • Réponse possible des heures plus tard, ou jamais.

Le helper HA (le contrat v1)

  • input_text.airtria_vehicle_label, déclaré max: 255 (défaut HA = 100 → rejet silencieux ; incident vécu forecast_updated max:32 le 08/09).
  • Contenu = UN JSON atomique portant une LISTE de sessions (atomicité indispensable, HA n'a pas de transaction entre entités) : {"sessions":["2026-09-08 19:39:12.334+02","2026-09-09 01:26:04.887+02"], "label":"VW","confirmed_at":"","source":"whatsapp"}
  • Chaque entrée de sessions = clef debut rendue par la requête SANS reformatage (les debut portent les ms, ex. 2026-09-12 11:55:54.493+02 ; un arrondi minute ne matcherait jamais ; pas d'aller-retour par un objet Date). Le service EVCS joint sur ±1 s, pour chaque entrée de la liste.
  • Taille : 2 sessions ≈ 150 car, 3 ≈ 180, sous 255. Au-delà, publier en deux écritures espacées de > 90 s (le service aura polé entre les deux).
  • Après CHAQUE écriture : relire l'état et vérifier qu'il est identique à l'envoi (seul moyen de détecter un rejet silencieux HA).
  • Auto-correction (« JEEP… non VW ») = republier avec confirmed_at plus récent ; le service applique le dernier pour une session_debut donnée.
  • On NE vide PAS le helper à {} : l'invalidation est structurelle (session_debut). Le vidage ouvrirait une fenêtre de perte si le service est arrêté entre publication et vidage.

Garde-fous d'écriture (non négociables — restriction CONVENTIONNELLE)

Le canal choisi est un token HA, qui peut techniquement écrire n'importe quel service HA : la restriction à airtria_* ne tient QUE par la discipline du code. Donc, côté OpenClaw :

  • Le nom de l'entité écrite est une CONSTANTE EN DUR unique (input_text.airtria_vehicle_label), jamais construite dynamiquement, jamais interpolée.
  • Un test type « mode ombre » ÉCHOUE si une écriture cible autre chose que cette constante (équivalent du tests/advisor/run.mjs d'EVCS qui garde advisor_*).
  • Aucune écriture d'actionneur, jamais (liste des variables à propriétaire unique dans le MANUEL EVCS ; carte-decisions.mjs / enquete.mjs avant tout doute).

Recoupement a posteriori (qualité de données)

  • Comparer session_charge.debranche_a aux départs attendus : VW 08:50 (input_datetime.victron_evcs_departure_time), JEEP 06:30 (input_datetime.victron_evcs_departure_time_jeep).
  • debranche_a n'est renseignée que si le service voit le débranchement (1 fois sur 12 récemment) → ne pas supposer qu'elle est toujours là.
  • Ne signaler que si ça CONTREDIT le label (comportement idiot), pas à chaque session. Pas de notification pour l'état normal.

Côté service EVCS (v1, fait par la session EVCS)

  • Lit le helper chaque cycle (60 s), JSON.parse en try/catch ; objet vide / illisible / champs manquants → ne rien faire.
  • confirmed_at antérieur au debut → ignoré. Jointure ±1 s sur session_debut.
  • Met à jour vehicule ; vehicule_sur passe à VRAI uniquement sur confirmation humaine (changement de sens : défaut basculé à faux ; lignes antérieures gardent l'ancienne sémantique). Trace l'application dans evenement avec la source.

Validation avant mise en service

  • tools/campagne-conseiller.mjs (dépôt EVCS) rejoue des instants réels ; cumuler 3 runs (un run de 5 passages = bruit, écart-type ~0,22).
  • Jeu de calibrage = les 12 sessions du 07 au 12/09, étiquetées par le propriétaire (voir ci-dessous). ⚠️ Le v0 ne DÉDUIT pas VW vs JEEP (impossible : la semaine, la routine était inversée — femme/JEEP en congés, fils/VW en stage sans retour le midi). Ce qui est testé = le ROUTAGE : MEGANE auto-détectée invité par la règle puissance ; toutes les autres correctement routées vers « demander » (sélecteur périmé). Ce sont les réponses du propriétaire qui donnent VW/JEEP. Si le routage rate une des 12, revoir la règle, pas l'étiquette.

Cibles de routage du v0 (les 6 branchements du 08-12/09)

  • 11/09 16:53→18:27 (Megane, 1 session) : auto-invité par la règle puissance, AUCUNE question.
  • 5 autres branchements (2-3-2-1-1) : routés vers la question (sélecteur périmé).
  • Total attendu = 5 questions pour 12 sessions. Un regroupement qui rend des paires soir/nuit (au lieu du triplet du mercredi) est FAUX.

Jeu de calibrage FINAL (corrigé en base, arbitré par le propriétaire 2026-09-13)

branchement                     sessions                              étiquette
sam 12/09 11:55 → (en cours)    11:55  5,94                           JEEP
sam 12/09 11:42 → 11:53         11:42  0,09                           JEEP
ven 11/09 16:53 → 18:27         16:54  7,45  6360W/28A                MEGANE (auto)
jeu 10/09 18:19 → ven 16:27     18:20 2,97 · 01:24 4,26               VW
mer 09/09 15:55 → jeu 09:05     15:55 6,61 · 19:35 0,77 · 01:25 4,88  JEEP (triplet)
mar 08/09 19:39 → mer 08:57     19:39 0,76 · 01:26 5,01               VW

Semaine : 5 VW, 6 JEEP, 1 MEGANE (contre 11 VW + 1 JEEP avant correction). Le triplet mercredi = JEEP les trois : la femme a branché la JEEP, le fils n'a pas voulu la débrancher le soir pour mettre la VW → il a roulé à l'essence le jeudi. Contexte humain qu'aucune règle ne produit — la raison d'être du concierge. MEGANE = voiture de service (Rennes) + dépannage d'amis → « invité » RÉCURRENT.

Cas tordus que le module DOIT gérer (dans les données de la semaine)

  • sam 12/09 : on 11:42, off 11:53, on 11:55 = DEUX branchements (2 min de débranchement), pas du flapping. La session 0,09 kWh en est la trace.
  • ven 11/09 : off 16:27, on 16:53 = DEUX branchements (26 min ; débranche un véhicule foyer pour brancher la Megane).
  • ⚠️ AUCUN seuil de durée pour fusionner : 2 min et 26 min sont l'une et l'autre de vraies séparations. Règle = « toute transition compte » ; défensive limitée aux incohérences STRUCTURELLES (on déjà ouvert, off sans ouverture).
  • Le branchement 12/09 11:55 est ENCORE OUVERT (borne haute nulle) → groupe en cours, ne pas conclure qu'il est incomplet.

Fenêtre d'analyse : commencer sur un 'on'

Une fenêtre doit démarrer sur un on, jamais au milieu d'un branchement : sinon le premier événement est un off orphelin (branchement ouvert avant la fenêtre), la session ouverte avant tombe hors intervalle et est classée « incertaine » à tort (ex. démarrer au 08/09 00:00 fait apparaître 6 questions au lieu de 5, à cause de la session 08/09 01:26). En production, aucun souci (on suit le flux) ; règle valable surtout au rejeu historique. Calibration calée sur 08/09 19:00.

Invariant à vérifier EN PERMANENCE (le contrôle le plus solide du projet)

Un seul câble ⟹ toutes les sessions d'un même branchement portent la MÊME étiquette. Un groupe à étiquettes mixtes = frontière fausse (Airtria) OU ligne fausse (base) — les deux méritent une alerte. Propriété PHYSIQUE : ne nécessite aucune connaissance métier (ni routine, ni horaire, ni puissance), et aucune évolution de règle ne peut l'invalider. À faire tourner en continu, pas seulement à la calibration — c'est ce qui aurait signalé le triplet incohérent le jour même.

Points de vigilance

  • ⚠️ Inventaire BMS mensonger (découvert 2026-09-13) : le BMS annonce 4 modules / 400 Ah alors que le parc en compte 9 (le LV-Hub ne remonte que le groupe maître). Mesuré : 0,312 kWh/point de SOC (vs 0,169 sur l'ancien parc de 7). NE JAMAIS utiliser capacite_installee ni nombre_de_modules_en_ligne comme source de capacité (sous-estiment de ~38 %). Source qui fait foi : input_number.evcs_balance_modules_en_service (13 aujourd'hui). Concerne surtout un futur rôle 2 (rétro-évaluation énergétique), pas le concierge véhicule.
  • Helper HA en place : input_text.airtria_vehicle_label (max:255, état initial unknown = « rien à appliquer »). Kill-switch consommateur input_boolean.airtria_concierge_enabled (OFF) = propriété d'EVCS, Airtria n'y touche pas. Aller-retour écriture/relecture validé le 2026-09-13.
  • SPOF : OpenClaw vit sur 10.10.10.17, la machine qui porte aussi HA et n8n. Faible enjeu pour l'ID véhicule, mais réel. Chien de garde sur 10.10.10.22.
  • Prérequis à câbler : (1) accès lecture PostgreSQL pour Airtria (PG_LECTURE_PASSWORD, déposé protégé comme le token HA) ; (2) création du helper input_text.airtria_vehicle_label (max:255) dans le paquet HA.