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: '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: '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: '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' },
|
{ 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`);
|
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]) =>
|
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' })) });
|
({ 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') {
|
} else if (commande === 'cherche') {
|
||||||
const [motif] = args;
|
const [motif] = args;
|
||||||
const trouves = [];
|
const trouves = [];
|
||||||
|
|||||||
@@ -263,6 +263,37 @@ async function ecrireBilan(sql, st) {
|
|||||||
return rowCount;
|
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) {
|
async function ecrireDecision(sql, st) {
|
||||||
const etat = st.get('input_text.victron_evcs_surplus_state');
|
const etat = st.get('input_text.victron_evcs_surplus_state');
|
||||||
if (!etat) return 0;
|
if (!etat) return 0;
|
||||||
@@ -509,10 +540,32 @@ async function migrer(client) {
|
|||||||
)`);
|
)`);
|
||||||
await client.query(
|
await client.query(
|
||||||
'CREATE INDEX IF NOT EXISTS cellule_module_ts_idx ON cellule (module, ts DESC)');
|
'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,
|
// Le role de lecture doit voir la nouvelle table. S'il n'existe pas encore,
|
||||||
// on ne fait pas echouer le demarrage pour autant.
|
// on ne fait pas echouer le demarrage pour autant.
|
||||||
await client.query(`DO $$ BEGIN
|
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 $$`);
|
EXCEPTION WHEN undefined_object THEN NULL; END $$`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,6 +859,7 @@ async function cycle(client) {
|
|||||||
mesures: await ecrireMesures(sql, st),
|
mesures: await ecrireMesures(sql, st),
|
||||||
bilan: await ecrireBilan(sql, st),
|
bilan: await ecrireBilan(sql, st),
|
||||||
decision: await ecrireDecision(sql, st),
|
decision: await ecrireDecision(sql, st),
|
||||||
|
observation: await ecrireObservation(sql, st),
|
||||||
prevision: await ecrirePrevision(sql, st),
|
prevision: await ecrirePrevision(sql, st),
|
||||||
session: await ecrireSession(sql, client, st),
|
session: await ecrireSession(sql, client, st),
|
||||||
evenement: await detecterEvenements(sql, st),
|
evenement: await detecterEvenements(sql, st),
|
||||||
@@ -820,12 +874,13 @@ async function cycle(client) {
|
|||||||
return 0;
|
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) {
|
if (total > 0 || ONCE) {
|
||||||
console.log(
|
console.log(
|
||||||
`${new Date().toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })} ` +
|
`${new Date().toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })} ` +
|
||||||
`${DRY ? '[BLANC] ' : ''}mesures=${r.mesures} bilan=${r.bilan} ` +
|
`${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