From 4b42b732dc4c6b3a1643f70b9dfb5e9084f9c38b Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Thu, 27 Aug 2026 07:39:12 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20le=20contrat=20de=20recharge=20et=20les?= =?UTF-8?q?=20deux=20pi=C3=A8ges=20de=20t=C3=A9l=C3=A9m=C3=A9trie,=20plus?= =?UTF-8?q?=20le=20retour=20au=20moteur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md portait un exemple de SetChargingInfo qui invitait à l'écriture partielle. Il porte maintenant la règle inverse, avec la preuve machine, et les deux pièges de lecture de GetLoadTelemetry : ne pas sommer allocatedW sans filtrer sur funding, et ne pas lire l'allocation d'une borne comme son état. Retour au moteur dans BRIEF_plugin_depuis_maquettes.md. Un point actionnable chez eux : la description trompeuse vit dans nymeaenergyjsonhandler.cpp:114 et part dans Introspect chez tout client. Elle ne se contente pas de ne pas prévenir, elle autorise explicitement l'erreur — le prochain client la croira. Une ligne à corriger, ou une écriture à rendre réellement partielle. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq --- CLAUDE.md | 40 +++++++++++++++++++ docs/BRIEF_plugin_depuis_maquettes.md | 56 +++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 6fbe624..53e2c1b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,6 +52,19 @@ nymeaService.call('NymeaEnergy.SetChargingInfo', { }); ``` +> 🔴 **`SetChargingInfo` n'est PAS une écriture partielle**, malgré ce que dit son propre +> `Introspect` (« Only given properties will be set, others will be untouched »). +> `ChargingInfo` est reconstruit intégralement à chaque appel : un champ **absent du JSON +> retombe sur le défaut C++**, et celui de `targetPercentage` vaut **80**. Muet — réponse +> `EnergyErrorNoError`, et la `ChargingInfoChanged` qui suit porte 80 comme si +> l'utilisateur l'avait choisi. Reproduit sur `.75` le 2026-08-27 (`+etm22`) : le champ +> valait `0`, un appel portant le seul `chargingMode` l'a laissé à `80`. +> +> **Donc : lire (`GetChargingInfos`), patcher, réécrire l'objet complet** — sans +> `chargingState`, qui est `r:` et ferait rejeter l'appel. Et **ne jamais afficher +> `targetPercentage` comme une cible choisie par l'utilisateur** tant que l'écran ne l'a +> pas lui-même écrite : 80 peut être l'empreinte d'une écriture qui ne parlait pas de cible. + `chargingState` est en **lecture seule** (`r:`) dans la charge utile : `ChargingStateIdle` · `ChargingStateSurplusCharging` · `ChargingStateSpotMarketCharging` · `ChargingStateTimeRequirement`. @@ -108,6 +121,7 @@ Min+PV + échéance → ChargingModeEcoMinWithTargetTime + targetPercentage + | `GetLoadConfig` | Lire les charges déclarées | Community | | `SetLoadConfig(loadConfigs[])` | Écrire — **remplace TOUT l'ensemble**, refus en bloc | Community | | `GetLoadTelemetry` | État runtime de l'arbitrage (budget, allocation, verrous, motifs) | Community | +| — | Depuis `+etm22`, `loads[]` contient **aussi les bornes** (`mechanism.kind = "evcharger"`) et chaque entrée porte `o:funding` (`"surplus"` / `"grid"`) | — | | `ClearLoadFault(loadId)` | Lever le verrou de défaut — **l'ack ne prouve rien** | Community | **Recharge EV** @@ -233,6 +247,32 @@ batteryGreen boostRed pvGreen minPvBlue accentTeal --- +## Télémétrie d'arbitrage — deux pièges de lecture + +**1. `funding` — ne jamais sommer `allocatedW` sans filtrer.** Depuis `+etm22` une borne +peut figurer dans `loads[]` au titre du surplus **ou** du réseau (échéance, tarif +dynamique). L'identité vérifiable est : *somme des `allocatedW` financés au surplus == +`budget.allocatedW`* ; une borne financée au réseau vit dans `budget.evReservedW`. Sommer +sans filtrer donne un écart que rien ne permet d'interpréter. Omis en mode dégradé — sans +plan, pas de financement. Cf. `LoadTelemetry.surplusAllocatedW`. + +**2. ⚠️ Pour une BORNE, `allocatedW` et son motif sont une INTENTION, pas un ordre.** +`adjustEvChargers()` re-décide le surplus après le waterfall et passe outre le refus de +l'arbitre : relevé sur `.75`, l'arbitre publie « 0 W / `BELOW_MIN_POWER` » et le proxy +commande `ON` à 6 A dans le même cycle. L'état réel se lit dans +`mechanism.chargingEnabled` / `mechanism.currentA`, qui viennent du Thing. **Ne pas écrire +« la borne ne charge pas parce que le budget est sous le plancher » à partir du seul +motif.** La réserve **ne vaut pas** pour l'ECS et la PAC, où l'allocation publiée **est** +la commande. Elle tombe avec le lot plugin **3g-2** ; côté app, elle est portée par +`LoadTelemetryEntry.allocationIsCommand`. + +**Motif `BATTERY_RESERVE`** — params `socPercent`, `reservePercent`, `withheldW`, tous +entiers **déjà en pourcentage** (ne pas confondre `reservePercent` = 40 avec +`batteryLevelConsideration` = 0,4). Le défaut d'usine de ce réglage est passé de 0,9 à +**0,2**, mais une box déjà déployée garde la valeur qu'elle a persistée. + +--- + ## Règles de modification - Tout nouvel écran → valider maquette avec Patrick avant de coder - Tout nouvel appel RPC → vérifier dans `INTERFACE.md` que la méthode existe diff --git a/docs/BRIEF_plugin_depuis_maquettes.md b/docs/BRIEF_plugin_depuis_maquettes.md index 797d436..12e7cfe 100644 --- a/docs/BRIEF_plugin_depuis_maquettes.md +++ b/docs/BRIEF_plugin_depuis_maquettes.md @@ -1,5 +1,61 @@ # Brief plugin — ce que les maquettes « Configurer » demandent au moteur +--- + +## 2026-08-27 — retour sur le brief 3g-1 (`docs/BRIEF_depuis_plugin.md`) + +Les quatre points sont traités côté app. Deux constats en retour, dont un actionnable chez +vous. + +### A. `SetChargingInfo` — la box PROMET l'écriture partielle, par écrit + +`nymeaenergyjsonhandler.cpp:114` publie cette description, qui part dans +`JSONRPC.Introspect` et donc chez tout client : + +> *« Set the charging info for an EV charger. Only given properties will be set, others +> will be untouched. »* + +C'est l'inverse du comportement réel — un champ omis retombe sur son défaut C++. La phrase +ne se contente pas de ne pas prévenir : **elle autorise explicitement l'erreur**. Un client +qui la lit écrit un champ à la fois, en toute bonne foi, et repose 80 % de cible à chaque +changement de mode. + +Reproduit ici le 2026-08-27 sur `.75` (`+etm22`), borne `88160e45` : +`targetPercentage` valait `0` ; un `SetChargingInfo` portant le seul `chargingMode` l'a +laissé à **`80`**, réponse `EnergyErrorNoError`. Banc restauré. + +**Demande :** corriger la description (une ligne), ou rendre l'écriture réellement +partielle. La première suffit — l'app fait déjà lire-patcher-réécrire. Tant que la phrase +est là, le prochain client la croira. + +### B. `BATTERY_RESERVE` — la garde `withheldW > 0` fait bien son travail + +En cherchant à observer le motif sur `.75`, la bascule du seuil à 0,9 a produit exactement +le cas que votre garde couvre : `surplusW` à **−3 551 W**, donc rien à confisquer, donc +**aucun `BATTERY_RESERVE` publié** — les charges sont sorties en `SURPLUS_INSUFFICIENT` et +`BELOW_MIN_POWER`, ce qui est la vérité. Le motif n'a donc pas pu être vu en vrai d'ici : +il faut du surplus RÉEL sous le seuil, et le banc importait au moment de l'essai. Seuil +remis à 0,4. + +Le rendu français côté app est aligné sur `socPercent` / `reservePercent` / `withheldW` et +mène par les watts retenus, comme votre `decisionreason.cpp`. + +### C. Ce que l'app dépend désormais de 3g-2 + +L'écran affiche l'état réel d'une borne (`mechanism.chargingEnabled` / `currentA`) **avant** +l'allocation, et étiquette celle-ci « décision de l'arbitre ». C'est un contournement, pas +une conception : il existe uniquement parce que les deux peuvent se contredire. Le jour où +`adjustEvChargers()` ne commande plus le surplus, `allocationIsCommand` rend `true` partout +et ces branches disparaissent. Prévenez-nous au déploiement. + +### D. Méthode + +`origin` est basculé en SSH (`gitea-lan:pakutz79/etm-powersync-app.git`). La forge était +injoignable au moment du lot — `192.168.1.113` sans route, front public en 503 — donc les +commits d'hier et celui-ci attendent en local. Ils partent dès qu'elle répond. + +--- + **Origine :** session du 2026-08-25, côté app. Écrit après le passage sur appareil et la conception des six écrans « Configurer » (`docs/mockups/`). **Cible :** `etm-powersync-energy-plugin-etm`, plugin déployé au moment du relevé :