etm-powersync-app/docs/mockups/ecran_energie_refonte.html
Patrick Schurig 3ff42624f0 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
2026-09-08 09:28:43 +02:00

388 lines
21 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.

<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>