From e84f59d04a9568daef01dd9b2a10c3d447e099a4 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Wed, 26 Aug 2026 18:43:54 +0200 Subject: [PATCH] =?UTF-8?q?docs(brief):=20trois=20constats=20du=20moteur?= =?UTF-8?q?=20vers=20l'app,=20dont=20un=20retir=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. SetChargingInfo remplace un targetPercentage absent par 80, EN SILENCE — réponse EnergyErrorNoError, aucun avertissement, et la seule trace au journal imprime déjà 80. L'objet est reconstruit entier à chaque écriture : un champ omis retombe sur son défaut, il n'est pas « inchangé ». L'app doit lire-patcher-réécrire, et ne pas afficher 80 comme une cible que l'utilisateur aurait choisie. 2. L'alerte sur SampleRate15Mins était FAUSSE et je la retire : ma sonde lisait currentPowerProduction là où les entrées de log portent production. Vérifié dans les deux sens — 51 échantillons non nuls par RPC, 7908 lignes en base. L'historique 90 jours n'est bloqué par rien. Le piège réel est le nom des champs, sans préfixe currentPower. 3. loads[] contient enfin les bornes (buildTelemetry les sautait) et porte un champ funding surplus/grid, sans lequel sommer allocatedW donne un écart ininterprétable. Avec la mise en garde qui va avec : adjustEvChargers() re-décide après le waterfall, donc pour une borne l'allocation publiée est une intention, pas un ordre. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H --- docs/BRIEF_depuis_plugin.md | 149 ++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 docs/BRIEF_depuis_plugin.md diff --git a/docs/BRIEF_depuis_plugin.md b/docs/BRIEF_depuis_plugin.md new file mode 100644 index 0000000..f9565a2 --- /dev/null +++ b/docs/BRIEF_depuis_plugin.md @@ -0,0 +1,149 @@ +# Brief — du moteur vers l'app + +Ce que l'agent plugin (`etm-powersync-energy-plugin-etm`) constate et qui **change quelque +chose côté app**. Une section datée par lot. Le plus récent en haut. + +--- + +## 2026-08-26 — passage machine 3g-1 sur le banc `.75` + +**Contexte** : `powersync-energy-plugin-nymea 1.15.2+etm22` déployé sur `.75`. Relevé complet +dans le dépôt du plugin, `docs/RELEVE_3g1.md`. + +--- + +### 1. `SetChargingInfo` substitue `targetPercentage = 80` à un champ absent — **en silence** + +**Envoyer un `chargingInfo` sans `targetPercentage` ne laisse pas la valeur en place : elle est +remplacée par 80.** Constaté en restaurant le banc — le champ valait `0`, j'ai réécrit l'objet +sans lui, il valait `80` à la relecture. + +**Cause** : `ChargingInfo` est reconstruit intégralement à chaque écriture (`unpack`), +et son membre C++ porte `m_targetPercentage = 80` comme valeur par défaut. Un champ absent du +JSON n'est donc pas « inchangé » : il **retombe sur le défaut**. Cela vaut pour tout l'objet, +`targetPercentage` n'est que le cas où le défaut n'est pas neutre. + +**Et la substitution est muette.** Vérifié sur machine, journal du banc à 17:35:56 : + +- la réponse RPC est `EnergyErrorNoError` — aucune erreur, aucun avertissement ; +- le seul avertissement du chemin d'écriture ne se déclenche que pour `targetPercentage > 100` ; +- la seule trace au journal est une ligne de **debug** qui imprime `Target percentage: 80`, + c'est-à-dire le **résultat** — jamais le fait qu'un champ était absent ; +- la notification `ChargingInfoChanged` qui suit porte 80, comme si l'utilisateur l'avait choisi. + +> **Ce que l'app doit en faire.** Toujours envoyer `targetPercentage`, même inchangé — donc +> lire l'objet, patcher le champ voulu, réécrire l'objet complet (le même chemin que pour +> `priority` dans `LoadConfig`). Et **ne jamais afficher `targetPercentage` comme une cible +> choisie par l'utilisateur** tant que l'écran n'a pas lui-même écrit cette valeur : 80 peut +> être une valeur héritée d'une écriture qui ne parlait pas de cible du tout. + +Le risque concret : un écran qui change le seul mode de charge repose une cible de 80 % à +l'insu de l'utilisateur, sans que rien ne le signale. + +--- + +### 2. `GetPowerBalanceLogs` en `SampleRate15Mins` — **RIEN À SIGNALER, l'alerte était fausse** + +J'avais signalé que ce taux rendait des échantillons tous à zéro. **C'était mon erreur de +lecture, pas un défaut de la box.** Je la consigne quand même pour qu'elle ne circule pas. + +Ce qui s'est passé : ma sonde lisait les clés `currentPowerProduction` / `currentPowerConsumption` +alors que les entrées de log portent **`production`, `consumption`, `acquisition`, `storage`** — +sans le préfixe `currentPower`, contrairement à `GetPowerBalance`. Tout tombait donc sur le +défaut `0`. + +**Vérifié après coup, dans les deux sens :** + +- par RPC, mêmes bornes de temps, avec les bonnes clés : **51 échantillons, 51 non nuls** ; +- dans la base du banc (`/var/lib/nymea/energylogs.sqlite`), la table `powerBalance` porte + **7 908 lignes à `sampleRate = 15`**, du 2026-06-05 à aujourd'hui 16:30, valeurs réelles. + Les autres taux (1, 60, 180, 1440, 10080, 43200) sont peuplés aussi. + +> **Conséquence pour l'app : l'historique 90 jours n'est bloqué par rien de ce côté.** Le seul +> piège est le nom des champs — **les entrées de log ne portent pas le préfixe `currentPower`**, +> à la différence de `GetPowerBalance`. Un modèle qui réutilise les mêmes clés pour les deux +> appels lira des zéros sans la moindre erreur. C'est exactement ce qui m'est arrivé. + +--- + +### 3. Contrat neuf — `loads[].funding`, et les bornes entrent enfin dans `loads[]` + +#### 3.1 Les bornes franchissent la frontière RPC (`+etm22`) + +Jusqu'ici `buildTelemetry()` **sautait les bornes** : elles étaient arbitrées, leurs décisions +apparaissaient au journal du plugin, mais `GetLoadTelemetry` n'en publiait aucune. L'écran ne +pouvait donc pas les voir. C'est corrigé. + +`loads[]` contient désormais les bornes de recharge, avec leur `decision`, leur `mechanism` +(`kind: "evcharger"`, `chargingEnabled`, `currentA`, `phaseCount`, `pluggedIn`) et leur +`faultCode` le cas échéant. + +Les motifs EV **`EV_SURPLUS`, `EV_ECO_MIN`, `EV_IDLE` ne sont plus émis** : une borne reçoit +maintenant les motifs communs à tous les mécanismes — `SURPLUS_SETPOINT`, `BELOW_MIN_POWER`, +`SURPLUS_INSUFFICIENT`, `LOAD_UNAVAILABLE`. Les trois codes restent rendus côté plugin pour +qu'un journal ancien reste lisible, mais plus rien ne les produit. `EV_SPOT_MARKET` et +`EV_DEADLINE` subsistent. + +#### 3.2 Le champ `funding` — `"surplus"` ou `"grid"` + +**Nouveau, optionnel (`o:`), présent dès qu'il y a un plan.** Il devient nécessaire du seul fait +que les bornes sont dans `loads[]`, car deux d'entre elles peuvent y être pour des raisons +différentes : + +| `funding` | d'où vient l'allocation | où elle est comptée | +|---|---|---| +| `"surplus"` | la cascade (waterfall PV) | dans `budget.allocatedW` | +| `"grid"` | le proxy — échéance de départ, tarif dynamique — qui **soutire au réseau** | dans `budget.evReservedW` | + +> **Sommer `loads[].allocatedW` sans filtrer sur `funding == "surplus"` donne un écart avec +> `budget.allocatedW` que rien ne permet d'interpréter** : défaut du moteur, ou borne servie au +> réseau ? L'identité à vérifier est donc : **somme des `allocatedW` des charges financées au +> surplus == `budget.allocatedW`**. + +**Omis en mode dégradé** : sans plan, il n'y a pas de financement. Règle maison habituelle — +omis, jamais nul. + +#### 3.3 ⚠️ Mise en garde — pour une borne, l'allocation publiée est une INTENTION, pas un ordre + +**`adjustEvChargers()` re-décide le surplus après le waterfall et passe outre le refus de +l'arbitre.** Observé sur le banc, deux lignes consécutives du même cycle (17:34:00) : + +``` +[Arbitre] "{09e520fd…}" → "Budget 1096 W sous le premier palier … — 0 W" +---------------- Adjusting chargers ---------------- +Executing action Simulated wallbox to power: ON, Charging current: 6A, Issuer: Surplus +``` + +L'arbitre refuse et publie `BELOW_MIN_POWER` ; le chemin d'exécution du proxy commande `ON` à +6 A une milliseconde plus tard. **Tant que ce n'est pas traité (lot 3g-2, côté plugin), ce que +la télémétrie publie pour une borne n'est pas ce qui est commandé.** + +> **Ce que l'app doit en faire.** Pour les bornes — et seulement pour elles —, ne pas présenter +> `allocatedW` et son motif comme l'état de la borne. Ce sont **la décision de l'arbitre**, ce +> qui n'est pas la même chose que ce que la borne fait. L'état réel se lit dans +> `mechanism.chargingEnabled` / `mechanism.currentA`, qui viennent du Thing. +> +> Concrètement : ne pas écrire « la borne ne charge pas parce que le budget est sous le +> plancher » à partir du seul motif. Les deux peuvent se contredire aujourd'hui, et c'est +> l'écran qui aurait l'air faux. +> +> Cette réserve **ne vaut pas** pour l'ECS, la PAC et les autres charges pilotées : là, +> l'allocation publiée **est** la commande. + +--- + +### 4. Rappel — clés ARB à prévoir côté app + +Deux motifs sont émis par le banc et n'ont pas encore de rendu français dans `telemetry_text.dart` : + +- **`BELOW_MIN_POWER`** — params `budgetW`, `minPowerW`, et `state` pour les mécanismes à états + (SG-Ready). Déjà traité dans `telemetry_text.dart`, à vérifier côté ARB. +- **`BATTERY_RESERVE`** — params **`socPercent`, `reservePercent`, `withheldW`**. Le + `kCodesReserveBatterie` de `telemetry_text.dart` est encore **vide**, et les clés qu'il + cherche (`batteryLevel`/`soc`, `threshold`/`batteryLevelConsideration`) **ne sont pas celles + publiées**. À aligner sur les trois noms ci-dessus. + +Et un réglage à exposer dans l'écran installateur : **`batteryLevelConsideration`** — son défaut +d'usine est passé de 0,9 à **0,2** (à 0,9 avec une batterie à 50 %, le HEMS ne pilote plus rien, +sans erreur ni panne). En revanche **`minimalChargingCurrent` ne doit jamais être présenté comme +configurable** : il n'existe pas comme réglage, il se déduit de la borne et de la voiture.