etm-powersync-app/docs/mockups/configurer_mecanismes_mockup.html
Patrick Schurig eac1dde4ee docs: recalage sur la rév. 3 du contrat charge pilotée
Le miroir INTERFACE_etmvariableload.md est passé en rév. 3 pendant la session (corps
identique au canonique du dépôt plugin, vérifié). Son §10 s'adresse nommément à l'agent
app — et il RATIFIE les maquettes dessinées aujourd'hui : relays[] = {thingId, powerW}
pour l'ECS relais, maxPowerW seul pour le continu, powerLevels absents de la config et
dérivés par le routeur.

Trois recalages :

- Le picker de relais filtre sur l'interface nymea « power » et présente les things par
  NOM — nommé dans la maquette, il ne l'était pas.
- Le point 4 du brief plugin est tranché par la rév. 3 : la duplication de la
  combinatoire est voulue (l'app dérive pour montrer, le routeur pour exécuter). La
  demande devient donc un moyen de détecter la divergence — la télémétrie publie déjà
  maxStageW, l'app peut y confronter le maximum de sa propre liste.
- En-tête du pont VM Service, que le commit précédent avait laissé dans sa forme brute.

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

1002 lines
57 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)}
</style>
</head>
<body>
<div class="head">
<h1>Six écrans, trois 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>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>
</div>
</body>
</html>