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
335 lines
17 KiB
Python
335 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Genere le rapport sur l'etat du parc batterie.
|
||
|
||
Rejouable : les mesures vivent dans les constantes en tete, la mise en page
|
||
en dessous. Mettre a jour une mesure, relancer, republier.
|
||
"""
|
||
import pathlib, datetime
|
||
|
||
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 = [
|
||
("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", "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"),
|
||
("Y230202C30371426", "récent", "1", "0", "—", 1, 1, "en service"),
|
||
("Y230202C30371425", "récent", "2", "0", "—", 1, 1, "en service"),
|
||
("Y230202C30371431", "récent", "3", "0", "—", 1, 1, "en service"),
|
||
("Y230202C30371427", "récent", "4", "0", "—", 1, 0, "en service"),
|
||
]
|
||
|
||
CHRONO = [
|
||
("13:04:48", "cellule 7 du module …0322 à <b>3526 mV</b>, cellule 6 du module …0319 à <b>3541</b>", "sous le seuil"),
|
||
("13:05:08", "les mêmes à <b>3555</b> et <b>3575 mV</b>", "niveau 3 franchi"),
|
||
("13:05:13", "le BMS abaisse le courant de charge autorisé de <b>50 à 30 A</b>", "cinq secondes plus tard"),
|
||
("13:15:10", "les cellules redescendent à 3499 et 3522 mV, l’alarme repasse au vert", "le plafond, lui, reste à 30 A"),
|
||
("13:22:40", "plafond <b>30 → 40 A</b>", "après 7 min 31 s"),
|
||
("13:26:39", "plafond <b>40 → 35 A</b>, meneuses à 3563 et 3548 mV", "il redéclenche"),
|
||
("13:38:10", "plafond <b>35 → 40 A</b>", "après 11 min 31 s"),
|
||
]
|
||
|
||
SEUILS = [(3480, "niveau 1"), (3520, "niveau 2"), (3550, "niveau 3 — le courant se réduit"), (3600, "niveau 4")]
|
||
|
||
def couleur(a):
|
||
if a >= 600: return "var(--mort)"
|
||
if a >= 400: return "var(--grave)"
|
||
if a >= 250: return "var(--attention)"
|
||
return "var(--sain)"
|
||
|
||
def figure():
|
||
"""Barres horizontales : amplitude de chaque cellule, par module."""
|
||
LARG, LIGNE, GAUCHE, HAUT = 760, 15, 96, 26
|
||
ech = 0.86 # px par mV
|
||
out, y = [], 0
|
||
for code, place, _n, amps in MODULES:
|
||
out.append(f'<text x="0" y="{y+13}" class="fm">{code[-4:]}</text>'
|
||
f'<text x="0" y="{y+25}" class="fp">{place}</text>')
|
||
y += HAUT
|
||
for i, a in enumerate(amps, 1):
|
||
larg = a * ech
|
||
out.append(
|
||
f'<text x="{GAUCHE-8}" y="{y+10}" class="fc">{i}</text>'
|
||
f'<rect x="{GAUCHE}" y="{y+2}" width="{larg:.1f}" height="10" rx="2" fill="{couleur(a)}"/>'
|
||
f'<text x="{GAUCHE+larg+6}" y="{y+11}" class="fv">{a}</text>')
|
||
y += LIGNE
|
||
y += 14
|
||
reperes = []
|
||
for mv in (200, 400, 600):
|
||
x = GAUCHE + mv * ech
|
||
reperes.append(f'<line x1="{x}" y1="0" x2="{x}" y2="{y-14}" class="rep"/>'
|
||
f'<text x="{x}" y="{y-2}" class="fr">{mv} mV</text>')
|
||
return (f'<svg viewBox="0 0 {LARG} {y+4}" role="img" '
|
||
f'aria-label="Amplitude de chaque cellule, par module">'
|
||
+ "".join(reperes) + "".join(out) + '</svg>')
|
||
|
||
lignes_parc = "".join(
|
||
f'<tr class="{"mal" if int(n) else ""}"><td class="mono">{c}</td><td>{p}</td>'
|
||
f'<td class="num">{a}</td><td class="num gros">{n}</td><td class="cel">{d}</td>'
|
||
f'<td class="num">{hv}</td><td class="num">{ov}</td><td class="etat">{e}</td></tr>'
|
||
for c, p, a, n, d, hv, ov, e in PARC)
|
||
|
||
lignes_chrono = "".join(
|
||
f'<tr><td class="mono h">{h}</td><td>{q}</td><td class="note">{n}</td></tr>'
|
||
for h, q, n in CHRONO)
|
||
|
||
lignes_seuils = "".join(
|
||
f'<li><span class="mono">{mv} mV</span> {nom}</li>' for mv, nom in SEUILS)
|
||
|
||
HTML = f"""<title>Les cent cinq cellules</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Chivo:wght@400;700;900&family=Source+Serif+4:opsz,wght@8..60,400;8..60,600&family=JetBrains+Mono:wght@400;700&display=swap">
|
||
<style>
|
||
:root {{
|
||
--fond:#F6F8F7; --carte:#FFFFFF; --encre:#14191B; --sourd:#5B6669;
|
||
--trait:#DCE2E1; --trait-fort:#B9C3C1;
|
||
--sain:#2E7D62; --attention:#A8760C; --grave:#C2521E; --mort:#9B1D20;
|
||
--lueur:rgba(46,125,98,.08);
|
||
}}
|
||
@media (prefers-color-scheme: dark) {{
|
||
:root:not([data-theme="light"]) {{
|
||
--fond:#0E1112; --carte:#161A1B; --encre:#E7EBEA; --sourd:#97A2A4;
|
||
--trait:#252B2C; --trait-fort:#3A4344;
|
||
--sain:#5BB894; --attention:#D9A63C; --grave:#E8794A; --mort:#E05C60;
|
||
--lueur:rgba(91,184,148,.10);
|
||
}}
|
||
}}
|
||
:root[data-theme="dark"] {{
|
||
--fond:#0E1112; --carte:#161A1B; --encre:#E7EBEA; --sourd:#97A2A4;
|
||
--trait:#252B2C; --trait-fort:#3A4344;
|
||
--sain:#5BB894; --attention:#D9A63C; --grave:#E8794A; --mort:#E05C60;
|
||
--lueur:rgba(91,184,148,.10);
|
||
}}
|
||
* {{ box-sizing:border-box; }}
|
||
body {{ background:var(--fond); color:var(--encre);
|
||
font:400 17px/1.62 "Source Serif 4", Georgia, serif; margin:0;
|
||
-webkit-font-smoothing:antialiased; }}
|
||
.page {{ max-width:1000px; margin:0 auto; padding:0 24px 96px; }}
|
||
.mesure {{ max-width:64ch; }}
|
||
h1, h2, h3, .fm, .etiq {{ font-family:Chivo, "Helvetica Neue", Arial, sans-serif; }}
|
||
h1 {{ font-weight:900; font-size:clamp(2.4rem,6.5vw,4rem); line-height:1.02;
|
||
letter-spacing:-.025em; margin:0 0 .4em; text-wrap:balance; }}
|
||
h2 {{ font-weight:700; font-size:1.5rem; letter-spacing:-.012em;
|
||
margin:4.2rem 0 .2em; text-wrap:balance; }}
|
||
h2 .num {{ color:var(--sourd); font-weight:400; margin-right:.5em;
|
||
font-variant-numeric:tabular-nums; }}
|
||
h3 {{ font-weight:700; font-size:1.05rem; margin:2.4rem 0 .4em; }}
|
||
p {{ margin:0 0 1.05em; }}
|
||
a {{ color:var(--sain); }}
|
||
strong, b {{ font-weight:600; }}
|
||
.mono {{ font-family:"JetBrains Mono", ui-monospace, monospace; font-size:.86em;
|
||
font-variant-numeric:tabular-nums; }}
|
||
header {{ padding:72px 0 8px; border-bottom:1px solid var(--trait); margin-bottom:2.5rem; }}
|
||
.etiq {{ font-size:.72rem; font-weight:700; letter-spacing:.14em;
|
||
text-transform:uppercase; color:var(--sourd); margin-bottom:1.6rem; }}
|
||
.chapeau {{ font-size:1.24rem; line-height:1.5; color:var(--sourd);
|
||
max-width:56ch; margin-bottom:2rem; }}
|
||
.maj {{ font-size:.8rem; color:var(--sourd); padding-bottom:1.4rem; }}
|
||
.verdict {{ display:grid; gap:1px; background:var(--trait);
|
||
grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
|
||
border:1px solid var(--trait); border-radius:10px; overflow:hidden; margin:2rem 0 0; }}
|
||
.verdict div {{ background:var(--carte); padding:18px 20px; }}
|
||
.verdict .n {{ font-family:Chivo,sans-serif; font-weight:900; font-size:2.3rem;
|
||
line-height:1; letter-spacing:-.03em; font-variant-numeric:tabular-nums; }}
|
||
.verdict .l {{ font-size:.86rem; color:var(--sourd); margin-top:.45em; line-height:1.35; }}
|
||
figure {{ margin:2rem 0; }}
|
||
.cadre {{ background:var(--carte); border:1px solid var(--trait); border-radius:10px;
|
||
padding:22px 20px; overflow-x:auto; }}
|
||
svg {{ display:block; min-width:640px; width:100%; height:auto; }}
|
||
.fm {{ font-size:12.5px; font-weight:700; fill:var(--encre); }}
|
||
.fp {{ font-size:10.5px; fill:var(--sourd); font-family:"Source Serif 4",serif; }}
|
||
.fc {{ font-size:9.5px; fill:var(--sourd); text-anchor:end;
|
||
font-family:"JetBrains Mono",monospace; }}
|
||
.fv {{ font-size:9.5px; fill:var(--sourd); font-family:"JetBrains Mono",monospace; }}
|
||
.fr {{ font-size:9.5px; fill:var(--sourd); text-anchor:middle;
|
||
font-family:"JetBrains Mono",monospace; }}
|
||
.rep {{ stroke:var(--trait-fort); stroke-width:1; stroke-dasharray:2 4; }}
|
||
figcaption {{ font-size:.88rem; color:var(--sourd); margin-top:.9rem; max-width:62ch; }}
|
||
.tabl {{ overflow-x:auto; margin:1.6rem 0; border:1px solid var(--trait);
|
||
border-radius:10px; background:var(--carte); }}
|
||
table {{ border-collapse:collapse; width:100%; min-width:660px;
|
||
font-family:"Source Serif 4",serif; font-size:.94rem; }}
|
||
th {{ font-family:Chivo,sans-serif; font-size:.7rem; font-weight:700;
|
||
letter-spacing:.09em; text-transform:uppercase; color:var(--sourd);
|
||
text-align:left; padding:13px 14px; border-bottom:1px solid var(--trait); }}
|
||
td {{ padding:11px 14px; border-bottom:1px solid var(--trait); vertical-align:top; }}
|
||
tr:last-child td {{ border-bottom:none; }}
|
||
tr.mal td {{ background:var(--lueur); }}
|
||
.num {{ text-align:right; font-variant-numeric:tabular-nums;
|
||
font-family:"JetBrains Mono",monospace; font-size:.85rem; }}
|
||
.gros {{ font-weight:700; font-size:1rem; }}
|
||
.cel {{ font-family:"JetBrains Mono",monospace; font-size:.8rem; color:var(--sourd); }}
|
||
.etat {{ font-size:.85rem; color:var(--sourd); }}
|
||
.h {{ font-weight:700; white-space:nowrap; }}
|
||
.note {{ color:var(--sourd); font-size:.88rem; }}
|
||
.legende {{ display:flex; flex-wrap:wrap; gap:18px; font-size:.85rem;
|
||
color:var(--sourd); margin-top:1rem; }}
|
||
.legende span {{ display:inline-flex; align-items:center; gap:7px; }}
|
||
.pastille {{ width:11px; height:11px; border-radius:3px; display:inline-block; }}
|
||
.encart {{ border-left:3px solid var(--grave); background:var(--carte);
|
||
padding:16px 20px; margin:1.8rem 0; border-radius:0 8px 8px 0; }}
|
||
.encart p:last-child {{ margin-bottom:0; }}
|
||
ul {{ padding-left:1.2em; }} li {{ margin-bottom:.45em; }}
|
||
footer {{ margin-top:5rem; padding-top:1.6rem; border-top:1px solid var(--trait);
|
||
font-size:.85rem; color:var(--sourd); }}
|
||
</style>
|
||
|
||
<div class="page">
|
||
<header>
|
||
<div class="etiq">Parc Pylontech · diagnostic en cours</div>
|
||
<h1>Les cent cinq cellules</h1>
|
||
<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 : {MAJ}</div>
|
||
</header>
|
||
|
||
<div class="verdict">
|
||
<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 s</div><div class="l">entre le franchissement du seuil et la coupure</div></div>
|
||
<div><div class="n">30 A</div><div class="l">plafond imposé au parc entier, pour 150 A nominaux</div></div>
|
||
</div>
|
||
|
||
<div class="mesure">
|
||
<h2><span class="num">1</span>Le verdict</h2>
|
||
<p>Le parc n’est pas usé. Quatre modules sur huit sont <strong>irréprochables</strong> :
|
||
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>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 dix cellules.</p>
|
||
</div>
|
||
|
||
<figure>
|
||
<div class="cadre">{figure()}</div>
|
||
<div class="legende">
|
||
<span><i class="pastille" style="background:var(--sain)"></i>saine</span>
|
||
<span><i class="pastille" style="background:var(--attention)"></i>décalée</span>
|
||
<span><i class="pastille" style="background:var(--grave)"></i>gravement atteinte</span>
|
||
<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. Le premier module est le
|
||
<strong>témoin</strong> : 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 : c’est l’écart à ses propres voisines qui compte, pas la
|
||
valeur absolue.</figcaption>
|
||
</figure>
|
||
|
||
<div class="mesure">
|
||
<h2><span class="num">2</span>Le parc, module par module</h2>
|
||
<p>L’identité d’un module est son code-barres, jamais son adresse : chaque
|
||
chaîne renumérote ses modules à partir de 1, il existe donc deux « module
|
||
1 » dans cette installation.</p>
|
||
</div>
|
||
|
||
<div class="tabl"><table>
|
||
<thead><tr><th>Code-barres</th><th>Pack</th><th>Adr.</th><th>Cellules atteintes</th>
|
||
<th>Lesquelles</th><th>Surtensions</th><th>Sous-tensions</th><th></th></tr></thead>
|
||
<tbody>{lignes_parc}</tbody>
|
||
</table></div>
|
||
|
||
<div class="mesure">
|
||
<p>Le contraste est net : <strong>498 surtensions</strong> pour le premier
|
||
module contre <strong>1</strong> pour chacun des quatre modules du pack récent,
|
||
qui ont pourtant encaissé dix fois plus de cycles.</p>
|
||
|
||
<h2><span class="num">3</span>Le déclenchement, chronométré</h2>
|
||
<p>Le 5 septembre, la console a été branchée sur les trois modules du pack ancien
|
||
pendant que l’alarme montait. Ce qui suit est le relevé, à la seconde.</p>
|
||
</div>
|
||
|
||
<div class="tabl"><table>
|
||
<thead><tr><th>Heure</th><th>Ce qui se passe</th><th></th></tr></thead>
|
||
<tbody>{lignes_chrono}</tbody>
|
||
</table></div>
|
||
|
||
<div class="mesure">
|
||
<div class="encart">
|
||
<p><strong>La découverte la plus utile de la journée</strong> tient dans la
|
||
quatrième ligne : l’alarme repasse au vert bien avant que le plafond ne
|
||
remonte. Chercher l’alarme dans les journaux fait donc manquer l’essentiel du
|
||
phénomène — elle dure quelques minutes, le bridage dure des heures.</p>
|
||
</div>
|
||
|
||
<p>Les seuils ne sont pas des valeurs choisies : ce sont ceux que le
|
||
matériel déclare lui-même.</p>
|
||
<ul>{lignes_seuils}</ul>
|
||
|
||
<h2><span class="num">4</span>Ce qui a été essayé</h2>
|
||
<p>La consigne de tension de charge a été abaissée de <span class="mono">51,3</span>
|
||
à <span class="mono">51,0 V</span> pour voir si le contournement suffisait à ne
|
||
plus jamais déclencher.</p>
|
||
<p><strong>Il ne suffit pas.</strong> À 92 % de charge, les cellules meneuses
|
||
ont atteint 3563 et 3548 mV malgré la consigne abaissée, et le plafond a
|
||
recommencé à osciller entre 35 et 40 A. Le système ne reste pas « bloqué »
|
||
à une valeur : il cherche en permanence le courant maximal que la cellule la
|
||
plus faible tolère.</p>
|
||
<p>Une mesure explique pourquoi la tension seule ne peut pas régler la question.
|
||
À tension de parc identique, la cellule meneuse était à <span class="mono">3555 mV
|
||
sous 50 A</span> et à <span class="mono">3496 mV sous 30 A</span>. Son excursion
|
||
est donc largement produite par sa résistance interne multipliée par le courant,
|
||
et pas seulement par son état de charge.</p>
|
||
|
||
<h2><span class="num">5</span>D’où viennent les dégâts</h2>
|
||
<p>Pas de l’usure. Le pack récent a encaissé trois cent quarante cycles en un an
|
||
et ses soixante cellules sont intactes : cycler n’abîme pas ces modules.</p>
|
||
<p>Les deux cellules terminales du parc sont descendues à
|
||
<span class="mono">2953</span> et <span class="mono">2939 mV</span>, sous le seuil
|
||
d’alarme basse. Un accumulateur au lithium-fer-phosphate mené là sous charge perd
|
||
de la capacité de façon irréversible. <strong>Ces cellules n’ont pas vieilli,
|
||
elles ont été vidées trop bas.</strong></p>
|
||
<div class="encart">
|
||
<p>Et il y a une boucle à rompre. La jauge d’état de charge du parc suit la
|
||
cellule la plus faible. Quand le système croit toucher son plancher de sécurité
|
||
à 22 %, cette cellule est déjà bien plus bas que le chiffre ne le laisse
|
||
croire. <strong>Le plancher est exprimé dans une grandeur que la cellule
|
||
endommagée fausse elle-même</strong> : il ne protège pas ce qu’il croit
|
||
protéger, et rien n’empêche le même mécanisme d’atteindre le pack sain.</p>
|
||
</div>
|
||
|
||
<h2><span class="num">6</span>Ce qui vient</h2>
|
||
<p>Une alimentation stabilisée permettra de reprendre le rééquilibrage au banc,
|
||
en commençant par le module isolé : il n’a qu’une cellule à traiter, il est
|
||
hors service, et rien de ce qu’on y tentera ne peut nuire au parc.</p>
|
||
<p>Une limite à garder en tête : un rééquilibrage corrige un <em>décalage</em>
|
||
de charge, pas une <em>perte</em> de capacité. Les cellules modérément décalées
|
||
sont de bonnes candidates. Les deux cellules terminales le sont beaucoup moins —
|
||
et c’est précisément ce que l’essai sur le module isolé dira, sans risque.</p>
|
||
</div>
|
||
|
||
<footer>Relevés faits sur la console série des BMS Pylontech, que ni Home
|
||
Assistant ni le GX Victron n’exposent : le système ne publie que l’agrégat
|
||
du parc. Ce rapport évolue au fil des travaux.</footer>
|
||
</div>
|
||
"""
|
||
|
||
p = pathlib.Path("docs/pages/rapport-batteries.html")
|
||
p.parent.mkdir(parents=True, exist_ok=True)
|
||
p.write_text(HTML, encoding="utf-8")
|
||
print(f"{p} {len(HTML)} caracteres")
|