Le rapport en trois langues, avec ce qu'on demande a Pylontech
Le generateur separe desormais les MESURES des MOTS : les chiffres vivent dans gen-rapport-batteries.py, les trois langues dans rapport_textes.py. Une correction de traduction ne peut donc pas toucher un chiffre, ni l'inverse - et les trois versions partagent exactement les memes donnees puisqu'elles lisent la meme source. La version destinee au fabricant gagne une section que la francaise n'avait pas : trois questions precises, sur la recuperabilite des cellules face a leur composante resistive, sur la procedure recommandee pour un module dans cet etat, et sur ce que compte exactement le SOHCount par cellule. Le chinois charge Noto Sans SC et desserre l'interligne ; les mesures de colonne passent en em plutot qu'en ch, un caractere han n'ayant pas la largeur d'un zero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
@@ -1,101 +1,101 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Genere le rapport sur l'etat du parc batterie.
|
||||
"""Genere le rapport sur l'etat du parc batterie, en trois langues.
|
||||
|
||||
Rejouable : les mesures vivent dans les constantes en tete, la mise en page
|
||||
en dessous. Mettre a jour une mesure, relancer, republier.
|
||||
Les MESURES vivent ici, les MOTS dans tools/rapport_textes.py. Une correction
|
||||
de traduction ne peut donc jamais toucher un chiffre, ni l'inverse.
|
||||
|
||||
python3 tools/gen-rapport-batteries.py
|
||||
"""
|
||||
import pathlib, datetime
|
||||
import importlib.util, pathlib
|
||||
|
||||
MAJ = "5 septembre 2026, 17 h"
|
||||
_sp = importlib.util.spec_from_file_location(
|
||||
"rt", str(pathlib.Path(__file__).with_name("rapport_textes.py")))
|
||||
rt = importlib.util.module_from_spec(_sp); _sp.loader.exec_module(rt)
|
||||
|
||||
# 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]),
|
||||
MAJ = {"fr": "5 septembre 2026, 17 h", "en": "5 September 2026, 17:00",
|
||||
"zh": "2026 年 9 月 5 日 17:00"}
|
||||
|
||||
# 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 sans demontage.
|
||||
CODES = ["K220803C30160323", "K220803C30160322", "K220803C30160319", "K220803C30160324"]
|
||||
AMPL = [
|
||||
[191,188,188,189,190,197,187,190,189,192,191,188,187,192,198],
|
||||
[182,178,178,178,177,548,371,181,179,179,182,691,178,273,178],
|
||||
[183,521,184,186,182,318,183,186,184,473,184,443,184,188,424],
|
||||
[138,124,138,137,139,132,134,142,143,135,136,137,128,135,683],
|
||||
]
|
||||
|
||||
# code-barres, pack, adresse, nb atteintes, lesquelles, HV, OV, ecart median, etat
|
||||
PARC = [
|
||||
("K220803C30160322", "ancien", "1", "4", "6 · 7 · 12 · 14", 498, 86, 108, "en service"),
|
||||
("K220803C30160323", "ancien", "2", "0", "—", 0, 0, 6, "en service"),
|
||||
("K220803C30160319", "ancien", "3", "5", "2 · 6 · 10 · 12 · 15", 416, 179, 45, "en service"),
|
||||
("K220803C30160324", "ancien", "4", "1", "15", 29, 59, 57, "isolée"),
|
||||
("Y230202C30371426", "récent", "1", "0", "—", 1, 1, 2, "en service"),
|
||||
("Y230202C30371425", "récent", "2", "0", "—", 1, 1, 2, "en service"),
|
||||
("Y230202C30371431", "récent", "3", "0", "—", 1, 1, 2, "en service"),
|
||||
("Y230202C30371427", "récent", "4", "0", "—", 1, 0, 2, "en service"),
|
||||
("K220803C30160322","a","1","4","6 · 7 · 12 · 14", 498, 86,108,"s"),
|
||||
("K220803C30160323","a","2","0","—", 0, 0, 6,"s"),
|
||||
("K220803C30160319","a","3","5","2 · 6 · 10 · 12 · 15", 416,179, 45,"s"),
|
||||
("K220803C30160324","a","4","1","15", 29, 59, 57,"i"),
|
||||
("Y230202C30371426","r","1","0","—", 1, 1, 2,"s"),
|
||||
("Y230202C30371425","r","2","0","—", 1, 1, 2,"s"),
|
||||
("Y230202C30371431","r","3","0","—", 1, 1, 2,"s"),
|
||||
("Y230202C30371427","r","4","0","—", 1, 0, 2,"s"),
|
||||
]
|
||||
|
||||
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)"
|
||||
return ("var(--mort)" if a >= 600 else "var(--grave)" if a >= 400
|
||||
else "var(--attention)" if a >= 250 else "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
|
||||
def figure(lang):
|
||||
LARG, LIGNE, GAUCHE, HAUT, ech = 760, 15, 96, 26, 0.86
|
||||
out, y = [], 0
|
||||
for code, place, _n, amps in MODULES:
|
||||
for code, place, nrec, amps in zip(CODES, rt.PLACES[lang], rt.RELEVES[lang], AMPL):
|
||||
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>')
|
||||
l = a * ech
|
||||
out.append(f'<text x="{GAUCHE-8}" y="{y+10}" class="fc">{i}</text>'
|
||||
f'<rect x="{GAUCHE}" y="{y+2}" width="{l:.1f}" height="10" rx="2" fill="{couleur(a)}"/>'
|
||||
f'<text x="{GAUCHE+l+6}" y="{y+11}" class="fv">{a}</text>')
|
||||
y += LIGNE
|
||||
y += 14
|
||||
reperes = []
|
||||
rep = []
|
||||
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>')
|
||||
rep.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">' + "".join(rep) + "".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">{med}</td><td class="num">{hv}</td><td class="num">{ov}</td>'
|
||||
f'<td class="etat">{e}</td></tr>'
|
||||
for c, p, a, n, d, hv, ov, med, e in PARC)
|
||||
def page(lang):
|
||||
t = rt.T[lang]
|
||||
cjk = lang == "zh"
|
||||
police = ('"Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif' if cjk
|
||||
else '"Source Serif 4", Georgia, serif')
|
||||
lien = ("https://fonts.googleapis.com/css2?family=Chivo:wght@400;700;900"
|
||||
"&family=Noto+Sans+SC:wght@400;500;700&family=JetBrains+Mono:wght@400;700&display=swap"
|
||||
if cjk else
|
||||
"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")
|
||||
titre_pol = ('"Noto Sans SC", sans-serif' if cjk else 'Chivo, "Helvetica Neue", Arial, sans-serif')
|
||||
|
||||
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)
|
||||
verdict = "".join(f'<div><div class="n">{n}</div><div class="l">{l}</div></div>'
|
||||
for n, l in t["v"])
|
||||
parc = "".join(
|
||||
f'<tr class="{"mal" if int(n) else ""}"><td class="mono">{c}</td>'
|
||||
f'<td>{rt.PACKS[lang][p]}</td><td class="num">{a}</td><td class="num gros">{n}</td>'
|
||||
f'<td class="cel">{d}</td><td class="num">{med}</td><td class="num">{hv}</td>'
|
||||
f'<td class="num">{ov}</td><td class="etat">{rt.ETATS[lang][e]}</td></tr>'
|
||||
for c, p, a, n, d, hv, ov, med, e in PARC)
|
||||
chrono = "".join(f'<tr><td class="mono h">{h}</td><td>{q}</td><td class="note">{x}</td></tr>'
|
||||
for h, q, x in rt.CHRONO_L[lang])
|
||||
seuils = "".join(f'<li><span class="mono">{mv} mV</span> {nom}</li>'
|
||||
for mv, nom in rt.SEUILS_L[lang])
|
||||
ths = "".join(f"<th>{x}</th>" for x in t["th"])
|
||||
thc = "".join(f"<th>{x}</th>" for x in t["thc"])
|
||||
leg = "".join(f'<span><i class="pastille" style="background:var(--{v})"></i>{n}</span>'
|
||||
for v, n in zip(("sain","attention","grave","mort"), t["leg"]))
|
||||
quest = "".join(f"<li>{q}</li>" for q in t["q"])
|
||||
|
||||
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>
|
||||
return f"""<title>{t['titre']}</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">
|
||||
<link rel="stylesheet" href="{lien}">
|
||||
<style>
|
||||
:root {{
|
||||
--fond:#F6F8F7; --carte:#FFFFFF; --encre:#14191B; --sourd:#5B6669;
|
||||
@@ -119,55 +119,50 @@ HTML = f"""<title>Les cent cinq cellules</title>
|
||||
}}
|
||||
* {{ 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; }}
|
||||
font:400 17px/{1.75 if cjk else 1.62} {police}; 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; }}
|
||||
.mesure {{ max-width:{'46em' if cjk else '64ch'}; }}
|
||||
h1, h2, h3, .fm, .etiq {{ font-family:{titre_pol}; }}
|
||||
h1 {{ font-weight:900; font-size:clamp(2.2rem,6vw,3.7rem); line-height:1.08;
|
||||
letter-spacing:{'-.01em' if cjk else '-.025em'}; margin:0 0 .4em; text-wrap:balance; }}
|
||||
h2 {{ font-weight:700; font-size:1.5rem; 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; }}
|
||||
strong, b {{ font-weight:{700 if cjk else 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; }}
|
||||
.chapeau {{ font-size:1.2rem; line-height:1.55; color:var(--sourd);
|
||||
max-width:{'40em' if cjk else '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));
|
||||
grid-template-columns:repeat(auto-fit,minmax(155px,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 .n {{ font-family:{titre_pol}; font-weight:900; font-size:2.3rem;
|
||||
line-height:1; 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; }}
|
||||
.fp {{ font-size:10.5px; fill:var(--sourd); font-family:{police}; }}
|
||||
.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; }}
|
||||
.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; }}
|
||||
figcaption {{ font-size:.88rem; color:var(--sourd); margin-top:.9rem;
|
||||
max-width:{'44em' if cjk else '62ch'}; line-height:1.65; }}
|
||||
.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);
|
||||
table {{ border-collapse:collapse; width:100%; min-width:680px;
|
||||
font-family:{police}; font-size:.94rem; }}
|
||||
th {{ font-family:{titre_pol}; font-size:.7rem; font-weight:700;
|
||||
letter-spacing:.08em; {'' if cjk else '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; }}
|
||||
@@ -179,6 +174,9 @@ tr.mal td {{ background:var(--lueur); }}
|
||||
.etat {{ font-size:.85rem; color:var(--sourd); }}
|
||||
.h {{ font-weight:700; white-space:nowrap; }}
|
||||
.note {{ color:var(--sourd); font-size:.88rem; }}
|
||||
table.mini {{ min-width:0; width:auto; margin:1.2rem 0; border:1px solid var(--trait);
|
||||
border-radius:8px; }}
|
||||
table.mini th {{ padding:9px 16px; }} table.mini td {{ padding:8px 16px; }}
|
||||
.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; }}
|
||||
@@ -186,155 +184,55 @@ tr.mal td {{ background:var(--lueur); }}
|
||||
.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; }}
|
||||
ol, ul {{ padding-left:1.3em; }} li {{ margin-bottom:.7em; }}
|
||||
footer {{ margin-top:5rem; padding-top:1.6rem; border-top:1px solid var(--trait);
|
||||
font-size:.85rem; color:var(--sourd); }}
|
||||
font-size:.85rem; color:var(--sourd); line-height:1.6; }}
|
||||
</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>
|
||||
<div class="etiq">{t['etiq']}</div>
|
||||
<h1>{t['titre']}</h1>
|
||||
<p class="chapeau">{t['chapeau']}</p>
|
||||
<div class="maj">{t['maj']} : {MAJ[lang]}</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="verdict">{verdict}</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>
|
||||
<div class="mesure"><h2><span class="num">1</span>{t['h1']}</h2>{t['p1']}</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>
|
||||
<div class="cadre">{figure(lang)}</div>
|
||||
<div class="legende">{leg}</div>
|
||||
<figcaption>{t['figcap']}</figcaption>
|
||||
</figure>
|
||||
|
||||
<div class="mesure"><h2><span class="num">2</span>{t['h2']}</h2>{t['p2']}</div>
|
||||
<div class="tabl"><table><thead><tr>{ths}</tr></thead><tbody>{parc}</tbody></table></div>
|
||||
<div class="mesure">{t['p3']}
|
||||
|
||||
<h2><span class="num">3</span>{t['h3']}</h2>{t['p4']}</div>
|
||||
<div class="tabl"><table><thead><tr>{thc}</tr></thead><tbody>{chrono}</tbody></table></div>
|
||||
<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 class="encart">{t['enc1']}</div>
|
||||
{t['p5']}<ul>{seuils}</ul>
|
||||
|
||||
<h2><span class="num">4</span>{t['h4']}</h2>{t['p6']}
|
||||
|
||||
<h2><span class="num">5</span>{t['h5']}</h2>{t['p7']}
|
||||
|
||||
<h2><span class="num">6</span>{t['h6']}</h2>
|
||||
<ol>{quest}</ol>
|
||||
</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>Écart médian</th><th>Surtensions</th><th>Sous-tensions</th><th></th></tr></thead>
|
||||
<tbody>{lignes_parc}</tbody>
|
||||
</table></div>
|
||||
|
||||
<div class="mesure">
|
||||
<p>Lisez la colonne des écarts médians. Les cinq modules sains tiennent entre
|
||||
<strong>2 et 6 millivolts</strong> ; les trois autres sont à 45, 57 et 108.
|
||||
Le même contraste apparaît sur les surtensions : <strong>498</strong> pour
|
||||
le premier module contre <strong>une seule</strong> pour chacun des quatre du
|
||||
pack récent, qui ont pourtant encaissé dix fois plus de cycles.</p>
|
||||
<p>Les soixante cellules du pack récent balayent toutes entre 168 et 191 mV
|
||||
d’amplitude, sans exception. L’inventaire est clos : il n’y a rien à
|
||||
chercher ailleurs que dans les trois modules déjà nommés.</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>
|
||||
<footer>{t['meth']}</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")
|
||||
SORTIES = {"fr": "rapport-batteries.html", "en": "battery-report.html",
|
||||
"zh": "battery-report-zh.html"}
|
||||
base = pathlib.Path("docs/pages"); base.mkdir(parents=True, exist_ok=True)
|
||||
for lang, nom in SORTIES.items():
|
||||
h = page(lang)
|
||||
(base / nom).write_text(h, encoding="utf-8")
|
||||
print(f" {nom:26s} {lang} {len(h):6d} caracteres")
|
||||
|
||||
Reference in New Issue
Block a user