From e45ca671513deebcc3e2978e1cd59f0c252dc80c Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Fri, 28 Aug 2026 12:59:58 +0200 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20les=20trois=20branches=20inve?= =?UTF-8?q?nt=C3=A9es=20du=20Sankey=20s'annoncent=20comme=20estim=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marquage, pas refonte. PAC = 38 % de la consommation maison, eau chaude = 18 %, autres = 22 % : trois ratios posés à la maquette, jamais remplacés, et un seul (« Autres ») portait le cercle pointillé. Les trois le portent maintenant, leurs flux passent en pointillé, et une légende sous le schéma nomme ce qui est quoi : « Répartition estimée : PAC, eau chaude et autres usages sont des parts types, pas des mesures. Seule la voiture est mesurée à la borne. » Le trait pointillé seul ne suffisait pas : quatre chiffres en kW se ressemblent tous et se lisent tous comme des mesures. Nommer les branches évite de faire chercher lesquelles. Brancher les trois sur GetLoadTelemetry.measuredW reste le vrai lot — la source existe (chauffe-eau 1500 W, PAC 800 W au relevé du jour), mais c'est une refonte de l'écran d'accueil et le banc est pris par la campagne V2C. Quatre tests montés sur la vue, dont deux qui figent ce que la voiture affiche : « non lu » sans borne — jamais 0,0 kW, qui échangerait une fiction contre une autre — et la mesure quand il y en a une. Deux pièges de harnais notés dans le fichier : pumpAndSettle est inutilisable (les particules tournent en boucle, disableAnimations ne change que la valeur lue), et toucher un nœud à la première trame le rate en silence — les nœuds entrent en ScaleTransition depuis une échelle nulle. PREPA_BANC_20260828.md corrigé en parallèle (dépôt plugin) : trois de ses affirmations sont tombées avec le déploiement du matin. 158 tests, flutter analyze inchangé à 27 remarques. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f --- TODO.md | 23 ++-- .../dashboard/widgets/energy_flow_card.dart | 36 ++++++- test/energy_flow_cons_test.dart | 101 ++++++++++++++++++ 3 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 test/energy_flow_cons_test.dart diff --git a/TODO.md b/TODO.md index 7fbca20..991baad 100644 --- a/TODO.md +++ b/TODO.md @@ -43,8 +43,9 @@ 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` **existe** sur la - Trydan (valeur 3), contrairement à ce que `docs/PREPA_BANC_20260828.md` avait conclu. + 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 @@ -64,14 +65,16 @@ (`chargingMode`, `chargingPower`, `solarSourcePercent`) sont **supprimés du modèle** : la fiction n'a plus de porte d'entrée. -- [ ] 🔴 **La vue « consommation » du Sankey est INVENTÉE — tableau de bord, écran - d'accueil.** `features/dashboard/widgets/energy_flow_card.dart` : au drill-down - consommation, *PAC = 38 % de la maison*, *Eau chaude = 18 %*, *Autres = 22 %* — trois - ratios en dur, dont un seul (`Autres`) porte le marqueur `estimated`. La voiture, elle, - a été branchée sur la mesure réelle de la borne le 2026-08-28 (`currentPower`, « non - lu » sans borne). Le reste attend une décision : la source existe — `GetLoadTelemetry` - publie `measuredW` **par charge** (relevé du jour : chauffe-eau 1500 W, PAC 800 W) — mais - brancher le Sankey dessus est une refonte de l'écran d'accueil, pas un correctif. +- [ ] **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 `GetLoadTelemetry.measuredW` + (relevé du jour : chauffe-eau 1500 W, PAC 800 W). C'est une refonte de l'écran d'accueil, + pas un correctif, et le banc est pris par la campagne V2C. 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 diff --git a/lib/features/dashboard/widgets/energy_flow_card.dart b/lib/features/dashboard/widgets/energy_flow_card.dart index dbf6fef..6b3208d 100644 --- a/lib/features/dashboard/widgets/energy_flow_card.dart +++ b/lib/features/dashboard/widgets/energy_flow_card.dart @@ -167,6 +167,22 @@ class _EnergyFlowCardState extends State ), ), + // ── Ce qui est mesuré, et ce qui ne l'est pas ─────────────────────── + // + // Le trait pointillé porte l'information, mais il ne se lit pas tout seul : sans + // cette ligne, quatre chiffres en kW se ressemblent tous et se lisent tous comme + // des mesures. Une seule des quatre branches en est une. + if (_view == _DrillView.cons) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + 'Répartition estimée : PAC, eau chaude et autres usages sont des parts ' + 'types, pas des mesures. Seule la voiture est mesurée à la borne.', + style: EtmTokens.sans(size: 11, color: muted), + textAlign: TextAlign.center, + ), + ), + // ── Hint (overview only) ──────────────────────────────────────────── if (_view == _DrillView.overview) Padding( @@ -224,10 +240,20 @@ class _EnergyFlowCardState extends State ]; case _DrillView.cons: + // ⚠️ TROIS BRANCHES SUR QUATRE SONT DES RATIOS EN DUR — 38 %, 18 %, 22 % de la + // consommation maison. Ce ne sont pas des mesures, et rien dans la box ne les + // produit : elles ont été posées à la maquette et jamais remplacées. + // + // Elles sont donc marquées `dashed` / `estimated`, comme « Autres » l'était déjà + // seul, et la légende sous le schéma le dit en toutes lettres. Le marquage n'est + // pas le correctif : la source existe (`GetLoadTelemetry` publie `measuredW` par + // charge), et l'y brancher est un lot à part — cf. `TODO.md`. En attendant, un + // écran qui annonce son estimation ne trompe personne ; un écran qui affiche + // « PAC 1,3 kW » en dur, si. final total = d.homePower.clamp(1.0, double.infinity); return [ - FlowSegment(from: Offset(0.5, hE), to: Offset(0.5, nE), color: EtmTokens.heat, powerW: total * 0.38), - FlowSegment(from: Offset(1-hE, 0.5), to: Offset(1-nE, 0.5), color: EtmTokens.water, powerW: total * 0.18), + FlowSegment(from: Offset(0.5, hE), to: Offset(0.5, nE), color: EtmTokens.heat, powerW: total * 0.38, dashed: true), + FlowSegment(from: Offset(1-hE, 0.5), to: Offset(1-nE, 0.5), color: EtmTokens.water, powerW: total * 0.18, dashed: true), FlowSegment(from: Offset(0.5, 1-hE), to: Offset(0.5, 1-nE), color: EtmTokens.eco, powerW: evW ?? 0), FlowSegment(from: Offset(hE, 0.5), to: Offset(nE, 0.5), color: EtmTokens.gridColor, powerW: total * 0.22, dashed: true), ]; @@ -380,10 +406,12 @@ class _NodeLayerState extends State<_NodeLayer> )), _at(Offset(0.5, 0.12), ScaleTransition(scale: _stagger(1), child: _sat(context, icon: Icons.heat_pump_outlined, color: EtmTokens.heat, - label: 'PAC', value: '${(total * 0.38 / 1000).toStringAsFixed(1)} kW'))), + label: 'PAC', value: '${(total * 0.38 / 1000).toStringAsFixed(1)} kW', + estimated: true))), _at(Offset(0.88, 0.5), ScaleTransition(scale: _stagger(2), child: _sat(context, icon: Icons.water_drop_outlined, color: EtmTokens.water, - label: 'Eau chaude', value: '${(total * 0.18 / 1000).toStringAsFixed(1)} kW'))), + label: 'Eau chaude', value: '${(total * 0.18 / 1000).toStringAsFixed(1)} kW', + estimated: true))), _at(Offset(0.5, 0.88), ScaleTransition(scale: _stagger(3), child: _sat(context, icon: Icons.ev_station_rounded, color: eco(context), label: 'Voiture', diff --git a/test/energy_flow_cons_test.dart b/test/energy_flow_cons_test.dart new file mode 100644 index 0000000..085ff8e --- /dev/null +++ b/test/energy_flow_cons_test.dart @@ -0,0 +1,101 @@ +/// La vue « consommation » du Sankey — ce qui est mesuré, et ce qui ne l'est pas. +/// +/// Trois de ses quatre branches sont des **ratios en dur** posés à la maquette : PAC = 38 % +/// de la consommation maison, eau chaude = 18 %, autres = 22 %. Un seul nœud (« Autres ») +/// portait le cercle pointillé ; les deux autres se lisaient comme des mesures. Et la +/// voiture affichait 3,6 kW en permanence, véhicule débranché — la valeur par défaut de +/// `EnergyData.chargingPower`, supprimée le 2026-08-28. +/// +/// Ces tests figent le marquage. Ils ne valident pas les ratios : les remplacer par +/// `measuredW` de `GetLoadTelemetry` est un lot à part. +library; + +import 'package:etm_powersync_app/features/dashboard/widgets/energy_flow_card.dart'; +import 'package:etm_powersync_app/models/energy_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Largeur contrainte : le schéma est un carré, et en pleine largeur de la fenêtre de test +/// (800 px) il déborde des 600 px de haut — le nœud « Maison » se retrouve hors écran et +/// aucune touche ne l'atteint. +Widget _harness({double? evChargerPowerW}) => MediaQuery( + data: const MediaQueryData(disableAnimations: true), + child: MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 340, + child: EnergyFlowCard( + data: const EnergyData(homePower: 4000, pvPower: 5000), + evChargerPowerW: evChargerPowerW, + ), + ), + ), + ), + ), + ); + +/// `pumpAndSettle` est inutilisable ici : les particules du schéma tournent en boucle +/// (`repeat()`), et `disableAnimations` ne change que la valeur lue, pas le contrôleur. On +/// avance donc d'un nombre fini de trames. +Future _poser(WidgetTester tester) async { + for (var i = 0; i < 6; i++) { + await tester.pump(const Duration(milliseconds: 120)); + } +} + +/// Ouvre le détail de consommation — il n'est atteignable qu'en touchant « Maison ». +/// +/// Le `_poser` AVANT la touche n'est pas décoratif : les nœuds entrent en `ScaleTransition` +/// et, sur la première trame, leur échelle vaut zéro — la touche traverse sans rien +/// atteindre, silencieusement. +Future _ouvrirConso(WidgetTester tester) async { + await _poser(tester); + await tester.tap(find.text('Maison')); + await _poser(tester); +} + +void main() { + testWidgets('la vue d\'ensemble ne parle pas d\'estimation — tout y est mesuré', + (tester) async { + await tester.pumpWidget(_harness()); + await _poser(tester); + expect(find.textContaining('Répartition estimée'), findsNothing); + }); + + testWidgets('la vue consommation ANNONCE que trois branches sont estimées', + (tester) async { + await tester.pumpWidget(_harness(evChargerPowerW: 0)); + await _ouvrirConso(tester); + + final note = find.textContaining('Répartition estimée'); + expect(note, findsOneWidget); + final texte = tester.widget(note).data!; + // Les trois branches sont NOMMÉES : « certaines valeurs sont estimées » laisserait + // chercher lesquelles, et quatre chiffres en kW se ressemblent tous. + expect(texte, contains('PAC')); + expect(texte, contains('eau chaude')); + expect(texte, contains('autres usages')); + expect(texte, contains('Seule la voiture est mesurée')); + }); + + testWidgets('sans borne, la voiture affiche « non lu » — jamais 0,0 kW', + (tester) async { + // 3,6 kW en dur vivait ici, voiture débranchée, sur le premier écran de l'app. + // Le remplacer par un zéro aurait échangé une fiction contre une autre : « rien ne + // charge » n'est pas « je ne mesure rien ». + await tester.pumpWidget(_harness()); + await _ouvrirConso(tester); + + expect(find.text('non lu'), findsOneWidget); + expect(find.textContaining('3.6'), findsNothing); + }); + + testWidgets('avec une borne, elle affiche la puissance mesurée', (tester) async { + await tester.pumpWidget(_harness(evChargerPowerW: 3907)); + await _ouvrirConso(tester); + + expect(find.text('3.9 kW'), findsOneWidget); + expect(find.text('non lu'), findsNothing); + }); +}