Revue : etat des leviers joint a chaque constat, transmis au conseiller et historise

This commit is contained in:
Tallagrand Thierry
2026-10-08 09:56:36 +02:00
parent dea9c88530
commit b7448b72c4
8 changed files with 83 additions and 11 deletions

View File

@@ -796,6 +796,7 @@ async function migrer(client) {
chiffres jsonb,
PRIMARY KEY (jour, code, debut)
)`);
await client.query('ALTER TABLE revue_constat ADD COLUMN IF NOT EXISTS etat text');
await client.query(`
CREATE TABLE IF NOT EXISTS revue_piste (
jour date NOT NULL,
@@ -1195,13 +1196,13 @@ async function revueDeLaJournee(sql, client, st) {
await publierCapteur('sensor.evcs_revue_jour', jour, {
friendly_name: 'Revue de la journee',
a_examiner: aExaminer,
constats: r.constats.map((c) => ({ code: c.code, gravite: c.gravite, texte: c.texte, contexte: c.contexte })),
constats: r.constats.map((c) => ({ code: c.code, gravite: c.gravite, texte: c.texte, contexte: c.contexte, etat: c.etat || '' })),
texte: r.texte,
});
for (const c of r.constats) {
await sql(`INSERT INTO revue_constat (jour, code, gravite, debut, fin, texte, contexte, chiffres)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT DO NOTHING`,
[jour, c.code, c.gravite, new Date(c.debut), new Date(c.fin), c.texte, c.contexte, JSON.stringify(c.chiffres || {})]);
await sql(`INSERT INTO revue_constat (jour, code, gravite, debut, fin, texte, contexte, chiffres, etat)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT DO NOTHING`,
[jour, c.code, c.gravite, new Date(c.debut), new Date(c.fin), c.texte, c.contexte, JSON.stringify(c.chiffres || {}), c.etat || null]);
}
await journaliser(sql, 'revue', `Revue de la journee : ${aExaminer} point(s) a examiner`, { jour, texte: r.texte });
await publierRevueHistorique(client);

View File

@@ -65,6 +65,33 @@ function plages(debut, fin, pred) {
return out;
}
// RELEVE DE L'ETAT a un instant (08/10, proprietaire) : le conseiller ne voyait
// que la raison du controleur au debut de l'episode, et ses pistes du 08/10
// etaient fausses faute de voir l'ACM a 30 A, la consigne tenue ou le mode
// « sans EDF ». Chaque constat porte donc l'etat des leviers a ce moment-la.
function etatA(d, t) {
const v = (serie, unite, dec = 0) => {
const x = serie ? aInstant(serie, t) : null;
return x === null || !Number.isFinite(Number(x)) ? 'inconnu' : `${Math.round(Number(x) * 10 ** dec) / 10 ** dec}${unite}`;
};
let x = null; for (const r of d.decisions || []) { if (r.ts <= t) x = r; else break; }
const mode = d.modeNuit ? aInstant(d.modeNuit, t) : null;
return `a ${hhmm(t)} : ` + [
`SOC ${v(d.soc, ' %')}`,
`maison ${v(d.maisonW, ' W')}`,
`reseau ${v(d.reseauW, ' W')}`,
`consigne reseau ${v(d.consigneW, ' W')}`,
`courant de charge max fixe par l'ACM ${v(d.acmA, ' A')}`,
`borne ${v(d.borneW, ' W')}`,
`session HC engagee ${x ? (x.session_engagee ? 'oui' : 'non') : 'inconnu'}`,
`mode de nuit ${mode || 'inconnu'}`,
`budget voiture sans EDF ${v(d.budgetKwh, ' kWh', 2)}`,
`SOC requis (bilan) ${v(d.requis, ' %', 1)}`,
`cible du matin ${x && Number.isFinite(Number(x.cible_matin_pct)) ? `${x.cible_matin_pct} %` : 'inconnu'}`,
`controleur ${x ? `${x.etat} / ${x.source_mode}` : 'inconnu'}`,
].join(', ');
}
/**
* @param {object} d
* debut, fin : ms
@@ -107,6 +134,7 @@ export function constatsRevue(d) {
+ (socDebut !== null ? `, batterie a ${Math.round(socDebut)} % a la fermeture` : '')
+ (ouverture?.auteur ? ` ; ferme par ${ouverture.auteur}` : '') + '.',
chiffres: { minutes: Math.round(min), kwh: kwh === null ? null : Math.round(kwh * 100) / 100, soc: socDebut },
etat: etatA(d, p.debut),
contexte: (aInstant(d.decisions.map((x) => ({ ts: x.ts, v: x.raison })), p.debut) || '').slice(0, 400),
});
}
@@ -127,6 +155,7 @@ export function constatsRevue(d) {
debut: p.debut, fin: p.fin,
texte: `Voiture branchee mais bloquee de ${hhmm(p.debut)} a ${hhmm(p.fin)} (${duree(min)}) alors qu'EDF etait ferme en heures creuses (etat ${x.etat}).`,
chiffres: { minutes: Math.round(min) },
etat: etatA(d, p.debut + MIN),
contexte: (x.raison || '').slice(0, 400),
});
}
@@ -156,6 +185,7 @@ export function constatsRevue(d) {
texte: `Heures creuses de midi en charge maximale (jour faible) : batterie ${soc0 === null ? '?' : Math.round(soc0)} % -> ${Math.round(soc)} %`
+ (soc0 === null ? '.' : ` (+${f1((soc - soc0) * KWH_PAR_POINT)} kWh).`),
chiffres: { socDebut: soc0, socFin: soc },
etat: etatA(d, t - 60 * MIN),
contexte: (x.raison || '').slice(0, 400),
});
continue;
@@ -168,6 +198,8 @@ export function constatsRevue(d) {
debut: t, fin: t,
texte: `Fin des heures creuses de ${f.nom} (${hhmm(t)}) : batterie a ${Math.round(soc)} % pour ${f1(objectif)} % vises, il manquait ${f1(manque * KWH_PAR_POINT)} kWh.`,
chiffres: { soc, objectif, manqueKwh: Math.round(manque * KWH_PAR_POINT * 10) / 10 },
// Milieu de la fenetre : la ou la recharge aurait du se faire.
etat: etatA(d, t - (f.nom === 'nuit' ? 3 * 60 : 60) * MIN),
contexte: (x.raison || '').slice(0, 400),
});
}
@@ -212,6 +244,15 @@ export async function donneesRevue(client, fenetres, finMs) {
debut, fin, fenetres, contacteur, decisions,
edfKwh: await serie('sensor.edf_general_ok_energie_totale'),
reseauW: await serie('sensor.victron_grid_power_l1'),
maisonW: await serie('sensor.victron_consumption_power_l1'),
borneW: await serie('sensor.victron_evcs_ac_power'),
consigneW: await serie('number.gx_device_ac_power_setpoint'),
acmA: await serie('sensor.victron_max_charge_current_setting'),
budgetKwh: await serie('input_number.evcs_balance_night_vehicle_budget_kwh'),
modeNuit: (await q(`SELECT ts, v FROM (
(SELECT ts, detail->>'apres' v FROM evenement WHERE detail->>'entite'='input_select.victron_evcs_night_charge_mode' AND ts < $1 ORDER BY ts DESC LIMIT 1)
UNION ALL (SELECT ts, detail->>'apres' v FROM evenement WHERE detail->>'entite'='input_select.victron_evcs_night_charge_mode' AND ts >= $1 AND ts < $2)) s ORDER BY ts`, [d0, d1]))
.map((r) => ({ ts: ms(r), v: r.v })),
soc: await serie('sensor.victron_battery_soc'),
requis: await serie('input_number.evcs_balance_required_soc'),
};