/// 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, 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. /// /// Le plugin V2C `1.15.2+etm13` **construit** ce `sessionEnergy` : il accumule les /// incréments de `chargeEnergy`, détecte les remises à zéro, et ne repart qu'à l'ouverture /// d'une session. **Relevé sur charge réelle le 2026-08-30** (Skoda branchée, lien rétabli), /// deux échantillons à deux heures d'écart : /// /// | | 12:15 | 14:12 | /// |---|---|---| /// | `sessionEnergy` | 7,5147 kWh | 15,8015 → 15,8405 kWh, en mouvement | /// | `chargeEnergy` | 0,3103 kWh | 8,5972 → 8,6361 kWh | /// | écart | **7,2044** | **7,2044** | /// /// L'écart est **constant au dix-millième sur deux heures** : c'est l'accumulateur du /// plugin, et il porte les incréments d'avant la dernière remise à zéro. La remise à zéro /// elle-même n'a pas été surprise en direct, mais l'accumulation, elle, est prouvée par cet /// écart — elle ne se déduisait pas du code. /// /// **Aucun avancement n'est affiché pour autant** : c'est une décision (LM-1009 §D1 — /// l'avancement se dit en énergie livrée, et il n'y a pas encore d'écran pour le porter), /// plus une mesure manquante. LM-1009 §C1 reste vraie pour les bornes qui ne publient pas /// `sessionEnergy` ; **la V2C n'en fait plus partie**. library; import 'energy_data.dart' show ChargingMode; /// Pleine puissance — et **mode d'office** de toute borne détectée par le moteur. const String kChargingModeNormal = 'ChargingModeNormal'; /// Ce que l'écran doit dire d'une borne en [EvChargerLive.isUnmanaged], mot pour mot aux /// deux endroits qui l'affichent : la tuile des favoris, où les modes se changent, et la /// carte de configuration, que l'installateur ouvre à la mise en service. const String kEvUnmanagedNote = 'Pleine puissance, hors arbitrage : le HEMS ne pilote pas cette borne. ' 'C\'est aussi le mode d\'office d\'une borne qui vient d\'être détectée — ' 'choisir PV ou Min+PV la fait entrer dans l\'arbitrage du surplus.'; 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, **ou borne injoignable**. /// /// ## Pourquoi l'injoignable produit `null` ici, et pas la dernière valeur connue /// /// Mesuré sur `.75` le 2026-08-30, V2C Trydan : quand le lien Modbus tombe, la box ne se /// contente pas de figer les états — elle **publie des zéros**. /// /// | État | Avant la coupure | Après | /// |---|---|---| /// | `connected` | true | **false** | /// | `pluggedIn` | true | **false** | /// | `charging` | true | **false** | /// | `currentPower` | 4 396 W | **0** | /// | `statusMessage` | "" | « Hors ligne depuis 30/08 12:39 — timeout Modbus … » | /// /// Une voiture branchée et en charge devient donc, à l'écran, « aucun véhicule branché, /// 0 W » — **un fait forgé, pas une absence**. Et avant ces zéros, la dernière valeur /// connue reste affichée un moment comme si elle était actuelle (vérifié : aucun état ne /// bouge pendant 45 s alors que la borne est censée tirer 4 356 W). /// /// L'app refuse donc de relayer les deux : sous [horsLigne], ces trois champs sont mis à /// `null` **à la construction**, pour qu'aucun écran ne puisse afficher ni le zéro forgé /// ni la valeur périmée. Seuls [joignable] et [messageEtat] disent quelque chose de vrai. final bool? pluggedIn; final bool? charging; final double? currentPowerW; /// L'état `connected` du Thing — **le seul champ qui reste vrai quand le lien tombe**. /// /// `null` = la borne ne publie pas cet état ; on ne conclut alors rien. final bool? joignable; /// `statusMessage` du Thing : une phrase déjà écrite par le plugin, en français, avec /// l'heure de la coupure et la piste à suivre. On l'affiche telle quelle — la /// reformuler perdrait l'horodatage et le diagnostic. final String? messageEtat; /// Le mode reste lisible **même hors ligne**, et c'est voulu : il est stocké dans la /// box (`GetChargingInfos`), pas lu sur la borne. Un lien coupé n'invalide donc pas le /// mode, seulement les états du matériel — et les trois boutons continuent d'écrire, /// puisqu'ils écrivent eux aussi dans la box. /// /// 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, this.joignable, this.messageEtat, }); /// **La box ne joint plus la borne.** Rien de ce qu'elle publierait d'autre n'est un fait. /// /// `joignable == null` n'est pas hors ligne : une borne qui ne publie pas `connected` ne /// dit rien de son lien, et supposer la panne serait le symétrique du zéro forgé. bool get horsLigne => joignable == false; /// 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, }; /// **La borne charge à pleine puissance, hors arbitrage.** /// /// `ChargingModeNormal` n'est pas seulement le « boost » que quelqu'un peut choisir : /// c'est le **mode d'office d'une borne détectée**. Une borne neuve n'est donc jamais /// pilotée par le HEMS tant que personne n'a posé un mode éco — et rien, ni dans la /// télémétrie ni sur l'écran, ne le disait. L'installateur pose une borne, elle charge à /// fond, et il cherche pourquoi le HEMS l'ignore. /// /// ⚠️ **L'app ne peut PAS distinguer « personne n'a choisi » de « quelqu'un a choisi /// Boost »** : la box publie le même `chargingMode` dans les deux cas. La note dit donc /// l'état — pleine puissance, hors arbitrage — et signale qu'il est aussi le défaut ; /// elle n'accuse personne de ne pas avoir réglé. Même règle que la note de rangs ex /// æquo : reproductible n'est pas choisi, et on ne fait pas dire à un écran ce que la /// donnée ne porte pas. bool get isUnmanaged => apiMode == kChargingModeNormal; /// 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'; }