etm-powersync-app/docs/mockups/configurer_mecanismes_mockup.html
Patrick Schurig b298c366d8 docs(maquette): la charge différée sort de l'écran — sans prévision, elle ne se règle pas
Quatrième passe, même logique que la précédente : ce qui demande un calcul revient à Héos,
pas à un formulaire. Démarrer assez tard pour arriver à 100 % juste avant la pointe suppose
de savoir ce que le soleil va faire — sans prévision ça ne marche pas, et une heure saisie à
la main serait un pari présenté comme un réglage.

Le gain reste écrit dans la note, parce qu'il justifie que le moteur s'en occupe : pic PV de
midi libéré pour l'ECS et la voiture, moins de temps passé à 100 % donc moins de
vieillissement calendaire, et aucun kWh sacrifié — la même énergie, stockée plus tard.

La frise du point de non-retour n'est PAS supprimée : elle remonte dans l'encart du motif
commun, où elle illustre ce que les trois cas partagent au lieu d'illustrer un réglage. C'est
même sa vraie place — une prévision n'engage personne, un point de non-retour si.

Et l'encart gagne une distinction qui manquait : quels paramètres sont calculés (charge
différée → Héos) et lesquels sont saisis (charge réseau, échéance véhicule). Le motif tient
dans les deux cas, ce qui renforce l'argument : le moteur porte la forme, Héos ou
l'utilisateur la remplit.

L'écran de configuration batterie ne garde donc que quatre blocs : le nom, le rang en lecture,
la décharge pendant recharge VE, la réserve adaptative, et la charge réseau manuelle. Quatre
passes, quatre retraits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
2026-08-27 21:25:41 +02:00

1281 lines
77 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ETM PowerSync — Configurer : les cinq mécanismes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--bg:#081f2b; --panel:#0d2b3b; --card:#12384a; --card2:#0f3242;
--line:rgba(255,255,255,.08); --line2:rgba(255,255,255,.14);
--blue:#31a3dd; --yellow:#fec113; --green:#35c98a; --red:#e07a7a; --violet:#9d8ce0;
--txt:#e8f1f6; --muted:#90a8b6; --muted2:#6f8b9a;
}
*{box-sizing:border-box}
body{margin:0;background:
radial-gradient(900px 500px at 15% -10%, #0e3145 0%, transparent 60%),
radial-gradient(900px 600px at 110% 10%, #0b2535 0%, transparent 55%),
var(--bg);
font-family:'Inter',system-ui,sans-serif;color:var(--txt);min-height:100vh;padding:34px 20px 60px}
.head{max-width:1300px;margin:0 auto 26px}
.head h1{font-family:'Poppins';font-weight:700;font-size:20px;margin:0 0 6px}
.head p{margin:0 0 8px;color:var(--muted);font-size:13.5px;line-height:1.55;max-width:900px}
.head code{color:#9fd4ee} .head b{color:var(--yellow)}
.stage{display:flex;gap:34px;justify-content:center;flex-wrap:wrap;align-items:flex-start}
.col{display:flex;flex-direction:column;align-items:center;gap:12px;max-width:392px}
.cap{color:var(--muted2);font-size:12px;font-family:'Poppins';font-weight:500;letter-spacing:.4px;text-transform:uppercase}
.cap b{color:var(--blue)}
.phone{width:392px;background:var(--panel);border:1px solid var(--line2);border-radius:30px;
overflow:hidden;box-shadow:0 30px 80px -30px rgba(0,0,0,.7);display:flex;flex-direction:column;height:880px}
.appbar{display:flex;align-items:center;gap:12px;padding:18px 16px 14px;border-bottom:1px solid var(--line);
background:linear-gradient(#102f40,#0d2b3b)}
.appbar .ic{width:26px;height:26px;display:grid;place-items:center;color:var(--muted);font-size:18px}
.appbar h2{font-family:'Poppins';font-weight:600;font-size:15px;margin:0;flex:1;text-align:center}
.scroll{overflow:auto;padding:16px 15px 26px}
.scroll::-webkit-scrollbar{width:0}
.field{margin-top:18px}
.field:first-child{margin-top:4px}
.field>label{display:block;font-family:'Poppins';font-weight:600;font-size:12px;color:var(--muted);
letter-spacing:.3px;margin-bottom:9px}
.field>label small{color:var(--muted2);font-weight:500;letter-spacing:0}
.saved-flag{font-size:10.5px;color:var(--green);background:rgba(53,201,138,.12);border:1px solid rgba(53,201,138,.3);
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
.other-flag{font-size:10.5px;color:var(--violet);background:rgba(157,140,224,.12);border:1px solid rgba(157,140,224,.32);
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
.todo-flag{font-size:10.5px;color:var(--yellow);background:rgba(254,193,19,.1);border:1px solid rgba(254,193,19,.32);
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
.derived-flag{font-size:10.5px;color:var(--muted2);background:rgba(255,255,255,.05);border:1px solid var(--line2);
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
.inp{width:100%;background:var(--card2);border:1px solid var(--line2);border-radius:10px;color:var(--txt);
font-family:'Poppins';font-weight:600;font-size:13.5px;padding:11px 12px}
.sel{width:100%;background:var(--card2);border:1px solid var(--line2);border-radius:10px;color:var(--txt);
font-family:'Inter';font-size:12.5px;padding:11px 12px;display:flex;align-items:center;gap:8px}
.sel i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:11px}
.mode-hint{margin-top:9px;font-size:11.5px;color:var(--muted2);line-height:1.5}
.mode-hint b{color:var(--muted)}
.seg{display:flex;background:var(--card2);border:1px solid var(--line);border-radius:11px;padding:4px;gap:4px}
.seg button{flex:1;border:0;background:transparent;color:var(--muted);font-family:'Poppins';font-weight:600;
font-size:11.5px;padding:9px 4px;border-radius:8px;cursor:pointer}
.seg button.on{background:var(--blue);color:#04222f}
/* table d'états SG-Ready */
.st{border:1px solid var(--line);border-radius:12px;background:var(--card2);margin-bottom:9px;padding:11px 12px}
.st .h{display:flex;align-items:center;gap:9px;margin-bottom:8px}
.st .badge{width:24px;height:24px;border-radius:7px;font-family:'Poppins';font-weight:700;font-size:12px;
display:grid;place-items:center;flex:none}
.st .badge.s1{background:rgba(224,122,122,.18);color:var(--red)}
.st .badge.s2{background:rgba(255,255,255,.08);color:var(--muted)}
.st .badge.s3{background:rgba(53,201,138,.16);color:var(--green)}
.st .badge.s4{background:rgba(254,193,19,.16);color:var(--yellow)}
.st .h b{font-family:'Poppins';font-weight:600;font-size:12.5px;flex:1}
.st .h span{font-size:10.5px;color:var(--muted2)}
.st .contacts{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px}
.kchip{font-size:11px;border-radius:8px;padding:5px 9px;border:1px solid var(--line2);
background:var(--panel);color:var(--muted);font-family:'Poppins';font-weight:600;cursor:pointer}
.kchip.on{border-color:var(--blue);color:var(--blue);background:rgba(49,163,221,.1)}
.st .pw{display:flex;align-items:center;gap:9px}
.st .pw label{font-size:11px;color:var(--muted2);flex:1}
.st .pw input{width:96px;background:var(--panel);border:1px solid var(--line2);border-radius:9px;color:var(--txt);
font-family:'Poppins';font-weight:600;font-size:12.5px;padding:8px;text-align:center}
.st.locked{opacity:.62}
.stepper{display:flex;align-items:center;justify-content:space-between;padding:11px 12px;border:1px solid var(--line);
border-radius:11px;background:var(--card2);margin-bottom:8px}
.stepper .lab{font-size:12.5px}.stepper .lab small{display:block;color:var(--muted2);font-size:10.5px;margin-top:1px}
.stepper .ctl{display:flex;align-items:center;gap:13px}
.stepper .ctl b{font-family:'Poppins';font-weight:600;font-size:13.5px;min-width:56px;text-align:right}
.pm{width:26px;height:26px;border-radius:8px;border:1px solid var(--line2);color:var(--blue);background:transparent;
font-size:16px;display:grid;place-items:center;cursor:pointer}
.sw{width:38px;height:22px;border-radius:20px;background:rgba(53,201,138,.85);position:relative;cursor:pointer;flex:none}
.sw::after{content:"";position:absolute;top:2px;left:18px;width:18px;height:18px;border-radius:50%;background:#fff}
.sw.off{background:rgba(255,255,255,.16)} .sw.off::after{left:2px}
.warn{margin-top:10px;border:1px solid rgba(224,122,122,.32);background:rgba(224,122,122,.07);
border-radius:11px;padding:10px 11px;font-size:11px;color:var(--muted);line-height:1.5}
.warn b{color:var(--red);display:block;font-family:'Poppins';font-size:11.5px;margin-bottom:3px}
.info{margin-top:10px;border:1px solid rgba(49,163,221,.26);background:rgba(49,163,221,.06);
border-radius:11px;padding:10px 11px;font-size:11px;color:var(--muted);line-height:1.5}
.info b{color:var(--blue);display:block;font-family:'Poppins';font-size:11.5px;margin-bottom:3px}
.blocked{border:1px dashed rgba(254,193,19,.35);background:rgba(254,193,19,.05);border-radius:12px;padding:12px}
.blocked p{margin:9px 0 0;font-size:10.8px;color:var(--muted2);line-height:1.5} .blocked p b{color:var(--yellow)}
.blocked .row{display:flex;align-items:center;gap:10px;opacity:.55}
.blocked .row .lab{font-size:12.5px;flex:1}
/* EV */
.evmode{display:flex;gap:8px}
.evmode button{flex:1;border:1px solid var(--line2);background:var(--card2);color:var(--muted);
font-family:'Poppins';font-weight:600;font-size:12px;padding:14px 6px;border-radius:12px;cursor:pointer;
display:flex;flex-direction:column;align-items:center;gap:5px}
.evmode button.on{border-color:var(--green);background:rgba(53,201,138,.1);color:var(--green)}
.evmode button u{text-decoration:none;font-size:17px}
.evmode button small{font-weight:500;font-size:9.5px;color:var(--muted2)}
.evmode button.on small{color:rgba(53,201,138,.75)}
.maps{margin-top:10px;font-size:10.8px;color:var(--muted2);line-height:1.6}
.maps code{color:#9fd4ee}
.state{display:flex;align-items:center;gap:9px;background:var(--card2);border:1px solid var(--line);
border-radius:11px;padding:11px 12px}
.state .dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px rgba(53,201,138,.15)}
.state b{font-family:'Poppins';font-size:12.5px;font-weight:600;flex:1}
.state i{font-style:normal;font-size:10.5px;color:var(--muted2)}
/* zones */
.zone{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:12px;margin-bottom:9px}
.zone .z1{display:flex;align-items:center;gap:10px}
.zone .z1 b{font-family:'Poppins';font-weight:600;font-size:13.5px;flex:1}
.zone .temp{font-family:'Poppins';font-weight:700;font-size:17px}
.zone .temp small{font-size:11px;font-weight:500;color:var(--muted2);display:block;text-align:right}
.zone .z2{margin-top:9px;display:flex;gap:6px;flex-wrap:wrap}
.tchip{font-size:10.5px;border-radius:7px;padding:4px 8px;border:1px solid var(--line);
background:var(--card2);color:var(--muted2)}
.zone .ovr{margin-top:9px;padding-top:9px;border-top:1px dashed var(--line);font-size:11px;color:var(--yellow)}
.empty{border:1px dashed var(--line2);border-radius:13px;padding:22px 14px;text-align:center}
.empty b{display:block;font-family:'Poppins';font-size:13px;margin-bottom:5px}
.empty p{margin:0 0 13px;font-size:11.5px;color:var(--muted2);line-height:1.5}
.empty button{background:var(--blue);color:#04222f;border:0;border-radius:10px;font-family:'Poppins';
font-weight:600;font-size:12.5px;padding:10px 16px;cursor:pointer}
.rowsel{display:flex;align-items:center;gap:9px;background:var(--card2);border:1px solid var(--line);
border-radius:11px;padding:10px 11px;margin-bottom:7px}
.rowsel .lab{font-size:12px;flex:1}
.rowsel .cnt{font-size:11px;color:var(--muted2)}
.rowsel i{font-style:normal;color:var(--muted2);font-size:11px}
/* lignes de relais */
.relay{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.relay .k{width:30px;font-family:'Poppins';font-weight:700;font-size:12px;color:var(--muted2);flex:none}
.relay .thing{flex:1;background:var(--card2);border:1px solid var(--line2);border-radius:10px;
font-size:12.5px;padding:10px 11px;display:flex;align-items:center;gap:6px;min-width:0}
.relay .thing i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:10px}
.relay .w{width:88px;flex:none;background:var(--card2);border:1px solid var(--line2);border-radius:10px;
color:var(--txt);font-family:'Poppins';font-weight:600;font-size:13px;padding:10px 8px;text-align:center}
.relay .del{color:var(--muted2);font-size:14px;cursor:pointer;flex:none;width:14px;text-align:center}
.addrow{width:100%;background:transparent;border:1px dashed var(--line2);color:var(--muted);
font-family:'Poppins';font-weight:600;font-size:12px;padding:10px;border-radius:10px;cursor:pointer}
.preview{margin-top:11px;border:1px solid rgba(53,201,138,.22);background:rgba(53,201,138,.06);
border-radius:11px;padding:11px 12px;font-size:11.5px;color:var(--muted);line-height:1.55}
.preview b{color:var(--green);font-family:'Poppins'}
.preview em{display:block;font-style:normal;color:var(--muted2);font-size:10.5px;margin-top:6px}
/* paliers (modulable) */
.chips{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.pchip{display:flex;align-items:center;gap:8px;background:var(--card2);border:1px solid var(--line2);
border-radius:9px;padding:9px 11px;font-family:'Poppins';font-weight:600;font-size:13px}
.pchip i{font-style:normal;color:var(--muted2);font-weight:500;font-size:10.5px}
.pchip.off{opacity:.6} .pchip .x{color:var(--muted2);cursor:pointer;font-size:12px}
.add{width:34px;height:34px;border-radius:9px;border:1px dashed var(--line2);color:var(--muted);
display:grid;place-items:center;font-size:18px;background:transparent;cursor:pointer}
.assist-btn{margin-top:11px;display:flex;align-items:center;gap:8px;width:100%;justify-content:center;
background:transparent;border:1px dashed rgba(49,163,221,.4);color:var(--blue);font-family:'Poppins';
font-weight:600;font-size:12px;padding:10px;border-radius:10px;cursor:pointer}
/* sélecteur de things */
.search{width:100%;background:var(--card2);border:1px solid var(--line2);border-radius:11px;color:var(--muted2);
font-size:12.5px;padding:11px 12px;margin-bottom:11px}
.filters{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
.fchip{font-size:11px;border-radius:20px;padding:5px 11px;border:1px solid var(--line2);
background:var(--card2);color:var(--muted);font-family:'Poppins';font-weight:600;cursor:pointer}
.fchip.on{border-color:var(--blue);color:var(--blue);background:rgba(49,163,221,.1)}
.pickrow{display:flex;align-items:center;gap:11px;background:var(--card2);border:1px solid var(--line);
border-radius:11px;padding:11px 12px;margin-bottom:7px}
.cb{width:19px;height:19px;border-radius:6px;border:2px solid var(--muted2);flex:none}
.cb.on{border-color:var(--blue);background:var(--blue);position:relative}
.cb.on::after{content:"✓";position:absolute;inset:0;display:grid;place-items:center;color:#04222f;
font-size:12px;font-weight:700}
.pickrow .nm{flex:1;min-width:0}
.pickrow .nm b{display:block;font-size:12.5px;font-family:'Poppins';font-weight:600}
.pickrow .nm span{font-size:10.5px;color:var(--muted2)}
.pickrow .taken{font-size:10px;color:var(--yellow);background:rgba(254,193,19,.1);
border:1px solid rgba(254,193,19,.3);border-radius:20px;padding:3px 8px;font-weight:600}
.pickrow.dim{opacity:.5}
.save{display:flex;gap:10px;margin-top:24px}
.btn{text-align:center;font-family:'Poppins';font-weight:600;font-size:13px;padding:12px;border-radius:11px;cursor:pointer}
.btn.go{background:var(--blue);color:#04222f;flex:2;border:0}
.btn.alt{flex:1.3;background:transparent;border:1px solid var(--line2);color:var(--txt)}
.note{width:392px;font-size:11.5px;color:var(--muted2);line-height:1.55;text-align:left}
.note b{color:var(--yellow)} .note code{color:#9fd4ee}
.note ul{margin:7px 0 0;padding-left:16px} .note li{margin-bottom:5px}
.pill{display:inline-block;font-size:10px;font-family:'Poppins';font-weight:600;border-radius:20px;
padding:2px 9px;margin-bottom:6px}
.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}
.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>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 — et, comme les autres,
<b>il ne porte pas le rang</b> : celui-ci se règle par glisser-déposer dans la liste. Défaut : la
batterie en <b>première position de charge</b>, ce qui reproduit le comportement actuel. <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
ceux de la box. ② <code>AirConditioning</code> expose <b>8 méthodes</b>, pas 3.</p>
</div>
<div class="stage">
<!-- ══════════ ECS RELAIS / MULTIPALIER ══════════ -->
<div class="col">
<div class="phone">
<div class="appbar"><div class="ic">‹</div><h2>Configurer · Chauffe-eau</h2><div class="ic"></div></div>
<div class="scroll">
<div class="field">
<label>Nom de la charge <span class="saved-flag">enregistré · label</span></label>
<input class="inp" value="Chauffe-eau">
<div class="mode-hint">Le seul texte de l'installation que l'utilisateur voit dans chaque motif
de décision.</div>
</div>
<div class="field">
<label>Domaine <span class="saved-flag">enregistré · domain</span></label>
<div class="sel">Eau chaude sanitaire <i>▾</i></div>
</div>
<div class="field">
<label>Mécanisme <span class="saved-flag">enregistré · adapter</span></label>
<div class="seg">
<button class="on">Routeur de relais</button><button>Modulable</button><button>SG-Ready</button>
</div>
<div class="info">
<b>Ce sont vos contacteurs</b>
La box ouvre et ferme des contacts qu'elle commande directement, et compose les puissances
en les combinant. C'est le seul mécanisme où l'app déclare du <b>matériel</b>.
<span style="display:block;margin-top:5px;color:var(--muted2)">Si les étages sont à l'intérieur
de l'appareil et qu'on lui envoie une consigne, ce n'est pas ici : c'est « modulable ».</span>
</div>
</div>
<div class="field">
<label>Relais commandés <span class="saved-flag">enregistré · relays[]</span><br>
<small>un contacteur par ligne — puissance de la résistance câblée derrière</small></label>
<div class="relay">
<span class="k">K1</span>
<div class="thing">Relay1 <i>Digital Output</i></div>
<input class="w" value="500 W">
<span class="del">×</span>
</div>
<div class="relay">
<span class="k">K2</span>
<div class="thing">Relay2 <i>Digital Output</i></div>
<input class="w" value="1 000 W">
<span class="del">×</span>
</div>
<div class="relay">
<span class="k">K3</span>
<div class="thing">Relay3 <i>Digital Output</i></div>
<input class="w" value="2 000 W">
<span class="del">×</span>
</div>
<button class="addrow">+ Ajouter un relais</button>
<div class="preview">
3 relais → paliers atteignables :
<b>0 · 500 · 1 000 · 1 500 · 2 000 · 2 500 · 3 000 · 3 500 W</b>
<em>Déduit, jamais enregistré. La box compose elle-même les paliers en essayant toutes les
combinaisons de contacts (2<sup>N</sup>, plafond 16 relais). Deux relais de même puissance ne
créent pas de palier supplémentaire — les doublons sont écartés.</em>
</div>
<div class="mode-hint"><b>Le picker liste les things par NOM</b>, filtrés sur
l'interface nymea <code>power</code> — taper un UUID au doigt est une erreur
garantie. Il résout nom → ThingId et n'ajoute aucun champ au contrat.</div>
<div class="mode-hint"><b>Un ECS simple, c'est un seul relais</b> — deux paliers, 0 et P. Une
résistance multipalier en déclare 2, 3 ou 4. La puissance saisie est celle de la
<b>résistance</b>, pas le calibre du contacteur : rien dans nymea ne dit ce qu'un relais
commande.</div>
<div class="warn">
<b>⚠ Ce que la box refuse, en bloc et sans le dire</b>
<code>relays[]</code> vide · un <code>powerW</code> ≤ 0 · un même Thing sur deux lignes ·
un <code>mode</code> autre que <code>fixed</code>. Le motif n'apparaît qu'au journal de la box —
le formulaire doit donc valider <b>avant</b> d'envoyer.
</div>
</div>
<div class="field">
<label>Temporisations <span class="saved-flag">enregistré · minOnS / minOffS</span></label>
<div class="stepper">
<div class="lab">Maintien ON minimal<small>anti court-cycle à l'enclenchement</small></div>
<div class="ctl"><button class="pm">−</button><b>60 s</b><button class="pm">+</button></div>
</div>
<div class="stepper">
<div class="lab">Maintien OFF minimal<small>délai avant réenclenchement</small></div>
<div class="ctl"><button class="pm">−</button><b>60 s</b><button class="pm">+</button></div>
</div>
<div class="mode-hint">Ces deux durées produisent les verrous <b>LOCK_MIN_ON</b> et
<b>LOCK_MIN_OFF</b> que la carte de la charge affiche en secondes restantes.</div>
</div>
<div class="field">
<label>Compteur dédié <span class="todo-flag">prévu · pas encore supporté</span></label>
<div class="blocked">
<div class="row">
<div class="lab">Rattacher un compteur à cette charge</div>
<span class="sw off"></span>
</div>
<div class="row" style="margin-top:9px">
<div class="sel" style="flex:1">Choisir un appareil de mesure… <i>▾</i></div>
</div>
<p><b>Aucun champ n'existe côté box.</b> <code>SetLoadConfig</code> n'accepte que
<code>adapter · enabled · id · label · mode · priority · domain · maxPowerW · minOffS · minOnS ·
needs · powerLevels · relays · sgReady</code>. nymea rejette toute clé inconnue <b>avant</b> le
handler : inventer <code>meterThingId</code> ferait échouer l'enregistrement de <b>toutes</b> les
charges. <span style="display:block;margin-top:5px">Ici le besoin est le plus fort des trois
mécanismes : la box <b>estime</b> la consommation depuis les paliers déclarés, elle ne la
mesure pas.</span></p>
</div>
</div>
<div class="field">
<label>Arbitrage <span class="saved-flag">enregistré · enabled</span></label>
<div class="stepper">
<div class="lab">Inclure dans l'arbitrage<small>rang 1 · Eau chaude sanitaire</small></div>
<div class="ctl"><span class="sw"></span></div>
</div>
<div class="mode-hint">À la désactivation, le retrait passe par l'état sûr : <b>tous les relais
ouverts</b>. Vérifié sur matériel côté plugin.</div>
</div>
<div class="save"><div class="btn alt">Annuler</div><div class="btn go">Enregistrer</div></div>
</div>
</div>
<div class="cap"><b>Routeur de relais</b> — ECS simple ou multipalier</div>
<div class="note">
<span class="pill load">NymeaEnergy.SetLoadConfig</span><br>
Le seul mécanisme où l'app déclare des <b>contacteurs</b>. C'est aussi le seul où les paliers ont un
sens : ils naissent des combinaisons de relais, ils ne se saisissent pas.
<ul>
<li><b><code>mode</code> doit valoir <code>fixed</code></b> — la box refuse toute autre valeur sur
ce mécanisme. Le champ n'a donc pas à être proposé : il est déterminé par le choix du mécanisme.</li>
<li><b>Toucher aux relais franchit <code>sameHardware()</code></b> : reconstruction de l'adaptateur
et réarmement des verrous à froid — contrairement à <code>label</code>, <code>domain</code>,
<code>priority</code> et <code>enabled</code>, qui passent en mise à jour sur place.</li>
<li>Configuration réelle du banc <code>.75</code> : trois relais 500 / 1 000 / 2 000 W, huit paliers,
<code>maxStageW: 3 500</code> — la télémétrie l'a confirmé pendant le passage sur appareil.</li>
</ul>
</div>
</div>
<!-- ══════════ ECS MODULABLE ══════════ -->
<div class="col">
<div class="phone">
<div class="appbar"><div class="ic">‹</div><h2>Configurer · Chauffe-eau</h2><div class="ic"></div></div>
<div class="scroll">
<div class="field">
<label>Nom de la charge <span class="saved-flag">enregistré · label</span></label>
<input class="inp" value="Chauffe-eau">
</div>
<div class="field">
<label>Mécanisme <span class="saved-flag">enregistré · adapter</span></label>
<div class="seg">
<button>Routeur de relais</button><button class="on">Modulable</button><button>SG-Ready</button>
</div>
<div class="info">
<b>Une consigne continue, pas des étages</b>
La box envoie une <b>valeur en watts</b>, n'importe laquelle entre 0 et le plafond, et l'appareil
module. Aucun contact n'est commandé, aucun palier n'est déclaré.
<span style="display:block;margin-top:5px;color:var(--muted2)">Des paliers ⇒ ce sont des
contacteurs ⇒ c'est « routeur de relais ». Les deux mécanismes ne se recouvrent pas.</span>
</div>
</div>
<div class="field">
<label>Appareil piloté <span class="saved-flag">enregistré · id</span><br>
<small>un seul Thing, portant l'interface etmvariableload</small></label>
<div class="sel">Chauffe-eau Modbus · etmvariableload <i>▾</i></div>
<div class="warn">
<b>⚠ Changer d'appareil, c'est changer de charge</b>
L'identifiant de la charge <b>est</b> le ThingId piloté. Choisir un autre appareil ne modifie donc
pas cette charge : cela en crée une autre et supprime celle-ci — avec son passage par l'état sûr
(consigne 0 W). À dire avant d'enregistrer, pas après.
</div>
</div>
<div class="field">
<label>Puissance nominale <span class="saved-flag">enregistré · maxPowerW</span><br>
<small>plafond de modulation — la consigne ira de 0 à cette valeur</small></label>
<div class="amp">
<button class="pm">−</button>
<div class="v">3 000<small>W</small></div>
<button class="pm">+</button>
</div>
<div class="mode-hint">Requis, et strictement positif : la box refuse une charge modulable sans
plafond. C'est la plaque signalétique de la résistance, pas une estimation.</div>
</div>
<div class="field">
<label>Temporisations <span class="saved-flag">enregistré · minOnS / minOffS</span></label>
<div class="stepper">
<div class="lab">Maintien ON minimal<small>anti court-cycle</small></div>
<div class="ctl"><button class="pm">−</button><b>0 s</b><button class="pm">+</button></div>
</div>
<div class="stepper">
<div class="lab">Maintien OFF minimal<small>délai avant reprise</small></div>
<div class="ctl"><button class="pm">−</button><b>0 s</b><button class="pm">+</button></div>
</div>
</div>
<div class="field">
<label>Compteur dédié <span class="todo-flag">prévu · pas encore supporté</span></label>
<div class="blocked">
<div class="row">
<div class="lab">Rattacher un compteur à cette charge</div>
<span class="sw off"></span>
</div>
<div class="row" style="margin-top:9px">
<div class="sel" style="flex:1">Choisir un appareil de mesure… <i>▾</i></div>
</div>
<p><b>Aucun champ n'existe côté box.</b> <code>SetLoadConfig</code> n'accepte que
<code>adapter · enabled · id · label · mode · priority · domain · maxPowerW · minOffS · minOnS ·
needs · powerLevels · relays · sgReady</code>, et nymea rejette toute clé inconnue <b>avant</b>
le handler. <span style="display:block;margin-top:5px">Ici la question se double : l'interface
remonte déjà la <b>puissance réelle</b> de l'appareil — un compteur externe mesurerait-il autre
chose ?</span></p>
</div>
</div>
<div class="field">
<label>Arbitrage <span class="saved-flag">enregistré · enabled</span></label>
<div class="stepper">
<div class="lab">Inclure dans l'arbitrage<small>rang 1 · Eau chaude sanitaire</small></div>
<div class="ctl"><span class="sw"></span></div>
</div>
<div class="mode-hint">À la désactivation, le retrait passe par l'état sûr : <b>consigne 0 W</b>.</div>
</div>
<div class="field">
<label>Configuration héritée <span class="derived-flag">lecture seule · mode fixed</span><br>
<small>n'apparaît que si la box renvoie déjà une charge en paliers</small></label>
<div class="blocked">
<div class="row" style="opacity:.8">
<div class="lab">Paliers déclarés : 0 · 600 · 1 200 · 1 800 · 2 400 W</div>
</div>
<p><b>L'app ne propose plus ce mode</b> — le plugin lui-même le qualifie de <i>legacy</i> — mais
elle ne le réécrit pas en douce : basculer une charge de <code>fixed</code> vers
<code>dynamic</code> change l'arbitrage réel, et cela demande un geste explicite.
<span style="display:block;margin-top:6px">La conversion exige aussi une
<b>puissance nominale</b>, que la déclaration en paliers ne porte pas : la box refuse
<code>dynamic</code> sans <code>maxPowerW</code>, et refuse <code>powerLevels</code> avec.</span>
</p>
<div class="row" style="opacity:1;margin-top:10px">
<div class="sel" style="flex:1;border-color:rgba(254,193,19,.4);color:var(--yellow)">
Convertir en modulation continue…</div>
</div>
</div>
</div>
<div class="save"><div class="btn alt">Annuler</div><div class="btn go">Enregistrer</div></div>
</div>
</div>
<div class="cap"><b>Modulable</b> — une consigne continue</div>
<div class="note">
<span class="pill load">NymeaEnergy.SetLoadConfig</span><br>
<code>etmvariableload</code> : <b>un seul appareil</b>, une consigne en watts, aucun palier.
<code>relays[]</code> y est interdit — la box refuse en bloc.
<ul>
<li><b>Le critère de choix entre les deux mécanismes</b>, et c'est le seul qui compte pour
l'installateur : <b>où sont les contacteurs ?</b> Sur les sorties de la box → routeur de relais, et
il déclare {Thing, W} par contacteur. Dans l'appareil, piloté par une consigne → modulable, et il
ne déclare qu'un plafond.</li>
<li><b>Le mode <code>fixed</code> existe encore côté box</b> — « multipalier natif, <i>legacy</i> »
dans les mots du plugin. L'app doit savoir le <b>lire</b> sans le proposer : une config déployée
qui l'utilise ne doit pas être convertie par le simple fait d'ouvrir l'écran.</li>
<li>Trois niveaux, trois responsables (contrat rév. 2) : le <b>câblage</b> au wizard Things, la
<b>déclaration</b> à cet écran, le <b>runtime</b> à l'interface. « Le thing décide du matériel ;
l'energymanager décide de la puissance. »</li>
</ul>
</div>
</div>
<!-- ══════════ SG-READY ══════════ -->
<div class="col">
<div class="phone">
<div class="appbar"><div class="ic">‹</div><h2>Configurer · PAC banc</h2><div class="ic"></div></div>
<div class="scroll">
<div class="field">
<label>Nom de la charge <span class="saved-flag">enregistré · label</span></label>
<input class="inp" value="PAC banc">
</div>
<div class="field">
<label>Mécanisme <span class="saved-flag">enregistré · adapter</span></label>
<div class="seg">
<button>Routeur de relais</button><button>Modulable</button><button class="on">SG-Ready</button>
</div>
</div>
<div class="field">
<label>Contacts SG-Ready <span class="derived-flag">déduit · pas de champ dédié</span><br>
<small>les deux entrées de la pompe à chaleur, et le relais qui les commande</small></label>
<div class="relay">
<span class="k">SG1</span>
<div class="thing">Relay4 <i>Digital Output</i></div>
</div>
<div class="relay">
<span class="k">SG2</span>
<div class="thing">Relay5 <i>Digital Output</i></div>
</div>
<div class="info">
<b>« SG1 » et « SG2 » n'existent pas côté box</b>
<code>sgReady.states[]</code> ne stocke qu'une <b>liste de relais par état</b>, sans nom ni ordre.
Les deux étiquettes sont une convention de cet écran, et elles se retrouvent à la lecture :
<b>SG1 = le contact fermé en état 1</b> (blocage), <b>SG2 = celui fermé en état 3</b>
(recommandation). C'est l'encodage SG-Ready standard, et c'est celui du banc.
</div>
<div class="warn">
<b>⚠ Un encodage non standard perd les étiquettes</b>
Si la configuration lue ne correspond pas à ce schéma — trois contacts, ou un état 1 sans contact
fermé — l'écran doit renoncer aux noms SG1/SG2 et afficher les relais tels quels, plutôt que
d'attribuer une étiquette au hasard. Le tableau ci-dessous reste éditable ; ce sont les noms qui
disparaissent, pas la configuration.
</div>
</div>
<div class="field">
<label>Table d'états <span class="saved-flag">enregistré · sgReady.states[]</span><br>
<small>quels contacts fermer pour chaque état, et ce que la PAC consomme alors</small></label>
<div class="st">
<div class="h"><span class="badge s1">1</span><b>Blocage</b><span>arrêt forcé</span></div>
<div class="contacts">
<span class="kchip on">SG1 fermé</span><span class="kchip">SG2 ouvert</span>
</div>
<div class="pw"><label>Puissance estimée</label><input value="0 W"></div>
</div>
<div class="st">
<div class="h"><span class="badge s2">2</span><b>Normal</b><span>aucune consigne</span></div>
<div class="contacts">
<span class="kchip">SG1 ouvert</span><span class="kchip">SG2 ouvert</span>
</div>
<div class="pw"><label>Puissance estimée</label><input value="0 W"></div>
</div>
<div class="st">
<div class="h"><span class="badge s3">3</span><b>Recommandation</b><span>incitation douce</span></div>
<div class="contacts">
<span class="kchip">SG1 ouvert</span><span class="kchip on">SG2 fermé</span>
</div>
<div class="pw"><label>Puissance estimée</label><input value="1 500 W"></div>
</div>
<div class="st">
<div class="h"><span class="badge s4">4</span><b>Forcé</b><span>marche imposée</span></div>
<div class="contacts">
<span class="kchip on">SG1 fermé</span><span class="kchip on">SG2 fermé</span>
</div>
<div class="pw"><label>Puissance estimée</label><input value="3 000 W"></div>
</div>
<div class="warn">
<b>⚠ L'état 2 doit être exprimable</b>
Si l'encodage des contacts ne permet pas de représenter l'état « normal », la pompe à chaleur
n'est <b>jamais</b> pilotable : la box publie <code>UNUSABLE_ENCODING</code> et la charge sort de
l'arbitrage définitivement (ECS-110). Le formulaire doit le refuser avant l'envoi.
</div>
<div class="info">
<b>Une estimation, jamais un engagement</b>
<code>estimatedPowerW</code> sert de base au <b>recrédit de budget</b> et au choix d'état : une
valeur fausse ne décale pas seulement la PAC, elle décale l'allocation de tout le waterfall en
aval. À relever sur la plaque signalétique, puis à mesurer sous les deux régimes.
</div>
</div>
<div class="field">
<label>Anti court-cycle <span class="saved-flag">enregistré · sgReady.minStateHoldS</span></label>
<div class="stepper">
<div class="lab">Maintien d'état minimal<small>durée avant tout changement d'état</small></div>
<div class="ctl"><button class="pm">−</button><b>300 s</b><button class="pm">+</button></div>
</div>
<div class="mode-hint">Pendant ce maintien, la télémétrie publie le verrou
<b>LOCK_MIN_STATE_HOLD</b> avec les secondes restantes — c'est ce que l'écran affiche
« au dernier cycle », sans le décompter lui-même.</div>
</div>
<div class="field">
<label>Compteur dédié <span class="todo-flag">prévu · pas encore supporté</span></label>
<div class="blocked">
<div class="row">
<div class="lab">Rattacher un compteur à cette charge</div>
<span class="sw off"></span>
</div>
<div class="row" style="margin-top:9px">
<div class="sel" style="flex:1">Choisir un appareil de mesure… <i>▾</i></div>
</div>
<p><b>Aucun champ n'existe côté box.</b> <code>SetLoadConfig</code> n'accepte que <code>adapter · enabled · id · label · mode · priority · domain · maxPowerW · minOffS · minOnS · needs · powerLevels · relays · sgReady</code>. nymea rejette toute clé inconnue <b>avant</b> le handler : inventer <code>meterThingId</code> ferait échouer l'enregistrement de <b>toutes</b> les charges.</p>
</div>
</div>
<div class="field">
<label>Arbitrage <span class="saved-flag">enregistré · enabled</span></label>
<div class="stepper">
<div class="lab">Inclure dans l'arbitrage<small>rang 2 · Chauffage</small></div>
<div class="ctl"><span class="sw"></span></div>
</div>
<div class="mode-hint">À la désactivation, le retrait passe par l'état sûr : <b>état 2</b>,
jamais le blocage. Vérifié sur matériel côté plugin.</div>
</div>
<div class="save"><div class="btn alt">Annuler</div><div class="btn go">Enregistrer</div></div>
</div>
</div>
<div class="cap"><b>SG-Ready</b> — même contrat que le relais</div>
<div class="note">
<span class="pill load">NymeaEnergy.SetLoadConfig</span><br>
Même famille que le routeur de relais : c'est une <code>LoadConfig</code>, elle est arbitrée par le
waterfall, elle apparaît dans <code>GetLoadTelemetry</code>, elle a un rang et un domaine.
<ul>
<li>Ce que les contacts commandent, ce sont des <b>états normés 1→4</b>, pas des watts. Les watts
n'y sont qu'une <b>estimation déclarée</b> — d'où le libellé.</li>
<li>Toucher aux contacts ou à la table d'états franchit <code>sameHardware()</code> :
<b>reconstruction de l'adaptateur</b> et réarmement des verrous à froid.</li>
<li><b>Le choix des Things se fait une fois, en haut</b> — pas état par état. Ce qui se coche dans le
tableau, c'est <b>fermé / ouvert</b>, et c'est ça qui devient <code>relays[]</code> à
l'enregistrement : la liste des contacts fermés pour cet état.</li>
</ul>
</div>
</div>
<!-- ══════════ BORNE DE RECHARGE ══════════ -->
<div class="col">
<div class="phone">
<div class="appbar"><div class="ic">‹</div><h2>Configurer · Borne de recharge</h2><div class="ic"></div></div>
<div class="scroll">
<div class="field">
<label>Appareil <span class="other-flag">chargingInfo · evChargerId</span></label>
<div class="sel">Terra AC Charger (TCP) <i>▾</i></div>
</div>
<div class="field">
<label>État actuel <span class="derived-flag">lecture seule · chargingState</span></label>
<div class="state"><span class="dot"></span><b>Recharge sur surplus</b><i>SurplusCharging</i></div>
</div>
<div class="field">
<label>Mode de recharge par défaut <span class="other-flag">chargingInfo · chargingMode</span><br>
<small>ce que la borne applique quand personne n'a rien demandé</small></label>
<div class="evmode">
<button class="on"><u>☀</u>PV<small>surplus seul</small></button>
<button><u>⚡</u>Min+PV<small>courant mini</small></button>
<button><u>🚀</u>Boost<small>pleine puissance</small></button>
</div>
<div class="maps">
PV → <code>ChargingModeEco</code> · Min+PV → <code>ChargingModeEcoWithMinCurrent</code> ·
Boost → <code>ChargingModeNormal</code><br>
avec échéance → <code>ChargingModeEcoWithTargetTime</code> /
<code>ChargingModeEcoMinWithTargetTime</code>
</div>
<div class="mode-hint"><b>C'est un défaut, pas une commande.</b> Le pilotage au jour le jour se
fait depuis la carte véhicule du tableau de bord ; ici on règle ce vers quoi la borne revient.
Les deux écrivent le même <code>chargingMode</code> — d'où l'importance de ne pas laisser croire
que ce bouton agit à l'instant.</div>
</div>
<div class="field">
<label>Échéance de départ <span class="other-flag">targetPercentage · endDateTime · repeatDays</span></label>
<div class="stepper">
<div class="lab">Fixer une heure de départ<small>bascule le mode en « …WithTargetTime »</small></div>
<div class="ctl"><span class="sw"></span></div>
</div>
<div class="stepper">
<div class="lab">Charge cible<small>targetPercentage</small></div>
<div class="ctl"><button class="pm">−</button><b>80 %</b><button class="pm">+</button></div>
</div>
<div class="stepper">
<div class="lab">Prête à<small>endDateTime</small></div>
<div class="ctl"><b>07:30</b></div>
</div>
<div class="sel">Répéter : Lun · Mar · Mer · Jeu · Ven <i>▾</i></div>
</div>
<div class="field">
<label>Courant minimum <span class="todo-flag">champ absent de la box</span></label>
<div class="blocked">
<div class="row">
<div class="lab">Courant maintenu en Min+PV</div>
<div class="ctl" style="display:flex;align-items:center;gap:12px">
<button class="pm">−</button><b style="font-family:Poppins;font-size:13.5px">6 A</b><button class="pm">+</button>
</div>
</div>
<p><b>Le mode existe, le réglage n'existe pas.</b> <code>ChargingModeEcoWithMinCurrent</code> est
bien dans l'énumération de la box, mais <code>ChargingInfo</code> n'a <b>aucun champ</b> de courant
minimum — ni <code>minCurrent</code>, ni équivalent. Soit la valeur est fixée ailleurs (paramètre
du Thing borne), soit le champ manque. <b>À trancher avec l'agent plugin avant de livrer ce
mode</b> : proposer un réglage qui ne part nulle part serait pire que de ne pas le proposer.</p>
</div>
</div>
<div class="field">
<label>Compteur dédié <span class="todo-flag">prévu · pas encore supporté</span></label>
<div class="blocked">
<div class="row">
<div class="lab">Rattacher un compteur à la borne</div>
<span class="sw off"></span>
</div>
<div class="row" style="margin-top:9px">
<div class="sel" style="flex:1">Choisir un appareil de mesure… <i>▾</i></div>
</div>
<p><b>Aucun champ n'existe côté box.</b> <code>ChargingInfo</code> ne porte que <code>chargingMode · evChargerId · assignedCarId · dailySpotMarketPercentage · endDateTime · repeatDays · spotMarketChargingEnabled · targetPercentage</code>, plus <code>chargingState</code> en lecture. Rien pour un compteur — et la borne mesure déjà, ce qui pose la question de <b>quelle</b> mesure fait foi.</p>
</div>
</div>
<div class="field">
<label>Tarif dynamique <span class="other-flag">spotMarketChargingEnabled · dailySpotMarketPercentage</span></label>
<div class="stepper">
<div class="lab">Recharger aux heures les moins chères<small>Predict AI</small></div>
<div class="ctl"><span class="sw off"></span></div>
</div>
</div>
<div class="save"><div class="btn alt">Annuler</div><div class="btn go">Enregistrer</div></div>
</div>
</div>
<div class="cap"><b>Borne</b> — un autre contrat</div>
<div class="note">
<span class="pill ev">NymeaEnergy.SetChargingInfo</span><br>
<b>Ce n'est pas une <code>LoadConfig</code>.</b> Le véhicule n'a ni <code>adapter</code>, ni
<code>priority</code>, ni <code>domain</code> — et il <b>n'apparaît pas</b> dans
<code>GetLoadTelemetry</code> en beta : il est décidé en amont du waterfall, et sa puissance est
seulement <b>réservée</b> dans <code>budget.evReservedW</code>. Il rejoindra <code>loads[]</code> à
l'étape 3g.
<ul>
<li><b>Les noms de champs de <code>CLAUDE.md</code> sont périmés.</b> La box déploie
<code>chargingMode</code> (pas <code>mode</code>), <code>targetPercentage</code> (pas
<code>targetSoc</code>), <code>endDateTime</code> en <b>Uint</b> (pas <code>endTime</code>), plus
<code>assignedCarId</code>, <code>repeatDays</code> et deux champs spot market. À corriger dans
<code>CLAUDE.md</code>.</li>
<li>Le namespace est <code>NymeaEnergy.*</code>, pas <code>EnergyPlugin.*</code>.</li>
</ul>
</div>
</div>
<!-- ══════════ ZONES HVAC ══════════ -->
<div class="col">
<div class="phone">
<div class="appbar"><div class="ic">‹</div><h2>Zones de climatisation</h2><div class="ic">+</div></div>
<div class="scroll">
<div class="field" style="margin-top:2px">
<label>Zones déclarées <span class="other-flag">AirConditioning.GetZones</span></label>
<div class="zone">
<div class="z1">
<b>Séjour</b>
<div class="temp">20,4 °<small>consigne 21,0 °</small></div>
</div>
<div class="z2">
<span class="tchip">2 thermostats</span><span class="tchip">1 vanne</span>
<span class="tchip">1 sonde int.</span><span class="tchip">1 contact fenêtre</span>
</div>
<div class="ovr">⏱ Dérogation 22,0 ° — encore 45 min</div>
</div>
<div class="zone">
<div class="z1">
<b>Chambres</b>
<div class="temp">18,9 °<small>consigne 19,0 °</small></div>
</div>
<div class="z2">
<span class="tchip">3 thermostats</span><span class="tchip">1 sonde int.</span>
</div>
</div>
<div class="zone">
<div class="z1">
<b>Bureau</b>
<div class="temp">21,1 °<small>réduit 17,0 °</small></div>
</div>
<div class="z2">
<span class="tchip">1 thermostat</span><span class="tchip">humidité 47 %</span>
<span class="tchip">COV 320</span>
</div>
</div>
</div>
<div class="field">
<label>Zone « Séjour » <span class="saved-flag">SetZoneThings · SetZoneName</span></label>
<input class="inp" value="Séjour" style="margin-bottom:9px">
<div class="rowsel"><span class="lab">Thermostats</span><span class="cnt">2 appareils</span><i>▾</i></div>
<div class="rowsel"><span class="lab">Vannes</span><span class="cnt">1 appareil</span><i>▾</i></div>
<div class="rowsel"><span class="lab">Sondes intérieures</span><span class="cnt">1 appareil</span><i>▾</i></div>
<div class="rowsel"><span class="lab">Sondes extérieures</span><span class="cnt">aucune</span><i>▾</i></div>
<div class="rowsel"><span class="lab">Contacts de fenêtre</span><span class="cnt">1 appareil</span><i>▾</i></div>
<div class="rowsel"><span class="lab">Notifications</span><span class="cnt">aucune</span><i>▾</i></div>
</div>
<div class="field">
<label>Compteur dédié <span class="todo-flag">prévu · pas encore supporté</span></label>
<div class="blocked">
<div class="row">
<div class="lab">Rattacher un compteur à la zone</div>
<span class="sw off"></span>
</div>
<div class="row" style="margin-top:9px">
<div class="sel" style="flex:1">Choisir un appareil de mesure… <i>▾</i></div>
</div>
<p><b>Aucun champ n'existe côté box.</b> <code>ZoneInfo</code> rattache des <code>indoorSensors</code> et <code>outdoorSensors</code> — des sondes de <b>température</b>, pas de consommation. Un compteur de zone est un champ à ajouter, et il faudra dire s'il mesure la zone ou l'appareil qui la chauffe.</p>
</div>
</div>
<div class="field">
<label>Dérogation <span class="saved-flag">SetZoneSetpointOverride</span></label>
<div class="seg">
<button>Aucune</button><button class="on">Minutée</button>
<button>Illimitée</button><button>Éventuelle</button>
</div>
<div class="stepper" style="margin-top:9px">
<div class="lab">Consigne dérogée<small>setpointOverride</small></div>
<div class="ctl"><button class="pm">−</button><b>22,0 °</b><button class="pm">+</button></div>
</div>
<div class="stepper">
<div class="lab">Durée<small>minutes — mode minuté seul</small></div>
<div class="ctl"><button class="pm">−</button><b>60 min</b><button class="pm">+</button></div>
</div>
</div>
<div class="field">
<label>Consigne de réduit <span class="saved-flag">SetZoneStandbySetpoint</span></label>
<div class="stepper">
<div class="lab">Hors planning<small>standbySetpoint</small></div>
<div class="ctl"><button class="pm">−</button><b>17,0 °</b><button class="pm">+</button></div>
</div>
</div>
<div class="field">
<label>Planning hebdomadaire <span class="saved-flag">SetZoneWeekSchedule</span></label>
<div class="sel">7 jours · 4 créneaux par jour <i>›</i></div>
<div class="mode-hint">Écran dédié — la forme exacte de <code>TemperatureWeekSchedule</code> reste
à introspecter, le type n'est pas détaillé dans le schéma de cette box.</div>
</div>
<div class="info" style="margin-top:20px">
<b>Sur le banc, il n'y a aucune zone</b>
<code>AirConditioning.GetZones</code> renvoie <code>zones: []</code> sur <code>.75</code>.
L'état vide ci-dessous est donc le <b>premier</b> écran que verra un installateur.
</div>
<div class="empty" style="margin-top:12px">
<b>Aucune zone déclarée</b>
<p>Une zone regroupe des thermostats, des vannes et des sondes sous une même consigne.
Sans zone, la climatisation n'est pas pilotée par l'énergie.</p>
<button>+ Créer une zone</button>
</div>
</div>
</div>
<div class="cap"><b>HVAC</b> — encore un autre contrat</div>
<div class="note">
<span class="pill ac">AirConditioning.* · tier Auto</span><br>
Troisième contrat : <b>ni <code>LoadConfig</code>, ni <code>ChargingInfo</code></b>. Une zone n'a pas de
rang de service ni de domaine — c'est un regroupement de Things sous une consigne de température.
<ul>
<li><b>8 méthodes, pas 3</b> comme l'annonce <code>CLAUDE.md</code> : s'ajoutent
<code>AddZone</code>, <code>RemoveZone</code>, <code>SetZoneName</code>,
<code>SetZoneThings</code> et <code>SetZoneStandbySetpoint</code>. Le CRUD complet est donc
possible depuis l'app.</li>
<li>Une zone expose <b>en lecture seule</b> température, humidité, PM2.5, COV, statut, et la
dérogation en cours avec sa fin. Tout le reste des champs de <code>ZoneInfo</code> est
<code>r:</code> — on écrit par les six méthodes <code>Set*</code>, jamais par un objet complet.</li>
<li><b>Lien avec l'énergie à établir :</b> rien dans <code>ZoneInfo</code> ne relie une zone à une
<code>LoadConfig</code>. La PAC SG-Ready et les zones vivent côte à côte sans se connaître — si
l'ordre de service doit tenir compte des zones, c'est une question de conception, pas de câblage.</li>
</ul>
</div>
</div>
<!-- ══════════ SÉLECTEUR DE THINGS (ZONE) ══════════ -->
<div class="col">
<div class="phone">
<div class="appbar"><div class="ic">‹</div><h2>Séjour · Thermostats</h2><div class="ic">✓</div></div>
<div class="scroll">
<div class="field" style="margin-top:2px">
<label>Rôle dans la zone <span class="saved-flag">SetZoneThings</span></label>
<div class="filters">
<span class="fchip on">Thermostats</span>
<span class="fchip">Vannes</span>
<span class="fchip">Sondes int.</span>
<span class="fchip">Sondes ext.</span>
<span class="fchip">Fenêtres</span>
<span class="fchip">Notifications</span>
</div>
<div class="mode-hint">Six listes distinctes, six paramètres de <code>SetZoneThings</code>. On
n'envoie que celles qu'on a touchées — les autres sont optionnelles et restent en place.</div>
</div>
<div class="field">
<label>Appareils compatibles <span class="derived-flag">filtré · interfaces nymea</span><br>
<small>ThingClass portant l'interface « thermostat »</small></label>
<div class="search">🔍 Rechercher un appareil…</div>
<div class="pickrow">
<span class="cb on"></span>
<div class="nm"><b>Thermostat Séjour Nord</b><span>Danfoss Ally · thermostat</span></div>
</div>
<div class="pickrow">
<span class="cb on"></span>
<div class="nm"><b>Thermostat Séjour Sud</b><span>Danfoss Ally · thermostat</span></div>
</div>
<div class="pickrow dim">
<span class="cb"></span>
<div class="nm"><b>Thermostat Chambre 1</b><span>Danfoss Ally · thermostat</span></div>
<span class="taken">zone « Chambres »</span>
</div>
<div class="pickrow">
<span class="cb"></span>
<div class="nm"><b>Thermostat Bureau</b><span>Shelly TRV · thermostat</span></div>
</div>
<div class="info">
<b>Un appareil dans deux zones ?</b>
Le schéma ne l'interdit pas — <code>SetZoneThings</code> prend une liste d'UUID sans contrainte
d'unicité déclarée. Tant que ce n'est pas éprouvé sur la box, l'app <b>signale</b> l'appartenance
existante sans bloquer, et ne prétend pas connaître la règle.
</div>
<div class="warn">
<b>⚠ Sur le banc, ces listes sont vides</b>
Les 58 ThingClass de <code>.75</code> ne portent <b>aucune</b> interface thermostat, vanne ou
sonde de température — il n'y a pas de matériel HVAC dessus. Les appareils ci-dessus sont
fictifs : cet écran n'est pas éprouvable en l'état, et la première vérification à faire est
d'y brancher un thermostat réel.
</div>
</div>
<div class="field">
<label>Compteur de zone <span class="todo-flag">prévu · pas encore supporté</span></label>
<div class="blocked">
<div class="row">
<div class="lab">Rattacher un compteur à la zone</div>
<span class="sw off"></span>
</div>
<div class="row" style="margin-top:9px">
<div class="sel" style="flex:1">Filtrer sur « energymeter »… <i>▾</i></div>
</div>
<p><b>Le filtre, lui, existe déjà.</b> <code>.75</code> porte les interfaces
<code>energymeter</code>, <code>smartmeter</code>, <code>smartmeterconsumer</code> et
<code>smartmeterproducer</code> : le jour où le champ est ajouté, l'app saura proposer les bons
appareils sans table maintenue à la main. C'est le <b>champ de destination</b> qui manque, pas la
façon de choisir.</p>
</div>
</div>
<div class="save"><div class="btn alt">Annuler</div><div class="btn go">Valider (2)</div></div>
</div>
</div>
<div class="cap"><b>Zone</b> — choisir les appareils</div>
<div class="note">
<span class="pill ac">AirConditioning.SetZoneThings</span><br>
Le même écran sert aux six rôles : thermostats, vannes, sondes intérieures, sondes extérieures,
contacts de fenêtre, notifications. Seul le filtre d'interface change.
<ul>
<li><b>Filtrer par interface, pas par nom de classe.</b> <code>ThingClass.interfaces</code> est une
liste en lecture sur chaque classe ; s'en servir évite la table de correspondance codée en dur que
le brief interdit — et qui vieillirait à chaque plugin ajouté.</li>
<li><b>Ne rien attendre du vocabulaire affiché.</b> Mesuré sur <code>.75</code> : sur 784 chaînes,
le français en traduit <b>zéro</b>. Les <code>displayName</code> sont des identifiants anglais, à
afficher tels quels.</li>
</ul>
</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="derived-flag">réglé dans la liste, pas ici</span></label>
<div class="state"><span class="dot" style="background:var(--yellow);box-shadow:0 0 0 4px rgba(254,193,19,.15)"></span><b>1<sup>re</sup> position de charge</b><i>défaut</i></div>
<div class="mode-hint">Le rang se règle <b>par glisser-déposer dans la liste des charges</b>, comme
pour toutes les autres — aucun écran de mécanisme n'en porte un second réglage. Il est rappelé
ici parce que c'est lui qui répond à « eau chaude avant batterie », et rien d'autre.</div>
<div class="info"><b>Défaut : la batterie en première position de charge.</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 :
remonter « 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>Décharge <span class="newf">à créer · enableDischarging</span></label>
<div class="stepper">
<div class="lab">Bloquer la décharge pendant la recharge du véhicule<small>la maison continue
d'être servie ; seule la voiture ne l'est pas</small></div>
<div class="ctl"><span class="sw"></span></div>
</div>
<div class="info"><b>Un seul interrupteur, et c'est le seul cas qu'aucun rang ne peut
exprimer.</b> « Charger l'eau chaude avant la batterie » se règle par le <b>rang</b> — il n'a
pas besoin d'un second réglage qui dirait la même chose autrement. Mais <b>la nuit, il n'y a
pas de surplus à répartir</b> : aucune position dans une file ne dit « ne vide pas la batterie
dans la voiture ».<br><br>
<b>Pas de tableau de permissions charge par charge.</b> Décider en général qui mérite d'être
servi par la batterie n'est pas un réglage d'installateur : c'est un arbitrage, et il
reviendra à <b>Héos</b> (l'optimiseur MILP) ou à une décision par le prix, quand l'un ou
l'autre existera. Offrir la question à la main aujourd'hui figerait dans un écran ce qui doit
se calculer.</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>Le rang n'est PAS sur cet écran</b>, et c'est volontaire : il se règle par glisser-déposer
dans la liste des charges, comme pour toutes les autres. Un second réglage de rang ici dirait la
même chose autrement, et les deux finiraient par diverger.</li>
<li><b>Pas de permission par charge non plus.</b> Le seul interrupteur de décharge couvre le cas
que le rang ne sait pas dire ; l'arbitrage fin — qui mérite d'être servi par la batterie, et
quand — reviendra à <b>Héos</b> ou à une décision par le prix. Un écran ne doit pas figer ce qui
doit se calculer.</li>
<li><b>La charge différée n'est pas un réglage non plus — c'est une décision d'Héos.</b> Démarrer
assez tard pour arriver à 100 % juste avant la pointe suppose de savoir ce que le soleil va
faire : <b>sans prévision, ça ne marche pas</b>, et une heure saisie à la main serait un pari
présenté comme un réglage. Le gain reste réel — pic PV de midi libéré pour l'ECS et la voiture,
moins de temps passé à 100 % donc moins de vieillissement calendaire, et <b>aucun kWh
sacrifié</b> : la même énergie, stockée plus tard. Mais il se calcule.</li>
<li><b><code>batteryLevelConsideration</code> fait aujourd'hui l'INVERSE de ce que le rang
permettra.</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<br><br>
<i style="color:var(--yellow);font-style:normal">paramètres calculés par Héos</i></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<br><br>
<i style="color:var(--green);font-style:normal">paramètres saisis par l'utilisateur</i></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<br><br>
<i style="color:var(--green);font-style:normal">paramètres saisis par l'utilisateur</i></span></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</em> — avant, on peut encore attendre ; après, on sert de
<b>tout ce qui reste</b>, prévision ou pas. <u>┃ échéance</u><br>
C'est la seule partie du motif qui doit être <b>montrée</b> : une prévision n'engage personne,
un point de non-retour si.
</div>
</div>
<p style="margin:10px 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> — et cela vaut que les paramètres soient
saisis ou calculés. C'est ce qui rend le motif implémentable une fois : le moteur porte la forme,
Héos ou l'utilisateur la remplit. Le point de non-retour est la partie qui se perd si on ne la
nomme pas, et c'est lui qui garantit qu'<b>une prévision fausse ne coûte rien</b>.</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>