diff --git a/TODO.md b/TODO.md index e21fa1e..24c0735 100644 --- a/TODO.md +++ b/TODO.md @@ -35,12 +35,22 @@ 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 — il ne reste que la trace.** - 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 de validation en cours produit - le cas qui lève la réserve — `chargeEnergy` à zéro, `sessionEnergy` qui continue. **Aucun - avancement affiché avant cette trace** (LM-1009 §C1). +- [ ] ⏳ **`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. - [ ] **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 diff --git a/docs/UI_data_contract.md b/docs/UI_data_contract.md index f8ad390..418548c 100644 --- a/docs/UI_data_contract.md +++ b/docs/UI_data_contract.md @@ -2,6 +2,10 @@ > Compagnon de `DASHBOARD_SPEC.md`. Tranche la question récurrente > « **qui calcule les valeurs affichées ?** » pour chaque écran. +> Rev. 6 — ajout du **§9** : deux réponses de la même box peuvent employer deux +> conventions sans qu'aucun signal ne le dise, et l'erreur est toujours muette. +> Deux occurrences en une semaine (clés de log sans préfixe `currentPower`, +> identifiants de borne avec/sans accolades). > Rev. 5 — sonde `.75` : réserve décision-2 **levée** (cumuls séparés) ; ajout de > 3 garde-fous : 2 conventions de signe (§7.1), buckets par Δ-de-cumuls (§7.3), > gestion reset compteurs (§2.1/§8.1) ; RP calquées sur le template nymea, Influx @@ -357,6 +361,54 @@ du « Plan de la journée »). --- +## 9. Deux réponses de la même box, deux conventions — et aucun signal + +> **Motif, pas anecdote.** Deux appels JSON-RPC de la même box, portant la même grandeur ou +> le même objet, n'emploient pas forcément la même convention. Aucun schéma, aucune erreur, +> aucun journal ne le dit : **la divergence est toujours muette**, et elle se manifeste par +> un écran vide, un zéro, ou un « non lu » parfaitement crédible. Deux occurrences en une +> semaine, chacune découverte en croisant deux réponses. + +### Les occurrences connues + +| Croisement | Convention A | Convention B | Symptôme si on les mélange | +|---|---|---|---| +| `Energy.GetPowerBalance` × `Energy.GetPowerBalanceLogs` | `currentPowerProduction`, `currentPowerConsumption`, … | `production`, `consumption`, `acquisition`, `storage` — **sans préfixe** | tout tombe sur le défaut `0` : l'historique paraît vide alors que la base porte 7 960 échantillons | +| `NymeaEnergy.GetChargingInfos` × `NymeaEnergy.GetLoadConfig` | `evChargerId` **sans** accolades — `4d1adafd-…` | id de charge **avec** accolades — `{4d1adafd-…}` | aucune correspondance : l'écran affiche « mode non lu » sur une borne parfaitement lue, et la note « borne hors arbitrage » ne sort jamais | +| *(famille voisine, même silence)* `currentPower*` en **W** × `total*` en **kWh** | watts | kilowattheures | facteur 1000 : une journée de 78 kWh s'affiche « 78 Wh » | + +### La règle + +**Ne jamais rapprocher deux réponses RPC sur une clé ou un identifiant sans l'avoir +normalisé — et écrire la normalisation à un seul endroit, nommé.** Dans cette app : +`NymeaService.chargingModeOf()` pour les identifiants, le bloc de clés de +`fetchPowerBalanceLogs()` pour les noms de champs. Une comparaison brute recopiée dans un +troisième écran rouvrira le même trou. + +### Comment on le trouve — et pourquoi il faut le chercher exprès + +Le symptôme n'est **jamais** une exception : c'est une valeur neutre plausible. Un test +unitaire qui construit ses deux côtés avec la même convention passe aussi. Trois gestes, +dans cet ordre : + +1. **Sonder les deux appels côte à côte** et comparer les charges utiles brutes, pas les + modèles Dart — `dart tools/rpc/probe.dart 192.168.1.75 ` pour chacun. +2. **Éprouver le rapprochement lui-même**, sur les deux formes réelles. Les tests d'accolades + (`test/merged_config_screen_test.dart`) sont écrits exactement pour ça, avec les deux + graphies de la même borne. +3. **Se méfier d'un vide qui ne coûte rien à expliquer.** « Aucune donnée sur cette + période », « mode non lu », « 0 W » : quand une lecture rend un neutre sur une + installation qu'on sait vivante, la première hypothèse est le croisement, pas la box. + +### Ce que ça implique pour l'affichage + +Une valeur neutre issue d'un croisement raté est **indiscernable** d'une vraie absence. +C'est la raison de fond de la règle « ne jamais afficher zéro pour une absence » qui +traverse `EvChargerLive`, `DailyBalance` et `LoadMechanism` : dire « non lu » laisse une +chance de découvrir le bug ; afficher `0` l'enterre. + +--- + ## Récap des décisions 1. Héos au centre du flow : **Option A — métaphore « cerveau » assumée** diff --git a/lib/models/ev_charger_live.dart b/lib/models/ev_charger_live.dart index 39bb0f1..e4898ca 100644 --- a/lib/models/ev_charger_live.dart +++ b/lib/models/ev_charger_live.dart @@ -24,8 +24,9 @@ /// portés ici pour autant. Le plugin V2C `1.15.2+etm13` **construit** ce `sessionEnergy` — /// il accumule les incréments de `chargeEnergy`, détecte les remises à zéro, et ne repart /// qu'à l'ouverture d'une session — mais la trace qui le montre à l'œuvre (`chargeEnergy` -/// à zéro, `sessionEnergy` qui continue) sort de la campagne en cours. **Aucun avancement -/// n'est affiché avant elle** : LM-1009 §C1, cf. `LoadMechanism`. +/// à zéro, `sessionEnergy` qui continue) **n'a pas été prise** : la campagne V2C est +/// arrêtée, la Trydan non-Pro étant inexploitable en WiFi. Elle se prendra sur une Trydan +/// Pro. **Aucun avancement n'est affiché avant elle** : LM-1009 §C1, cf. `LoadMechanism`. library; import 'energy_data.dart' show ChargingMode; diff --git a/lib/models/load_telemetry.dart b/lib/models/load_telemetry.dart index 5bee6cb..24e356b 100644 --- a/lib/models/load_telemetry.dart +++ b/lib/models/load_telemetry.dart @@ -199,11 +199,17 @@ class LoadMechanism { /// incréments de `chargeEnergy`, détecte les remises à zéro, et ne repart qu'à /// l'ouverture d'une session. L'égalité observée s'explique donc d'elle-même. /// - /// **La réserve tient quand même, jusqu'à la trace.** La campagne de validation en cours - /// produit exactement le cas qui la lève — `chargeEnergy` à zéro, `sessionEnergy` qui - /// continue. Tant qu'elle n'est pas au dossier, **aucun avancement de charge n'est - /// affiché** : la condition C1 de LM-1009 reste entière. Ce n'est pas de la défiance - /// envers le plugin, c'est la règle qui a évité d'afficher `chargeEnergy` lui-même. + /// **La réserve tient, et elle tiendra un moment : la trace ne viendra pas de cette + /// borne.** La campagne V2C est arrêtée le 2026-08-28. La Trydan **non-Pro** est en WiFi + /// ESP32 sans repli filaire, et le lien s'est révélé 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**, qui a un port Ethernet, y + /// reste. C'est sur elle que le cas se verra. + /// + /// Donc : `chargeEnergy` à zéro **avec** un `sessionEnergy` qui continue **n'a pas été + /// observé**, et **aucun avancement de charge n'est affiché** — la condition C1 de + /// LM-1009 reste entière. Ce n'est pas de la défiance envers le plugin, c'est la règle + /// qui a évité d'afficher `chargeEnergy` lui-même. /// /// > ⚠️ **`sessionEnergy` ne sera pas historisé.** `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 diff --git a/lib/services/nymea_service.dart b/lib/services/nymea_service.dart index 823d6e4..7135c4d 100644 --- a/lib/services/nymea_service.dart +++ b/lib/services/nymea_service.dart @@ -7,8 +7,8 @@ import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import '../models/daily_balance.dart'; -import '../models/energy_data.dart'; -import '../models/ev_charger_live.dart'; // EnergyData, HistoryPoint, PowerBalanceEntry +import '../models/energy_data.dart'; // EnergyData, HistoryPoint, PowerBalanceEntry +import '../models/ev_charger_live.dart'; import '../models/modbus_models.dart'; // ModbusRtuMaster, SerialPort import '../models/nymea_models.dart'; import 'energy_ratios.dart'; // seam interim ratios (§2.1) @@ -945,6 +945,10 @@ class NymeaService extends ChangeNotifier { /// Vérifié sur `.75` le 2026-08-27, avec les clés ci-dessous : **7 960 échantillons sur /// 90 jours** en `SampleRate15Mins`, dont 4 975 avec une production non nulle. Rien ne /// bloque l'historique 90 jours de ce côté. + /// + /// > Ce n'est pas un cas isolé : deux réponses de la même box peuvent employer deux + /// > conventions sans qu'aucun signal ne le dise, et l'erreur est toujours **muette**. + /// > Le motif et ses occurrences connues sont dans `docs/UI_data_contract.md` §9. Future> fetchPowerBalanceLogs({ required DateTime from, required DateTime to, @@ -1375,6 +1379,9 @@ class NymeaService extends ChangeNotifier { /// et c'est la même borne. Comparer les deux tels quels ne rend jamais rien — en /// silence, ce qui est le pire des cas : l'écran conclurait « mode non lu » sur une borne /// parfaitement lue. + /// + /// Deuxième occurrence connue du même motif — cf. `docs/UI_data_contract.md` §9, et la + /// première : les clés de `GetPowerBalanceLogs`, sans le préfixe `currentPower`. String? chargingModeOf(String id) { final nu = id.replaceAll('{', '').replaceAll('}', '').toLowerCase(); for (final e in _chargingInfos.entries) { diff --git a/tools/rpc/README.md b/tools/rpc/README.md index 1378cdd..5e2fc86 100644 --- a/tools/rpc/README.md +++ b/tools/rpc/README.md @@ -27,6 +27,13 @@ dart tools/rpc/