premiere mise en service du concierge

This commit is contained in:
2026-09-14 02:34:42 +02:00
parent 8c027a31f8
commit 40a4605a31
4 changed files with 562 additions and 0 deletions

View File

@@ -0,0 +1,314 @@
# Concierge véhicule Airtria — spécification (convenue OpenClaw ↔ EVCS, 2026-09-13)
## 🟢 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":"<horodatage Airtria>","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.