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
15 KiB
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 partestSgReadyRestartRecovery, 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 :
- Les contacts se sont réellement ouverts. L'écriture a eu lieu, pas seulement le motif.
loads[]n'a pas changé dans la seconde qui a suivi leSetLoadConfig— la PAC y figurait encore. Ce n'est pas un défaut :GetLoadTelemetryrend 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
- 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)queSgReadyAdapterrejette, et restait en état 4 sous compteur muet). Couvert partestLoadTelemetryRpc, 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 deTEST_TERRAIN.md. - Les issues dégradées de la reprise (motif non déclaré, contact injoignable) — §2.
- 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 :
JournalCapturese chaînait sur elle-même.qInstallMessageHandler()rend le handler courant ; à la deuxième capture vivante, ce handler estcollecte()elle-même. Chaque message se rappelait donc indéfiniment en concaténant dansm_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-vmidentique à 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.tmetownernon initialisés (simulation.cpp, trois sites) :freshSetup()peut sortir sur unQVERIFYen é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
- 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 defreshSetup()). Hors de ce lot, à traiter séparément. Le correctif deJournalCaptureretire 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. - Les puissances SG-Ready du banc — §7. C'est le point à clore en premier.
- Le repli L2 d'une PAC de configuration, au banc — §6.1.
- 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.frn'est pas résolu depuisetm-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
- La règle de détection de
+etm14est sans objet : plus aucunloadIddeloads[]n'est absent deGetLoadConfig. Une implémentation existante peut rester en place, elle ne se déclenchera plus. Aucunconfigurable: falsen'existe — ne pas l'attendre. - L'inclusion est à sens unique. Une charge de
GetLoadConfigabsente deloads[]est désactivée (enabled: false), pas perdue. GetLoadTelemetryrend le dernier cycle PUBLIÉ, il ne recalcule pas. Après unSetLoadConfig,loads[]ne reflète le changement qu'au cycle suivant (§4).pac-terrainest désormais configurable : renommable, classable (domain), rangeable, désactivable — un écran de réglage lui est dû comme à toute autre charge.