Generateur rejouable (tools/gen-page-batteries.py) plutot qu'un HTML ecrit a la main : les chiffres viennent des mesures et se recalculent. Base : 37 nuits completes issues des statistiques long terme de Home Assistant (fin juillet au 4 septembre, maison seule, voiture deduite) et la courbe energie/SOC etablie sur 576 paliers. Le chiffre qui cadre la decision : les 7 modules actifs delivrent 10,7 kWh a la maison, la nuit mediane en demande 13,2 — la batterie passe la nuit seule 1 fois sur 37. Reparer le module en panne porte cela a 5/37 seulement ; deux modules de plus le portent a 32/37. Reparer seul ne resout rien. Second point, moins attendu : au-dela de la consommation nocturne le stockage ne cycle plus. Le surplus de milieu de journee est un probleme de preneur, pas de stockage — la voiture absorbe deja 9,8 kWh/jour en mediane. Reserve inscrite en clair : toutes les mesures sont d'ete, les compteurs exploitables ne remontant qu'a fin juillet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
536 lines
27 KiB
Python
536 lines
27 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Page de decision : dimensionnement du parc batterie.
|
||
|
||
Rejouable. Toutes les valeurs viennent des mesures du 04/09/2026 :
|
||
37 nuits completes (statistiques long terme Home Assistant, juillet-septembre)
|
||
et la courbe energie/SOC etablie sur 576 paliers.
|
||
"""
|
||
import json, pathlib
|
||
|
||
NUITS = [10.2,11,11.2,11.6,11.9,12.4,12.4,12.5,12.5,12.6,12.6,12.7,12.8,12.9,
|
||
12.9,12.9,13.1,13.1,13.2,13.4,13.4,13.5,13.6,13.7,13.7,13.7,13.9,13.9,
|
||
14.2,14.4,15,15.2,15.6,15.9,16.4,16.7,17.2]
|
||
|
||
# kWh utiles par module, mesures. 1,65 dans la fenetre reelle (blocage a 94 %,
|
||
# plancher ESS a 20 %) ; 1,77 si le blocage a 94 % disparait avec le vieux pack.
|
||
BRIDE, LIBRE = 1.65, 1.77
|
||
ONDULEUR = 0.93 # rendement de restitution vers la maison
|
||
|
||
SCENARIOS = [
|
||
("Actuel", "7 modules actifs — un en panne, un qui faiblit", 7, BRIDE, "etat"),
|
||
("A", "Réparer le module en panne — 8 modules", 8, BRIDE, "faible"),
|
||
("B", "A + 2 neufs — 10 modules", 10, BRIDE, "bon"),
|
||
("C", "A + 4 neufs — 12 modules", 12, BRIDE, "bon"),
|
||
("D", "4 récents + 6 neufs, vieux pack retiré", 10, LIBRE, "bon"),
|
||
]
|
||
|
||
TRANCHES = [("21-31", 0.097, 88), ("35-44", 0.171, 78), ("45-54", 0.169, 76),
|
||
("55-64", 0.163, 85), ("65-74", 0.171, 80), ("75-84", 0.170, 81),
|
||
("85-94", 0.169, 62)]
|
||
|
||
JOURNEE = [("00",0.00,0,0,0),("01",0.00,1.88,1.57,1.30),("02",0.00,2.83,2.49,1.83),
|
||
("03",0.00,0.03,0.64,0),("04",0.00,0.02,0.69,0),("05",0.00,0.02,0.67,0),
|
||
("06",0.00,1.36,0.80,0),("07",0.02,0.72,0.67,0),("08",0.35,0.87,0.83,0),
|
||
("09",0.78,0.38,0.83,0),("10",2.18,0.16,1.23,0),("11",4.20,0.02,1.27,0),
|
||
("12",2.34,0.00,1.22,0),("13",3.51,0.00,2.99,0.96),("14",3.98,0.00,2.29,0.14),
|
||
("15",3.44,0.00,1.98,0),("16",3.59,0.00,1.87,0),("17",1.33,0.00,0.67,0),
|
||
("18",0.86,0.00,0.76,0.07),("19",0.12,0.00,0.12,0.09)]
|
||
|
||
def couvre(ac):
|
||
return sum(1 for n in NUITS if n <= ac + 1e-9)
|
||
|
||
def calc():
|
||
out = []
|
||
for code, nom, mods, par, ton in SCENARIOS:
|
||
dc = mods * par
|
||
ac = dc * ONDULEUR
|
||
n = couvre(ac)
|
||
out.append(dict(code=code, nom=nom, mods=mods, dc=dc, ac=ac,
|
||
n=n, pct=round(100 * n / len(NUITS)), ton=ton))
|
||
return out
|
||
|
||
# ----------------------------------------------------------------- graphiques
|
||
def histo_nuits(sc):
|
||
"""Repartition des 37 nuits, avec les seuils de chaque scenario."""
|
||
x0, x1, lo, hi = 74, 940, 9.5, 19.2
|
||
px = lambda v: x0 + (v - lo) / (hi - lo) * (x1 - x0)
|
||
bins = {}
|
||
for v in NUITS:
|
||
b = int((v - lo) // 0.5)
|
||
bins[b] = bins.get(b, 0) + 1
|
||
mx = max(bins.values())
|
||
base, hmax = 250, 168
|
||
s = [f'<svg viewBox="0 0 1000 330" role="img" aria-label="Répartition des 37 nuits">']
|
||
# grille
|
||
for v in range(10, 20):
|
||
s.append(f'<line class="grille" x1="{px(v):.0f}" y1="46" x2="{px(v):.0f}" y2="{base}"/>')
|
||
s.append(f'<text class="axe" x="{px(v):.0f}" y="{base+24}" text-anchor="middle">{v}</text>')
|
||
s.append(f'<text class="axe unite" x="{x1}" y="{base+48}" text-anchor="end">kWh consommés par la maison, de 20 h à 9 h</text>')
|
||
for b, n in sorted(bins.items()):
|
||
g = lo + b * 0.5
|
||
w = (px(g + 0.5) - px(g)) - 3
|
||
h = n / mx * hmax
|
||
s.append(f'<rect class="barre-nuit" x="{px(g)+1.5:.1f}" y="{base-h:.1f}" '
|
||
f'width="{w:.1f}" height="{h:.1f}" rx="2"><title>{n} nuit(s) entre '
|
||
f'{g:.1f} et {g+0.5:.1f} kWh</title></rect>')
|
||
s.append(f'<text class="compte" x="{px(g+0.25):.0f}" y="{base-h-7:.0f}" text-anchor="middle">{n}</text>')
|
||
# seuils
|
||
for i, r in enumerate(sc):
|
||
if r['ac'] > hi or r['ac'] < lo: continue
|
||
x = px(r['ac'])
|
||
cl = 'seuil-etat' if r['ton'] == 'etat' else 'seuil'
|
||
s.append(f'<line class="{cl}" x1="{x:.0f}" y1="34" x2="{x:.0f}" y2="{base}"/>')
|
||
anc = 'end' if r['ac'] > 17.4 else 'start'
|
||
dx = -7 if anc == 'end' else 7
|
||
y = 34 + 15 + (i % 2) * 17
|
||
s.append(f'<text class="etiq {cl}" x="{x+dx:.0f}" y="{y}" text-anchor="{anc}">'
|
||
f'{r["code"]} · {r["ac"]:.1f}'.replace(".", ",") + '</text>')
|
||
s.append('</svg>')
|
||
return '\n'.join(s)
|
||
|
||
def courbe_batterie():
|
||
x0, x1, base, hmax = 96, 940, 236, 156
|
||
n = len(TRANCHES)
|
||
w = (x1 - x0) / n
|
||
mx = 0.18
|
||
s = ['<svg viewBox="0 0 1000 320" role="img" aria-label="Énergie par point de SOC">']
|
||
for v, lab in [(0.05,'0,05'), (0.10,'0,10'), (0.15,'0,15')]:
|
||
y = base - v / mx * hmax
|
||
s.append(f'<line class="grille" x1="{x0}" y1="{y:.0f}" x2="{x1}" y2="{y:.0f}"/>')
|
||
s.append(f'<text class="axe" x="{x0-12}" y="{y+4:.0f}" text-anchor="end">{lab}</text>')
|
||
for i, (lab, k, np_) in enumerate(TRANCHES):
|
||
h = k / mx * hmax
|
||
x = x0 + i * w + 9
|
||
cl = 'barre-bas' if k < 0.12 else 'barre-haut'
|
||
s.append(f'<rect class="{cl}" x="{x:.0f}" y="{base-h:.0f}" width="{w-18:.0f}" '
|
||
f'height="{h:.0f}" rx="3"><title>{np_} paliers mesurés</title></rect>')
|
||
s.append(f'<text class="val" x="{x+(w-18)/2:.0f}" y="{base-h-10:.0f}" text-anchor="middle">'
|
||
f'{k:.3f}".replace"</text>'.replace('".replace"', '').replace('.', ','))
|
||
s.append(f'<text class="axe" x="{x+(w-18)/2:.0f}" y="{base+24:.0f}" text-anchor="middle">{lab} %</text>')
|
||
xg = x0 + 1 * w
|
||
s.append(f'<line class="genou" x1="{xg:.0f}" y1="40" x2="{xg:.0f}" y2="{base}"/>')
|
||
s.append(f'<text class="etiq genou" x="{xg+9:.0f}" y="54">genou à 33 %</text>')
|
||
s.append(f'<text class="axe unite" x="{x1}" y="{base+52}" text-anchor="end">kWh délivrés par point de SOC · 576 paliers mesurés sur 8 jours</text>')
|
||
s.append('</svg>')
|
||
return '\n'.join(s)
|
||
|
||
def journee():
|
||
x0, x1, base, hmax = 74, 950, 226, 150
|
||
n = len(JOURNEE)
|
||
w = (x1 - x0) / n
|
||
mx = 4.3
|
||
s = ['<svg viewBox="0 0 1000 300" role="img" aria-label="Énergie heure par heure">']
|
||
for v in (1, 2, 3, 4):
|
||
y = base - v / mx * hmax
|
||
s.append(f'<line class="grille" x1="{x0}" y1="{y:.0f}" x2="{x1}" y2="{y:.0f}"/>')
|
||
s.append(f'<text class="axe" x="{x0-10}" y="{y+4:.0f}" text-anchor="end">{v}</text>')
|
||
for i, (h, sol, res, mai, voi) in enumerate(JOURNEE):
|
||
x = x0 + i * w
|
||
bw = (w - 8) / 2
|
||
for j, (v, cl) in enumerate([(sol, 'sol'), (res, 'res')]):
|
||
if v <= 0: continue
|
||
ht = v / mx * hmax
|
||
s.append(f'<rect class="b-{cl}" x="{x+3+j*bw:.1f}" y="{base-ht:.1f}" '
|
||
f'width="{bw-1.5:.1f}" height="{ht:.1f}" rx="1.5"><title>{h} h · {v} kWh</title></rect>')
|
||
if i % 2 == 0:
|
||
s.append(f'<text class="axe" x="{x+w/2:.0f}" y="{base+22:.0f}" text-anchor="middle">{h}</text>')
|
||
pts = ' '.join(f'{x0+i*w+w/2:.1f},{base-m/mx*hmax:.1f}' for i,(_,_,_,m,_) in enumerate(JOURNEE))
|
||
s.append(f'<polyline class="ligne-mai" points="{pts}"/>')
|
||
for i, (h, _, _, mai, _) in enumerate(JOURNEE):
|
||
y = base - mai / mx * hmax
|
||
s.append(f'<circle class="pt-mai" cx="{x0+i*w+w/2:.1f}" cy="{y:.1f}" r="3.4">'
|
||
f'<title>{h} h · maison {mai} kWh</title></circle>')
|
||
s.append(f'<text class="axe unite" x="{x1}" y="{base+50}" text-anchor="end">kWh par heure · jeudi 4 septembre 2026</text>')
|
||
s.append('</svg>')
|
||
return '\n'.join(s)
|
||
|
||
sc = calc()
|
||
act = sc[0]
|
||
b = next(r for r in sc if r['code'] == 'B')
|
||
c = next(r for r in sc if r['code'] == 'C')
|
||
|
||
lignes_sc = '\n'.join(f''' <tr class="{r['ton']}">
|
||
<td class="code">{r['code']}</td>
|
||
<td>{r['nom']}</td>
|
||
<td class="n">{r['mods']}</td>
|
||
<td class="n">{r['dc']:.1f}</td>
|
||
<td class="n">{r['ac']:.1f}</td>
|
||
<td class="n forte">{r['n']}<span class="sur">/37</span></td>
|
||
<td class="jauge"><span style="--p:{r['pct']}%"></span><em>{r['pct']} %</em></td>
|
||
</tr>'''.replace('.', ',').replace(',1f', '.1f') for r in sc)
|
||
|
||
html = f'''<title>Dimensionner le parc batterie</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=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700&family=IBM+Plex+Mono:wght@400;600&family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,600;1,6..72,400&display=swap">
|
||
<style>
|
||
:root {{
|
||
--ground:#e7eaee; --surface:#f6f8f9; --surface-2:#dde3e8;
|
||
--ink:#101821; --ink-2:#3d4a57; --muted:#6b7885; --rule:#c6cfd7;
|
||
--solaire:#c9791a; --solaire-d:#a35f10; --stock:#166b78; --stock-d:#0f4e58;
|
||
--reseau:#a8422c; --vert:#3f6b3a;
|
||
--mono:"IBM Plex Mono",ui-monospace,"SF Mono",Menlo,monospace;
|
||
--titre:"Bricolage Grotesque","Helvetica Neue",Arial,sans-serif;
|
||
--corps:"Newsreader",Georgia,"Times New Roman",serif;
|
||
}}
|
||
@media (prefers-color-scheme: dark) {{
|
||
:root:not([data-theme="light"]) {{
|
||
--ground:#0d1319; --surface:#141c24; --surface-2:#1d2831;
|
||
--ink:#e4eaef; --ink-2:#b3c0cb; --muted:#8695a2; --rule:#2b3945;
|
||
--solaire:#e79a3c; --solaire-d:#c47d24; --stock:#3aa5b5; --stock-d:#2b8290;
|
||
--reseau:#d9705a; --vert:#7ab070;
|
||
}}
|
||
}}
|
||
:root[data-theme="dark"] {{
|
||
--ground:#0d1319; --surface:#141c24; --surface-2:#1d2831;
|
||
--ink:#e4eaef; --ink-2:#b3c0cb; --muted:#8695a2; --rule:#2b3945;
|
||
--solaire:#e79a3c; --solaire-d:#c47d24; --stock:#3aa5b5; --stock-d:#2b8290;
|
||
--reseau:#d9705a; --vert:#7ab070;
|
||
}}
|
||
* {{ box-sizing:border-box; }}
|
||
body {{ background:var(--ground); color:var(--ink); font-family:var(--corps);
|
||
font-size:17.5px; line-height:1.62; font-weight:300;
|
||
-webkit-font-smoothing:antialiased; padding:0 20px 96px; }}
|
||
.page {{ max-width:1040px; margin:0 auto; }}
|
||
.col {{ max-width:64ch; }}
|
||
h1,h2,h3,.chapeau,.code,.n,.axe,.val,.etiq,.compte,table,.jauge em,.tag,.puce b {{
|
||
font-family:var(--titre); }}
|
||
h1 {{ font-size:clamp(2.3rem,5.4vw,3.9rem); line-height:1.02; font-weight:700;
|
||
letter-spacing:-.028em; text-wrap:balance; margin:0 0 .5rem;
|
||
max-width:15ch; }}
|
||
.surtitre {{ font-family:var(--mono); font-size:.7rem; letter-spacing:.16em;
|
||
text-transform:uppercase; color:var(--muted); margin:56px 0 20px; }}
|
||
.chapeau {{ font-family:var(--corps); font-size:1.24rem; line-height:1.55;
|
||
color:var(--ink-2); max-width:56ch; font-weight:300; }}
|
||
h2 {{ font-size:1.62rem; font-weight:700; letter-spacing:-.018em;
|
||
margin:72px 0 4px; text-wrap:balance; }}
|
||
h2 + .sous {{ font-family:var(--mono); font-size:.72rem; letter-spacing:.1em;
|
||
text-transform:uppercase; color:var(--muted); margin:0 0 22px; }}
|
||
h3 {{ font-size:1.06rem; font-weight:700; margin:34px 0 6px; letter-spacing:-.01em; }}
|
||
p {{ margin:0 0 1.05em; }}
|
||
strong {{ font-weight:600; color:var(--ink); }}
|
||
em.i {{ font-style:italic; }}
|
||
hr {{ border:0; border-top:1px solid var(--rule); margin:64px 0; }}
|
||
a {{ color:var(--stock); }}
|
||
|
||
/* --- verdict --- */
|
||
.verdict {{ background:var(--surface); border:1px solid var(--rule);
|
||
border-left:4px solid var(--solaire); border-radius:3px;
|
||
padding:26px 30px; margin:38px 0 8px; max-width:66ch; }}
|
||
.verdict p:last-child {{ margin-bottom:0; }}
|
||
.verdict .gros {{ font-family:var(--titre); font-weight:700;
|
||
font-size:1.3rem; line-height:1.3; letter-spacing:-.015em; }}
|
||
|
||
/* --- planches --- */
|
||
.planche {{ background:var(--surface); border:1px solid var(--rule);
|
||
border-radius:4px; padding:26px 26px 18px; margin:26px 0 30px; }}
|
||
.planche h4 {{ font-family:var(--titre); font-size:.95rem; font-weight:700;
|
||
margin:0 0 3px; letter-spacing:-.005em; }}
|
||
.planche .note {{ font-size:.88rem; color:var(--muted); margin:0 0 18px;
|
||
max-width:64ch; line-height:1.5; }}
|
||
svg {{ width:100%; height:auto; display:block; overflow:visible; }}
|
||
.grille {{ stroke:var(--rule); stroke-width:1; }}
|
||
.axe {{ fill:var(--muted); font-size:12.5px; }}
|
||
.axe.unite {{ font-size:11.5px; letter-spacing:.02em; }}
|
||
.compte {{ fill:var(--muted); font-size:11px; }}
|
||
.val {{ fill:var(--ink-2); font-size:12px; font-weight:500; }}
|
||
.barre-nuit {{ fill:var(--stock); opacity:.62; }}
|
||
.barre-haut {{ fill:var(--stock); }}
|
||
.barre-bas {{ fill:var(--reseau); }}
|
||
.genou {{ stroke:var(--reseau); stroke-width:1.5; stroke-dasharray:5 4; }}
|
||
text.genou {{ fill:var(--reseau); stroke:none; }}
|
||
.seuil {{ stroke:var(--solaire); stroke-width:1.5; stroke-dasharray:4 3; }}
|
||
text.seuil {{ fill:var(--solaire-d); stroke:none; }}
|
||
.seuil-etat {{ stroke:var(--reseau); stroke-width:2; }}
|
||
text.seuil-etat {{ fill:var(--reseau); stroke:none; }}
|
||
.etiq {{ font-size:12.5px; font-weight:600; letter-spacing:.01em; }}
|
||
.b-sol {{ fill:var(--solaire); }}
|
||
.b-res {{ fill:var(--reseau); opacity:.85; }}
|
||
.pt-mai {{ fill:var(--ground); stroke:var(--ink-2); stroke-width:1.6; }}
|
||
.ligne-mai {{ fill:none; stroke:var(--ink-2); stroke-width:1.6; opacity:.55; }}
|
||
.legende {{ display:flex; flex-wrap:wrap; gap:20px; margin-top:14px;
|
||
font-family:var(--mono); font-size:.72rem; color:var(--muted); }}
|
||
.legende span {{ display:inline-flex; align-items:center; gap:7px; }}
|
||
.legende i {{ width:11px; height:11px; border-radius:2px; display:inline-block; }}
|
||
|
||
/* --- tableau --- */
|
||
.enrobe {{ overflow-x:auto; margin:26px 0 10px;
|
||
border:1px solid var(--rule); border-radius:4px; background:var(--surface); }}
|
||
table {{ border-collapse:collapse; width:100%; min-width:720px; font-size:.9rem; }}
|
||
th {{ font-size:.68rem; letter-spacing:.1em; text-transform:uppercase;
|
||
color:var(--muted); font-weight:600; text-align:left; padding:15px 14px 11px;
|
||
border-bottom:1px solid var(--rule); white-space:nowrap; }}
|
||
td {{ padding:13px 14px; border-bottom:1px solid var(--rule); vertical-align:middle; }}
|
||
tr:last-child td {{ border-bottom:0; }}
|
||
td.n, th.n {{ text-align:right; font-family:var(--mono);
|
||
font-variant-numeric:tabular-nums; }}
|
||
td.code {{ font-weight:700; font-size:1rem; width:42px; }}
|
||
td.forte {{ font-weight:600; font-size:1rem; }}
|
||
.sur {{ color:var(--muted); font-size:.78rem; font-weight:400; }}
|
||
tr.etat {{ background:color-mix(in srgb, var(--reseau) 9%, transparent); }}
|
||
tr.etat td.code {{ color:var(--reseau); font-size:.8rem; font-weight:600; }}
|
||
tr.bon td.code {{ color:var(--stock); }}
|
||
tr.faible td.code {{ color:var(--muted); }}
|
||
.jauge {{ min-width:126px; }}
|
||
.jauge span {{ display:block; height:7px; border-radius:4px;
|
||
background:var(--surface-2); position:relative; overflow:hidden; }}
|
||
.jauge span::after {{ content:""; position:absolute; inset:0 auto 0 0;
|
||
width:var(--p); background:var(--stock); border-radius:4px; }}
|
||
.jauge em {{ font-style:normal; font-size:.74rem; color:var(--muted);
|
||
display:block; margin-top:5px; font-variant-numeric:tabular-nums; }}
|
||
|
||
/* --- chiffres cles --- */
|
||
.cles {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(168px,1fr));
|
||
gap:1px; background:var(--rule); border:1px solid var(--rule);
|
||
border-radius:4px; overflow:hidden; margin:28px 0; }}
|
||
.cles div {{ background:var(--surface); padding:20px 20px 18px; }}
|
||
.cles b {{ font-family:var(--titre); font-size:2.1rem; font-weight:700;
|
||
display:block; line-height:1; letter-spacing:-.03em; }}
|
||
.cles b small {{ font-size:.9rem; font-weight:500; color:var(--muted);
|
||
letter-spacing:0; }}
|
||
.cles span {{ font-size:.84rem; color:var(--muted); display:block; margin-top:8px;
|
||
line-height:1.42; }}
|
||
|
||
/* --- listes --- */
|
||
ol.etapes {{ list-style:none; counter-reset:e; padding:0; margin:26px 0; }}
|
||
ol.etapes li {{ counter-increment:e; position:relative; padding:0 0 22px 46px; }}
|
||
ol.etapes li::before {{ content:counter(e); position:absolute; left:0; top:1px;
|
||
font-family:var(--titre); font-weight:700; font-size:.82rem;
|
||
width:28px; height:28px; border-radius:50%; background:var(--stock);
|
||
color:var(--surface); display:grid; place-items:center; }}
|
||
ol.etapes b {{ font-family:var(--titre); font-weight:700; }}
|
||
ul.puce {{ list-style:none; padding:0; margin:18px 0; }}
|
||
ul.puce li {{ padding:0 0 13px 22px; position:relative; }}
|
||
ul.puce li::before {{ content:""; position:absolute; left:2px; top:.72em;
|
||
width:6px; height:6px; border-radius:50%; background:var(--stock); }}
|
||
.tag {{ font-family:var(--mono); font-size:.66rem; letter-spacing:.09em;
|
||
text-transform:uppercase; padding:3px 8px; border-radius:3px;
|
||
background:var(--surface-2); color:var(--ink-2); white-space:nowrap; }}
|
||
.reserve {{ border-left:3px solid var(--rule); padding-left:22px;
|
||
color:var(--ink-2); font-size:.97rem; margin:26px 0; max-width:62ch; }}
|
||
.reserve strong {{ color:var(--ink); }}
|
||
footer {{ margin-top:78px; padding-top:26px; border-top:1px solid var(--rule);
|
||
font-family:var(--mono); font-size:.72rem; color:var(--muted); line-height:1.8; }}
|
||
</style>
|
||
|
||
<div class="page">
|
||
|
||
<p class="surtitre">Installation Airtria · 4 septembre 2026</p>
|
||
<h1>Dimensionner le parc batterie</h1>
|
||
<p class="chapeau col">Combien de stockage faut-il pour traverser la nuit sans réseau —
|
||
et à partir de quel point chaque module supplémentaire cesse de servir à quelque chose.</p>
|
||
|
||
<div class="verdict">
|
||
<p class="gros">Aujourd'hui, la batterie passe la nuit seule 1 fois sur 37.</p>
|
||
<p>Les 7 modules actifs délivrent {act['ac']:.1f} kWh à la maison. La nuit médiane
|
||
en demande <strong>13,2</strong>, et la plus lourde observée <strong>17,2</strong>.
|
||
Le réseau n'est pas un confort : c'est de l'arithmétique.</p>
|
||
</div>
|
||
|
||
<div class="cles">
|
||
<div><b>13,2<small> kWh</small></b><span>nuit médiane, maison seule, de 20 h à 9 h</span></div>
|
||
<div><b>15,9<small> kWh</small></b><span>9 nuits sur 10 restent en dessous</span></div>
|
||
<div><b>1,65<small> kWh</small></b><span>délivré par module, mesuré — pas 2,4 de plaque</span></div>
|
||
<div><b>37</b><span>nuits complètes mesurées, juillet à septembre</span></div>
|
||
</div>
|
||
|
||
<h2>Ce que la nuit demande</h2>
|
||
<p class="sous">37 nuits complètes · voiture déduite</p>
|
||
|
||
<div class="col">
|
||
<p>Chaque barre compte les nuits dont la consommation tombe dans une tranche
|
||
d'un demi-kilowattheure. La distribution est <strong>serrée</strong> : les deux
|
||
tiers des nuits tiennent entre 12,4 et 14,4 kWh. C'est une bonne nouvelle
|
||
pour dimensionner — il n'y a pas de queue lointaine à couvrir.</p>
|
||
<p>Les traits verticaux marquent ce que chaque scénario sait délivrer. Tout ce
|
||
qui est <em class="i">à gauche</em> du trait est couvert.</p>
|
||
</div>
|
||
|
||
<div class="planche">
|
||
<h4>Répartition des nuits, et ce que chaque scénario couvre</h4>
|
||
<p class="note">Consommation de la maison seule entre 20 h et 9 h, la
|
||
charge du véhicule soustraite. Source : statistiques long terme de Home Assistant,
|
||
fin juillet au 4 septembre 2026.</p>
|
||
{histo_nuits(sc)}
|
||
<div class="legende">
|
||
<span><i style="background:var(--stock);opacity:.62"></i>nuits mesurées</span>
|
||
<span><i style="background:var(--reseau)"></i>parc actuel</span>
|
||
<span><i style="background:var(--solaire)"></i>seuil d'un scénario</span>
|
||
</div>
|
||
</div>
|
||
|
||
<h2>Ce qu'un module délivre vraiment</h2>
|
||
<p class="sous">576 paliers de SOC mesurés</p>
|
||
|
||
<div class="col">
|
||
<p>Un US2000C annonce 2,4 kWh. Ce n'est pas le chiffre à utiliser. En
|
||
intégrant la puissance sur chaque palier où le SOC descend d'un point — 576
|
||
paliers sur huit jours, en écartant ceux où le réseau apportait quoi que ce
|
||
soit — la réalité de <em class="i">ce</em> parc apparaît :</p>
|
||
</div>
|
||
|
||
<div class="planche">
|
||
<h4>Énergie délivrée par point de SOC</h4>
|
||
<p class="note">Plate à 0,169 kWh de 33 à 93 %, sur six tranches indépendantes.
|
||
Puis un genou net : en dessous de 33 %, un point de SOC ne vaut plus que
|
||
<strong>57 %</strong> de ce qu'il vaut en haut d'échelle. Signature
|
||
classique d'un parc vieillissant — cohérent avec un module en panne sur huit
|
||
et un autre qui faiblit.</p>
|
||
{courbe_batterie()}
|
||
</div>
|
||
|
||
<div class="col">
|
||
<p>Dans la fenêtre réellement exploitée — le blocage de charge à 94 % en
|
||
haut, le plancher ESS à 20 % en bas — les 7 modules actifs contiennent
|
||
<strong>11,6 kWh</strong>, soit <strong>1,65 kWh par module</strong>.
|
||
Après restitution par l'onduleur, {act['ac']:.1f} kWh arrivent à la maison.</p>
|
||
</div>
|
||
|
||
<h2>Les scénarios</h2>
|
||
<p class="sous">colonne décisive : le nombre de nuits couvertes</p>
|
||
|
||
<div class="enrobe">
|
||
<table>
|
||
<thead><tr>
|
||
<th></th><th>Scénario</th><th class="n">Modules</th>
|
||
<th class="n">Utile DC</th><th class="n">Livré AC</th>
|
||
<th class="n">Nuits</th><th>Couverture</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
{lignes_sc}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="col">
|
||
<p>Le saut décisif est entre <strong>A</strong> et <strong>B</strong> : réparer
|
||
le module en panne ne fait passer la couverture que de 1 à 5 nuits sur 37.
|
||
Deux modules de plus la portent à 32. <strong>Réparer seul ne résout rien.</strong></p>
|
||
<p>Le scénario <strong>D</strong> mérite un regard : mêmes 10 modules que B, mais
|
||
le vieux groupe retiré. Le blocage de charge à 94 % disparaît avec lui, et
|
||
la même quantité de matériel délivre 1,1 kWh de plus.</p>
|
||
</div>
|
||
|
||
<h2>Le plafond que personne ne mentionne</h2>
|
||
<p class="sous">au-delà de la nuit, le stockage ne tourne plus</p>
|
||
|
||
<div class="col">
|
||
<p>Du stockage au-delà de la consommation nocturne <strong>ne cycle pas</strong>.
|
||
Il se remplit, il attend, et le lendemain midi il est déjà plein — donc il
|
||
n'absorbe rien. La nuit demande 13 à 17 kWh : au-delà d'environ douze
|
||
modules, en été, chaque module supplémentaire est un investissement qui dort.</p>
|
||
<p>La journée du 4 septembre le montre bien. Le réseau se tait à midi et ne
|
||
revient jamais ; la production dépasse largement la maison tout l'après-midi ;
|
||
et pourtant la batterie sature avant 18 h.</p>
|
||
</div>
|
||
|
||
<div class="planche">
|
||
<h4>Une journée entière — jeudi 4 septembre</h4>
|
||
<p class="note">Le réseau n'intervient qu'avant midi, pendant les heures creuses
|
||
et la reprise du plancher ESS. À partir de 11 h la production dépasse la
|
||
maison de plusieurs kilowattheures par heure. Le surplus de l'après-midi n'a
|
||
nulle part où aller.</p>
|
||
{journee()}
|
||
<div class="legende">
|
||
<span><i style="background:var(--solaire)"></i>production solaire</span>
|
||
<span><i style="background:var(--reseau);opacity:.85"></i>réseau importé</span>
|
||
<span><i style="background:var(--ink-2);opacity:.55"></i>maison</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col">
|
||
<p>Le lendemain est annoncé à <strong>57,4 kWh</strong>. En face : une maison
|
||
à 30, une batterie qui peut en prendre 8, et pas de voiture — elle part à 8 h 50.
|
||
<strong>Une trentaine de kilowattheures seront refusés</strong>, et même douze
|
||
modules n'y changeraient presque rien.</p>
|
||
<p>Le surplus de milieu de journée n'est pas un problème de stockage. C'est un
|
||
problème de preneur. La voiture absorbe déjà 9,8 kWh par jour en médiane :
|
||
c'est le meilleur rendement de la liste, et il ne coûte rien.</p>
|
||
</div>
|
||
|
||
<h2>Le point dur : les quatre vieux modules</h2>
|
||
<p class="sous">là où se joue vraiment la décision</p>
|
||
|
||
<div class="col">
|
||
<p>Un module mort, un autre qui faiblit, et un pack qui <strong>bloque la charge
|
||
à 94 %</strong> — tous dans le groupe le plus ancien. Or dans un groupe
|
||
Pylontech en parallèle, le BMS gouverne l'ensemble : <strong>le pack faible impose
|
||
sa loi aux modules neufs</strong>.</p>
|
||
<p>Ajouter des modules neufs derrière quatre modules fatigués, c'est les faire
|
||
vieillir au rythme des anciens et hériter du plafond de 94 %. L'observation
|
||
du propriétaire va dans le même sens : rebrancher le module isolé ferait
|
||
peut-être <em class="i">descendre</em> le blocage à 85 %.</p>
|
||
<p>Ce n'est pas un module à récupérer. C'est un groupe à trancher.</p>
|
||
</div>
|
||
|
||
<h3>Réparation, garantie, reprise</h3>
|
||
<div class="col">
|
||
<p><span class="tag">à confirmer auprès du distributeur</span></p>
|
||
<ul class="puce">
|
||
<li><b>Lire les logs, oui.</b> L'outil est <strong>BatteryView</strong>, de
|
||
Pylontech, sur le port console RS232 du module : historique d'alarmes, tensions
|
||
par cellule, nombre de cycles. C'est ce qui distingue une <em class="i">cellule
|
||
dérivante</em> — souvent récupérable par plusieurs charges lentes et complètes
|
||
qui laissent le BMS équilibrer — d'un <em class="i">verrouillage par décharge
|
||
profonde</em>, que certains distributeurs relancent par une source externe à
|
||
faible courant.</li>
|
||
<li><b>Réparation : non.</b> Le BMS est intégré et le module scellé. Un
|
||
remplacement de cellules par un atelier tiers annule la garantie.</li>
|
||
<li><b>Garantie : c'est la vraie piste.</b> Les US2000C sont couverts une dizaine
|
||
d'années sous conditions, et le recours passe par le <strong>distributeur</strong>,
|
||
pas par Pylontech. Avec les logs, le nombre de cycles et six mois de traçabilité
|
||
d'usage, le dossier est défendable.</li>
|
||
<li><b>Reprise : aucun programme connu.</b> La fin de vie relève de la filière
|
||
DEEE — du recyclage, pas de la valeur.</li>
|
||
<li><b>À vérifier avant de commander :</b> l'US2000C est-il encore commercialisé ?
|
||
S'il ne l'est plus, les scénarios B et C tombent et la question devient celle
|
||
d'un parc homogène neuf.</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>Ce que je ferais</h2>
|
||
<p class="sous">dans cet ordre, sans tout décider aujourd'hui</p>
|
||
|
||
<ol class="etapes col">
|
||
<li><b>Faire lire les logs des quatre vieux modules</b>, pas seulement celui en
|
||
panne. Vous saurez si vous avez un module à remplacer ou un groupe à retirer —
|
||
et cela ne coûte rien.</li>
|
||
<li><b>Brancher le second véhicule</b> les jours de forte production. Vous saurez
|
||
en une journée combien de kilowattheures un second preneur absorbe réellement,
|
||
et ce chiffre déplace le dimensionnement.</li>
|
||
<li><b>Viser 10 à 12 modules équivalents neufs</b>, soit 16 à 20 kWh utiles.
|
||
C'est la plage qui couvre 85 à 100 % des nuits d'été et qui cycle tous les jours.</li>
|
||
<li><b>Si les logs condamnent le vieux groupe, ne pas rafistoler.</b> Un parc
|
||
homogène neuf donne la même capacité en moins d'unités, avec une garantie neuve
|
||
et un plafond de charge qui ne vous suit plus.</li>
|
||
</ol>
|
||
|
||
<div class="reserve">
|
||
<p><strong>Ces chiffres sont tous d'été.</strong> Les compteurs exploitables ne
|
||
remontent qu'à fin juillet. L'hiver, la nuit dure 17 heures au lieu de 13 et la
|
||
production ne recharge pas : aucun dimensionnement raisonnable ne fera passer un
|
||
hiver breton sans réseau, et la stratégie des deux charges en heures creuses
|
||
restera la bonne.</p>
|
||
<p><strong>Dimensionnez pour vos nuits d'été, pas contre l'hiver.</strong> C'est
|
||
en été que le stockage travaille tous les jours, et c'est en été que chaque
|
||
kilowattheure stocké est gratuit.</p>
|
||
</div>
|
||
|
||
<footer>
|
||
Sources : 37 nuits complètes et 38 journées issues des statistiques long terme de
|
||
Home Assistant (fin juillet – 4 septembre 2026) · courbe énergie/SOC établie sur
|
||
576 paliers mesurés · relevé horaire du 4 septembre issu de la base
|
||
d'historisation.<br>
|
||
Capacité par module mesurée sur le parc réel, blocage de charge à 94 % et
|
||
plancher ESS à 20 % inclus. Rendement de restitution retenu : 93 %.
|
||
</footer>
|
||
|
||
</div>
|
||
'''
|
||
|
||
p = pathlib.Path('docs/pages/dimensionnement-batteries.html')
|
||
p.write_text(html, encoding='utf-8')
|
||
print(f"{p} — {len(html)} caracteres")
|
||
for r in sc:
|
||
print(f" {r['code']:<12} {r['mods']:>2} modules {r['dc']:>5.1f} DC {r['ac']:>5.1f} AC {r['n']:>2}/37 {r['pct']:>3} %")
|