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:
43
HANDOFF.md
43
HANDOFF.md
@@ -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
|
||||
`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.
|
||||
- ⚠️ **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*
|
||||
|
||||
44
tests/allers-retours/run.mjs
Normal file
44
tests/allers-retours/run.mjs
Normal 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
24
tools/allers-retours.mjs
Normal 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;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import pg from 'pg';
|
||||
import { creerSchemaKm, ingererReleves, ingererNiveauSaisi, ingererJeep } from './kilometrage.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 { transitionsDansSerie } from './allers-retours.mjs';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const ONCE = process.argv.includes('--once');
|
||||
@@ -158,10 +159,13 @@ function demarrageHaMs(st) {
|
||||
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(
|
||||
`INSERT INTO evenement (genre, source, resume, detail) VALUES ($1, $2, $3, $4)`,
|
||||
[genre, 'historisation', resume, JSON.stringify(detail || {})]
|
||||
`INSERT INTO evenement (ts, genre, source, resume, detail)
|
||||
VALUES (COALESCE($5::timestamptz, now()), $1, $2, $3, $4)`,
|
||||
[genre, 'historisation', resume, JSON.stringify(detail || {}), ts]
|
||||
);
|
||||
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
|
||||
// journaliser : sinon chaque redemarrage du service produirait douze fausses
|
||||
// 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) {
|
||||
if (!memoire.amorce) {
|
||||
for (const [id] of TRANSITIONS_SUIVIES.map((t) => [t[0]])) {
|
||||
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.amorce = true;
|
||||
@@ -473,8 +501,30 @@ async function detecterEvenements(sql, st) {
|
||||
// lecture. La consigner ferait deux lignes de bruit a chaque hoquet MQTT.
|
||||
if (cur === '' || cur === 'unavailable' || cur === 'unknown') continue;
|
||||
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 === 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);
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user