Sondé avant de câbler (`+etm56`) : `periodStartedAt` = 1788562800 = 00:00 Londres = 01:00 Paris, `measuredSince` = 07:04 Londres, `timeZone` = Europe/London. Sept heures d'écart entre les deux bornes. - `EnergyRatiosSnapshot` — les deux instants et le fuseau. `baselineRepriseEnJournee` dit quand les ratios de la box ne parlent que depuis un redémarrage : c'est le silence que `measuredSince` existe pour fermer, et quand les deux coïncident — le cas normal — il n'y a rien à dire de plus ; - `fetchEnergyRatios()`, et `fetchDailyBalance()` qui prend désormais la frontière publiée. Repli sur minuit local pour une box antérieure, écrit comme tel. Et un effet de bord attrapé avant qu'il ne parte : `startsLate` comparait à minuit du TÉLÉPHONE. Avec la frontière de la box, il aurait crié « partiel » tous les jours sur un bilan complet — un avertissement permanent cesse d'être lu. Il se compare maintenant à `periodStart`, porté par le bilan, et un test fige le piège. Les taux journaliers de `DailyBalance` sont donc corrigés du même geste : ils se calculent déjà par différence de compteurs sur la fenêtre, c'était la fenêtre qui était fausse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
122 lines
5.7 KiB
Dart
122 lines
5.7 KiB
Dart
/// **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;
|
||
|
||
/// **Le début de la période demandé** — la frontière de journée de l'installation
|
||
/// (`periodStartedAt`), ou minuit local en repli sur une box antérieure à `+etm56`.
|
||
///
|
||
/// Distinct de [from], qui est le premier échantillon **réellement** trouvé. C'est leur
|
||
/// écart, et lui seul, qui dit qu'un bilan est partiel.
|
||
final DateTime periodStart;
|
||
|
||
/// 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.periodStart,
|
||
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é.
|
||
/// ⚠️ **Se compare à [periodStart], jamais à minuit du téléphone.** La journée de la box
|
||
/// commence à SA minuit : sur `.75` en `Europe/London` vue d'un appareil français, c'est
|
||
/// 01:00 local. Comparer à minuit local ferait crier « partiel » **tous les jours**, sur un
|
||
/// bilan parfaitement complet — un avertissement permanent cesse d'être lu.
|
||
bool get startsLate =>
|
||
from.difference(periodStart) > const Duration(minutes: 20);
|
||
}
|