diff --git a/deploy/historisation/grafana/dashboards/batteries.json b/deploy/historisation/grafana/dashboards/batteries.json index 104487b..c78ca64 100644 --- a/deploy/historisation/grafana/dashboards/batteries.json +++ b/deploy/historisation/grafana/dashboards/batteries.json @@ -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": { diff --git a/tools/gen-grafana-dashboards.py b/tools/gen-grafana-dashboards.py index 61e5bda..4f0d1e0 100644 --- a/tools/gen-grafana-dashboards.py +++ b/tools/gen-grafana-dashboards.py @@ -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", diff --git a/tools/historise-evcs.mjs b/tools/historise-evcs.mjs index 3b96a36..eb2fff3 100644 --- a/tools/historise-evcs.mjs +++ b/tools/historise-evcs.mjs @@ -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; } diff --git a/tools/pylontech-console.mjs b/tools/pylontech-console.mjs index 164a2c5..716825d 100644 --- a/tools/pylontech-console.mjs +++ b/tools/pylontech-console.mjs @@ -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)} ` +