etm-powersync-app/lib/models/ev_charger_live.dart
Patrick Schurig 95769938fc fix(ev): la tuile des favoris devient muette — elle lit la borne, ou le dit
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
2026-08-28 12:49:28 +02:00

77 lines
3.4 KiB
Dart

/// Ce qu'une borne de recharge **publie réellement**, et rien d'autre.
///
/// ## Pourquoi ce type existe
///
/// L'app portait trois grandeurs de recharge qui n'étaient jamais lues sur la box :
/// `EnergyData.chargingMode` (défaut `pv`), `chargingPower` (défaut 3,6 kW) et
/// `solarSourcePercent` (défaut 82 %). Elles étaient affichées comme des mesures — « 3600
/// W », « 82 % solaire » — sur un écran de favoris que le client peut ouvrir. C'est la même
/// famille de défaut que le SOC à 62 % en dur et les quatre pièces de climatisation
/// fictives : une valeur par défaut présentée comme une lecture.
///
/// Ce type ne porte donc **que des champs nullables**, et `null` veut dire *la box ne le
/// publie pas* — jamais zéro. L'écran doit le dire, pas le combler.
///
/// ## Ce qui est lisible aujourd'hui, mesuré sur `.75` le 2026-08-28
///
/// Les deux bornes du banc (Simulated wallbox, V2C Trydan) publient `pluggedIn`,
/// `charging`, `currentPower` et `maxChargingCurrent`. Le mode, lui, ne vient pas du Thing
/// mais de `NymeaEnergy.GetChargingInfos` — les deux bornes y étaient en
/// `ChargingModeNormal`, `ChargingStateIdle`.
///
/// **`sessionEnergy` existe** sur les deux (4,229 kWh sur la V2C), et le renommage
/// `currentL1/L2/L3` **est déployé** — plus de `powerL1/L2/L3`. Ces deux champs ne sont pas
/// portés ici pour autant : voir la note de `LoadMechanism`
/// (`models/load_telemetry.dart`), l'égalité `sessionEnergy == chargeEnergy` relevée ce
/// jour ne prouve pas encore que le premier survit à une interruption de charge, et c'est
/// exactement ce que LM-1009 §C1 demande de savoir avant d'afficher un avancement.
library;
import 'energy_data.dart' show ChargingMode;
class EvChargerLive {
/// `thingId` de la borne — l'identifiant que `SetChargingInfo` attend.
final String thingId;
final String name;
/// États du Thing. `null` = non publié par cette borne.
final bool? pluggedIn;
final bool? charging;
final double? currentPowerW;
/// Mode **tel que la box le porte** (`ChargingModeEco`, `ChargingModeNormal`, …), lu par
/// `GetChargingInfos`. `null` = pas encore lu, ou aucune entrée pour cette borne — et
/// dans ce cas l'écran n'a **aucun** mode à présenter comme courant.
final String? apiMode;
const EvChargerLive({
required this.thingId,
required this.name,
this.pluggedIn,
this.charging,
this.currentPowerW,
this.apiMode,
});
/// Traduction du mode de la box vers les trois boutons de l'IU.
///
/// Les cinq valeurs de l'énumération se replient sur trois : les variantes « avec
/// échéance » gardent le bouton de base, l'échéance étant une option à côté. `null` pour
/// une valeur inconnue — une borne peut porter un mode qu'aucune version d'app ne
/// connaît, et deviner serait pire que dire « mode non reconnu ».
ChargingMode? get uiMode => switch (apiMode) {
'ChargingModeNormal' => ChargingMode.boost,
'ChargingModeEco' || 'ChargingModeEcoWithTargetTime' => ChargingMode.pv,
'ChargingModeEcoWithMinCurrent' ||
'ChargingModeEcoMinWithTargetTime' =>
ChargingMode.minPv,
_ => null,
};
/// Vrai quand la borne porte une échéance dans son mode — l'information est dans le nom
/// de l'énumération, pas dans un champ à part.
bool get hasDeadlineMode =>
apiMode == 'ChargingModeEcoWithTargetTime' ||
apiMode == 'ChargingModeEcoMinWithTargetTime';
}