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
239 lines
12 KiB
Python
239 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Genere le rapport sur l'etat du parc batterie, en trois langues.
|
|
|
|
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 importlib.util, pathlib
|
|
|
|
_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)
|
|
|
|
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","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"),
|
|
]
|
|
|
|
def couleur(a):
|
|
return ("var(--mort)" if a >= 600 else "var(--grave)" if a >= 400
|
|
else "var(--attention)" if a >= 250 else "var(--sain)")
|
|
|
|
def figure(lang):
|
|
LARG, LIGNE, GAUCHE, HAUT, ech = 760, 15, 96, 26, 0.86
|
|
out, y = [], 0
|
|
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):
|
|
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
|
|
rep = []
|
|
for mv in (200, 400, 600):
|
|
x = GAUCHE + mv * ech
|
|
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>')
|
|
|
|
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')
|
|
|
|
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"])
|
|
|
|
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="{lien}">
|
|
<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.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:{'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; }}
|
|
p {{ margin:0 0 1.05em; }}
|
|
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.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(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:{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:{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; }}
|
|
.rep {{ stroke:var(--trait-fort); stroke-width:1; stroke-dasharray:2 4; }}
|
|
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: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; }}
|
|
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; }}
|
|
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; }}
|
|
.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; }}
|
|
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); line-height:1.6; }}
|
|
</style>
|
|
|
|
<div class="page">
|
|
<header>
|
|
<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">{verdict}</div>
|
|
|
|
<div class="mesure"><h2><span class="num">1</span>{t['h1']}</h2>{t['p1']}</div>
|
|
|
|
<figure>
|
|
<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">
|
|
<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>
|
|
|
|
<footer>{t['meth']}</footer>
|
|
</div>
|
|
"""
|
|
|
|
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")
|