Trois maquettes, neuf écrans, après le passage sur appareil qui a montré que /energy/setup décrit autre chose que l'installation. - charges_pilotables_mockup.html : la liste à deux niveaux, avec le bandeau d'arbitrage hérité de /energy/loads (qui disparaît) et la télémétrie sur la carte de chaque charge. - configurer_mecanismes_mockup.html : les six « Configurer » — routeur de relais, modulable, SG-Ready, borne, zones HVAC, sélecteur d'appareils. - raccordement_reseau_mockup.html : compteur réseau et protection de surcharge. Décisions de conception que les images ne portent pas seules : les paliers n'existent que chez le routeur de relais et y sont DÉDUITS des combinaisons ; le mode fixed d'etmvariableload (legacy) n'est plus proposé mais reste lisible, parce qu'ouvrir un écran ne doit pas convertir une config déployée ; SG1/SG2 sont une convention d'app retrouvée par l'encodage standard, la box ne stocke qu'une liste de relais par état ; le compteur par charge est dessiné inactif avec sa raison ; la borne règle un mode par DÉFAUT, pas une commande. CLAUDE.md corrigé contre JSONRPC.Introspect (plugin 1.15.2+etm15) : - Energy.SetChargingMode n'existe pas — le « bug critique » décrivait un appel impossible. Energy.* n'a que cinq méthodes. - Le namespace EnergyPlugin.* n'existe pas : c'est NymeaEnergy.*, 20 méthodes et 14 notifications, toutes listées. - ChargingInfo : chargingMode / targetPercentage / endDateTime (Uint), et non mode / targetSoc / endTime. minCurrent n'existe nulle part. - phasePowerLimit est en AMPÈRES par phase, et 0 coupe toute la recharge intelligente. - AirConditioning expose 8 méthodes, pas 3. Toutes les méthodes citées ont été revérifiées une par une : aucune introuvable. Règle ajoutée : contrôler ce fichier contre Introspect avant chaque lot — trois fois aujourd'hui il a envoyé un agent dans le vide. Et docs/BRIEF_plugin_depuis_maquettes.md : les six points que les maquettes remontent vers le moteur, dont un seul blocage dur (pas de champ de courant minimum pour le mode Min+PV). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
318 lines
18 KiB
HTML
318 lines
18 KiB
HTML
<!doctype html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>ETM PowerSync — Charges pilotables (liste à deux niveaux)</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;
|
||
--txt:#e8f1f6; --muted:#90a8b6; --muted2:#6f8b9a; --r:16px;
|
||
}
|
||
*{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:1180px;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:860px}
|
||
.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}
|
||
|
||
.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:860px}
|
||
.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:15.5px;margin:0;flex:1;text-align:center}
|
||
.scroll{overflow:auto;padding:16px 15px 26px}
|
||
.scroll::-webkit-scrollbar{width:0}
|
||
|
||
.sect{font-family:'Poppins';font-weight:600;font-size:12px;color:var(--muted);letter-spacing:.3px;
|
||
margin:20px 0 9px;display:flex;align-items:baseline;gap:8px}
|
||
.sect:first-child{margin-top:2px}
|
||
.sect small{font-weight:500;color:var(--muted2);letter-spacing:0;font-size:11px}
|
||
|
||
/* ─ bandeau arbitrage (hérite de l'écran supprimé) ─ */
|
||
.arb{background:var(--card2);border:1px solid var(--line);border-radius:13px;padding:12px 13px}
|
||
.arb .top{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
||
.arb .dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px rgba(53,201,138,.15)}
|
||
.arb .top b{font-family:'Poppins';font-size:12.5px;font-weight:600}
|
||
.arb .top i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:11px}
|
||
.arb .grid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
|
||
.arb .cell{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:7px 4px;text-align:center}
|
||
.arb .cell u{display:block;text-decoration:none;font-size:9.5px;color:var(--muted2);margin-bottom:2px}
|
||
.arb .cell b{font-family:'Poppins';font-weight:600;font-size:11.5px}
|
||
|
||
/* ─ domaine ─ */
|
||
.dom{display:flex;align-items:center;gap:8px;margin:16px 0 8px;padding:0 2px}
|
||
.dom .grip{color:var(--muted2);font-size:15px;cursor:grab}
|
||
.dom h3{font-family:'Poppins';font-weight:600;font-size:13px;margin:0;flex:1}
|
||
.dom .arw{width:26px;height:26px;border-radius:8px;border:1px solid var(--line2);color:var(--muted);
|
||
display:grid;place-items:center;font-size:13px;background:transparent;cursor:pointer}
|
||
.dom.unclassed h3{color:var(--muted2)}
|
||
|
||
/* ─ carte de charge ─ */
|
||
.load{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:12px;margin-bottom:9px}
|
||
.load .r1{display:flex;align-items:center;gap:10px}
|
||
.load .grip{color:var(--muted2);font-size:15px;cursor:grab}
|
||
.rank{width:23px;height:23px;border-radius:7px;background:rgba(49,163,221,.16);color:var(--blue);
|
||
font-family:'Poppins';font-weight:700;font-size:11.5px;display:grid;place-items:center;flex:none}
|
||
.load .nm{flex:1;min-width:0}
|
||
.load .nm b{font-family:'Poppins';font-weight:600;font-size:13.5px;display:block}
|
||
.load .nm span{font-size:11px;color:var(--muted2)}
|
||
.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}
|
||
|
||
.tel{margin-top:10px;padding-top:9px;border-top:1px dashed var(--line);font-size:11.5px;line-height:1.5}
|
||
.tel .a{font-family:'Poppins';font-weight:600;color:var(--green);font-size:12px}
|
||
.tel .m{color:var(--muted)}
|
||
.tel .why{color:var(--muted2);margin-top:2px}
|
||
.tel .lock{color:var(--yellow);margin-top:2px}
|
||
.tel.stale .a{color:var(--muted2)}
|
||
.tel.stale{opacity:.75}
|
||
|
||
.fault{margin-top:9px;background:rgba(224,122,122,.1);border:1px solid rgba(224,122,122,.3);
|
||
border-radius:10px;padding:9px 10px;font-size:11px;line-height:1.45}
|
||
.fault b{color:var(--red);font-family:'Poppins';font-size:11.5px;display:block;margin-bottom:2px}
|
||
.fault button{margin-top:7px;background:transparent;border:1px solid rgba(224,122,122,.45);color:var(--red);
|
||
font-family:'Poppins';font-weight:600;font-size:11px;padding:6px 11px;border-radius:8px;cursor:pointer}
|
||
|
||
.cfg{margin-top:10px;width:100%;background:transparent;border:1px solid rgba(49,163,221,.42);color:var(--blue);
|
||
font-family:'Poppins';font-weight:600;font-size:12px;padding:9px;border-radius:10px;cursor:pointer;
|
||
display:flex;align-items:center;justify-content:center;gap:7px}
|
||
|
||
.noload{background:var(--card2);border:1px dashed var(--line2);border-radius:12px;padding:11px 12px;
|
||
display:flex;align-items:center;gap:10px}
|
||
.noload .nm b{font-family:'Poppins';font-weight:600;font-size:13px;display:block;color:var(--muted)}
|
||
.noload .nm span{font-size:11px;color:var(--muted2)}
|
||
.noload button{margin-left:auto;background:var(--blue);color:#04222f;border:0;border-radius:9px;
|
||
font-family:'Poppins';font-weight:600;font-size:11.5px;padding:8px 13px;cursor:pointer}
|
||
|
||
/* ─ formulaire ─ */
|
||
.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}
|
||
.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}
|
||
.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}
|
||
|
||
.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:13px;padding:11px 12px;display:flex;align-items:center;gap:8px}
|
||
.sel i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:11px}
|
||
|
||
.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}
|
||
.mode-hint{margin-top:9px;font-size:11.5px;color:var(--muted2);line-height:1.5}
|
||
|
||
.relay{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
||
.relay .k{width:32px;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}
|
||
.relay .thing i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:10px}
|
||
.relay .w{width:92px;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:16px;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}
|
||
|
||
.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}
|
||
|
||
.blocked{border:1px dashed rgba(254,193,19,.35);background:rgba(254,193,19,.05);border-radius:12px;padding:12px}
|
||
.blocked .row{display:flex;align-items:center;gap:10px;opacity:.55}
|
||
.blocked .row .lab{font-size:12.5px;flex:1}
|
||
.blocked p{margin:9px 0 0;font-size:10.8px;color:var(--muted2);line-height:1.5}
|
||
.blocked p b{color:var(--yellow)}
|
||
|
||
.danger{margin-top:9px;font-size:10.8px;color:var(--muted2);line-height:1.5;
|
||
border-left:2px solid rgba(224,122,122,.5);padding-left:9px}
|
||
.danger b{color:var(--red)}
|
||
|
||
.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}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="head">
|
||
<h1>Charges pilotables — l'ordre, la télémétrie et le mécanisme au même endroit</h1>
|
||
<p>Décision du 2026-08-25 : <b>l'écran « Ordre de service des charges » disparaît</b>. Le rang se règle
|
||
ici au glisser-déposer, le regroupement <b>à deux niveaux</b> est conservé (domaines ordonnés entre eux,
|
||
charges ordonnées dedans), et le bloc de télémétrie descend sur la carte de chaque charge.</p>
|
||
<p>Les six écrans « Configurer » qu'ouvre le bouton de chaque carte sont dans
|
||
<code>configurer_mecanismes_mockup.html</code>.</p>
|
||
<p><b>Le changement de fond n'est pas visuel :</b> ces cartes doivent <b>être</b> les charges lues par
|
||
<code>NymeaEnergy.GetLoadConfig</code>, pas des <code>LoadDescriptor</code> reconstruits depuis des
|
||
<code>EmsRole</code>. C'est la cause racine du défaut constaté sur appareil — l'écran actuel affichait
|
||
« 6/6 rôles configurés · tout est assigné » en pointant sur des appareils absents de la box, pendant que
|
||
celle-ci arbitrait deux charges qui n'apparaissaient nulle part.</p>
|
||
</div>
|
||
|
||
<div class="stage">
|
||
|
||
<!-- ══════════ ÉCRAN 1 ══════════ -->
|
||
<div class="col">
|
||
<div class="phone">
|
||
<div class="appbar"><div class="ic">‹</div><h2>Rôles & appareils</h2><div class="ic">⟳</div></div>
|
||
<div class="scroll">
|
||
|
||
<div class="sect">Arbitrage en direct <small>· dernier cycle il y a 41 s</small></div>
|
||
<div class="arb">
|
||
<div class="top"><span class="dot"></span><b>Budget de surplus du cycle</b><i>2 571 W entrants</i></div>
|
||
<div class="grid">
|
||
<div class="cell"><u>Surplus</u><b>2 571</b></div>
|
||
<div class="cell"><u>Réservé VE</u><b>0</b></div>
|
||
<div class="cell"><u>Recrédité</u><b>500</b></div>
|
||
<div class="cell"><u>Alloué</u><b>3 000</b></div>
|
||
<div class="cell"><u>Reliquat</u><b>71</b></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sect">Charges pilotables <small>· glissez les domaines, puis les charges</small></div>
|
||
|
||
<!-- domaine 1 -->
|
||
<div class="dom">
|
||
<span class="grip">⠿</span>
|
||
<h3>Eau chaude sanitaire</h3>
|
||
<button class="arw">˄</button><button class="arw">˅</button>
|
||
</div>
|
||
|
||
<div class="load">
|
||
<div class="r1">
|
||
<span class="grip">⠿</span><span class="rank">1</span>
|
||
<div class="nm"><b>Chauffe-eau</b><span>Routeur de relais · 3 relais · min ON 60 s</span></div>
|
||
<span class="sw"></span>
|
||
</div>
|
||
<div class="tel">
|
||
<span class="a">Alloué 3 000 W</span> <span class="m">· palier 3 000 W sur 3 500 W</span>
|
||
<div class="why">Consigne de 3 000 W servie par le surplus, arrondie sur un palier déclaré
|
||
(3 071 W de budget).</div>
|
||
<div class="lock">🔒 Maintien ON — 60 s restantes <span style="color:var(--muted2)">(au dernier cycle)</span></div>
|
||
</div>
|
||
<button class="cfg">⚙ Configurer</button>
|
||
</div>
|
||
|
||
<!-- domaine 2 -->
|
||
<div class="dom">
|
||
<span class="grip">⠿</span>
|
||
<h3>Chauffage</h3>
|
||
<button class="arw">˄</button><button class="arw">˅</button>
|
||
</div>
|
||
|
||
<div class="load">
|
||
<div class="r1">
|
||
<span class="grip">⠿</span><span class="rank">2</span>
|
||
<div class="nm"><b>PAC banc</b><span>SG-Ready · maintien d'état 300 s</span></div>
|
||
<span class="sw"></span>
|
||
</div>
|
||
<div class="tel">
|
||
<span class="a">Alloué 3 000 W</span> <span class="m">· état SG-Ready 4 — estimation 3 000 W</span>
|
||
<div class="why">Pompe à chaleur en état 4 (forcé) — estimation 3 000 W sur 3 477 W de budget.</div>
|
||
</div>
|
||
<button class="cfg">⚙ Configurer</button>
|
||
</div>
|
||
|
||
<!-- domaine 3 : non classé + défaut -->
|
||
<div class="dom unclassed">
|
||
<span class="grip">⠿</span>
|
||
<h3>Non classé</h3>
|
||
<button class="arw">˄</button><button class="arw">˅</button>
|
||
</div>
|
||
|
||
<div class="load">
|
||
<div class="r1">
|
||
<span class="grip">⠿</span><span class="rank">3</span>
|
||
<div class="nm"><b>Sèche-serviettes</b><span>Routeur de relais · 1 relais</span></div>
|
||
<span class="sw off"></span>
|
||
</div>
|
||
<div class="tel stale">
|
||
<span class="a">Non arbitrée</span> <span class="m">· absente du dernier cycle publié</span>
|
||
<div class="why">Charge désactivée : son rôle reste décrit, elle ne consomme aucun budget.</div>
|
||
</div>
|
||
<button class="cfg">⚙ Configurer</button>
|
||
</div>
|
||
|
||
<div class="load" style="border-color:rgba(224,122,122,.35)">
|
||
<div class="r1">
|
||
<span class="grip">⠿</span><span class="rank">4</span>
|
||
<div class="nm"><b>Plancher chauffant</b><span>Charge modulable</span></div>
|
||
<span class="sw"></span>
|
||
</div>
|
||
<div class="fault">
|
||
<b>⚠ En défaut</b>
|
||
Échec d'écriture : l'échelle de commande est épuisée, plus aucune commande n'est émise tant que
|
||
le défaut n'est pas levé.
|
||
<button>🔓 Lever le défaut</button>
|
||
</div>
|
||
<button class="cfg">⚙ Configurer</button>
|
||
</div>
|
||
|
||
<div class="sect">Sans appareil</div>
|
||
<div class="noload">
|
||
<div class="nm"><b>Borne de recharge</b><span>Aucune charge déclarée sur la box</span></div>
|
||
<button>+ Déclarer</button>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
<div class="cap">Charges pilotables — deux niveaux + télémétrie</div>
|
||
<div class="note">
|
||
<b>Ce qui part sur la box :</b> le glisser-déposer produit une suite d'entiers <code>priority</code> à plat —
|
||
la box ne sait rien des domaines, le rang de domaine se déduit de l'ordre d'apparition. Le toggle écrit
|
||
<code>enabled</code>.
|
||
<ul>
|
||
<li><b>« Non arbitrée »</b> n'est pas une erreur : une charge de <code>GetLoadConfig</code> absente de
|
||
<code>loads[]</code> se lit <b>désactivée</b>, jamais perdue. L'inclusion ne vaut que dans un sens.</li>
|
||
<li><b>« Lever le défaut »</b> n'apparaît que si <code>available: false</code>, et son résultat n'est
|
||
jamais annoncé sur le retour du RPC : mesuré sur le banc, <code>ClearLoadFault</code> répond
|
||
<code>EnergyErrorNoError</code>, lève réellement le verrou… et le cycle suivant le repose si la cause
|
||
demeure. Le verdict vient de la télémétrie relue, deux cycles plus tard.</li>
|
||
<li><b>« Déclarer »</b> mène au même formulaire que « Configurer », vide.</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|