`1.15.2+etm23` construit arm64, posé sur `.75` par deploy.sh, exercé.
FRANCHI, journal et RPC à l'appui :
- Une charge, un commandeur. 9 lignes « Executing action Simulated wallbox »
sur la fenêtre, 9 AVANT « Adjusting chargers » (dispatch de l'arbitre), 0
après (proxy). 6 cycles sur 6 portent « is commanded by the ETM waterfall
this cycle. Not re-deciding here. » Le doublet du relevé 3g-1 §3.3 a
disparu, et quelqu'un commande — l'arbitre a bien allumé la borne à 10 A
sur sa propre décision.
- Le plancher publié est l'exigence réelle : 2 070 W sous +etm22, 4 140 W
sous +etm23, MÊME borne, MÊME journée. Et il suit le nombre de phases visé
d'un cycle à l'autre (4 140 en triphasé, 1 380 en monophasé).
- L'entrée evcharger est créée d'office, journalisée une fois par borne,
sans charge utile ; l'aller-retour GetLoadConfig → SetLoadConfig verbatim
est neutre et ne reconstruit aucun adaptateur ; loads[] ⊆ GetLoadConfig
est rétabli.
TROUVÉ, et c'est le point du passage machine :
- GRAVE — une borne SANS VÉHICULE BRANCHÉ absorbe le budget de la cascade.
4 853 W alloués à une wallbox `pluggedIn: false` qui tire 0 W, la PAC
derrière ne survivant que sur son propre recrédit. Trois cycles
consécutifs, 2 350 / 4 853 / 5 464 W donnés à personne. C'est le défaut du
relevé 3g-1 §3.4 par une autre porte : là le plancher mentait, ici c'est le
filtre d'entrée. evParticipatesInWaterfall() reprend le filtre du proxy —
voiture assignée, mode non manuel — mais pas pluggedIn. Le proxy n'en avait
pas besoin : son allocation n'était pas une cascade. Défaut de 3g-1,
correction PROPOSÉE ET NON APPLIQUÉE, à traiter dans son propre commit.
- Mineur — le rang par défaut des bornes auto-provisionnées retombe à
ÉGALITÉ avec le rang 1 déjà occupé, au lieu de « en tête ». Le tri reste
total et reproductible, mais ce n'est pas ce qui était demandé.
NON OBSERVÉ, et pas fabriqué : EV_GRID_START (le budget est passé de 385 W à
2 350 W sans s'arrêter dans la fenêtre de tolérance), BATTERY_RESERVE avec un
surplus réel, LM-1204-b matériel (la Terra AC ne répond pas à l'ARP depuis le
banc lui-même), et le cas de transition — aucune borne n'était sous commande au
moment de la bascule.
Banc rendu à son état d'origine : ChargingInfos identiques champ par champ,
voiture de test retirée, wallbox éteinte. Les entrées evcharger restent — elles
seraient recréées au prochain démarrage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
13 KiB
RECAP — 3g-2 : une charge, un commandeur
Écrit le 2026-08-27. Branche feature/beta-rulebased.
DÉPASSÉ SUR SA MOITIÉ MACHINE le 2026-08-27 :
1.15.2+etm23a été construit, posé sur.75et exercé. Ce qui a été observé — et les deux défauts que le passage a trouvés — est dansdocs/RELEVE_3g2.md, qui fait autorité sur le comportement machine. Ce document-ci reste la description du LOT : ce qu'il corrige, et pourquoi.Ce document n'est PAS un relevé machine. Tout ce qui suit a été éprouvé en simulation et par lecture de code. La distinction machine / test / lecture est celle de
docs/RELEVE_LOTD.md, et la mélanger est ce qui a envoyé un agent dans le vide le 2026-08-25.
1. La Terra AC, vérifiée avant de commencer
Elle ne répond pas, et l'obstacle est PHYSIQUE, pas logiciel.
| Vérification | Résultat |
|---|---|
ping 192.168.1.132 depuis le poste de dev |
Destination Host Unreachable |
ping depuis le banc .75, même segment L2 |
Destination Host Unreachable |
Table ARP du banc pour .132 |
INCOMPLETE — aucune réponse ARP |
| TCP 502 depuis le banc | No route to host |
Balayage complet de 192.168.1.0/24 depuis le banc |
rien en .132 ; aucune autre adresse ne porte un Modbus TCP inconnu (502 ouvert seulement en .103, .210, .211) |
Integrations.GetThings sur .75 |
Terra AC Charger (TCP) en ThingSetupStatusInProgress — elle retente sans fin |
Une machine qui ne répond pas à l'ARP est hors du réseau : éteinte, débranchée, ou sur un autre segment. Rien de ce que fait ce lot ne peut la joindre. LM-1204-b garde donc sa moitié matérielle non franchie, comme au relevé du 2026-08-26 — ce qui est levé ici, c'est sa moitié ARBITRAGE (§3).
2. Ce que le lot corrige, dans l'ordre où le banc l'avait signalé
2.1 La double commande (RELEVE_3g1.md §3.3) — le plus grave
adjustEvChargers() re-décidait le surplus après le waterfall et passait outre le refus de
l'arbitre. La conséquence, énoncée sans détour dans le relevé : tant que ceci n'est pas traité,
la transplantation ne commande rien.
Corrigé par les deux bouts :
- les actions EV du waterfall sont dispatchées vers
EvAdapter::applyAction()— elles ne l'étaient pas du tout ;m_loadAdaptersne contient pas les bornes, et le dispatch s'arrêtait là ; - les bornes que le waterfall a commandées sortent du chemin proxy, par deux crochets
virtuels posés dans l'amont (
evSurplusPlannedByWaterfallpourplanSurplusCharging,evCommandedByWaterfallpouradjustEvChargers). Deux et non un : ils sont interrogés à deux moments du cycle où la réponse n'est pas la même — avant le plan on sait seulement à qui la borne appartient, après on sait ce qui a été commandé.
Le jeu des bornes commandées est nommé par le scheduler (Plan::waterfallEvIds), jamais
redéduit : un critère redérivé plus tard (« motif différent d'EV_DEADLINE… ») dériverait du
vrai au premier motif ajouté — et EV_GRID_START porte justement funding: grid tout en venant
du waterfall.
Effet de bord traité : chargingState était posé par adjustEvChargers(). Les bornes qui n'y
passent plus le reçoivent de l'arbitre (setEvChargingState), sans quoi l'app afficherait « en
charge » une borne que l'arbitre vient d'éteindre.
2.2 evFloorW() sous le seuil de démarrage réel (RELEVE_3g1.md §3.4)
evFloorW() valait min × acquisitionTolerance × 230 × phases : 2 070 W annoncés pour une
borne qui en exige 4 140. Le waterfall allouait 2 828 W, l'adaptateur convertissait en 4,1 A,
refusait d'enclencher sous 6 A, et 2 828 W étaient retirés du budget pour personne.
Le plancher est désormais l'exigence réelle : min × 230 × phases. La tolérance n'y est
plus — ce sont deux grandeurs différentes, et les confondre publiait un plancher qui n'existe
nulle part.
La tolérance n'a pas disparu pour autant. Elle est devenue ce qu'elle était déjà chez le
proxy — un seuil de déclenchement — mais visible : motif EV_GRID_START {budgetW, floorW, gridW}, funding: grid, comptabilité partagée entre budget.allocatedW et
budget.evReservedW. Brèche assumée et bornée, écrite dans docs/DESIGN_3g.md §3.1-bis.
2.3 Le rang de borne (RELEVE_3g1.md §3.2)
priority = 100 en dur, pour toutes : même clé de tri, std::sort non stable, et laquelle
démarre n'était ni réglable ni reproductible.
- Le rang vit dans
LoadConfig.priority, comme celui de toute autre charge (LM-1206/LM-1207 : pas de second système de priorité, sinon « VE1 > ECS > VE2 » devient inexprimable). - Toute borne détectée reçoit d'office une entrée
evcharger— rang, libellé,domain: ev, et aucune charge utile. Création journalisée, une ligne par borne, à la création seulement. - Le tri est total : à rang égal, l'identifiant départage.
- L'entrée survit à son Thing : borne remplacée ou déposée, elle reste publiée
available: false/faultCode: THING_MISSING/ motifLOAD_UNAVAILABLE. La supprimer d'office rendrait « borne remplacée » indistinguable de « borne jamais configurée ».
Et cela rétablit loads[] ⊆ GetLoadConfig, que 3g-1 avait rompu sans le dire : les bornes
étaient arbitrées et publiées sans figurer dans aucune configuration — la charge fantôme du lot
B-bis, réapparue par l'autre bout.
3. Ce qui est ÉPROUVÉ, et par quoi
Suite complète, un processus par test (AGENTS.md) : simulation 27/27 (dont les 16 scénarios
run), charging 16/16, loadmodel 16/16, spotmarket 7/7. Build amd64 0 erreur / 0 warning.
| Ce qui est prouvé | Par |
|---|---|
| Le rang décide qui charge, et l'inverser change la borne servie — moitié arbitrage de LM-1204-b | testEvChargerRankDecidesWhoCharges |
Le discriminant est le MOTIF : BELOW_MIN_POWER {budgetW: 620, minPowerW: 1380} sur la borne refusée |
idem |
| La borne refusée est éteinte, et le proxy ne repasse pas derrière | idem |
Aller-retour GetLoadConfig → SetLoadConfig verbatim : rien ne change, rien n'est reconstruit |
idem |
loads[] ⊆ GetLoadConfig par la frontière RPC |
idem |
EV_GRID_START publie gridW et la comptabilité se partage (budgetW + gridW == allocatedW) |
testEvGridStartIsAnnouncedAndBounded |
| Au plus une borne soutire par cycle | idem |
L'entrée survit au Thing, publiée THING_MISSING |
testEvChargerConfigOutlivesItsThing |
Une entrée evcharger refuse toute charge utile, et revendique son Thing |
testEvChargerConfigIsRankOnly, …RefusesPayloads, …ClaimsItsThing |
| Le TEXTE des motifs neufs, catalogue refermé | testDecisionReasonRendering |
Le catalogue de motifs avait dérivé : BATTERY_RESERVE et BELOW_MIN_POWER étaient entrés
en 3g-1 sans ligne de rendu attendue ni entrée dans la liste de clôture. Leur texte — celui sur
lequel le banc s'appuie — n'était donc éprouvé par rien. Réparé.
4. Ce que le lot a fait apparaître, et qui n'était pas dans la commande
Ces quatre points ne se sont vus que parce que les décisions du waterfall atteignent enfin le matériel. Trois auraient produit des défauts silencieux en service.
- La réserve de stockage ne mordait que sur les augmentations. Le recrédit anti-clignotement
rendait à chaque charge sa propre consommation, y compris sous la réserve : une charge en
marche le restait, pendant que le motif publiait « aucune charge n'est servie ». Sous la
réserve, le budget est une DÉCISION de retenir, pas une mesure — le recrédit y est supprimé
(LM-1203-b : « annulé, pas réduit »), et
withheldWchiffre les deux termes retenus. Attrapé parbatteryLevelConsiderationStopCharging, qui échouait. - Le prédicat de bascule de phases avait divergé.
canSetPhaseCount()dit seulement que la borne porte l'étatdesiredPhaseCount; la vraie question — borne et voiture — est tranchée par le manager. Une borne monophasée se voyait viser 3 phases, son plancher publié triplait, et sous surcharge elle commandait 3 × 6 A là où le proxy commandait 1 × 6 A. - Le courant de référence du choix de phases était le courant actuel et non le courant possible : une borne en triphasé y restait quel que soit l'effondrement du surplus.
- L'écrêtage par la limite de phase avait été perdu en 3g-1 : il vivait dans
planSurplusCharging(), que la borne ne traverse plus. Sans conséquence tant que rien n'était exécuté — 3g-2 exécute. Le plafond est de nouveau déclaré (LoadContext.declared.maxPowerW), donc respecté avant d'allouer ; il ne descend jamais sous le plancher de la borne, parce que couper est le geste de la couche L4 et non celui de la stratégie.
5. Ce qui n'est PAS prouvé, et qu'il ne faut pas déclarer vérifié
- Rien n'a tourné sur machine. Pas de paquet arm64, pas de déploiement.
- LM-1204-b, moitié matérielle : deux bornes réelles. La Terra AC est absente du réseau (§1). Une wallbox simulée ne prouve ni le délai d'établissement, ni la négociation de courant, ni ce qu'une borne fait d'une consigne qu'elle refuse.
BATTERY_RESERVEavec un surplus réel : la bascule à 0,9 du 2026-08-26 a donné un surplus de −3 551 W — rien à confisquer,withheldW: 0, garde active et motif tu. C'est le comportement correct. Il faut du soleil et un SoC sous le seuil : à guetter, pas à fabriquer.- Le démarrage sous tolérance sur du matériel réel : la simulation prouve la décision et sa comptabilité, pas qu'une borne accepte de démarrer avec 480 W soutirés.
- arm64 : non compilé (infra CI
etm-powersync-deploy).
6. Écarts de simulation assumés, et pourquoi
14 des 16 scénarios run sont identiques au bit près à la référence. Deux points de contrôle
ont changé, dans le même scénario et par la même cause :
| Scénario | Point | Avant | Après |
|---|---|---|---|
simulation-1-phase-32A |
pas 444 | 7 A | 6 A — borne à l'ARRÊT : c'est la dernière consigne émise avant la coupure |
simulation-1-phase-32A |
pas 900 | 88 % / 30 A | 87 % — un ampère par-ci par-là sur 900 pas de rétroaction |
simulation-2-phase-16A |
pas 900 | 16 A, en charge | 89 %, pas en charge — la projection dit qu'il reste du temps |
La cause est unique : la conversion watts → ampères se fait désormais après l'allocation du waterfall, au lieu d'être calculée nativement en ampères par le proxy. L'arrondi tombe parfois de l'autre côté.
Ce qui compte est inchangé dans les deux scénarios : la voiture est à 100 % à l'échéance
(pas 960). Le mécanisme d'échéance fonctionne — dans 2-phase-16A il se déclenche simplement
plus tard, parce que la trajectoire de la journée laisse la voiture un peu plus chargée.
Un écart a été refusé et le code a été réaligné sur l'amont plutôt que le test :
testOverloadProtectionEcoMode. Tronquer la conversion (plutôt qu'arrondir) coûtait un ampère à chaque cycle — 230 W par phase, abandonnés toute la journée — et faisait basculer la protection de surcharge de « réduire au minimum » à « couper ». L'arrondi au plus proche est celui du proxy ; l'enveloppe est une allocation issue d'une mesure dont le bruit dépasse 115 W, pas une limite. La limite, elle, est le plafond déclaré, et il est respecté avant.
7. Pour reprendre
# 1. Construire le paquet arm64 (conteneur LXD build-cross-arm64, cf. AGENTS.md)
# 2. Déployer — JAMAIS un dpkg -i à la main :
cd ~/projects/etm-powersync/etm-powersync-deploy
./deploy.sh --dry-run powersync-energy-plugin-nymea_1.15.2+etmNN_arm64.deb # puis sans --dry-run
À vérifier en priorité au banc, dans cet ordre :
- Les entrées
evchargerapparaissent dansGetLoadConfigau premier cycle, une ligne de journal par borne. Vérifier qu'aucune configuration existante n'a été bousculée. - Une seule commande par borne et par cycle. Le journal ne doit plus montrer une ligne
[Arbitre]suivie d'unExecuting action … Issuer: Surplusqui dit autre chose. GetLoadTelemetrypublie les bornes avec leur rang, et la somme desallocatedWfinancés au surplus retombe surbudget.allocatedW.- Le plancher publié d'une borne branchée :
min × 230 × phases, sans facteur de tolérance. Sur la wallbox simulée du banc, attention à sondesiredPhaseCount: 3permanent. EV_GRID_STARTquand le surplus est entre la moitié et la totalité du plancher : le motif doit chiffrergridW, et une seule borne doit soutirer.- LM-1204-b dès qu'une seconde borne réelle répond.