Historisation : allers-retours brefs de la borne et du contacteur retrouves dans l'historique HA (echange VW -> JEEP de 20 s)

This commit is contained in:
Tallagrand Thierry
2026-10-06 23:28:56 +02:00
parent f189df588d
commit 28851c83e4
4 changed files with 166 additions and 7 deletions

View File

@@ -1847,6 +1847,47 @@ orange-cloture (verifie par SPARK : envoye le 05/10 a 17:42).
de 90 % de SOC, EDF a midi des que le surplus prevu ne couvre pas le besoin ; plus de de 90 % de SOC, EDF a midi des que le surplus prevu ne couvre pas le besoin ; plus de
`force_grid_now` a la main. ⚠️ La session ne vaut que dans la fenetre HC de midi (12:24-14:24) : `force_grid_now` a la main. ⚠️ La session ne vaut que dans la fenetre HC de midi (12:24-14:24) :
une voiture branchee apres 14:24 n'est pas couverte. une voiture branchee apres 14:24 n'est pas couverte.
- ⚠️ **Defaut decouvert apres coup, a traiter** : `force_grid_now` n'a donne que **2,0 kWh** (13:18 -> 14:22).
Deux arrets « batterie trop sollicitee » (13:46 -41,9 A, 14:22 -47,3 A) sur des pointes de la maison a
7,1-7,7 kW (voiture 3,5 kW + cuisine ~4 kW) alors que le reseau ne fournissait que ~3,9 kW ; la garde
batterie du mode reseau juge sur UN echantillon, et le blocage anti-cliquetis de 20 min a coute ~40 min
de charge. Deux pistes soumises au proprietaire (tolerance de duree en mode reseau ; brider a 6 A au
lieu de couper), decision en attente.
- 14:24:00 echeance de `force_grid_now` en reel (trace « retour auto_solar_plus (sortie de la fenetre
d'heures creuses), SOC 57 % »), EDF ouvert 14:25:03.
### 06/10 soir — Production 14,5 kWh, et un echange VW -> JEEP de 20 s que personne n'a vu
- **Production reelle 14,5 kWh** a 18:06 (journee finie) pour VRM 22,1 (matin), ECMWF ~25-28,
Meteo-France ~25-29, ICON ~15-18 : ICON le plus juste, VRM -34 %.
- VW branchee 16:16:11 (question concierge 16:40 apres deux cycles d'attente Uconnect) ; reponses
WhatsApp 18:08 « VW », 18:09 « 0% » -> l'agent a fusionne (premier appel annule par OpenClaw),
« ✅ Noté : VW … 🔋 Niveau noté : 0 % » a 18:10:30. J'avais ecrit a 18:10:21 le selecteur VW,
l'etiquette (source proprietaire) et le mode de nuit **complete** (proprietaire : EDF de toute facon
cette nuit pour la batterie, SOC 62 pour 71,4 requis) ; le concierge a reecrit l'etiquette 9 s apres.
- **19:52:36 off / 19:52:56 on : echange VW -> JEEP en 20 s, JAMAIS journalise.** Le service lit l'etat
une fois par cycle (~75 s) : etat identique avant et apres -> aucune transition dans `evenement`,
donc ni branchement neuf, ni question du concierge (SPARK : son SQL_TRANS ne voyait qu'un branchement
depuis 16:16), ni regle « fin de trajet » JEEP (Uconnect 19:52:17, 28 %, 112 831 km). A 22:34 « J'ai
branche la JEEP » a donc relabellise le branchement de 16:16 en JEEP. Aucune session depuis 16:16 :
aucun kWh mal attribue.
- **Correctif (accord du proprietaire, deploye 23:26)** : `detecterEvenements` garde le `last_changed`
de chaque entite ; sur `ALLERS_RETOURS_SUIVIS` (borne branchee, contacteur EDF), si l'etat est
identique mais que `last_changed` a avance, il relit l'historique HA de l'intervalle et journalise
chaque transition A SON HORODATAGE HA (`journaliser(..., ts)`, colonne ts explicite, detail
`aller_retour: true`). Fonction pure `transitionsDansSerie` dans `tools/allers-retours.mjs`, tests
`node tests/allers-retours/run.mjs` 4/4 (dont le cas reel), mutation « indisponible compte » qui mord ;
kilometrage 21/21. Sauvegarde `historise-evcs.mjs.bak-20261006` (scratchpad) ; retour = `git revert`
+ `docker restart evcs-historisation`.
- **Rattrapage en base** : les deux transitions de 19:52 inserees a leur heure HA ; etiquette du
branchement 16:16 remise a **VW** ; helper concierge pointe le branchement **19:52:56 = JEEP**.
Au cycle suivant (23:28) la regle « fin de trajet » a joue seule : « Branchement identifie : JEEP
(fin de trajet juste avant le branchement, Uconnect), niveau 28 % ». Niveau HA saisi a 28 % par moi
a 23:24 (reference du compteur 4069,9 = compteur courant, rien de livre depuis 19:52).
- **Nuit 06->07** : SOC 37 % a 23:14 (soiree lourde, 62 -> 37 % depuis 18:06), requis 48,8 / 61,8 ;
EDF a 01:24 (~7,3 kWh maison + batterie) + JEEP 28 % -> plein (~7,2 kWh), mode complete. A verifier
le 07/10 : session JEEP comptee sur le branchement de 19:52:56, capacite estimee JEEP (28 % -> plein),
SOC a 9 h. Mode de nuit dans HA : EVCS Master onglet Vehicules, carte « Vehicule branche et cette
nuit » (« Charge cette nuit ») ; Bilan energetique onglet Production, carte « Voiture cette nuit ».
--- ---
*Document mis a jour le : 06/10/2026 (~13:35) — voiture a midi : la batterie n'est plus comptee comme du solaire (EDF automatique les jours de depart si le surplus ne couvre pas) ; nuit 05->06 : JEEP chargee sur EDF en complete (6,32 kWh), budget 0 du a la prevision, Windy/ECMWF contre VRM, a comparer ce soir ; concierge aligne sur la preuve fin de trajet ; a verifier : nuit 05->06 et mardi faible, comparaison VRM/Forecast.Solar ; JEEP identifiee aussi par sa fin de trajet, prevision de la nuit 05->06 ; identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10* *Document mis a jour le : 06/10/2026 (~23:35) — allers-retours brefs de la borne retrouves dans l'historique HA (echange VW -> JEEP de 20 s), production 14,5 kWh contre 22 prevus, defaut garde batterie en mode reseau a trancher ; voiture a midi : la batterie n'est plus comptee comme du solaire (EDF automatique les jours de depart si le surplus ne couvre pas) ; nuit 05->06 : JEEP chargee sur EDF en complete (6,32 kWh), budget 0 du a la prevision, Windy/ECMWF contre VRM, a comparer ce soir ; concierge aligne sur la preuve fin de trajet ; a verifier : nuit 05->06 et mardi faible, comparaison VRM/Forecast.Solar ; JEEP identifiee aussi par sa fin de trajet, prevision de la nuit 05->06 ; identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10*

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env node
// Allers-retours brefs retrouves dans l'historique HA (06/10/2026).
// Usage : node tests/allers-retours/run.mjs
import { transitionsDansSerie } from '../../tools/allers-retours.mjs';
let ok = 0, ko = 0;
function verifie(nom, obtenu, attendu) {
const a = JSON.stringify(obtenu), b = JSON.stringify(attendu);
if (a === b) { ok++; console.log(`✅ ${nom}`); }
else { ko++; console.log(`❌ ${nom}\n attendu ${b}\n obtenu ${a}`); }
}
const s = (state, last_changed) => ({ state, last_changed });
// Cas reel du 06/10 : echange VW -> JEEP en 20 s, entre deux cycles.
// L'historique HA commence par l'etat a l'instant de debut (16:16:10).
const reel = [
s('on', '2026-10-06T14:16:10.124Z'),
s('off', '2026-10-06T17:52:36.000Z'),
s('on', '2026-10-06T17:52:56.474Z'),
];
verifie('echange-20s-du-06-10',
transitionsDansSerie(reel, 'on', '2026-10-06T14:16:10.124Z', '2026-10-06T17:52:56.474Z'),
[{ avant: 'on', apres: 'off', a: '2026-10-06T17:52:36.000Z' },
{ avant: 'off', apres: 'on', a: '2026-10-06T17:52:56.474Z' }]);
// Un hoquet d'indisponibilite n'est pas un branchement.
verifie('indisponible-ignore',
transitionsDansSerie([s('on', '2026-10-06T14:16:10Z'), s('unavailable', '2026-10-06T15:00:00Z'), s('on', '2026-10-06T15:00:05Z')],
'on', '2026-10-06T14:16:10Z', '2026-10-06T15:00:05Z'),
[]);
// L'entree de depart (= dernier changement deja vu) n'est pas rejouee.
verifie('point-de-depart-exclu',
transitionsDansSerie([s('on', '2026-10-06T14:16:10Z')], 'on', '2026-10-06T14:16:10Z', '2026-10-06T14:16:10Z'),
[]);
// Deux allers-retours dans le meme intervalle : les quatre transitions, dans l'ordre.
verifie('deux-allers-retours',
transitionsDansSerie([s('on', '2026-10-06T10:00:00Z'), s('off', '2026-10-06T10:00:10Z'), s('on', '2026-10-06T10:00:20Z'),
s('off', '2026-10-06T10:00:40Z'), s('on', '2026-10-06T10:00:50Z')], 'on', '2026-10-06T10:00:00Z', '2026-10-06T10:00:50Z').length,
4);
console.log(ko ? `\n${ko} ECHEC(S)` : `\nALLERS-RETOURS OK — ${ok}/${ok}`);
process.exit(ko ? 1 : 0);

24
tools/allers-retours.mjs Normal file
View File

@@ -0,0 +1,24 @@
// Transitions d'etat retrouvees dans l'historique HA (06/10/2026).
// Voir ALLERS_RETOURS_SUIVIS dans historise-evcs.mjs : un aller-retour plus
// bref qu'un cycle du service ne se voit que par le `last_changed` de HA.
const txt = (s) => (s ?? '').toString().trim();
export const ETATS_ABSENTS = new Set(['', 'unavailable', 'unknown']);
// Transitions reelles entre `depuisIso` (exclu) et `jusquaIso` (inclus), en
// partant de l'etat `depart`. Les lectures indisponibles ne comptent pas.
// Pure (testee) : `serie` = reponse de /api/history/period pour une entite.
export function transitionsDansSerie(serie, depart, depuisIso, jusquaIso) {
const depuis = Date.parse(depuisIso), jusqua = Date.parse(jusquaIso);
const res = [];
let courant = depart;
for (const e of serie || []) {
const t = Date.parse(e.last_changed);
if (!(t > depuis && t <= jusqua)) continue;
const etat = txt(e.state);
if (ETATS_ABSENTS.has(etat) || etat === courant) continue;
res.push({ avant: courant, apres: etat, a: e.last_changed });
courant = etat;
}
return res;
}

View File

@@ -32,6 +32,7 @@ import pg from 'pg';
import { creerSchemaKm, ingererReleves, ingererNiveauSaisi, ingererJeep } from './kilometrage.mjs'; import { creerSchemaKm, ingererReleves, ingererNiveauSaisi, ingererJeep } from './kilometrage.mjs';
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs'; import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes, nuitMaisonPrevue } from './diagnostics-agent.mjs'; import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes, nuitMaisonPrevue } from './diagnostics-agent.mjs';
import { transitionsDansSerie } from './allers-retours.mjs';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const ONCE = process.argv.includes('--once'); const ONCE = process.argv.includes('--once');
@@ -158,10 +159,13 @@ function demarrageHaMs(st) {
return n >= 10 ? meilleur * 1000 : null; return n >= 10 ? meilleur * 1000 : null;
} }
async function journaliser(sql, genre, resume, detail) { async function journaliser(sql, genre, resume, detail, ts = null) {
// `ts` : horodatage HA d'une transition retrouvee apres coup (aller-retour
// bref, cf. detecterEvenements) ; sinon l'heure d'ecriture (defaut de la table).
await sql( await sql(
`INSERT INTO evenement (genre, source, resume, detail) VALUES ($1, $2, $3, $4)`, `INSERT INTO evenement (ts, genre, source, resume, detail)
[genre, 'historisation', resume, JSON.stringify(detail || {})] VALUES (COALESCE($5::timestamptz, now()), $1, $2, $3, $4)`,
[genre, 'historisation', resume, JSON.stringify(detail || {}), ts]
); );
console.log(` evenement [${genre}] ${resume}`); console.log(` evenement [${genre}] ${resume}`);
} }
@@ -430,13 +434,37 @@ async function ecrirePrevision(sql, st) {
// Memoire entre cycles. Au PREMIER passage on enregistre l'etat sans rien // Memoire entre cycles. Au PREMIER passage on enregistre l'etat sans rien
// journaliser : sinon chaque redemarrage du service produirait douze fausses // journaliser : sinon chaque redemarrage du service produirait douze fausses
// transitions. Seul le demarrage du service lui-meme est consigne. // transitions. Seul le demarrage du service lui-meme est consigne.
const memoire = { amorce: false, demarrageHa: null, valeurs: new Map() }; const memoire = { amorce: false, demarrageHa: null, valeurs: new Map(), changes: new Map() };
// ALLERS-RETOURS BREFS (06/10/2026). Le service lit l'etat une fois par cycle
// (~75 s) : un debranchement-rebranchement de 20 s (echange VW -> JEEP a
// 19:52:36 / 19:52:56 le 06/10) tombe entre deux lectures, l'etat est le meme
// avant et apres, et AUCUN branchement n'etait vu — ni par l'etiquetage, ni par
// le concierge qui lit cette table. Le `last_changed` de HA, lui, avance : sur
// ces entites, s'il a bouge alors que l'etat est identique, on relit
// l'historique HA de l'intervalle et on journalise chaque transition a SON
// horodatage. C'est la signature d'un echange de vehicule.
const ALLERS_RETOURS_SUIVIS = new Set([
'binary_sensor.victron_evcs_vehicle_connected',
'switch.edf_general_ok_switch',
]);
async function transitionsCachees(id, depart, depuisIso, jusquaIso) {
const r = await fetch(
`${HA_URL}/api/history/period/${encodeURIComponent(depuisIso)}?end_time=` +
`${encodeURIComponent(new Date(Date.parse(jusquaIso) + 1000).toISOString())}` +
`&filter_entity_id=${id}&minimal_response&no_attributes`,
{ headers: { Authorization: `Bearer ${HA_TOKEN}` }, signal: AbortSignal.timeout(15000) },
);
if (!r.ok) throw new Error(`historique HA ${r.status}`);
const series = await r.json();
return transitionsDansSerie(series[0], depart, depuisIso, jusquaIso);
}
async function detecterEvenements(sql, st) { async function detecterEvenements(sql, st) {
if (!memoire.amorce) { if (!memoire.amorce) {
for (const [id] of TRANSITIONS_SUIVIES.map((t) => [t[0]])) { for (const [id] of TRANSITIONS_SUIVIES.map((t) => [t[0]])) {
const e = st.get(id); const e = st.get(id);
if (e) memoire.valeurs.set(id, txt(e.state)); if (e) { memoire.valeurs.set(id, txt(e.state)); memoire.changes.set(id, e.last_changed); }
} }
memoire.demarrageHa = demarrageHaMs(st); memoire.demarrageHa = demarrageHaMs(st);
memoire.amorce = true; memoire.amorce = true;
@@ -473,8 +501,30 @@ async function detecterEvenements(sql, st) {
// lecture. La consigner ferait deux lignes de bruit a chaque hoquet MQTT. // lecture. La consigner ferait deux lignes de bruit a chaque hoquet MQTT.
if (cur === '' || cur === 'unavailable' || cur === 'unknown') continue; if (cur === '' || cur === 'unavailable' || cur === 'unknown') continue;
const prec = memoire.valeurs.get(id); const prec = memoire.valeurs.get(id);
const changePrec = memoire.changes.get(id);
memoire.changes.set(id, e.last_changed);
if (prec === undefined) { memoire.valeurs.set(id, cur); continue; } if (prec === undefined) { memoire.valeurs.set(id, cur); continue; }
if (prec === cur) continue; if (prec === cur) {
if (!ALLERS_RETOURS_SUIVIS.has(id) || !changePrec
|| !(Date.parse(e.last_changed) > Date.parse(changePrec))) continue;
let cachees = [];
try {
cachees = await transitionsCachees(id, prec, changePrec, e.last_changed);
} catch (err) {
console.log(` aller-retour ${id} : historique illisible (${err.message})`);
}
for (const tr of cachees) {
const quiC = await auteurDeLaTransition(id, tr.a);
const resumeC = `${libelle} : ${tr.avant} -> ${tr.apres} (aller-retour bref, retrouve dans l historique HA)`
+ (quiC ? (quiC.type === 'derivation' ? ` — derive de ${quiC.nom}` : ` — par ${quiC.nom}`) : '');
await journaliser(sql, genre, resumeC, {
entite: id, avant: tr.avant, apres: tr.apres, horodatage_ha: tr.a, aller_retour: true,
...(quiC ? { auteur: quiC.auteur, auteur_nom: quiC.nom, auteur_type: quiC.type, declencheur: quiC.declencheur } : {}),
}, tr.a);
n++;
}
continue;
}
const qui = await auteurDeLaTransition(id, e.last_changed); const qui = await auteurDeLaTransition(id, e.last_changed);
// L auteur est colle au RESUME, pas seulement au detail : c est le resume // L auteur est colle au RESUME, pas seulement au detail : c est le resume
// que Grafana affiche en annotation sur tous les tableaux et dans les // que Grafana affiche en annotation sur tous les tableaux et dans les