/// Ratios énergétiques affichés (autoconsommation / autonomie), en %. /// `null` = **n/a** (dénominateur nul) — jamais de NaN, jamais d'aberration. class EnergyRatios { final double? autoconsommation; // % dans [0, 100], ou null = n/a final double? autonomie; // % dans [0, 100], ou null = n/a const EnergyRatios({this.autoconsommation, this.autonomie}); static const none = EnergyRatios(); } /// **SEAM UNIQUE — INTERIM (§2.1).** /// /// Tout le calcul app-side des ratios vit **ici et nulle part ailleurs**. /// Cible Phase 2 : remplacer le corps de [compute] par la lecture d'**UN** state /// unique de l'energymanager → le swap se fait en **un seul endroit**. /// /// Méthode interim (§2.1 / §8.1) : dériver par **Δ de cumuls** sur la période /// affichée (journée depuis minuit local), **pas** par intégration du signal /// live → déterministe, pas de drift de polling. /// /// ``` /// autonomie = (Δ totalConsumption − Δ totalAcquisition) / Δ totalConsumption /// autoconsommation = (Δ totalProduction − Δ totalReturn) / Δ totalProduction /// ``` class EnergyRatiosInterim { // Baseline = cumuls au début de la période courante (minuit local / reseed). double? _baseProduction; double? _baseReturn; double? _baseConsumption; double? _baseAcquisition; int? _baseDay; // index de jour local /// Réinitialise la baseline (ex. switch d'installation, §Étape 8). void reset() { _baseProduction = null; _baseReturn = null; _baseConsumption = null; _baseAcquisition = null; _baseDay = null; } /// Calcule les ratios à partir des **cumuls** courants (mêmes unités en entrée, /// elles s'annulent dans le ratio). `now` sert à détecter le changement de jour. EnergyRatios compute({ required double totalProduction, required double totalReturn, required double totalConsumption, required double totalAcquisition, required DateTime now, }) { final day = _localDay(now); // Reseed si : 1er appel, nouveau jour local, OU compteur **non monotone** // (Δ<0 → restart nymead / re-add thing / rollover). Jamais de ratio négatif. final nonMonotone = _baseProduction != null && (totalProduction < _baseProduction! || totalReturn < _baseReturn! || totalConsumption < _baseConsumption! || totalAcquisition < _baseAcquisition!); if (_baseDay != day || _baseProduction == null || nonMonotone) { _baseDay = day; _baseProduction = totalProduction; _baseReturn = totalReturn; _baseConsumption = totalConsumption; _baseAcquisition = totalAcquisition; } final dProduction = totalProduction - _baseProduction!; final dReturn = totalReturn - _baseReturn!; final dConsumption = totalConsumption - _baseConsumption!; final dAcquisition = totalAcquisition - _baseAcquisition!; return EnergyRatios( autoconsommation: _ratio(dProduction - dReturn, dProduction), autonomie: _ratio(dConsumption - dAcquisition, dConsumption), ); } /// Ratio en % borné [0, 100]. Dénominateur ≤ 0 (nuit sans prod, etc.) → `null` /// (n/a) — **jamais de NaN**. static double? _ratio(double numerator, double denominator) { if (denominator <= 0) return null; return (numerator / denominator * 100).clamp(0.0, 100.0); } static int _localDay(DateTime t) => DateTime(t.year, t.month, t.day).millisecondsSinceEpoch ~/ 86400000; /// **INTERIM (§2.2)** — bande d'autoconsommation **instantanée** (W). /// Formule canonique unique : `max(production − max(export, 0), 0)`. /// Réseau net-signé (négatif = export). **Aucun `.abs()`, aucun flip de signe.** static double selfConsumptionPower(double production, double acquisition) { final export = acquisition < 0 ? -acquisition : 0.0; final self = production - export; return self > 0 ? self : 0.0; } }