docs(recap): la journée du 2026-08-26 — trois recalages de contrat, et la réserve batterie

Le fil conducteur des six commits du jour tient en une phrase : l'app ne recalcule pas
ce que la box publie, et n'anticipe pas ce qu'elle n'a pas encore publié.

Le point à retenir est le seuil de réserve batterie : 0,9 en usine, batterie du banc à
50 %. Quand la règle montera dans le budget, `.75` ne pilotera plus rien par grand
soleil, sans erreur ni panne. Le RECAP le dit, et le TODO garde les trois crochets
laissés ouverts — dont le `Set` volontairement vide qui attend le code du moteur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
This commit is contained in:
Patrick Schurig 2026-08-26 18:06:54 +02:00
parent 801ba15094
commit c1af47476d
2 changed files with 122 additions and 3 deletions

110
RECAP.md
View File

@ -1,8 +1,112 @@
# RECAP — etm-powersync-app · reprise de session
> **Session 2026-08-25.** Branche **`feature/beta-add-config`**, **poussée**.
> La section « État au 2026-06-30 » plus bas reste valable pour tout ce qu'elle décrit.
> La **soirée du 25** (lot C) est en tête ci-dessous ; le reste de la journée suit.
> **Session 2026-08-26.** Branche **`feature/beta-add-config`**, **poussée** (`801ba15`).
> La journée du **26** est en tête ci-dessous ; la soirée du **25** (lot C) suit ; la section
> « État au 2026-06-30 » plus bas reste valable pour tout ce qu'elle décrit.
---
## Journée 2026-08-26 — se recaler sur la box, trois fois de suite
La box a bougé trois fois dans la journée (`+etm16`, `+etm19`, puis `3g`). Le fil conducteur
de tout ce qui suit tient en une phrase : **l'app ne recalcule pas ce que la box publie, et
n'anticipe pas ce qu'elle n'a pas encore publié.**
| Commit | Contenu |
|---|---|
| `aada8e2` | Les 4 points ouverts du RECAP : glissement, sélecteur de mécanisme, chip de domaine, thème |
| `7089148` | Le harnais vérifie lui-même le chip et l'écran de mécanisme |
| `cd76df3` | **`+etm16`** — table de paliers, `stagesW`, compteur et sonde |
| `ab2a27b` | **`+etm19`** — `BELOW_MIN_POWER`, compteurs rattachés depuis l'écran, glissement exercé |
| `449c6ac` | **`3g`** — les valeurs de borne sont du RUNTIME |
| `801ba15` | **`3g`** — la réserve batterie sort de l'usine |
### Ce qui a été corrigé, et pourquoi ça avait échoué
- **Le glissement ne marchait pas** — et c'était ma régression : j'avais imbriqué un
`ReorderableListView` dans un autre, les deux se disputaient le geste. Le classement est
désormais **inversé à dessein** : on glisse les *domaines*, et les flèches ordonnent *dans*
un domaine. Restait une **zone morte de 3 px** dans la poignée : `ReorderableDragStartListener`
délègue le test de toucher à ses enfants, un `Container` transparent la referme.
- **Le test du glissement déclarait forfait** deux fois sur trois : le banc revient
régulièrement à une charge par domaine, et sans domaine partagé il n'y a rien à glisser. Le
test **pose maintenant sa propre condition** — il regroupe, exerce, puis rend le classement
d'origine. Un test qui dépend de l'état du terrain ne teste rien.
- **« la config du rootmeter est perdue »** — elle ne l'était pas : la box l'avait toujours,
l'app ne la **lisait** jamais (`syncRootMeter()`). Le symptôme et la cause étaient à deux
écrans de distance.
- **`/energy/setup` est gardé par le mode installateur** : le routeur renvoyait ailleurs, et le
symptôme affiché était « aucune charge lue ». Le harnais déverrouille au PIN `1234` et
**reverrouille après** le maintien d'écran — l'ordre inverse passait au vert en montrant le
tableau de bord.
### Les trois recalages de contrat
**`+etm16` — ce que la box compare vraiment.** `sameHardware()` est public, et la comparaison
porte sur la **table de paliers**, pas sur la liste de relais : réordonner `relays[]` ne
reconstruit plus rien (vérifié au banc — « 2 inchangée(s) », aucun état sûr). Mon premier
miroir comparait des listes et criait au loup ; corrigé après lecture de
`RelayStageTable::operator==` — chaque combinaison se compare **comme un ensemble**, mais les
paliers s'apparient par position. Les paliers dérivés arrivent maintenant en `stagesW` : un
miroir de moins à faire diverger. Les trois champs optionnels (`meterThingId`, `sensorThingId`,
plancher) sont traités **comme un seul objet**, grisage dérivé du schéma, jamais anticipés dans
le modèle.
**`+etm19` — `BELOW_MIN_POWER`.** Deux variantes, à distinguer de `SURPLUS_INSUFFICIENT` et
`SG_NORMAL` qui ne se déclenchent plus que sur budget ≤ 0. Le repli aurait tenu : un code
inconnu n'a jamais affiché de vide.
**`3g` — deux valeurs de runtime, et un réglage qui change de portée.** « Minimum » désigne
deux objets différents : le **plancher déclaré** d'une charge modulable (une propriété de
l'appareil, qui se règle) et le **minimum d'une borne**, `max(borne, voiture)`, qui dépend du
véhicule branché et du nombre de phases — de l'ordre de 1,4 kW en monophasé contre 4,1 kW en
triphasé, **pour le même courant**. L'app affiche la valeur telle que la box la donne : aucune
conversion côté app, sinon il faudrait deviner laquelle des deux règles s'applique. Et le
budget **ne se partage pas** entre bornes : aujourd'hui chacune reçoit l'enveloppe entière.
### La réserve batterie — le point à retenir de la journée
`batteryLevelConsideration` vaut **0,9** en usine. Le banc `.75` a sa batterie à **50 %** : le
seuil est au-dessus de ce que la batterie atteint. Aujourd'hui ça ne gèle que la recharge du
véhicule ; **quand la règle montera dans le budget, `.75` ne pilotera plus rien** — ni ECS, ni
PAC, ni bornes — par grand soleil, sans erreur ni panne, rien à lire dans un journal.
Un paramètre qui peut produire ce symptôme n'est pas un paramètre d'usine.
`BatteryReserveCard` le lit, l'écrit, **alerte quand le seuil dépasse le SOC réellement
atteint**, et dit la portée à venir : régler ce seuil « pour la voiture » couperait le reste
demain. Elle n'invente aucun défaut — box muette, carte muette.
Le motif d'arbitrage est **préparé, pas anticipé** : la clé ARB `decisionBatteryReserve` existe
et `kCodesReserveBatterie` est un `Set` **volontairement vide** dans `telemetry_text.dart`. Le
crochet attend le code que publiera le moteur ; deviner son nom aurait produit un affichage qui
ne se déclenche jamais.
### Vérifié sur l'appareil, contre `.75`
Campagne verte, zéro exception. La notification précède l'accusé (4/4 écritures).
`enabled:false` ouvre bien les contacts (ECS-413). Les deux compteurs **PAC-Meter** et
**ECS-Meter** sont rattachés **depuis l'écran**, et `measuredW` est désormais **distinct** par
charge — chauffe-eau 1 500 W, PAC 800 W, cette dernière affichant « Mesuré 800 W · sous la
consigne » face à son estimation de 3 000 W.
> Deux pièges de méthode, notés parce qu'ils se représenteront :
> j'ai d'abord **affirmé des `measuredW` distincts** pour prouver le rattachement — deux
> compteurs au repos lisent légitimement 0,0 W tous les deux ; l'assertion porte maintenant sur
> des `meterThingId` distincts plus un détachement montrant `null` ≠ 0. Et une assertion
> cherchait les cartes **sans faire défiler** : dans un `ListView`, ce qui n'est pas à l'écran
> n'est pas construit — elle ne prouvait que la hauteur de l'écran.
### Reste ouvert
- `minPowerW == null` **sans véhicule branché** : pas exercé sur appareil, la borne n'entre pas
dans `loads[]` avant `3g`.
- `kCodesReserveBatterie` à câbler quand le moteur publiera le code du motif.
- **Les zones** (`ac_screen.dart` est encore de la donnée fictive, `.75` n'en déclare aucune) —
explicitement **à ne pas entreprendre**.
---
## Soirée 2026-08-25 — LOT C (ci-dessous)
---

15
TODO.md
View File

@ -1,5 +1,20 @@
# TODO — ETM PowerSync App
## Charges pilotables — ce qui attend la box (2026-08-26)
- [ ] **`kCodesReserveBatterie`** — le `Set` est volontairement VIDE dans
`lib/services/telemetry_text.dart`. Y mettre le code du motif « budget annulé par la réserve
batterie » **quand le moteur le publiera**, pas avant : deviner son nom produirait un
affichage qui ne se déclenche jamais.
- [ ] **`minPowerW == null` sans véhicule branché** — couvert en test unitaire
(`test/ev_runtime_test.dart`), **pas exercé sur appareil** : la borne n'entre pas dans
`loads[]` avant `3g`. À reprendre quand l'arbitrage prendra la recharge en charge.
- [ ] **Réserve batterie — portée élargie.** Le jour où le seuil gouverne tout le budget,
reprendre le texte de `BatteryReserveCard` : il annonce aujourd'hui un futur, il devra
décrire un présent.
- [ ] ~~Zones~~ — `ac_screen.dart` est de la donnée fictive et `.75` n'en déclare aucune.
**Décidé : à ne pas entreprendre.**
## En cours / priorité haute
- [ ] **TariffProvider — connexion RPC réelle**