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

@@ -12,7 +12,7 @@
"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 || ' · ' || module END AS __text, module AS __value FROM cellule ORDER BY module, ts DESC"
}
]
},
@@ -76,7 +76,7 @@
{
"type": "table",
"title": "Sante des modules — tous, d'un coup d'oeil",
"description": "Le resume qu'on lit en premier, tous modules ensemble — les autres panneaux demandent de choisir un module d'avance. L'ecart entre la cellule la plus haute et la plus basse est l'indicateur qui resume 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.",
"description": "Le resume qu'on lit en premier, tous modules ensemble — les autres panneaux demandent de choisir un module d'avance. L'ecart entre la cellule la plus haute et la plus basse est l'indicateur qui resume 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. « 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.",
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "evcs-pg"
@@ -91,7 +91,7 @@
"format": "table",
"rawQuery": true,
"editorMode": "code",
"rawSql": "WITH vivants AS (\n SELECT module FROM cellule GROUP BY module\n HAVING max(ts) > now() - interval '2 hours'),\ndernier AS (\n SELECT DISTINCT ON (module) module, ts FROM cellule\n WHERE module IN (SELECT module FROM vivants)\n ORDER BY module, ts DESC),\nmaintenant 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 FROM cellule c JOIN dernier d ON d.module = c.module AND d.ts = c.ts\n GROUP BY c.module, d.ts),\nperiode 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)\nSELECT 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 round(m.ma / 1000.0, 1) AS \"Courant (A)\",\n to_char(m.ts, 'DD/MM HH24:MI') AS \"Dernier releve\"\n FROM maintenant m LEFT JOIN periode p USING (module)\n ORDER BY m.ecart DESC"
"rawSql": "WITH vivants AS (\n SELECT module FROM cellule GROUP BY module\n HAVING max(ts) > now() - interval '2 hours'),\ndernier AS (\n SELECT DISTINCT ON (module) module, ts, rang FROM cellule\n WHERE module IN (SELECT module FROM vivants)\n ORDER BY module, ts DESC),\nmaintenant 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, 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, d.rang),\nperiode 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)\nSELECT 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 round(m.ma / 1000.0, 1) AS \"Courant (A)\",\n to_char(m.ts, 'DD/MM HH24:MI') AS \"Dernier releve\"\n FROM maintenant m LEFT JOIN periode p USING (module)\n ORDER BY m.ecart DESC"
}
],
"gridPos": {

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)} ` +