fix(borne): une borne injoignable ne montre ni le zéro forgé ni la valeur périmée
Mesuré sur `.75` le 2026-08-30 : quand le lien Modbus de la V2C tombe, la box ne fige pas les états, elle publie des zéros — `pluggedIn` false, `charging` false, `currentPower` 0 — alors que la Skoda était branchée et en charge à 4 356 W. Avant ça, la dernière valeur connue reste publiée comme si elle était actuelle (vérifié : aucun état ne bouge en 45 s pendant une charge supposée). L'app refusait déjà d'inventer un zéro ; elle en relayait un. Sous `connected == false`, `EvChargerLive` met `pluggedIn`, `charging` et `currentPowerW` à `null` À L'ENTRÉE — un seul endroit, pas un test par écran — et porte `joignable` + `messageEtat`. La tuile dit « Hors ligne » et reprend la phrase du plugin, qui porte l'heure de la coupure et la piste WiFi. Deux garde-fous dans les tests : `joignable == null` n'est PAS hors ligne (une borne qui ne publie pas `connected` ne dit rien de son lien — supposer la panne serait le symétrique du zéro forgé), et le MODE reste affiché hors ligne, parce qu'il est stocké dans la box et non lu sur la borne. Brief moteur : les zéros à la perte du lien, `chargingState` figé à `Idle` pendant une charge réelle, et `ChargingModeNormal` qui n'est pas un boost — il restaure les valeurs manuelles mémorisées (ici arrêt, 6 A) et sort la borne de l'arbitrage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
This commit is contained in:
parent
a1ffd13107
commit
2c9cca042e
@ -2,6 +2,85 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-30 (soir) — trois constats sur la borne, et deux d'entre eux FORGENT un fait
|
||||
|
||||
**Contexte, mesuré** : V2C Trydan, Skoda branchée et en charge (4 356 W, 3 phases), passage
|
||||
en Boost depuis l'app à 12:15:34, lien Modbus tombé à 12:15:31 — trois secondes avant.
|
||||
|
||||
### 1. `chargingState` reste `Idle` pendant une charge réelle
|
||||
|
||||
En `ChargingModeNormal` l'arbitre sort la borne du waterfall — `[Arbitre] Borne "V2C Trydan"
|
||||
hors waterfall (mode manuel ou aucun véhicule branché)`, à chaque cycle — et cesse donc de
|
||||
maintenir `chargingState`. Le champ garde sa dernière valeur, `ChargingStateIdle`, pendant que
|
||||
la borne tire 4 356 W.
|
||||
|
||||
**Un champ que plus personne ne maintient devrait être absent, pas figé.** C'est la règle que
|
||||
`draw`, `counts` et `measurement` appliquent déjà — appliquée cette fois à un champ **vivant
|
||||
qui cesse de l'être**, ce qui est le cas le plus difficile à voir : rien ne change, et c'est
|
||||
justement le problème. Nous ne l'affichons pas, et nous ne l'afficherons pas tant qu'il peut
|
||||
être périmé sans le dire.
|
||||
|
||||
### 2. À la perte du lien, les états publiés passent à ZÉRO — c'est le plus grave
|
||||
|
||||
Relevé état par état sur le même Thing, avant et après `marking unreachable` :
|
||||
|
||||
| État | Avant | 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 — vérifier signal WiFi de la borne » |
|
||||
|
||||
La voiture était **branchée et en charge**. Un client qui relaie ces états affiche donc
|
||||
« aucun véhicule branché, 0 W » — **un fait forgé, pas une absence**. Et avant les zéros, la
|
||||
dernière valeur connue reste publié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.
|
||||
|
||||
**La demande** : à la perte du lien, **ne pas republier** `pluggedIn` / `charging` /
|
||||
`currentPower`. Les figer est déjà trompeur ; les remettre à zéro **fabrique une
|
||||
affirmation** — et c'est exactement ce que la règle « un champ absent vaut mieux qu'un champ
|
||||
nul ou forgé » interdit. Nous ne savons pas d'où viennent ces zéros (plugin V2C ou remise à
|
||||
zéro générique de nymea à la déconnexion), et c'est à vous de le dire.
|
||||
|
||||
Deux choses sauvent la lecture, et elles sont bien faites : `connected` bascule, et
|
||||
`statusMessage` porte **l'heure de la coupure et la piste à suivre**, déjà rédigés. Nous
|
||||
l'affichons tel quel.
|
||||
|
||||
> **Corrigé côté app aujourd'hui** : sous `connected == false`, `EvChargerLive` met
|
||||
> `pluggedIn`, `charging` et `currentPowerW` à `null` **à l'entrée** — ni le zéro forgé, ni la
|
||||
> valeur périmée. La tuile dit « Hors ligne » et reprend votre phrase. Le **mode**, lui, reste
|
||||
> affiché : il est stocké dans la box, pas lu sur la borne, donc un lien coupé ne l'invalide
|
||||
> pas.
|
||||
|
||||
### 3. `ChargingModeNormal` n'est pas un boost — et notre écran le promet
|
||||
|
||||
Le journal est explicite :
|
||||
|
||||
```
|
||||
EV charger "V2C Trydan" changed to normal mode. Restoring manual values of false 6 A 3 phases
|
||||
```
|
||||
|
||||
Passer en Normal **restaure les valeurs manuelles mémorisées** — ici *power `false`, 6 A* —
|
||||
et sort la borne de l'arbitrage. Donc : ce que le mode restaure est ce qui traînait, pas un
|
||||
maximum ; et **plus personne ne commande ensuite**, donc rien ne monte le courant.
|
||||
|
||||
Or l'attente derrière le bouton est explicite (Patrick, 2026-08-30) : **en Boost la borne
|
||||
doit charger à sa puissance maximale.** Notre correspondance `[Boost] → ChargingModeNormal`
|
||||
(`CLAUDE.md`) ne la tient pas.
|
||||
|
||||
**La question, et sa contrainte** : comment une app exprime-t-elle « charge maintenant, à
|
||||
fond » ? Nous **n'écrirons pas les actions du Thing** (`power`, `maxChargingCurrent`) depuis
|
||||
l'écran : `+etm23` a rétabli *une charge, un commandeur*, et un boost écrit par l'app
|
||||
rouvrirait exactement le défaut que ce lot a fermé — deux commandeurs, et une consigne publiée
|
||||
qui n'atteint pas le matériel. Il faut donc que ça vienne de vous : soit le mode fixe lui-même
|
||||
les valeurs manuelles au maximum de la borne, soit un mode (ou un paramètre) dédié le porte.
|
||||
|
||||
Tant que ce n'est pas tranché, **nous ne renommons pas le bouton** : l'écran dirait « Boost »
|
||||
sur un mode qui, selon ce qui traîne en mémoire, peut laisser la borne à l'arrêt.
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-30 (suite) — `draw.committedW` en préalable : OUI, sans réserve
|
||||
|
||||
**Et le refus de livrer `counts` sur `budget.evReservedW` est le bon appel.** Une identité
|
||||
|
||||
@ -47,11 +47,48 @@ class EvChargerLive {
|
||||
final String thingId;
|
||||
final String name;
|
||||
|
||||
/// États du Thing. `null` = non publié par cette borne.
|
||||
/// É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.
|
||||
@ -64,8 +101,16 @@ class EvChargerLive {
|
||||
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
|
||||
|
||||
@ -1483,13 +1483,22 @@ class NymeaService extends ChangeNotifier {
|
||||
if (thing == null) return null;
|
||||
final info = _chargingInfos[id];
|
||||
final power = _stateByName(thing, 'currentPower');
|
||||
final joignable = _stateByName(thing, 'connected') as bool?;
|
||||
final msg = _stateByName(thing, 'statusMessage');
|
||||
// Borne injoignable : la box publie des ZÉROS (pluggedIn false, charging false,
|
||||
// currentPower 0) là où elle ne sait plus rien — mesuré sur la V2C le 2026-08-30.
|
||||
// Les relayer ferait dire à l'écran « aucun véhicule branché » pendant une charge.
|
||||
// On les jette ici, à l'entrée, plutôt que dans chaque écran.
|
||||
final horsLigne = joignable == false;
|
||||
return EvChargerLive(
|
||||
thingId: id,
|
||||
name: thing.name,
|
||||
pluggedIn: _stateByName(thing, 'pluggedIn') as bool?,
|
||||
charging: _stateByName(thing, 'charging') as bool?,
|
||||
currentPowerW: power is num ? power.toDouble() : null,
|
||||
pluggedIn: horsLigne ? null : _stateByName(thing, 'pluggedIn') as bool?,
|
||||
charging: horsLigne ? null : _stateByName(thing, 'charging') as bool?,
|
||||
currentPowerW: (!horsLigne && power is num) ? power.toDouble() : null,
|
||||
apiMode: info?['chargingMode'] as String?,
|
||||
joignable: joignable,
|
||||
messageEtat: (msg is String && msg.isNotEmpty) ? msg : null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -60,7 +60,9 @@ class EvFavoriteTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
final couleur = live.charging == true ? EtmTokens.green : EtmTokens.blue;
|
||||
final couleur = live.horsLigne
|
||||
? EtmTokens.amber
|
||||
: (live.charging == true ? EtmTokens.green : EtmTokens.blue);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
@ -80,7 +82,11 @@ class EvFavoriteTile extends StatelessWidget {
|
||||
children: [
|
||||
// La puissance MESURÉE, ou le fait qu'elle ne soit pas publiée. Jamais 0 W
|
||||
// par défaut : « 0 W » et « pas de mesure » sont deux choses différentes.
|
||||
if (live.currentPowerW != null)
|
||||
if (live.horsLigne)
|
||||
Text('Hors ligne',
|
||||
style: EtmTokens.sans(
|
||||
size: 20, weight: FontWeight.w700, color: couleur))
|
||||
else if (live.currentPowerW != null)
|
||||
Text('${live.currentPowerW!.round()} W',
|
||||
style: EtmTokens.mono(
|
||||
size: 26, weight: FontWeight.w700, color: couleur))
|
||||
@ -123,6 +129,13 @@ class EvFavoriteTile extends StatelessWidget {
|
||||
|
||||
/// Branché ? En charge ? — et « non publié » quand la borne ne le dit pas.
|
||||
String _etat(EvChargerLive live) {
|
||||
// Hors ligne : la box ne sait rien du branchement, et ce qu'elle publierait
|
||||
// (« aucun véhicule branché », « 0 W ») est forgé. On dit la coupure, avec la phrase
|
||||
// du plugin quand elle existe — elle porte l'heure et la piste.
|
||||
if (live.horsLigne) {
|
||||
return live.messageEtat ??
|
||||
'Borne injoignable — la box ne sait plus si un véhicule est branché';
|
||||
}
|
||||
if (live.pluggedIn == null && live.charging == null) {
|
||||
return 'Branchement non publié par cette borne';
|
||||
}
|
||||
|
||||
@ -32,6 +32,58 @@ EvChargerLive _v2c({
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('borne injoignable — ni le zéro forgé, ni la valeur périmée', () {
|
||||
// Mesuré sur `.75` le 2026-08-30 : quand le lien Modbus de la V2C tombe, la box
|
||||
// publie `connected: false`, `pluggedIn: false`, `charging: false`,
|
||||
// `currentPower: 0` et un `statusMessage`. La voiture, elle, était branchée et en
|
||||
// charge à 4 356 W. `NymeaService.evChargerFor` jette ces zéros ; ces tests
|
||||
// vérifient ce que la tuile en fait.
|
||||
EvChargerLive horsLigne({String? message}) => EvChargerLive(
|
||||
thingId: '4d1adafd-cafc-4813-93b1-ca8933ce71ab',
|
||||
name: 'V2C Trydan',
|
||||
apiMode: 'ChargingModeNormal',
|
||||
joignable: false,
|
||||
messageEtat: message,
|
||||
);
|
||||
|
||||
testWidgets('elle dit la coupure, avec la phrase du plugin', (tester) async {
|
||||
await tester.pumpWidget(_harness(
|
||||
live: horsLigne(
|
||||
message: 'Hors ligne depuis 30/08 12:39 — timeout Modbus — '
|
||||
'vérifier signal WiFi de la borne')));
|
||||
|
||||
expect(find.text('Hors ligne'), findsOneWidget);
|
||||
expect(find.textContaining('timeout Modbus'), findsOneWidget);
|
||||
// …et surtout PAS ce que la box publiait au même instant.
|
||||
expect(find.textContaining('Aucun véhicule branché'), findsNothing);
|
||||
expect(find.textContaining('0 W'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('sans phrase du plugin, elle nomme quand même l\'ignorance',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(_harness(live: horsLigne()));
|
||||
expect(find.textContaining('ne sait plus si un véhicule est branché'),
|
||||
findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('le MODE reste lu — il vient de la box, pas de la borne',
|
||||
(tester) async {
|
||||
// Un lien coupé n'invalide pas un réglage stocké dans la box : le cacher
|
||||
// priverait le client de la seule chose encore vraie à l'écran.
|
||||
await tester.pumpWidget(_harness(live: horsLigne()));
|
||||
expect(find.textContaining('Mode Boost'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('`joignable == null` n\'est PAS hors ligne', (tester) async {
|
||||
// Une borne qui ne publie pas `connected` ne dit rien de son lien. Supposer la
|
||||
// panne serait le symétrique exact du zéro forgé.
|
||||
await tester.pumpWidget(_harness(
|
||||
live: _v2c(pluggedIn: true, charging: true, powerW: 4356)));
|
||||
expect(find.text('Hors ligne'), findsNothing);
|
||||
expect(find.text('4356 W'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('la tuile ne montre que ce que la box publie', () {
|
||||
testWidgets('sans borne, elle le dit — elle ne disparaît pas',
|
||||
(tester) async {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user