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:
19
HANDOFF.md
19
HANDOFF.md
@@ -618,5 +618,22 @@ fait tomber les tests (mutation verifiee). Tests 32/32.
|
|||||||
code doit donc etre teste sur TOUT le domaine d'entree (ici les sept libelles),
|
code doit donc etre teste sur TOUT le domaine d'entree (ici les sept libelles),
|
||||||
pas seulement sur les cas du banc.
|
pas seulement sur les cas du banc.
|
||||||
|
|
||||||
|
⚠️ **Correction du diagnostic ci-dessus** : ce defaut etait reel, mais ce n'est
|
||||||
|
PAS lui qui a produit la reponse du matin. Transcript recupere par SPARK (vocal,
|
||||||
|
~11:49, « bilan energetique de la nuit ») : « la JEEP a pris 0,56 kWh (100 %
|
||||||
|
batterie, zero reseau), elle est arrivee a la cible de 35 % et s'est arretee vers
|
||||||
|
4 h ». FABRICATION : l'outil de l'agent ne lit que la session EN COURS (aucune a
|
||||||
|
11:49), et la voiture n'a pas de SOC — les « 35 % » sont la cible de la batterie
|
||||||
|
maison. J'avais conclu « l'agent a relaye fidelement » sans avoir vu sa reponse.
|
||||||
|
Correctif : `sensor.evcs_bilan_nuit` (etat `complet` / `en_cours`), calcule par
|
||||||
|
`donneesNuit()` de `tools/rapport-nuit.mjs` — le MEME code que le courriel de
|
||||||
|
07:30, extrait sans changer le texte (identique caractere pour caractere sur
|
||||||
|
cinq nuits : 25, 24, 20, 10 et 06/09). Attributs : vehicules (kWh, dont reseau,
|
||||||
|
debut, fin), fermetures reseau et import, SOC batterie maison, `soc_voiture` =
|
||||||
|
« aucun capteur », et `texte_rapport` (le rapport lisible a voix haute). Publie
|
||||||
|
au premier cycle puis toutes les 5 min. Tests 37/37.
|
||||||
|
SPARK active aussi la journalisation des entrees/sorties d'outils (retention
|
||||||
|
courte, jetons masques) : ce matin on n'a pas pu voir ce que l'agent avait lu.
|
||||||
|
|
||||||
---
|
---
|
||||||
*Document mis a jour le : 25/09/2026 — capteur de charge corrige (origine explicite par libelle), incident du matin*
|
*Document mis a jour le : 25/09/2026 — bilan de nuit publie pour l'agent (source unique avec le rapport du matin), incident de 11:49*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Les cas PV reprennent les gammes du banc OpenClaw (HANDOFF §77) : G3, G4, G5, G7.
|
// Les cas PV reprennent les gammes du banc OpenClaw (HANDOFF §77) : G3, G4, G5, G7.
|
||||||
// node tests/diagnostics-agent/run.mjs
|
// node tests/diagnostics-agent/run.mjs
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, MPPT_MODES, ORIGINE_PAR_SOURCE } from '../../tools/diagnostics-agent.mjs';
|
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, MPPT_MODES, ORIGINE_PAR_SOURCE } from '../../tools/diagnostics-agent.mjs';
|
||||||
|
|
||||||
const LIM = 'voltage_current_limited', ACT = 'mppt_active';
|
const LIM = 'voltage_current_limited', ACT = 'mppt_active';
|
||||||
function st({ pv, conso, reseau, soc = 80, courant = 0, edf = 'off', modes }) {
|
function st({ pv, conso, reseau, soc = 80, courant = 0, edf = 'off', modes }) {
|
||||||
@@ -146,6 +146,37 @@ const okLib = libelles.length >= 6 && nonCouverts.length === 0;
|
|||||||
if (!okLib) ko++;
|
if (!okLib) ko++;
|
||||||
console.log(`${okLib ? 'OK ' : 'KO '} les ${libelles.length} libelles de source du controleur ont une origine${okLib ? '' : ` (manquent : ${nonCouverts.join(', ') || 'table introuvable'})`}`);
|
console.log(`${okLib ? 'OK ' : 'KO '} les ${libelles.length} libelles de source du controleur ont une origine${okLib ? '' : ` (manquent : ${nonCouverts.join(', ') || 'table introuvable'})`}`);
|
||||||
|
|
||||||
const n = cas.length + cel.length + prod.length + charge.length + 1;
|
// --- Bilan de la nuit : la nuit reelle du 24 au 25/09 (incident de 11:49) ---
|
||||||
|
const nuit = {
|
||||||
|
debut: new Date('2026-09-24T16:00:00Z'), fin: new Date('2026-09-25T08:00:00Z'),
|
||||||
|
dateLue: 'vendredi 25 septembre',
|
||||||
|
fermetures: [{ ts: new Date('2026-09-25T04:01:00Z') }],
|
||||||
|
e: { importe: 2.71, voiture: 7.96 },
|
||||||
|
b: { depart: 97, bas: 31, heure_bas: new Date('2026-09-25T07:16:00Z'), maintenant: 32 },
|
||||||
|
sessions: [{ debut: new Date('2026-09-24T23:25:00Z'), fin: new Date('2026-09-25T04:42:00Z'), terminee: true,
|
||||||
|
vehicule: 'JEEP', energie_kwh: 7.96, energie_compteur_kwh: 7.9, energie_reseau_kwh: 2.2, source_mode: 'Assistance batterie nuit' }],
|
||||||
|
};
|
||||||
|
const bn = [
|
||||||
|
['INCIDENT 11:49 : la session terminee est bien visible, 7,9 kWh dont 2,2 reseau, 01:25 -> 06:42',
|
||||||
|
bilanNuit(nuit, 'x', new Date('2026-09-25T09:49:00Z')),
|
||||||
|
(r) => r.etat === 'complet' && r.attributs.vehicules[0].energie_kwh === 7.9 && r.attributs.vehicules[0].dont_reseau_kwh === 2.2
|
||||||
|
&& r.attributs.vehicules[0].debut === '01:25' && r.attributs.vehicules[0].fin === '06:42'],
|
||||||
|
['aucun SOC voiture publie ; le 35 % n existe nulle part', bilanNuit(nuit, 'x', new Date('2026-09-25T09:49:00Z')),
|
||||||
|
(r) => !JSON.stringify(r).includes('35') && /aucun capteur/.test(r.attributs.soc_voiture)],
|
||||||
|
['reseau : fermeture 06:01, import 2,7', bilanNuit(nuit, 'x', new Date('2026-09-25T09:49:00Z')),
|
||||||
|
(r) => r.attributs.reseau_fermetures.join() === '06:01' && r.attributs.reseau_importe_kwh === 2.7],
|
||||||
|
['avant 10:00 -> en_cours, session ouverte sans heure de fin', bilanNuit({ ...nuit,
|
||||||
|
sessions: [{ ...nuit.sessions[0], terminee: false }] }, 'x', new Date('2026-09-25T03:00:00Z')),
|
||||||
|
(r) => r.etat === 'en_cours' && r.attributs.vehicules[0].fin === null],
|
||||||
|
['nuit sans charge -> liste vide', bilanNuit({ ...nuit, sessions: [] }, 'x', new Date('2026-09-25T09:49:00Z')),
|
||||||
|
(r) => r.attributs.vehicules.length === 0],
|
||||||
|
];
|
||||||
|
for (const [nom, r, test] of bn) {
|
||||||
|
const ok = test(r);
|
||||||
|
if (!ok) { ko++; console.log(JSON.stringify(r)); }
|
||||||
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = cas.length + cel.length + prod.length + charge.length + 1 + bn.length;
|
||||||
console.log(`\n${n - ko}/${n}`);
|
console.log(`\n${n - ko}/${n}`);
|
||||||
process.exit(ko ? 1 : 0);
|
process.exit(ko ? 1 : 0);
|
||||||
|
|||||||
@@ -266,3 +266,49 @@ export function chargeSolaire(st) {
|
|||||||
return r('en_attente',
|
return r('en_attente',
|
||||||
`Voiture branchee mais pas en charge. Le controleur dit : ${raison || 'sans raison publiee'} (source : ${source}).`);
|
`Voiture branchee mais pas en charge. Le controleur dit : ${raison || 'sans raison publiee'} (source : ${source}).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Bilan de la nuit — les donnees du rapport du matin, pour l'agent
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Incident du 25/09 ~11:49 : a « bilan energetique de la nuit », l'agent, qui
|
||||||
|
// n'avait de source que pour la session EN COURS (aucune a cette heure), a
|
||||||
|
// invente « 0,56 kWh, zero reseau, arrivee a la cible de 35 %, arretee vers
|
||||||
|
// 4 h ». Realite : JEEP 7,9 kWh dont 2,2 reseau, 01:25 -> 06:42. Les « 35 % »
|
||||||
|
// etaient la cible de la BATTERIE MAISON ; la voiture n'a aucun capteur de SOC.
|
||||||
|
// On publie donc ce que calcule tools/rapport-nuit.mjs (donneesNuit), le meme
|
||||||
|
// code que le courriel de 07:30 : les deux ne peuvent pas diverger.
|
||||||
|
// La nuit decrite va de 18:00 la veille a 10:00 ce matin ; avant 10:00 elle est
|
||||||
|
// `en_cours` (fenetre plafonnee a maintenant, rien d'extrapole).
|
||||||
|
const hParis = (t) => (t ? new Date(t).toLocaleTimeString('fr-FR',
|
||||||
|
{ timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' }) : null);
|
||||||
|
const r1 = (x) => (x === null || x === undefined || !Number.isFinite(Number(x)) ? null : Math.round(Number(x) * 10) / 10);
|
||||||
|
|
||||||
|
export function bilanNuit(d, texte, maintenant = new Date()) {
|
||||||
|
const enCours = maintenant < d.fin;
|
||||||
|
const vehicules = (d.sessions || []).map((s) => ({
|
||||||
|
vehicule: s.vehicule ?? 'non identifie',
|
||||||
|
debut: hParis(s.debut),
|
||||||
|
fin: s.terminee ? hParis(s.fin) : null,
|
||||||
|
terminee: !!s.terminee,
|
||||||
|
energie_kwh: r1(s.energie_compteur_kwh ?? s.energie_kwh),
|
||||||
|
dont_reseau_kwh: r1(s.energie_reseau_kwh),
|
||||||
|
source_controleur: s.source_mode ?? null,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
etat: enCours ? 'en_cours' : 'complet',
|
||||||
|
attributs: {
|
||||||
|
nuit_du: d.dateLue,
|
||||||
|
fenetre: `${hParis(d.debut)} la veille -> ${hParis(d.fin)}${enCours ? ' (en cours, arretee a maintenant)' : ''}`,
|
||||||
|
reseau_fermetures: (d.fermetures || []).map((f) => hParis(f.ts)),
|
||||||
|
reseau_importe_kwh: r1(d.e?.importe),
|
||||||
|
voiture_totale_kwh: r1(d.e?.voiture),
|
||||||
|
vehicules,
|
||||||
|
batterie_maison_depart_pct: r1(d.b?.depart ?? d.b?.haut),
|
||||||
|
batterie_maison_plus_bas_pct: r1(d.b?.bas),
|
||||||
|
batterie_maison_heure_plus_bas: hParis(d.b?.heure_bas),
|
||||||
|
batterie_maison_maintenant_pct: r1(d.b?.maintenant),
|
||||||
|
soc_voiture: 'aucun capteur : ne jamais en annoncer un',
|
||||||
|
texte_rapport: texte,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
import { texteRapport, envoyer } from './rapport-nuit.mjs';
|
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
|
||||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire } from './diagnostics-agent.mjs';
|
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit } from './diagnostics-agent.mjs';
|
||||||
|
|
||||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
const ONCE = process.argv.includes('--once');
|
const ONCE = process.argv.includes('--once');
|
||||||
@@ -1848,6 +1848,20 @@ async function publierDiagnosticPv(st) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bilan de la nuit pour l'agent vocal : les donnees du rapport du matin (meme
|
||||||
|
// code, donc memes chiffres que le courriel). Toutes les 5 min suffit.
|
||||||
|
let bilanNuitPublie = false; // publie des le premier cycle, puis toutes les 5 min
|
||||||
|
async function publierBilanNuit(client) {
|
||||||
|
if (DRY || !client) return 0;
|
||||||
|
const d = await donneesNuit(client);
|
||||||
|
const bn = bilanNuit(d, texteDepuis(d));
|
||||||
|
await publierCapteur('sensor.evcs_bilan_nuit', bn.etat, {
|
||||||
|
friendly_name: 'EVCS — bilan de la derniere nuit', icon: 'mdi:weather-night', ...bn.attributs,
|
||||||
|
calcule_a: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
async function publierCellulesHelpers(client) {
|
async function publierCellulesHelpers(client) {
|
||||||
if (DRY) return 0;
|
if (DRY) return 0;
|
||||||
// Base injoignable : on publie « muet » plutot que de laisser au capteur son
|
// Base injoignable : on publie « muet » plutot que de laisser au capteur son
|
||||||
@@ -1926,6 +1940,9 @@ async function cycle(client) {
|
|||||||
: await publierPrevisionContreReel(client).catch((e) => {
|
: await publierPrevisionContreReel(client).catch((e) => {
|
||||||
console.error(`prevision contre reel : ${e.message}`); return 0;
|
console.error(`prevision contre reel : ${e.message}`); return 0;
|
||||||
}),
|
}),
|
||||||
|
bilanNuit: (DRY || (bilanNuitPublie && new Date().getMinutes() % 5 !== 1)) ? 0 : await publierBilanNuit(client).then((n) => { bilanNuitPublie = true; return n; }).catch((e) => {
|
||||||
|
console.error(`bilan de nuit : ${e.message}`); return 0;
|
||||||
|
}),
|
||||||
rapport: DRY ? 0 : await envoyerRapportDuMatin(sql, client).catch((e) => {
|
rapport: DRY ? 0 : await envoyerRapportDuMatin(sql, client).catch((e) => {
|
||||||
console.error('rapport de nuit :', e.message); return 0;
|
console.error('rapport de nuit :', e.message); return 0;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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
|
// 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.
|
// 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 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 debut = new Date(fin); debut.setDate(debut.getDate() - 1); debut.setHours(18, 0, 0, 0);
|
||||||
const q = (sql, v = []) => client.query(sql, v);
|
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
|
// ------------------------------------------------ le reseau
|
||||||
const { rows: bascules } = await q(
|
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 count(*) FROM g WHERE v < 0) AS points_negatifs,
|
||||||
(SELECT min(v) FROM g) AS pire_injection`, [debut, fin]);
|
(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
|
// ------------------------------------------------ la batterie
|
||||||
const { rows: [b] } = await q(
|
const { rows: [b] } = await q(
|
||||||
`SELECT max(valeur) AS haut, min(valeur) AS bas,
|
`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`,
|
FROM mesure WHERE cle = 'sensor.victron_battery_soc' AND ts >= $1 AND ts < $2`,
|
||||||
[debut, fin]);
|
[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) {
|
if (b && b.bas !== null) {
|
||||||
dire(`La batterie est partie de ${n0(b.depart ?? b.haut)} pour cent hier soir,`,
|
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)},`,
|
`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) {
|
if (!sessions.length) {
|
||||||
dire('Aucun vehicule n\'a charge cette nuit.');
|
dire('Aucun vehicule n\'a charge cette nuit.');
|
||||||
} else {
|
} 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 :
|
// ⚠️ 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
|
// 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.');
|
: '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) {
|
if (trouMin >= 120) {
|
||||||
const h = Math.floor(trouMin / 60);
|
const h = Math.floor(trouMin / 60);
|
||||||
const m = 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.');
|
+ ' 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) {
|
if (soucis.length) {
|
||||||
dire(`A signaler : ${soucis.map((s) => s.resume.toLowerCase()).join('. ')}.`);
|
dire(`A signaler : ${soucis.map((s) => s.resume.toLowerCase()).join('. ')}.`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user