diff --git a/tools/historise-evcs.mjs b/tools/historise-evcs.mjs index 9c98baa..3f04503 100644 --- a/tools/historise-evcs.mjs +++ b/tools/historise-evcs.mjs @@ -171,6 +171,36 @@ const txt = (s) => (s ?? '').toString().trim(); // // Un appel par transition reelle — quelques-uns par jour, pas par cycle. En cas // d echec on n invente rien : pas d auteur, et la ligne est ecrite quand meme. +// --------------------------------------------------------------------- +// QUI A PROVOQUE CETTE TRANSITION — reecrit le 14/09/2026 +// --------------------------------------------------------------------- +// ⚠️ La version precedente filtrait sur `x.context_name`. CE CHAMP N EXISTE +// PAS dans le journal de bord de Home Assistant : la fonction rendait `null` a +// tous les coups et les 416 transitions journalisees portaient un auteur vide. +// Personne ne s en etait apercu parce qu un auteur absent ressemble a un auteur +// inconnaissable. Or le contexte EST la, sous d autres noms. +// +// Trois signatures, relevees sur des transitions dont je connaissais l auteur : +// +// automatisation / script ligne voisine dont l entity_id est `script.*` ou +// `automation.*` — elle se nomme elle-meme. +// service exterieur (n8n) `context_user_id` sur la ligne cible, et la rafale +// se CLOT par une ligne de NARRATION (un `message` +// sans `state`) qui porte le nom du workflow et sa +// raison : « Victron Auto Charge », « EVCS Bilan +// energetique (ombre) ». +// derivation ni l un ni l autre, mais `context_entity_id` : la +// valeur DECOULE d un autre capteur. Ce n est PAS un +// auteur et il ne faut pas l annoncer comme tel. +// +// ⚠️ ON BALAIE VERS L AVANT pour trouver la narration, jamais vers l arriere : +// un workflow ecrit ses helpers PUIS se journalise. Vers l arriere on +// ramasserait la narration du workflow PRECEDENT. Verifie sur le cas ou l Auto +// Charge Manager et le Water Heater Advisor ecrivent dans la MEME seconde : +// chaque helper revient bien a son propre workflow, 3 sur 3. +const NARRATION = (x) => x && x.message !== undefined && x.state === undefined; +const DELAI_NARRATION_MS = 1500; + async function auteurDeLaTransition(entite, horodatageHa) { try { const t = Date.parse(horodatageHa); @@ -183,14 +213,38 @@ async function auteurDeLaTransition(entite, horodatageHa) { ); if (!r.ok) return null; const lignes = await r.json(); - const l = (Array.isArray(lignes) ? lignes : []).find((x) => x.entity_id === entite && x.context_name); - if (!l) return null; - return { - auteur: l.context_entity_id || l.context_name, - nom: l.context_name, - type: l.context_domain || l.context_event_type || null, - declencheur: l.context_message || null, - }; + if (!Array.isArray(lignes)) return null; + + const i = lignes.findIndex((x) => x.entity_id === entite && x.state !== undefined + && Math.abs(Date.parse(x.when) - t) < 2000); + if (i < 0) return null; + const cible = lignes[i]; + + const acteur = lignes.find((x) => /^(automation|script)\./.test(String(x.entity_id || '')) + && Math.abs(Date.parse(x.when) - t) < 2000); + if (acteur) { + return { auteur: acteur.entity_id, nom: acteur.name || acteur.entity_id, + type: acteur.entity_id.split('.')[0], declencheur: cible.context_service || null }; + } + + if (cible.context_user_id) { + const n = lignes.slice(i + 1).find(NARRATION); + if (n && Date.parse(n.when) - t < DELAI_NARRATION_MS) { + // `declencheur` porte la RAISON que le workflow a lui-meme publiee. + // C est ce que « expliquer les decisions » reclamait, et c etait deja la. + return { auteur: n.entity_id || n.name, nom: n.name, type: 'service', + declencheur: (n.message || '').slice(0, 200) }; + } + return { auteur: cible.context_user_id, nom: 'service externe (non nomme)', type: 'service', + declencheur: cible.context_domain && cible.context_service + ? `${cible.context_domain}.${cible.context_service}` : null }; + } + + if (cible.context_entity_id) { + return { auteur: cible.context_entity_id, nom: cible.context_entity_id, + type: 'derivation', declencheur: null }; + } + return null; } catch { return null; } @@ -409,8 +463,10 @@ async function detecterEvenements(sql, st) { // que Grafana affiche en annotation sur tous les tableaux et dans les // panneaux « journal des evenements », et que le rapport de nuit reprend. // Le mettre la le rend visible partout sans toucher a un seul tableau. + // Une derivation n'est pas un ecrivain : « par sensor.X » serait un + // contresens pour une valeur qui DECOULE d'un autre capteur. const resume = `${libelle} : ${prec} -> ${cur}` - + (qui ? ` — par ${qui.nom}` : ''); + + (qui ? (qui.type === 'derivation' ? ` — derive de ${qui.nom}` : ` — par ${qui.nom}`) : ''); await journaliser(sql, genre, resume, { entite: id, avant: prec, apres: cur, horodatage_ha: e.last_changed,