sensor.evcs_bilan_nuit : le bilan de nuit de l'agent vient du code du rapport du matin
Incident 25/09 11:49 : l'agent sans source avait invente 0,56 kWh / zero reseau / 35 % pour une nuit a 7,9 kWh dont 2,2 reseau. donneesNuit() extrait de rapport-nuit.mjs (texte identique sur 5 nuits), publie pour l'agent. Tests 37/37. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uJ47NxgfpGnuuMr3f2n7Z
This commit is contained in:
@@ -58,16 +58,16 @@ function pluriel(n, sing, plur) { return n <= 1 ? `${n} ${sing}` : `${n} ${plur}
|
||||
// Le rapport lui-meme. `jour` est la date du MATIN ; la nuit examinee va de
|
||||
// 18 heures la veille a 10 heures ce matin-la.
|
||||
// ---------------------------------------------------------------------
|
||||
export async function texteRapport(client, jour = new Date()) {
|
||||
// ⚠️ UNE SEULE SOURCE pour le courriel du matin ET l'agent vocal (25/09) :
|
||||
// `donneesNuit` fait toutes les lectures, `texteRapport` les met en phrases, et
|
||||
// le service d'historisation publie les memes donnees dans
|
||||
// sensor.evcs_bilan_nuit. Ce matin-la, l'agent sans source avait invente
|
||||
// « 0,56 kWh, zero reseau, arretee vers 4 h » pour une nuit a 7,9 kWh dont 2,2
|
||||
// reseau. Les deux sorties ne peuvent plus diverger : meme code, memes chiffres.
|
||||
export async function donneesNuit(client, jour = new Date()) {
|
||||
const fin = new Date(jour); fin.setHours(10, 0, 0, 0);
|
||||
const debut = new Date(fin); debut.setDate(debut.getDate() - 1); debut.setHours(18, 0, 0, 0);
|
||||
const q = (sql, v = []) => client.query(sql, v);
|
||||
const p = []; // paragraphes
|
||||
const dire = (...l) => p.push(l.join(' '));
|
||||
|
||||
const dateLue = fin.toLocaleDateString('fr-FR',
|
||||
{ timeZone: 'Europe/Paris', weekday: 'long', day: 'numeric', month: 'long' });
|
||||
dire(`Rapport de la nuit du ${dateLue}.`);
|
||||
|
||||
// ------------------------------------------------ le reseau
|
||||
const { rows: bascules } = await q(
|
||||
@@ -101,14 +101,6 @@ export async function texteRapport(client, jour = new Date()) {
|
||||
(SELECT count(*) FROM g WHERE v < 0) AS points_negatifs,
|
||||
(SELECT min(v) FROM g) AS pire_injection`, [debut, fin]);
|
||||
|
||||
if (!fermetures.length) {
|
||||
dire('Le contacteur est reste ouvert toute la nuit : aucune energie achetee au reseau.');
|
||||
} else {
|
||||
dire(`Le reseau a ete ferme ${pluriel(fermetures.length, 'fois', 'fois')},`,
|
||||
`a ${fermetures.map((f) => heure(f.ts)).join(', puis a ')}.`,
|
||||
`Au total ${n1(e.importe)} kilowattheures importes.`);
|
||||
}
|
||||
|
||||
// ------------------------------------------------ la batterie
|
||||
const { rows: [b] } = await q(
|
||||
`SELECT max(valeur) AS haut, min(valeur) AS bas,
|
||||
@@ -125,6 +117,64 @@ export async function texteRapport(client, jour = new Date()) {
|
||||
FROM mesure WHERE cle = 'sensor.victron_battery_soc' AND ts >= $1 AND ts < $2`,
|
||||
[debut, fin]);
|
||||
|
||||
// ------------------------------------------------ la voiture
|
||||
const { rows: sessions } = await q(
|
||||
`SELECT debut, fin, terminee, vehicule, energie_kwh, energie_compteur_kwh, energie_station_kwh,
|
||||
energie_reseau_kwh, source_mode
|
||||
FROM session_charge WHERE debut >= $1 AND debut < $2 ORDER BY debut`, [debut, fin]);
|
||||
// ------------------------------------------------ ce qu'on a deploye le 6
|
||||
const { rows: [d] } = await q(
|
||||
`SELECT count(*) FILTER (WHERE etat = 'hold_on_charge_encore_elevee') AS maintiens,
|
||||
count(*) FILTER (WHERE etat LIKE 'turn_on_charge%') AS protections
|
||||
FROM decision WHERE ts >= $1 AND ts < $2`, [debut, fin]);
|
||||
const { rows: [h] } = await q(
|
||||
`SELECT count(*) AS n FROM evenement
|
||||
WHERE ts >= $1 AND ts < $2 AND resume LIKE '%charge maison%'`, [debut, fin]);
|
||||
|
||||
// ------------------------------------------------ prevision solaire
|
||||
// Une panne de prevision est invisible autrement : les nodes n8n portent
|
||||
// `onError: continueRegularOutput`, donc les executions restent « success »
|
||||
// meme quand l'appel echoue, et quand Home Assistant est lui aussi
|
||||
// injoignable le message d'echec ne peut meme pas etre publie. Le service
|
||||
// d'historisation, lui, tourne sur l'autre NAS : c'est le seul temoin qui
|
||||
// survit. Releve de la nuit du 07 au 08/09 : 4 h 45 sans une seule
|
||||
// publication, et personne ne l'a su avant le lendemain.
|
||||
const { rows: [prev] } = await q(
|
||||
`SELECT round(EXTRACT(EPOCH FROM max(ecart)) / 60) AS trou
|
||||
FROM (SELECT publie_a - lag(publie_a) OVER (ORDER BY publie_a) AS ecart
|
||||
FROM prevision WHERE publie_a >= $1 AND publie_a < $2) t`,
|
||||
[debut, fin]);
|
||||
const trouMin = Number(prev?.trou ?? 0);
|
||||
// ------------------------------------------------ anomalies
|
||||
const { rows: soucis } = await q(
|
||||
`SELECT resume FROM evenement WHERE ts >= $1 AND ts < $2
|
||||
AND (resume ILIKE '%injoignable%' OR resume ILIKE '%redemarr%'
|
||||
OR resume ILIKE '%erreur%' OR resume ILIKE '%echec%')
|
||||
ORDER BY ts`, [debut, fin]);
|
||||
|
||||
const dateLue = fin.toLocaleDateString('fr-FR',
|
||||
{ timeZone: 'Europe/Paris', weekday: 'long', day: 'numeric', month: 'long' });
|
||||
return { debut, fin, dateLue, fermetures, e, b, sessions, d, h, trouMin, soucis };
|
||||
}
|
||||
|
||||
export async function texteRapport(client, jour = new Date()) {
|
||||
return texteDepuis(await donneesNuit(client, jour));
|
||||
}
|
||||
|
||||
export function texteDepuis({ fermetures, e, b, sessions, trouMin, soucis, dateLue }) {
|
||||
const p = []; // paragraphes
|
||||
const dire = (...l) => p.push(l.join(' '));
|
||||
dire(`Rapport de la nuit du ${dateLue}.`);
|
||||
|
||||
if (!fermetures.length) {
|
||||
dire('Le contacteur est reste ouvert toute la nuit : aucune energie achetee au reseau.');
|
||||
} else {
|
||||
dire(`Le reseau a ete ferme ${pluriel(fermetures.length, 'fois', 'fois')},`,
|
||||
`a ${fermetures.map((f) => heure(f.ts)).join(', puis a ')}.`,
|
||||
`Au total ${n1(e.importe)} kilowattheures importes.`);
|
||||
}
|
||||
|
||||
|
||||
if (b && b.bas !== null) {
|
||||
dire(`La batterie est partie de ${n0(b.depart ?? b.haut)} pour cent hier soir,`,
|
||||
`elle est descendue au plus bas a ${n0(b.bas)} pour cent vers ${heure(b.heure_bas)},`,
|
||||
@@ -135,10 +185,7 @@ export async function texteRapport(client, jour = new Date()) {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------ la voiture
|
||||
const { rows: sessions } = await q(
|
||||
`SELECT debut, vehicule, energie_kwh, energie_compteur_kwh, energie_station_kwh
|
||||
FROM session_charge WHERE debut >= $1 AND debut < $2 ORDER BY debut`, [debut, fin]);
|
||||
|
||||
if (!sessions.length) {
|
||||
dire('Aucun vehicule n\'a charge cette nuit.');
|
||||
} else {
|
||||
@@ -149,14 +196,6 @@ export async function texteRapport(client, jour = new Date()) {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------ ce qu'on a deploye le 6
|
||||
const { rows: [d] } = await q(
|
||||
`SELECT count(*) FILTER (WHERE etat = 'hold_on_charge_encore_elevee') AS maintiens,
|
||||
count(*) FILTER (WHERE etat LIKE 'turn_on_charge%') AS protections
|
||||
FROM decision WHERE ts >= $1 AND ts < $2`, [debut, fin]);
|
||||
const { rows: [h] } = await q(
|
||||
`SELECT count(*) AS n FROM evenement
|
||||
WHERE ts >= $1 AND ts < $2 AND resume LIKE '%charge maison%'`, [debut, fin]);
|
||||
|
||||
// ⚠️ Ne jamais qualifier l'injection de negligeable sans regarder la pointe :
|
||||
// le 6 septembre, une pointe a 2 819 watts pendant deux minutes n'avait rien
|
||||
@@ -174,20 +213,7 @@ export async function texteRapport(client, jour = new Date()) {
|
||||
: 'C\'est du bruit de regulation, sans consequence.');
|
||||
}
|
||||
|
||||
// ------------------------------------------------ prevision solaire
|
||||
// Une panne de prevision est invisible autrement : les nodes n8n portent
|
||||
// `onError: continueRegularOutput`, donc les executions restent « success »
|
||||
// meme quand l'appel echoue, et quand Home Assistant est lui aussi
|
||||
// injoignable le message d'echec ne peut meme pas etre publie. Le service
|
||||
// d'historisation, lui, tourne sur l'autre NAS : c'est le seul temoin qui
|
||||
// survit. Releve de la nuit du 07 au 08/09 : 4 h 45 sans une seule
|
||||
// publication, et personne ne l'a su avant le lendemain.
|
||||
const { rows: [prev] } = await q(
|
||||
`SELECT round(EXTRACT(EPOCH FROM max(ecart)) / 60) AS trou
|
||||
FROM (SELECT publie_a - lag(publie_a) OVER (ORDER BY publie_a) AS ecart
|
||||
FROM prevision WHERE publie_a >= $1 AND publie_a < $2) t`,
|
||||
[debut, fin]);
|
||||
const trouMin = Number(prev?.trou ?? 0);
|
||||
|
||||
if (trouMin >= 120) {
|
||||
const h = Math.floor(trouMin / 60);
|
||||
const m = trouMin % 60;
|
||||
@@ -196,12 +222,7 @@ export async function texteRapport(client, jour = new Date()) {
|
||||
+ ' vieillissante pendant ce temps, et la cible de la nuit a pu retomber sur la regle des paliers.');
|
||||
}
|
||||
|
||||
// ------------------------------------------------ anomalies
|
||||
const { rows: soucis } = await q(
|
||||
`SELECT resume FROM evenement WHERE ts >= $1 AND ts < $2
|
||||
AND (resume ILIKE '%injoignable%' OR resume ILIKE '%redemarr%'
|
||||
OR resume ILIKE '%erreur%' OR resume ILIKE '%echec%')
|
||||
ORDER BY ts`, [debut, fin]);
|
||||
|
||||
if (soucis.length) {
|
||||
dire(`A signaler : ${soucis.map((s) => s.resume.toLowerCase()).join('. ')}.`);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user