Décision de Patrick : trois modes suffisent — Manuel, PV, Min+PV. Le boost n'est pas un quatrième mode, c'est la dérogation (+etm61). LE FAIT. Le bouton écrivait `ChargingModeNormal`, qui sort la borne de l'arbitrage et lui restitue ses valeurs manuelles stockées — manualChargingEnabled, manualMaxChargingCurrent, manualDesiredPhaseCount. Sur .75 elles valaient false / 6 A / 3 phases : mesuré le 2026-09-08 à 11:09:51, l'appui a ARRÊTÉ la charge et armé un verrou de 300 s. Pas une régression — c'est ce que Normal a toujours fait. C'est le nom qui mentait. Renommé jusque dans le code (`ChargingMode.boost` -> `ChargingMode.manuel`), pas seulement sur l'étiquette : un identifiant faux reste un piège pour le prochain lecteur, et c'est précisément ce qui a duré des mois ici. L'ordre met Manuel en tête, parce que ChargingModeNormal est aussi le mode d'office d'une borne détectée. ET LE RENOMMAGE NE SUFFISAIT PAS. « Manuel » dit QUI commande, pas ce que la borne va faire : tant que l'app n'offre pas de régler ces valeurs stockées, le mode veut dire « ce qui traînait la dernière fois ». Passer en manuel demande donc confirmation, et la confirmation dit la conséquence — la charge peut s'arrêter — AVANT, pas après. ⚠️ La confirmation ne se déclenche pas sur un ré-appui du mode déjà en place : une confirmation qu'on apprend à cliquer sans lire ne protège plus de rien. Un test le fige. Vérifié sur .75 (1.15.2+etm61) : SetLoadDerogation et ClearLoadDerogation sont dans Introspect, avec derogationEnded / durationS / restoredMode. Le contrat de la dérogation est écrit dans CLAUDE.md — durée obligatoire bornée [60, 21600], état lu dans decision.code == "DEROGATION_ACTIVE" et jamais dans le mode, fin au niveau de la trame avec six causes. Aucun bouton de boost n'est écrit : c'est le lot suivant. Et une sonde pour le moteur, faite au passage : la V2C est bien restée en ChargingModeNormal et elle est ABSENTE de loads[] — hors arbitrage. Leur hypothèse est confirmée sans redémarrage. 339 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
166 lines
8.4 KiB
Dart
166 lines
8.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, 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.manuel,
|
|
'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';
|
|
}
|