Files
OPENCLAW_ARCHIVE3_17/workspace/airtria-concierge-vehicule-SPEC.md
Thierry Tallagrand f121f0a196 workspace: rapport solaire, prevision de nuit, concierge, courrier, fiche allegee
- Fiche agent maison (AGENTS.md) : prevision de nuit relayee par script, import
  EDF, regle JEEP midi, saisie vehicule/niveau/depart, concierge qui relaie
  « ✅ Noté » ou « Rien n'est noté », groupe A deplace (28 649 -> 25 831 car.).
- Nouvelles references : reference-concierge.md, reference-workspace.md.
- Scripts : rapport-solaire.js, prevision-nuit.js, airtria-ha-vehicule.js,
  concierge-live.js (answer ne sort plus jamais vide, mode CONCIERGE_SIMULATION).
- Courrier Orange / AIRTRIA / Gmail : audits, regles, menage, recents.
- Bancs EVCS tests-pv (G5 accepte « anomal »).
- .gitignore : liste blanche etendue aux nouveaux scripts et bancs.
2026-09-30 11:32:44 +02:00

382 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 = variable `AIRTRIA_RO_PASSWORD`, injectée dans le conteneur par le
`env_file: .env` du compose openclaw (secret dans /mnt/ARCH3/docker/openclaw/.env,
600, hors git). EVCS possède le rôle et peut RENOUVELER le mot de passe : le code lit
la variable d'environnement (source vivante), jamais un fichier figé — un `.pg-airtria`
périmé ferait échouer l'auth en silence. Après un changement du .env, RECRÉER le
conteneur (`docker compose up -d`) : un simple restart ne relit pas env_file. 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.
## Le sélecteur temps réel (ajout du 19/09/2026, demande du propriétaire via EVCS)
Répondre WhatsApp ne suffisait pas : le sélecteur `input_select.victron_evcs_vehicle`
pilote l'heure de départ (VW 08:50 / JEEP 06:30), donc la priorité et l'urgence de
recharge de nuit, et il restait à changer À LA MAIN (vécu le 19/09 : VW branchée
11:01, sélecteur resté sur JEEP). Désormais, sur confirmation HUMAINE :
- `setVehicleSelector(label, source)` dans concierge-write.js écrit AUSSI le
sélecteur, via `input_select.select_option`, PUIS relit l'état — HA IGNORE en
silence une option hors liste, la relecture est le seul détecteur.
- Options valides EXACTEMENT `VW` et `JEEP` (casse comprise), liste EN DUR dans le
code, volontairement PAS lue depuis HA : y ajouter une valeur est une décision,
pas un effet de bord d'un changement côté HA.
- MEGANE / INVITE / ZERO : on N'ÉCRIT PAS le sélecteur (il ne désigne que les deux
véhicules du foyer) ; l'étiquette seule est publiée, le sélecteur reste tel quel.
- Source HUMAINE uniquement (`whatsapp`) : la déduction règle-puissance ne conclut
jamais VW vs JEEP et ne doit JAMAIS écrire le sélecteur — le garde est dans la
fonction, pas seulement chez l'appelant.
- Si l'écriture du sélecteur échoue APRÈS la publication de l'étiquette, la réponse
WhatsApp le dit (« pense à le passer sur X à la main ») au lieu d'échouer :
l'étiquette publiée ne doit pas être perdue pour un sélecteur.
## 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 ne tient QUE par la discipline du code.
Donc, côté OpenClaw :
- Les noms des entités écrites sont DEUX CONSTANTES EN DUR
(`input_text.airtria_vehicle_label` et `input_select.victron_evcs_vehicle`),
jamais construites dynamiquement, jamais interpolées. Pas une de plus.
- concierge-write.test.js ÉCHOUE si une écriture cible autre chose que ces deux
constantes, si un service autre que set_value/select_option apparaît, si le
sélecteur accepte un label hors foyer ou une source non humaine, ou si une
relecture manque (équivalent du tests/advisor/run.mjs d'EVCS qui garde advisor_*).
- Aucune autre é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.
## Supervision — alerte d'échec d'auth (2026-09-16)
Motivation : le 15/09, EVCS a renouvelé le mot de passe `airtria_ro` ; le concierge a
alors échoué à se connecter, mais EN SILENCE — sortie vide, donc aucun WhatsApp émis,
aucune trace visible : il cessait simplement de poser la question sans que personne le
sache. Même angle mort que le service EVCS (une trace écrite seulement AU RETOUR à la
normale, dans PG, donc jamais quand c'est PG qui tombe).
Correctif (module `concierge-health.js`, même patron que le moniteur LLM) :
- **Battement de cœur À CHAQUE cycle** : `input_datetime.concierge_last_run` reçoit un
horodatage (valeur qui CHANGE — HA ne rafraîchit pas le `last_changed` d'un helper
réécrit à l'identique). Un concierge mort devient détectable (capteur `binary_sensor`
de fraîcheur, > 20 min → « figé »).
- **Statut live** : `input_text.concierge_status` (max:255) = `OK · HH:MM` ou
`⚠️ <erreur> · HH:MM`.
- **Alerte WhatsApp UNIQUEMENT sur transition** (ok↔erreur), jamais un rappel périodique
(« events not states ») : une panne qui dure ne spamme pas le canal. Dédup par état
local `concierge-health-state.json`.
- **Chemin HA indépendant de PostgreSQL** : l'alerte « auth KO » part précisément quand
c'est l'auth qui tombe (le signal de vie n'emprunte pas le chemin qui tombe). Best-effort :
si les helpers manquent, le concierge n'échoue pas ; l'alerte WhatsApp part quand même
(elle ne dépend pas des helpers). Interrupteur `input_boolean.concierge_monitor_enabled`
(OFF = pas de WhatsApp, affichage maintenu).
- Refactor de `concierge-detection.js` : `readPgPassword()` LÈVE au lieu de `process.exit`,
mot de passe résolu paresseusement dans `makeClient()` → l'échec devient RATTRAPABLE par
l'étage live, qui le publie. Paquet HA : `concierge-health-ha-package.yaml.draft`.
Machine à états vérifiée par `concierge-health.test.js` (dédup + transitions, sans réseau).