5 Commits

Author SHA1 Message Date
Patrick Schurig
7eefb93c6c docs: relevé frais sur charge réelle — sessionEnergy corrigé, chargingState confirmé
Lien Modbus rétabli, Skoda en charge, valeurs en mouvement.

- `sessionEnergy` : notre doc était EN RETARD, pas contredite. La V2C la publie
  depuis le plugin +etm13 et elle bouge (15,8015 → 15,8405 kWh). L'écart avec
  `chargeEnergy` vaut 7,2044 kWh aux deux échantillons pris à deux heures
  d'écart — constant au dix-millième : c'est l'accumulateur du plugin, et il
  prouve l'accumulation que le code seul ne montrait pas. LM-1009 C1 reste vraie
  pour les bornes sans cet état ; la V2C n'en fait plus partie. La maquette ne la
  nomme donc plus dans l'exemple « non mesurable ».
- `chargingState` reste `Idle` sur un lien VIVANT, pendant que la borne tire
  4 372 W : ce n'est pas de la péremption, personne ne maintient le champ.

Et un constat qui n'est pas dû au WiFi : la box a journalisé « Restoring manual
values of false 6 A » trois secondes après la coupure ; la borne est restée à
`power: true`, 7 A, et le lien est revenu sans que rien ne réapplique. En mode
manuel personne ne commande, donc l'écart est permanent — 4,4 kW tirés,
`draw.committedW` à 0. Question posée au moteur.

Aucun avancement n'est affiché pour autant : c'est une décision (D1), pas une
mesure qui manquerait.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-08-30 15:15:49 +02:00
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
376acb2181 docs(motif): deux réponses de la même box, deux conventions — et aucun signal
Le piège des accolades n'était pas un détail de chargingModeOf(). C'est la deuxième
occurrence en une semaine d'un motif qui se répétera : deux appels JSON-RPC de la même box,
portant le même objet, n'emploient pas la même convention, et RIEN ne le dit. Écrit comme
tel dans docs/UI_data_contract.md §9, avec renvoi depuis les deux sites de code et depuis
tools/rpc/README.md — là où quelqu'un croise deux réponses brutes.

Les occurrences, leur symptôme :
  · GetPowerBalance (currentPowerProduction…) × GetPowerBalanceLogs (production…) →
    tout tombe à zéro, l'historique paraît vide sur 7 960 échantillons bien présents
  · GetChargingInfos (evChargerId sans accolades) × GetLoadConfig (id avec) →
    « mode non lu » sur une borne parfaitement lue, et la note « hors arbitrage » muette
  · famille voisine, même silence : currentPower* en W × total* en kWh, facteur 1000

Ce qui en fait un motif et pas trois anecdotes : le symptôme n'est JAMAIS une exception,
c'est une valeur neutre plausible, et un test unitaire qui construit ses deux côtés avec la
même convention passe aussi. D'où les trois gestes écrits au §9 — sonder les deux appels
côte à côte sur les charges utiles brutes, éprouver le rapprochement sur les deux formes
réelles, et se méfier d'un vide qui ne coûte rien à expliquer.

Et le lien avec une règle qu'on suivait déjà sans la relier à ça : une valeur neutre issue
d'un croisement raté est indiscernable d'une vraie absence. « Non lu » laisse une chance de
trouver le bug ; « 0 » l'enterre.

── Campagne V2C arrêtée, et la réserve sessionEnergy avec elle ────────────────

La Trydan non-Pro est en WiFi ESP32 sans repli filaire, lien inexploitable à deux mètres du
point d'accès : 70 % de scrutations avortées, 12 bascules en injoignable en vingt minutes.
Le modèle sort du catalogue ETM ; seule la Trydan Pro (port Ethernet) y reste.

La trace qui lève C1 — chargeEnergy à zéro AVEC un sessionEnergy qui continue — n'a donc
pas été prise et ne viendra pas de cette borne. Les notes restent en place telles quelles,
avec la raison mise à jour : elles seront levées sur une Trydan Pro. Aucun avancement de
charge affiché d'ici là.

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:15:51 +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