Le parc remonte au-dessus de 93 %, et la nuit se raconte par courriel
Le plafond a 93 % n etait pas une protection : le BMS autorisait 53,2 V, nous imposions 51,3, et la cellule la plus haute du pack recent etait 57 mV sous la premiere alarme. Le garde-fou datait des cellules faibles du pack ancien, hors service depuis hier — il protegeait un parc qui n est plus la, au prix de 0,68 kWh, soit 12 % de la reserve utile. Le helper de tension et ses paliers ne sont lus par personne : les valeurs sont une table codee en dur dans l Auto Charge Manager, qui entre au passage dans le depot. Seul le profil de la tranche 90-95 % passe a 52,4 V, soit 3,493 V par cellule, sous la tension nominale que Pylontech annonce. Verifie dans la minute : parc de 51,12 a 52,33 V, courant de -5,3 a +16,1 A, aucune alarme. Et le rapport de nuit part par courriel a 6 h 47. Il ne vit pas dans une session Claude, qui s arrete avec l ecran, mais dans le service d historisation, seul composant allume en permanence — il n y avait rien a surveiller, tout est deja enregistre, il manquait une lecture. Le texte est ecrit pour etre entendu : pas de tableau, des phrases courtes, des virgules decimales. Canal : le webhook n8n qui existait deja, teste de bout en bout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
@@ -29,6 +29,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import pg from 'pg';
|
||||
import { texteRapport, envoyer } from './rapport-nuit.mjs';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const ONCE = process.argv.includes('--once');
|
||||
@@ -650,6 +651,51 @@ async function ecrireSession(sql, client, st) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Le rapport de nuit, envoye par courriel une fois par jour.
|
||||
//
|
||||
// Pourquoi ici plutot que dans un cron ou dans une session interactive : c'est
|
||||
// ce service qui est allume en permanence. Une session Claude s'arrete des que
|
||||
// l'ecran se ferme, et le planificateur qu'elle porte s'arrete avec elle ; un
|
||||
// cron DSM tournerait sur un hote qui n'a meme pas node. Le service, lui, a
|
||||
// deja la base, le depot et une boucle a la minute.
|
||||
//
|
||||
// L'idempotence ne s'appuie sur aucun fichier d'etat : elle se lit dans la
|
||||
// table `evenement`, qui est de toute facon la trace que l'on veut garder.
|
||||
// Redemarrer le conteneur ne renvoie donc pas le rapport.
|
||||
// ---------------------------------------------------------------------
|
||||
const RAPPORT_HEURE = 6;
|
||||
const RAPPORT_MINUTE = 47;
|
||||
|
||||
async function envoyerRapportDuMatin(sql, client) {
|
||||
if (!client) return 0;
|
||||
const parts = Object.fromEntries(
|
||||
new Intl.DateTimeFormat('fr-FR', {
|
||||
timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', hour12: false,
|
||||
}).formatToParts(new Date()).filter((x) => x.type !== 'literal')
|
||||
.map((x) => [x.type, Number(x.value)]));
|
||||
|
||||
const minutesDuJour = parts.hour * 60 + parts.minute;
|
||||
if (minutesDuJour < RAPPORT_HEURE * 60 + RAPPORT_MINUTE) return 0;
|
||||
// Fenetre de rattrapage : si le service etait arrete a 6 h 47, il envoie au
|
||||
// premier cycle suivant, mais pas au milieu de l'apres-midi.
|
||||
if (minutesDuJour > 10 * 60) return 0;
|
||||
|
||||
const jour = `${parts.year}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')}`;
|
||||
const { rowCount } = await client.query(
|
||||
`SELECT 1 FROM evenement WHERE genre = 'rapport'
|
||||
AND ts >= $1::date AND ts < $1::date + interval '1 day' LIMIT 1`, [jour]);
|
||||
if (rowCount) return 0;
|
||||
|
||||
const texte = await texteRapport(client, new Date());
|
||||
await envoyer(texte, 'EVCS - rapport de la nuit');
|
||||
await journaliser(sql, 'rapport', 'Rapport de nuit envoye par courriel',
|
||||
{ jour, caracteres: texte.length });
|
||||
console.log(` rapport de nuit envoye (${texte.length} caracteres)`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Verse dans la base les relevés de cellule deposes par le poste qui porte le
|
||||
// cable serie. Celui-ci n'ecrit qu'un CSV dans le depot partage : il n'a donc
|
||||
@@ -717,12 +763,15 @@ async function cycle(client) {
|
||||
// Ingestion des relevés de cellule deposes par le poste au cable serie.
|
||||
// Hors essai a blanc : l'insertion est massive et idempotente, la simuler
|
||||
// n'apprendrait rien.
|
||||
rapport: DRY ? 0 : await envoyerRapportDuMatin(sql, client).catch((e) => {
|
||||
console.error('rapport de nuit :', e.message); return 0;
|
||||
}),
|
||||
cellules: DRY ? 0 : await ingererCellules(client).catch((e) => {
|
||||
console.error(`ingestion cellules en echec : ${e.message}`);
|
||||
return 0;
|
||||
}),
|
||||
};
|
||||
const total = r.mesures + r.bilan + r.decision + r.prevision + r.evenement + r.session + r.cellules;
|
||||
const total = r.mesures + r.bilan + r.decision + r.prevision + r.evenement + r.session + r.cellules + (r.rapport || 0);
|
||||
if (total > 0 || ONCE) {
|
||||
console.log(
|
||||
`${new Date().toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })} ` +
|
||||
|
||||
Reference in New Issue
Block a user