docs(maquette): écran Énergie — le relevé d'abord, la maquette ensuite

Quatre demandes de Patrick. Sondé sur .75 avant de dessiner quoi que ce soit :
Energy.GetThingPowerLogs répond, six Things demandés, cinq répondent.

TRAÇABLE : Maison (dérivée Grid+PV), PAC-Meter, ECS-Meter, Simulated wallbox —
tous quatre en smartmeterconsumer, tous à 0 W aujourd'hui sauf Grid et PV.

PAS D'HISTORIQUE : V2C Trydan. Sa classe `trydan` ne déclare pas l'interface
`smartmeter`, donc elle est ABSENTE de GetThingPowerLogs — la borne simulée, qui
la déclare, répond. Sa puissance instantanée reste lisible, sa courbe n'existe
pas. Une ligne à zéro dirait « elle n'a rien tiré », ce qui est autre chose.

AUCUNE SOURCE : la batterie. Aucun Thing ne porte battery / energystorage /
batterymonitor. Donc ni SOC ni charge/décharge — et le « Batterie 0 % » du
tableau de bord est un zéro forgé de plus (EnergyData.batterySOC vaut 0 par
défaut et personne ne l'écrase). À corriger dans le même lot.

HORS NYMEA : la météo. Aucun Thing météo ; Open-Meteo serait un appel externe.

Et les trois « on ne voit pas » du premier graphe ont trois causes distinctes,
dont deux partagent une racine : `production` est publiée NÉGATIVE, donc la
courbe est sous l'axe ET `selfConsumptionPower` (production − export, clampé à
0) rend zéro à tous les pas. Le SOC, lui, est correctement absent.

Le signe se corrigera au RENDU, jamais au transport : la magnitude est vérifiée
(identité sur 97 échantillons sur 97), mais un .abs() dans `fetchPowerBalanceLogs`
effacerait la trace du défaut et masquerait un vrai zéro le jour d'un changement
de convention.

Maquette publiée : https://claude.ai/code/artifact/f90d71d0-b876-4477-9231-4089d521b8ea
Aucune ligne de code écrite — quatre décisions attendent Patrick.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
This commit is contained in:
Patrick Schurig 2026-09-08 09:28:43 +02:00
parent 5a3f9a4fc6
commit 3ff42624f0

View File

@ -0,0 +1,387 @@
<title>Écran Énergie — refonte</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
<style>
:root{
--bg:#0E1A24; --bg2:#132330; --surface:#172B3A; --line:#22415A;
--ink:#E8F1F7; --muted:#8FA9BC;
--solar:#FEC113; --house:#31A3DD; --import:#E5674E; --export:#4FB477;
--eco:#34C77B; --pac:#E88C30; --ecs:#2AA7A0; --ve:#7C6CE4; --dead:#3E5468;
--ok:#34C77B; --no:#E5674E; --warn:#E8B93A;
--sans:'IBM Plex Sans',system-ui,-apple-system,Segoe UI,sans-serif;
--mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
}
*{box-sizing:border-box}
body{background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.55;
margin:0;padding:0 20px 90px}
.wrap{max-width:1180px;margin:0 auto}
header{padding:56px 0 34px;border-bottom:1px solid var(--line)}
.eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.14em;text-transform:uppercase;
color:var(--muted)}
h1{font-size:clamp(30px,4.4vw,46px);line-height:1.08;margin:12px 0 14px;font-weight:700;
letter-spacing:-.02em;text-wrap:balance}
.lede{font-size:17px;color:var(--muted);max-width:64ch;margin:0}
.lede strong{color:var(--ink);font-weight:600}
h2{font-size:23px;font-weight:700;margin:52px 0 6px;letter-spacing:-.01em;text-wrap:balance}
h2 .num{font-family:var(--mono);font-size:13px;color:var(--muted);
display:block;letter-spacing:.14em;margin-bottom:6px;font-weight:500}
h3{font-size:15px;font-weight:600;margin:26px 0 8px}
p{max-width:70ch}
.sub{color:var(--muted);margin:0 0 20px;max-width:70ch}
code,.m{font-family:var(--mono);font-size:.9em;background:var(--bg2);
padding:1px 6px;border-radius:5px;border:1px solid var(--line)}
.scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
table{border-collapse:collapse;width:100%;min-width:660px;font-size:14px;margin:8px 0 4px}
th,td{text-align:left;padding:11px 13px;border-bottom:1px solid var(--line);vertical-align:top}
th{font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;
color:var(--muted);font-weight:500;white-space:nowrap}
td.n{font-family:var(--mono);text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
tr.dim td{color:var(--muted)}
.tag{font-family:var(--mono);font-size:11px;padding:2px 8px;border-radius:20px;
border:1px solid;white-space:nowrap;display:inline-block}
.t-ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);
background:color-mix(in srgb,var(--ok) 10%,transparent)}
.t-no{color:var(--no);border-color:color-mix(in srgb,var(--no) 45%,transparent);
background:color-mix(in srgb,var(--no) 10%,transparent)}
.t-warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent);
background:color-mix(in srgb,var(--warn) 10%,transparent)}
.note{border-left:3px solid var(--line);padding:2px 0 2px 18px;margin:22px 0;color:var(--muted)}
.note.red{border-color:var(--no)}
.note.amber{border-color:var(--warn)}
.note strong{color:var(--ink)}
.grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:20px;margin-top:20px}
.card{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:18px 20px}
.card h4{margin:0 0 6px;font-size:14.5px;font-weight:600}
.card p{margin:0;font-size:13.5px;color:var(--muted)}
/* ── téléphone ─────────────────────────────────────────── */
.phones{display:flex;gap:26px;flex-wrap:wrap;margin-top:26px}
.phone{width:352px;flex:0 0 auto;background:var(--bg2);border:1px solid var(--line);
border-radius:26px;padding:14px;box-shadow:0 18px 46px rgba(0,0,0,.34)}
.phone .cap{font-family:var(--mono);font-size:11px;color:var(--muted);
letter-spacing:.08em;padding:2px 4px 10px}
.scr{background:var(--bg);border-radius:16px;padding:14px 12px;border:1px solid var(--line)}
.bar{display:flex;justify-content:space-between;align-items:center;font-size:12px;
color:var(--muted);font-family:var(--mono);padding:0 2px 10px}
.kpis{display:grid;grid-template-columns:1fr 1fr;gap:9px}
.kpi{background:var(--surface);border:1px solid var(--line);border-radius:11px;padding:11px 12px}
.kpi .v{font-family:var(--mono);font-size:20px;font-weight:600;
font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.kpi .l{font-size:11px;color:var(--muted);margin-top:2px}
.kpi .d{font-size:10.5px;color:var(--muted);opacity:.85;margin-top:1px}
.kpi.solar .v{color:var(--solar)} .kpi.house .v{color:var(--house)}
.kpi.exp .v{color:var(--export)} .kpi.imp .v{color:var(--import)}
.sect{font-size:11.5px;color:var(--muted);font-weight:600;margin:16px 2px 8px;
letter-spacing:.03em;display:flex;justify-content:space-between;align-items:baseline}
.sect span{font-family:var(--mono);font-size:10.5px;font-weight:400}
.chart{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:12px 10px 8px}
.legend{display:flex;flex-wrap:wrap;gap:9px;margin-top:9px;font-size:10.5px;color:var(--muted)}
.legend i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px;
vertical-align:-1px}
.legend .off{opacity:.45;text-decoration:line-through}
svg{display:block;width:100%;height:auto}
.impossible{background:color-mix(in srgb,var(--no) 7%,var(--surface));
border:1px solid color-mix(in srgb,var(--no) 30%,var(--line));
border-radius:11px;padding:11px 13px;margin-top:9px}
.impossible .h{font-size:12.5px;font-weight:600;color:var(--no)}
.impossible .b{font-size:11.5px;color:var(--muted);margin-top:3px}
ol.dec{counter-reset:d;list-style:none;padding:0;margin:18px 0 0}
ol.dec li{counter-increment:d;position:relative;padding:16px 0 16px 52px;
border-top:1px solid var(--line)}
ol.dec li::before{content:counter(d,decimal-leading-zero);position:absolute;left:0;top:16px;
font-family:var(--mono);font-size:13px;color:var(--muted);
border:1px solid var(--line);border-radius:7px;padding:3px 8px}
ol.dec b{display:block;font-size:15px;margin-bottom:4px}
ol.dec p{margin:0;color:var(--muted);font-size:14px}
footer{margin-top:60px;padding-top:22px;border-top:1px solid var(--line);
color:var(--muted);font-size:13px}
</style>
<div class="wrap">
<header>
<div class="eyebrow">ETM PowerSync · maquette à valider · 8 septembre 2026</div>
<h1>Écran Énergie — ce que la box permet, et ce qu'elle ne permet pas</h1>
<p class="lede">Quatre demandes, <strong>quatre séries traçables</strong>, et
<strong>deux qui n'ont aucune source</strong>. Tout ce qui suit est mesuré sur
<span class="m">.75</span> ce matin — pas déduit d'un contrat.</p>
</header>
<h2><span class="num">01 — le relevé</span>Ce que la box publie, par consommateur</h2>
<p class="sub"><span class="m">Energy.GetThingPowerLogs</span> existe et répond. Sonde de
00:00 à 09:00, pas de 15 min, six Things demandés — <strong>cinq répondent</strong>.</p>
<div class="scroll">
<table>
<thead><tr>
<th>Série demandée</th><th>Source</th><th>Interfaces du Thing</th>
<th class="n">Relevé ce matin</th><th>Verdict</th>
</tr></thead>
<tbody>
<tr>
<td><b>Maison</b></td><td><span class="m">Grid</span> + <span class="m">PV</span>, dérivée</td>
<td><span class="m">smartmeter</span></td>
<td class="n">284 → 3 095 W</td><td><span class="tag t-ok">traçable</span></td>
</tr>
<tr>
<td><b>PAC</b></td><td><span class="m">PAC-Meter</span></td>
<td><span class="m">smartmeterconsumer</span></td>
<td class="n">0 W toute la journée</td><td><span class="tag t-ok">traçable</span></td>
</tr>
<tr>
<td><b>ECS</b></td><td><span class="m">ECS-Meter</span></td>
<td><span class="m">smartmeterconsumer</span></td>
<td class="n">0 W toute la journée</td><td><span class="tag t-ok">traçable</span></td>
</tr>
<tr>
<td><b>VE 2</b></td><td><span class="m">Simulated wallbox</span></td>
<td><span class="m">evcharger · smartmeterconsumer</span></td>
<td class="n">0 W toute la journée</td><td><span class="tag t-ok">traçable</span></td>
</tr>
<tr class="dim">
<td><b>VE 1</b></td><td><span class="m">V2C Trydan</span></td>
<td><span class="m">evcharger · power</span> — pas <span class="m">smartmeter</span></td>
<td class="n">absente de la réponse</td><td><span class="tag t-warn">pas d'historique</span></td>
</tr>
<tr class="dim">
<td><b>Batterie</b> (charge / décharge, SOC)</td><td>—</td>
<td>aucun Thing <span class="m">battery · energystorage · batterymonitor</span></td>
<td class="n">—</td><td><span class="tag t-no">aucune source</span></td>
</tr>
<tr class="dim">
<td><b>Prévision météo</b></td><td>—</td>
<td>aucun Thing météo sur la box</td>
<td class="n">—</td><td><span class="tag t-no">hors nymea</span></td>
</tr>
</tbody>
</table>
</div>
<div class="note red">
<strong>La V2C n'a pas d'historique de puissance, et ce n'est pas un oubli.</strong>
Sa classe <span class="m">trydan</span> ne déclare pas l'interface <span class="m">smartmeter</span>,
donc elle n'entre pas dans <span class="m">GetThingPowerLogs</span> — la borne simulée, elle,
la déclare et répond. On peut afficher sa puissance <em>instantanée</em>
(<span class="m">currentPower</span>, et <span class="m">measurement</span> dans la télémétrie),
mais <strong>pas sa courbe</strong>. Deux bornes côte à côte dont une seule a un passé : l'écran
doit le dire, pas tracer une ligne à zéro.
</div>
<div class="note red">
<strong>Il n'y a pas de batterie sur cette installation.</strong> Aucun Thing ne porte les
interfaces que l'app cherche. Donc ni SOC, ni charge, ni décharge — et le
<span class="m">Batterie 0 %</span> affiché aujourd'hui sur le tableau de bord est un
<strong>zéro forgé</strong> : <span class="m">EnergyData.batterySOC</span> vaut 0 par défaut et
personne ne l'écrase. À corriger dans le même lot.
</div>
<h2><span class="num">02 — le premier graphe</span>Trois courbes invisibles, trois causes différentes</h2>
<p class="sub">Aucune n'est un problème de dessin. Les trois sont dans les données ou dans leur
lecture, et deux partagent la même racine.</p>
<div class="grid2">
<div class="card">
<h4 style="color:var(--solar)">Production — sous l'axe</h4>
<p>La box publie <span class="m">production</span> <b>négatif</b> : −103 W ce matin.
Le transport garde les signes bruts (§7.1 du contrat, à raison), donc la courbe est tracée
à −0,1 kW pendant que l'échelle monte à 3,1 — invisible, collée à l'axe.</p>
</div>
<div class="card">
<h4 style="color:var(--eco)">Autoconsommation — clouée à zéro</h4>
<p><span class="m">selfConsumptionPower</span> calcule <span class="m">production − export</span>
puis <span class="m">&gt; 0 ? … : 0</span>. Avec une production négative le résultat est
<b>toujours</b> zéro. Même racine que la précédente.</p>
</div>
<div class="card">
<h4 style="color:var(--muted)">SOC — rien à tracer</h4>
<p><span class="m">batterySOCSource</span> cherche un Thing
<span class="m">battery/energystorage/batterymonitor</span> et n'en trouve aucun : il rend
<span class="m">null</span>, la courbe n'est pas construite. <b>Comportement correct</b> —
ce qui manque, c'est de le <em>dire</em>.</p>
</div>
</div>
<div class="note amber">
<strong>Le signe se corrige au RENDU, jamais au transport.</strong> Le magnitude est juste :
l'identité <span class="m">acquisition = consommation − |production|</span> tient sur
<b>97 échantillons sur 97</b>. Mais poser un <span class="m">.abs()</span> dans le transport
effacerait la seule trace qu'on ait du défaut, et masquerait un vrai zéro le jour où la box
changera de convention. La dérivation prend la magnitude ; le champ reste brut.
</div>
<h2><span class="num">03 — la proposition</span>L'écran</h2>
<p class="sub">Trois blocs. Les cartes disent la <b>journée</b>, le premier graphe dit les
<b>flux</b>, le second dit <b>qui consomme</b>. Chaque nombre porte de quoi il est la fraction.</p>
<div class="phones">
<!-- ── colonne 1 ─────────────────────────────── -->
<div class="phone">
<div class="cap">1 — cartes journalières + flux</div>
<div class="scr">
<div class="bar"><span>Énergie</span><span>08 sept.</span></div>
<div class="sect">Aujourd'hui <span>depuis 00:00</span></div>
<div class="kpis">
<div class="kpi solar"><div class="v">0,08 kWh</div><div class="l">Production</div>
<div class="d">79 Wh — journée couverte</div></div>
<div class="kpi house"><div class="v">6,47 kWh</div><div class="l">Consommation</div></div>
<div class="kpi exp"><div class="v">0,02 kWh</div><div class="l">Vers réseau</div></div>
<div class="kpi imp"><div class="v">6,42 kWh</div><div class="l">Depuis réseau</div></div>
<div class="kpi"><div class="v" style="color:var(--eco)">70 %</div>
<div class="l">Autoconsommation</div><div class="d">sur 79 Wh produits</div></div>
<div class="kpi"><div class="v" style="color:var(--eco)">1 %</div>
<div class="l">Autonomie</div><div class="d">sur 6,47 kWh consommés</div></div>
</div>
<div class="sect">Flux de la journée <span>kW</span></div>
<div class="chart">
<svg viewBox="0 0 320 150" role="img"
aria-label="Courbes production, consommation, autoconsommation sur la journée">
<g stroke="#22415A" stroke-width=".6">
<line x1="26" y1="14" x2="314" y2="14"/><line x1="26" y1="48" x2="314" y2="48"/>
<line x1="26" y1="82" x2="314" y2="82"/><line x1="26" y1="116" x2="314" y2="116"/>
</g>
<g font-family="IBM Plex Mono" font-size="7.5" fill="#8FA9BC">
<text x="2" y="17">4</text><text x="2" y="51">3</text>
<text x="2" y="85">2</text><text x="2" y="119">1</text><text x="2" y="140">0</text>
<text x="26" y="148">00h</text><text x="150" y="148">09h</text><text x="292" y="148">24h</text>
</g>
<line x1="26" y1="136" x2="314" y2="136" stroke="#22415A" stroke-width="1"/>
<!-- consommation -->
<path d="M26 128 L60 124 L94 118 L128 100 L150 82 L170 96 L200 108 L240 116 L280 122 L314 126"
fill="none" stroke="#31A3DD" stroke-width="1.8"/>
<!-- production (magnitude) -->
<path d="M26 136 L70 135 L100 133 L128 130 L150 128 L180 126 L210 129 L250 133 L290 136 L314 136"
fill="none" stroke="#FEC113" stroke-width="1.8"/>
<!-- autoconsommation -->
<path d="M26 136 L70 135.5 L100 134 L128 132 L150 130 L180 128.5 L210 131 L250 134 L290 136 L314 136"
fill="none" stroke="#34C77B" stroke-width="1.5" stroke-dasharray="3 2"/>
</svg>
<div class="legend">
<span><i style="background:#FEC113"></i>Production</span>
<span><i style="background:#31A3DD"></i>Consommation</span>
<span><i style="background:#34C77B"></i>Autoconsommation</span>
<span class="off"><i style="background:#3E5468"></i>SOC batterie</span>
</div>
<div class="impossible">
<div class="h">SOC — non affichable</div>
<div class="b">Aucune batterie déclarée sur cette installation. La courbe n'est pas
masquée : elle n'existe pas.</div>
</div>
</div>
</div>
</div>
<!-- ── colonne 2 ─────────────────────────────── -->
<div class="phone">
<div class="cap">2 — répartition par consommateur</div>
<div class="scr">
<div class="sect">Qui consomme <span>kW empilés</span></div>
<div class="chart">
<svg viewBox="0 0 320 190" role="img"
aria-label="Aires empilées par consommateur sur la journée">
<g stroke="#22415A" stroke-width=".6">
<line x1="26" y1="18" x2="314" y2="18"/><line x1="26" y1="58" x2="314" y2="58"/>
<line x1="26" y1="98" x2="314" y2="98"/><line x1="26" y1="138" x2="314" y2="138"/>
</g>
<g font-family="IBM Plex Mono" font-size="7.5" fill="#8FA9BC">
<text x="2" y="21">4</text><text x="2" y="61">3</text>
<text x="2" y="101">2</text><text x="2" y="141">1</text><text x="2" y="182">0</text>
<text x="26" y="188">00h</text><text x="150" y="188">09h</text><text x="292" y="188">24h</text>
</g>
<line x1="26" y1="178" x2="314" y2="178" stroke="#22415A" stroke-width="1"/>
<!-- Maison (base) -->
<path d="M26 178 L26 160 L70 156 L110 150 L150 118 L190 138 L240 150 L290 158 L314 160 L314 178 Z"
fill="#31A3DD" opacity=".75"/>
<!-- PAC (nulle aujourd'hui : liseré) -->
<path d="M26 160 L70 156 L110 150 L150 118 L190 138 L240 150 L290 158 L314 160"
fill="none" stroke="#E88C30" stroke-width="1.4" stroke-dasharray="2 3"/>
<text x="176" y="112" font-family="IBM Plex Mono" font-size="7" fill="#8FA9BC">
PAC · ECS · VE 2 — 0 W mesuré</text>
</svg>
<div class="legend">
<span><i style="background:#31A3DD"></i>Maison</span>
<span><i style="background:#E88C30"></i>PAC</span>
<span><i style="background:#2AA7A0"></i>ECS</span>
<span><i style="background:#7C6CE4"></i>VE 2</span>
<span class="off"><i style="background:#3E5468"></i>VE 1 · Batterie</span>
</div>
<div class="impossible">
<div class="h">VE 1 (V2C) — pas d'historique</div>
<div class="b">La borne ne déclare pas d'interface de comptage : sa puissance
instantanée est lisible, sa courbe n'existe pas. Une ligne à zéro dirait
« elle n'a rien tiré », ce qui est une autre affirmation.</div>
</div>
<div class="impossible">
<div class="h">Batterie — aucune source</div>
<div class="b">Ni charge ni décharge : rien à empiler.</div>
</div>
</div>
<div class="sect">Prévision solaire <span>Open-Meteo</span></div>
<div class="chart">
<div style="font-size:12.5px;color:var(--muted)">
<b style="color:var(--ink)">Hors nymea.</b> Aucun Thing météo sur la box : la prévision
viendrait d'un appel HTTP direct de l'app à Open-Meteo, avec les coordonnées de
l'installation. C'est une <b>source externe</b>, donc une décision à part —
et une fonctionnalité de palier <span class="m">Auto</span>.
</div>
</div>
</div>
</div>
</div>
<h2><span class="num">04 — à trancher</span>Ce que je ne décide pas seul</h2>
<ol class="dec">
<li>
<b>Les couleurs des consommateurs n'existent pas dans le thème.</b>
<p><span class="m">app_theme.dart</span> fixe les couleurs de <em>flux</em> (solaire, maison,
réseau, batterie) mais rien pour PAC / ECS / VE. Je propose ambre pour la PAC, sarcelle pour
l'ECS, indigo pour les bornes — trois teintes qui ne rentrent en collision avec aucune
couleur de flux existante. À valider, elles deviendront des tokens.</p>
</li>
<li>
<b>Une série sans passé : ligne absente ou ligne à zéro ?</b>
<p>Je propose <b>absente, et nommée sous le graphe</b>. Une ligne plate à zéro affirme
« cette charge n'a rien tiré », ce que personne n'a mesuré. C'est la règle déjà appliquée
aux cartes de charge, portée au graphe.</p>
</li>
<li>
<b>Le taux avec son dénominateur — partout, ou seulement quand il est petit ?</b>
<p>Je propose <b>partout</b>. Un seuil demanderait une constante inventée, et celle que
j'avais proposée samedi (0,05 kWh) aurait laissé passer les 79 Wh de ce matin. « 70 % sur
79 Wh produits » se lit juste toute l'année ; « 70 % » ne se lit juste que les bons jours.</p>
</li>
<li>
<b>La météo, maintenant ou plus tard ?</b>
<p>Elle sort du périmètre nymea : appel externe, coordonnées de l'installation à saisir,
gestion du hors-ligne. C'est un lot en soi. Je propose de <b>finir les deux graphes d'abord</b>
et de garder la place à l'écran.</p>
</li>
</ol>
<footer>
Relevé sur <span class="m">.75</span> — plugin <span class="m">1.15.2+etm58</span>,
nymea <span class="m">1.15.2+202606191336~trixie1</span>. Fenêtre 00:00 → 09:00 du
8 septembre 2026, pas de 15 min.
Aucune ligne de code écrite avant validation de cette maquette.
</footer>
</div>