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' })} ` +
|
||||
|
||||
60
tools/patch-20260906c-tension-de-charge-52v4.mjs
Normal file
60
tools/patch-20260906c-tension-de-charge-52v4.mjs
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env node
|
||||
// Le parc plafonnait a 93 % de SOC, et ce n'etait pas une protection.
|
||||
//
|
||||
// Mesure du 06/09 a 15:01, quatre modules du pack recent :
|
||||
// ce que le BMS AUTORISE : 53,2 V (`maximum_allowed_charging_voltage`)
|
||||
// ce que nous IMPOSIONS : 51,3 V, soit 3,42 V par cellule
|
||||
// cellules : 3 386 a 3 423 mV, ecart 25-30 mV par module
|
||||
// premiere alarme haute : 3 480 mV — la cellule la plus haute est 57 mV en dessous
|
||||
// Aucune alarme, le BMS autorisait encore 40 A de charge : c'est la consigne de
|
||||
// tension qui plafonnait le SOC, pas la batterie.
|
||||
//
|
||||
// Origine : la gestion « tension de cellule elevee » posee pour proteger les
|
||||
// cellules faibles du pack ANCIEN (cellule 12 du module ...0322 a 3 649 mV,
|
||||
// cellule 15 du ...0324). Ces modules sont hors service depuis le 05/09. Le
|
||||
// garde-fou protegeait un parc qui n'est plus la, au prix du haut de la
|
||||
// reserve : de 93 a ~100 % sur quatre modules, environ 0,68 kWh, soit 12 % de
|
||||
// l'energie utile — precisement ce qui manque en fin de nuit.
|
||||
//
|
||||
// ⚠️ LE HELPER `input_number.victron_battery_charge_voltage_normal_v` (51,3) ET
|
||||
// SES PALIERS hcv_step1/step2 NE SONT LUS PAR PERSONNE. Verifie : le nom
|
||||
// n'apparait nulle part dans l'Auto Charge Manager. Les tensions sont une table
|
||||
// codee en dur dans le node « Compute Charge Targets ». Meme famille que le
|
||||
// toggle `allow_grid_hc_night`, parse mais cable nulle part jusqu'a P7.
|
||||
// La VRAIE protection est le BMS lui-meme, qui a demontre le 05/09 une reaction
|
||||
// en CINQ SECONDES : cellule au-dessus de 3 550 mV -> plafond de courant
|
||||
// effondre. Elle reste entiere.
|
||||
//
|
||||
// 52,4 V = 3,493 V par cellule, SOUS la tension de charge nominale que
|
||||
// Pylontech annonce pour l'US2000C (52,5 V) et bien sous les 53,2 V que ce BMS
|
||||
// autorise. Avec 30 mV d'ecart entre cellules, la plus haute atteindra ~3,51 V,
|
||||
// encore 40 mV sous le seuil qui declenche la reduction de courant.
|
||||
//
|
||||
// Seul `pv_topoff` est touche : c'est le profil de la tranche 90-95 % de SOC,
|
||||
// exactement la ou ca plafonnait. `pv_boost` (SOC < 80) et `pv_mid` (80-90)
|
||||
// sont limites par le courant, pas par la tension. `pv_hold` (SOC >= 95, 50,6 V)
|
||||
// n'est PAS touche : redescendre une fois le haut atteint est le bon
|
||||
// comportement pour du LiFePO4, on ne veut pas y sejourner.
|
||||
//
|
||||
// node tools/patch-20260906c-tension-de-charge-52v4.mjs
|
||||
|
||||
import fs from 'node:fs';
|
||||
|
||||
const F = 'workflows/victron-auto-charge-manager-v1.2.json';
|
||||
const wf = JSON.parse(fs.readFileSync(F, 'utf8'));
|
||||
const calc = wf.nodes.find((n) => n.name === 'Compute Charge Targets');
|
||||
if (!calc) throw new Error('noeud "Compute Charge Targets" introuvable');
|
||||
|
||||
let c = calc.parameters.jsCode;
|
||||
const avant = ' pv_topoff: { current_a: 80, voltage_v: 51.3 },';
|
||||
const apres = ' pv_topoff: { current_a: 80, voltage_v: 52.4 },';
|
||||
|
||||
if (c.includes(apres)) {
|
||||
console.log(' deja fait');
|
||||
} else {
|
||||
if (!c.includes(avant)) throw new Error('ancre pv_topoff introuvable');
|
||||
c = c.replace(avant, apres);
|
||||
calc.parameters.jsCode = c;
|
||||
fs.writeFileSync(F, JSON.stringify(wf, null, 2) + '\n');
|
||||
console.log(' pv_topoff : 51.3 -> 52.4 V (3,493 V/cellule)');
|
||||
}
|
||||
220
tools/rapport-nuit.mjs
Normal file
220
tools/rapport-nuit.mjs
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env node
|
||||
// =====================================================================
|
||||
// RAPPORT DE NUIT — texte simple, destine a etre LU A VOIX HAUTE
|
||||
// =====================================================================
|
||||
// Le proprietaire le recoit par courriel le matin, souvent en voiture : il se
|
||||
// le fait lire. D'ou les contraintes de forme, qui ne sont pas cosmetiques —
|
||||
// pas de tableau, pas de tiret de liste, pas d'abreviation, des phrases
|
||||
// courtes, et les nombres ecrits comme on les prononce (« 4,2 kilowattheures »
|
||||
// et non « 4.2 kWh »).
|
||||
//
|
||||
// Il n'y a RIEN a surveiller en direct : le service d'historisation tourne
|
||||
// toutes les soixante secondes sur une machine allumee en permanence et
|
||||
// enregistre deja tout. Ce fichier ne surveille pas, il LIT — et c'est
|
||||
// pourquoi il vit dans le service plutot que dans une session interactive,
|
||||
// qui, elle, s'arrete des que l'ecran se ferme.
|
||||
//
|
||||
// Usage :
|
||||
// node tools/rapport-nuit.mjs # affiche le rapport
|
||||
// node tools/rapport-nuit.mjs --envoyer # l'envoie par courriel
|
||||
// node tools/rapport-nuit.mjs --jour 2026-09-05
|
||||
// =====================================================================
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import pg from 'pg';
|
||||
|
||||
const RACINE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const WEBHOOK = 'https://n8n.airtria.pro/webhook/solar-email';
|
||||
const DESTINATAIRE = 'tallagrand.thierry@gmail.com';
|
||||
|
||||
function envFichier() {
|
||||
const out = {};
|
||||
for (const f of [path.join(RACINE, '.env'), '/app/.env']) {
|
||||
if (!fs.existsSync(f)) continue;
|
||||
for (const l of fs.readFileSync(f, 'utf8').split('\n')) {
|
||||
const i = l.indexOf('=');
|
||||
if (i > 0 && !l.trimStart().startsWith('#')) out[l.slice(0, i).trim()] = l.slice(i + 1).trim();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// « 4.2 » -> « 4,2 » : une virgule se lit, un point ne se lit pas.
|
||||
const n1 = (v) => (v === null || v === undefined || !Number.isFinite(Number(v)))
|
||||
? null : Number(v).toFixed(1).replace('.', ',');
|
||||
const n2 = (v) => (v === null || v === undefined || !Number.isFinite(Number(v)))
|
||||
? null : Number(v).toFixed(2).replace('.', ',');
|
||||
const n0 = (v) => (v === null || v === undefined || !Number.isFinite(Number(v)))
|
||||
? null : Math.round(Number(v)).toString();
|
||||
|
||||
const heure = (d) => new Date(d).toLocaleTimeString('fr-FR',
|
||||
{ timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' }).replace(':', ' h ');
|
||||
|
||||
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()) {
|
||||
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(
|
||||
`SELECT ts, detail->>'apres' AS apres FROM evenement
|
||||
WHERE genre = 'reseau' AND resume LIKE 'Contacteur EDF%'
|
||||
AND ts >= $1 AND ts < $2 ORDER BY ts`, [debut, fin]);
|
||||
const fermetures = bascules.filter((b) => b.apres === 'on');
|
||||
|
||||
const { rows: [e] } = await q(
|
||||
`WITH mn AS (SELECT generate_series($1::timestamptz, $2::timestamptz, interval '1 minute') t),
|
||||
j AS (SELECT mn.t,
|
||||
(SELECT valeur FROM mesure m WHERE m.cle = 'sensor.victron_grid_power_l1'
|
||||
AND m.ts <= mn.t ORDER BY m.ts DESC LIMIT 1) g,
|
||||
(SELECT valeur FROM mesure m WHERE m.cle = 'sensor.victron_evcs_ac_power'
|
||||
AND m.ts <= mn.t ORDER BY m.ts DESC LIMIT 1) v,
|
||||
(SELECT valeur FROM mesure m WHERE m.cle = 'sensor.victron_battery_power'
|
||||
AND m.ts <= mn.t ORDER BY m.ts DESC LIMIT 1) b
|
||||
FROM mn)
|
||||
SELECT coalesce(sum(g) FILTER (WHERE g > 0), 0) / 60000.0 AS importe,
|
||||
coalesce(-sum(g) FILTER (WHERE g < 0), 0) / 60000.0 AS injecte,
|
||||
coalesce(sum(v) FILTER (WHERE v > 0), 0) / 60000.0 AS voiture,
|
||||
coalesce(sum(b) FILTER (WHERE b > 0), 0) / 60000.0 AS batterie_chargee,
|
||||
count(*) FILTER (WHERE g < 0) AS points_negatifs,
|
||||
min(g) AS pire_injection
|
||||
FROM j`, [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,
|
||||
(SELECT ts FROM mesure m2 WHERE m2.cle = 'sensor.victron_battery_soc'
|
||||
AND m2.ts >= $1 AND m2.ts < $2
|
||||
ORDER BY m2.valeur ASC, m2.ts ASC LIMIT 1) AS heure_bas,
|
||||
-- Valeur AU DEBUT de la fenetre, pas le maximum : dire « partie de »
|
||||
-- en donnant le maximum de la nuit ferait commencer le recit par un
|
||||
-- chiffre atteint plus tard.
|
||||
(SELECT valeur FROM mesure m1 WHERE m1.cle = 'sensor.victron_battery_soc'
|
||||
AND m1.ts <= $1 ORDER BY m1.ts DESC LIMIT 1) AS depart,
|
||||
(SELECT valeur FROM mesure m3 WHERE m3.cle = 'sensor.victron_battery_soc'
|
||||
AND m3.ts < $2 ORDER BY m3.ts DESC LIMIT 1) AS maintenant
|
||||
FROM mesure WHERE cle = 'sensor.victron_battery_soc' AND ts >= $1 AND ts < $2`,
|
||||
[debut, fin]);
|
||||
|
||||
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)},`,
|
||||
`et elle est ce matin a ${n0(b.maintenant)} pour cent.`);
|
||||
if (Number(b.bas) <= 20) {
|
||||
dire('Attention : elle a touche son plancher de vingt pour cent,',
|
||||
'donc le reseau a pris le relais de lui-meme.');
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------ 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 {
|
||||
for (const s of sessions) {
|
||||
const kwh = s.energie_compteur_kwh ?? s.energie_kwh;
|
||||
dire(`Le vehicule ${s.vehicule ?? 'non identifie'} a charge a partir de ${heure(s.debut)},`,
|
||||
`et il a pris ${n1(kwh)} kilowattheures.`);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------ 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
|
||||
// d'anodin, alors que l'energie totale restait faible.
|
||||
if (Number(e.points_negatifs) === 0) {
|
||||
dire('Cote qualite du raccordement, aucune minute d\'injection vers le reseau.');
|
||||
} else {
|
||||
const pointe = Math.abs(Number(e.pire_injection));
|
||||
dire(`Il reste ${pluriel(Number(e.points_negatifs), 'minute', 'minutes')} ou le compteur`,
|
||||
`est passe en negatif, au plus fort ${n0(pointe)} watts,`,
|
||||
`soit ${n0(e.injecte * 1000)} wattheures en tout.`,
|
||||
pointe >= 1000
|
||||
? 'Une pointe de cette taille merite un coup d\'oeil : elle vient d\'une charge'
|
||||
+ ' qui disparait d\'un coup alors que la batterie ne peut plus absorber.'
|
||||
: 'C\'est du bruit de regulation, sans consequence.');
|
||||
}
|
||||
|
||||
// ------------------------------------------------ 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 {
|
||||
dire('Aucun incident technique pendant la nuit.');
|
||||
}
|
||||
|
||||
// ------------------------------------------------ la conclusion
|
||||
const vider = Number(b?.bas ?? 100) <= 20;
|
||||
const cher = Number(e.importe) > 8;
|
||||
dire(vider || cher
|
||||
? 'En resume, la nuit a demande plus au reseau que prevu. Le detail est dans le depot.'
|
||||
: 'En resume, la nuit s\'est passee comme prevu.');
|
||||
|
||||
return p.join('\n\n');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
export async function envoyer(texte, sujet) {
|
||||
const url = `${WEBHOOK}?${new URLSearchParams(
|
||||
{ to: DESTINATAIRE, subject: sujet, message: texte })}`;
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`webhook courriel : HTTP ${r.status}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
const estCli = process.argv[1] && process.argv[1].endsWith('rapport-nuit.mjs');
|
||||
if (estCli) {
|
||||
const env = envFichier();
|
||||
const arg = (n) => { const i = process.argv.indexOf(n); return i > 0 ? process.argv[i + 1] : null; };
|
||||
const jour = arg('--jour') ? new Date(arg('--jour') + 'T12:00:00') : new Date();
|
||||
const client = new pg.Client({
|
||||
host: process.env.PGHOST || '10.10.10.22',
|
||||
port: Number(process.env.PGPORT || 5433),
|
||||
database: 'evcs', user: 'evcs_lecture', password: env.PG_LECTURE_PASSWORD,
|
||||
});
|
||||
await client.connect();
|
||||
const texte = await texteRapport(client, jour);
|
||||
await client.end();
|
||||
console.log(texte);
|
||||
if (process.argv.includes('--envoyer')) {
|
||||
await envoyer(texte, 'EVCS - rapport de la nuit');
|
||||
console.log('\n[courriel envoye]');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user