Releve du pack 1 (US5000) sur yd-5CG6110DG0 : service, regle udev, suffixe de fichier

Le cable du pack 1 est a puce Prolific, sans pilote sur DSM : lu par un
portable a noyau recent. --suffixe pack1 separe les CSV des deux lecteurs ;
evcs_cellules regarde 15 min (deux lecteurs non synchronises) et publie
modules_lus (9 attendus).

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:
Tallagrand Thierry
2026-09-26 00:41:47 +02:00
parent 07ee35bdfd
commit 08a1cee1b1
5 changed files with 47 additions and 3 deletions

View File

@@ -0,0 +1,2 @@
# Cable Pylontech du pack 1 (Prolific PL2303, 26/09/2026) : nom stable par numero de serie
SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="23a3", ATTRS{serial}=="BJDAb2A7N11", SYMLINK+="pylontech-pack1", GROUP="dialout", MODE="0660"

View File

@@ -0,0 +1,28 @@
# Releve des cellules du PACK 1 (4 x US5000) sur yd-5CG6110DG0 (10.10.10.61).
# Le pack 2 est lu par le conteneur du Synology .17 ; le pack 1 ne peut pas
# l'etre : son cable est a puce Prolific PL2303 (067b:23a3), sans pilote sur DSM.
# Ce portable (noyau 7.0) le reconnait.
#
# Installation : sudo cp releve-pack1.service /etc/systemd/system/ &&
# sudo systemctl daemon-reload && sudo systemctl enable --now releve-pack1
[Unit]
Description=Releve des cellules Pylontech du pack 1 (US5000)
Wants=network-online.target
After=network-online.target remote-fs.target
# Le lecteur lit son code et ecrit ses CSV dans le depot, monte en NFS.
RequiresMountsFor=/mnt/projects/EVCS/EVCS
[Service]
User=tmzf5455
WorkingDirectory=/mnt/projects/EVCS/EVCS
# /dev/pylontech-pack1 : lien stable pose par 90-pylontech.rules d'apres le
# NUMERO DE SERIE du cable, jamais d'apres l'ordre d'enumeration.
# --suffixe pack1 : fichiers ...-pack1-suivi.csv, distincts de ceux du pack 2.
ExecStart=/usr/bin/node tools/pylontech-console.mjs --port /dev/pylontech-pack1 --suivi 600 --modules 1,2,3,4 --suffixe pack1
# Cable absent, port fige (garde anti-blocage du lecteur), NFS coupe, coupure
# de courant : on relance, sans limite. Le portable n'a plus de batterie.
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

View File

@@ -132,6 +132,10 @@ export function etatCellules(r) {
statut: r.spread_max === null ? 'muet' : 'frais', statut: r.spread_max === null ? 'muet' : 'frais',
age_releve_min: age, seuil_perime_min: RELEVE_PERIME_MIN, age_releve_min: age, seuil_perime_min: RELEVE_PERIME_MIN,
module_le_plus_sensible: r.weakest || null, module_le_plus_sensible: r.weakest || null,
// Nombre de modules dans le releve : 9 attendus quand les deux packs sont
// lus (4 US5000 + 5 US2000C). Moins = un lecteur muet, l'ecart ne couvre
// alors qu'une partie du parc.
...(r.modules_lus !== undefined ? { modules_lus: r.modules_lus } : {}),
}, },
}; };
} }

View File

@@ -1873,14 +1873,18 @@ async function publierCellulesHelpers(client) {
if (!client) { await muet('base injoignable'); return 0; } if (!client) { await muet('base injoignable'); return 0; }
const { rows: [r] } = await client.query( const { rows: [r] } = await client.query(
`WITH mx AS (SELECT max(ts) AS t FROM cellule), `WITH mx AS (SELECT max(ts) AS t FROM cellule),
-- 15 min et non 5 (26/09) : deux lecteurs, un par pack, chacun toutes les
-- 10 min et sans synchronisation ; une fenetre plus courte que le pas
-- ferait disparaitre un pack sur deux selon l'instant du calcul.
recent AS (SELECT DISTINCT ON (module) module, ts FROM cellule recent AS (SELECT DISTINCT ON (module) module, ts FROM cellule
WHERE ts > (SELECT t FROM mx) - interval '5 minutes' ORDER BY module, ts DESC), WHERE ts > (SELECT t FROM mx) - interval '15 minutes' ORDER BY module, ts DESC),
spreads AS (SELECT c.module, max(c.mv) - min(c.mv) AS ecart spreads AS (SELECT c.module, max(c.mv) - min(c.mv) AS ecart
FROM cellule c JOIN recent rc ON rc.module = c.module AND rc.ts = c.ts FROM cellule c JOIN recent rc ON rc.module = c.module AND rc.ts = c.ts
GROUP BY c.module) GROUP BY c.module)
SELECT (SELECT max(ecart) FROM spreads) AS spread_max, SELECT (SELECT max(ecart) FROM spreads) AS spread_max,
(SELECT module FROM spreads ORDER BY ecart DESC NULLS LAST LIMIT 1) AS weakest, (SELECT module FROM spreads ORDER BY ecart DESC NULLS LAST LIMIT 1) AS weakest,
(SELECT round(EXTRACT(EPOCH FROM (now() - t)) / 60)::int FROM mx) AS age_min`) (SELECT round(EXTRACT(EPOCH FROM (now() - t)) / 60)::int FROM mx) AS age_min,
(SELECT count(*) FROM spreads)::int AS modules_lus`)
.catch(async (e) => { await muet('base injoignable').catch(() => {}); throw e; }); .catch(async (e) => { await muet('base injoignable').catch(() => {}); throw e; });
// Capteur unique pour l'agent (25/09) : releve perime => ni ecart ni module // Capteur unique pour l'agent (25/09) : releve perime => ni ecart ni module
// visibles (G13b du banc OpenClaw). Publie avant le test de table vide, pour // visibles (G13b du banc OpenClaw). Publie avant le test de table vide, pour

View File

@@ -15,6 +15,7 @@
// node tools/pylontech-console.mjs --port /dev/ttyUSB0 --commande help // node tools/pylontech-console.mjs --port /dev/ttyUSB0 --commande help
// node tools/pylontech-console.mjs --port /dev/ttyUSB0 --historique // node tools/pylontech-console.mjs --port /dev/ttyUSB0 --historique
// node tools/pylontech-console.mjs --port /dev/ttyUSB0 --suivi 30 // node tools/pylontech-console.mjs --port /dev/ttyUSB0 --suivi 30
// node tools/pylontech-console.mjs --port /dev/pylontech-pack1 --suivi 600 --modules 1,2,3,4 --suffixe pack1
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -371,7 +372,12 @@ async function main() {
if (arg('suivi')) { if (arg('suivi')) {
const pas = Math.max(10, +(arg('suivi', '30')) || 30); const pas = Math.max(10, +(arg('suivi', '30')) || 30);
const nom = `${horodateFichier()}-suivi.csv`; // --suffixe pack1 : deux lecteurs (un par pack, sur deux machines depuis le
// 26/09) ecrivent dans le MEME dossier ; sans suffixe, deux demarrages dans la
// meme minute melangeraient leurs lignes dans un seul fichier. Le nom finit
// toujours par -suivi.csv, seul motif que lit l'ingestion.
const suffixe = arg('suffixe') ? `-${String(arg('suffixe')).replace(/[^\w-]/g, '')}` : '';
const nom = `${horodateFichier()}${suffixe}-suivi.csv`;
const f = path.join(DOSSIER, nom); const f = path.join(DOSSIER, nom);
fs.mkdirSync(DOSSIER, { recursive: true }); fs.mkdirSync(DOSSIER, { recursive: true });
if (!fs.existsSync(f)) { if (!fs.existsSync(f)) {