etm-powersync-app/lib/models/energy_data.dart
Patrick Schurig a6dd319ef2 fix(rôles): le compteur RÉSEAU n'est plus détecté comme compteur solaire
Signalé sur banc après changement de matériel : Fronius SunSpec retiré, Eastron
SDM72 posé en rootMeter et SDM120 en compteur de production. L'écran affichait le
SDM72 dans les DEUX rôles, avec « auto · lecture seule » — donc sans moyen de le
corriger.

La box était juste (`GetRootMeter` rend bien le SDM72) et les interfaces
distinguaient les deux sans ambiguïté :

  Grid / sdm72          : energymeter, smartmeter, connectable
  PV   / sdm120Producer : smartmeterproducer, smartmeter, connectable

C'est notre liste qui ne discriminait pas : le rôle solaire acceptait
`energymeter`, `meter` et `smartmeter` — portées par TOUT compteur, réseau
compris — et ignorait `smartmeterproducer`, la seule qui dise « ce compteur
mesure une production ».

Deux corrections, la seconde tenant même si une future classe déclare des
interfaces trop larges :

- le rôle solaire exige désormais une interface de production ;
- le Thing acté comme compteur RACINE n'est plus candidat à aucun autre rôle.

Reste ouvert, et c'est le vrai fond du signalement : une détection automatique
ne devrait pas être un cul-de-sac. Les lignes de compteur n'ont aujourd'hui
AUCUN geste — pas de sélecteur, quel que soit l'état. Le badge « lecture seule »
est honnête, mais une inférence fausse doit pouvoir se corriger.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 12:31:59 +02:00

199 lines
8.0 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.

class EnergyData {
final double pvPower; // Watts - production solaire
final double homePower; // Watts - consommation maison
final double batteryPower; // Watts - puissance batterie (+ charge, - décharge)
/// **Réseau, signé : `+` SOUTIRÉ, `−` INJECTÉ.**
///
/// ⚠️ Le commentaire précédent disait l'inverse — « + injection, − soutirage » — et la
/// carte de flux l'a cru : le 2026-09-06, une installation qui **exportait 2,5 kW**
/// affichait « consommation réseau », flèche à l'envers. Le sens de l'animation était
/// inversé, et rien dans les tests ne pouvait le voir.
///
/// La convention vient de la box et se vérifie à la sonde : `currentPowerAcquisition`
/// vaut **−2 511 W** pendant que la production (5,76 kW) dépasse la consommation
/// (3,25 kW) de 2,5 kW. Elle est transportée telle quelle (§7.1, signes bruts).
///
/// Ne pas relire ce signe à la main : passer par [gridImportW] / [gridExportW]. Le
/// dépôt portait DEUX conventions opposées dans deux widgets voisins, et celle qui était
/// à l'écran était la fausse.
final double gridPower;
final double batterySOC; // % - état de charge batterie
final double temperature; // °C
final String weatherIcon;
// Journée
final double dayProductionWh;
final double daySelfConsumptionWh;
final double dayGridInjectionWh;
/// Compteurs CUMULÉS de la box (kWh), tels qu'elle les publie — pas des totaux du jour.
///
/// `Energy.GetPowerBalance` rend `totalReturn` (exporté) et `totalAcquisition`
/// (importé). Ils sont **cumulés depuis toujours**, pas remis à zéro chaque nuit : les
/// afficher sous un libellé « aujourd'hui » serait faux.
///
/// `NaN` signifie **valeur non exploitable**, et c'est un état à part entière : le banc
/// `.75` publie des compteurs manifestement corrompus (7,5e+33 kWh importés) alors que
/// `totalProduction` reste sain. L'app ne les corrige pas — elle refuse de les
/// présenter comme une mesure (cf. [gridExportKwhOrNull]).
final double gridExportKwh;
final double gridImportKwh;
final double selfConsumptionRate; // %
final double autonomyRate; // %
final double dayGains; // €
// ── Borne de recharge : PLUS RIEN ICI ──────────────────────────────────────
//
// `chargingMode`, `chargingPower` (3,6 kW) et `solarSourcePercent` (82 %) ont vécu ici
// avec des valeurs par défaut que rien ne remplaçait : `_updateEnergyData` ne les
// touchait pas, et le mode n'avançait qu'en optimiste après une commande envoyée depuis
// l'app. Trois écrans les affichaient comme des mesures — la tuile des favoris, le
// Sankey du tableau de bord, et la carte véhicule (supprimée le 2026-08-28).
//
// Ce que la box publie vraiment sur une borne vit désormais dans `EvChargerLive`
// (`models/ev_charger_live.dart`), champ par champ, nullable : `null` veut dire « non
// publié », jamais zéro.
final bool gridLimitOk;
const EnergyData({
this.pvPower = 0,
this.homePower = 0,
this.batteryPower = 0,
this.gridPower = 0,
this.batterySOC = 0,
this.temperature = 0,
this.weatherIcon = 'cloud_snow',
this.dayProductionWh = 0,
this.daySelfConsumptionWh = 0,
this.dayGridInjectionWh = 0,
this.gridExportKwh = 0,
this.gridImportKwh = 0,
this.selfConsumptionRate = 0,
this.autonomyRate = 0,
this.dayGains = 0,
this.gridLimitOk = true,
});
/// Puissance **soutirée** au réseau, ou 0. Voir [gridPower] pour la convention.
double get gridImportW => gridPower > 0 ? gridPower : 0;
/// Puissance **injectée** vers le réseau, ou 0.
double get gridExportW => gridPower < 0 ? -gridPower : 0;
EnergyData copyWith({
double? pvPower,
double? homePower,
double? batteryPower,
double? gridPower,
double? batterySOC,
double? temperature,
String? weatherIcon,
double? dayProductionWh,
double? daySelfConsumptionWh,
double? dayGridInjectionWh,
double? gridExportKwh,
double? gridImportKwh,
double? selfConsumptionRate,
double? autonomyRate,
double? dayGains,
bool? gridLimitOk,
}) {
return EnergyData(
pvPower: pvPower ?? this.pvPower,
homePower: homePower ?? this.homePower,
batteryPower: batteryPower ?? this.batteryPower,
gridPower: gridPower ?? this.gridPower,
batterySOC: batterySOC ?? this.batterySOC,
temperature: temperature ?? this.temperature,
weatherIcon: weatherIcon ?? this.weatherIcon,
dayProductionWh: dayProductionWh ?? this.dayProductionWh,
daySelfConsumptionWh: daySelfConsumptionWh ?? this.daySelfConsumptionWh,
dayGridInjectionWh: dayGridInjectionWh ?? this.dayGridInjectionWh,
gridExportKwh: gridExportKwh ?? this.gridExportKwh,
gridImportKwh: gridImportKwh ?? this.gridImportKwh,
selfConsumptionRate: selfConsumptionRate ?? this.selfConsumptionRate,
autonomyRate: autonomyRate ?? this.autonomyRate,
dayGains: dayGains ?? this.dayGains,
gridLimitOk: gridLimitOk ?? this.gridLimitOk,
);
}
/// Plafond de plausibilité pour un compteur d'énergie domestique cumulé, en kWh.
///
/// 10 GWh, soit environ **trois mille ans** de consommation d'une maison. Aucune
/// installation résidentielle ne l'atteindra ; tout ce qui le dépasse est un compteur
/// cassé, pas un record. Le seuil est volontairement absurde-haut : il ne s'agit pas de
/// juger une valeur, seulement d'attraper les corrompues.
static const double kPlausibleMaxKwh = 1e7;
/// Vrai si [v] peut être présenté comme une mesure.
static bool isPlausibleKwh(double v) =>
v.isFinite && v >= 0 && v <= kPlausibleMaxKwh;
/// Export cumulé, ou `null` si la box publie une valeur inexploitable.
double? get gridExportKwhOrNull =>
isPlausibleKwh(gridExportKwh) ? gridExportKwh : null;
/// Import cumulé, ou `null` si la box publie une valeur inexploitable.
double? get gridImportKwhOrNull =>
isPlausibleKwh(gridImportKwh) ? gridImportKwh : null;
}
enum ChargingMode { pv, minPv, boost }
class HistoryPoint {
final DateTime time;
final double pvWh;
final double homeWh;
final double gridWh;
final double batteryWh;
const HistoryPoint({
required this.time,
this.pvWh = 0,
this.homeWh = 0,
this.gridWh = 0,
this.batteryWh = 0,
});
}
// ── Entrée Energy.GetPowerBalanceLogs ─────────────────────────────────────────
class PowerBalanceEntry {
final DateTime timestamp;
final double productionW; // puissance PV (valeur absolue, W)
final double consumptionW; // consommation maison (W)
final double acquisitionW; // import réseau (W, positif)
final double storageW; // batterie : +charge / -décharge (W)
/// Compteurs cumulés depuis l'origine, en **kWh** — et non en Wh comme le nom de ces
/// champs l'a longtemps dit.
///
/// Vérifié sur `.75` le 2026-08-27 par recoupement, parce qu'aucune documentation ne le
/// tranchait : sur une fenêtre de 21,75 h, l'intégration trapézoïdale des puissances
/// instantanées (qui sont bien en **W**) rend **77 697 Wh**, quand la différence de
/// `totalProduction` sur la même fenêtre rend **78,31**. Le rapport est mille.
///
/// Le mauvais nom n'était pas cosmétique : l'écran Énergie affichait « 78 Wh » pour une
/// journée de 78 kWh, et personne ne pouvait le voir sans connaître l'ordre de grandeur
/// attendu.
final double totalProductionKwh;
final double totalConsumptionKwh;
/// Injecté vers le réseau, cumulé (kWh).
final double totalReturnKwh;
/// Soutiré au réseau, cumulé (kWh).
final double totalAcquisitionKwh;
const PowerBalanceEntry({
required this.timestamp,
this.productionW = 0,
this.consumptionW = 0,
this.acquisitionW = 0,
this.storageW = 0,
this.totalProductionKwh = 0,
this.totalConsumptionKwh = 0,
this.totalReturnKwh = 0,
this.totalAcquisitionKwh = 0,
});
}