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
8.0 KiB
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
ChargingInfoChangedqui 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 pourprioritydansLoadConfig). Et ne jamais affichertargetPercentagecomme 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 tablepowerBalanceporte 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 deGetPowerBalance. 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[].allocatedWsans filtrer surfunding == "surplus"donne un écart avecbudget.allocatedWque rien ne permet d'interpréter : défaut du moteur, ou borne servie au réseau ? L'identité à vérifier est donc : somme desallocatedWdes 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
allocatedWet 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 dansmechanism.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— paramsbudgetW,minPowerW, etstatepour les mécanismes à états (SG-Ready). Déjà traité danstelemetry_text.dart, à vérifier côté ARB.BATTERY_RESERVE— paramssocPercent,reservePercent,withheldW. LekCodesReserveBatteriedetelemetry_text.dartest 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.