fix(dashboard): « sans production » n'est pas « pas mesurable »
Signalé sur appareil de nuit : tuile Autoconsommation « — · pas mesurable »,
sur une installation dont les quatre compteurs marchent.
D'où vient le nombre : la tuile lit `DailyBalance`, pas `GetEnergyRatios`.
`selfConsumptionRate` y rend `null` dès que `productionKwh <= 0` — production
nulle depuis minuit, donc 0/0.
Le « — » est JUSTE et ne bouge pas. Un « 0 % » affirmerait qu'aucune production
n'a été autoconsommée : une phrase sur une production qui n'existe pas, soit un
zéro forgé de plus. Ce qui était faux, c'est le motif écrit dessous — il
accusait la mesure là où il n'y avait simplement rien à mesurer.
Les deux causes d'un taux absent sont désormais séparées :
- pas de bilan (échantillons, cumuls, compteur en recul) → « pas mesurable » ;
- dénominateur nul → « sans production » / « sans consommation ».
C'est la distinction habituelle du dépôt prise par l'autre bout : d'ordinaire on
empêche une absence de se dire zéro ; ici on l'empêche d'accuser la mesure.
Mesuré sur .75 au même instant : production 0 W, `selfConsumptionRate` absent de
GetEnergyRatios sur trois relevés, `autonomyRate: 0` présent — la box fait la
même distinction, sur la même arithmétique.
Le premier jet du test asserait `find.text('0 %'), findsNothing` : il mordait
sur le 0 % de l'Autonomie, qui est une mesure. Corrigé en le voyant échouer, et
la raison est dans le test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
This commit is contained in:
parent
b843d8936a
commit
92495c0e87
@ -66,6 +66,30 @@ String _instantane(EnergyRatiosSnapshot r) {
|
||||
'autonomie ${pc(r.autonomyRate)}';
|
||||
}
|
||||
|
||||
/// **Pourquoi la tuile n'a pas de nombre** — et les deux raisons ne se ressemblent pas.
|
||||
///
|
||||
/// Un taux absent a deux causes qui ne disent pas du tout la même chose :
|
||||
///
|
||||
/// - **la box n'a pas de bilan** — moins de deux échantillons, cumuls absents, compteur en
|
||||
/// recul. Là, on ne sait pas : « pas mesurable » ;
|
||||
/// - **le dénominateur est nul** — aucune production depuis minuit, donc aucune fraction de
|
||||
/// production à rapporter. Là, on sait parfaitement, et il n'y a **rien à mesurer**.
|
||||
///
|
||||
/// Signalé sur appareil le 2026-09-07, de nuit : la tuile Autoconsommation affichait
|
||||
/// « — · pas mesurable » sur une installation dont tous les compteurs marchaient. Le « — »
|
||||
/// était juste — un `0 %` aurait affirmé qu'aucune production n'a été autoconsommée, une
|
||||
/// phrase sur une production qui n'existe pas — mais **le motif était faux**, et c'est lui
|
||||
/// qui se lit comme une panne.
|
||||
///
|
||||
/// C'est la distinction habituelle de ce dépôt prise par l'autre bout : d'ordinaire on
|
||||
/// empêche une absence de se dire zéro ; ici on empêche une absence d'accuser la mesure.
|
||||
String? _motifAbsence(DailyBalance? b, {required bool production}) {
|
||||
if (b == null) return 'pas mesurable';
|
||||
final denominateur = production ? b.productionKwh : b.consumptionKwh;
|
||||
if (denominateur <= 0) return production ? 'sans production' : 'sans consommation';
|
||||
return 'pas mesurable';
|
||||
}
|
||||
|
||||
class KpiRow extends StatelessWidget {
|
||||
final EnergyData data;
|
||||
|
||||
@ -131,6 +155,7 @@ class KpiRow extends StatelessWidget {
|
||||
child: _RingTile(
|
||||
label: 'Autoconsommation',
|
||||
value: b?.selfConsumptionRate,
|
||||
motifAbsence: _motifAbsence(b, production: true),
|
||||
color: EtmTokens.solarOf(context),
|
||||
),
|
||||
),
|
||||
@ -139,6 +164,7 @@ class KpiRow extends StatelessWidget {
|
||||
child: _RingTile(
|
||||
label: 'Autonomie',
|
||||
value: b?.autonomyRate,
|
||||
motifAbsence: _motifAbsence(b, production: false),
|
||||
color: EtmTokens.ecoOf(context),
|
||||
),
|
||||
),
|
||||
@ -215,11 +241,18 @@ class KpiRow extends StatelessWidget {
|
||||
class _RingTile extends StatefulWidget {
|
||||
final String label;
|
||||
|
||||
/// `null` = **non mesurable**, et surtout pas 0 %. Un anneau vide et un anneau à zéro se
|
||||
/// `null` = **pas de nombre**, et surtout pas 0 %. Un anneau vide et un anneau à zéro se
|
||||
/// ressemblent trop pour qu'on laisse le second dire le premier.
|
||||
final double? value;
|
||||
|
||||
/// Ce qui s'écrit à la place, quand [value] est `null` — voir [_motifAbsence].
|
||||
final String? motifAbsence;
|
||||
final Color color;
|
||||
const _RingTile({required this.label, required this.value, required this.color});
|
||||
const _RingTile(
|
||||
{required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
this.motifAbsence});
|
||||
|
||||
@override
|
||||
State<_RingTile> createState() => _RingTileState();
|
||||
@ -334,7 +367,7 @@ class _RingTileState extends State<_RingTile>
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
widget.value == null
|
||||
? '${widget.label} · pas mesurable'
|
||||
? '${widget.label} · ${widget.motifAbsence ?? 'pas mesurable'}'
|
||||
: widget.label,
|
||||
style: EtmTokens.sans(
|
||||
size: 11,
|
||||
|
||||
@ -39,6 +39,26 @@ DailyBalance _bilan() => DailyBalance(
|
||||
exportKwh: 1.0,
|
||||
);
|
||||
|
||||
/// La NUIT du 2026-09-07 sur `.75` : consommation réelle, production nulle depuis minuit.
|
||||
DailyBalance _bilanDeNuit() => DailyBalance(
|
||||
from: DateTime(2026, 9, 7),
|
||||
to: DateTime(2026, 9, 7, 2, 30),
|
||||
periodStart: DateTime(2026, 9, 7),
|
||||
sampleCount: 10,
|
||||
productionKwh: 0.0,
|
||||
consumptionKwh: 1.4,
|
||||
importKwh: 1.4,
|
||||
exportKwh: 0.0,
|
||||
);
|
||||
|
||||
Widget _harnessBilan(DailyBalance b) => MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: KpiRow(data: const EnergyData(), balance: b),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _harness({EnergyRatiosSnapshot? ratios}) => MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
@ -129,4 +149,58 @@ void main() {
|
||||
await tester.pumpWidget(_harness());
|
||||
expect(find.textContaining('Instantané'), findsNothing);
|
||||
});
|
||||
|
||||
|
||||
group('un taux sans dénominateur — le « — » est juste, le MOTIF ne l\'était pas', () {
|
||||
// Signalé sur appareil le 2026-09-07 : « Autoconsommation — · pas mesurable » de nuit,
|
||||
// sur une installation dont les quatre compteurs marchaient. Deux causes distinctes
|
||||
// portaient la même phrase, et celle qui s'affichait accusait la mesure.
|
||||
testWidgets('sans production, la tuile dit « sans production »', (tester) async {
|
||||
await tester.binding.setSurfaceSize(const Size(1000, 2000));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
await tester.pumpWidget(_harnessBilan(_bilanDeNuit()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Autoconsommation · sans production'), findsOneWidget);
|
||||
// Le contrôle qui MORD : l'ancien motif ne doit plus être là pour cette tuile.
|
||||
expect(find.textContaining('Autoconsommation · pas mesurable'), findsNothing);
|
||||
// Et surtout PAS un zéro sur CETTE tuile : « 0 % » affirmerait qu'aucune production
|
||||
// n'a été autoconsommée, une phrase sur une production qui n'existe pas. Le tiret y
|
||||
// est, et il y est une seule fois — l'autre tuile, elle, a bien un nombre.
|
||||
//
|
||||
// ⚠️ Ne pas écrire `find.text('0 %'), findsNothing` : le 0 % de l'Autonomie est une
|
||||
// MESURE et le contrôle mordrait sur elle. Première rédaction de ce test, corrigée
|
||||
// en la voyant échouer.
|
||||
expect(find.text('—'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('l\'autonomie, elle, a son dénominateur et vaut 0 %', (tester) async {
|
||||
await tester.binding.setSurfaceSize(const Size(1000, 2000));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
await tester.pumpWidget(_harnessBilan(_bilanDeNuit()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tout vient du réseau : 0 % est une MESURE, pas un trou. Les deux tuiles sont
|
||||
// voisines et leurs deux « rien » sont de natures opposées — c'est le cas qui
|
||||
// interdit de traiter les taux ensemble.
|
||||
expect(find.text('0 %'), findsOneWidget);
|
||||
expect(find.textContaining('Autonomie · '), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('sans bilan du tout, le motif reste « pas mesurable »', (tester) async {
|
||||
await tester.binding.setSurfaceSize(const Size(1000, 2000));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: KpiRow(data: const EnergyData(), balance: null),
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Autoconsommation · pas mesurable'), findsOneWidget);
|
||||
expect(find.textContaining('Autonomie · pas mesurable'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user