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); + }); +}