1) « Vers réseau » — TROIS défauts, dont un seul était visible.
La valeur aberrante (9,10184409403335e+33 kWh) ne vient PAS de l'app : Energy.GetPowerBalance
la publie telle quelle sur .75. Trois des quatre compteurs cumulés sont corrompus
(totalReturn 9,1e+33, totalAcquisition 7,5e+33, totalConsumption -1,6e+33) alors que
totalProduction reste sain, et que les états du Thing compteur sont tous plausibles
(1429 kWh importés, 88 kWh exportés). C'est donc l'accumulation de l'experience-plugin
énergie qui déraille, côté box — signalé, pas corrigé ici.
Les deux autres défauts étaient bien dans l'app :
- « Depuis réseau » lisait l'OPPOSÉ du compteur d'export au lieu de totalAcquisition,
qui a son propre compteur. Dès que l'export était positif, la tuile affichait
0,0 kWh en permanence, quelle que soit l'électricité réellement achetée ;
- les deux tuiles étaient libellées « aujourd'hui » alors que ces compteurs sont
CUMULÉS depuis l'origine, jamais remis à zéro.
Corrigé : EnergyData porte gridExportKwh / gridImportKwh, transportés tels quels — l'app
ne maquille pas une donnée que la box publie fausse. Un plafond de plausibilité
(10 GWh, ~3000 ans de consommation) distingue « mesure » de « compteur cassé » : au-delà
la tuile affiche « — · compteur box invalide ». Imprimer le nombre le présenterait comme
une mesure ; l'effacer laisserait croire à zéro.
2) Thème : 126 sites basculés des constantes claires vers les variantes contextuelles
(EtmTokens.bg → bgOf(context), card → surfaceOf, ink → inkOf, muted → mutedOf,
faint → faintOf, line → lineOf) dans ac_screen, things_screen, energy_screen,
favorites_screen, main_drawer, ev_charging_card, energy_flow_widget. Ces écrans
restaient en palette claire quel que soit le sélecteur.
9 sites RESTENT en constante, et c'est délibéré : ce sont des tables statiques et des
helpers de haut niveau (categoryInfoMap, _options des favoris, _modeColor, peintres du
flux) où la couleur est une identité d'icône et où aucun BuildContext n'est en portée.
Les faire suivre le thème demande de threader le contexte jusqu'à ces tables — un
refactor à part entière, pas un correctif de fin de soirée.
Tests 80/80, analyze 0 erreur.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
246 lines
8.0 KiB
Dart
246 lines
8.0 KiB
Dart
import 'package:flutter/material.dart';
|
||
import '../../../models/energy_data.dart';
|
||
import '../../../theme/etm_tokens.dart';
|
||
import '../painters/ring_painter.dart';
|
||
|
||
class KpiRow extends StatelessWidget {
|
||
final EnergyData data;
|
||
const KpiRow({super.key, required this.data});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final autoconso = data.selfConsumptionRate.clamp(0.0, 100.0);
|
||
final autonomy = data.autonomyRate.clamp(0.0, 100.0);
|
||
// Deux compteurs DISTINCTS de la box, et non un seul et son opposé.
|
||
//
|
||
// Ces deux tuiles lisaient toutes les deux `dayGridInjectionWh` — l'export — la
|
||
// seconde en prenant son opposé. Or l'import a son propre compteur
|
||
// (`totalAcquisition`) : dès que l'export était positif, « Depuis réseau » affichait
|
||
// 0,0 kWh en permanence, quelle que soit l'électricité réellement achetée.
|
||
//
|
||
// `null` = la box publie une valeur inexploitable ; la tuile le dit au lieu
|
||
// d'imprimer un nombre absurde (constaté sur `.75` : 9,1e+33 kWh exportés, alors que
|
||
// `totalProduction` restait sain — les compteurs cumulés de l'experience-plugin
|
||
// énergie sont corrompus, pas ceux du Thing compteur).
|
||
final toGrid = data.gridExportKwhOrNull;
|
||
final fromGrid = data.gridImportKwhOrNull;
|
||
|
||
return Column(
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: _RingTile(
|
||
label: 'Autoconsommation',
|
||
value: autoconso,
|
||
color: EtmTokens.solarOf(context),
|
||
),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: _RingTile(
|
||
label: 'Autonomie',
|
||
value: autonomy,
|
||
color: EtmTokens.ecoOf(context),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: _ValueTile(
|
||
// « aujourd'hui » était faux : `totalReturn` est cumulé depuis
|
||
// l'origine et n'est pas remis à zéro chaque nuit.
|
||
label: 'Vers réseau · cumulé',
|
||
value: toGrid,
|
||
icon: Icons.north_west_rounded,
|
||
color: EtmTokens.ecoOf(context),
|
||
),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: _ValueTile(
|
||
label: 'Depuis réseau · cumulé',
|
||
value: fromGrid,
|
||
icon: Icons.south_east_rounded,
|
||
color: EtmTokens.importColor,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Ring tile — horizontal (ring left, text right) ────────────────────────────
|
||
|
||
class _RingTile extends StatefulWidget {
|
||
final String label;
|
||
final double value;
|
||
final Color color;
|
||
const _RingTile({required this.label, required this.value, required this.color});
|
||
|
||
@override
|
||
State<_RingTile> createState() => _RingTileState();
|
||
}
|
||
|
||
class _RingTileState extends State<_RingTile>
|
||
with SingleTickerProviderStateMixin {
|
||
late final AnimationController _ctrl;
|
||
late Animation<double> _ring;
|
||
late Animation<int> _count;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 1100));
|
||
_ring = Tween<double>(begin: 0, end: widget.value / 100)
|
||
.animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOutCubic));
|
||
_count = IntTween(begin: 0, end: widget.value.round())
|
||
.animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOutCubic));
|
||
_ctrl.forward();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_ctrl.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: EtmTokens.surfaceOf(context),
|
||
borderRadius: BorderRadius.circular(EtmTokens.radiusCard),
|
||
boxShadow: EtmTokens.cardShadowOf(context),
|
||
border: Border.all(color: EtmTokens.lineOf(context), width: 1),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
|
||
child: Row(
|
||
children: [
|
||
// Ring — 50×50, strokeWidth 5 (matches HTML .ring SVG)
|
||
SizedBox(
|
||
width: 50, height: 50,
|
||
child: AnimatedBuilder(
|
||
animation: _ring,
|
||
builder: (context, _) => CustomPaint(
|
||
size: const Size(50, 50),
|
||
painter: RingPainter(
|
||
value: _ring.value,
|
||
color: widget.color,
|
||
strokeWidth: 5,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
AnimatedBuilder(
|
||
animation: _count,
|
||
builder: (context, _) => Text(
|
||
'${_count.value} %',
|
||
style: EtmTokens.mono(
|
||
size: 18,
|
||
weight: FontWeight.w600,
|
||
color: EtmTokens.inkOf(context),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 3),
|
||
Text(
|
||
widget.label,
|
||
style: EtmTokens.sans(
|
||
size: 11,
|
||
color: EtmTokens.mutedOf(context),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Value tile — horizontal (icon left, text right) ───────────────────────────
|
||
|
||
class _ValueTile extends StatelessWidget {
|
||
final String label;
|
||
|
||
/// Valeur en kWh, ou `null` si la box en publie une **inexploitable**.
|
||
///
|
||
/// Le cas n'est pas théorique : `.75` publie 9,1e+33 kWh exportés. Imprimer ce nombre
|
||
/// le présente comme une mesure ; l'effacer laisse croire à zéro. On affiche donc « — »
|
||
/// et on dit d'où vient le trou, ce qui est la seule lecture honnête des trois.
|
||
final double? value;
|
||
final IconData icon;
|
||
final Color color;
|
||
|
||
const _ValueTile({
|
||
required this.label,
|
||
required this.value,
|
||
required this.icon,
|
||
required this.color,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: EtmTokens.surfaceOf(context),
|
||
borderRadius: BorderRadius.circular(EtmTokens.radiusCard),
|
||
boxShadow: EtmTokens.cardShadowOf(context),
|
||
border: Border.all(color: EtmTokens.lineOf(context), width: 1),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 30, height: 30,
|
||
decoration: BoxDecoration(
|
||
color: color.withValues(alpha: 0.12),
|
||
borderRadius: BorderRadius.circular(9),
|
||
),
|
||
child: Icon(icon, color: color, size: 16),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
value == null ? '—' : '${value!.toStringAsFixed(1)} kWh',
|
||
style: EtmTokens.mono(
|
||
size: 16,
|
||
weight: FontWeight.w600,
|
||
color: value == null
|
||
? EtmTokens.mutedOf(context)
|
||
: EtmTokens.inkOf(context),
|
||
),
|
||
),
|
||
const SizedBox(height: 3),
|
||
Text(
|
||
value == null ? '$label · compteur box invalide' : label,
|
||
style: EtmTokens.sans(
|
||
size: 10.5,
|
||
color: EtmTokens.mutedOf(context),
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|