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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
150 lines
8.0 KiB
Markdown
150 lines
8.0 KiB
Markdown
# 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<ChargingInfo>`),
|
|
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.
|