4 Commits

Author SHA1 Message Date
Patrick Schurig
2c9cca042e 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
2026-08-30 13:43:31 +02:00
Patrick Schurig
51a1ab929c feat(config): retirer une charge orpheline, et régler le mode de recharge à un seul endroit
Deux manques trouvés en usage réel sur téléphone, contre .75.

── 1. Retirer une charge dont l'appareil n'existe plus ────────────────────────

Le plugin crée d'office une entrée par borne détectée mais ne la retire pas quand le Thing
s'en va : la Terra AC supprimée de nymea tenait encore le rang 4 deux jours plus tard, et
l'app savait le dire sans savoir y remédier.

Le bouton n'apparaît que sur THING_MISSING. Sur une charge vivante le geste n'aurait pas de
sens — le plugin recrée l'entrée au cycle suivant, et un bouton qui se défait tout seul
ferait douter de la box.

Le chemin est celui qui était demandé, et pour la raison qui était donnée :
  · LIRE (GetLoadConfig, frais — pas l'état en mémoire, la box a pu changer d'avis)
  · PATCHER (payloadWithoutLoad : les autres charges recopiées VERBATIM, champ par champ)
  · RÉÉCRIRE (SetLoadConfig, qui remplace l'ensemble)

Jamais une reconstruction depuis le modèle typé : un champ qu'une box plus récente porterait
et que l'app ne connaît pas serait perdu pour TOUTES les charges d'un coup, en silence. Les
rangs ne sont pas renumérotés — le trou laissé (1,2,3,5) ne change l'ordre de personne,
alors qu'une renumérotation réécrirait le rang de charges que personne n'a touchées.

Rien à moitié écrit : un refus laisse la configuration locale intacte (aucune adoption avant
confirmation) et la box refuse l'ensemble ou l'accepte en entier. Le geste est refusé tant
qu'un brouillon est en cours — sinon la suppression emporterait des modifications non
enregistrées. La confirmation annonce ce que l'utilisateur ne peut pas deviner : retirer une
charge réécrit toutes les autres.

── 2. Le mode de recharge, un seul lieu d'écriture ───────────────────────────

Les trois boutons quittent la tuile des favoris pour l'écran « Configurer » de la borne :
c'est l'écran de mise en service, celui où on découvre qu'une borne neuve est en
ChargingModeNormal. Deux endroits qui écrivent SetChargingInfo seraient deux comportements
qui divergent.

La tuile des favoris garde la LECTURE et l'avertissement « hors arbitrage » : c'est le
client qui ouvre ses favoris, et il doit voir que sa borne n'est pas pilotée même s'il doit
aller ailleurs pour le corriger.

⚠️ Bug latent trouvé en déplaçant : setChargingInfo écrivait toujours sur la PREMIÈRE borne
configurée (_findEvChargerId). Sur le banc qui en porte trois, régler la V2C depuis son
propre écran aurait réglé la wallbox simulée. La borne est maintenant visée explicitement,
et evChargerFor() réconcilie les deux graphies d'identifiant (§9 du contrat de données).

181 tests (+12), flutter analyze inchangé à 27 remarques.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
2026-08-28 14:36:50 +02:00
Patrick Schurig
e0264b5ed6 feat(ev): une borne à pleine puissance n'est pas arbitrée — les deux écrans le nomment
Geste de mise en service, signalé par le moteur : toute borne détectée reçoit d'office
ChargingModeNormal, c'est-à-dire pleine puissance HORS arbitrage. Une borne neuve n'est
donc jamais pilotée tant que personne n'a posé un mode éco, et rien ne le disait —
l'installateur pose une borne, elle charge à fond, et il cherche du côté du HEMS un défaut
qui n'existe pas.

La note apparaît aux deux endroits qui comptent, mot pour mot (kEvUnmanagedNote) :
  · la carte de configuration, celle qu'on ouvre à la mise en service ;
  · la tuile des favoris, où les trois boutons de mode sont juste à côté.

⚠️ Elle nomme l'ÉTAT, pas une négligence. La box publie le même chargingMode pour un boost
délibéré et pour une borne jamais réglée : l'app ne peut pas les distinguer, donc elle dit
« pleine puissance, hors arbitrage » et rappelle que c'est aussi le mode d'office. 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. Un mode NON LU ne déclenche rien non plus : on ne
conclut pas d'une absence.

Le piège qui aurait rendu la note invisible est couvert par un test : GetLoadConfig rend
l'id de charge AVEC accolades ({4d1adafd-…}), GetChargingInfos rend l'evChargerId SANS.
Comparer les deux tels quels ne rend jamais rien — en silence : l'écran aurait conclu
« mode non lu » sur une borne parfaitement lue. chargingModeOf() réconcilie les deux.

Six tests montés (trois par écran), dont les deux contrôles négatifs.

Et la réserve sessionEnergy est documentée à sa juste place : le champ est CONSTRUIT par
le plugin V2C 1.15.2+etm13 — il accumule les incréments de chargeEnergy, détecte les
remises à zéro, ne repart qu'à l'ouverture d'une session — donc l'égalité observée hier
s'explique d'elle-même. La réserve tient jusqu'à la trace que la campagne produira
(chargeEnergy à zéro, sessionEnergy qui continue) : aucun avancement affiché d'ici là.

Noté aussi, pour la suite : sessionEnergy n'est PAS historisé (suggestLogging écrasé par
things.conf sur un Thing déjà appairé ; la V2C n'y a que connected, charging, power). La
valeur courante est juste, il n'y aura pas de courbe — le choix des états enregistrés est
côté app, entrée au TODO.

164 tests, flutter analyze inchangé à 27 remarques.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
2026-08-28 13:08:01 +02:00
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