Les observations du conseiller cessent de s evaporer
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
@@ -66,6 +66,7 @@ if (!commande || commande === 'outils') {
|
||||
{ nom: 'decisions', usage: 'decisions <workflowId> <hhmm_debut> <hhmm_fin> [date]', quoi: 'ce qu un workflow n8n a decide, cycle par cycle' },
|
||||
{ nom: 'ecrivains', usage: 'ecrivains <entite>', 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 <motif>', 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 = [];
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user