etm-powersync-app/lib/services/energy_ratios.dart
Patrick Schurig 3821bf3fdb refactor(energy): retire le calcul de ratios app-side, isole l'interim derrière EnergyRatiosInterim
- supprime selfRate/autoRate de _parsePowerBalance, le getter autoconsommationW, les .abs() interim
- ratios dérivés par Δ-de-cumuls (§2.1/§8.1) dans lib/services/energy_ratios.dart
- reseed jour-roulant & Δ non-monotone ; gardes : dén≤0→null, pas de NaN, clamp [0,100]
- selfConsumptionPower net-signé sans .abs() (§2.2) ; signes nymea bruts préservés (§7.1)
- seam swappable vers un state plugin en Phase 2 (une seule fonction)

Interim (Phase 1). Gestion du signe d'affichage renvoyée à la Phase 4 (§1.1).
Réf. UI_DATA_CONTRACT.md rev.5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:51:50 +02:00

100 lines
3.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// 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;
}
}