Livré AVANT le déploiement, parce que ce refus-là est muet : le motif ne franchit pas la frontière RPC, et le symptôme au doigt aurait été « mon glissement ne s'enregistre pas », sans cause visible à l'écran. Même famille que le §9 du contrat de données. Trois points vérifiés, dans l'ordre demandé : 1. LE RÉAPLATISSAGE REND TOUJOURS DES RANGS DISTINCTS. flattenToPayload numérote 1..n sur l'ordre courant, donc par construction — mais « par construction » ne se vérifie pas tout seul : le test le refait après CHAQUE permutation de domaines et après un déplacement intra-domaine, en partant de la config du banc qui porte déjà deux charges au rang 1. Les états intermédiaires du glissement ne partent pas : seul `save()` écrit. 2. RIEN NE PART SI LE BROUILLON N'EST PAS COHÉRENT. validateSet() — le miroir client de LoadConfigStore::validateSet() — apprend le contrôle des rangs et rend un conflit `tiedPriority` qui NOMME les deux charges et le rang partagé. L'écran distingue désormais les trois refus (thing revendiqué deux fois, identifiant en double, rangs à égalité) là où il annonçait « deux charges revendiquent le même appareil » pour tout. 3. ⚠️ LE CHEMIN DE SUPPRESSION ÉTAIT LE PLUS EXPOSÉ, et rien ne l'aurait montré. Il réécrit les survivantes VERBATIM — c'est sa règle, et elle est bonne — donc il transporte les rangs tels que la box les porte. Or c'est le plugin lui-même qui a créé des égalités avant +etm27 (borne détectée au rang 1 d'une installation déjà réglée). Une suppression légitime aurait donc été refusée en bloc, sans motif. Elle refuse maintenant côté app, avec la raison — plutôt que de renumérer en douce : un geste de nettoyage qui redistribue les rangs devient un geste d'arbitrage. Deux tests de load_priority_test construisaient leurs entrées sans `priority` : les deux tombaient donc à égalité, et le nouveau contrôle masquait la règle qu'ils éprouvaient. Rangs distincts posés, avec la raison en commentaire. Les deux autres nouveautés d'+etm27 sont au TODO, non codées : `o:rankOrigin` (badge « rang par défaut, pas un choix », avec la règle absent = rien d'affirmé) et l'entrée en queue des bornes auto-provisionnées — qui devrait faire taire d'elle-même la note d'ordre entrelacé. 213 tests (+8), flutter analyze inchangé à 27 remarques. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
243 lines
17 KiB
Markdown
243 lines
17 KiB
Markdown
# 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 ».
|
||
- [ ] **`o:rankOrigin` — badge « rang par défaut, pas un choix ».** `"auto"` écrit par le
|
||
moteur quand il crée une entrée d'office, **effacé dès qu'un rang différent arrive**
|
||
pour cet id. **Absent = rien d'affirmé**, jamais « choisi par un humain » — même règle
|
||
que `measurement.source`. `"user"` est ce qu'un client écrit pour dire « j'ai vu ce
|
||
défaut et il me va » sans déplacer la charge ; l'app ne peut **jamais** écrire `"auto"`,
|
||
le moteur le refuse. C'est l'équivalent, pour une charge arrivée seule, de la note de
|
||
rangs ex æquo — et il faudra décider si le badge remplace cette note ou s'y ajoute.
|
||
- [ ] **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
|