`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
212 lines
13 KiB
Markdown
212 lines
13 KiB
Markdown
# 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+etm23` a été construit, posé sur
|
||
> `.75` et exercé. Ce qui a été observé — et les deux défauts que le passage a trouvés — est
|
||
> dans **`docs/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_loadAdapters` ne 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 (`evSurplusPlannedByWaterfall` pour `planSurplusCharging`,
|
||
`evCommandedByWaterfall` pour `adjustEvChargers`). 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` / motif `LOAD_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.
|
||
|
||
1. **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 `withheldW` chiffre les deux termes retenus.
|
||
*Attrapé par `batteryLevelConsiderationStopCharging`, qui échouait.*
|
||
2. **Le prédicat de bascule de phases avait divergé.** `canSetPhaseCount()` dit seulement que la
|
||
borne *porte* l'état `desiredPhaseCount` ; 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.
|
||
3. **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.
|
||
4. **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_RESERVE` avec 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
|
||
|
||
```bash
|
||
# 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 :
|
||
|
||
1. **Les entrées `evcharger` apparaissent dans `GetLoadConfig`** au premier cycle, une ligne de
|
||
journal par borne. Vérifier qu'aucune configuration existante n'a été bousculée.
|
||
2. **Une seule commande par borne et par cycle.** Le journal ne doit plus montrer une ligne
|
||
`[Arbitre]` suivie d'un `Executing action … Issuer: Surplus` qui dit autre chose.
|
||
3. **`GetLoadTelemetry` publie les bornes avec leur rang**, et la somme des `allocatedW` financés
|
||
au surplus retombe sur `budget.allocatedW`.
|
||
4. **Le plancher publié** d'une borne branchée : `min × 230 × phases`, sans facteur de tolérance.
|
||
Sur la wallbox simulée du banc, attention à son `desiredPhaseCount: 3` permanent.
|
||
5. **`EV_GRID_START`** quand le surplus est entre la moitié et la totalité du plancher : le motif
|
||
doit chiffrer `gridW`, et une seule borne doit soutirer.
|
||
6. **LM-1204-b** dès qu'une seconde borne réelle répond.
|