Patrick Schurig c2f08cb804 fix(3g-1): une borne sans véhicule branché est hors arbitrage
Trouvé au banc le 2026-08-27, en exerçant 3g-2 (docs/RELEVE_3g2.md §3.1).

Une wallbox en Eco, voiture assignée, mais `pluggedIn: false`, s'est vu allouer
4 853 W qu'elle ne pouvait pas tirer — `currentPower: 0`, `charging: false`. La
PAC derrière elle n'a survécu que sur son propre recrédit. Trois cycles
consécutifs : 2 350 / 4 853 / 5 464 W retirés du budget et donnés à personne.

evParticipatesInWaterfall() reprenait le filtre d'entrée 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, donc gaspiller ne privait
personne. En cascade, si. L'asymétrie était déjà visible dans le code : la
boucle EV du scheduler, qui sert les financements réseau, teste
`!ev->pluggedIn()` depuis toujours ; seul le chemin waterfall ne le faisait pas.

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. Défaut de 3g-1, révélé par 3g-2 parce
que la décision atteint enfin le matériel — et corrigé à part du lot, comme les
quatre précédents.

Hors arbitrage, et non « servie à 0 avec un motif » : même famille que les deux
autres exclusions (DESIGN_3g §4). Une borne débranchée ne consomme rien, sa
consommation nulle est déjà au compteur, et lui donner un rang la ferait
apparaître dans la cascade pour n'y rien faire. Le journal l'annonce comme les
deux autres cas.

testUnpluggedChargerTakesNoBudget, VÉRIFIÉ ÉCHOUANT SANS LE CORRECTIF : la
borne débranchée disparaît du contexte et la charge suivante reçoit les 3 000 W
entiers ; la seconde moitié rebranche le véhicule pour prouver que c'est bien
`pluggedIn` qui décide, et non une exclusion fortuite.

Suite complète : simulation 28/28, charging 16/16, loadmodel 16/16,
spotmarket 7/7. Build amd64 0/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
2026-08-27 12:52:21 +02:00

11 KiB
Raw Blame History

Relevé machine — 3g-2 sur le banc .75, 2026-08-27

Paquet : 1.15.2+etm23, construit arm64 dans build-cross-arm64, posé par etm-powersync-deploy/deploy.sh (rollback : /var/lib/etm-deploy/archive/). Compteur : Fronius SunSpec. Bornes : Terra AC réelle (absente du réseau) + wallbox simulée.

Méthode. Ce qui suit distingue ce qui a été observé sur machine, ce qui vient d'un test unitaire, et ce qui n'est qu'une lecture de code (docs/RELEVE_LOTD.md). Tout ce qui est numéroté ci-dessous est machine, journal ou RPC à l'appui.


1. La Terra AC — toujours absente, et c'est physique

Vérification Résultat
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 ; aucun autre Modbus TCP inconnu
Integrations.GetThings Terra AC Charger (TCP) en ThingSetupStatusInProgress, en boucle

Une machine qui ne répond pas à l'ARP est hors du réseau. LM-1204-b garde sa moitié matérielle non franchie — il faut deux bornes RÉELLES, et il n'y en a qu'une.


2. Ce qui est FRANCHI

2.1 Une charge, un commandeur — le critère central du lot

Avant (+etm22, même banc, wallbox en Eco avec voiture assignée) : la borne était arbitrée, et adjustEvChargers() reprenait la main derrière.

Après (+etm23), chaque cycle porte, dans cet ordre :

[Arbitre] "{09e520fd…}" → "Surplus PV 2350 W — Simulated wallbox consigne 2350 W" | activé: true | phases: 1
Executing action Simulated wallbox to power: ON, Carging current: 10A, Switch phases to: 1, Issuer: Surplus
---------------- Adjusting chargers  ---------------
"Simulated wallbox" is commanded by the ETM waterfall this cycle. Not re-deciding here.

Et dans planSurplusCharging, un cycle plus haut : "Simulated wallbox" surplus is planned by the ETM waterfall. Skipping proxy planning.

Comptage sur la fenêtre d'observation : 9 lignes Executing action Simulated wallbox, 9 avant Adjusting chargers (donc émises par le dispatch de l'arbitre), 0 après (donc aucune dérivée par le proxy). 6 cycles sur 6 portent la ligne de saut.

Le doublet a disparu, et quelqu'un commande. C'est le critère du lot, et il est franchi.

2.2 Le plancher publié est l'exigence réelle

Sur la MÊME borne, dans la même journée :

motif publié
+etm22 BELOW_MIN_POWER {budgetW: 84, minPowerW: **2070**}
+etm23 BELOW_MIN_POWER {budgetW: 395, minPowerW: **4140**}

2 070 W était 6 A × 0,5 × 230 × 3 — le plancher multiplié par la tolérance d'acquisition. 4 140 W est 6 A × 230 × 3, ce que la borne exige réellement. Le défaut du relevé 3g-1 §3.4 est fermé, et le chiffre le montre.

Et le plancher suit le nombre de phases VISÉ, cycle à cycle : au cycle suivant, la référence de bascule ayant changé, le journal publie BELOW_MIN_POWER {budgetW: 385, minPowerW: **1380**} — soit 6 A × 230 × 1. C'est le piège le plus silencieux de DESIGN_3g §3.3, observé en train de fonctionner.

2.3 L'entrée evcharger auto-provisionnée

Au premier cycle après la bascule, une ligne par borne, à la création seulement :

[EnergyArbitrator] Borne "Simulated wallbox" ajoutée à la configuration des charges
                   (rang 1 , domaine ev) — entrée créée automatiquement, modifiable par SetLoadConfig.
[EnergyArbitrator] Borne "Terra AC Charger (TCP)" ajoutée à la configuration des charges (…)

GetLoadConfig passe de 2 à 4 entrées. Les deux entrées de borne portent adapter: evcharger, mode: dynamic, domain: ev, et aucune charge utile — vérifié champ par champ (relays, sgReady, powerLevels, maxPowerW, minPowerW, minOnS, minOffS tous vides ou nuls).

Aller-retour NEUTRE, par la frontière RPC : GetLoadConfig → SetLoadConfig verbatim → GetLoadConfig rend un objet identique, et le journal du cycle suivant dit 0 créée(s), 0 mise(s) à jour en place, 2 inchangée(s), 0 retirée(s) — aucun adaptateur reconstruit, aucun verrou réarmé.

loads[] ⊆ GetLoadConfig est rétabli : les trois charges publiées ont leur entrée. La Terra AC est dans GetLoadConfig sans être dans loads[] — c'est le sens autorisé de l'inclusion : sans voiture assignée, elle est hors arbitrage, et le journal le dit ([Arbitre] Borne "Terra AC Charger (TCP)" hors waterfall).

2.4 La bascule elle-même

nymead arrêté 10:03:24, démarré 10:03:35, premier cycle d'arbitrage 10:04:00 — 36 s sans arbitrage. L'ECS a repris son palier par relecture des contacts (reprise au démarrage : palier 0 W [correspondance exacte], ECS-411).

⚠️ Le cas de transition que je voulais éprouver ne l'a PAS été : au moment de la bascule, aucune borne n'était sous commande (surplus négatif, wallbox à l'arrêt). « Une borne commandée par un chemin qui n'existe plus » reste donc non observé. Ce qui EST observé et qui en couvre la moitié : l'arbitre émet activement des power: OFF (4 fois sur la fenêtre), donc le mécanisme qui rattraperait une borne laissée allumée est en place et tourne.


3. CE QUE LA MACHINE A TROUVÉ, ET QUI N'EST PAS DANS LE LOT

3.1 GRAVE — une borne SANS VÉHICULE BRANCHÉ absorbe le budget de la cascade

Trois cycles consécutifs, wallbox en Eco avec voiture assignée mais pluggedIn: false :

[Arbitre] "chauffe-eau"  → "Surplus PV 8353 W — chauffe-eau consigne 3500 W (palier)"
[Arbitre] "{09e520fd…}"  → "Surplus PV 4853 W — Simulated wallbox consigne 4853 W"   ← 4 853 W
[Arbitre] "pac-terrain"  → "Surplus PV 1500 W — PAC banc recommandée (état 3, ~1500 W)"

Et dans la même charge utile de télémétrie, pour cette borne :

allocatedW: 4853   mécanisme: { chargingEnabled: false, pluggedIn: false, currentA: 7, phaseCount: 1 }

La borne est commandée power: ON, elle ne tire rien (currentPower: 0), et 4 853 W ont été retirés du budget et donnés à personne. La PAC derrière elle n'a survécu que sur son propre recrédit. Les deux cycles voisins donnent 2 350 W puis 5 464 W au même vide.

C'est le défaut du relevé 3g-1 §3.4 qui revient par une autre porte : là c'était le plancher qui mentait, ici c'est le filtre d'entrée.

Cause : EnergyArbitrator::evParticipatesInWaterfall() reprend le filtre d'entrée 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, gaspiller ne privait personne. En cascade, si. L'asymétrie est visible dans le code même : la boucle EV du scheduler, elle, teste bien if (!ev->available() || !ev->pluggedIn()) continue; — seul le chemin waterfall ne le fait pas.

Défaut de 3g-1, révélé par 3g-2 parce que la décision atteint enfin le matériel.

CORRIGÉ (commit dédié, après ce relevé) : pluggedIn rejoint le filtre d'entrée. Une borne sans véhicule est hors arbitrage, comme une borne sans voiture assignée ou en mode manuel (DESIGN_3g §4) — absente du contexte, pas servie à 0 : sa consommation est nulle et déjà au compteur. Le journal l'annonce comme les deux autres cas.

Éprouvé par testUnpluggedChargerTakesNoBudget, vérifié échouant sans le correctif : la borne débranchée disparaît du contexte, la charge suivante reçoit les 3 000 W entiers, et la seconde moitié du test rebranche le véhicule pour prouver que c'est bien pluggedIn qui décide et non une exclusion fortuite.

3.2 Mineur — le rang par défaut retombe à ÉGALITÉ, pas « en tête »

Les deux bornes ont reçu rang 1, à égalité avec chauffe-eau qui l'occupait déjà.

La règle implémentée est « le plus petit rang existant moins un, borné à 1 ». Quand une charge occupe déjà le rang 1, « moins un » vaut 0, le plancher le ramène à 1, et l'intention — mettre la borne en tête sans bousculer les charges déjà classées — dégénère en égalité.

Sans conséquence sur la reproductibilité : le tri est total, l'identifiant départage, et l'ordre est stable d'un cycle à l'autre. Mais ce n'est pas ce qui était demandé. Trois issues possibles, aucune tranchée : autoriser le rang 0, renuméroter les autres (exclu par « sans bousculer »), ou assumer l'égalité et laisser l'app poser les vrais rangs à la mise en service.

3.3 Pour mémoire — deux écritures de configuration dans le même cycle

Deux bornes détectées ⇒ deux setConfigs() ⇒ deux changed() ⇒ deux rebuildLoadAdapters() dans le cycle de démarrage. Les deux sont incrémentaux (2 inchangée(s)), donc aucun adaptateur n'est détruit et ECS-412 tient. Sans conséquence, mais à savoir si le nombre de bornes grandit.


4. CE QUI N'EST PAS PROUVÉ

  • EV_GRID_START : NON observé. Guetté six cycles durant, comme demandé — jamais fabriqué. Le budget de la borne est passé de 385 W à 2 350 W sans s'arrêter dans la fenêtre [plancher × 0,5 ; plancher[ (690–1 380 W en monophasé). Le motif reste donc éprouvé par le seul test de simulation. À guetter, comme BELOW_MIN_POWER l'a été.
  • BATTERY_RESERVE avec un surplus réel sous le seuil : toujours pas. Le SoC est à 50 %, le seuil à 0,4 — la réserve n'est pas active.
  • LM-1204-b, moitié matérielle : deux bornes réelles (§1).
  • Le cas de transition « borne commandée par un chemin qui n'existe plus » (§2.4).
  • La charge réelle d'un véhicule : aucune voiture n'a été branchée — la wallbox simulée n'expose aucune action pluggedIn, seulement connect/disconnect (interface connectable, état connected). C'est d'ailleurs ce qui a rendu §3.1 visible.

5. État où le banc a été rendu

valeur pourquoi
Paquet 1.15.2+etm23 posé par deploy.sh, +etm22 archivé
ChargingInfos identiques à l'origine — les deux bornes en ChargingModeNormal, sans voiture, targetPercentage: 0 comparées champ par champ au relevé d'avant
Voiture de test retirée (3G2-CAR-A-SUPPRIMER)
Wallbox simulée éteinte (power: false) l'arbitre ne la commande plus, elle est hors arbitrage
LoadConfig 4 entrées : chauffe-eau, PAC banc, + les 2 bornes auto-provisionnées les entrées de borne RESTENT — elles seraient recréées au prochain démarrage
batteryLevelConsideration 0,4 · acquisitionTolerance 0,5 · phasePowerLimit 0 inchangés

Dernier cycle rendu : surplus 5 634 W, chauffe-eau 3 500 W, pac-terrain 3 000 W, aucune borne dans loads[].