Page de decision : dimensionnement du parc batterie

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
This commit is contained in:
Tallagrand Thierry
2026-09-04 20:15:44 +02:00
parent ba99c506d7
commit 7c2813a60a
2 changed files with 1100 additions and 0 deletions

View File

@@ -0,0 +1,565 @@
<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 10.7&nbsp;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&nbsp;h à 9&nbsp;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&nbsp;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&nbsp;h et 9&nbsp;h, la
charge du véhicule soustraite. Source : statistiques long terme de Home Assistant,
fin juillet au 4 septembre 2026.</p>
<svg viewBox="0 0 1000 330" role="img" aria-label="Répartition des 37 nuits">
<line class="grille" x1="119" y1="46" x2="119" y2="250"/>
<text class="axe" x="119" y="274" text-anchor="middle">10</text>
<line class="grille" x1="208" y1="46" x2="208" y2="250"/>
<text class="axe" x="208" y="274" text-anchor="middle">11</text>
<line class="grille" x1="297" y1="46" x2="297" y2="250"/>
<text class="axe" x="297" y="274" text-anchor="middle">12</text>
<line class="grille" x1="386" y1="46" x2="386" y2="250"/>
<text class="axe" x="386" y="274" text-anchor="middle">13</text>
<line class="grille" x1="476" y1="46" x2="476" y2="250"/>
<text class="axe" x="476" y="274" text-anchor="middle">14</text>
<line class="grille" x1="565" y1="46" x2="565" y2="250"/>
<text class="axe" x="565" y="274" text-anchor="middle">15</text>
<line class="grille" x1="654" y1="46" x2="654" y2="250"/>
<text class="axe" x="654" y="274" text-anchor="middle">16</text>
<line class="grille" x1="744" y1="46" x2="744" y2="250"/>
<text class="axe" x="744" y="274" text-anchor="middle">17</text>
<line class="grille" x1="833" y1="46" x2="833" y2="250"/>
<text class="axe" x="833" y="274" text-anchor="middle">18</text>
<line class="grille" x1="922" y1="46" x2="922" y2="250"/>
<text class="axe" x="922" y="274" text-anchor="middle">19</text>
<text class="axe unite" x="940" y="298" text-anchor="end">kWh consommés par la maison, de 20 h à 9 h</text>
<rect class="barre-nuit" x="120.1" y="231.3" width="41.6" height="18.7" rx="2"><title>1 nuit(s) entre 10.0 et 10.5 kWh</title></rect>
<text class="compte" x="141" y="224" text-anchor="middle">1</text>
<rect class="barre-nuit" x="209.4" y="212.7" width="41.6" height="37.3" rx="2"><title>2 nuit(s) entre 11.0 et 11.5 kWh</title></rect>
<text class="compte" x="230" y="206" text-anchor="middle">2</text>
<rect class="barre-nuit" x="254.1" y="212.7" width="41.6" height="37.3" rx="2"><title>2 nuit(s) entre 11.5 et 12.0 kWh</title></rect>
<text class="compte" x="275" y="206" text-anchor="middle">2</text>
<rect class="barre-nuit" x="298.7" y="212.7" width="41.6" height="37.3" rx="2"><title>2 nuit(s) entre 12.0 et 12.5 kWh</title></rect>
<text class="compte" x="320" y="206" text-anchor="middle">2</text>
<rect class="barre-nuit" x="343.3" y="82.0" width="41.6" height="168.0" rx="2"><title>9 nuit(s) entre 12.5 et 13.0 kWh</title></rect>
<text class="compte" x="364" y="75" text-anchor="middle">9</text>
<rect class="barre-nuit" x="388.0" y="156.7" width="41.6" height="93.3" rx="2"><title>5 nuit(s) entre 13.0 et 13.5 kWh</title></rect>
<text class="compte" x="409" y="150" text-anchor="middle">5</text>
<rect class="barre-nuit" x="432.6" y="119.3" width="41.6" height="130.7" rx="2"><title>7 nuit(s) entre 13.5 et 14.0 kWh</title></rect>
<text class="compte" x="453" y="112" text-anchor="middle">7</text>
<rect class="barre-nuit" x="477.3" y="212.7" width="41.6" height="37.3" rx="2"><title>2 nuit(s) entre 14.0 et 14.5 kWh</title></rect>
<text class="compte" x="498" y="206" text-anchor="middle">2</text>
<rect class="barre-nuit" x="566.5" y="212.7" width="41.6" height="37.3" rx="2"><title>2 nuit(s) entre 15.0 et 15.5 kWh</title></rect>
<text class="compte" x="587" y="206" text-anchor="middle">2</text>
<rect class="barre-nuit" x="611.2" y="212.7" width="41.6" height="37.3" rx="2"><title>2 nuit(s) entre 15.5 et 16.0 kWh</title></rect>
<text class="compte" x="632" y="206" text-anchor="middle">2</text>
<rect class="barre-nuit" x="655.8" y="231.3" width="41.6" height="18.7" rx="2"><title>1 nuit(s) entre 16.0 et 16.5 kWh</title></rect>
<text class="compte" x="677" y="224" text-anchor="middle">1</text>
<rect class="barre-nuit" x="700.4" y="231.3" width="41.6" height="18.7" rx="2"><title>1 nuit(s) entre 16.5 et 17.0 kWh</title></rect>
<text class="compte" x="721" y="224" text-anchor="middle">1</text>
<rect class="barre-nuit" x="745.1" y="231.3" width="41.6" height="18.7" rx="2"><title>1 nuit(s) entre 17.0 et 17.5 kWh</title></rect>
<text class="compte" x="766" y="224" text-anchor="middle">1</text>
<line class="seuil-etat" x1="185" y1="34" x2="185" y2="250"/>
<text class="etiq seuil-etat" x="192" y="49" text-anchor="start">Actuel · 10,7</text>
<line class="seuil" x1="322" y1="34" x2="322" y2="250"/>
<text class="etiq seuil" x="329" y="66" text-anchor="start">A · 12,3</text>
<line class="seuil" x1="596" y1="34" x2="596" y2="250"/>
<text class="etiq seuil" x="603" y="49" text-anchor="start">B · 15,3</text>
<line class="seuil" x1="870" y1="34" x2="870" y2="250"/>
<text class="etiq seuil" x="863" y="66" text-anchor="end">C · 18,4</text>
<line class="seuil" x1="695" y1="34" x2="695" y2="250"/>
<text class="etiq seuil" x="702" y="49" text-anchor="start">D · 16,5</text>
</svg>
<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&nbsp;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&nbsp;%</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>
<svg viewBox="0 0 1000 320" role="img" aria-label="Énergie par point de SOC">
<line class="grille" x1="96" y1="193" x2="940" y2="193"/>
<text class="axe" x="84" y="197" text-anchor="end">0,05</text>
<line class="grille" x1="96" y1="149" x2="940" y2="149"/>
<text class="axe" x="84" y="153" text-anchor="end">0,10</text>
<line class="grille" x1="96" y1="106" x2="940" y2="106"/>
<text class="axe" x="84" y="110" text-anchor="end">0,15</text>
<rect class="barre-bas" x="105" y="152" width="103" height="84" rx="3"><title>88 paliers mesurés</title></rect>
<text class="val" x="156" y="142" text-anchor="middle">0,097</text>
<text class="axe" x="156" y="260" text-anchor="middle">21-31 %</text>
<rect class="barre-haut" x="226" y="88" width="103" height="148" rx="3"><title>78 paliers mesurés</title></rect>
<text class="val" x="277" y="78" text-anchor="middle">0,171</text>
<text class="axe" x="277" y="260" text-anchor="middle">35-44 %</text>
<rect class="barre-haut" x="346" y="90" width="103" height="146" rx="3"><title>76 paliers mesurés</title></rect>
<text class="val" x="397" y="80" text-anchor="middle">0,169</text>
<text class="axe" x="397" y="260" text-anchor="middle">45-54 %</text>
<rect class="barre-haut" x="467" y="95" width="103" height="141" rx="3"><title>85 paliers mesurés</title></rect>
<text class="val" x="518" y="85" text-anchor="middle">0,163</text>
<text class="axe" x="518" y="260" text-anchor="middle">55-64 %</text>
<rect class="barre-haut" x="587" y="88" width="103" height="148" rx="3"><title>80 paliers mesurés</title></rect>
<text class="val" x="639" y="78" text-anchor="middle">0,171</text>
<text class="axe" x="639" y="260" text-anchor="middle">65-74 %</text>
<rect class="barre-haut" x="708" y="89" width="103" height="147" rx="3"><title>81 paliers mesurés</title></rect>
<text class="val" x="759" y="79" text-anchor="middle">0,170</text>
<text class="axe" x="759" y="260" text-anchor="middle">75-84 %</text>
<rect class="barre-haut" x="828" y="90" width="103" height="146" rx="3"><title>62 paliers mesurés</title></rect>
<text class="val" x="880" y="80" text-anchor="middle">0,169</text>
<text class="axe" x="880" y="260" text-anchor="middle">85-94 %</text>
<line class="genou" x1="217" y1="40" x2="217" y2="236"/>
<text class="etiq genou" x="226" y="54">genou à 33 %</text>
<text class="axe unite" x="940" y="288" text-anchor="end">kWh délivrés par point de SOC · 576 paliers mesurés sur 8 jours</text>
</svg>
</div>
<div class="col">
<p>Dans la fenêtre réellement exploitée — le blocage de charge à 94&nbsp;% en
haut, le plancher ESS à 20&nbsp;% en bas — les 7 modules actifs contiennent
<strong>11,6&nbsp;kWh</strong>, soit <strong>1,65&nbsp;kWh par module</strong>.
Après restitution par l'onduleur, 10.7&nbsp;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&nbsp;DC</th><th class="n">Livré&nbsp;AC</th>
<th class="n">Nuits</th><th>Couverture</th>
</tr></thead>
<tbody>
<tr class="etat">
<td class="code">Actuel</td>
<td>7 modules actifs — un en panne, un qui faiblit</td>
<td class="n">7</td>
<td class="n">11,5</td>
<td class="n">10,7</td>
<td class="n forte">1<span class="sur">/37</span></td>
<td class="jauge"><span style="--p:3%"></span><em>3 %</em></td>
</tr>
<tr class="faible">
<td class="code">A</td>
<td>Réparer le module en panne — 8 modules</td>
<td class="n">8</td>
<td class="n">13,2</td>
<td class="n">12,3</td>
<td class="n forte">5<span class="sur">/37</span></td>
<td class="jauge"><span style="--p:14%"></span><em>14 %</em></td>
</tr>
<tr class="bon">
<td class="code">B</td>
<td>A + 2 neufs — 10 modules</td>
<td class="n">10</td>
<td class="n">16,5</td>
<td class="n">15,3</td>
<td class="n forte">32<span class="sur">/37</span></td>
<td class="jauge"><span style="--p:86%"></span><em>86 %</em></td>
</tr>
<tr class="bon">
<td class="code">C</td>
<td>A + 4 neufs — 12 modules</td>
<td class="n">12</td>
<td class="n">19,8</td>
<td class="n">18,4</td>
<td class="n forte">37<span class="sur">/37</span></td>
<td class="jauge"><span style="--p:100%"></span><em>100 %</em></td>
</tr>
<tr class="bon">
<td class="code">D</td>
<td>4 récents + 6 neufs, vieux pack retiré</td>
<td class="n">10</td>
<td class="n">17,7</td>
<td class="n">16,5</td>
<td class="n forte">35<span class="sur">/37</span></td>
<td class="jauge"><span style="--p:95%"></span><em>95 %</em></td>
</tr>
</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&nbsp;% disparaît avec lui, et
la même quantité de matériel délivre 1,1&nbsp;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&nbsp;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&nbsp;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&nbsp;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>
<svg viewBox="0 0 1000 300" role="img" aria-label="Énergie heure par heure">
<line class="grille" x1="74" y1="191" x2="950" y2="191"/>
<text class="axe" x="64" y="195" text-anchor="end">1</text>
<line class="grille" x1="74" y1="156" x2="950" y2="156"/>
<text class="axe" x="64" y="160" text-anchor="end">2</text>
<line class="grille" x1="74" y1="121" x2="950" y2="121"/>
<text class="axe" x="64" y="125" text-anchor="end">3</text>
<line class="grille" x1="74" y1="86" x2="950" y2="86"/>
<text class="axe" x="64" y="90" text-anchor="end">4</text>
<text class="axe" x="96" y="248" text-anchor="middle">00</text>
<rect class="b-res" x="138.7" y="160.4" width="16.4" height="65.6" rx="1.5"><title>01 h · 1.88 kWh</title></rect>
<rect class="b-res" x="182.5" y="127.3" width="16.4" height="98.7" rx="1.5"><title>02 h · 2.83 kWh</title></rect>
<text class="axe" x="184" y="248" text-anchor="middle">02</text>
<rect class="b-res" x="226.3" y="225.0" width="16.4" height="1.0" rx="1.5"><title>03 h · 0.03 kWh</title></rect>
<rect class="b-res" x="270.1" y="225.3" width="16.4" height="0.7" rx="1.5"><title>04 h · 0.02 kWh</title></rect>
<text class="axe" x="271" y="248" text-anchor="middle">04</text>
<rect class="b-res" x="313.9" y="225.3" width="16.4" height="0.7" rx="1.5"><title>05 h · 0.02 kWh</title></rect>
<rect class="b-res" x="357.7" y="178.6" width="16.4" height="47.4" rx="1.5"><title>06 h · 1.36 kWh</title></rect>
<text class="axe" x="359" y="248" text-anchor="middle">06</text>
<rect class="b-sol" x="383.6" y="225.3" width="16.4" height="0.7" rx="1.5"><title>07 h · 0.02 kWh</title></rect>
<rect class="b-res" x="401.5" y="200.9" width="16.4" height="25.1" rx="1.5"><title>07 h · 0.72 kWh</title></rect>
<rect class="b-sol" x="427.4" y="213.8" width="16.4" height="12.2" rx="1.5"><title>08 h · 0.35 kWh</title></rect>
<rect class="b-res" x="445.3" y="195.7" width="16.4" height="30.3" rx="1.5"><title>08 h · 0.87 kWh</title></rect>
<text class="axe" x="446" y="248" text-anchor="middle">08</text>
<rect class="b-sol" x="471.2" y="198.8" width="16.4" height="27.2" rx="1.5"><title>09 h · 0.78 kWh</title></rect>
<rect class="b-res" x="489.1" y="212.7" width="16.4" height="13.3" rx="1.5"><title>09 h · 0.38 kWh</title></rect>
<rect class="b-sol" x="515.0" y="150.0" width="16.4" height="76.0" rx="1.5"><title>10 h · 2.18 kWh</title></rect>
<rect class="b-res" x="532.9" y="220.4" width="16.4" height="5.6" rx="1.5"><title>10 h · 0.16 kWh</title></rect>
<text class="axe" x="534" y="248" text-anchor="middle">10</text>
<rect class="b-sol" x="558.8" y="79.5" width="16.4" height="146.5" rx="1.5"><title>11 h · 4.2 kWh</title></rect>
<rect class="b-res" x="576.7" y="225.3" width="16.4" height="0.7" rx="1.5"><title>11 h · 0.02 kWh</title></rect>
<rect class="b-sol" x="602.6" y="144.4" width="16.4" height="81.6" rx="1.5"><title>12 h · 2.34 kWh</title></rect>
<text class="axe" x="621" y="248" text-anchor="middle">12</text>
<rect class="b-sol" x="646.4" y="103.6" width="16.4" height="122.4" rx="1.5"><title>13 h · 3.51 kWh</title></rect>
<rect class="b-sol" x="690.2" y="87.2" width="16.4" height="138.8" rx="1.5"><title>14 h · 3.98 kWh</title></rect>
<text class="axe" x="709" y="248" text-anchor="middle">14</text>
<rect class="b-sol" x="734.0" y="106.0" width="16.4" height="120.0" rx="1.5"><title>15 h · 3.44 kWh</title></rect>
<rect class="b-sol" x="777.8" y="100.8" width="16.4" height="125.2" rx="1.5"><title>16 h · 3.59 kWh</title></rect>
<text class="axe" x="797" y="248" text-anchor="middle">16</text>
<rect class="b-sol" x="821.6" y="179.6" width="16.4" height="46.4" rx="1.5"><title>17 h · 1.33 kWh</title></rect>
<rect class="b-sol" x="865.4" y="196.0" width="16.4" height="30.0" rx="1.5"><title>18 h · 0.86 kWh</title></rect>
<text class="axe" x="884" y="248" text-anchor="middle">18</text>
<rect class="b-sol" x="909.2" y="221.8" width="16.4" height="4.2" rx="1.5"><title>19 h · 0.12 kWh</title></rect>
<polyline class="ligne-mai" points="95.9,226.0 139.7,171.2 183.5,139.1 227.3,203.7 271.1,201.9 314.9,202.6 358.7,198.1 402.5,202.6 446.3,197.0 490.1,197.0 533.9,183.1 577.7,181.7 621.5,183.4 665.3,121.7 709.1,146.1 752.9,156.9 796.7,160.8 840.5,202.6 884.3,199.5 928.1,221.8"/>
<circle class="pt-mai" cx="95.9" cy="226.0" r="3.4"><title>00 h · maison 0 kWh</title></circle>
<circle class="pt-mai" cx="139.7" cy="171.2" r="3.4"><title>01 h · maison 1.57 kWh</title></circle>
<circle class="pt-mai" cx="183.5" cy="139.1" r="3.4"><title>02 h · maison 2.49 kWh</title></circle>
<circle class="pt-mai" cx="227.3" cy="203.7" r="3.4"><title>03 h · maison 0.64 kWh</title></circle>
<circle class="pt-mai" cx="271.1" cy="201.9" r="3.4"><title>04 h · maison 0.69 kWh</title></circle>
<circle class="pt-mai" cx="314.9" cy="202.6" r="3.4"><title>05 h · maison 0.67 kWh</title></circle>
<circle class="pt-mai" cx="358.7" cy="198.1" r="3.4"><title>06 h · maison 0.8 kWh</title></circle>
<circle class="pt-mai" cx="402.5" cy="202.6" r="3.4"><title>07 h · maison 0.67 kWh</title></circle>
<circle class="pt-mai" cx="446.3" cy="197.0" r="3.4"><title>08 h · maison 0.83 kWh</title></circle>
<circle class="pt-mai" cx="490.1" cy="197.0" r="3.4"><title>09 h · maison 0.83 kWh</title></circle>
<circle class="pt-mai" cx="533.9" cy="183.1" r="3.4"><title>10 h · maison 1.23 kWh</title></circle>
<circle class="pt-mai" cx="577.7" cy="181.7" r="3.4"><title>11 h · maison 1.27 kWh</title></circle>
<circle class="pt-mai" cx="621.5" cy="183.4" r="3.4"><title>12 h · maison 1.22 kWh</title></circle>
<circle class="pt-mai" cx="665.3" cy="121.7" r="3.4"><title>13 h · maison 2.99 kWh</title></circle>
<circle class="pt-mai" cx="709.1" cy="146.1" r="3.4"><title>14 h · maison 2.29 kWh</title></circle>
<circle class="pt-mai" cx="752.9" cy="156.9" r="3.4"><title>15 h · maison 1.98 kWh</title></circle>
<circle class="pt-mai" cx="796.7" cy="160.8" r="3.4"><title>16 h · maison 1.87 kWh</title></circle>
<circle class="pt-mai" cx="840.5" cy="202.6" r="3.4"><title>17 h · maison 0.67 kWh</title></circle>
<circle class="pt-mai" cx="884.3" cy="199.5" r="3.4"><title>18 h · maison 0.76 kWh</title></circle>
<circle class="pt-mai" cx="928.1" cy="221.8" r="3.4"><title>19 h · maison 0.12 kWh</title></circle>
<text class="axe unite" x="950" y="276" text-anchor="end">kWh par heure · jeudi 4 septembre 2026</text>
</svg>
<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&nbsp;kWh</strong>. En face : une maison
à 30, une batterie qui peut en prendre 8, et pas de voiture — elle part à 8&nbsp;h&nbsp;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&nbsp;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&nbsp;%</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&nbsp;%. 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&nbsp;%.</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&nbsp;kWh utiles.
C'est la plage qui couvre 85 à 100&nbsp;% 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>

535
tools/gen-page-batteries.py Normal file
View File

@@ -0,0 +1,535 @@
# -*- 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}&nbsp;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&nbsp;h à 9&nbsp;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&nbsp;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&nbsp;h et 9&nbsp;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&nbsp;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&nbsp;%</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&nbsp;% en
haut, le plancher ESS à 20&nbsp;% en bas — les 7 modules actifs contiennent
<strong>11,6&nbsp;kWh</strong>, soit <strong>1,65&nbsp;kWh par module</strong>.
Après restitution par l'onduleur, {act['ac']:.1f}&nbsp;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&nbsp;DC</th><th class="n">Livré&nbsp;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&nbsp;% disparaît avec lui, et
la même quantité de matériel délivre 1,1&nbsp;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&nbsp;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&nbsp;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&nbsp;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&nbsp;kWh</strong>. En face : une maison
à 30, une batterie qui peut en prendre 8, et pas de voiture — elle part à 8&nbsp;h&nbsp;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&nbsp;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&nbsp;%</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&nbsp;%. 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&nbsp;%.</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&nbsp;kWh utiles.
C'est la plage qui couvre 85 à 100&nbsp;% 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} %")