SgReadyAdapter::applyAction() renvoyait, quand le verrou minStateHold
refusait la transition, l'action DEMANDÉE telle quelle. Un appelant qui
relisait `state` y retrouvait l'état qu'il venait de demander et concluait
au succès — alors que rien n'avait été écrit et que les contacts n'avaient
pas bougé.
Ce n'est pas un défaut du délestage, c'est un défaut de contrat. N'importe
quelle descente d'état pouvait échouer en silence, et `available` restait
vrai : la charge avait l'air saine pendant que la PAC tirait 3 kW. Même
famille qu'ECS-111 — une issue indiscernable de son contraire — transposée
au retour d'une commande au lieu de la télémétrie.
Toute sortie décrit désormais l'état RÉELLEMENT tenu, `estimatedPowerW`
compris. L'idempotence n'y échappe pas : demander « état 4, 0 W » à une PAC
qui en tire 3000 doit rendre 3000, pas le zéro de l'appelant — sinon il lit
son enveloppe, pas une réponse. Même correction sur la branche « motif
vide » du RelayRouter et de l'EtmVariableLoadAdapter.
Le défaut était LATENT : aucun appelant ne lisait ce retour, le dispatch
l'ignore. Le premier à s'y fier fut le délestage L4 en cours d'écriture, qui
a publié 3 kW rendus par une PAC restée en état 4. Un contrat qu'aucun
appelant n'exerce n'est pas un contrat tenu, c'est un contrat non testé.
Deux pistes écartées, mesurées et non supposées. m_currentState n'était pas
en cause : il est écrit synchroniquement, sans attendre d'acquittement.
ECS-411-b non plus : la relecture des contacts répond à une divergence entre
état interne et matériel, et il n'y en avait aucune — l'état interne était
juste, c'est le retour qui mentait.
Les trois pièges rencontrés à l'étape 4 sont consignés dans
DESIGN_DELESTAGE §6bis, les deux placements écartés compris. L'étape 4
elle-même reste non livrée.
Suite complète : simulation 53/53, charging 48/48, loadmodel 21/21,
spotmarket 32/32, doxygen 0 avertissement. Contre-épreuve : sur l'ancien
retour, refuse.state vaut 2 — l'état demandé — au lieu de 4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Le comportement était « non défini » depuis l'écriture de la spec. telemetry()
SAUTAIT un relais qui ne résout plus : ou bien plus aucun relais actif ne
mesurait et l'on retombait sur le nominal du PALIER ENTIER — part du fantôme
comprise — ou bien des survivants mesuraient et l'on publiait une somme
sous-évaluée.
Et ce n'était pas de l'affichage : telemetry().currentPowerW alimente le recrédit
anti-clignotement, donc le BUDGET. L'arbitre se trompait sur le surplus
disponible pendant qu'available restait vrai. Un mensonge qui décide, pas un
mensonge qui s'affiche.
Option (a), la seule cohérente avec la famille : available faux, faultCode
THING_MISSING, traitement ECS-410 — sortie de l'ARBITRAGE, maintien dans la
COMPTABILITÉ, figée à la puissance crue engagée. La puissance est donc RETIRÉE du
budget des suivantes au lieu de leur être recréditée. Aucune somme partielle.
(b) — compter 0 W et servir le reste — est écartée : un relais absent du registre
est un Thing supprimé ou un plugin qui ne le voit plus, on ne sait pas dans quel
état est le contact, et servir le reste reviendrait à supposer.
Jugé sur TOUS les relais déclarés, jamais sur ceux du palier courant — sinon la
disponibilité basculerait au rythme du thermostat. Même règle que
deviceMeasurement(), qui traitait DÉJÀ le cas correctement : aligner l'ancien sur
le neuf, pas inventer une politique.
THING_MISSING prime sur WRITE_FAILED : un relais absent EXPLIQUE les écritures
qui échouent, et annoncer WRITE_FAILED enverrait l'opérateur au bus quand la
cause est dans la configuration. D'où le corollaire, écrit dans la doc de
clearFault : ClearLoadFault lève le défaut d'ÉCRITURE, il ne fait pas exister un
Thing.
testEcsPartialFailure a dû être ajusté et c'est instructif : il utilisait un
relais JAMAIS CONFIGURÉ comme raccourci pour forcer un échec d'écriture. Le
raccourci a maintenant un sens propre, et le test distingue les deux causes — le
défaut collant se lève, le câblage manquant non.
Trouvé ni par un test ni par le banc, mais par la LECTURE d'une règle que l'index
a rendue visible. Consigné comme tel dans spec_ecs.md et AGENTS.md : c'est le
premier défaut réel exhumé par le chantier de documentation.
simulation 33/33, charging 16/16, loadmodel 20/20, amd64 0/0, doxygen 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Étapes 1 et 3 du chantier de documentation.
DIX NUMÉROS RETIRÉS — ECS-100/101/102/300/301/304/400/402/403/404. Ils
n'existaient QUE comme une étiquette de trois mots dans une table de statut :
aucun énoncé, nulle part. Leur reconstruire un texte depuis l'étiquette et le
code aurait été inventer une exigence en croyant la restituer, et une règle
inventée qui porte un numéro fait autorité pour toujours. Si le texte d'origine
n'existe nulle part, la règle n'existe pas ; ce que le code fait reste vrai, il
n'a simplement plus de règle à citer.
Retirés, pas effacés : tools/rules-retired.txt les porte, l'index les publie avec
leur date, leur motif et les endroits qui les citent encore. Un identifiant
croisé dans un vieux relevé renvoie donc à quelque chose — RELEVE_ECS306.md cite
toujours ECS-404, et c'est bien. Un numéro retiré n'est JAMAIS réattribué : le
générateur refuse une définition qui en reprendrait un, une même référence ne
doit pas désigner deux exigences selon la date de lecture.
Les citations VIVANTES de ces numéros sont retirées (spec_ecs §2/§4/§5,
AGENTS.md) — les phrases tiennent sans elles. Les citations HISTORIQUES restent.
DEUX TABLES SUPPRIMÉES, pas corrigées. §1 déclarait ECS-410/411/412 « absents »
alors que les trois étaient implémentées et testées. §9 « Traçabilité » nommait
HUIT tests qui n'ont jamais été écrits. Son défaut n'était pas d'être périmée
mais de mélanger un CONSTAT et une INTENTION sous la même colonne — « ECS-306 →
testEcsBudgetUnderLock » est un fait, « ECS-303 → banc » est un projet ; réunis,
le second garantit que l'ensemble devient faux. Les deux sont séparés : le
constat est généré, l'intention devient « Ce qui reste à éprouver », avec pour
chaque exigence POURQUOI elle n'est pas éprouvée.
DOXYGEN (étape 3). doc-generated/RULES.md entre en INPUT — ses ancres {#LM-1105}
rendent \rule{} résoluble — et l'alias \rule{1} est défini. Les specs restent
HORS périmètre : les y ajouter ferait entrer deux fois le même énoncé et
doublerait les ancres. Les 250 citations existantes ne sont pas converties : le
générateur les voit déjà, et \ref sert l'autre sens — du commentaire vers la
règle. Éprouvé sur un cas réel, \rule{LM-1209-c} résout sans avertissement.
Piège d'ordonnancement corrigé au passage : gen-rules.py écrit DANS $SORTIE, donc
l'appeler avant `rm -rf "$SORTIE"` l'effaçait aussitôt — doxygen signalait
« RULES.md is not a readable file ». L'index se produit après le nettoyage. Son
code de retour ne stoppe pas la génération (un index incomplet reste utile à
lire) mais est répercuté à la fin.
Avertissements doxygen : 30 → 26, dont 3 étaient les miens — rankOrigin(),
setRankOrigin() et knownRankOrigins() n'étaient pas documentés (DoD 5).
L'index sort désormais RC=0 : 111 règles, aucun orphelin, aucun domicile ambigu.
69 spécifiées seules, 16 citées par un test, 14 par le code, 12 vérifiées sur
machine. Build 0/0, loadmodel 20/20.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Le bloc d'enregistrement codé en dur d'energypluginnymea.cpp est supprimé, avec
registerSgReadyAdapter(), la table m_sgReadyAdapters et claimedRelays(). Il n'existe plus
aucun chemin d'enregistrement en dur : m_loadAdapters est la table UNIQUE des charges
arbitrées, et toutes ses entrées viennent de LoadConfigStore.
Contrat : loads[] de GetLoadTelemetry est désormais INCLUS dans GetLoadConfig, par
CONSTRUCTION et non par discipline. La règle de détection publiée à +etm14 — « un loadId sans
équivalent dans GetLoadConfig est une charge non configurable » — est retirée d'INTERFACE.md :
elle n'avait qu'un porteur, pac-terrain, qui vient de disparaître. Aucun configurable:false
n'est ajouté, il n'aurait plus rien à signaler. L'inclusion ne vaut que dans ce sens : une
charge enabled:false est configurée sans être publiée.
SÉCURITÉ — repli L2 d'une PAC de configuration, trou ouvert par LM-300 et refermé ici. Le
repli envoyait un Setpoint(0) à toute la table et un State(2) à une table séparée réservée à
la PAC en dur ; une PAC construite depuis la CONFIG vivant dans la première recevait donc une
action que SgReadyAdapter rejette d'entrée, et rien d'autre — elle restait en état 4 sous
compteur muet quand SAFETY.md §L2 exige l'état 2. Le motif DEGRADED_L2 s'affichait
correctement : seule l'écriture manquait. Le repli lit maintenant supportedKinds (LM-301-b).
ECS-411/-411-b au SG-Ready : l'état de départ est relu sur les contacts, avec relevé contact
par contact et l'issue — y compris quand l'issue est l'état neutre. ECS-412 au minStateHold :
armement paresseux au démarrage à froid, jamais permanent.
Harnais de test : JournalCapture se chaînait sur elle-même (récursion infinie à croissance
quadratique dès la deuxième capture vivante, donc OOM) ; tm/owner non initialisés sur trois
sites de freshSetup().
Vérifié sur .75 avant publication — docs/RELEVE_LOTBbis.md. Suites : simulation 85,
charging 74, spotmarket 44, zéro échec, un processus par test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
ClearLoadFault est le SEUL levier de reprise à distance : ECS-410 pose un verrou collant que
seuls ce RPC ou une reconstruction lèvent. L'arbitre journalisait la demande de l'opérateur,
jamais son issue — si bien qu'appelé depuis l'étranger, on ne distinguait pas « le défaut a
été levé » de « il n'y en avait pas » de « l'appel n'a pas atteint l'adaptateur ». L'opérateur
relance trois fois et conclut que le système est cassé.
L'issue est écrite dans l'ARBITRE, pas dans chaque adaptateur : une ligne là couvre les quatre
d'un coup et ne peut pas être oubliée par un adaptateur futur ; répétée dans chacun, elle
manquerait le cinquième. Les trois cas sont distincts à la lecture.
ILoadAdapter::clearFault() rend un bool au lieu de void. Le type de retour force chaque
implémentation, présente et future, à répondre à la question « un défaut a-t-il réellement été
levé ». EvAdapter, qui n'a pas de mécanisme de défaut, le dit désormais explicitement plutôt
que par un corps vide.
Règle 7-d — un refus doit être au moins aussi visible que l'application correspondante. Le
refus par verrou minStateHold de SgReadyAdapter était en qCDebug quand l'application, dix
lignes plus bas, était en qCInfo. L'asymétrie est le défaut, pas le niveau : un resserrement
de la journalisation — et on vient d'en faire un pour tenir deux semaines — aurait fait
disparaître le refus AVANT le succès, ne laissant au journal que les décisions abouties. Or un
refus est plus informatif qu'une application réussie : il dit qu'une décision a été prise et
n'a pas été exécutée. Vérification faite sur les autres adaptateurs : c'était le seul cas. Les
qCDebug restants sont symétriques, et le marqueur L2 par cycle reste en debug à dessein, son
entrée étant en qCWarning et sa sortie en qCInfo.
Le retour idempotent du routeur reste DIFFÉRÉ, et ira en qCDebug : c'est le cas le plus
fréquent — plusieurs milliers de lignes par jour sur un budget ramené à 6,5 Mo — et son
ambiguïté est partiellement couverte, le scheduler journalisant sa décision et l'armement à
froid s'exécutant avant ce retour. Faible gain, coût réel : l'inverse des deux cas corrigés.
Le test porte sur le TEXTE des trois issues et vérifie qu'elles sont deux à deux distinctes.
Suite complète : 112 tests, 0 échec.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le smartmeter Modbus ECS est câblé et vérifié depuis le 2026-08-09 : d'où vient la mesure
n'est plus une décision à prendre, et exposer un second Eastron simulé reviendrait à simuler
ce qu'on possède en vrai.
L'ordre est posé : appairer les compteurs réels sur .75 (root-meter maison, producteur,
smartmeter ECS, PAC si prêt), puis retirer la vue SunSpec simulée du rôle de rootmeter — en
établissant d'abord ce que devient la configuration de l'arbitre quand il change, tout en
dépend — et vérifier que le compteur ECS n'est jamais désigné rootmeter.
Au 2026-08-13, things.conf ne déclare que la vue Fronius/SunSpec : aucun de ces compteurs
n'est appairé.
ECS-500 différé : le calibrage améliore la précision, pas l'interprétabilité.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Le critère était « consigne > budget ». Il est vrai dès que le budget passe sous zéro à
palier nul — 0 > -389 — et le motif annonçait alors « Verrou minOn — chauffe-eau maintenue
à 0 W (puissance engagée, budget -389 W) » : un verrou qui ne mordait pas, sur une
puissance engagée nulle, quand le soutirage était la seule cause.
Constaté au banc le 2026-08-13 sur +etm10, sept fois en quatre heures, toujours au cycle
suivant un palier 0. Le défaut est antérieur à ECS-309 : celui-ci a ajouté la branche
minOff après la branche minOn sans voir que la première capturait déjà un cas étranger.
Corriger une branche ne dit rien de ses voisines.
Le critère compare désormais la consigne appliquée à la consigne VOULUE avant écrêtage —
la mémoire qu'ECS-309 avait justement introduite. Relevée par le verrou → minOn ; rabaissée
→ minOff ; inchangée → le budget. Les deux branches se lisent sur le même axe, et le motif
nomme un mécanisme parce qu'il a agi, non parce qu'il aurait pu.
Ce n'était pas qu'un défaut de lisibilité. La branche minOn étant évaluée en premier, elle
masquait le motif d'armement à froid d'ECS-412 chaque fois que le budget était négatif au
redémarrage — c'est-à-dire au fond du creux, précisément l'instant où il faut redémarrer
pour observer minOff. La campagne du volet 2 aurait mesuré le mauvais verrou.
Le test reproduit le cas du banc : palier 0, budget négatif, aucun verrou actif. Sans le
correctif il produit le texte exact relevé sur la machine. Suite complète : 112 tests
(34 simulation + 46 charging + 32 spotmarket), 0 échec.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le principe dégagé par les tests d'ECS-411 puis d'ECS-414 ne vit pas sous ECS-414 :
il vaut pour tout mécanisme, présent et futur. Il est donc inscrit en LM-104
(spec_loadmodel.md §1), ECS-411 et ECS-414 n'en étant que deux applications.
Les deux moitiés sont indissociables, et c'est l'omission de la seconde qui piège :
supposer « contact fermé » puis en déduire « donc rien à écrire » transforme une
hypothèse de prudence en masquage de panne. La prudence porte sur ce qu'on DIT de
l'installation, jamais sur ce qu'on lui ENVOIE.
Consigne aussi : le tableau ECS-110 (les deux Q_ASSERT de sgreadyadapter sont
supprimés, l'échéance prospective est tombée), la charge utile sg-ready réellement
implémentée en LM-300, et le statut de spec_loadmodel.md — §3 n'est plus une intention.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Même modèle qu'ECS-410, rien de nouveau conçu : m_pending, verdict à zéro,
échelle bornée à trois barreaux, `this` en contexte de connexion.
PLANCHER PROPRE À CHAQUE ADAPTATEUR. Pour EtmVariableLoadAdapter, consigne 0 W.
Pour SgReadyAdapter, le barreau 3 est l'ÉTAT 2 — pas « tous contacts ouverts ».
Ouvrir les deux contacts d'une PAC est une COMMANDE, et selon l'encodage câblé
c'est potentiellement le BLOCAGE, donc l'inverse d'une mise en sécurité.
ATOMICITÉ DU REPLI — le point qui n'existait pas sur le routeur relais. Un état
SG-Ready est porté par deux bits ; si une écriture échoue, on est dans un motif
valide mais NON VOULU, et le retour depuis ce motif peut exiger de traverser le
blocage. applyStateRelays() prend désormais un ENSEMBLE de relais réellement
fermés au lieu d'un index d'état, de sorte que transientHarm ordonne le repli
exactement comme il ordonne l'aller. Sans cela, la récupération serait plus
dangereuse que la panne qu'elle corrige.
Le repli écrit les contacts directement, sans repasser par applyAction : le verrou
minStateHoldS n'est donc jamais consulté — équivalent d'un force = true, comme le
repli L2. À 900 s sur une PAC, attendre un quart d'heure pour sortir d'un état non
voulu ne serait pas défendable.
DEUX INCOHÉRENCES TROUVÉES PAR LE TEST, et corrigées :
1. readActualOn() traitait un contact injoignable comme OUVERT, alors qu'ECS-411
le suppose FERMÉ. Conséquence : un repli ne demandant aucune écriture
« réussissait » à vide alors qu'on ne savait rien du matériel.
2. Corollaire inverse : une fois supposé fermé, un contact injoignable n'était
plus jamais écrit quand la cible le voulait fermé — l'échec était masqué et
la transition paraissait réussie sans qu'aucune écriture n'ait été tentée.
Un contact non vérifiable est désormais TOUJOURS commandé.
Test testSgReadyPartialFailure. Ce qu'il verrouille n'est pas le défaut mais le
PLANCHER : le repli ramène K1 à l'ouverture, soit l'état 2 sur cet encodage. Un
plancher « contacts ouverts par principe » aurait pu, sur un autre câblage, valoir
l'état 1 — le blocage. Vérifie aussi que minStateHoldS = 900 s ne retarde pas le
repli, que la charge figée a plancher == plafond, et que la levée relit l'état.
Une correction de mon propre scénario au passage : j'avais écrit un cas
« récupération au barreau 2 » qui n'est pas constructible avec un contact
définitivement injoignable — rien ne peut alors être confirmé, et le défaut est
l'issue honnête.
Build amd64 0 erreur. Simulation : 18/18.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ECS-410 n'est implémenté que par RelayRouter. SgReadyAdapter et
EtmVariableLoadAdapter jettent toujours le ThingActionInfo* retourné par
executeAction et gardent donc le défaut d'origine — annoncer un état non appliqué.
Rattaché au lot de mise en configuration du SgReadyAdapter, qui vient ensuite et
touche les mêmes fichiers. Les points d'accroche (clearFault, applySafeState) sont
déjà déclarés ; seul le suivi asynchrone manque.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exigence seule, aucune implémentation.
Constat de banc du 2026-08-09 : un SetLoadConfig posant enabled: false sur une
charge alors au palier 3500 W a détruit l'adaptateur en laissant les TROIS RELAIS
FERMÉS. Plus personne ne les commandait ; ils y seraient restés indéfiniment,
juste avant une intervention de câblage.
ECS-413 exige que désactiver une charge — ou la retirer de la configuration —
laisse son matériel dans l'état sûr DÉFINI PAR SON ADAPTATEUR, appliqué AVANT la
destruction de l'adaptateur, l'échelle d'ECS-410 s'appliquant si l'écriture échoue.
L'état sûr est propre à chaque adaptateur, et une formulation « tout couper »
serait fausse : relais ouverts pour RelayRouter, consigne 0 W pour
EtmVariableLoadAdapter, mais ÉTAT 2 pour SgReadyAdapter — jamais le blocage,
conformément à SAFETY.md. Couper une PAC en la bloquant serait une régression de
sécurité, pas une mise en sécurité.
Mécanisme : le chemin d'action normal avec force = true, celui du mode dégradé L2.
Rien de nouveau à inventer.
Périmètre explicitement borné : NE S'APPLIQUE PAS à l'arrêt du plugin ni à un
redémarrage de nymead, où l'état doit être CONSERVÉ — c'est ce qu'ECS-411 relit au
démarrage, et couper l'eau chaude à chaque redémarrage de service serait une
régression. La distinction est intentionnelle : la désactivation est un acte
délibéré de l'opérateur, un redémarrage n'en est pas un.
Test testEcsDisableLeavesSafeState, avec son cas négatif obligatoire : un rebuild
SANS désactivation ne doit pas déclencher la mise en sécurité, sinon ECS-412 est
annulé et chaque changement de rang coupe la charge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deux corrections de fond.
1. « Les 500 W consommés de plus que comptés sont démontrés » allait un cran trop
loin. Dans le cycle à verrou expiré, le routeur applique RÉELLEMENT 3000 W : il
n'y a aucun écart consommé/compté dans ce cycle-là. Ce qui est démontré
directement, c'est la moitié comptable — au même budget, honorer ou ignorer le
verrou change le résidu de 500 W et fait basculer la charge de rang 3. L'autre
moitié est observée dans le PREMIER cycle. Le résultat tient par composition de
deux moitiés observées dans deux cycles différents du même binaire : solide,
mais pas une observation directe. Un +etm2 réel reste le seul moyen de voir les
deux dans le même cycle.
2. Rétractation erronée corrigée. J'avais écrit que la question du compteur ECS ne
se posait pas, LoadConfig n'ayant aucun champ de rattachement. Le danger n'a
jamais été là : si le compteur de la charge est désigné comme ROOTMETER dans
nymea, sa puissance entre dans le bilan de surplus que l'arbitre lit par
internalRootMeter(), et le budget est faussé pour toutes les charges. Aucune
configuration n'est nécessaire — une désignation dans l'interface suffit.
Inscrit sous ECS-504 comme piège de mise en service, applicable au câblage du
compteur Modbus prévu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Défaut constaté AU BANC le 2026-08-09, pas en test : une charge sonde de rang 3
est restée figée à 0 W sous 4 kW de surplus disponible.
Mécanisme. lockWindow() traitait un m_lastSwitch nul comme sentinelle
« elapsed = 0 » à CHAQUE cycle. Une charge démarrant au palier 0 avec
minOffS > 0 voyait donc offHeld vrai en permanence, maxStage forcé à 0 : elle ne
pouvait jamais s'enclencher, donc jamais commuter, donc jamais valider
m_lastSwitch. Blocage circulaire. L'intention d'ECS-412 — armer plutôt que
purger — était juste ; mon écriture rendait l'armement DÉFINITIF au lieu de
transitoire.
Portée réelle : toute charge relay-router démarrant relais ouverts avec
minOffS > 0 était gelée. Le chauffe-eau du banc (minOffS = 60) n'y échappait que
parce que ses relais étaient déjà fermés et qu'ECS-411 lui faisait reprendre un
palier non nul — au premier démarrage sur installation froide, il aurait été
bloqué lui aussi.
Correction : armement PARESSEUX. applyAction(), chemin non-const qui reçoit le
temps de cycle, estampille m_lastSwitch au premier now s'il est invalide. Le
verrou expire alors après sa durée configurée. Le repli « elapsed = 0 » ne
couvre plus que les cycles précédant la première action. L'invariant « temps =
paramètre, jamais l'horloge » est préservé : aucune horloge n'est lue, et le
contrat d'ILoadAdapter n'est pas modifié — passer now au constructeur aurait
changé l'interface pour un cas particulier.
Symétrie vérifiée : relais fermés au départ → ECS-411 donne un palier non nul,
c'est minOn qui s'arme ; relais ouverts → palier 0, c'est minOff. Les deux sont
désormais transitoires de la même façon.
Test — le vrai livrable : testEcsColdStartLockExpires. Palier 0 au départ,
minOffS = 120, budget de 5000 W. La charge reste éteinte à t0 et à t0+119, puis
s'enclenche à t0+121. Couvre aussi l'expiration de la fenêtre exposée au
scheduler (lockMaxPowerW : 0 pendant le verrou, 1000 après). Aucun test ne
combinait « palier 0 au départ » et « minOffS > 0 » — c'était le trou exact.
spec_ecs.md : ECS-412 précise que l'armement est transitoire, d'une durée égale
au verrou configuré, posé paresseusement, avec le tableau de symétrie et le
renvoi au test.
Build amd64 0 erreur. Simulation : 15/15.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La configuration de la box de test déclarait 2 relais à 600/1200 W ; elle en
déclare désormais 3 à 500/1000/2000, posés par SetLoadConfig (RPC, rebuild à
chaud). Trois relais GPIO, Relay1/2/3 ; Relay4/5 restent au SG-Ready.
Formulation retenue : « réglée pour ÉMULER l'installation de référence », pas
« alignée sur le câblage réel ». La différence n'est pas rhétorique — elle dit
que la vérification physique en attente (INST-102) porte sur l'installation
CLIENTE, pas sur les broches de .75. Mesurer le banc à la pince ne confirmerait
que le banc.
Fait établi au passage : le câblage de .75 était DÉJÀ documenté dans
docs/TEST_TERRAIN.md:24-26 sous les noms R500/R1000/R2000, broches BCM 5/6/13,
avec les mêmes ThingId. La configuration à 600/1200 était donc un reliquat,
contredit par la documentation du banc elle-même — ce n'est pas la spec qui a été
imposée au banc, c'est le banc qui a été remis d'accord avec sa propre doc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Correction. La 0.5.1 avait corrigé le câblage en 500/1000/1500 sur la foi du
simulateur du banc ; le simulateur ne reflète pas l'installation. Le câblage réel
est 500/1000/2000. Quatrième révision de cette donnée.
Les trois consignations reviennent à l'état de la 0.5.0 :
- ECS-302 NON exercé — pondération binaire, aucune redondance d'encodage. Il
perd son caractère de préalable structurel et redescend derrière ECS-305 dans
l'étape 3 ;
- ECS-303 RÉTABLI, mais reformulé sans puissances : « hystérésis élargie aux
frontières de recombinaison », déclenchée dès qu'un passage de palier impose
de basculer plus de deux relais ;
- 8 valeurs distinctes, plafond 3500 W, usure 7/3/1 par balayage.
DÉCOUPLAGE — c'est le vrai objet de cette version. Les puissances ne vivent plus
que dans un bloc unique, §4.0 « Installation de référence », qui porte les trois
valeurs, leur source et leur date, ainsi qu'un tableau « ce que cette
installation exerce et ce qu'elle n'exerce pas ». Aucune exigence ne contient
plus de puissance : celles dont le rang ou la portée en dépendent le disent par
renvoi. Vérifié — hors §4.0, il ne subsiste que le changelog, le journal et
l'illustration P = U²/R d'ECS-500.
Motif : ce paramètre a fait quatre allers-retours, chacun coûtant la réécriture
de trois exigences. Le cinquième ne coûtera qu'un paragraphe.
Source inscrite telle qu'elle est : DÉCLARATION DE L'UTILISATEUR, NON MESURÉE.
Une vérification physique — plaque signalétique ou pince ampèremétrique — est en
attente et reste un préalable à l'ouverture de l'étape 3.
Note ajoutée sous ECS-411 : sur cette installation les encodages sont uniques,
donc le modèle d'état ne peut pas diverger du physique. Sur un câblage à
encodages multiples, une correspondance par puissance seule renverrait un palier
dont l'encodage canonique diffère de l'ensemble réellement fermé — la reprise
doit alors restituer l'ensemble de relais, pas seulement l'index.
Le simulateur du banc sera aligné sur cette installation dans un lot SÉPARÉ, sur
le dépôt etm-powersync-hems-sim.
Aucun code touché.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La 0.5.0 consignait 500/1000/2000 sur la foi de la consigne d'étape 1. Le
simulateur du banc contredit cette donnée ; arbitrage : le simulateur reflète
l'installation, la donnée était fausse.
Câblage : 500/1000/1500 W, additif, 7 valeurs distinctes de 0 à 3000 W, avec UNE
redondance — 1500 W admet {R1500} et {R500+R1000}.
Trois corrections :
1. ECS-302 est EXERCÉ. Il redevient préalable structurel de l'étape 3 et reprend
son rang devant tout ce qui s'appuiera sur m_relayMapping.
2. ECS-303 SUPPRIMÉ. Le « point dur » 1500→2000 n'est pas une exigence
d'hystérésis, c'est une conséquence directe d'ECS-302 non traité :
relayrouter.cpp:58 retient la première combinaison rencontrée (masque le plus
bas), donc {R500+R1000} pour 1500 W, et jette {R1500}. La transition vers
2000 W = {R500+R1500} demande alors d'ouvrir R1000 et de fermer R1500, avec le
creux qu'impose l'ordre coupure-avant-fermeture. Avec {R1500} pour 1500 W, la
même transition ne coûterait QU'UNE commutation : fermer R500. Aucune entrée
ECS-303 ni ECS-605 de ce côté.
3. 7 valeurs et plafond 3000 W, non 8 et 3500. ECS-305 reste valable, comptes
corrigés : par balayage complet R500 commute 5 fois, R1000 3 fois, R1500 une
seule (vérifié sur l'encodage effectivement retenu par le code actuel).
RÉSERVE INSCRITE : la source est le simulateur (sim/sim_ecs_router.py:38, sommé
dans _power_w(), confirmé en vol sur MQTT), PAS une mesure physique. Ce chiffre a
déjà changé trois fois et il commande désormais l'ordre de l'étape 3 : il doit
être confirmé sur plaque signalétique ou au pince ampèremétrique AVANT
l'ouverture de cette étape. Consigné en §13-3 et en garde-fou sous l'étape 3.
Aucun code touché.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Précondition de la consigne d'étape 1 : les deux arbitrages étaient RELEVÉS au
journal, pas TRANCHÉS. Ils le sont maintenant, datés, dans les termes reçus.
§13-1 CLOS — le schéma « ARCHITECTURE CIBLE » d'AGENTS.md a été redessiné par le
lot de consolidation (f6a4be5) : RelayRouter et EtmVariableLoadAdapter y
figurent, le kind Stage en est retiré, SocketScheduler et BatteryAdapter sont
marqués non écrits.
§13-2 CLOS, lecture (b) — ECS-412 réduit à m_lastSwitch et redirigé vers sa cause
racine (rebuild incrémental : ne reconstruire que ce qui a changé) ; ECS-411
remonté en étape 1. L'étape 1 porte donc TROIS exigences : ECS-306, ECS-411,
ECS-412. Ajouté explicitement : au démarrage à froid, m_lastSwitch étant
irrécupérable, le verrou DOIT être ARMÉ et non purgé — l'implémentation naturelle
(QDateTime nul = verrou inactif) fait l'inverse et court-circuiterait la
protection compresseur sur une boucle de redémarrage.
Câblage du banc confirmé 500/1000/2000 (pondération binaire, 8 paliers de 0 à
3500 W au pas de 500, aucune redondance). Trois conséquences consignées :
- ECS-302 n'est PAS exercé par ce matériel — un seul encodage par palier. La
restructuration de m_relayMapping perd sa justification immédiate et cesse
d'être un préalable structurel de l'étape 3.
- ECS-305 monte en importance : sur un balayage complet R500 commute 7 fois,
R1000 3 fois, R2000 une seule. C'est la seule mesure d'usure utile.
- ECS-303 CRÉÉ — point dur 1500→2000 W : les trois relais basculent d'un coup,
donc trou de puissance à chaque franchissement avec l'ordre
coupure-avant-fermeture. La parade est un seuil d'hystérésis élargi à cette
frontière, pas un réordonnancement. À MESURER au banc avant de coder.
Aucun code touché.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deux documents normatifs, préalables au lot AGENTS.md qui les référence.
spec_ecs.md 0.4.3 — spécification ECS multi-palier, écrite après audit du code :
- 0.4.1 : statut réel des binaires de test, portée du point de gouvernance
AGENTS.md, citation de règle dans ECS-306, recouvrement ECS-411/ECS-412 ;
- 0.4.2 : étape 2 « type domaine » → « noyau de calcul » (collision avec LM-100) ;
- 0.4.3 : ECS-110 étendu — la validation ne doit pas reposer sur Q_ASSERT,
absent du binaire release (QT_NO_DEBUG).
spec_loadmodel.md 0.1.1 — modèle de charges (domaine × mécanisme), intention de
conception. Ne déclenche aucun travail.
Aucun code touché.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>