etm-powersync-energy-plugin.../docs/DESIGN_COMMANDE_PERDUE.md
Patrick Schurig 217f20b58f docs: le critère de tri de l'étape 4, écrit AVANT les données
CRITÈRE DE TRI (DESIGN_COMMANDE_PERDUE.md §6), daté et poussé avant l'ouverture de
la fenêtre d'observation. Un critère rédigé après coup se plie aux résultats sans
jamais mentir, et sans qu'on puisse le prouver. S'il doit changer, ce sera un
commit visible avec sa raison.

Deux conditions, toutes deux nécessaires : `measurement.source` != `none`, ET le
compteur doit avoir VARIÉ sur la fenêtre. Un compteur d'écart-type nul n'est pas
un compteur, c'est une constante déguisée, et tout écart mesuré contre lui est un
artefact.

PAS de tri par nom de charge. Écarter chauffe-eau et pac-terrain nommément serait
fragile deux fois : ç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.

LES FAUX POSITIFS DE LA V2C sont d'une autre nature, et il faut les séparer avant
de compter. Elle a un vrai compteur, donc elle passe le critère — mais chaque perte
de lien produit une divergence qui n'est pas une commande perdue : c'est une mesure
absente. « Elle n'obéit pas » et « on ne sait pas si elle obéit » sont deux
affirmations que l'écart seul confond.

C'est la distinction `none` contre l'absence, appliquée à un écart. Et le pire cas
est documenté : à la perte du lien la V2C publie currentPower = 0 (LM-1012-d), donc
le zéro est PUBLIÉ, pas absent, et rien dans l'objet `command` ne le distingue d'un
zéro réel. Il produit la divergence maximale — le faux positif qui ressemble trait
pour trait au cas cherché. Conséquence portée au lot : `command` devrait dire que
son observedW n'est pas fiable, comme progress.regime le fait pour l'avancement.

Le compte des écartés sera publié aussi : un tri dont on ne dit pas ce qu'il a jeté
n'est pas un tri, c'est une sélection.

CINQUIÈME FORME D'HOMOGÉNÉITÉ DU BANC (AGENTS.md + INVENTAIRE_MOCK.md), et elle
inverse le mécanisme. Les quatre précédentes MASQUENT un cas ; celle-ci en FABRIQUE
un faux. Un cas masqué se signale par un test qui ne mord pas ; un cas fabriqué se
signale par des données qui rassurent.

14 épisodes en 24 h, de quoi croire le phénomène fréquent et dimensionner un lot
dessus. L'instrument fonctionne, la donnée est réelle, le compte est juste — seule
la question à laquelle ces nombres répondent n'est pas celle qu'on croyait poser,
et un nombre n'annonce jamais de quelle question il est la réponse.

Manque noté au mock : aucun compteur dont la valeur SUIVE la commande de la charge
qu'il mesure. Tant que c'est le cas, aucun test ne peut exercer une charge qui
obéit, donc aucun ne peut distinguer l'obéissance de la désobéissance.
2026-09-05 16:54:38 +02:00

13 KiB
Raw Blame History

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 —

"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.