Patrick Schurig 8b7bfbcded feat(config): le badge dit la provenance du rang, la note alerte sur un refus
Deux natures, donc deux messages — et la fin d'une redondance : la note d'ex æquo et le
badge disaient tous deux « ce rang n'est le choix de personne », ce qui garantissait que
ni l'un ni l'autre ne serait lu.

── Le badge PREND ce rôle, et le fait mieux ─────────────────────────────────

`rankOrigin == "auto"` (+etm27) → « Rang posé par la box, pas un choix », sur la carte de
la charge concernée. La note ne pouvait que le DÉDUIRE d'une égalité de rangs, donc elle
était muette sur une borne arrivée seule là où son rang ne heurtait personne — le cas qui
devient la norme, puisque les bornes auto-provisionnées entrent désormais en queue.

Ce que le badge n'affirme pas : il n'apparaît que sur "auto". Un champ ABSENT ne dit rien —
ni choix humain, ni pose d'office — et une box antérieure à +etm27 rend exactement l'écran
d'avant. Même règle que measurement.source. "user" n'affiche rien non plus : quelqu'un a
regardé ce rang, même sans déplacer la charge.

── La note CHANGE de nature : constat → alerte avec remède ──────────────────

Depuis +etm27, deux charges au même rang font refuser TOUT l'ensemble. En voir deux à
l'écran ne dit donc plus quelque chose sur l'ordre de service : ça dit que cette
configuration est héritée d'avant +etm27 — le plugin lui-même créait ces égalités — et
qu'elle sera rejetée à la première écriture, y compris pour un simple renommage, sans motif
lisible puisque le refus ne franchit pas la frontière RPC.

Elle passe donc en rouge, annonce le rejet, et donne le seul geste qui répare : des rangs
distincts. Le garde-fou de la suppression le disait déjà au moment du geste ; l'écran le dit
maintenant avant qu'on le tente.

Cinq tests montés : badge sur "auto" seulement, rien sur "user", rien sur champ absent,
l'alerte porte bien un refus ET un remède, et elle se tait sur des rangs distincts.

Reste au TODO : écrire `rankOrigin: "user"` depuis l'app — « ce rang me va » sans déplacer
la charge. Sans ce geste, le badge reste affiché pour dire une chose que l'installateur a
déjà tranchée.

218 tests (+5), flutter analyze inchangé à 27 remarques.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
2026-08-29 09:35:17 +02:00

251 lines
18 KiB
Markdown
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.

# TODO — ETM PowerSync App
## Ouvert au 2026-08-27
- [ ] 🔜 **`1.15.2+etm27` — trois nouveautés, dont une déjà couverte.**
Déploiement annoncé pour demain matin.
- [x] ~~**Refus en bloc sur deux rangs identiques.**~~ **Couvert le 2026-08-28, avant le
déploiement** : `validateSet()` porte le contrôle (`tiedPriority`), l'écran nomme les
deux charges et leur rang, et le chemin de suppression — qui réécrit verbatim, donc
transporte les rangs de la box — refuse plutôt que d'envoyer une égalité héritée. Huit
tests, dont le réaplatissage après **chaque** permutation. Le refus de la box est muet
(le motif ne franchit pas la frontière RPC) : sans ce miroir, le symptôme aurait été
« mon glissement ne s'enregistre pas ».
- [x] ~~**`o:rankOrigin` — badge « rang par défaut, pas un choix ».**~~ **Fait le
2026-08-29 : le badge REMPLACE la note dans ce rôle**, deux formulations pour une idée
faisant qu'aucune n'est lue. Il ne s'affiche que sur `"auto"` — un champ absent
n'affirme rien, ni choix humain ni pose d'office, et une box antérieure à `+etm27` rend
l'écran d'avant sans badge. `"user"` n'en affiche aucun : quelqu'un a regardé ce rang.
La note d'ex æquo, elle, **change de nature** : depuis `+etm27` deux rangs doublés
signifient une configuration héritée qui sera **rejetée en bloc à la première
écriture** — ce n'est plus un constat sur l'ordre de service, c'est une alerte avec son
remède (réordonner).
- [ ] **Écrire `rankOrigin: "user"` — « ce rang me va », sans déplacer la charge.** Le
badge s'efface aujourd'hui d'une seule façon : déplacer la charge, ce qui fait effacer
le champ par le moteur. Accepter le rang tel quel demande une écriture — et donc le
chemin lire → patcher → réécrire verbatim, avec le garde-fou des rangs à égalité. Utile
surtout sur une installation dont l'ordre par défaut convient : sans ce geste, le badge
reste à l'écran pour dire une chose que l'installateur a déjà tranchée.
- [ ] **Les bornes auto-provisionnées entrent en QUEUE (`1 + max`)**, plus en tête. Une
borne détectée après la mise en service ne passe donc plus devant l'ECS classé exprès ;
sur une installation neuve, la liste étant vide, le VE garde le rang 1. Vérifier que la
note d'ordre entrelacé et celle de rangs ex æquo se taisent bien dans ce nouveau cas —
elles décrivaient un défaut qui disparaît.
- [x] ~~⏳ Retirer l'alias `measuredW`~~ — **fait le 2026-08-28**, `1.15.2+etm26` déployé
sur `.75` et vérifié par sonde : `measuredW` a disparu de la charge utile, `measurement`
est en ligne. `MeasurementSource.legacy` est parti avec lui ; ce qui reste,
`MeasurementSource.inconnue`, couvre une box **plus récente** que l'app, pas plus
ancienne. Sources relevées sur le banc : `meter` 1 500 W (chauffe-eau, ECS-Meter),
`meter` 800 W (pac-terrain, PAC-Meter), et la V2C **absente de `loads[]`** — pas `none`.
- [x] ~~🔴 **Bilan JOURNALIER**~~ — **fait le 2026-08-28.** Les quatre tuiles montrent la
journée, sous un en-tête « Aujourd'hui » qui le dit une fois. Méthode : différence des
compteurs entre minuit local et maintenant, jamais l'intégration des puissances —
mesurée contre estimée, 0,8 % d'écart sur `.75`. Le contrôle qui la valide est
l'identité *production + soutiré == consommation + injecté*, vérifiée exactement sur le
banc.
- **Deux bugs d'unité trouvés en chemin** : les compteurs `total*` sont en **kWh** et le
modèle les nommait `Wh`. L'écran Énergie les divisait donc encore par mille — une
journée de 78 kWh s'y lisait « 78 Wh » — et la simulation accumulait des Wh.
- Vérifié sur appareil contre `.75` en lecture seule. Le passage a trouvé quatre libellés
tronqués qu'aucun des 138 tests unitaires ne pouvait voir.
- [ ] ❓ **« ECS d'abord, batterie ensuite » — à porter au moteur.** Demandé le 2026-08-27,
et **pas exprimable aujourd'hui**. `batteryLevelConsideration` fait l'inverse : il annule
le budget *sous* le seuil, donc la batterie passe avant. La batterie n'est pas dans le
waterfall — aucune entrée `GetLoadConfig`, et `enableCharging` / `chargingRate` /
`enableDischarging` n'apparaissent nulle part dans le plugin ETM. Le levier existe
pourtant : la classe **SunSpec Storage** de `.75` déclare ces cinq actions. Question :
*le stockage est-il une charge arbitrable, ou reste-t-il hors du budget ?* Une règle
`Rules.*` serait un contournement — deux commandeurs sur la batterie, exactement le
défaut que 3g-2 vient de supprimer sur les bornes.
- [ ] **Zones de climatisation — cadrage écrit, banc à préparer.** Voir
`docs/CADRAGE_zones_clim.md`. Le blocage n'est pas « aucune zone déclarée » : sur les 58
classes de `.75`, **aucune ne porte `thermostat`, `closablesensor`, `temperaturesensor`
ni `notifications`** — aucun plugin installé ne sait en créer une. Deux paquets du dépôt
apt du banc suffisent (`nymea-plugin-generic-heatingcooling`,
`nymea-plugin-generic-sensors`). Six décisions à prendre avant de coder.
- [ ] **`EV_GRID_START` / `PHASE_LIMIT` — éprouvés par test seulement.** Les clés ARB sont
prêtes, les motifs n'ont **jamais été vus sur machine**. Signaler la première occurrence
réelle. Ne rien bâtir autour d'ici là.
- [ ] ⏳ **`sessionEnergy` est CONSTRUIT par le plugin — mais la trace n'existe pas, et
n'existera pas sur cette borne.** Le plugin V2C `1.15.2+etm13` (déployé le 2026-08-28 à
midi) accumule les incréments de `chargeEnergy`, détecte les remises à zéro, et ne repart
qu'à l'ouverture d'une session : ce n'est **pas** un alias du registre `0x0BC4`.
**La campagne V2C est arrêtée le 2026-08-28** — la Trydan non-Pro est en WiFi ESP32 sans
repli filaire, lien inexploitable à deux mètres du point d'accès (70 % de scrutations
avortées, 12 bascules en injoignable en vingt minutes) ; le modèle sort du catalogue ETM,
seule la **Trydan Pro** (port Ethernet) y reste. Le cas qui lève la réserve —
`chargeEnergy` à zéro, `sessionEnergy` qui continue — se verra sur elle. **Aucun
avancement affiché d'ici là** (LM-1009 §C1), et la note reste en place telle quelle.
- [x] ~~Le piège des accolades~~ — **écrit comme MOTIF le 2026-08-28**, pas comme correctif
local : `docs/UI_data_contract.md` §9, « Deux réponses de la même box, deux conventions ».
Deux occurrences en une semaine (clés de log sans préfixe `currentPower`, identifiants de
borne avec/sans accolades), la règle, la façon de le chercher, et un renvoi depuis les
deux sites de code plus `tools/rpc/README.md` — là où quelqu'un croise deux réponses.
- [ ] **Lot TOTALISATEUR — `smartmeterconsumer` sur la Trydan, et ce qu'il débloque.**
Ce n'est pas qu'une déclaration d'interface : un cumul d'énergie ouvre le **logging
d'énergie natif** de nymea sur la borne, et avec lui les **rapports mensuels**. C'est ce
qui manque aussi à l'historique de recharge (entrée plus bas). Détail de l'écart : Relevé du 2026-08-28 : la classe `wallbox` (borne simulée) déclare
`connectable, evcharger, power, smartmeter, smartmeterconsumer` ; la classe `trydan` n'a
que `connectable, evcharger, power`. La V2C n'est donc **pas proposée comme compteur** —
l'app a élargi son sélecteur aux bornes publiant `currentPower` (correctif du jour), mais
c'est un contournement de l'écart, pas sa disparition.
**Le manque est de fond, et c'est le même que celui qui a bloqué LM-1009** : l'interface
`smartmeter` attend un **cumul** d'énergie ; la wallbox simulée publie
`totalEnergyConsumed`, la Trydan n'a que `sessionEnergy` / `chargeEnergy` — une session,
pas un total. À chercher dans la carte Modbus V2C, ou à **construire** comme
`sessionEnergy` l'a été (`1.15.2+etm13`). C'est un lot plugin, pas un correctif d'app.
- [ ] ⏳ **Journal de sessions de recharge — côté moteur, rien à câbler ici.** Attendu
comme la source propre d'un historique par session, là où l'app ne peut aujourd'hui
montrer qu'un instantané. Ne rien bâtir avant qu'il existe : un journal reconstruit côté
app divergerait du sien à la première interruption.
- [ ] **Historique des états d'une borne — `sessionEnergy` n'est pas enregistré.**
`suggestLogging` est écrasé par les préférences persistées de `things.conf` sur un Thing
déjà appairé, et la V2C n'y a que `connected`, `charging`, `power`. La valeur courante est
juste — « la nuit relue à 7 h » fonctionne — mais **il n'y aura pas de courbe**. Le
réglage est côté app : choisir les états enregistrés d'un Thing. À décider si un
historique de recharge est voulu ; sans lui, l'écran ne peut montrer qu'un instantané.
- [x] ~~⏳ `sessionEnergy` existe — mais rien ne prouve qu'il survive à une interruption~~ —
**répondu le 2026-08-28** par l'origine du champ (entrée ci-dessus). Le relevé qui a posé
la question est conservé : `Integrations.GetThings` sur `.75`, cet après-midi —
la V2C publie `sessionEnergy` **et** `chargeEnergy`, tous deux à **4,229 kWh** — égaux,
ce qui est exactement ce qu'on attend d'une session sans interruption. Le test qui
tranche est une **remise à zéro observée de `chargeEnergy` avec un `sessionEnergy` qui
continue**. Tant qu'elle n'est pas vue, LM-1009 §C1 reste entière : **pas d'avancement
affiché**. À vérifier sur la campagne de mesures en cours.
- Deux attentes **levées** au même relevé : le renommage `currentL1/L2/L3` (unité
Ampère) **est déployé** — plus de `powerL1/L2/L3` — et `phaseCount` vaut **3** sur la
Trydan, le réglage ayant été posé. `PREPA_BANC_20260828.md` (dépôt plugin) est corrigé
en conséquence : il décrivait l'état de la veille au soir.
- [x] ~~**`EVChargingCard` n'est montée par AUCUN écran**~~ — **tranché le 2026-08-28 :
elle part.** La monter aurait publié de la fiction. Trois des grandeurs qu'elle affiche
ne sont **jamais lues sur la box** : `chargingMode`, `chargingPower` (3,6 kW) et
`solarSourcePercent` (82 %) sont des valeurs par défaut de `EnergyData`, et le mode n'est
écrit qu'en optimiste après une commande — jamais relu par `GetChargingInfos`. Sa feuille
« Paramètres de charge » proposait en plus un **courant minimum**, qui n'existe dans aucun
contrat, et son bouton « Enregistrer » ne faisait que `Navigator.pop` : rien n'était écrit.
- [x] ~~**La tuile EV des favoris affiche des valeurs par défaut comme des mesures**~~ —
**fait le 2026-08-28 : rendue muette, pas retirée.** La retirer aurait laissé un trou
dans une liste que le client a composée lui-même. `EvFavoriteTile`
(`widgets/ev_favorite_tile.dart`) lit la borne — `pluggedIn`, `charging`, `currentPower`
du Thing, mode par `GetChargingInfos` + `ChargingInfoChanged` — et **dit** ce qui n'est
pas publié plutôt que d'afficher zéro. L'écriture du mode **restitue le refus** et les
cibles tactiles passent de 7–10 px à 44 px. Les trois champs fictifs de `EnergyData`
(`chargingMode`, `chargingPower`, `solarSourcePercent`) sont **supprimés du modèle** :
la fiction n'a plus de porte d'entrée.
- [ ] **La vue « consommation » du Sankey reste une ESTIMATION — elle le dit désormais.**
`features/dashboard/widgets/energy_flow_card.dart` : *PAC = 38 % de la maison*,
*Eau chaude = 18 %*, *Autres = 22 %* sont trois ratios en dur posés à la maquette. Depuis
le 2026-08-28 les **trois** portent le cercle pointillé (`estimated`) — un seul l'avait —
et une légende sous le schéma nomme les branches concernées : « seule la voiture est
mesurée à la borne ». La voiture, elle, lit la mesure réelle (`currentPower`, « non lu »
sans borne) au lieu des 3,6 kW en dur qu'elle affichait en permanence.
**Reste le vrai lot** : brancher les trois branches sur `measurement` **par charge**
(`GetLoadTelemetry`), pas sur des ratios. Relevé du 2026-08-28 : chauffe-eau 1 500 W et
pac-terrain 800 W, tous deux en `source: "meter"`. Deux règles à respecter dans la
refonte, elles ne sont pas décoratives : une charge en `none` **n'a pas de branche** — pas
une branche à zéro — et une charge **absente de `loads[]`** ne dit rien, donc ne se dessine
pas non plus. C'est une refonte de l'écran d'accueil, pas un correctif.
Ce qu'une carte de recharge dédiée pourrait dire, si elle revient un jour :
- le **mode courant**, lu par `NymeaEnergy.GetChargingInfos` et tenu à jour par
`ChargingInfoChanged` — l'écriture existe déjà et est vérifiée
(`nymea_service.setChargingInfo`, lecture → patch → réécriture complète) ;
- la **puissance mesurée** de la borne, prise sur le Thing (`currentPower`) ;
- l'**échéance** (`targetPercentage` + `endDateTime`) — mais uniquement comme **intention
saisie**, jamais comme un avancement : LM-1009 §C1. Pas de SOC véhicule (il n'existe
pas), pas de `chargeEnergy` (remis à zéro à chaque interruption), pas de courant
minimum (aucun champ), pas de `phaseCount` (la V2C ne le publie pas).
- [ ] **Le passage sur appareil doit devenir une habitude, pas un rattrapage.** Il a glissé
deux fois, et au premier passage il a trouvé deux cibles tactiles hors norme (26 px, 18 px)
qu'aucun des 129 tests unitaires ne pouvait voir. `integration_test/ui_3g2_on_device_test.dart`
est le harnais. Rappel MIUI : la confirmation d'installation est redemandée à **chaque**
fois — faire un `adb install -r -t` préalable, puis lancer le test.
- [ ] 🔴 **Journal de recharges — à porter au MOTEUR, lot à part.** Une ligne par recharge :
début, fin, énergie livrée ; plus tard le véhicule et la part surplus / réseau, que `funding`
distingue déjà. **La raison est un manque, pas un confort : il n'existe aucun rapport, nulle
part** — ni par session, ni par charge, ni par mois. Le bilan journalier s'arrête à la
journée.
- **Pourquoi le moteur et pas l'app** : c'est lui qui voit le début et la fin d'une session,
et qui connaît déjà la ventilation surplus / réseau. L'app ne peut que la reconstituer.
- **Pourquoi pas en intégrant une courbe** : ça reviendrait à ré-estimer une somme que le
plugin connaît **exactement** — `sessionEnergy` de la V2C est accumulé au watt-heure près
(validé le 2026-08-28 : 9,04023 + 0,182573 = 9,222803, somme exacte, pas un ordre de
grandeur). Intégrer une puissance pour retrouver ce chiffre serait perdre de la précision
qu'on a déjà.
- **Ne remplace pas l'enregistrement de `currentPower`**, qui reste nécessaire : il couvre la
consommation sur une période indépendamment des sessions, donc y compris une recharge
lancée à la main sur la borne, hors pilotage — cas qu'un journal de sessions raterait.
## Charges pilotables — ce qui attend la box (2026-08-26)
- [x] ~~**`kCodesReserveBatterie`**~~ — **fait le 2026-08-27.** Le motif est publié :
`BATTERY_RESERVE`, params `socPercent` / `reservePercent` / `withheldW`. Les clés que le
fichier cherchait (`batteryLevel`, `threshold`) n'avaient **jamais** existé côté plugin —
le motif sortait donc en repli anglais sur le seul cas qu'aucune mesure ne révèle.
- [ ] **`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 — à ne pas entreprendre~~ — **décision revue le 2026-08-27** : le lot est
cadré (`docs/CADRAGE_zones_clim.md`), voir l'entrée en tête de ce fichier. `ac_screen.dart`
reste de la donnée fictive — quatre pièces, huit températures crédibles affichées comme
des mesures.
## En cours / priorité haute
- [ ] **TariffProvider — connexion RPC réelle**
- `load()` appelle encore un `Future.delayed` fictif
- Brancher sur nymea JSON-RPC pour récupérer les prix live (ENTSO-E, Tibber, etc.)
- [ ] **EnergySetupProvider — persistance**
- Les assignations de rôles (EV, PAC, batterie…) ne survivent pas au redémarrage
- Sérialiser en SharedPreferences ou JSON local
- [ ] **SchedulerProvider — exécution réelle**
- `forceRecalc()` ne fait rien pour l'instant
- Brancher sur l'API nymea Energy Scheduler (si disponible)
## Screens à implémenter
- [ ] **DeveloperScreen** — logs, introspection JSON-RPC, test ping nymea
- [ ] **AboutScreen** — version app, version nymea, licences
- [ ] **SystemSettingsScreen** — adresse serveur nymea, port, transport (TCP/WS)
## Amélioration UX
- [ ] **Timeline** — remplacer la simulation bell-curve par les vraies créneaux du scheduler nymea
- [ ] **RoleConfigFlow Step 3** — test de connexion sur RPC réel au lieu du délai simulé
- [ ] **InstallerModeProvider** — persister le hash PIN dans SharedPreferences (actuellement en mémoire)
- [ ] **AppSettingsProvider** — persister les préférences (thème, densité, écrans actifs) dans SharedPreferences
## Intégration nymea live
- [ ] Mapping `EmsRole → nymea thingClass` pour les installations réelles
- [ ] Vérifier les interfaces nymea sur serveur réel (comparer avec `docs/nymea_integrations.md`)
- [ ] `Energy.GetSchedule` / `Energy.SetSchedule` si l'API le supporte
## Technique / dette
- [ ] Ajouter des tests unitaires pour `TariffProvider` et `EnergySetupProvider`
- [ ] Gérer le cas où `NymeaService.thingClasses` est vide en mode live (classes non chargées)
- [ ] Revoir `_Step1ThingList` : passer en `context.watch` pour réactivité si les things changent