Les cellules disent desormais si le module chargeait

Le proprietaire veut « dans Grafana le detail de chaque cellule pendant la
charge ». Le detail existait deja ; ce qui manquait etait le « pendant la
charge » — rien ne disait QUAND le module chargeait, et un ecart entre cellules
ne veut rien dire hors de son courant : le 05/09 la cellule meneuse etait a
3555 mV sous 50 A et a 3496 mV sous 30 A, meme module, meme tension de parc.

Le releve console porte le courant depuis le 17/09, mais l ingestion ecrivait
`null` : elle ne lisait que les quinze tensions. Elle repere maintenant la
colonne `courant_mA` PAR SON NOM — les US5000 et les US2000C n emettent pas les
memes colonnes, et une position ferait lire la mauvaise grandeur des qu on
deplace le cable d un pack a l autre. Absente des anciens fichiers, donc les
lignes deja versees gardent leur courant inconnu.

Deux panneaux au tableau « Les batteries ».

« Sante des modules — tous, d un coup d oeil » : le resume qu on lit en premier,
tous modules ensemble, la ou les autres panneaux demandent de choisir un module
d avance. Ecart maintenant, ecart maximum sur la periode, cellule la plus haute
et la plus basse, courant au moment du releve, date du dernier releve. Releve au
deploiement : cinq modules vivants, ecarts de 1 a 3 mV — le parc restant est
sain, les trois modules atteints sont sortis le 12/09.

« Le module chargeait-il ? » : le courant du MODULE, lu dans la table des
cellules et non dans celle du parc, ce qui permet de voir qu un module se
charge moins que ses voisins.

Verifie contre la base et pas seulement execute : 75/75 requetes valides ne
prouve que l execution, donc les deux nouvelles ont ete rejouees avec la vraie
fenetre et rendent bien les grandeurs voulues.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
Tallagrand Thierry
2026-09-18 02:56:03 +02:00
parent 3e4d553a0f
commit 463f1b1b06
3 changed files with 359 additions and 11 deletions

View File

@@ -73,6 +73,212 @@
]
},
"panels": [
{
"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.",
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "evcs-pg"
},
"targets": [
{
"refId": "A",
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "evcs-pg"
},
"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"
}
],
"gridPos": {
"x": 0,
"y": 0,
"w": 24,
"h": 8
},
"fieldConfig": {
"defaults": {
"custom": {
"align": "left",
"cellOptions": {
"type": "auto"
},
"filterable": true
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Ecart maintenant"
},
"properties": [
{
"id": "unit",
"value": "mvolt"
},
{
"id": "custom.cellOptions",
"value": {
"type": "color-background"
}
},
{
"id": "thresholds",
"value": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "#EAB839",
"value": 20
},
{
"color": "orange",
"value": 80
},
{
"color": "red",
"value": 150
}
]
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "Ecart max sur la periode"
},
"properties": [
{
"id": "unit",
"value": "mvolt"
},
{
"id": "custom.cellOptions",
"value": {
"type": "color-text"
}
},
{
"id": "thresholds",
"value": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "#EAB839",
"value": 50
},
{
"color": "orange",
"value": 150
},
{
"color": "red",
"value": 300
}
]
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "Plus haute"
},
"properties": [
{
"id": "unit",
"value": "mvolt"
},
{
"id": "thresholds",
"value": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "#EAB839",
"value": 3480
},
{
"color": "orange",
"value": 3520
},
{
"color": "red",
"value": 3550
},
{
"color": "dark-red",
"value": 3600
}
]
}
},
{
"id": "custom.cellOptions",
"value": {
"type": "color-text"
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "Plus basse"
},
"properties": [
{
"id": "unit",
"value": "mvolt"
}
]
},
{
"matcher": {
"id": "byName",
"options": "Courant (A)"
},
"properties": [
{
"id": "unit",
"value": "amp"
},
{
"id": "decimals",
"value": 1
}
]
}
]
},
"options": {
"showHeader": true,
"cellHeight": "sm",
"footer": {
"show": false
}
}
},
{
"type": "timeseries",
"title": "Cellule par cellule — $module",
@@ -96,7 +302,7 @@
],
"gridPos": {
"x": 0,
"y": 0,
"y": 8,
"w": 24,
"h": 10
},
@@ -148,6 +354,59 @@
}
}
},
{
"type": "timeseries",
"title": "Le module chargeait-il ? — $module",
"description": "Positif en charge, negatif en decharge. A lire en regard du panneau au-dessus : l'ecart entre cellules s'ouvre avec le courant, et une cellule abimee ne se trahit qu'en charge.",
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "evcs-pg"
},
"targets": [
{
"refId": "A",
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "evcs-pg"
},
"format": "time_series",
"rawQuery": true,
"editorMode": "code",
"rawSql": "SELECT ts AS time, 'courant du module' AS metric,\n courant_ma / 1000.0 AS a\nFROM cellule\nWHERE module = '$module' AND cellule = 1\n AND courant_ma IS NOT NULL AND $__timeFilter(ts)\nORDER BY 1"
}
],
"gridPos": {
"x": 0,
"y": 18,
"w": 24,
"h": 5
},
"fieldConfig": {
"defaults": {
"unit": "amp",
"color": {
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 15,
"lineWidth": 1
}
},
"overrides": []
},
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": true,
"calcs": []
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
}
},
{
"type": "timeseries",
"title": "Ecart entre la plus haute et la plus basse",
@@ -171,7 +430,7 @@
],
"gridPos": {
"x": 0,
"y": 10,
"y": 23,
"w": 12,
"h": 8
},
@@ -239,7 +498,7 @@
],
"gridPos": {
"x": 12,
"y": 10,
"y": 23,
"w": 12,
"h": 8
},
@@ -308,7 +567,7 @@
],
"gridPos": {
"x": 0,
"y": 18,
"y": 31,
"w": 12,
"h": 10
},
@@ -433,7 +692,7 @@
],
"gridPos": {
"x": 12,
"y": 18,
"y": 31,
"w": 12,
"h": 10
},

View File

@@ -1196,13 +1196,70 @@ batteries = dashboard("evcs-batteries", "Les batteries", (
"publie que l'agregat du parc. Les paliers de couleur sont ceux du materiel "
"lui-meme : 3480, 3520, 3550 et 3600 mV."), [
table("Sante des modules — tous, d'un coup d'oeil", [
sql("A",
"WITH vivants AS (\n"
" 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"
" 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"
" FROM cellule c JOIN dernier d ON d.module = c.module AND d.ts = c.ts\n"
" GROUP BY c.module, d.ts),\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"
" 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", fmt="table")],
PG, 0, 0, 24, 8,
desc="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.",
overrides=[
sur("Ecart maintenant", [{"id": "unit", "value": "mvolt"},
{"id": "custom.cellOptions",
"value": {"type": "color-background"}},
{"id": "thresholds", "value": seuils(
("green", None), ("#EAB839", 20),
("orange", 80), ("red", 150))}]),
sur("Ecart max sur la periode", [{"id": "unit", "value": "mvolt"},
{"id": "custom.cellOptions",
"value": {"type": "color-text"}},
{"id": "thresholds", "value": seuils(
("green", None), ("#EAB839", 50),
("orange", 150), ("red", 300))}]),
sur("Plus haute", [{"id": "unit", "value": "mvolt"},
{"id": "thresholds", "value": SEUILS_CELLULE},
{"id": "custom.cellOptions",
"value": {"type": "color-text"}}]),
sur("Plus basse", [{"id": "unit", "value": "mvolt"}]),
sur("Courant (A)", [{"id": "unit", "value": "amp"},
{"id": "decimals", "value": 1}])]),
ts("Cellule par cellule — $module", [
sql("A", "SELECT ts AS time, 'cellule ' || lpad(cellule::text, 2, '0') AS metric,\n"
" mv\n"
"FROM cellule\n"
"WHERE module = '$module' AND $__timeFilter(ts)\n"
"ORDER BY 1, 2")],
PG, 0, 0, 24, 10, unit="mvolt",
PG, 0, 8, 24, 10, unit="mvolt",
desc="Une courbe par cellule. Une cellule saine suit exactement ses "
"quatorze voisines ; une cellule qui a perdu de la capacite s'en "
"detache en haut de charge ET en bas de decharge, parce qu'elle se "
@@ -1216,6 +1273,28 @@ batteries = dashboard("evcs-batteries", "Les batteries", (
# montrer que les modules ENCORE SUR LA CHAINE. Le releveur lit aussi les
# modules isoles tant qu'ils sont sous tension — le pack ancien a cesse
# d'emettre le 05/09 mais restait affiche sur une fenetre de sept jours,
# Sans lui, « le detail de chaque cellule pendant la charge » n'est pas
# lisible : rien ne dit sur le panneau precedent QUAND le module chargeait.
# Or un ecart entre cellules ne veut rien dire hors de son courant — le 05/09
# la cellule meneuse etait a 3555 mV sous 50 A et a 3496 mV sous 30 A, meme
# module, meme tension de parc.
# ⚠️ Le courant est lu DANS la table des cellules, pas dans celle du parc :
# c'est le courant du MODULE, et c'est ce qui permet de voir qu'un module se
# charge moins que ses voisins. Il n'est renseigne que depuis le 17/09/2026 ;
# avant cette date la courbe est vide, et c'est un fait et non une panne.
ts("Le module chargeait-il ? — $module", [
sql("A", "SELECT ts AS time, 'courant du module' AS metric,\n"
" courant_ma / 1000.0 AS a\n"
"FROM cellule\n"
"WHERE module = '$module' AND cellule = 1\n"
" AND courant_ma IS NOT NULL AND $__timeFilter(ts)\n"
"ORDER BY 1")],
PG, 0, 18, 24, 5, unit="amp",
desc="Positif en charge, negatif en decharge. A lire en regard du panneau "
"au-dessus : l'ecart entre cellules s'ouvre avec le courant, et une "
"cellule abimee ne se trahit qu'en charge.",
extra={"defaults": {"custom": {"fillOpacity": 15, "lineWidth": 1}}}),
# soit huit courbes dont quatre mortes. Le critere est « a emis dans les
# deux dernieres heures », pas un prefixe de code-barres : rebrancher un
# pack le fait revenir tout seul.
@@ -1225,7 +1304,7 @@ batteries = dashboard("evcs-batteries", "Les batteries", (
" AND module IN (SELECT module FROM cellule\n"
" GROUP BY module HAVING max(ts) > now() - interval '2 hours')\n"
"GROUP BY ts, module ORDER BY 1")],
PG, 0, 10, 12, 8, unit="mvolt",
PG, 0, 23, 12, 8, unit="mvolt",
desc="L'indicateur qui resume un module en une courbe. Un module sain de "
"ce parc tient sous 20 mV ; au-dela de 150 mV le BMS commence a "
"reduire le courant de charge de TOUT le parc."),
@@ -1242,7 +1321,7 @@ batteries = dashboard("evcs-batteries", "Les batteries", (
"FROM mesure\n"
"WHERE cle = 'sensor.airtria_8_us2000c_maximum_allowed_charge_current'\n"
" AND $__timeFilter(ts) ORDER BY 1")],
PG, 12, 10, 12, 8, unit="mvolt",
PG, 12, 23, 12, 8, unit="mvolt",
desc="Le plafond de charge est trace a l'echelle (20 mV par ampere) pour "
"tenir sur le meme axe. Le 05/09 a 13:05:08 la cellule meneuse est "
"passee de 3541 a 3555 mV ; cinq secondes plus tard le plafond "
@@ -1257,7 +1336,7 @@ batteries = dashboard("evcs-batteries", "Les batteries", (
"FROM cellule\n"
"WHERE module = '$module' AND $__timeFilter(ts)\n"
"GROUP BY cellule ORDER BY 4 DESC", fmt="table")],
PG, 0, 18, 12, 10,
PG, 0, 31, 12, 10,
desc="Le classement qui designe les cellules a traiter. Sur un module "
"sain les quinze amplitudes sont identiques a quelques millivolts "
"pres. Une amplitude de trois a quatre fois celle des voisines "
@@ -1279,7 +1358,7 @@ batteries = dashboard("evcs-batteries", "Les batteries", (
sql("B", "SELECT ts AS time, valeur AS \"Courant du parc\"\n"
"FROM mesure WHERE cle = 'sensor.victron_battery_current'\n"
" AND $__timeFilter(ts) ORDER BY 1")],
PG, 12, 18, 12, 10,
PG, 12, 31, 12, 10,
desc="Le contexte sans lequel un ecart entre cellules ne veut rien dire : "
"l'ecart grandit avec l'etat de charge, et l'excursion de la cellule "
"abimee grandit avec le courant. Le 05/09, a tension de parc egale, "

View File

@@ -1012,15 +1012,25 @@ async function ingererCellules(client) {
const cols = l[0].split(',');
const iC = cols.indexOf('c1_mV');
if (iC < 0) continue;
// Le courant du module, ajoute au releve le 17/09/2026. Repere PAR SON NOM :
// les US5000 et les US2000C n'emettent pas les memes colonnes, et l'ancrer
// sur une position ferait lire la mauvaise grandeur des qu'on deplace le
// 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');
const val = [];
for (const ligne of l.slice(1)) {
const c = ligne.split(',');
if (c.length < iC + 15) continue;
if (!/^[A-Z]\d{6}[A-Z]\d+$/.test(c[1])) continue; // sans code-barres, on ne devine pas
// Signe : negatif en decharge, positif en charge — c'est ce qui permet de
// filtrer « pendant la charge » sans deviner.
const ma = iI >= 0 ? Number(c[iI]) : NaN;
const courant = Number.isFinite(ma) ? Math.round(ma) : 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, null, null, 'suivi']);
val.push([c[0], c[1], k + 1, mv, null, courant, null, 'suivi']);
}
}
for (let i = 0; i < val.length; i += 500) {