class EnergyData { final double pvPower; // Watts - production solaire final double homePower; // Watts - consommation maison final double batteryPower; // Watts - puissance batterie (+ charge, - décharge) /// **Réseau, signé : `+` SOUTIRÉ, `−` INJECTÉ.** /// /// ⚠️ Le commentaire précédent disait l'inverse — « + injection, − soutirage » — et la /// carte de flux l'a cru : le 2026-09-06, une installation qui **exportait 2,5 kW** /// affichait « consommation réseau », flèche à l'envers. Le sens de l'animation était /// inversé, et rien dans les tests ne pouvait le voir. /// /// La convention vient de la box et se vérifie à la sonde : `currentPowerAcquisition` /// vaut **−2 511 W** pendant que la production (5,76 kW) dépasse la consommation /// (3,25 kW) de 2,5 kW. Elle est transportée telle quelle (§7.1, signes bruts). /// /// Ne pas relire ce signe à la main : passer par [gridImportW] / [gridExportW]. Le /// dépôt portait DEUX conventions opposées dans deux widgets voisins, et celle qui était /// à l'écran était la fausse. final double gridPower; final double batterySOC; // % - état de charge batterie final double temperature; // °C final String weatherIcon; // Journée final double dayProductionWh; final double daySelfConsumptionWh; final double dayGridInjectionWh; /// Compteurs CUMULÉS de la box (kWh), tels qu'elle les publie — pas des totaux du jour. /// /// `Energy.GetPowerBalance` rend `totalReturn` (exporté) et `totalAcquisition` /// (importé). Ils sont **cumulés depuis toujours**, pas remis à zéro chaque nuit : les /// afficher sous un libellé « aujourd'hui » serait faux. /// /// `NaN` signifie **valeur non exploitable**, et c'est un état à part entière : le banc /// `.75` publie des compteurs manifestement corrompus (7,5e+33 kWh importés) alors que /// `totalProduction` reste sain. L'app ne les corrige pas — elle refuse de les /// présenter comme une mesure (cf. [gridExportKwhOrNull]). final double gridExportKwh; final double gridImportKwh; final double selfConsumptionRate; // % final double autonomyRate; // % final double dayGains; // € // ── Borne de recharge : PLUS RIEN ICI ────────────────────────────────────── // // `chargingMode`, `chargingPower` (3,6 kW) et `solarSourcePercent` (82 %) ont vécu ici // avec des valeurs par défaut que rien ne remplaçait : `_updateEnergyData` ne les // touchait pas, et le mode n'avançait qu'en optimiste après une commande envoyée depuis // l'app. Trois écrans les affichaient comme des mesures — la tuile des favoris, le // Sankey du tableau de bord, et la carte véhicule (supprimée le 2026-08-28). // // Ce que la box publie vraiment sur une borne vit désormais dans `EvChargerLive` // (`models/ev_charger_live.dart`), champ par champ, nullable : `null` veut dire « non // publié », jamais zéro. final bool gridLimitOk; const EnergyData({ this.pvPower = 0, this.homePower = 0, this.batteryPower = 0, this.gridPower = 0, this.batterySOC = 0, this.temperature = 0, this.weatherIcon = 'cloud_snow', this.dayProductionWh = 0, this.daySelfConsumptionWh = 0, this.dayGridInjectionWh = 0, this.gridExportKwh = 0, this.gridImportKwh = 0, this.selfConsumptionRate = 0, this.autonomyRate = 0, this.dayGains = 0, this.gridLimitOk = true, }); /// Puissance **soutirée** au réseau, ou 0. Voir [gridPower] pour la convention. double get gridImportW => gridPower > 0 ? gridPower : 0; /// Puissance **injectée** vers le réseau, ou 0. double get gridExportW => gridPower < 0 ? -gridPower : 0; EnergyData copyWith({ double? pvPower, double? homePower, double? batteryPower, double? gridPower, double? batterySOC, double? temperature, String? weatherIcon, double? dayProductionWh, double? daySelfConsumptionWh, double? dayGridInjectionWh, double? gridExportKwh, double? gridImportKwh, double? selfConsumptionRate, double? autonomyRate, double? dayGains, bool? gridLimitOk, }) { return EnergyData( pvPower: pvPower ?? this.pvPower, homePower: homePower ?? this.homePower, batteryPower: batteryPower ?? this.batteryPower, gridPower: gridPower ?? this.gridPower, batterySOC: batterySOC ?? this.batterySOC, temperature: temperature ?? this.temperature, weatherIcon: weatherIcon ?? this.weatherIcon, dayProductionWh: dayProductionWh ?? this.dayProductionWh, daySelfConsumptionWh: daySelfConsumptionWh ?? this.daySelfConsumptionWh, dayGridInjectionWh: dayGridInjectionWh ?? this.dayGridInjectionWh, gridExportKwh: gridExportKwh ?? this.gridExportKwh, gridImportKwh: gridImportKwh ?? this.gridImportKwh, selfConsumptionRate: selfConsumptionRate ?? this.selfConsumptionRate, autonomyRate: autonomyRate ?? this.autonomyRate, dayGains: dayGains ?? this.dayGains, gridLimitOk: gridLimitOk ?? this.gridLimitOk, ); } /// Plafond de plausibilité pour un compteur d'énergie domestique cumulé, en kWh. /// /// 10 GWh, soit environ **trois mille ans** de consommation d'une maison. Aucune /// installation résidentielle ne l'atteindra ; tout ce qui le dépasse est un compteur /// cassé, pas un record. Le seuil est volontairement absurde-haut : il ne s'agit pas de /// juger une valeur, seulement d'attraper les corrompues. static const double kPlausibleMaxKwh = 1e7; /// Vrai si [v] peut être présenté comme une mesure. static bool isPlausibleKwh(double v) => v.isFinite && v >= 0 && v <= kPlausibleMaxKwh; /// Export cumulé, ou `null` si la box publie une valeur inexploitable. double? get gridExportKwhOrNull => isPlausibleKwh(gridExportKwh) ? gridExportKwh : null; /// Import cumulé, ou `null` si la box publie une valeur inexploitable. double? get gridImportKwhOrNull => isPlausibleKwh(gridImportKwh) ? gridImportKwh : null; } /// Les trois modes de recharge offerts par l'écran. /// /// 🔴 **`manuel` s'appelait `boost`, et le nom était FAUX.** Il pose /// `ChargingModeNormal`, qui **sort la borne de l'arbitrage** et restitue ses valeurs /// manuelles — `manualChargingEnabled`, `manualMaxChargingCurrent`, /// `manualDesiredPhaseCount`. Sur `.75` ces valeurs valaient `false / 6 A / 3 phases` : /// mesuré le 2026-09-08 à 11:09:51, le bouton « Boost » a donc **arrêté la charge** et armé /// un verrou de 300 s. /// /// Ce n'était pas une régression : c'est ce que `Normal` a toujours fait. Le nom promettait /// le contraire du comportement, et c'est le nom qui a menti pendant des mois — d'où le /// renommage jusque dans le code, et pas seulement sur l'étiquette. /// /// Le vrai boost existe depuis `+etm61`, et **ce n'est pas un mode** : c'est une /// dérogation (`NymeaEnergy.SetLoadDerogation`) qui se superpose au mode courant, sert la /// charge à son maximum en tête de passe éco, et s'efface d'elle-même. Elle aura son propre /// bouton d'action, avec une durée obligatoire — pas une entrée de ce sélecteur. /// /// L'ordre compte : `manuel` en tête, parce que `ChargingModeNormal` est aussi le mode /// **d'office** d'une borne détectée (cf. `EvChargerLive.isUnmanaged`). enum ChargingMode { manuel, pv, minPv } class HistoryPoint { final DateTime time; final double pvWh; final double homeWh; final double gridWh; final double batteryWh; const HistoryPoint({ required this.time, this.pvWh = 0, this.homeWh = 0, this.gridWh = 0, this.batteryWh = 0, }); } // ── Entrée Energy.GetPowerBalanceLogs ───────────────────────────────────────── class PowerBalanceEntry { final DateTime timestamp; final double productionW; // puissance PV (valeur absolue, W) final double consumptionW; // consommation maison (W) final double acquisitionW; // import réseau (W, positif) final double storageW; // batterie : +charge / -décharge (W) /// Compteurs cumulés depuis l'origine, en **kWh** — et non en Wh comme le nom de ces /// champs l'a longtemps dit. /// /// Vérifié sur `.75` le 2026-08-27 par recoupement, parce qu'aucune documentation ne le /// tranchait : sur une fenêtre de 21,75 h, l'intégration trapézoïdale des puissances /// instantanées (qui sont bien en **W**) rend **77 697 Wh**, quand la différence de /// `totalProduction` sur la même fenêtre rend **78,31**. Le rapport est mille. /// /// Le mauvais nom n'était pas cosmétique : l'écran Énergie affichait « 78 Wh » pour une /// journée de 78 kWh, et personne ne pouvait le voir sans connaître l'ordre de grandeur /// attendu. final double totalProductionKwh; final double totalConsumptionKwh; /// Injecté vers le réseau, cumulé (kWh). final double totalReturnKwh; /// Soutiré au réseau, cumulé (kWh). final double totalAcquisitionKwh; /// **Les quatre cumuls étaient-ils PRÉSENTS dans la charge utile ?** /// /// ⚠️ Sans ce drapeau, un champ absent devient `0.0` au décodage — et un bilan bâti sur /// deux bornes à zéro s'affiche « 0,0 kWh · Vers réseau » comme s'il était mesuré. C'est /// le zéro forgé, du côté de l'écran cette fois : « rien échangé » et « la box ne le /// publie pas » sont deux états, et le second n'a pas le droit de porter le premier. /// /// Le cas est réel : un compteur racine bien en place et des cumuls qui n'arrivent pas — /// une intégration incomplète côté box. La distinction doit tenir **quelle que soit la /// raison** de l'absence, donc sans attendre que cette raison soit corrigée. final bool totalsPresent; const PowerBalanceEntry({ required this.timestamp, this.totalsPresent = false, this.productionW = 0, this.consumptionW = 0, this.acquisitionW = 0, this.storageW = 0, this.totalProductionKwh = 0, this.totalConsumptionKwh = 0, this.totalReturnKwh = 0, this.totalAcquisitionKwh = 0, }); }