Elle reste : la retirer aurait laissé un trou dans une liste que le client a composée lui-même. Elle affiche maintenant ce que la box publie, et là où il n'y a rien à lire, elle le dit. Ce qui est lu, et sa source · branché / en charge ← états pluggedIn, charging du Thing · puissance ← état currentPower du Thing · mode ← GetChargingInfos + notification ChargingInfoChanged Absent = « non publié », jamais zéro. Un mode que l'app ne connaît pas se lit « mode non reconnu (X) » plutôt que d'être deviné, et aucun bouton n'est allumé tant que la box n'a pas dit ce qu'elle porte. L'écriture restitue le refus, sinon elle n'existe pas. Les pastilles de 7 à 10 px qui avalaient les codes d'erreur de setChargingInfo passent à 44 px, montrent l'attente, et affichent le refus tel que la box le nomme. Et l'app ne tient plus d'état de mode local : après une écriture acceptée, elle RELIT GetChargingInfos. Elle se souvenait de ce qu'elle avait demandé, ce qui n'est pas ce que la borne porte. La fiction perd sa porte d'entrée. chargingMode, chargingPower (3,6 kW) et solarSourcePercent (82 %) sont SUPPRIMÉS de EnergyData : trois valeurs par défaut que _updateEnergyData ne touchait jamais. Ce que la box publie sur une borne vit désormais dans EvChargerLive, champ par champ, nullable. ⚠️ Et le Sankey du tableau de bord en tirait un flux « Voiture » permanent à 3,6 kW, voiture débranchée. Il est branché sur la mesure de la borne — « non lu » quand il n'y en a pas. Mais la même vue invente encore PAC = 38 % de la maison, Eau chaude = 18 %, Autres = 22 %, dont un seul marqué « estimé » : entrée 🔴 au TODO, avec la source réelle (GetLoadTelemetry publie measuredW par charge : 1500 W et 800 W ce midi). Brancher le Sankey dessus est une refonte de l'écran d'accueil, pas un correctif — à décider. Deux attentes tombent, une reste (relevé Integrations.GetThings sur .75, cet après-midi) · currentL1/L2/L3 est DÉPLOYÉ (5,86 / 5,94 / 6,02 A) — powerL1/L2/L3 a disparu · phaseCount EXISTE sur la Trydan (3) — la note qui le disait absent était en retard · sessionEnergy existe (4,229 kWh) MAIS vaut exactement chargeEnergy : c'est ce qu'on attend d'une session sans interruption, donc ça ne prouve rien. LM-1009 §C1 reste entière tant qu'une remise à zéro de chargeEnergy n'a pas été vue avec un sessionEnergy qui continue. Le brief moteur dit désormais du masquage WRITE_FAILED que ce n'est pas « ne se produit pas » mais « ne s'est pas encore produit » : il attend une charge variable qui perd son Thing, et le banc n'a pas eu ce cas. 154 tests (8 nouveaux sur la tuile, dont les cibles tactiles et la restitution du refus), flutter analyze à 27 remarques, aucune nouvelle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
179 lines
7.0 KiB
Dart
179 lines
7.0 KiB
Dart
class EnergyData {
|
|
final double pvPower; // Watts - production solaire
|
|
final double homePower; // Watts - consommation maison
|
|
final double batteryPower; // Watts - puissance batterie (+ charge, - décharge)
|
|
final double gridPower; // Watts - réseau (+ injection, - soutif)
|
|
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,
|
|
});
|
|
|
|
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,
|
|
});
|
|
} |