Patrick Schurig 4166fb3f2a docs(releve): lot B-bis — coordonnées de publication et ce qu'en retient l'agent app
Canal, pool, ajout seul, et le fait que le paquet posé sur le banc et le paquet publié
sont le même (.so sha256 identique, banc redéployé avec le paquet reconstruit).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
2026-08-25 18:37:19 +02:00

289 lines
15 KiB
Markdown

# RELEVÉ — lot B-bis : la PAC entre en configuration
Version du paquet vérifié : **`1.15.2+etm15`** · Machine : **`.75` (`hems`, arm64, nymead
`1.15.2+202606191336~trixie1`)** · Date : **2026-08-25**.
> **Ce qui compte dans ce document, c'est la colonne « où ».** Un extrait issu d'un test
> unitaire ne vaut pas vérification machine, et il est annoncé comme tel. Ici, les quatre
> critères du lot ont été vérifiés **sur `.75`** ; ce qui ne l'a pas été est listé au §6, et la
> raison y est écrite en toutes lettres.
---
## 0. Le constat d'origine, relevé AVANT le déploiement
`.75` tournait encore en **`1.15.2+etm14`**. Les deux frontières, interrogées à la suite :
| | `GetLoadConfig` | `GetLoadTelemetry` |
|---|---|---|
| `chauffe-eau` (relay-router) | ✔ | ✔ |
| `pac-terrain` (sg-ready) | **absent** | ✔ (état 2, verrou `minStateHold` 240 s) |
**L'écart que le lot referme est donc daté et mesuré, pas rapporté.** `pac-terrain` était
pleinement arbitrée — elle portait un verrou, elle publiait un mécanisme, elle apparaissait dans
`loads[]` — et restait introuvable dans la configuration. C'est l'unique porteur de la règle de
détection publiée à `+etm14`, celle que ce lot retire.
---
## 1. L'ORDRE de migration, exercé dans le bon sens
`docs/TEST_TERRAIN.md` §1.d impose de **poser la config sg-ready d'abord**, sur le `+etm14` en
cours d'exécution, **puis** d'installer le `+etm15`. La raison n'est pas cosmétique : la
suppression du bloc codé en dur est un événement de **compilation**, pas d'exécution —
`applySafeState()` (ECS-413) ne peut pas s'appliquer à un objet que le nouveau binaire ne
construit jamais. Installer d'abord laisserait une fenêtre où plus personne ne commande K1/K2.
**Vérifié sur `.75`.** `SetLoadConfig` des deux charges à 17:14:33, sur `+etm14` :
```
[EnergyArbitrator] sg-ready construit depuis config: "PAC banc" ( 4 états )
[EnergyArbitrator] SgReadyAdapter en dur "pac-terrain" retiré : ses contacts sont désormais
commandés par une charge sg-ready de configuration.
[EnergyArbitrator] 2 charge(s) pilotée(s) active(s) (config) — 1 créée(s), 1 inchangée(s)
```
C'est exactement la passation prévue par LM-300 : l'exemplaire en dur **cède** les contacts à
l'adaptateur de configuration. **K1/K2 n'ont jamais été sans propriétaire.** Le code qui produit
cet avertissement disparaît avec `+etm15` — il n'a plus d'objet une fois qu'il n'existe plus
d'exemplaire en dur, et c'est précisément pourquoi il fallait l'exercer *avant*.
Déploiement ensuite par `etm-powersync-deploy/deploy.sh` (jamais un `dpkg -i` à la main,
cf. `AGENTS.md`) : `+etm15` installé, version confirmée, `nymead` redémarré à 17:16:23.
---
## 2. Critère — la reprise d'état au démarrage journalise ce qu'elle a LU (ECS-411 / -411-b)
**Vérifié sur `.75`**, journal du nouveau processus (`nymead[57877]`, `+etm15`) :
```
[RelayRouter] chauffe-eau — reprise au démarrage : palier 500 W [correspondance exacte] ;
relais lus : {2ebe6be(1000W)=ouvert, {8538782(500W)=fermé,
{b033b21(2000W)=ouvert ; somme lue 500 W
[SgReadyAdapter] PAC banc — reprise au démarrage : état 2 (~0 W estimés) [correspondance
exacte] ; contacts lus : {beaf92e=ouvert, {bf236e6=ouvert
```
**L'exigence portait sur le cas nul, et c'est lui qui est relevé ici.** L'état repris est 2 —
l'état neutre, celui que l'ancien code supposait *sans lire*. La ligne existe quand même, et
elle publie le relevé **contact par contact** : « contacts lus, tous ouverts » se distingue donc
de « reprise non exécutée ». C'est tout l'objet d'ECS-411-b, et c'est le cas qu'un banc au repos
produit naturellement.
Rejoué une seconde fois à 17:26:21, à la reconstruction de l'adaptateur (§4) : même forme, même
relevé. La reprise n'est pas un événement de démarrage de service, c'est un événement de
**construction d'adaptateur**.
> Les deux autres issues — motif non déclaré, contact injoignable → avertissement `INCERTAIN` —
> sont couvertes par `testSgReadyRestartRecovery`, **en test unitaire, pas sur la machine** :
> les provoquer au banc demanderait de débrancher un relais câblé.
---
## 3. Critère — `GetLoadConfig` et `GetLoadTelemetry` concordent
**Vérifié sur `.75`**, cycle du 17:23 :
```
GetLoadConfig : ['chauffe-eau', 'pac-terrain']
GetLoadTelemetry : ['chauffe-eau', 'pac-terrain']
loads[] inclus dans GetLoadConfig : True
orphelins (arbitrés hors config) : AUCUN
```
| `loadId` | mécanisme publié | alloué | décision |
|---|---|---|---|
| `chauffe-eau` | `relay`, `stageW` 3500 / `maxStageW` 3500 | 3500 W | `SURPLUS_SETPOINT` |
| `pac-terrain` | `sgReady`, `state` 4, `estimatedPowerW` 3000 | 3000 W | `SG_FORCED` |
**L'inclusion ne vaut que dans ce sens, et l'autre direction a été exercée aussi** (§4) : une
charge configurée peut être absente de `loads[]`, et cela se lit **« désactivée »**, jamais
« perdue ».
---
## 4. Critère — `enabled: false` passe par l'état sûr, sur matériel réel
C'est le troisième point d'attention du lot : `enabled: false` **ne met pas** une charge en
sécurité par lui-même ; c'est ECS-413 — la charge qui **sort** d'une configuration vivante — qui
la fait passer par `applySafeState()` avant destruction.
**Vérifié sur `.75`**, PAC en état 4 (donc les deux contacts fermés) au moment du geste :
| | `GetLoadConfig` | `loads[]` | contacts K1/K2 (`Relay4`/`Relay5`) |
|---|---|---|---|
| avant | `chauffe-eau`, `pac-terrain` | les deux | **fermé, fermé** |
| après `enabled: false` | `chauffe-eau`, `pac-terrain` | `chauffe-eau` seule | **ouvert, ouvert** |
```
[SgReadyAdapter] "PAC banc" → état 2 ( 0 W estimés) | "Charge désactivée — mise en état sûr
(état 2) avant retrait (ECS-413)"
[EnergyArbitrator] 1 charge(s) pilotée(s) active(s) (config) — 1 inchangée(s), 1 retirée(s).
```
Deux choses se lisent ici, et la seconde est facile à manquer :
1. **Les contacts se sont réellement ouverts.** L'écriture a eu lieu, pas seulement le motif.
2. **`loads[]` n'a pas changé dans la seconde qui a suivi le `SetLoadConfig`** — la PAC y
figurait encore. Ce n'est pas un défaut : `GetLoadTelemetry` rend ce qui a été **publié** au
dernier cycle, il ne recalcule pas (`AGENTS.md`). Elle en est sortie au cycle suivant. Un
client qui attend une disparition immédiate se trompe de contrat.
Configuration restaurée (`enabled: true`) à 17:26:21 — le banc est laissé dans son état nominal.
---
## 5. Critère — la PAC s'arbitre comme avant le lot, et l'armement à froid EXPIRE
C'est le deuxième point d'attention : la reconstruction réarme `minStateHold`. Sans conséquence
au banc (300 s), réelle sur une PAC (900 s). Ce qu'il fallait prouver n'est pas que le verrou
s'arme — c'est qu'il **n'est pas permanent** (le défaut ECS-412 corrigé côté routeur, où une
charge démarrant contacts ouverts ne pouvait plus jamais s'enclencher).
**Vérifié sur `.75`**, redémarrage à 17:16:23, `minStateHoldS: 300` :
| Cycle | Surplus | Décision `pac-terrain` |
|---|---|---|
| 17:17 → 17:21 | 5008 → 12185 W | `Verrou minStateHold — PAC banc maintenue état 2` |
| **17:22** | 8718 W | **`Surplus abondant — PAC banc forcée (état 4, ~3000 W)`** |
| 17:23 | 11394 W | état 4 `INCHANGÉ` |
Le verrou a tenu ~5 min 40 puis a **cédé de lui-même**, alors même que le surplus dépassait
largement le seuil de forçage dès 17:17. L'armement est donc bien **paresseux et transitoire** :
`applyAction()` estampille au premier `now` reçu et le compte à rebours court.
**Le waterfall est intact** : `chauffe-eau` (rang 1) se sert la première (3500 W), la PAC (rang
2) voit le reliquat. C'est le comportement d'avant le lot, obtenu désormais par une charge
**entièrement configurée**.
---
## 6. Ce qui n'a PAS été vérifié sur la machine
1. **Le repli L2 d'une PAC de configuration** — le trou de sécurité que ce lot referme (une PAC
de config recevait un `Setpoint(0)` que `SgReadyAdapter` rejette, et restait en état 4 sous
compteur muet). Couvert par `testLoadTelemetryRpc`, qui vérifie l'**état ET les contacts**,
pas le motif. **Non rejoué au banc** : provoquer un compteur muet demande de figer le
simulateur >90 s, ce qui suspend toute la planification et n'était pas nécessaire au vu de la
couverture unitaire. À joindre à la prochaine campagne §5 de `TEST_TERRAIN.md`.
2. **Les issues dégradées de la reprise** (motif non déclaré, contact injoignable) — §2.
3. **La polarité des contacts K1/K2 face à une vraie PAC** — inchangé depuis le lot B : aucune
PAC n'est sous tension. Voir ci-dessous, c'est le point le plus important de ce relevé.
---
## 7. ⚠️ Les puissances SG-Ready posées au banc ne sont PAS des données d'installation
`estimatedPowerW` **{état 3 : 1500 W, état 4 : 3000 W}** et `minStateHoldS: 300` sont
**reconduits du bloc codé en dur supprimé**, où ils décrivaient une PAC **inventée** pour faire
tourner l'arbitrage. Au 2026-08-25, **aucune PAC n'est sous tension sur K1/K2**.
Les reconduire rend §4 de `TEST_TERRAIN.md` calculable — **pas véridique**. Ce n'est pas
cosmétique : `estimatedPowerW` sert de base au **recrédit budget** et au **choix d'état**, si
bien qu'une estimation fausse ne décale pas seulement la PAC, elle décale l'allocation de **tout
le waterfall en aval**. Le champ porte son nom — une estimation, jamais un engagement — mais une
estimation reste censée viser la bonne machine.
**À remplacer par les valeurs de la machine réellement câblée** (plaque signalétique, puis mesure
sous les deux régimes) avant toute conclusion transposable à une installation. Marqué comme tel
dans `docs/TEST_TERRAIN.md` §1.d.
---
## 8. Suites de tests, et la contrainte d'exécution qui les encadre
**La suite de simulation ne doit PAS être lancée en un seul processus.** Chaque test instancie un
serveur nymea complet et rien n'est rendu entre deux ; en un processus, le binaire atteint 7-8
Gio de RSS et l'OOM killer emporte la session graphique (constaté deux fois le 2026-08-25).
Protocole retenu : **un processus par test**, chacun encadré par
`systemd-run --user --scope -p MemoryMax=2G -p MemorySwapMax=0`.
| Suite | Processus | Assertions | Échecs | Pic RSS |
|---|---|---|---|---|
| `simulation` | 24 | 85 | 0 | 61-66 Mo (413 Mo pour `run`, data-driven) |
| `charging` | 15 | 74 | 0 | 62-63 Mo |
| `spotmarket` | 7 | 44 | 0 | 63-64 Mo |
Build : **0 erreur / 0 avertissement**, local (`-Wall -Werror -Wextra`) et cross-arm64 (seules
subsistent les notices `dh` propres au cross-build).
Deux défauts trouvés en montant ce harnais, corrigés ici :
- **`JournalCapture` se chaînait sur elle-même.** `qInstallMessageHandler()` rend le handler
courant ; à la **deuxième** capture vivante, ce handler est `collecte()` elle-même. Chaque
message se rappelait donc indéfiniment en concaténant dans `m_texte` — récursion infinie à
croissance quadratique. C'est ce que le plafond de 2 Gio a tué, et c'est un bien meilleur
candidat qu'une accumulation graduelle pour expliquer un OOM **déterministe** (`total-vm`
identique à 200 ko près sur les deux occurrences). Une seule capture vivante ne déclenchait
rien : le seul test qui en ouvre deux est celui ajouté par ce lot.
- **`tm` et `owner` non initialisés** (`simulation.cpp`, trois sites) : `freshSetup()` peut
sortir sur un `QVERIFY` en échec sans les renseigner, et ils étaient déréférencés juste après.
Initialisés par l'appelant, vérifiés après l'appel — un échec de test lisible au lieu d'un
comportement indéfini.
**Instabilité d'infrastructure, signalée et non corrigée.** Le harnais nymea rouvre un port
**fixe** (4847, plugin mock système) à chaque cycle `cleanupTestCase()`/`initTestCase()` ; les
tests qui réinstancient un serveur en cours de route échouent donc par intermittence sur
`Failed to open HTTP port. Port in use?`. Observé une fois sur `testEcsRelayTopologies`, qui
passe 7 fois sur 7 en isolation. Le harnais rejoue un test en échec **une** fois et l'étiquette
`INSTABLE` plutôt que de masquer. Même famille que la cause de fond ci-dessous : une ressource
non rendue à la destruction d'une instance de serveur.
---
## 9. Ce qui reste ouvert
1. **La mémoire non rendue entre deux tests** — pourquoi la destruction d'une instance de serveur
ne rend ni la mémoire ni le port 4847 (`cleanup()`, `cleanupTestCase()`, durée de vie des
objets de `freshSetup()`). **Hors de ce lot, à traiter séparément.** Le correctif de
`JournalCapture` retire ce qui faisait tomber la VM, mais ne dit rien de l'accumulation
elle-même — et cette question n'a **pas** été rejouée en un seul processus, la consigne
l'interdisant.
2. **Les puissances SG-Ready du banc** — §7. C'est le point à clore en premier.
3. **Le repli L2 d'une PAC de configuration, au banc** — §6.1.
4. Les points 2 à 4 de `docs/RELEVE_LOTB.md` §9 restent tels quels : silences de refus (7-c/7-d),
critère 3, et les autres.
---
## 10. Publication — où l'agent app trouve le paquet
| | |
|---|---|
| Version publiée | **`1.15.2+etm15`** (arm64) |
| Canal | **`powersync-nightly`** — inchangé depuis le lot B ; `stable` et `testing` ne contiennent toujours aucun `powersync-energy-plugin-nymea` |
| Dépôt local | `etm-powersync-dev:/mnt/builddisk/apt-repo` (`reprepro list powersync-nightly powersync-energy-plugin-nymea`) |
| Public | `192.168.1.241:/var/www/html/repos.etm-powersync.fr/` — pool et index vérifiés après rsync |
| Banc `.75` | `1.15.2+etm15` installé, `nymead` actif |
```
deb [signed-by=/etc/apt/keyrings/powersync.gpg] https://repos.etm-powersync.fr powersync-nightly main
```
**Publication en AJOUT SEUL**, comme au lot B : `reprepro --keepunreferencedfiles`. Sans cette
option, `+etm14` aurait perdu sa dernière référence et son binaire aurait été retiré du pool.
`+etm12` et `+etm14` y sont toujours, local et public. La synchronisation est en **ajout pur** —
le dry-run préalable ne montrait aucune ligne de suppression, et la passe `--delete` a été
retirée du script le 2026-08-13.
**Le paquet posé sur le banc et le paquet publié sont le MÊME.** Le premier build ne portait pas
les dernières entrées de changelog ; l'arbre a donc été figé, reconstruit, et le `.so` des deux
paquets comparé — `sha256` identique. Le banc a été redéployé avec le paquet reconstruit, celui
qui est au pool. Rien n'a été publié qui n'ait tourné sur `.75`.
> `repos.etm-powersync.fr` n'est pas résolu depuis `etm-powersync-dev` : la vérification a été
> faite **sur le serveur** (`192.168.1.241`), pas par HTTPS — même limite qu'au lot B.
### Ce que l'agent app doit retenir
1. **La règle de détection de `+etm14` est sans objet** : plus aucun `loadId` de `loads[]` n'est
absent de `GetLoadConfig`. Une implémentation existante peut rester en place, elle ne se
déclenchera plus. **Aucun `configurable: false` n'existe** — ne pas l'attendre.
2. **L'inclusion est à sens unique.** Une charge de `GetLoadConfig` absente de `loads[]` est
**désactivée** (`enabled: false`), pas perdue.
3. **`GetLoadTelemetry` rend le dernier cycle PUBLIÉ, il ne recalcule pas.** Après un
`SetLoadConfig`, `loads[]` ne reflète le changement qu'au cycle suivant (§4).
4. **`pac-terrain` est désormais configurable** : renommable, classable (`domain`), rangeable,
désactivable — un écran de réglage lui est dû comme à toute autre charge.