etm-powersync-energy-plugin.../docs/DESIGN_COMMANDE_PERDUE.md
Patrick Schurig bf8d890c8e docs: conditions de la fenêtre d'observation du week-end
Posé dimanche 2026-09-06 à 08:19 (Europe/Paris) : chauffe-eau, minEnergyWhPerDay
3000 → 2000, dailyDeadline inchangé à 12:00. Discipline habituelle — capture sur
disque avant écriture, 76 clés comparées, 1 divergence attendue, 0 imprévue — mais
PAS de restauration : la configuration reste en place tout le week-end.

Symboles de débogage laissés en place jusqu'à lundi, donc aucune couture
supplémentaire pendant la fenêtre. Les deux dernières restent 06:53:30Z (fuseau) et
09:22:57 (installation des symboles), toutes deux du 2026-09-05.

CE QUE L'ÉCRAN MONTRE À LA POSE, et pourquoi ce n'est pas un défaut. À 08:19 le
palier est ECO_FLOOR_MET avec deliveredWh = 30 475 Wh pour une cible de 2 000.
Arithmétiquement correct et sans intérêt : le compteur de l'ECS publie 1 500 W en
permanence, donc deliveredWh monte de 1,5 kWh/h que la charge soit commandée ou
non. La période a basculé hier à 12:00 ; vingt heures plus tard elle a « livré »
30 kWh.

LA FENÊTRE UTILE EST À 12:00 et dure ~80 minutes. À la bascule, la base est reprise
(LM-1013-c) : deliveredWh repart de 0 face à 2 000 Wh sur 24 h, soit une cadence de
83 W financée par le réseau si le surplus manque, que le compteur fixe comble en
2000/1500 = 80 min. Deux fenêtres sur le week-end — dimanche 12:00 et lundi 12:00.

ET C'EST LA CINQUIÈME FORME D'HOMOGÉNÉITÉ QUI FIXE CE PLAFOND. Sur un banc dont le
compteur ne suit pas la commande, on ne peut pas avoir à la fois une cible modeste
et un chemin d'achat longuement exercé : ce qui décide n'est pas la cible, c'est la
constance du compteur. Tenir l'achat en éveil tout le week-end demanderait de
dépasser 36 kWh/jour, c'est-à-dire acheter au plafond — précisément ce qu'on
voulait éviter. La contrainte est du banc, pas du moteur.
2026-09-06 08:20:33 +02:00

269 lines
16 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.

# DESIGN — la commande perdue, et l'écart que personne ne mesure
*(Ouvert le 2026-09-01, sur le premier constat de `RELEVE_APP_20260831.md`. Le design avant le
code, comme pour le délestage et le §10.)*
## 1. Le fait, tel qu'il a été mesuré
Coupure Modbus. La box journalise `Restoring manual values of false 6 A` trois secondes après.
La borne, elle, reste à `power: true`, **7 A** — la consigne que l'arbitre lui avait donnée au
cycle d'avant. Le lien revient : **rien ne réapplique**. En mode manuel personne ne commande,
donc l'écart est **permanent**.
**4,4 kW réellement tirés, `draw.committedW` à 0, un budget de surplus de 2 349 W qui ignore ces
watts.**
## 2. Ce que le fait dit vraiment — et il est plus large que son titre
« Réappliquer la commande de l'arbitre » ne décrit pas le cas : **en mode manuel, l'arbitre ne
commande pas**. Ce qui s'est passé est autre chose, et c'est pire :
1. Le proxy amont a voulu restaurer des valeurs manuelles (`false 6 A`) ;
2. l'écriture a échoué pendant l'indisponibilité ;
3. **personne ne l'a rejouée** ;
4. l'organe est resté sur une consigne **dont plus aucun commandeur ne se réclame** — ni
l'arbitre, qui ne commande plus, ni le mode manuel, dont la consigne n'a pas atterri.
> **La troisième forme de la même famille.** Après la télémétrie qui ne distinguait pas une
> issue de son contraire (`ECS-111`) et la valeur de retour qui faisait passer un refus pour un
> succès (`ECS-415`), voici **l'exécution** : une écriture perdue, un état sain en apparence,
> `available` à vrai, et personne pour savoir que la commande n'a jamais atterri.
## 3. Ce que le moteur a déjà — et ce qui manque
| Pièce | État |
|---|---|
| Dernière consigne écrite, charges en watts | **existe** — `m_currentSetpointW`, `m_currentStage` |
| Dernière consigne écrite, **bornes VE** | **MANQUE** — `EvAdapter` ne garde que `m_lastActionAt` |
| Ce que l'organe FAIT | **existe** — `EvCharger::maxChargingCurrent()`, états du Thing |
| Ce que l'organe MESURE, et ce que ça vaut | **existe** — `deviceMeasurement()`, `measurement.source` (`LM-1105`/`LM-1106`) |
| Détection de la FIN d'une indisponibilité | **MANQUE** — le seul chemin est `clearFault()`, actionné par l'opérateur |
| Comparaison commandé / constaté | **MANQUE** — nulle part |
**Le constat porte sur les bornes, et c'est précisément là que la mémoire de la commande manque.**
Ce n'est pas un hasard : les adaptateurs de charges en watts gardent leur consigne parce qu'ils
en ont besoin pour l'écrêtage de verrou ; la borne, elle, était pilotée par le proxy amont, qui
n'a jamais eu à se relire.
## 4. LES DÉCISIONS
**4.1 — Réappliquer, ou signaler ?**
Réappliquer automatiquement une consigne vieille de plusieurs minutes est un geste **dangereux** :
le monde a changé pendant l'indisponibilité — le surplus, l'heure, l'échéance. Rejouer une
décision périmée, c'est décider avec des données mortes, exactement ce que le mode dégradé L2
refuse de faire.
**Recommandation : SIGNALER toujours, RÉAPPLIQUER jamais telle quelle.** Au retour de
disponibilité, le cycle normal reprend la main et décide sur des données fraîches — c'est déjà
le bon comportement. Ce qui manque n'est pas le rejeu, c'est que **l'écart soit dit tant qu'il
dure**. Et il dure indéfiniment quand personne ne reprend la main, ce qui est le cas du relevé.
**4.2 — Que publie-t-on, et sous quelle forme ?**
Un écart n'a de sens que rapporté à ce qu'on sait de l'organe. Proposition : un objet `command`
au niveau de la charge, publié **seulement quand la comparaison est possible** —
```jsonc
"command": { "expectedW": 1380, "observedW": 4400, "divergent": true, "since": "…" }
```
**`measurement.source` conditionne ce que vaut cette comparaison**, et doit donc la gouverner :
contre `none`, il n'y a rien à comparer et l'objet est **absent** — pas `divergent: false`, qui
affirmerait la concordance qu'on ne peut pas constater. Même règle que partout ailleurs ici.
**4.3 — La COMPTABILITÉ : le diagnostic était à corriger, et le vrai défaut est plus grave.**
> **MESURÉ le 2026-09-01, avant d'implanter.** Le constat disait « un budget de surplus qui
> ignore ces watts ». **Ce n'est pas ce qui se passe.** Quand une charge consomme et **obéit**,
> la comptabilité est juste : sa conso est recréditée puis réallouée à elle-même, et les
> suivantes ne reçoivent que le surplus réel (mesuré : export 500 W, A à 4 400 W obéissante → B
> reçoit 500 W). **Corriger cela aurait été corriger un non-défaut.**
**Le vrai défaut : le recrédit est un PRÊT consenti contre l'obéissance.** Son hypothèse est
écrite dans le code — *« le budget est une mesure dont la conso de la charge a déjà été
retranchée »* — c'est-à-dire **« je peux rendre ces watts parce que je peux les reprendre »**.
Rien ne vérifie que l'emprunteur rembourse.
Mesuré, le cas où la promesse est engagée : export 500 W, A consomme 4 400 W, plancher de A à
5 000 W. A est commandée à **0**, et ses 4 400 W sont **redistribués à B**.
| | si A obéit | si A n'obéit pas |
|---|---|---|
| A | tombe à 0 | reste à 4 400 W |
| B | 4 000 W réels | 4 000 W **fictifs** |
| maison | 4 000 W pour 4 900 de production | **8 400 W pour 4 900** → 3 500 W importés |
**Et l'identité de réconciliation TIENT dans les deux cas** — `surplus + recrédit − alloué ==
restant`. C'est ce qui distingue un prêt d'une erreur : la comptabilité est juste, c'est
l'**hypothèse sur laquelle elle repose** qui n'est pas vérifiée. Aucun contrôle de cohérence ne
pouvait donc l'attraper.
**Ce qui change maintenant : l'obéissance est CONSTATABLE.** `command.divergent` (étape 2) dit
si la charge a suivi au cycle précédent. Le recrédit peut donc être **conditionné** — c'est la
proposition, et elle demande une décision parce qu'elle a son propre risque : l'anti-clignotement
existe pour une raison, et le suspendre trop vite ferait osciller une charge saine sur un écart
transitoire. `since` est précisément ce qui permet de distinguer les deux.
**Ce que le §4.3 d'origine proposait — retrancher la conso subie du budget — est SANS OBJET** :
elle en est déjà retranchée, par le compteur. La note est conservée ici pour que personne ne la
réimplante en la croyant oubliée.
**4.3-bis — l'ancienne formulation, conservée pour mémoire.**
`draw.committedW` à 0 pendant que 4,4 kW sont tirés n'est pas un défaut d'affichage : le budget
de surplus est calculé sans ces watts, donc **le moteur alloue de l'énergie qui n'existe pas**.
Une charge que l'arbitre ne commande plus mais qui consomme est de la **consommation fixe**, au
même titre que la base de la maison — c'est exactement le raisonnement d'`ECS-410` pour une
charge gelée, appliqué à une cause nouvelle.
**Recommandation : la retrancher du budget, sans l'allouer.** Elle n'est pas servie, elle est
subie. La compter comme allouée serait prétendre l'avoir décidée.
> **Sur 18 kVA c'est invisible ; sur 6 kVA c'est le disjoncteur.** Et le délestage ne rattrape
> pas ce cas : il agit sur la marge MESURÉE, qui inclut bien ces 4,4 kW — donc il protège. Ce
> qui reste faux est le **budget**, qui promet un surplus déjà consommé.
**4.4 — Faut-il un état « commande en vol perdue » distinct d'un défaut ?**
`ECS-410`/`ECS-414` couvrent l'écriture qui échoue trois fois → défaut. Ici l'écriture a échoué
**pendant une indisponibilité**, et la charge est ensuite redevenue disponible : le défaut se
lève, la divergence reste. À trancher : est-ce un `faultCode` de plus, ou un état orthogonal ?
## 5. ORDRE DE TRAVAIL — deux étapes à comportement constant d'abord
**Étape 1 — la MÉMOIRE de la commande, sans rien en faire.** `EvAdapter` retient sa dernière
consigne appliquée, comme les autres adaptateurs le font déjà. Publication seule, comportement
constant. Sans cette pièce, aucune des suivantes n'est calculable.
**Étape 2 — l'ÉCART publié, sans décider dessus.** L'objet `command`, gouverné par
`measurement.source`. C'est la moitié qui rend le défaut **visible**, et elle ne change aucune
décision — même stratégie qu'à l'étape C du §10, où publier le régime a précédé de décider avec.
**Étape 3 — la COMPTABILITÉ corrigée.** La conso subie sort du budget. C'est ici que le
comportement change, et c'est la seule étape qui le fasse.
**Étape 4 — la reprise en main**, si elle est jugée nécessaire après avoir vu les traces des
étapes 2 et 3. **Délibérément en dernier** : c'est la seule qui commande du matériel, et on
saura alors combien de fois le cas se produit réellement.
---
## 6. CRITÈRE DE TRI DES TRACES — écrit le 2026-09-05, AVANT d'avoir les données
> **Écrit avant exprès.** Un critère rédigé après coup se plie aux résultats : on écarte ce qui
> gêne et on garde ce qui confirme, sans jamais mentir, et sans jamais pouvoir le prouver. Celui-ci
> est daté et poussé avant l'ouverture de la fenêtre d'observation. S'il doit changer ensuite, ce
> sera un commit visible, avec sa raison.
### Pourquoi il faut un tri
Le journal de divergence (`+etm56`) a produit **14 épisodes en 24 h** sur le banc, avant même le
retour de la V2C. Aucun n'est exploitable :
```
08:05 DÉBUT "chauffe-eau" — attendu 3500 W, mesuré 1500 W
08:13 DÉBUT "chauffe-eau" — attendu 500 W, mesuré 1500 W
10:01 DÉBUT "chauffe-eau" — attendu 3000 W, mesuré 1500 W
```
`ECS-Meter` publie **1500 W en permanence**, `PAC-Meter` **800 W** — vérifié sur trois lectures
espacées. Ce sont des simulateurs à valeur fixe. L'écart mesuré n'est donc pas une désobéissance :
c'est la distance entre une consigne qui varie et un compteur qui ne varie pas. La divergence
« finit » quand la consigne passe par 1500 W — une coïncidence arithmétique, pas une obéissance.
**Dimensionner l'étape 4 là-dessus aurait été la dimensionner sur des divergences qui n'existent
pas.**
### Le critère, en deux conditions
Une divergence n'est exploitable que sur une charge dont le compteur **suit réellement la
consigne**. Deux conditions, et les deux sont nécessaires :
1. **`measurement.source` ≠ `none`.** Sans mesure propre, l'écart est calculé contre une
estimation, et il ne dit rien de la charge.
2. **Le compteur doit avoir VARIÉ sur la fenêtre**, indépendamment de la consigne. Un compteur
dont l'écart-type est nul sur la période n'est pas un compteur : c'est une constante déguisée,
et tout écart contre lui est un artefact.
**Ne PAS trier par nom de charge.** Écarter `chauffe-eau` et `pac-terrain` nommément serait
fragile de deux façons : ça se périme au premier renommage, et ça n'attrape pas la troisième
charge à compteur fixe qu'on ajoutera sans y penser. Le critère porte sur une **propriété
observable de la donnée**, pas sur une liste.
### Les faux positifs de la V2C, et ils sont d'une autre nature
La V2C a un vrai compteur, donc elle passe le critère ci-dessus. Mais **chaque perte de lien
produira une divergence qui n'est pas une commande perdue** : c'est une **mesure absente**.
Il faut donc distinguer deux affirmations que l'écart seul confond :
| | ce que ça veut dire | geste |
|---|---|---|
| **elle n'obéit pas** | la commande est partie, la mesure la contredit | c'est le cas de l'étape 4 |
| **on ne sait pas si elle obéit** | la mesure manque ou est périmée | rien à reprendre en main |
**C'est exactement la distinction `none` contre l'absence d'un champ, appliquée à un écart.** Une
mesure absente qui se lit comme une mesure à zéro produit une divergence maximale, et c'est le pire
faux positif possible : il ressemble trait pour trait au cas qu'on cherche.
Concrètement, une divergence dont la mesure vaut 0 pendant que le lien est tombé doit être
**comptée à part**, jamais avec les autres. Le relevé du 2026-08-30 le documente : à la perte du
lien, la V2C publie `currentPower` **0** et ses booléens à `false`, tandis que `sessionEnergy`
reste à sa dernière valeur (`LM-1012-d`). Le zéro est donc **publié**, pas absent — et rien dans
l'objet `command` ne le distingue d'un zéro réel.
> **Conséquence à porter au lot**, et à trancher avant de compter : `command` devrait pouvoir dire
> que son `observedW` n'est pas fiable, plutôt que de laisser déduire. C'est la même parade que
> `progress.regime` pour l'avancement. Tant qu'elle n'existe pas, le tri se fait à la main sur les
> traces, en croisant avec les pertes de lien du journal.
### Ce qui sera compté, et ce qui ne le sera pas
- **Compté** : les épisodes sur une charge à compteur variable, hors fenêtres de perte de lien.
- **Compté à part** : les épisodes concomitants d'une perte de lien — ils mesurent la
disponibilité, pas l'obéissance.
- **Écarté** : les épisodes sur une charge dont le compteur est constant sur la fenêtre.
- **Le compte des écartés est publié aussi.** Un tri dont on ne dit pas ce qu'il a jeté n'est pas
un tri, c'est une sélection.
---
## 7. CONDITIONS DE LA FENÊTRE D'OBSERVATION — week-end du 2026-09-06
Noté pour que les traces de lundi soient lisibles. **Rien n'a été restauré** : la configuration
ci-dessous reste en place tout le week-end.
| | |
|---|---|
| **Posé le** | dimanche 2026-09-06 à **08:19** (Europe/Paris ; box et atelier à la même heure depuis la correction du 2026-09-05) |
| **Charge** | `chauffe-eau` |
| **Changement** | `minEnergyWhPerDay` **3000 → 2000**, `dailyDeadline` inchangé à `"12:00"` |
| **Vérification** | 76 clés comparées entre capture et relecture : 1 divergence attendue, 0 imprévue |
| **Capture d'avant** | conservée hors dépôt, `scratchpad/loadconfig_AVANT_weekend.json` |
| **Moteur** | `1.15.2+etm56` · plugin générique `1.14.2+etm3` |
| **Symboles de débogage** | `libnymea1-dbgsym` / `libnymea-core-dbgsym` arm64 **laissés en place** jusqu'à lundi — donc **aucune couture supplémentaire** pendant la fenêtre |
### Ce que l'écran montre à la pose, et pourquoi ce n'est pas un défaut
À 08:19, le palier éco est `ECO_FLOOR_MET` avec `deliveredWh` = **30 475 Wh** pour une cible de
2 000, `measuredSince` = `2026-09-05T10:00:00Z`. L'obligation est tenue vingt fois.
**C'est arithmétiquement correct et sans intérêt** : le compteur de l'ECS publie 1 500 W en
permanence (cf. §6), donc `deliveredWh` monte de 1,5 kWh par heure que la charge soit commandée ou
non. La période courante a basculé hier à 12:00 ; vingt heures plus tard, elle a « livré » 30 kWh.
### La fenêtre utile est à 12:00, et elle dure ~80 minutes
À chaque bascule de période, la base d'avancement est reprise (`LM-1013-c`). `deliveredWh` repart
de 0 face à une cible de 2 000 Wh sur 24 h — soit une cadence de **83 W**, financée par le réseau
si le surplus manque. Le compteur fixe la comble en **2000 / 1500 = 80 minutes**, après quoi
l'écran repasse à `ECO_FLOOR_MET` pour les 22,7 h restantes.
**Donc deux fenêtres exploitables sur le week-end** : dimanche 12:00 et lundi 12:00, ~80 min
chacune, où l'on voit un vrai plancher éco, son financement et sa cadence. Le reste du temps,
l'écran montre l'état « tenue » — qui est aussi une forme à valider.
> **Et c'est la cinquième forme d'homogénéité qui fixe ce plafond** (`INVENTAIRE_MOCK.md` §7).
> Sur un banc dont le compteur ne suit pas la commande, on ne peut pas avoir à la fois une cible
> **modeste** et un chemin d'achat **longuement exercé** : ce qui décide n'est pas la cible, c'est
> la constance du compteur. Choisir une cible qui tienne l'achat en éveil tout le week-end
> reviendrait à dépasser 36 kWh/jour — c'est-à-dire acheter au plafond, précisément ce qu'on
> voulait éviter. La contrainte est du banc, pas du moteur.