L auteur des transitions etait lisible depuis le debut, on cherchait le mauvais champ

auteurDeLaTransition 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.

Le contexte etait la, sous d autres noms, avec trois signatures distinctes
relevees sur des transitions dont je connaissais l auteur : une ligne voisine
script/automation qui se nomme ; un service exterieur dont la rafale se CLOT par
une ligne de narration portant le nom du workflow ET sa raison ; ou une simple
derivation depuis un autre capteur — qui n est pas un auteur et ne doit pas etre
annoncee comme tel.

On balaie vers l AVANT pour trouver la narration : un workflow ecrit ses helpers
puis se journalise, donc vers l arriere on ramasserait celle du precedent.
Verifie sur la seconde ou l Auto Charge Manager et le Water Heater Advisor
ecrivent ensemble — chaque helper revient a son propre workflow, 3 sur 3.

Benefice non cherche : le declencheur porte desormais la raison que le workflow
a lui-meme publiee. C est ce que « expliquer les decisions » reclamait, et
c etait deja dans le journal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
Tallagrand Thierry
2026-09-14 03:03:56 +02:00
parent 83c88b9aba
commit b9d79ed22a

View File

@@ -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,