/// **Bilan du jour** — l'énergie réellement comptée entre minuit local et maintenant. /// /// ## Pourquoi ce type existe /// /// Le tableau de bord affichait quatre tuiles dont **aucune n'était journalière** : deux /// pourcentages instantanés, et deux compteurs cumulés **depuis l'origine de /// l'installation** — `totalReturn` et `totalAcquisition` ne sont pas remis à zéro chaque /// nuit. Les deux dernières portaient même le mot « aujourd'hui », ce qui était faux. /// /// ## La méthode : une DIFFÉRENCE de compteurs, jamais une intégration /// /// Le bilan se calcule par `total(maintenant) − total(minuit)` sur les quatre compteurs /// cumulés des entrées de log. C'est une **mesure**, prise sur les mêmes compteurs que /// ceux du fournisseur. /// /// L'alternative — intégrer les puissances instantanées — est une **estimation** : elle /// suppose que chaque échantillon représente son intervalle, ce qui est faux dès qu'une /// charge démarre entre deux points. Vérifié sur `.75` le 2026-08-27 : sur 88 échantillons /// de 15 minutes, l'intégration donne **77,70 kWh** là où la différence des compteurs donne /// **78,31** — 0,8 % d'écart, systématiquement à la baisse. /// /// Les deux ne sont donc pas interchangeables, et **il n'y a pas de repli silencieux de /// l'une vers l'autre** : quand la mesure n'est pas exploitable, ce type le dit. /// /// ## L'unité est le kWh, et le contraire a coûté un bug /// /// Les champs `total*` des entrées de log sont en **kWh**, malgré le suffixe `Wh` que /// portait le modèle Dart. Vérifié par recoupement : l'intégration des puissances (en W) /// rend 77 697 Wh quand la différence des compteurs rend 78,31 — le rapport est mille. /// L'écran Énergie affichait donc « 78 Wh » pour une journée de 78 kWh. library; /// Bilan d'une journée. Toutes les énergies en **kWh**. class DailyBalance { /// Début et fin réels de la fenêtre mesurée — **pas** les bornes demandées. /// /// Le premier échantillon peut arriver après minuit (service redémarré, box installée /// dans la journée). Publier la borne demandée laisserait croire à une journée complète. final DateTime from; final DateTime to; /// Nombre d'échantillons dans la fenêtre. Deux au minimum : une différence a besoin de /// deux points. final int sampleCount; final double productionKwh; final double consumptionKwh; /// Soutiré au réseau (`totalAcquisition`). final double importKwh; /// Injecté vers le réseau (`totalReturn`). final double exportKwh; const DailyBalance({ required this.from, required this.to, required this.sampleCount, required this.productionKwh, required this.consumptionKwh, required this.importKwh, required this.exportKwh, }); /// Énergie produite ET consommée sur place — ce qui n'est pas parti au réseau. double get selfConsumptionKwh => productionKwh - exportKwh; /// Part de la production consommée sur place. `null` sans production : à 3 h du matin, /// « 0 % d'autoconsommation » se lirait comme un défaut alors qu'il n'y a rien à /// autoconsommer. double? get selfConsumptionRate { if (productionKwh <= 0) return null; return (selfConsumptionKwh / productionKwh * 100).clamp(0, 100); } /// Part de la consommation couverte sans le réseau. `null` sans consommation. double? get autonomyRate { if (consumptionKwh <= 0) return null; return ((consumptionKwh - importKwh) / consumptionKwh * 100).clamp(0, 100); } /// **L'identité que les compteurs doivent vérifier** : /// `production + import == consommation + export`. /// /// Ce n'est pas décoratif. Elle est le seul contrôle disponible sur des compteurs qu'on /// ne peut pas recouper autrement, et elle a tenu **exactement** sur `.75` (112,244 des /// deux côtés). Fausse, elle désigne un compteur cassé ou une remise à zéro au milieu de /// la fenêtre — et le bilan ne doit alors pas être présenté comme une mesure. /// /// Tolérance : 1 % de la production, plancher 0,1 kWh. Les valeurs voyagent en `Double` /// et les compteurs ne s'incrémentent pas tous au même instant. bool get identityHolds { final ecart = (productionKwh + importKwh) - (consumptionKwh + exportKwh); final tolerance = (productionKwh * 0.01).clamp(0.1, double.infinity); return ecart.abs() <= tolerance; } /// Durée réellement couverte. Duration get span => to.difference(from); /// Vrai si la fenêtre commence **sensiblement après minuit** — le bilan est alors /// partiel, et le dire vaut mieux que de le présenter comme la journée entière. /// /// Seuil à 20 minutes : un échantillonnage au quart d'heure ne place pas forcément un /// point à 00:00 pile, et crier « partiel » pour un décalage d'échantillonnage rendrait /// l'avertissement inutile à force d'être affiché. bool get startsLate { final minuit = DateTime(from.year, from.month, from.day); return from.difference(minuit) > const Duration(minutes: 20); } }