From 9448676cbd7b01974a25b15cbba2dfedf6a41443 Mon Sep 17 00:00:00 2001 From: Tallagrand Thierry Date: Sat, 12 Sep 2026 09:12:35 +0200 Subject: [PATCH] Chaque transition dit desormais QUI l a provoquee MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le 12/09, le contacteur reseau a bascule six fois en dix minutes. Trouver le coupable a demande sept requetes enchainees — executions n8n des deux workflows, liste des ecrivains, puis automations.yaml — parce que la table des evenements disait QUE ca avait bascule, jamais PAR QUI. Home Assistant le savait pourtant. Son logbook resout le contexte d un changement d etat et nomme l auteur. Sur la coupure de 06:29:44 : auteur : automation.victron_grid_off_si_soc_35 declencheur : triggered by numeric state of sensor.victron_battery_soc Un appel par transition REELLE — quelques-uns par jour, pas par cycle. Echec de lecture => pas d auteur, la ligne est ecrite quand meme : on ne devine jamais. ⚠️ L auteur est colle au RESUME, pas seulement au detail jsonb. C est le resume 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. remonte l auteur et son declencheur : l enquete de ce matin tient desormais en UNE commande, et l observateur pourra nommer le coupable au lieu de dire « un des trois ». Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj --- tools/enquete.mjs | 6 +++-- tools/historise-evcs.mjs | 51 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/tools/enquete.mjs b/tools/enquete.mjs index d244403..3c1e775 100644 --- a/tools/enquete.mjs +++ b/tools/enquete.mjs @@ -71,11 +71,13 @@ if (!commande || commande === 'outils') { }); } else if (commande === 'bascules') { const [motif, h = '6'] = args; - const r = sql(`select to_char(ts at time zone 'Europe/Paris','YYYY-MM-DD HH24:MI:SS'), genre, resume + const r = sql(`select to_char(ts at time zone 'Europe/Paris','YYYY-MM-DD HH24:MI:SS'), genre, resume, + coalesce(detail->>'auteur',''), coalesce(detail->>'declencheur','') from evenement where ts > now() - interval '${Number(h)} hours' ${motif ? `and resume ilike '%${motif.replace(/'/g, '')}%'` : ''} order by ts desc limit 80`); - const lignes = r.map(([quand, genre, resume]) => ({ quand, genre, resume })); + const lignes = r.map(([quand, genre, resume, auteur, declencheur]) => + ({ quand, genre, resume, ...(auteur ? { auteur } : {}), ...(declencheur ? { declencheur } : {}) })); // Un ecart court entre deux transitions du meme objet est la signature d une // oscillation : on le calcule ici pour que l appelant n ait pas a le refaire. for (let i = 0; i < lignes.length - 1; i++) { diff --git a/tools/historise-evcs.mjs b/tools/historise-evcs.mjs index 0339d13..bab85bb 100644 --- a/tools/historise-evcs.mjs +++ b/tools/historise-evcs.mjs @@ -155,6 +155,47 @@ const num = (v) => { }; const txt = (s) => (s ?? '').toString().trim(); +// --------------------------------------------------------------------- +// QUI A PROVOQUE CETTE TRANSITION (12/09/2026) +// --------------------------------------------------------------------- +// Le 12/09, le contacteur reseau a bascule six fois en dix minutes. Trouver le +// coupable a demande sept requetes enchainees — executions n8n des deux +// workflows, liste des ecrivains, puis automations.yaml — parce que la table +// des evenements disait QUE ca avait bascule, jamais PAR QUI. +// +// Home Assistant le sait pourtant : son logbook resout le contexte d un +// changement d etat et nomme l automatisation, le script ou l utilisateur qui +// l a declenche. Pour la coupure de 06:29:44 il rend : +// context_entity_id : automation.victron_grid_off_si_soc_35 +// context_message : triggered by numeric state of sensor.victron_battery_soc +// +// 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. +async function auteurDeLaTransition(entite, horodatageHa) { + try { + const t = Date.parse(horodatageHa); + if (!Number.isFinite(t)) return null; + const debut = new Date(t - 5000).toISOString(); + const fin = new Date(t + 5000).toISOString(); + const r = await fetch( + `${HA_URL}/api/logbook/${debut}?end_time=${encodeURIComponent(fin)}&entity_id=${encodeURIComponent(entite)}`, + { headers: { Authorization: `Bearer ${HA_TOKEN}` }, signal: AbortSignal.timeout(8000) }, + ); + 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, + }; + } catch { + return null; + } +} + async function etats() { const r = await fetch(`${HA_URL}/api/states`, { headers: { Authorization: `Bearer ${HA_TOKEN}` }, @@ -332,9 +373,17 @@ async function detecterEvenements(sql, st) { const prec = memoire.valeurs.get(id); if (prec === undefined) { memoire.valeurs.set(id, cur); continue; } if (prec === cur) continue; - await journaliser(sql, genre, `${libelle} : ${prec} -> ${cur}`, { + 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 + // panneaux « journal des evenements », et que le rapport de nuit reprend. + // Le mettre la le rend visible partout sans toucher a un seul tableau. + const resume = `${libelle} : ${prec} -> ${cur}` + + (qui ? ` — par ${qui.nom}` : ''); + await journaliser(sql, genre, resume, { entite: id, avant: prec, apres: cur, horodatage_ha: e.last_changed, + ...(qui ? { auteur: qui.auteur, auteur_nom: qui.nom, auteur_type: qui.type, declencheur: qui.declencheur } : {}), }); memoire.valeurs.set(id, cur); n++;