Le rang du module dans sa chaine accompagne desormais chaque releve de cellules

Le proprietaire demandait si le rang dans le pack etait accessible ou si seul
le code-barres l'etait. Il l'etait depuis le debut : l'outil de releve resout
adresse <-> code-barres au demarrage mais ne l'ecrivait pas. Desormais :
- pylontech-console.mjs --suivi ecrit une colonne 'adresse' (vide en mono-module,
  jamais devinee) ;
- historise-evcs.mjs l'ingere par son NOM dans cellule.rang, enregistre PAR
  RELEVE et non dans une table figee — un recablage du LVHUB renumerote, et
  l'historique de chaque ligne doit garder le rang vrai a son instant ;
- Grafana : colonne 'Rang' dans le tableau de sante, et la liste deroulante
  des modules affiche 'rang - code-barres' quand le rang est connu.
75/75 requetes valides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
Tallagrand Thierry
2026-09-18 12:12:12 +02:00
parent 3b81038739
commit 8c35f8c2ca
4 changed files with 30 additions and 15 deletions

View File

@@ -1182,7 +1182,10 @@ vehicules = dashboard("evcs-vehicules", "Les vehicules", (
VARIABLE_MODULE = {
"name": "module", "label": "Module", "type": "query", "datasource": PG,
"refresh": 1, "includeAll": False, "multi": False,
"query": "SELECT DISTINCT module FROM cellule ORDER BY 1",
"query": ("SELECT DISTINCT ON (module) "
"CASE WHEN rang IS NULL THEN module "
"ELSE rang || ' \u00b7 ' || module END AS __text, "
"module AS __value FROM cellule ORDER BY module, ts DESC"),
}
# Echelle d'alarme du materiel, lue dans `sysinfo` du LVHUB le 05/09/2026.
@@ -1202,21 +1205,21 @@ batteries = dashboard("evcs-batteries", "Les batteries", (
" SELECT module FROM cellule GROUP BY module\n"
" HAVING max(ts) > now() - interval '2 hours'),\n"
"dernier AS (\n"
" SELECT DISTINCT ON (module) module, ts FROM cellule\n"
" SELECT DISTINCT ON (module) module, ts, rang FROM cellule\n"
" WHERE module IN (SELECT module FROM vivants)\n"
" ORDER BY module, ts DESC),\n"
"maintenant AS (\n"
" SELECT c.module, max(c.mv) - min(c.mv) AS ecart,\n"
" max(c.mv) AS haute, min(c.mv) AS basse,\n"
" max(c.courant_ma) AS ma, d.ts\n"
" max(c.courant_ma) AS ma, d.ts, d.rang\n"
" FROM cellule c JOIN dernier d ON d.module = c.module AND d.ts = c.ts\n"
" GROUP BY c.module, d.ts),\n"
" GROUP BY c.module, d.ts, d.rang),\n"
"periode AS (\n"
" SELECT module, max(ecart) AS ecart_max FROM (\n"
" SELECT module, ts, max(mv) - min(mv) AS ecart\n"
" FROM cellule WHERE $__timeFilter(ts) GROUP BY module, ts) x\n"
" GROUP BY module)\n"
"SELECT m.module AS \"Module\",\n"
"SELECT m.rang AS \"Rang\", m.module AS \"Module\",\n"
" m.ecart AS \"Ecart maintenant\",\n"
" p.ecart_max AS \"Ecart max sur la periode\",\n"
" m.haute AS \"Plus haute\", m.basse AS \"Plus basse\",\n"
@@ -1231,7 +1234,9 @@ batteries = dashboard("evcs-batteries", "Les batteries", (
"un module en un nombre : sous 20 mV il est sain, au-dela de 150 mV "
"le BMS reduit le courant de charge de TOUT le parc. La colonne "
"« Courant » dit si le module etait en charge au moment du releve : "
"un ecart ne se compare qu'a courant comparable.",
"un ecart ne se compare qu'a courant comparable. « Rang » est "
"l'adresse du module dans SA chaine au dernier releve — chaque "
"chaine numerote a partir de 1, l'identite reste le code-barres.",
overrides=[
sur("Ecart maintenant", [{"id": "unit", "value": "mvolt"},
{"id": "custom.cellOptions",

View File

@@ -656,6 +656,11 @@ async function migrer(client) {
)`);
await client.query(
'CREATE INDEX IF NOT EXISTS cellule_module_ts_idx ON cellule (module, ts DESC)');
// Rang du module dans SA chaine AU MOMENT du releve. Enregistre par ligne et
// non dans une table de correspondance : chaque chaine renumerote a partir
// de 1, et un recablage du LVHUB peut tout renumeroter — la valeur figee
// mentirait sur l'historique. Absent des anciens releves => NULL.
await client.query('ALTER TABLE cellule ADD COLUMN IF NOT EXISTS rang smallint');
// Ce que le CONSEILLER a observe. Ses helpers sont ecrases toutes les
// quinze minutes : sans cette table, un signalement grave emis a 3 h du
@@ -1018,6 +1023,9 @@ async function ingererCellules(client) {
// cable d'un pack a l'autre. Absent des anciens fichiers => null, donc les
// lignes deja versees gardent leur courant inconnu.
const iI = cols.indexOf('courant_mA');
// Le rang dans la chaine, ajoute au releve le 18/09/2026 — par son NOM,
// comme le courant : la position en fin de ligne n'est pas un contrat.
const iA = cols.indexOf('adresse');
const val = [];
for (const ligne of l.slice(1)) {
const c = ligne.split(',');
@@ -1027,17 +1035,19 @@ async function ingererCellules(client) {
// filtrer « pendant la charge » sans deviner.
const ma = iI >= 0 ? Number(c[iI]) : NaN;
const courant = Number.isFinite(ma) ? Math.round(ma) : null;
const ra = iA >= 0 ? parseInt(c[iA], 10) : NaN;
const rang = Number.isFinite(ra) && ra >= 1 && ra <= 16 ? ra : null;
for (let k = 0; k < 15; k++) {
const mv = Number(c[iC + k]);
if (!(mv > 1000)) continue;
val.push([c[0], c[1], k + 1, mv, null, courant, null, 'suivi']);
val.push([c[0], c[1], k + 1, mv, null, courant, null, 'suivi', rang]);
}
}
for (let i = 0; i < val.length; i += 500) {
const lot = val.slice(i, i + 500);
const ph = lot.map((_, k) => `($${k * 8 + 1},$${k * 8 + 2},$${k * 8 + 3},$${k * 8 + 4},$${k * 8 + 5},$${k * 8 + 6},$${k * 8 + 7},$${k * 8 + 8})`).join(',');
const ph = lot.map((_, k) => `($${k * 9 + 1},$${k * 9 + 2},$${k * 9 + 3},$${k * 9 + 4},$${k * 9 + 5},$${k * 9 + 6},$${k * 9 + 7},$${k * 9 + 8},$${k * 9 + 9})`).join(',');
const r = await client.query(
`INSERT INTO cellule (ts, module, cellule, mv, soc, courant_ma, etat, source)
`INSERT INTO cellule (ts, module, cellule, mv, soc, courant_ma, etat, source, rang)
VALUES ${ph} ON CONFLICT (ts, module, cellule) DO NOTHING`, lot.flat());
lignes += r.rowCount;
}

View File

@@ -298,7 +298,7 @@ async function main() {
`${horodateFichier()}-${tampon === 'event' ? 'evenements' : 'histo'}-distant-${bc}.csv`);
fs.mkdirSync(DOSSIER, { recursive: true });
fs.writeFileSync(f, 'heure_reelle,module,etat,courant_mA,soc,' +
Array.from({ length: 15 }, (_, i) => `c${i + 1}_mV`).join(',') + ',ecart_mV\n');
Array.from({ length: 15 }, (_, i) => `c${i + 1}_mV`).join(',') + ',ecart_mV,adresse\n');
console.log(`Module ${bc} (adresse ${ad}), tampon ${tampon} -> ${f}`);
let ecrits = 0, vides = 0, apresBascule = 0;
@@ -426,7 +426,7 @@ async function main() {
const hi = Math.max(...v), lo = Math.min(...v);
const p = puis.get(ad);
fs.appendFileSync(f, [new Date().toISOString(), codes.get(ad),
p ? p.mA : '', ...v, hi - lo].join(',') + '\n');
p ? p.mA : '', ...v, hi - lo, ad].join(',') + '\n');
console.log(`${horodate()} ${codes.get(ad).slice(-4)} haute ${hi} mV (cellule ${c.find((x) => x.mV === hi).n})` +
` basse ${lo} mV ecart ${String(hi - lo).padStart(3)} mV` +
(p ? ` ${(p.mA / 1000).toFixed(1)} A ${p.soc || ''}` : '') +
@@ -441,7 +441,7 @@ async function main() {
const v = cel.map((c) => c.mV);
const hi = Math.max(...v), lo = Math.min(...v);
fs.appendFileSync(f, [
new Date().toISOString(), cel[0].etat, cel[0].mA, ...v, hi - lo,
new Date().toISOString(), cel[0].etat, cel[0].mA, ...v, hi - lo, '',
].join(',') + '\n');
const nHi = cel.find((c) => c.mV === hi).n;
console.log(`${horodate()} ${String(cel[0].etat).padEnd(7)} ` +