Un temoin sain, et l'historique de toute une chaine par un seul branchement

datalist reste local, mais data <adresse> history <n> est adressable
et rend l'enregistrement complet, quinze cellules comprises. Les trois
modules de la chaine ancienne sont donc extraits d'un seul
branchement, tous corriges du meme decalage lu au chronometre : 369
minutes. Les jeux sont enfin comparables entre eux, ce que les
decalages deduits - divergents de deux heures et demie - ne
permettaient pas.

Le module d'adresse 2 donne le temoin qui manquait : meme chaine,
memes courants, memes journees, et ses quinze cellules tiennent dans
onze millivolts d'amplitude les unes des autres, ecart median 6 mV.
Ce qui a tue les autres ne vient donc ni du regime de charge, ni du
pilotage, ni de l'installation.

Sur cette base commune la cellule 14 du module ...0322 passe a 273 mV
et rejoint les atteintes : dix cellules, pas neuf.

L'horloge se propage bien du maitre a sa chaine - j'avais conclu
l'inverse en verifiant une seconde apres l'ecriture, il en faut vingt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
Tallagrand Thierry
2026-09-05 16:20:58 +02:00
parent 8cf25114f8
commit 8d076603de
10 changed files with 5465 additions and 30 deletions

View File

@@ -6,23 +6,25 @@ en dessous. Mettre a jour une mesure, relancer, republier.
"""
import pathlib, datetime
MAJ = "5 septembre 2026, 16 h"
MAJ = "5 septembre 2026, 16 h 30"
# Amplitude min->max de chaque cellule, relevee sur l'historique du BMS.
# C'est LA grandeur qui separe une cellule saine d'une cellule morte : une
# cellule qui a perdu sa capacite se remplit et se vide avant ses voisines,
# donc elle balaye une plage bien plus large a courant identique.
MODULES = [
("K220803C30160322", "pack ancien · adresse 1", "1681 relevés, 14/08 → 05/09", [
182,178,178,178,177,548,371,181,179,179,182,691,178,251,178]),
("K220803C30160319", "pack ancien · adresse 3", "1798 relevés, 30/07 → 05/09", [
184,521,187,186,183,318,185,186,187,473,187,445,186,189,424]),
("K220803C30160323", "pack ancien · adresse 2 — LE TÉMOIN", "1793 relevés, 30/07 → 05/09", [
191,188,188,189,190,197,187,190,189,192,191,188,187,192,198]),
("K220803C30160322", "pack ancien · adresse 1", "1793 relevés, 14/08 → 05/09", [
182,178,178,178,177,548,371,181,179,179,182,691,178,273,178]),
("K220803C30160319", "pack ancien · adresse 3", "1476 relevés, 30/07 → 05/09", [
183,521,184,186,182,318,183,186,184,473,184,443,184,188,424]),
("K220803C30160324", "isolée (BAT3) · adresse 4", "322 relevés, 23/07 → 05/09", [
138,124,138,137,139,132,134,142,143,135,136,137,128,135,683]),
]
PARC = [
("K220803C30160322", "ancien", "1", "3", "6 · 7 · 12", 498, 86, "en service"),
("K220803C30160322", "ancien", "1", "4", "6 · 7 · 12 · 14", 498, 86, "en service"),
("K220803C30160323", "ancien", "2", "0", "—", 0, 0, "en service"),
("K220803C30160319", "ancien", "3", "5", "2 · 6 · 10 · 12 · 15", 416, 179, "en service"),
("K220803C30160324", "ancien", "4", "1", "15", 29, 59, "isolée"),
@@ -192,14 +194,14 @@ footer {{ margin-top:5rem; padding-top:1.6rem; border-top:1px solid var(--trait)
<header>
<div class="etiq">Parc Pylontech · diagnostic en cours</div>
<h1>Les cent cinq cellules</h1>
<p class="chapeau">Huit modules, quinze cellules chacun. Neuf de ces cellules
brident la charge des cent cinq autres. Voici lesquelles, comment on le sait,
<p class="chapeau">Huit modules, quinze cellules chacun. Dix de ces cellules
brident la charge des quatre-vingt-quinze autres. Voici lesquelles, comment on le sait,
et ce qu’on peut en faire.</p>
<div class="maj">Dernière mise à jour&nbsp;: {MAJ}</div>
</header>
<div class="verdict">
<div><div class="n">9</div><div class="l">cellules atteintes sur 105</div></div>
<div><div class="n">10</div><div class="l">cellules atteintes sur 105</div></div>
<div><div class="n">3</div><div class="l">modules concernés sur 8</div></div>
<div><div class="n">5&nbsp;s</div><div class="l">entre le franchissement du seuil et la coupure</div></div>
<div><div class="n">30&nbsp;A</div><div class="l">plafond imposé au parc entier, pour 150 A nominaux</div></div>
@@ -211,9 +213,9 @@ footer {{ margin-top:5rem; padding-top:1.6rem; border-top:1px solid var(--trait)
soixante cellules sans une seule anomalie, après plus de trois cents cycles. Un
cinquième l’est tout autant.</p>
<p>Trois modules portent l’ensemble du problème, et à l’intérieur de ces trois,
<strong>neuf cellules</strong>. Quand l’une d’elles atteint 3550 mV, le BMS réduit
<strong>dix cellules</strong>. Quand l’une d’elles atteint 3550 mV, le BMS réduit
le courant de charge — non pas de son module, mais <strong>de tout le parc</strong>.
Cinq modules sains sont bridés par neuf cellules.</p>
Cinq modules sains sont bridés par dix cellules.</p>
</div>
<figure>
@@ -225,11 +227,17 @@ Cinq modules sains sont bridés par neuf cellules.</p>
<span><i class="pastille" style="background:var(--mort)"></i>terminale</span>
</div>
<figcaption>Amplitude balayée par chaque cellule entre sa tension minimale et
sa tension maximale sur la période d’observation. Une cellule saine suit ses
voisines à quelques millivolts près. Une cellule qui a perdu de la capacité se
remplit et se vide avant elles, donc elle balaye une plage bien plus large à
courant identique — c’est le signe qui ne trompe pas, et il ne demande aucun
démontage.</figcaption>
sa tension maximale sur la période d’observation. Le premier module est le
<strong>témoin</strong>&nbsp;: il vit dans la même chaîne que les deux abîmés,
a subi les mêmes courants et les mêmes journées, et ses quinze cellules tiennent
dans <strong>onze millivolts d’amplitude les unes des autres</strong>. C’est à
cela que ressemble un module sain, et c’est ce qui rend les autres barres
lisibles. Une cellule qui a perdu de la capacité se remplit et se vide avant ses
voisines, donc elle balaye une plage bien plus large à courant identique — le
signe ne demande aucun démontage. Les amplitudes du module isolé sont plus
faibles parce qu’il a passé la période hors service, sur une plage de charge
plus étroite&nbsp;: c’est l’écart à ses propres voisines qui compte, pas la
valeur absolue.</figcaption>
</figure>
<div class="mesure">

View File

@@ -123,6 +123,28 @@ function lireHistorique(f) {
return { module: bc[1], decalageMin, origine, lignes, nb: rangs.length };
}
// ------------------------------------------------- historique distant (CSV)
// Produit par `--historique-distant <adresse>` : deja horodate a l'heure
// REELLE (le decalage est lu et applique a l'ecriture) et deja nomme par
// code-barres. Rien a corriger, rien a deviner.
function lireHistoDistant(f) {
const lignes = [];
const l = lireTexte(f).trim().split('\n');
const cols = l[0].split(',');
const iC = cols.indexOf('c1_mV');
for (const ligne of l.slice(1)) {
const c = ligne.split(',');
if (c.length < iC + 15) continue;
for (let k = 0; k < 15; k++) {
const mv = +c[iC + k];
if (!(mv > 1000)) continue;
lignes.push([c[0], c[1], k + 1, mv, c[4] === '' ? null : +c[4],
c[3] === '' ? null : +c[3], c[2] || null, 'historique']);
}
}
return lignes.length ? { module: l[1].split(',')[1], lignes } : null;
}
// --------------------------------------------------------------------- suivi
function lireSuivi(f) {
const lignes = [];
@@ -157,7 +179,10 @@ const paquets = [];
for (const f of fichiers.sort()) {
const n = path.basename(f);
try {
if (n.endsWith('-suivi.csv')) {
if (n.includes('-histo-distant-')) {
const r = lireHistoDistant(f);
if (r) { paquets.push(r.lignes); console.log(` ${n}\n ${r.module}, ${r.lignes.length / 15} enregistrements, horodates a l'export`); }
} else if (n.endsWith('-suivi.csv')) {
const r = lireSuivi(f);
if (r) { paquets.push(r.lignes); console.log(` ${n} suivi, ${r.lignes.length / 15} releves, modules ${r.modules.join(' ')}`); }
} else if (/histor/i.test(n) && n.endsWith('.txt')) {

View File

@@ -238,6 +238,60 @@ async function main() {
return;
}
// --historique-distant <adresse> : l'historique d'un AUTRE module de la
// chaine. `datalist` est local, mais `data <adresse> history <n>` est
// adressable et rend l'enregistrement complet, quinze cellules comprises.
// Un seul branchement par chaine suffit donc pour tout son historique.
if (arg('historique-distant')) {
const ad = parseInt(String(arg('historique-distant')), 10);
if (!(ad >= 1 && ad <= 16)) { console.log('Adresse invalide.'); process.exit(1); }
// Le decalage se mesure AVANT, car les enregistrements stockes portent
// l'heure qu'avait le module quand il les a ecrits.
const rtc = await commande(l, 'time', 8000);
const mr = rtc.match(/RTC\s+(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
let decMs = 0;
if (mr) {
const [d, h] = mr[1].split(' ');
const [A, M, J] = d.split('-').map(Number); const [H, N, S] = h.split(':').map(Number);
decMs = new Date(A, M - 1, J, H, N, S).getTime() - Date.now();
console.log(`Horloge du module : ${mr[1]} — ${Math.round(decMs / 60000)} min d'avance, corriges a l'ecriture.`);
} else {
console.log('Horloge non lue : les horodatages seront ceux du module, non corriges.');
}
const info = await commande(l, `info ${ad}`, 8000);
const bc = (info.match(/Barcode\s*:\s*(\S+)/) || [])[1] || `adresse${ad}`;
const f = path.join(DOSSIER, `${tampon()}-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');
console.log(`Module ${bc} (adresse ${ad}) -> ${f}`);
let ecrits = 0, vides = 0;
for (let n = 1; n <= 1800; n++) {
const t = await commande(l, `data ${ad} history ${n}`, 8000);
const mt = t.match(/Time\s*:\s*(\d\d-\d\d-\d\d \d\d:\d\d:\d\d)/);
const cel = cellules(t);
if (!mt || !cel || cel.length < 15) {
if (++vides > 20) break; // fin du tampon
continue;
}
vides = 0;
const [d, h] = mt[1].split(' ');
const [A, M, J] = d.split('-').map(Number); const [H, N, S] = h.split(':').map(Number);
const quand = new Date(new Date(2000 + A, M - 1, J, H, N, S).getTime() - decMs);
const v = cel.map((c) => c.mV);
const soc = (t.match(/Percent\s*:\s*(\d+)/) || [])[1] || '';
fs.appendFileSync(f, [quand.toISOString(), bc, cel[0].etat, cel[0].mA, soc,
...v, Math.max(...v) - Math.min(...v)].join(',') + '\n');
ecrits++;
if (n % 25 === 0) process.stdout.write(`\r ${n} interroges, ${ecrits} ecrits `);
}
console.log(`\n${ecrits} enregistrements ecrits dans ${f}`);
return;
}
if (arg('suivi')) {
const pas = Math.max(10, +(arg('suivi', '30')) || 30);
const nom = `${tampon()}-suivi.csv`;