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
289 lines
15 KiB
Markdown
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.
|