Patrick Schurig 6acaee38f9 fix(relayrouter): ECS-411-b — la reprise s'annonce, et annonce ce qu'elle a lu
La correspondance exacte ne journalisait qu'au palier non nul (`if (i > 0)`). Au palier 0 —
le cas de l'armement minOff d'ECS-412, celui qu'on vient précisément observer — le journal
était muet, et « relais lus, tous ouverts » ne se distinguait pas de « reprise non
exécutée ». Constaté au banc le 2026-08-13 : ECS-411 fonctionnait, et son contrôle sur le
chemin réel était impossible depuis les traces.

Le relevé porte désormais l'état de CHAQUE relais — identifiant, puissance déclarée, et
fermé / ouvert / injoignable→supposé fermé — avec le palier retenu, la branche empruntée et
la somme lue. Sortie unique pour les trois branches. Un relais injoignable fait passer la
ligne en avertissement : le palier déduit est plausible, la lecture ne l'est pas, et
« palier repris : 0 » couvrait ces deux situations sans les distinguer.

Un défaut de fond a été trouvé PAR ce relevé, qui affichait « (0W) » pour le relais 1500 W.
La nominale par relais était redéduite des encodages solo, or la déduplication par puissance
peut en évincer un : avec 500/1000/1500, le palier 1500 est encodé {500,1000} (masque 3,
rencontré avant le masque 4) et le relais 1500 n'apparaît seul nulle part. Sa nominale valait
0, donc un démarrage sur ce seul relais donnait sumW = 0, aucune correspondance exacte, et un
palier 0 annoncé pendant que 1500 W circulaient — l'annonce en sous-estimation qu'ECS-411
existe pour interdire. Elle est prise à la déclaration (m_relayNominalW). Rendre la lecture
visible est ce qui a rendu le défaut trouvable : c'est le sens de la règle, pas un effet
secondaire.

Règle 7-c dans AGENTS.md : un silence ne doit jamais être ambigu. Journaliser l'issue, pas
seulement l'issue remarquable ; journaliser ce qui a été lu, pas seulement ce qui en est
déduit. Trois autres endroits signalés dans spec_ecs.md, non corrigés — le retour idempotent
du routeur, clearFault() sur charge saine, et le refus minStateHold en qCDebug quand
l'application est en qCInfo.

RELEVE_ECS306 §3.0, en tête du protocole : un état nymea n'émet changed() que si la valeur
bouge, donc une grandeur forcée constante déclenche le watchdog L2 en 90 s et fait mesurer le
watchdog au lieu de l'arbitrage. Constaté au banc en gelant le simulateur.

Les tests portent sur le TEXTE publié, via un collecteur de messages : un test « journal non
vide » n'aurait rien vu. Trois cas, tous vérifiés échouant sans les correctifs. Suite
complète : 112 tests, 0 échec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 20:13:18 +02:00

300 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Relevé banc — ECS-306 et commutations par relais
Dépôt : `etm-powersync-energy-plugin-etm` · Banc : `192.168.1.75` (`nymea-dev-rpi`)
Installation émulée : `specs/spec_ecs.md` §4.0 (INST-100), cf. INST-104.
> **Ce document est publié AVANT la mesure.** La section « Prédiction » est
> horodatée du **2026-08-09** et commitée avant tout profil lancé. Une
> reconstruction analytique publiée après coup ne vaut rien ; publiée avant, elle
> est falsifiable. Les résultats viendront s'ajouter en dessous, sans jamais
> réécrire la prédiction — si elle est fausse, elle reste, et c'est le
> renseignement utile.
---
## 1. Dispositif
Trois charges, un seul budget de surplus, cascade par rang ascendant.
| Charge | Rang | Adaptateur | Paliers / états | Verrous |
|---|---|---|---|---|
| `chauffe-eau` | 1 | `relay-router` — Relay1/2/3 (500/1000/2000 W) | 0…3500 W, 8 valeurs | `minOnS`/`minOffS` = 60 s (300 s pour le volet 1, cf. §3) |
| PAC SG-Ready | 2 | `sg-ready` — Relay4/5, codé en dur | P3 = 1500 W, P4 = 3000 W | `minStateHoldS` = 300 s |
| `sonde-residu` | 3 | `relay-router` — Relay6, **1 relais, 100 W** | 0 / 100 W | `minOnS`/`minOffS` = 10 s |
**La sonde est un instrument de mesure, pas une charge.** Aucune résistance n'est
câblée sur Relay6 : le relais claque, rien ne consomme. Ce qu'elle rend
observable est une **décision d'allocation** — le résidu que le waterfall laisse
à la charge suivante — et non une puissance. Son palier à 100 W est choisi très
au-dessous de l'écart attendu (500 W) pour que le basculement soit franc.
Pilotage du surplus : `banc/cmd/manual {"grandeur":"pv","mode":true,"value":<W>}`
sur le broker `192.168.1.131`, `modele.py:178`. `banc/cmd/pause` fige l'horloge du
modèle et rend le profil reproductible.
---
## 2. Prédiction — 2026-08-09, avant mesure
### Pourquoi la télémétrie ne discrimine pas
Les Things sont des relais **GPIO** : leur ThingClass n'expose pas `currentPower`.
`RelayRouter::telemetry()` retombe donc sur le **nominal commandé**
(`relayrouter.cpp:113-114`), et `currentPowerW == palier commandé` en permanence.
Conséquence : `budget_charge = budget + palier_courant`. Tant que le budget reste
positif, les deux versions choisissent le même palier et laissent le même résidu.
**La divergence n'apparaît qu'en import net**, budget signé négatif.
### Volet 1 — point de fonctionnement discriminant
Surplus 3600 W jusqu'à ce que le chauffe-eau atteigne 3500 W et arme `minOn`,
puis chute à **300 W** maintenue.
```
budget_charge = 300 + 3500 = 3200
```
| | Palier retenu | Résidu annoncé | PAC (rang 2) | Sonde (rang 3) |
|---|---|---|---|---|
| **`+etm2`** | 3000 (plus haut ≤ 3200) | **+200 W** | état 2 (200 < P3) | **ALLUMÉE** (100 200) |
| **`+etm3`** | 3500 (forcé par `lockMinPowerW`) | **300 W** | état 2 | **ÉTEINTE** (100 > 300) |
Sous `+etm2`, le routeur écrête malgré tout à 3500 W (verrou interne) : **500 W
sont consommés de plus que comptés**, et ce sont ces 500 W que la sonde révèle.
**Prédiction falsifiable : la sonde s'allume sous `+etm2`, reste éteinte sous
`+etm3`. Si elle s'allume sous `+etm3`, le raisonnement est faux.**
Prédiction secondaire : **la PAC ne changera pas d'état** entre les deux
versions — 200 W comme 300 W sont tous deux sous P3 = 1500 W. Le volet 1 ne
montrera donc rien sur la PAC ; c'est la sonde qui porte la démonstration. Sans
elle, ce relevé aurait conclu « aucun effet observable » à tort.
### Volet 2 — commutations par relais
Balayage complet 0 → 3500 W puis retour, plateaux ≥ 90 s (`minOnS` = 60 s).
Comptage attendu sur un aller simple :
| Relais | Commutations | Part |
|---|---|---|
| R500 | **7** | bit de poids faible |
| R1000 | **3** | |
| R2000 | **1** | |
| **total** | **11** | |
La transition **1500 → 2000 W** (`{R500,R1000}``{R2000}`) bascule **les trois
relais**, soit **3 des 11** commutations à elle seule. C'est la seule du câblage
dans ce cas.
**Piège prédit — le cliquet.** `currentPowerW` valant le nominal commandé, le
budget rendu à la charge inclut toujours son propre palier : un profil qui ne
fait que **décroître en surplus positif** ne fera jamais redescendre le palier.
Il faut des plateaux à **surplus négatif** pour provoquer la descente, sinon le
chauffe-eau reste à 3500 W du début à la fin et le relevé est vide.
---
## 3. Protocole
### 3.0 — PRÉALABLE : le compteur doit BOUGER, pas seulement être publié
**À lire avant toute autre ligne de ce protocole.** Il conditionne la validité de
tout ce qui suit, quel que soit le volet.
Un état nymea n'émet `changed()` que si **la valeur change**. Le watchdog L2
compte les *mises à jour* du compteur racine, pas les publications : une valeur
republiée à l'identique n'en produit aucune. Au bout de **90 s** sans changement,
l'arbitre déclare le compteur muet, bascule en **mode dégradé L2**, force toutes
les charges à leur repli et **suspend la planification**.
Conséquence directe : **une mesure à valeur forcée constante ne mesure pas
l'arbitrage, elle mesure le watchdog.** Un profil en plateaux de 90 s ou plus,
injecté comme une consigne fixe, s'auto-détruit — et le relevé se remplit de
`consigne 0 W (force) | "Compteur muet depuis >90 s"` au lieu de décisions.
> Constaté au banc le 2026-08-13, en forçant le surplus pour observer l'armement
> minOff : le simulateur avait été **gelé** (`banc/cmd/pause`), sur le
> raisonnement — faux — que la publication continue suffisait à entretenir la
> fraîcheur. `tick()` sort avant tout recalcul quand `paused` est vrai, donc la
> valeur ne bougeait plus. L2 a mordu en 105 s, forcé l'ECS et la PAC à 0 et
> suspendu la planification. La mesure visée avait été capturée 80 s plus tôt ;
> elle aurait été perdue avec un cycle de plus.
**Règle pour les deux volets.** Toute grandeur forcée doit varier à un pas
inférieur à 90 s — quelques dizaines de watts suffisent, l'amplitude est
indifférente, seul le changement compte. Sur le banc, épingler l'horloge simulée
en alternant deux valeurs proches (ex. `banc/cmd/heure` à 11,9 puis 12,1 toutes
les 20 s) tient le budget à peu près constant tout en entretenant les
changements. **Ne jamais utiliser `banc/cmd/pause`** pendant une campagne.
Corollaire favorable : des **compteurs réels** bougent d'eux-mêmes. Une fois les
Eastron et le smartmeter ECS appairés (§7 de `spec_ecs.md`), ce piège disparaît
de lui-même — c'est une raison de plus de faire l'appairage avant la campagne.
### 3.1 — Campagnes
Deux campagnes **séparées**, jamais fusionnées : si un résultat surprend, il faut
pouvoir dire lequel des deux réglages en est la cause.
**Volet 1**`minOnS` du chauffe-eau porté à **300 s** le temps de l'essai, pour
élargir la fenêtre d'observation. Écart de réglage assumé et déclaré ; remis à
60 s après.
**Volet 2**`minOnS` à 60 s, plateaux ≥ 90 s, avec plateaux à surplus négatif.
**Ordre d'exécution** : `+etm3` étant déjà installé, mesurer l'**après** d'abord,
puis descendre en `+etm2`, refaire le même profil, puis remonter en `+etm3`.
**Vérification obligatoire au retour en `+etm2`** : relire paliers et rangs par
`NymeaEnergy.GetLoadConfig` **avant** de lancer le profil. La configuration a été
écrite par `+etm3` ; si `+etm2` la rejetait — silencieusement ou non — on
mesurerait deux dispositifs différents sans le voir.
---
## 4. Résultats
### 4.0 — Un défaut trouvé avant toute mesure (2026-08-09)
La première tentative a révélé un **blocage circulaire dans ECS-412**, pas dans le
banc : la charge sonde restait figée à 0 W sous 4 kW de surplus disponible. Un
horodatage de dernière commutation nul servait de sentinelle « écoulé = 0 » à
chaque cycle, si bien que toute charge démarrant relais ouverts avec `minOffS > 0`
ne pouvait jamais s'enclencher.
**La prédiction du §2 annonçait « sonde éteinte sous `+etm3` » — et elle l'était,
mais pour la mauvaise raison.** Sans un test de falsification cherchant *pourquoi*
elle se vérifiait, ce relevé aurait coché la prédiction et conclu à tort. Une
prédiction juste peut être confirmée par un mécanisme faux ; c'est la limite de
l'exercice, et elle mérite d'être écrite ici.
Corrigé en `1.15.2+etm4` (armement paresseux et transitoire), couvert par
`testEcsColdStartLockExpires`. **Vérifié au banc après déploiement** : sonde
éteinte deux cycles, puis enclenchée — le verrou court puis expire.
### 4.1 — Volet 1, expérience naturelle sous `+etm4`
Le contraste attendu entre `+etm2` et `+etm3` a été observé **au sein d'un même
binaire**, sur deux cycles consécutifs au même budget : l'un verrou actif, l'autre
verrou expiré. Le cycle à verrou expiré reproduit exactement ce que ferait
`+etm2`, puisque sans verrou le clamp d'ECS-306 n'a rien à contraindre.
| | Budget charge | Chauffe-eau | Résidu annoncé | Sonde (rang 3) |
|---|---|---|---|---|
| **Verrou minOn ACTIF** | 3202 W | **maintenu 3500 W** | **198 W** | **éteinte** |
| **Verrou EXPIRÉ** | 3201 W | 3000 W (plus haut ≤ budget) | **+201 W** | **allumée** |
Traces brutes :
```
"chauffe-eau" → "Verrou minOn — chauffe-eau maintenue à 3500 W
(puissance engagée, budget 3202 W) ; le résidu en tient compte"
"sonde-residu" → "Verrou minOn — sonde residu maintenue à 0 W (budget -198 W)"
"chauffe-eau" → "Surplus PV 3201 W — chauffe-eau consigne 3000 W (palier)"
"sonde-residu" → "Surplus PV 201 W — sonde residu consigne 100 W (palier)"
```
**Arithmétique vérifiée**, à 2 W près du prédit :
`3202 3500 = 298`, puis recrédit de la sonde elle-même (100 W, alors allumée)
`198`. Côté verrou expiré : `3201 3000 = +201`, sonde éteinte donc aucun
recrédit → `+201`, au-dessus de son palier de 100 W.
### Ce qui est démontré, et ce qui ne l'est pas
**Démontré, directement** : au même budget, honorer le verrou ou l'ignorer change
le résidu de 500 W et **fait basculer la charge de rang 3**. C'est la moitié
*comptable* de l'affaire, et elle est observée sans ambiguïté.
**NON démontré directement** : l'écart entre consommé et compté. Dans le cycle à
verrou expiré, le routeur applique **réellement** 3000 W — il n'y a donc aucun
écart dans ce cycle-là. L'autre moitié — le routeur qui écrête à 3500 W pendant
que le scheduler ne compte que 3000 — est observée dans le **premier** cycle, pas
dans le second.
Le résultat tient donc par **composition de deux moitiés observées, chacune dans
un cycle différent du même binaire**. C'est solide, mais ce n'est pas une
observation directe du défaut complet. **Un `+etm2` réellement installé reste le
seul moyen de voir les deux moitiés dans le même cycle** — c'est ce qui reste à
faire, et c'est pourquoi l'expérience naturelle ne le remplace pas.
**La prédiction secondaire est confirmée aussi** : la PAC de rang 2 est restée en
état 4 sur les deux cycles. Sans la charge sonde, ce relevé aurait conclu « aucun
effet observable ».
### 4.2 — Volet 2, commutations : PRÉDICTION FALSIFIÉE (protocole en cause)
Profil de 18 plateaux de 100 s, montée 0 → 3600 W puis descente jusqu'à 3000 W.
| Cible surplus | Palier | Commutations |
|---|---|---|
| 0 → 3600 (8 plateaux, montée) | **3500 W tout du long** | **0** |
| 3600 → 500 (6 plateaux, descente) | **3500 W tout du long** | **0** |
| 0 | 3000 W | 1 — R500 |
| 500 | 2000 W | 1 — R1000 |
| 1500 | 500 W | 2 — R500, R2000 |
| 3000 | 0 W | 1 — R500 |
**Mesuré : R500 = 3, R1000 = 1, R2000 = 1 — total 5.**
**Prédit : 7 / 3 / 1 — total 11.**
**Transitions à trois relais observées : AUCUNE.** La frontière 1500 → 2000 n'a
jamais été franchie.
#### Le cliquet est confirmé, et plus fort que prédit
**Quatorze plateaux consécutifs sans une seule commutation**, de 0 à 3600 W puis
retour à 500 W. Le palier n'a bougé qu'une fois le surplus **négatif**. La
prédiction du §2 sur ce point est non seulement juste, elle était trop prudente :
ce n'est pas seulement la descente en surplus positif qui ne fait rien redescendre,
c'est **toute la plage positive**.
#### Pourquoi la prédiction 7/3/1 est fausse : un EMBALLEMENT propre au banc
Ce n'est pas un « mauvais point de départ ». Le chauffe-eau était à 3500 W pour une
raison **mécanique**, et elle mérite d'être comprise avant qu'on ne « corrige » la
mauvaise chose.
Deux faits se composent sur ce banc :
1. les Things sont des relais **GPIO** : leur ThingClass n'expose pas
`currentPower`, donc le recrédit anti-clignotement (correction B) crédite le
**nominal commandé** ;
2. le rootmeter de `.75` est la **vue SunSpec du simulateur**, aveugle aux
résistances réellement câblées sur ces broches. La consommation de l'ECS ne fait
donc **jamais** baisser l'export mesuré.
Résultat : `budget_charge = surplus + palier` **double-compte**. Chaque cycle, le
budget apparent dépasse le palier courant, la charge monte d'un cran — et
recommence. Tant que le surplus dépasse 500 W, le palier grimpe jusqu'au plafond.
C'est un **emballement**, et il explique à lui seul les quatorze plateaux sans
commutation.
> **NE PAS « CORRIGER » LA CORRECTION B.** Cet emballement est un **artefact de
> banc**, pas un défaut du moteur. Sur une installation réelle, l'ECS est **derrière
> le compteur réseau** : sa consommation fait réellement chuter l'export, le
> recrédit compense exactement ce que la charge vient de retirer, et la boucle est
> juste. Supprimer ou amoindrir le recrédit casserait l'anti-oscillation qu'il
> protège (ECS-404) sans rien régler. Le défaut est dans la **cécité du compteur du
> banc**, pas dans l'arithmétique du budget.
#### Conséquence sur ECS-303 : NON affaibli
La remarque « la frontière 1500 → 2000 est inatteignable en montée » vaut **pour ce
banc**, où l'emballement interdit toute montée pas à pas. Elle ne vaut pas pour une
installation réelle : la rampe matinale y traverse la bande 1500-2000 W lentement,
et la frontière est franchie normalement.
**ECS-303 reste donc justifié.** Et le volet 2 n'est **pas** à refaire depuis le
palier 0 sur le simulateur — il est à faire **après le câblage** de la résistance
physique et du compteur, sur la vraie installation. C'est le seul contexte où le
comptage de commutations dit quelque chose sur la durée de vie des contacteurs.
### 4.3 — Ce qui reste à mesurer
- **`+etm2` réellement installé**, pour confirmer que le cycle « verrou expiré »
est bien un substitut fidèle. L'expérience naturelle est convaincante mais elle
n'est pas la comparaison demandée.
- **Volet 2** — relevé de commutations par relais : non commencé.