+etm55 vérifié (posé le 04/09 18:21:08). Le taux constant est en ligne : la maquette porte le bon chiffrage — ×4,75 sur une fenêtre quotidienne, facteur ln(T·P/E) + 1 — et les deux interdits du régime `unmeasurable` : ni annoncer que l'achat va s'intensifier, ni lire `atMaximum: false` comme « tout va bien », qui signifie qu'il n'y a aucun étalement, donc rien qui puisse cesser. La troisième nature d'heure murale touche du code livré : `fetchDailyBalance()` et la baseline des ratios prennent minuit du TÉLÉPHONE, alors qu'une journée de reporting suit l'installation. Sur `.75` en Europe/London vue d'un téléphone français, entre minuit et 1 h l'app compte une journée que la box compte encore dans la précédente. L'app ne peut pas le corriger seule : `System.GetTime` publie un nom IANA, et un décalage ne s'en déduit pas sans base de fuseaux — qu'il faudrait embarquer pour recalculer une frontière que la box connaît, et qui resterait fausse les jours de changement d'heure. Demandé au moteur : publier le DÉBUT de la journée de reporting en cours, en epoch, à côté du fuseau. La divergence est écrite aux deux endroits du code en attendant. Noté sans conclure : le banc n'a aucune charge `unmeasurable`, donc la loi est déployée mais pas exerçable de bout en bout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
106 lines
4.3 KiB
Dart
106 lines
4.3 KiB
Dart
/// 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).
|
||
//
|
||
// ⚠️ « local » = fuseau du TÉLÉPHONE, et une journée de reporting devrait suivre
|
||
// l'INSTALLATION (cf. `NymeaService.fetchDailyBalance`). Sur une box en `Europe/London`
|
||
// vue d'un téléphone français, la bascule de jour arrive une heure trop tôt. La cible
|
||
// Phase 2 — lire UN state de l'energymanager — fait disparaître le problème avec le
|
||
// calcul ; d'ici là, la divergence est connue et écrite.
|
||
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;
|
||
}
|
||
}
|