4 Commits

Author SHA1 Message Date
Patrick Schurig
a430fb00c8 fix(instants): un seul lecteur d'horodatage de box — et ma cause d'hier était FAUSSE
Le contrat porte désormais la règle : tout horodatage publié est en UTC avec son
marqueur, ou en epoch. Trois champs, deux formats, rien ne l'exigeait.

`instantDeLaBox` (lib/models/box_instant.dart) devient l'unique lecteur pour ce
que la box publie — epoch ou ISO, marqué ou nu — et requalifie la forme nue en
UTC avant de rendre l'heure du lecteur. Politique valable pour la BOX seule :
`Installation.lastSeen` est écrite en local sans marqueur et relue pareil, donc
la lire en UTC la décalerait. Deux héritages opposés sous une même forme, d'où
l'absence délibérée d'un « parser une date » unique.

Le `.toLocal()` posé hier dans telemetry_text est retiré : le modèle est seul
propriétaire de la conversion. Le rejouer serait sans effet mais laisserait
croire qu'elle peut manquer ailleurs.

CORRECTION. J'avais écrit que le défaut avait survécu « parce que box et
téléphone partageaient l'heure ». C'est faux, et la fausseté est utile : l'écart
est entre UTC et l'APPAREIL, donc visible sur tout téléphone hors UTC+0 quel que
soit le fuseau de la box. Mesuré ce matin avec les deux en Europe/Paris — la box
publie 04:33:28Z, l'APK d'hier affiche « 04:33 », il fallait lire 06:33. La
bascule en Europe/London n'a pas donné la visibilité, elle a donné le MOTIF de
regarder. La version fausse fait conclure « les fuseaux coïncident, donc c'est
réglé » et laisse le défaut en place.

`GetVariableLoadState` n'existe pas sur .75 (absente d'Introspect) et l'app ne
lit aucun `since` : rien à corriger de ce côté. Le champ existe en revanche dans
GetLoadTelemetry — `loads[].command {divergent, expectedW, observedW, since}` —
et il porte bien le Z, vérifié à la sonde. Aucun écran ne le consomme encore.

Contrôle négatif de la ligne d'horloge : même binaire qu'hier, box repassée en
Europe/Paris, la ligne a disparu sous le champ d'échéance.
docs/captures/2026-09-07_fuseau-box-paris-ligne-absente.png

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-07 07:34:24 +02:00
Patrick Schurig
b843d8936a fix(fuseau): un INSTANT se rend chez le lecteur — « mesuré depuis » affichait l'heure UTC
Trouvé sur appareil pendant la bascule volontaire de .75 en Europe/London, en
capturant la ligne d'horloge de l'échéance : la box publie
`measuredSince: "2026-09-06T14:38:00Z"` et l'écran affichait « mesuré depuis
14:38 » — ni l'heure de la box (15:38 BST), ni celle du téléphone (16:38 CEST).
L'heure de personne.

`DateTime.tryParse` d'une chaîne en `Z` rend un DateTime `isUtc: true` ; lire
`.hour` sans `toLocal()` donne l'heure UTC. Le défaut est identiquement
invisible tant que box et appareil partagent l'offset : il a fallu DEUX fuseaux
pour qu'il se voie, et c'est la couture ouverte pour la capture qui l'a montré.

Deux natures d'heure, désormais séparées dans CLAUDE.md :
- un RÉGLAGE (`dailyDeadline`) est lu par la box → on nomme son horloge ;
- un INSTANT (`measuredSince`) est absolu → il se rend chez le lecteur, sans
  mention.

Les deux tests qui gardaient « 03:42 » en dur gelaient l'heure UTC et validaient
le défaut : leur attendu se dérive maintenant de `.toLocal()`. Le contrôle
négatif ne mord que hors UTC, et le test le dit au lieu de laisser lire un vert
comme une preuve.

Capture de la ligne d'horloge : docs/captures/2026-09-06_fuseau-box-london.png

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 19:57:38 +02:00
Patrick Schurig
42eef05c3b fix(carte): le parent domine, et la phrase éco cesse de se répéter
Deuxième passage sur appareil, écran des charges. Trois défauts, dont deux de
notre fait et un que les fixtures ne pouvaient pas produire.

1. LA FRACTION ÉTAIT ÉCRITE DEUX FOIS — `ECO_FLOOR_MET` porte déjà livré et
   cible dans son motif, et la ligne d'avancement les réécrivait dessous :
   « Plancher atteint — 29,9 kWh sur 3,0 kWh livrés » puis « 29,9 kWh sur 3,0
   kWh livrés · mesuré depuis 10:00 ». C'est le défaut de la ligne des ratios de
   la veille, refait à l'identique : déclencher sur ce qu'on SAIT plutôt que sur
   ce qui se VOIT. La ligne ne garde plus que ce que le motif ne dit pas.

2. « X SUR Y » N'EST PAS UNE FRACTION une fois le plancher dépassé — le compteur
   continue de courir après que l'obligation est tenue, et le numérateur finit à
   dix fois le dénominateur. Aucune fixture ne le montrait : toutes avaient un
   livré inférieur à la cible. Reformulé « 3,0 kWh dus, 30,2 kWh livrés ».

3. LE PARENT NE DOMINAIT PAS — à taille et graisse égales, l'allocation et les
   deux niveaux se lisaient comme trois lignes sœurs, et les pastilles colorées
   tiraient l'œil davantage que la commande qu'elles expliquent : le contresens
   exact que la forme doit empêcher (LM-1003). L'allocation passe en 13,5 gras,
   les pastilles perdent leur fond, et un filet vertical rattache chaque niveau
   à la commande.

Vérifié à l'écran après correction, et le repli l'était déjà : V2C et Simulated
wallbox tiennent sur une seule ligne, sans étiquette orpheline ni vide résiduel.

Et le signe du solaire est CONFIRMÉ, ce qui n'avait pas pu l'être hier faute de
production : la box publie -4 376 W, l'écran affiche « 4.4 kW ».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 08:11:22 +02:00
Patrick Schurig
58bf57e67f feat(§10): la carte de charge porte ses deux niveaux — une commande, deux lignes
Premier lot d'écran, sur des champs tous posés (+etm55) et tous vus à la sonde.
L'allocation reste le parent ; les niveaux en sont la décomposition. Une charge
reçoit UNE commande par cycle quelles que soient les passes (LM-1003) : deux
lignes décrivent une seule décision.

- `EcoProgress` — regime, targetWh, deliveredWh (absent sous `unmeasurable`,
  jamais 0), purchasedWh (publié dans TOUS les régimes : c'est la commande du
  moteur, donc un fait connu sans mesure), measuredSince, measuredGaps ;
- `LoadLevel` gagne `decision` et `progress` : chaque passe porte son motif.
  Lire la racine seule afficherait « rien fait » sur une charge qui achète —
  relevé du 2026-09-01 05:47, `SURPLUS_INSUFFICIENT` en racine et 767 W achetés
  au niveau éco ;
- `ecoDecisionText` / `ecoProgressText` : les phrases des trois motifs éco et de
  leurs quatre causes. Aucun nom de paramètre n'est deviné — `cause`,
  `missingWh`, les quatre valeurs et `measuredGaps` sont vérifiés dans le binaire
  posé. Seule la FORME de `measuredGaps` reste non observée, donc on n'en lit que
  la présence ;
- garde R6 à l'écran : `ECO_FLOOR_MET` sous `unmeasurable` est DÉNONCÉ comme une
  incohérence de la box, pas rendu. Servie oui, tenue jamais.

Deux interdits écrits dans le code, pas seulement dans la maquette : sous
`unmeasurable` on n'annonce pas que l'achat va s'intensifier (le taux est
constant depuis +etm55), et `atMaximum: false` n'y veut pas dire « on n'est pas
au plafond » mais « il n'y a aucun étalement, donc rien qui puisse cesser ». Un
booléen à false se lit spontanément comme rassurant.

Les charges utiles des tests viennent de `.75` — le cas éco-servi/confort-en-
attente du 2026-09-01, le cas mesuré du 2026-09-04 — pas d'une lecture de
contrat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-05 07:54:22 +02:00