etm-powersync-app/docs/BRIEF_depuis_plugin.md
Patrick Schurig e84f59d04a docs(brief): trois constats du moteur vers l'app, dont un retiré
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
2026-08-26 18:43:54 +02:00

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 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.