docs(maquette): « Configurer · Batterie » — septième écran, et un brief pour le moteur

La batterie devient une charge du waterfall, donc son bouton Configurer ouvre un écran de
mécanisme comme les cinq autres. Ajouté à la planche existante plutôt qu'à part : c'est
précisément la comparaison côte à côte qui avait révélé « six écrans, trois contrats » avant
qu'une ligne ne soit écrite.

RIEN DE CET ÉCRAN N'EXISTE. Ni BatteryAdapter, ni entrée LoadConfig pour le stockage, ni
prévision PV. Les seuls leviers réels de .75 sont les cinq actions de la classe SunSpec
Storage, et l'état réel affiché se limite à ce que le Thing publie — tout le reste porte un
badge « à créer », y compris là où ça alourdit la maquette.

Le rang par défaut est la batterie EN PREMIER, et ce n'est pas un choix de conception : c'est
la reproduction du comportement actuel. Une mise à jour ne change jamais l'arbitrage sans
geste ; la nouveauté est qu'il devient réglable.

Quatre blocs dans Configurer, dont deux qui ont demandé à être reformulés avant d'être
dessinés :

- Les blocages ne sont PAS une question de rang mais de SOURCE. « Ne pas vider la batterie
  dans la voiture la nuit » ne s'exprime pas par un ordre de priorité — la nuit il n'y a pas
  de surplus à répartir. La formulation juste est « quelles charges ont le droit d'être
  servies par la batterie », réglée charge par charge. Le contrat porte déjà
  Source { Solar, GridSource } ; il manque le troisième terme et le droit de le refuser.

- La réserve adaptative est montrée AVEC sa réserve : Victron a conçu Battery Life pour du
  plomb, et sur du LFP le problème traité — la sulfatation — n'existe quasiment pas. Si le
  mécanisme est repris, ce sera pour un motif à nommer, pas par mimétisme.

- La charge différée montre le POINT DE NON-RETOUR, pas la prévision. C'est lui qui garantit
  qu'une prévision fausse ne coûte rien.

- La charge réseau pose le mécanisme seul. Ni prix, ni Tempo, ni marché de gros : le jour où
  une source tarifaire existera, elle remplacera le doigt de l'utilisateur sans que le bloc
  ne change.

Le boost vers le véhicule est HORS Configurer, sur la carte du tableau de bord : c'est le
seul cas où la batterie est une source et non une charge, et c'est un geste immédiat, pas une
intention durable. Trois fins possibles, toutes obligatoires — une action qui ne sait pas
s'arrêter n'a pas sa place sur un écran client.

Et le motif que la planche fait ressortir : charge différée, charge réseau et échéance
véhicule (LM-1009) ont la MÊME forme — une cible, une échéance, un point de non-retour. Trois
colonnes côte à côte le montrent ; le moteur peut l'implémenter une fois.

Trois questions écrites pour le moteur, pas résolues ici : bloquer demande un interdit ACTIF
et non une allocation à zéro, puisque la batterie agit d'elle-même · cet interdit doit
EXPIRER, sinon un HEMS qui tombe laisse la batterie inerte · et controlHealthy == false doit
sortir la batterie de l'arbitrage, comme available == false pour une charge en défaut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
This commit is contained in:
Patrick Schurig 2026-08-27 20:54:57 +02:00
parent a1d0659db7
commit aa22a52187

View File

@ -207,15 +207,75 @@
.pill.load{background:rgba(53,201,138,.12);color:var(--green);border:1px solid rgba(53,201,138,.3)}
.pill.ev{background:rgba(157,140,224,.12);color:var(--violet);border:1px solid rgba(157,140,224,.32)}
.pill.ac{background:rgba(49,163,221,.12);color:var(--blue);border:1px solid rgba(49,163,221,.32)}
.pill.bat{background:rgba(254,193,19,.1);color:var(--yellow);border:1px solid rgba(254,193,19,.32)}
/* ── batterie ───────────────────────────────────────────────────────── */
.newf{font-size:10.5px;color:#ff9d6b;background:rgba(255,157,107,.1);border:1px solid rgba(255,157,107,.34);
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
.perm{display:flex;align-items:center;gap:10px;background:var(--card2);border:1px solid var(--line);
border-radius:11px;padding:10px 11px;margin-bottom:7px}
.perm .ic{width:24px;height:24px;border-radius:7px;display:grid;place-items:center;font-size:13px;flex:none;
background:rgba(255,255,255,.06)}
.perm .nm{flex:1;min-width:0}
.perm .nm b{display:block;font-family:'Poppins';font-weight:600;font-size:12.5px}
.perm .nm span{font-size:10.2px;color:var(--muted2)}
.src{display:flex;gap:5px;flex:none}
.sbtn{font-size:10px;font-family:'Poppins';font-weight:600;border-radius:7px;padding:5px 8px;
border:1px solid var(--line2);background:var(--panel);color:var(--muted2);cursor:pointer}
.sbtn.on{border-color:var(--green);color:var(--green);background:rgba(53,201,138,.1)}
.sbtn.no{border-color:rgba(224,122,122,.4);color:var(--red);background:rgba(224,122,122,.08)}
.tl{margin-top:10px;background:var(--card2);border:1px solid var(--line);border-radius:11px;padding:12px 12px 10px}
.tl .bar{position:relative;height:26px;border-radius:7px;overflow:hidden;
background:linear-gradient(90deg,#0f3242 0%,#123c4f 34%,#16506a 50%,#123c4f 66%,#0f3242 100%)}
.tl .seg1{position:absolute;left:56%;right:14%;top:0;bottom:0;background:rgba(53,201,138,.28)}
.tl .pnr{position:absolute;left:56%;top:-4px;bottom:-4px;width:2px;background:var(--red)}
.tl .pnr::after{content:"";position:absolute;top:-3px;left:-3px;width:8px;height:8px;border-radius:50%;
background:var(--red)}
.tl .peak{position:absolute;left:86%;top:0;bottom:0;width:2px;background:var(--yellow);opacity:.8}
.tl .ax{display:flex;justify-content:space-between;margin-top:6px;font-size:9.5px;color:var(--muted2)}
.tl .lg{margin-top:9px;font-size:10.5px;color:var(--muted2);line-height:1.55}
.tl .lg em{font-style:normal;color:var(--red);font-weight:600}
.tl .lg u{text-decoration:none;color:var(--yellow);font-weight:600}
.boost{border:1px solid rgba(157,140,224,.35);background:rgba(157,140,224,.07);border-radius:13px;padding:13px}
.boost .t{display:flex;align-items:center;gap:9px;margin-bottom:8px}
.boost .t b{font-family:'Poppins';font-weight:600;font-size:13px;flex:1}
.boost .t u{text-decoration:none;font-size:16px}
.boost .end{margin-top:9px;border-top:1px dashed rgba(157,140,224,.3);padding-top:9px;
font-size:10.6px;color:var(--muted2);line-height:1.55}
.boost .end b{color:var(--violet)}
.gauge{display:flex;align-items:center;gap:9px;margin:9px 0 2px}
.gauge .tr{flex:1;height:7px;border-radius:20px;background:rgba(255,255,255,.09);overflow:hidden}
.gauge .fl{height:100%;width:64%;background:var(--yellow)}
.gauge b{font-family:'Poppins';font-weight:700;font-size:13px}
.ask{margin-top:12px;border:1px solid rgba(224,122,122,.3);background:rgba(224,122,122,.06);
border-radius:11px;padding:11px 12px;font-size:11px;color:var(--muted);line-height:1.55}
.ask b{color:var(--red);display:block;font-family:'Poppins';font-size:11.5px;margin-bottom:5px}
.ask ol{margin:6px 0 0;padding-left:16px} .ask li{margin-bottom:6px}
.wide{max-width:824px;width:824px}
.wide h3{font-family:'Poppins';font-weight:600;font-size:13px;margin:0 0 7px;color:var(--txt)}
.mot{display:flex;gap:10px;margin:10px 0 4px;flex-wrap:wrap}
.mot .m{flex:1;min-width:230px;background:var(--card2);border:1px solid var(--line);border-radius:11px;padding:11px}
.mot .m b{display:block;font-family:'Poppins';font-weight:600;font-size:12px;margin-bottom:5px;color:var(--blue)}
.mot .m span{font-size:10.6px;color:var(--muted2);line-height:1.5;display:block}
</style>
</head>
<body>
<div class="head">
<h1>Six écrans, trois contrats — et ils ne se ressemblent pas</h1>
<h1>Sept écrans, quatre contrats — et ils ne se ressemblent pas</h1>
<p>Le bouton <b>Configurer</b> ne peut pas être un formulaire unique. SG-Ready est une
<code>LoadConfig</code> comme le routeur de relais ; la borne de recharge et les zones de climatisation
n'en sont pas du tout — elles vivent sur d'autres namespaces, avec d'autres champs et d'autres tiers.</p>
<p><b>Ajout du 2026-08-27 — la batterie, septième écran.</b> Elle devient une charge du waterfall, donc
son bouton <b>Configurer</b> ouvre un écran de mécanisme comme les autres. <b>Rien de ce qui suit
n'existe côté box</b> : ni <code>BatteryAdapter</code>, ni entrée <code>LoadConfig</code> pour le
stockage, ni prévision PV. Cette colonne est un <b>brief pour le moteur</b>, comme les six autres
l'ont été — et les seuls leviers réels de <code>.75</code> sont les cinq actions de la classe
<code>SunSpec Storage</code> : <code>enableCharging</code>, <code>enableDischarging</code>,
<code>chargingRate</code>, <code>dischargingRate</code>, <code>gridCharging</code>, plus l'état
<code>controlHealthy</code>. Tout le reste est à créer.</p>
<p><b>Deux écarts relevés en introspectant <code>.75</code> (plugin 1.15.2+etm15) :</b>
① le <code>ChargingInfo</code> réellement déployé ne porte <b>ni <code>mode</code>, ni <code>minCurrent</code>,
ni <code>targetSoc</code>, ni <code>endTime</code></b> — les noms écrits dans <code>CLAUDE.md</code> ne sont pas
@ -996,6 +1056,247 @@
</div>
</div>
<!-- ══════════ BATTERIE ══════════ -->
<div class="col">
<div class="phone">
<div class="appbar"><div class="ic">‹</div><h2>Configurer · Batterie</h2><div class="ic"></div></div>
<div class="scroll">
<div class="field">
<label>Nom de la charge <span class="newf">à créer · LoadConfig.label</span></label>
<input class="inp" value="Batterie">
</div>
<div class="field">
<label>Rang de service <span class="newf">à créer · LoadConfig.priority</span><br>
<small>où le stockage se sert dans la cascade du surplus</small></label>
<div class="stepper">
<div class="lab">Position<small>1 = servi en premier</small></div>
<div class="ctl"><button class="pm">−</button><b>1</b><button class="pm">+</button></div>
</div>
<div class="info"><b>Défaut : la batterie en premier.</b> Ce n'est pas un choix de conception,
c'est la reproduction du comportement actuel — aujourd'hui l'onduleur charge avant que le HEMS
ne serve quoi que ce soit. <b>Une mise à jour ne change jamais l'arbitrage sans geste.</b> La
nouveauté n'est pas l'ordre, c'est qu'il devient réglable : glisser « Eau chaude » au-dessus
suffit alors à obtenir « ECS d'abord, batterie ensuite ».</div>
</div>
<div class="field">
<label>État réel <span class="derived-flag">lecture seule · SunSpec Storage</span></label>
<div class="state"><span class="dot"></span><b>50 % · 10 kWh · 0 W</b><i>Holding</i></div>
<div class="mode-hint"><code>batteryLevel</code> · <code>capacity</code> ·
<code>currentPower</code> · <code>storageStatus</code> — publiés par le Thing, jamais
recalculés ici.</div>
</div>
<!-- a. BLOCAGES PAR INTENTION -->
<div class="field">
<label>Qui a le droit d'être servi par la batterie
<span class="newf">à créer · Source par charge</span><br>
<small>réglé PAR CHARGE, pas par rang</small></label>
<div class="perm">
<div class="ic">🚿</div>
<div class="nm"><b>Eau chaude</b><span>rang 2 · relay-router</span></div>
<div class="src"><span class="sbtn on">Solaire</span><span class="sbtn no">Batterie</span></div>
</div>
<div class="perm">
<div class="ic">🔥</div>
<div class="nm"><b>PAC banc</b><span>rang 3 · sg-ready</span></div>
<div class="src"><span class="sbtn on">Solaire</span><span class="sbtn on">Batterie</span></div>
</div>
<div class="perm">
<div class="ic">🚗</div>
<div class="nm"><b>Simulated wallbox</b><span>rang 4 · evcharger</span></div>
<div class="src"><span class="sbtn on">Solaire</span><span class="sbtn no">Batterie</span></div>
</div>
<div class="info"><b>Ce n'est pas une question de rang, c'est une question de SOURCE.</b>
« Bloquer la décharge pendant la recharge nocturne du véhicule » ne se dit pas avec un ordre de
priorité : la nuit, il n'y a pas de surplus à répartir, il y a une batterie qu'on ne veut pas
vider dans la voiture. La formulation juste est donc <b>quelles charges ont le droit d'être
servies par la batterie</b>, et elle se règle charge par charge.<br><br>
Le contrat porte déjà <code>Source { Solar, GridSource }</code> — il manque le troisième terme,
et le droit de le refuser par charge.</div>
</div>
<!-- b. CHARGE DIFFÉRÉE -->
<div class="field">
<label>Charge différée sur prévision <span class="newf">à créer · prévision PV absente</span></label>
<div class="stepper">
<div class="lab">Viser 100 % juste avant la pointe<small>au lieu de plafonner dès midi</small></div>
<div class="ctl"><span class="sw"></span></div>
</div>
<div class="stepper">
<div class="lab">Prête à<small>2 à 3 h avant la pointe du soir</small></div>
<div class="ctl"><b>16:30</b></div>
</div>
<div class="tl">
<div class="bar"><div class="seg1"></div><div class="pnr"></div><div class="peak"></div></div>
<div class="ax"><span>06:00</span><span>12:00</span><span>18:00</span><span>22:00</span></div>
<div class="lg">
<em>┃ point de non-retour — 13:20</em><br>
Avant : la charge attend, le pic PV va à l'ECS et à la voiture.<br>
Après : on charge de <b>tout ce qui reste</b>, prévision ou pas — une prévision qui se trompe
ne doit jamais coûter une batterie vide. <u>┃ pointe du soir</u>
</div>
</div>
<div class="mode-hint"><b>Ce que ça gagne :</b> le pic PV de midi est libéré pour l'ECS et le
véhicule, le vieillissement calendaire baisse (moins de temps passé à 100 %), et
<b>aucun kWh n'est sacrifié</b> — la même énergie est stockée, plus tard.</div>
</div>
<!-- c. RÉSERVE ADAPTATIVE -->
<div class="field">
<label>Réserve adaptative <span class="newf">à créer · à justifier</span></label>
<div class="stepper">
<div class="lab">Remonter le plancher après une nuit trop basse<small>façon « Battery Life »</small></div>
<div class="ctl"><span class="sw off"></span></div>
</div>
<div class="warn"><b>À reprendre pour la bonne raison, pas par mimétisme.</b>
Victron a conçu <i>Battery Life</i> pour du <b>plomb</b> : une batterie plomb laissée
partiellement déchargée se sulfate, d'où un plancher qui remonte tant qu'une recharge complète
n'a pas eu lieu. <b>Sur du LFP, le problème traité n'existe quasiment pas</b> — pas de
sulfatation, et un séjour à SOC bas n'abîme rien.<br><br>
Si ce mécanisme est repris, ce sera pour un autre motif à nommer explicitement (garder une
réserve de secours, lisser l'autonomie sur plusieurs jours gris). Le copier tel quel
importerait une contrainte de chimie que cette installation n'a pas.</div>
</div>
<!-- d. CHARGE DEPUIS LE RÉSEAU -->
<div class="field">
<label>Charger depuis le réseau <span class="newf">à créer · gridCharging</span><br>
<small>le mécanisme seul — ce qui le déclenche est hors périmètre</small></label>
<div class="seg"><button class="on">Manuel</button><button>Plage horaire</button></div>
<div class="stepper" style="margin-top:8px">
<div class="lab">Cible d'énergie<small>en kWh, pas en %</small></div>
<div class="ctl"><button class="pm">−</button><b>6 kWh</b><button class="pm">+</button></div>
</div>
<div class="stepper">
<div class="lab">Terminé à<small>heure de fin</small></div>
<div class="ctl"><b>06:00</b></div>
</div>
<div class="info"><b>Ni prix, ni Tempo, ni marché de gros ici.</b> Cet écran pose le mécanisme :
soutirer au réseau une quantité donnée avant une heure donnée. <b>Ce qui décide</b> qu'il faut
le faire reste dehors — le jour où une source tarifaire existera, elle remplacera le doigt de
l'utilisateur sans que ce bloc ne change.</div>
</div>
<div class="save"><div class="btn alt">Annuler</div><div class="btn go">Enregistrer</div></div>
</div>
</div>
<div class="cap"><b>Batterie</b> — une charge, enfin</div>
<div class="note">
<span class="pill bat">à créer — BatteryAdapter · LoadConfig</span><br>
<b>Rien de cet écran n'existe.</b> Le stockage n'a aucune entrée <code>GetLoadConfig</code>, et
<code>enableCharging</code> / <code>chargingRate</code> / <code>enableDischarging</code> ont
<b>zéro occurrence</b> dans <code>energyplugin/</code>. Le levier matériel, lui, est déclaré : la
classe <code>SunSpec Storage</code> de <code>.75</code> publie ces cinq actions.
<ul>
<li><b>Aucune valeur affichée ici n'est inventée.</b> L'état réel se limite à ce que le Thing
publie ; tout le reste porte un badge <span class="newf" style="float:none;display:inline-block;margin:0">à créer</span>.</li>
<li><b><code>batteryLevelConsideration</code> fait aujourd'hui l'INVERSE de ce que cet écran
permet.</b> Sous le seuil, le budget est <i>annulé</i> — la batterie passe donc avant, et l'ECS
ne reçoit rien. Si le rang arrive, il faut dire lequel des deux gagne, sinon deux réglages se
défont l'un l'autre en silence.</li>
</ul>
</div>
</div>
<!-- ══════════ BOOST — TABLEAU DE BORD, PAS RÉGLAGES ══════════ -->
<div class="col">
<div class="phone" style="height:auto;min-height:420px">
<div class="appbar"><div class="ic">☰</div><h2>Tableau de bord</h2><div class="ic">⋯</div></div>
<div class="scroll">
<div class="field" style="margin-top:2px">
<label>Batterie <span class="derived-flag">carte du tableau de bord</span></label>
<div class="gauge"><b>64 %</b><div class="tr"><div class="fl"></div></div><span
style="font-size:10.5px;color:var(--muted2)">6,4 / 10 kWh</span></div>
<div class="boost" style="margin-top:12px">
<div class="t"><u>⚡</u><b>Vider dans le véhicule</b><span class="sw off"></span></div>
<div style="font-size:11px;color:var(--muted);line-height:1.55">
Pleine puissance, tout de suite. C'est le <b>seul cas où la batterie est une source</b> et
non une charge — d'où sa place ici, sur un geste immédiat, et non dans les réglages.
</div>
<div class="stepper" style="margin-top:10px">
<div class="lab">S'arrête à<small>cible sur la batterie maison</small></div>
<div class="ctl"><button class="pm">−</button><b>20 %</b><button class="pm">+</button></div>
</div>
<div class="stepper">
<div class="lab">Ou au bout de<small>durée maximale</small></div>
<div class="ctl"><button class="pm">−</button><b>45 min</b><button class="pm">+</button></div>
</div>
<div class="end"><b>Il doit finir tout seul.</b> Cible atteinte, durée écoulée, ou véhicule
débranché — <b>jamais « jusqu'à annulation »</b>. Un transfert sans fin vide une maison
pendant que personne ne regarde, et l'écran qui l'a lancé n'est plus là pour le dire.</div>
</div>
</div>
</div>
</div>
<div class="cap"><b>Boost</b> — hors Configurer</div>
<div class="note">
<span class="pill ev">geste immédiat, pas un réglage</span><br>
Un réglage décrit une intention durable ; ceci est une action ponctuelle avec un effet immédiat et
visible. La mettre dans « Configurer » la rendrait introuvable au moment où on la veut, et
persistante alors qu'elle ne doit pas l'être.
<ul>
<li><b>Trois fins possibles, toutes obligatoires.</b> Une action qui ne sait pas s'arrêter n'a
pas sa place sur un écran client.</li>
<li>Côté box, il faudrait <code>enableDischarging</code> + <code>dischargingRate</code> — déjà
déclarés par la classe.</li>
</ul>
</div>
</div>
<!-- ══════════ LE MOTIF UNIQUE, ET LES QUESTIONS AU MOTEUR ══════════ -->
<div class="col wide">
<div class="note wide">
<h3>Un seul motif, pas trois écrans voisins</h3>
<p style="margin:0 0 4px;color:var(--muted2);font-size:11.5px;line-height:1.55">
Trois fonctionnalités demandées à des moments différents ont <b>exactement la même forme</b>. Si la
maquette le montre comme un motif unique, le moteur peut l'implémenter une fois au lieu de trois —
et l'écran peut le présenter une fois au lieu de trois.</p>
<div class="mot">
<div class="m"><b>Charge différée</b><span>cible : 100 % de la batterie<br>échéance : 2–3 h avant
la pointe<br>non-retour : au-delà, on charge de tout</span></div>
<div class="m"><b>Charge depuis le réseau</b><span>cible : 6 kWh soutirés<br>échéance : 06:00<br>
non-retour : au-delà, pleine puissance</span></div>
<div class="m"><b>Échéance véhicule (LM-1009)</b><span>cible : énergie livrée<br>échéance : heure
de départ<br>non-retour : au-delà, on passe au réseau</span></div>
</div>
<p style="margin:8px 0 0;color:var(--muted2);font-size:11.5px;line-height:1.55">
<b>Une cible, une échéance, un point de non-retour.</b> Le point de non-retour est la partie qui se
perd si on ne la nomme pas : c'est lui qui garantit qu'une prévision fausse ne coûte rien, et c'est
lui qu'un écran doit montrer — pas la prévision elle-même, qui n'engage personne.</p>
<div class="ask">
<b>Trois questions pour le moteur — à trancher, pas à deviner</b>
<ol>
<li><b>Bloquer demande un INTERDIT ACTIF, pas une allocation à zéro.</b> La batterie agit
d'elle-même : l'onduleur charge sans qu'on le lui demande. Ne rien lui allouer ne l'empêche donc
de rien — il faut lui dire <code>enableCharging = false</code>. C'est une différence de nature
avec toutes les autres charges du waterfall, où « 0 W alloué » suffit à ce qu'il ne se passe
rien.</li>
<li><b>Cet interdit doit EXPIRER.</b> Un HEMS qui tombe après avoir posé
<code>enableCharging = false</code> laisse une batterie inerte, sans que rien ne le signale et
sans que personne ne sache pourquoi. Il faut un bail à durée limitée, renouvelé tant que
l'arbitre vit — l'état sûr d'un stockage, c'est « il fait ce qu'il ferait sans nous », pas
« il reste sur le dernier ordre reçu ».</li>
<li><b><code>controlHealthy == false</code> doit SORTIR la batterie de l'arbitrage</b>, comme
<code>available == false</code> sort une charge en défaut. L'état existe déjà sur la classe. Une
batterie qu'on croit piloter et qui ne répond plus fausse le budget en silence, exactement comme
le faisait le double commandeur des bornes avant 3g-2.</li>
</ol>
</div>
</div>
</div>
</div>
</body>
</html>