L'entrée 🔴 du TODO. Aucune des quatre tuiles n'était journalière : deux pourcentages instantanés, deux compteurs cumulés depuis l'origine dont deux portaient le mot « aujourd'hui ». Elles montrent désormais la journée, sous un en-tête qui le dit une fois. MÉTHODE : une DIFFÉRENCE de compteurs entre minuit local et maintenant, jamais une intégration des puissances. La première est une mesure, prise sur les mêmes compteurs que ceux du fournisseur ; la seconde est une estimation qui suppose que chaque échantillon représente son intervalle. Mesuré sur .75 : l'intégration rend 77,70 kWh là où la différence rend 78,31 — 0,8 % d'écart, systématiquement à la baisse. Il n'y a donc PAS de repli silencieux de l'une vers l'autre : quand la mesure n'est pas exploitable, le type le dit. DEUX BUGS D'UNITÉ, trouvés en cherchant à savoir ce que valaient les compteurs. Aucune documentation ne tranchait, donc recoupement sur machine : l'intégration des puissances (en W) rend 77 697 Wh quand la différence de totalProduction rend 78,310. Le rapport est mille — LES COMPTEURS SONT EN kWh, et le modèle Dart les nommait Wh. 1. L'écran Énergie divisait ces kWh par mille pour les afficher : une journée de 78 kWh s'y lisait « 78 Wh », sous un axe intitulé « Bilan énergétique (Wh) ». Invisible sans connaître l'ordre de grandeur attendu. 2. La simulation accumulait des Wh : un écran juste en simulation aurait cassé d'un facteur mille contre une vraie box. LE CONTRÔLE QUI VALIDE LE BILAN : production + soutiré == consommation + injecté. C'est le seul recoupement disponible sur des compteurs qu'on ne peut pas vérifier autrement, et il tient EXACTEMENT sur .75 — 112,244 des deux côtés sur 21,75 h. Faux, il désigne un compteur cassé ou une remise à zéro, et la carte le signale au lieu de présenter comme une mesure ce qui ne se recoupe pas. Les trois pièges du TODO n'ont pas été repayés : bornes à minuit LOCAL via GetPowerBalanceLogs · décodage délégué à fetchPowerBalanceLogs, qui porte déjà les bonnes clés sans préfixe currentPower · plausibilité vérifiée sur les DEUX bornes avant de soustraire, parce que deux valeurs absurdes peuvent avoir une différence d'apparence saine. Plus un quatrième trouvé en chemin : un compteur qui RECULE (redémarrage en milieu de fenêtre) donne un bilan négatif, et un max(0, …) le rendrait faux en silence. Rien ne s'affiche jamais à zéro quand c'est inconnu. Sans production, le taux d'autoconsommation est null et non 0 % — à 3 h du matin, « 0 % » se lirait comme un défaut alors qu'il n'y a rien à autoconsommer. VÉRIFIÉ SUR APPAREIL contre .75, en lecture seule (le banc est en préparation V2C, rien n'a été écrit) : 32 échantillons depuis minuit, prod 28,37 · conso 26,55 · import 11,95 · export 13,77 kWh, identité OK, et les tuiles affichent 51 % / 55 % / 13,8 / 11,9. Le passage sur appareil a trouvé ce qu'aucun des 138 tests unitaires ne pouvait voir : LES QUATRE LIBELLÉS DÉBORDAIENT — « Autoconsomm… », « Vers réseau · aujou… ». Le suffixe « aujourd'hui » répété quatre fois mangeait le nom de la grandeur ; il est dit une fois en en-tête. Le harnais d'appareil échoue désormais sur tout libellé finissant par « … ». Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
112 lines
5.0 KiB
Dart
112 lines
5.0 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;
|
||
|
||
/// 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);
|
||
}
|
||
}
|