Trouvé au banc le 2026-08-27, en exerçant 3g-2 (docs/RELEVE_3g2.md §3.1).
Une wallbox en Eco, voiture assignée, mais `pluggedIn: false`, s'est vu allouer
4 853 W qu'elle ne pouvait pas tirer — `currentPower: 0`, `charging: false`. La
PAC derrière elle n'a survécu que sur son propre recrédit. Trois cycles
consécutifs : 2 350 / 4 853 / 5 464 W retirés du budget et donnés à personne.
evParticipatesInWaterfall() reprenait le filtre d'entrée du proxy — voiture
assignée, mode non manuel — mais pas `pluggedIn`. Le proxy n'en avait pas
besoin : son allocation n'était pas une cascade, donc gaspiller ne privait
personne. En cascade, si. L'asymétrie était déjà visible dans le code : la
boucle EV du scheduler, qui sert les financements réseau, teste
`!ev->pluggedIn()` depuis toujours ; seul le chemin waterfall ne le faisait pas.
C'est le défaut du relevé 3g-1 §3.4 par une autre porte : là le plancher
mentait, ici c'est le filtre d'entrée. Défaut de 3g-1, révélé par 3g-2 parce
que la décision atteint enfin le matériel — et corrigé à part du lot, comme les
quatre précédents.
Hors arbitrage, et non « servie à 0 avec un motif » : même famille que les deux
autres exclusions (DESIGN_3g §4). Une borne débranchée ne consomme rien, sa
consommation nulle est déjà au compteur, et lui donner un rang la ferait
apparaître dans la cascade pour n'y rien faire. Le journal l'annonce comme les
deux autres cas.
testUnpluggedChargerTakesNoBudget, VÉRIFIÉ ÉCHOUANT SANS LE CORRECTIF : la
borne débranchée disparaît du contexte et la charge suivante reçoit les 3 000 W
entiers ; la seconde moitié rebranche le véhicule pour prouver que c'est bien
`pluggedIn` qui décide, et non une exclusion fortuite.
Suite complète : simulation 28/28, charging 16/16, loadmodel 16/16,
spotmarket 7/7. Build amd64 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
`1.15.2+etm23` construit arm64, posé sur `.75` par deploy.sh, exercé.
FRANCHI, journal et RPC à l'appui :
- Une charge, un commandeur. 9 lignes « Executing action Simulated wallbox »
sur la fenêtre, 9 AVANT « Adjusting chargers » (dispatch de l'arbitre), 0
après (proxy). 6 cycles sur 6 portent « is commanded by the ETM waterfall
this cycle. Not re-deciding here. » Le doublet du relevé 3g-1 §3.3 a
disparu, et quelqu'un commande — l'arbitre a bien allumé la borne à 10 A
sur sa propre décision.
- Le plancher publié est l'exigence réelle : 2 070 W sous +etm22, 4 140 W
sous +etm23, MÊME borne, MÊME journée. Et il suit le nombre de phases visé
d'un cycle à l'autre (4 140 en triphasé, 1 380 en monophasé).
- L'entrée evcharger est créée d'office, journalisée une fois par borne,
sans charge utile ; l'aller-retour GetLoadConfig → SetLoadConfig verbatim
est neutre et ne reconstruit aucun adaptateur ; loads[] ⊆ GetLoadConfig
est rétabli.
TROUVÉ, et c'est le point du passage machine :
- GRAVE — une borne SANS VÉHICULE BRANCHÉ absorbe le budget de la cascade.
4 853 W alloués à une wallbox `pluggedIn: false` qui tire 0 W, la PAC
derrière ne survivant que sur son propre recrédit. Trois cycles
consécutifs, 2 350 / 4 853 / 5 464 W donnés à personne. C'est le défaut du
relevé 3g-1 §3.4 par une autre porte : là le plancher mentait, ici c'est le
filtre d'entrée. evParticipatesInWaterfall() reprend le filtre du proxy —
voiture assignée, mode non manuel — mais pas pluggedIn. Le proxy n'en avait
pas besoin : son allocation n'était pas une cascade. Défaut de 3g-1,
correction PROPOSÉE ET NON APPLIQUÉE, à traiter dans son propre commit.
- Mineur — le rang par défaut des bornes auto-provisionnées retombe à
ÉGALITÉ avec le rang 1 déjà occupé, au lieu de « en tête ». Le tri reste
total et reproductible, mais ce n'est pas ce qui était demandé.
NON OBSERVÉ, et pas fabriqué : EV_GRID_START (le budget est passé de 385 W à
2 350 W sans s'arrêter dans la fenêtre de tolérance), BATTERY_RESERVE avec un
surplus réel, LM-1204-b matériel (la Terra AC ne répond pas à l'ARP depuis le
banc lui-même), et le cas de transition — aucune borne n'était sous commande au
moment de la bascule.
Banc rendu à son état d'origine : ChargingInfos identiques champ par champ,
voiture de test retirée, wallbox éteinte. Les entrées evcharger restent — elles
seraient recréées au prochain démarrage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Le relevé machine du 2026-08-26 l'énonçait sans détour : « tant que ceci n'est
pas traité, la transplantation ne commande rien ». L'arbitre décidait,
publiait un motif juste, et adjustEvChargers() commandait autre chose une
milliseconde plus tard. Ce lot ferme les trois points que le banc a ouverts.
1. UNE CHARGE, UN COMMANDEUR
Les actions EV du waterfall sont enfin DISPATCHÉES — elles ne l'étaient pas du
tout : m_loadAdapters ne contient pas les bornes, et applyActionsToAdapters()
s'arrêtait là. Et les bornes ainsi commandées sortent du chemin proxy par deux
crochets virtuels posés dans l'amont, interrogés à deux moments du cycle où la
réponse n'est pas la même : evSurplusPlannedByWaterfall() avant le plan (à qui
la borne appartient-elle), evCommandedByWaterfall() après (qu'a-t-il commandé).
Sans cette distinction, une borne servie par l'échéance ne serait plus
commandée du tout.
Le jeu des bornes commandées est NOMMÉ par le scheduler (Plan::waterfallEvIds).
Le redéduire ailleurs — « motif différent d'EV_DEADLINE », « financement au
surplus » — dériverait du vrai au premier motif ajouté, et EV_GRID_START porte
justement funding=Grid tout en venant du waterfall. Il est vidé à l'entrée du
mode dégradé : le waterfall ne commandant plus, il ne peut plus revendiquer.
chargingState, que posait adjustEvChargers(), est repris par l'arbitre pour
les bornes qui n'y passent plus — sinon l'app afficherait « en charge » une
borne que l'arbitre vient d'éteindre.
2. evFloorW() REND L'EXIGENCE RÉELLE, ET LA TOLÉRANCE DEVIENT VISIBLE
Le plancher valait `min × acquisitionTolerance × 230 × phases` : 2 070 W
annoncés pour une borne qui en exige 4 140. Le waterfall allouait 2 828 W,
l'adaptateur refusait d'enclencher sous 6 A, et 2 828 W étaient retirés du
budget pour personne. Ce sont deux grandeurs différentes : le plancher est ce
que la borne EXIGE, la tolérance est la part qu'on accepte de ne pas couvrir.
La tolérance n'est pas supprimée pour autant — un réglage documenté (défaut
0,5) qui devient inerte sans le dire est un silence ambigu, et tout le parc
verrait son VE démarrer deux fois plus tard sans explication. Elle devient un
seuil de démarrage EXPLICITE : motif EV_GRID_START {budgetW, floorW, gridW},
funding=Grid, et une comptabilité qui se partage — budgetW dans
budget.allocatedW, gridW dans budget.evReservedW, budgetW + gridW ==
allocatedW.
C'est une brèche assumée dans le financement réseau que DESIGN_3g §3.1 déférait
à 3f. Elle est bornée par construction, pas par discipline : surplus réel exigé
(budgetW > 0), tout le budget restant consommé donc AU PLUS UNE borne par
cycle, et le plafond de la protection de surcharge s'applique avant. Écrite en
toutes lettres dans DESIGN_3g §3.1-bis.
3. LE RANG D'UNE BORNE EST CONFIGURABLE
Il vit là où vivent tous les rangs : LoadConfig.priority (LM-1206/LM-1207). Pas
de second système de priorité — un classement propre aux bornes ne saurait pas
exprimer « VE1 > ECS > VE2 », et deux classements qui ne peuvent pas
s'interclasser sont un défaut, pas deux fonctionnalités. ChargingInfo ne porte
donc aucun champ de rang.
Toute borne détectée reçoit d'office une entrée « evcharger », portant le rang
et RIEN d'autre : ses limites viennent du Thing et changent avec la voiture
branchée. Création journalisée, une ligne par borne, à la création seulement —
une écriture que personne n'a demandée doit laisser une trace. Le tri devient
TOTAL : à rang égal, l'identifiant départage, donc laquelle démarre est
reproductible même avant tout réglage.
L'entrée SURVIT à la disparition de son Thing : borne remplacée, ThingId
changé, appareil déposé. Elle reste publiée available: false / faultCode:
THING_MISSING / motif LOAD_UNAVAILABLE, et ne retient aucun budget. La
supprimer d'office rendrait « borne remplacée » indistinguable de « borne
jamais configurée ».
CORRIGE UNE RUPTURE D'INVARIANT DE 3g-1, non signalée à l'époque : en faisant
entrer les bornes dans loads[], 3g-1 publiait des charges qui ne figuraient
dans AUCUNE GetLoadConfig — la charge fantôme que le lot B-bis avait
supprimée, réapparue par l'autre bout. L'app s'appuie sur cet invariant pour
résoudre libellé, domaine et rang. L'auto-provisionnement le rétablit par
construction.
ÉPROUVÉ
- testEvChargerRankDecidesWhoCharges : deux bornes, un budget qui ne paie
qu'un plancher, la seconde refusée avec BELOW_MIN_POWER {budgetW: 620,
minPowerW: 1380}, et l'inversion du rang change la borne servie. C'est la
moitié ARBITRAGE de LM-1204-b. Couvre aussi l'aller-retour neutre par la
frontière RPC et l'inclusion loads[] ⊆ GetLoadConfig.
- testEvGridStartIsAnnouncedAndBounded : le motif chiffre le soutirage, la
comptabilité se réconcilie, une seule borne soutire par cycle.
- testEvChargerConfigOutlivesItsThing : l'entrée survit, publiée en défaut.
- testEvChargerConfigIsRankOnly / RefusesPayloads / ClaimsItsThing.
Deux points de contrôle de `run` bougent, dans un seul scénario et par une
cause unique : la conversion watts→ampères se fait désormais après
l'allocation, et l'arrondi tombe parfois de l'autre côté. 14 des 16 scénarios
sont identiques au bit près, et ce qui compte est inchangé dans les deux —
la voiture est à 100 % à l'échéance.
Suite complète : simulation 27/27, charging 16/16, loadmodel 16/16,
spotmarket 7/7. Build amd64 0/0.
RIEN N'A TOURNÉ SUR MACHINE. docs/RECAP_3g2.md dit ce qui reste à éprouver au
banc — dont LM-1204-b avec deux bornes réelles, la Terra AC étant vérifiée
absente du réseau (ARP INCOMPLETE depuis le banc lui-même).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
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
Constat d'origine, rejoué et daté au banc : 90 s d'écoute, arbitre actif, ~2 kW exportés
→ ZÉRO notification NymeaEnergy.*. L'état runtime de l'arbitre (allocation, budget, verrous,
paliers, disponibilité, motifs) ne franchissait aucune frontière RPC : il vivait exclusivement
dans qCInfo(dcNymeaEnergy). Expliquer les décisions au client n'avait donc aucun transport.
Après déploiement, même geste : 13 notifications en 480 s.
Aucune phrase composée par la box. La locale nymea est PAR CONNEXION (JSONRPC.Hello o:locale),
et NymeaEnergyJsonHandler est un JsonHandler sans pluginId — donc hors du catalogue de
traduction : rien ne rattraperait une phrase française émise là. La box transporte un CODE et
des PARAMÈTRES ; la phrase est fabriquée par le client.
LoadAction::reason devient DecisionReason{code, params}. La ligne française du journal est
RENDUE depuis ce couple par renderFr(), source unique — jamais maintenue en parallèle. Les 19
codes sont EXTRAITS des sites existants, pas inventés ; les motifs d'ECS-309 et ECS-309-b sont
repris mot pour mot. Prouvé par comparaison de traces (1726 lignes de journal identiques en
simulation), par une table d'or qui échoue au premier code sans phrase attendue, et au banc sur
7 motifs distincts. Le libellé de la charge n'entre pas dans la charge utile : il est substitué
au RENDU, par l'appelant qui le connaît.
Les verrous sortent en SECONDES RESTANTES, pas en watts. Ceci n'enfreint pas ECS-306 : cette
exigence interdit qu'un index de PALIER franchisse la frontière d'ALLOCATION ; une valeur
d'AFFICHAGE sortante est un autre chemin. Le mécanisme sort en union discriminée
(relay|variable|sgReady), toutes clés internes optionnelles au schéma (LM-302).
ILoadAdapter::runtimeView() est PURE VIRTUELLE à dessein — mais la pure virtuelle force à
RÉPONDRE, pas à répondre quelque chose. testRuntimeViewIsSubstantial() est le second verrou :
les quatre adaptateurs, amenés dans un état non trivial, doivent rendre les valeurs runtime
réelles, les verrous en secondes ET leur expiration, et les codes de défaut. Deux mutants
« vue vide » introduits puis retirés : le test échoue sur chacun.
budget publié : surplusW, evReservedW, recreditedW, allocatedW, remainingW, avec l'identité
surplus + recrédit − alloué == restant. recreditedW n'est pas décoratif : sans lui l'identité
serait fausse DÈS QU'UNE CHARGE CONSOMME, et le client chercherait un écart inexplicable. Le
banc l'a montré aussitôt — surplusW −250 W avec recreditedW 3000 W — rendant lisible l'artefact
de banc documenté dans AGENTS.md (relais sans mesure + rootmeter aveugle).
Champs omis, jamais nuls : timestamp absent = aucun cycle depuis le démarrage ; budget absent =
mode dégradé, planification suspendue. Publier un zéro se lirait « exécuté, rien à allouer ».
Cadence : changement qualitatif ou variation d'allocation > 50 W, plus un battement de 60 s.
C'est le battement qui rend un arbitre FIGÉ détectable — les notifications continuent tandis que
le timestamp cesse d'avancer. Observé 3 fois au banc. Volume mesuré : ≈ 1,7 Mo/jour, un client.
GetChargingSchedules rend enfin degradedMode : il était POUSSÉ en notification seule, donc
illisible par un client connecté après la bascule (famille ECS-411). Règles absolues 11 et 12
ajoutées à AGENTS.md.
LoadConfig : champ optionnel domain (ecs·ev·heating·hvac·battery), PURE métadonnée d'intention
que l'arbitre ne lit pas. Absente ou vide = non classée. Forme exacte découverte au banc et
corrigée dans la doc ET le test : GetLoadConfig sérialise via le méta-objet, donc la propriété
sort TOUJOURS, à "" — comme sgReady:{} ou powerLevels:[]. C'est le corollaire LM-302.
Règle 7-c, deux silences fermés : les retours idempotents du RelayRouter et du SgReadyAdapter
étaient MUETS — « consigne reçue, palier déjà bon » ne se distinguait pas de « l'action n'est
jamais parvenue à l'adaptateur », et c'est le cas ordinaire qu'on vient vérifier.
⚠️ Porté au contrat : loads[] n'est PAS en correspondance 1:1 avec GetLoadConfig. « pac-terrain »
est la PAC SG-Ready CODÉE EN DUR d'energypluginnymea.cpp (K1/K2 = Relay4/Relay5, {3:1500,
4:3000}) — arbitrée et publiée, mais inconfigurable : ni rang, ni domain, ni enabled:false.
INTERFACE.md porte la règle de détection pour l'app. Son sort reste À TRANCHER (RELEVE_LOTB §8-bis b).
Déploiement : la recette de TEST_TERRAIN.md §b portait un glob « dpkg -i /tmp/…_*_arm64.deb »,
de la même famille que celui qui a rétrogradé .75 de +etm12 à +etm9. Remplacée par
etm-powersync-deploy/deploy.sh (archivage avant dépôt, installation par nom, refus de
rétrogradation, contrôle de version après coup).
Vérifié sur .75 (1.15.2+etm13 puis +etm14) : critères 1, 2, 4, 5, 6, 7, 8 du brief. Critère 3
(épisode dégradé) vérifié en simulation seulement — le déclencher demande de faire taire un
simulateur SunSpec partagé, hors périmètre. Relevé complet, critère par critère, avec ce qui n'a
PAS été vérifié sur la machine et pourquoi : docs/RELEVE_LOTB.md.
Suites : simulation 38/38, charging 46/46, spotmarket 32/32, y compris en build DEBUG où les
Q_ASSERT_X du validateur JSON-RPC sont actifs (le schéma déclaré correspond donc à la charge
utile émise, retours ET notifications). Cross-arm64 : 0 avertissement. Doxygen : 10, inchangé.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MfvSskkYrrFriYcj4683D
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>
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>
Première génération réelle avec Graphviz (doxygen 1.9.8, la version de référence) : 14
avertissements, dont 8 introduits par moi cette session — \return et \param manquants sur
la charge utile sg-ready, claimedThingIds(), validateSet() et claimedRelays(). Corrigés.
Les 6 restants sont exactement relayrouter.h et energyarbitrator.h, les deux fichiers
différés jusqu'après l'étape 3 puisqu'ils vont être restructurés. Le résidu est donc
entièrement connu et daté.
CORRECTION — la note que j'avais écrite en tête du Doxyfile était fausse. Elle annonçait
que l'arête « EnergyArbitrator : public SmartChargingManager » n'apparaîtrait pas. Elle
apparaît : doxygen rend une classe de base non résolue en boîte simple. Ce qui manque est
derrière — la boîte est un cul-de-sac, sans membres, sans ancêtres, non cliquable (aucun
href dans le SVG). La conclusion ne change pas, ne pas élargir INPUT ; la description, si.
101 graphes SVG produits, 3,2 Mo de HTML, 1,8 Mo de XML.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
HAVE_DOT = YES, mais CALL_GRAPH et CALLER_GRAPH restent à NO, et pas pour une question
de coût : le moteur est signal-driven. powerBalanceChanged → verifyOverloadProtection()
est une connexion Qt, invisible à l'analyse statique ; un graphe d'appel montrerait
update() comme un point d'entrée orphelin et raterait le mécanisme principal de la
couche L4. Un schéma faux vaut moins que pas de schéma.
Ce que dot apporte réellement : héritage, collaboration, inclusions. SVG, lisible à toute
échelle et diffable, avec DOT_GRAPH_MAX_NODES = 60 pour éviter les pavés illisibles.
tools/gen-doc.sh : une seule commande, le Doxyfile épinglé. C'est la leçon du piège des
générateurs — 163 avertissements sans sortie contre 113 avec HTML+XML sur le même arbre.
Un chiffre de référence ne vaut que rattaché à une configuration exacte, et une commande
unique empêche qu'on régénère « à sa façon ». Le script signale aussi une version de
doxygen différente de la référence 1.9.8.
Avertissement ajouté en tête du Doxyfile, à côté de celui sur les générateurs : le graphe
d'héritage sera tronqué. EnergyArbitrator : public SmartChargingManager, mais l'amont est
hors INPUT. C'est voulu — élargir INPUT ferait exploser le compte d'avertissements. Sans
cette note, quelqu'un le « réparera ».
graphviz est documenté comme dépendance de la cible doc, pas du build : build-cross-arm64
compile, il ne dessine pas.
Hors de ce lot, comme convenu : \defgroup, carte \dot cliquable, WARN_AS_ERROR = YES,
hébergement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Constaté en construisant 1.15.2+etm7 depuis un git archive : qmake fige la liste des
.qm avant que lrelease ne les produise, et dh_install échoue en missing files. Le clone
canonique masque le défaut, ses .qm survivant d'un build antérieur.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Correction de mon explication du volet 2. Le chauffe-eau n'était pas à 3500 W par
reliquat mais par EMBALLEMENT, et le mécanisme change la conclusion.
Deux conditions se composent sur .75 : les Things sont des relais GPIO, dont la
ThingClass n'expose pas currentPower — le recrédit anti-clignotement crédite donc
le nominal commandé ; et le rootmeter est la vue SunSpec du simulateur, aveugle
aux résistances câblées sur ces broches — la conso ECS ne fait jamais baisser
l'export mesuré. budget_charge = surplus + palier double-compte, le palier monte
d'un cran par cycle tant que le surplus dépasse 500 W, jusqu'au plafond. C'est ce
qui explique les quatorze plateaux sans commutation, pas un point de départ
malheureux.
GARDE-FOU ajouté sous « Correction B » dans AGENTS.md et dans le relevé : c'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, et
le recrédit compense exactement ce qu'elle vient de retirer. Le supprimer casserait
l'anti-oscillation qu'il protège (ECS-404) sans rien régler. Sans cette note,
quelqu'un « corrigerait » la correction B sur la foi de cette mesure.
ECS-303 n'est donc PAS affaibli : ma remarque valait pour ce banc, où aucune montée
pas à pas n'est possible. Avec de la physique réelle, la rampe matinale traverse la
bande 1500-2000 W lentement et la frontière est franchie normalement. Le volet 2
n'est pas à refaire depuis le palier 0 sur le simulateur — il est à faire après le
câblage, sur la vraie installation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Documente ce qui a effectivement tourné, pas ce qui était supposé. Build
1.15.2+etm3 du 2026-08-08, cross arm64, réussi.
- L'exclusion DH_OPTIONS=-N nymea-energy-tests était citée pour debian-qt5/rules
alors que c'est qt6 qui construit (debian -> debian-qt6, dh --buildsystem=qmake6).
Vérifié avant le build : l'exclusion EST présente dans debian-qt6/rules:6, donc
seule la citation était périmée. Aucun paquet nymea-energy-tests produit.
- Les dépendances étaient annoncées « toutes en 1.15.0 » avec Qt5
(libqt5websockets5-dev, qtbase5-dev, qttools5-dev-tools). Le build réel utilise
nymea 1.15.2+202606191336~trixie1 et Qt 6.8.2 — chaîne Qt6 de bout en bout,
cohérente avec la box (aucun libqt5 installé sur .75, ldd du .so ne renvoie que
du Qt6).
- Ajouté : debian-qt6/changelog est un lien symbolique VERSIONNÉ vers
../debian-qt5/changelog, comme copyright et nymea-energy-tests.install.in. Il
n'y a qu'un changelog réel. Sans cette note, lire les deux fichiers donne
l'illusion d'une divergence — c'est l'erreur que j'ai commise et rapportée.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Couverture des fichiers non visés par la restructuration à venir : l'extraction
du noyau de calcul (étape 2) et celle de m_relayMapping (étape 3) ne les
touchent pas. relayrouter.h et energyarbitrator.h sont volontairement DIFFÉRÉS —
documenter ce qui va changer produirait du bruit d'historique.
Mesure sur la config du Doxyfile : 113 → 31 avertissements.
loadconfig.h 41→0 · surpluscontext.h 30→0 · sgreadyadapter.h 4→1 (le \return
restant relève du lot suivant) · etmvariableloadadapter.h 2→0 · evadapter.h 1→0
Ce sont des contrats, pas des étiquettes : enabled dit qu'une charge déclarée
mais exclue n'est JAMAIS pilotée ; priority qu'il s'agit d'un rang ascendant et
non d'un poids ; timestamp qu'il est la source unique du temps, dont dérivent
verrous et fenêtres ; setPowerLevels qu'il trie et déduplique ; fromMap qu'il
retourne une config NON validée ; internalRootMeter() qu'il peut être nul.
Ajouts « // [ETM] » hors etm/ (smartchargingmanager.h) : ils échappent au
périmètre du Doxyfile, la frontière étant un répertoire. Un inventaire explicite
est posé au marqueur [ETM] BEGIN, distinguant les trois cas — degradedMode() et
les trois accesseurs internal* sont des ajouts ETM et sont documentés ; les huit
changements de visibilité seule gardent la documentation de l'amont. La
définition de fait d'AGENTS.md renvoie à cet inventaire et l'étend explicitement
aux ajouts hors etm/.
Build amd64 0 erreur. Simulation : 7/7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Purge des affirmations devenues fausses depuis la rév. 3. Chaque correction est
vérifiée contre le code ; rien n'est repris du texte existant par défaut.
Zones : ÉTAT (tableau + Détail 3c) · « ce que le moteur sait faire » · DÉFÉRÉ
(2 entrées) · PLAN 3C · ARCHITECTURE CIBLE · WORKFLOW phase 3 · 3b révisé ·
Verrous minOn/minOff (2 endroits) · MODÈLE DE SÉCURITÉ (L2) · ROADMAP ·
RÉFÉRENCES.
Principales remises au fait :
- EcsRelayAdapter et le kind Stage, supprimés en 5100674, disparaissent du
schéma « non négociable » au profit de RelayRouter + EtmVariableLoadAdapter ;
SocketScheduler et BatteryAdapter, qui n'existent pas, sont marqués [3d]/[3f].
- EvAdapter::applyAction() n'est pas « inactif » : il est implémenté
(evadapter.cpp:62-94) mais jamais appelé (energyarbitrator.cpp:288).
3g est un travail de câblage, pas d'écriture.
- minStage/maxStage ont été retirés du contexte ; minState/maxState et
minStateHold subsistent et sont conservés tels quels.
- La configuration des charges est persistée et rechargée à chaud depuis
7184fe4 — le point (b) de la ROADMAP est fait côté moteur, seule l'UI
Flutter reste ouverte.
- Le watchdog L2 est en place, plus « à coder en 3c ».
- PLAN 3C n'est plus reproduit (pseudocode nommant une classe supprimée et un
identifiant « relay-stages » jamais livré) ; les corrections A et B, toujours
en vigueur, sont conservées avec leurs références actuelles.
- PRÉCONDITION ajoutée au basculement de SgReadyAdapter sur la config :
retirer d'abord Q_ASSERT(m_stateRelays.contains(2)) au profit d'un refus
explicite — l'assertion garde l'état 2, repli sûr du mode dégradé L2, et
disparaît en release.
Aucune règle absolue 1 à 10 modifiée (vérifié identique à HEAD).
INVARIANTS BUILD / PACKAGING intact. Aucun code touché.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Décision Patrick : délestage strict au budget, MAIS minOn/minOff (protection
compresseur, anti court-cycling) bornent le palier via l'adaptateur, pas le budget.
Paramètres par charge (config installateur, jamais hardcodé) + défauts indicatifs
par type (résistif / thermodynamique-PAC / SG-Ready). Note seam de temps unifié.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
QTimer 30s indépendant des signaux ; m_lastMeterUpdate picoté sur powerBalanceChanged.
Silence >90s → mode dégradé (appliqué à la TRANSITION uniquement) :
- ECS palier 0 force=true ;
- EV : clamp courant minimum SEULEMENT si déjà en charge (pas d'activation forcée ;
"jamais 0 A si branché" relève du failsafe L1, pas du repli logiciel).
update() suspend la planification + le dispatch tant que m_degradedMode (sécurité L4
en position 3 reste active) → pas de rallumage sur le cache d'un compteur mort, pas
d'oscillation. Reprise au retour du compteur.
SAFETY.md §L2 : nuance maintenu/démarré + suspension planification. AGENTS.md morceau 7 :
exiger ECS reste à 0 sur plusieurs cycles. SG-Ready/Batterie déférés 3e/3f ;
flag degradedMode exposé en 3c-6. Build 0 erreur / 0 warning.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Corrections A (déduction EV unique) et B (anti-clignotement) intégrées.
Tri priorité ascendant (rang 1 = premier servi, OPTIMIZER_PROTOCOL §5/annexe C) —
corrige l'inversion du PLAN 3C et 3 doc-comments (plan.h, loaddescriptor.h,
ecsrelayadapter.h). Build 0 erreur / 0 warning.
telemetry() ECS : currentPowerW MESURÉE si au moins un relais expose "currentPower"
(thermostat coupé → 0, pas de fantôme), DÉCLARÉE en repli seulement sans comptage.
Dette evadapter.cpp priority=100 (ancienne convention) inscrite en 3g.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>