From 2345b87d04637ea64cf13b51eab19f0043a263ea Mon Sep 17 00:00:00 2001 From: Tallagrand Thierry Date: Sun, 13 Sep 2026 11:30:18 +0200 Subject: [PATCH] Les observations du conseiller cessent de s evaporer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ses helpers sont ecrases toutes les quinze minutes : un signalement grave emis a 3 h du matin avait disparu au reveil, et rien dans la memoire du projet n en gardait trace. C est precisement ce que le proprietaire demandait quand il a dit vouloir un outil pour garder une trace des evolutions — voir un comportement idiot du systeme ne sert a rien si personne ne peut le relire le lendemain. Une table `observation`, alimentee par le service d historisation, clef sur l horodatage du marqueur du conseiller : rejouer un cycle n invente rien et un conseiller a l arret n ecrit pas la meme ligne toutes les minutes. La confiance est extraite de `_reason` (« [NN%] la piste ») pour rester interrogeable sans decouper une chaine en SQL. `node tools/enquete.mjs observations [heures] [gravite]` ne montre par defaut que ce qui a ete signale, et compte a part les cycles calmes : un observateur qui fonctionne est silencieux la plupart du temps, et noyer les signalements sous des centaines de « rien a dire » les rendrait invisibles. Essai a blanc : observation=1, le service ecrirait bien sa ligne. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj --- tools/enquete.mjs | 20 +++++++++++++ tools/historise-evcs.mjs | 61 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/tools/enquete.mjs b/tools/enquete.mjs index 3c1e775..5c9b8ed 100644 --- a/tools/enquete.mjs +++ b/tools/enquete.mjs @@ -66,6 +66,7 @@ if (!commande || commande === 'outils') { { nom: 'decisions', usage: 'decisions [date]', quoi: 'ce qu un workflow n8n a decide, cycle par cycle' }, { nom: 'ecrivains', usage: 'ecrivains ', quoi: 'qui peut ecrire sur un actionneur, et qui en est le proprietaire declare' }, { nom: 'bilan', usage: 'bilan [heures=6]', quoi: 'ce que le bilan energetique demandait' }, + { nom: 'observations', usage: 'observations [heures=24] [gravite]', quoi: 'ce que le conseiller a signale, et a quelle gravite' }, { nom: 'cherche', usage: 'cherche ', quoi: 'ou ce motif apparait dans les automatisations, scripts et paquets Home Assistant' }, ], }); @@ -144,6 +145,25 @@ if (!commande || commande === 'outils') { and extract(minute from ts at time zone 'Europe/Paris')::int % 30 = 0 order by ts desc limit 40`); sortie({ lignes: r.map(([h2, statut, soc, requis, couvrir, croisement, reseau]) => ({ h: h2, statut, soc: Number(soc), soc_requis: Number(requis), a_couvrir_kwh: Number(couvrir), croisement, reseau_necessaire: reseau === 't' })) }); +} else if (commande === 'observations') { + // Par defaut on ne montre QUE ce qui a ete signale : un conseiller qui + // fonctionne est silencieux la plupart du temps, et noyer les signalements + // dans des centaines de « rien a dire » les rendrait invisibles. + const [h = '24', gravite] = args; + const filtre = gravite ? `and gravite = '${gravite.replace(/'/g, '')}'` : "and gravite <> 'aucune'"; + const r = sql(`select to_char(ts at time zone 'Europe/Paris','DD/MM HH24:MI'), gravite, + constat, ce_qui_cloche, piste, round(soc_pct), statut + from observation where ts > now() - interval '${Number(h)} hours' ${filtre} + order by ts desc limit 40`); + const calmes = sql(`select count(*) from observation + where ts > now() - interval '${Number(h)} hours' and gravite = 'aucune'`); + sortie({ + fenetre_heures: Number(h), + cycles_calmes: Number(calmes[0]?.[0] ?? 0), + signalements: r.length, + lignes: r.map(([q, g, c, cl, p, soc, st]) => + ({ quand: q, gravite: g, constat: c, ce_qui_cloche: cl, piste: p, soc: Number(soc), statut: st })), + }); } else if (commande === 'cherche') { const [motif] = args; const trouves = []; diff --git a/tools/historise-evcs.mjs b/tools/historise-evcs.mjs index bab85bb..660b279 100644 --- a/tools/historise-evcs.mjs +++ b/tools/historise-evcs.mjs @@ -263,6 +263,37 @@ async function ecrireBilan(sql, st) { return rowCount; } +// Une observation par run du conseiller. La cle est l'horodatage de SON +// marqueur : rejouer un cycle n'invente rien, et un conseiller a l'arret +// n'ecrit pas la meme observation toutes les minutes. +async function ecrireObservation(sql, st) { + const marqueur = st.get('input_text.evcs_advisor_last_run'); + if (!marqueur || !txt(marqueur.state)) return 0; + const g = (id) => txt(st.get(id)?.state); + // `_reason` porte « [NN%] la piste ». On separe pour que la confiance + // reste interrogeable sans avoir a decouper une chaine en SQL. + const reason = g('input_text.evcs_advisor_reason') || ''; + const m = reason.match(/^\[(\d+)%\]\s*(.*)$/s); + const { rowCount } = await sql( + `INSERT INTO observation (ts, statut, gravite, constat, ce_qui_cloche, + piste, modele, soc_pct, brut) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) + ON CONFLICT (ts) DO NOTHING`, + [ + marqueur.last_updated, + g('input_text.evcs_advisor_status'), + g('input_text.evcs_advisor_gravite'), + g('input_text.evcs_advisor_constat'), + g('input_text.evcs_advisor_ce_qui_cloche'), + m ? m[2] : reason, + g('input_text.evcs_advisor_model'), + num(st.get('sensor.victron_battery_soc')?.state), + JSON.stringify({ confiance_pct: m ? Number(m[1]) : null, reason_brut: reason }), + ] + ); + return rowCount; +} + async function ecrireDecision(sql, st) { const etat = st.get('input_text.victron_evcs_surplus_state'); if (!etat) return 0; @@ -509,10 +540,32 @@ async function migrer(client) { )`); await client.query( 'CREATE INDEX IF NOT EXISTS cellule_module_ts_idx ON cellule (module, ts DESC)'); + + // Ce que le CONSEILLER a observe. Ses helpers sont ecrases toutes les + // quinze minutes : sans cette table, un signalement grave emis a 3 h du + // matin a disparu au reveil. C'est precisement la trace que le proprietaire + // demandait — voir un comportement idiot ne sert a rien si personne ne peut + // le relire le lendemain. + await client.query(` + CREATE TABLE IF NOT EXISTS observation ( + ts timestamptz PRIMARY KEY, + statut text, + gravite text, + constat text, + ce_qui_cloche text, + piste text, + modele text, + soc_pct double precision, + brut jsonb + )`); + // On interroge cette table par gravite bien plus souvent que par date : + // « montre-moi tous les signalements graves de la semaine ». + await client.query( + 'CREATE INDEX IF NOT EXISTS observation_gravite_idx ON observation (gravite, ts DESC)'); // Le role de lecture doit voir la nouvelle table. S'il n'existe pas encore, // on ne fait pas echouer le demarrage pour autant. await client.query(`DO $$ BEGIN - GRANT SELECT ON session_charge TO evcs_lecture; + GRANT SELECT ON session_charge, cellule, observation TO evcs_lecture; EXCEPTION WHEN undefined_object THEN NULL; END $$`); } @@ -806,6 +859,7 @@ async function cycle(client) { mesures: await ecrireMesures(sql, st), bilan: await ecrireBilan(sql, st), decision: await ecrireDecision(sql, st), + observation: await ecrireObservation(sql, st), prevision: await ecrirePrevision(sql, st), session: await ecrireSession(sql, client, st), evenement: await detecterEvenements(sql, st), @@ -820,12 +874,13 @@ async function cycle(client) { return 0; }), }; - const total = r.mesures + r.bilan + r.decision + r.prevision + r.evenement + r.session + r.cellules + (r.rapport || 0); + const total = r.mesures + r.bilan + r.decision + r.prevision + r.evenement + r.session + r.cellules + r.observation + (r.rapport || 0); if (total > 0 || ONCE) { console.log( `${new Date().toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })} ` + `${DRY ? '[BLANC] ' : ''}mesures=${r.mesures} bilan=${r.bilan} ` + - `decision=${r.decision} prevision=${r.prevision} session=${r.session} evenement=${r.evenement} cellules=${r.cellules}` + `decision=${r.decision} prevision=${r.prevision} session=${r.session} evenement=${r.evenement} ` + + `observation=${r.observation} cellules=${r.cellules}` ); } }