Ma rédaction se rassurait à tort : « +etm12 n'est pas perdu, .75 était réparable depuis le dépôt
APT à tout moment ». C'est vrai de cette version-là, et faux comme raisonnement.
.75 est le banc de BÊTA-TEST : on y pose un paquet AVANT de le publier. Sur le banc, un paquet
pas encore publié n'existe qu'à deux endroits — le répertoire de dépôt de la box et le pool
local de build. Ni au dépôt APT, ni chez un client, ni dans aucun git (un .deb ne se versionne
pas). L'écraser est IRRÉVERSIBLE, et c'est cela, plus que le ramassage multiple, que le verrou
d'archivage protège.
Le journal dpkg de la box, désormais cité, donne la chronologie qui le montre : +etm12 posé sur
le banc le 13 août à 19:48, publié au pool à 20:48 — une heure où il n'existait QUE sur le banc.
La plupart des paquets de banc ne quittent jamais cette fenêtre, puisqu'ils sont écartés : c'est
leur raison d'être. Le risque ne dépend pas de la chance qu'a eue une version particulière.
Le même journal donne la cascade du 25 août : quatre « upgrade » en onze secondes,
etm12→etm10→etm4→etm8→etm9. L'état final est celui du dernier configuré, pas du plus récent.
Renvoi ajouté vers les deux chemins de déploiement désormais écrits (BUILD-WORKFLOW.md §8,
DEPLOY.md §8) — le premier ne décrivait que le parc, laissant le chemin du banc s'improviser.
Documentation seule : docs/ n'est pas embarqué dans le .deb (vérifié : le paquet ne contient que
le .so, le changelog, le copyright et les .qm). 1.15.2+etm14 reste donc valide et fidèle à sa
source ; pas de nouvelle version.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MfvSskkYrrFriYcj4683D
L'agent app a besoin de savoir OÙ prendre le paquet : canal powersync-nightly, et non stable ni
testing, qui ne contiennent aucun powersync-energy-plugin-nymea.
Consigne au passage : reprepro a été invoqué avec --keepunreferencedfiles. Sans cette option il
aurait supprimé du pool le binaire d'+etm12, qui venait de perdre sa dernière référence — donc
il aurait CAUSÉ la perte que l'incident de rétrogradation décrivait à tort comme déjà survenue.
C'est la règle « une publication AJOUTE, elle n'enlève JAMAIS » de publish-to-repo.sh, appliquée
un cran plus haut, à l'inclusion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MfvSskkYrrFriYcj4683D
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
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>
writeRelay() jetait le ThingActionInfo* : aucun acquittement, aucun retour
arrière, available codé en dur à true, et m_currentStage mis à jour comme si tout
avait réussi — on annonçait une puissance non appliquée.
MODÈLE ASYNCHRONE. executeAction est asynchrone et update() ne doit jamais
attendre (AGENTS règle 5). « Attendre le résultat » ne veut donc pas dire bloquer
le cycle : l'écriture est émise, l'adaptateur retient combien d'acquittements il
attend (m_pending), le verdict tombe quand le compteur retombe à zéro, et la
conséquence est traitée au cycle suivant. Motif repris tel quel d'EvCharger
(evcharger.cpp:293-299), y compris `this` en contexte de connexion — si
l'adaptateur meurt, les callbacks sont coupés proprement.
INDÉTERMINATION. Pendant une transition, telemetry() annonce
max(m_stagePrev, m_stageTarget) : on ne sait pas ce qui est fermé, on annonce donc
la plus haute des deux puissances possibles. Même direction qu'ECS-411 (relais
injoignable supposé fermé) — ne jamais annoncer moins que ce qui peut être
appliqué. Sous-estimer fait sur-allouer les charges suivantes ; surestimer ne fait
que retarder une montée.
ÉCHELLE BORNÉE à trois barreaux, une tentative chacun, aucune boucle : cible →
retour arrière → arrêt total → défaut. Le retour arrière est asynchrone au même
titre et passe par le même compteur.
DÉFAUT COLLANT, pas clignotant. m_faulted est un verrou posé une seule fois, sans
délai ni expiration : available ne peut pas osciller d'un cycle à l'autre. Seul
NymeaEnergy.ClearLoadFault le lève — acte délibéré et journalisé de l'opérateur.
La reconstruction le lève aussi, mais par construction : un adaptateur neuf n'a
pas d'historique. À la levée, l'état matériel est RELU (ECS-411), pas supposé.
CANAL OUVERT. LoadContext n'avait AUCUN champ available : le publier aurait été
décoratif. Ajouté à LoadContextTelemetry, avec sa sémantique écrite noir sur
blanc — il gouverne l'allocation, PAS la comptabilité. Une charge en défaut ne
reçoit rien mais reste comptée : une puissance qu'on ne sait plus couper est de la
conso fixe, au même titre que la base de la maison. Le figeage est porté par
lockMin == lockMax == puissance crue engagée, jamais un plafond nul sous un
plancher non nul. Le même canal servira ECS-601.
OPTIMIZER_PROTOCOL.md mis à jour dans le MÊME lot, comme l'exige le §11 de la
spec : available, lockMinPowerW et lockMaxPowerW documentés avec leur sémantique.
clearFault() est PURE VIRTUELLE sur ILoadAdapter : les trois autres adaptateurs la
déclarent sans effet plutôt que d'hériter d'un défaut vide. Leur généralisation est
portée par ECS-414.
Test testEcsPartialFailure : cas nominal sans défaut, puis échelle complète via un
relais introuvable — défaut atteint, relais valide bien ramené à l'ouverture par la
tentative d'arrêt total, available faux, plancher == plafond == puissance comptée,
plus aucune commande une heure plus tard, puis levée délibérée.
Build amd64 0 erreur. Simulation : 16/16.
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>
Résultat brut : R500=3, R1000=1, R2000=1, total 5 commutations. Prédit 7/3/1,
total 11. Aucune transition à trois relais observée ; la frontière 1500→2000 n'a
jamais été franchie.
Le cliquet est confirmé, et plus fort que prédit : QUATORZE plateaux consécutifs
sans une seule commutation, de 0 à 3600 W puis retour à 500 W. Le palier n'a bougé
qu'en surplus négatif. La prédiction était trop prudente — ce n'est pas seulement
la descente en surplus positif qui ne fait rien redescendre, c'est toute la plage
positive.
La falsification vient de MON profil, pas du moteur. Le chauffe-eau était déjà à
3500 W au départ (reliquat du volet 1) : la montée n'avait rien à monter, et seule
la descente a compté — en sautant des paliers, puisque chaque plateau fait chuter
le budget de 500 à 1000 W d'un coup. Le comptage 7/3/1 supposait un parcours pas à
pas des 8 paliers ; il faut partir du palier 0.
Conséquence signalée sans être tranchée : la transition 1500→2000 n'est atteignable
qu'EN MONTÉE, et le cliquet interdit toute montée pas à pas dès que la charge est
déjà servie. Cette frontière pourrait donc être bien moins fréquente que la spec ne
le suppose, ce qui affaiblirait la justification d'ECS-303. À établir par un
balayage partant du palier 0.
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>
Résultats ajoutés SOUS la prédiction, sans y toucher.
4.0 — La première tentative a révélé un blocage circulaire dans ECS-412, pas dans
le banc. Point de méthode consigné : la prédiction annonçait « sonde éteinte sous
+etm3 » et elle l'était, mais pour la mauvaise raison. Sans test de falsification
cherchant POURQUOI elle se vérifiait, ce relevé aurait coché la prédiction et
conclu à tort. Une prédiction juste peut être confirmée par un mécanisme faux.
4.1 — Volet 1 mesuré sous +etm4. Le contraste attendu entre les deux versions a
été observé au sein d'un même binaire, sur deux cycles consécutifs au même
budget (~3200 W) : verrou actif → chauffe-eau maintenu à 3500 W, résidu −198 W,
sonde éteinte ; verrou expiré → palier 3000 W, résidu +201 W, sonde allumée. Le
cycle à verrou expiré reproduit ce que ferait +etm2, le clamp d'ECS-306 n'ayant
alors rien à contraindre.
Arithmétique vérifiée à 2 W près du prédit, recrédit de la sonde compris. Les
deux prédictions tiennent, y compris la secondaire : la PAC de rang 2 n'a pas
changé d'état — sans la charge sonde, le relevé aurait conclu « aucun effet
observable ».
Reste à mesurer : +etm2 réellement installé, et le volet 2 (commutations).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Horodatée 2026-08-09, commitée avant tout profil lancé. Une reconstruction
analytique publiée après la mesure ne vaut rien ; publiée avant, elle est
falsifiable et le commit en fait foi.
Prédiction principale, binaire : sur un point à budget_charge = 3200 W (surplus
−300 W, palier verrouillé à 3500), +etm2 retient le palier 3000 et annonce un
résidu de +200 W — la charge sonde de rang 3 s'allume ; +etm3 force 3500 par
lockMinPowerW et annonce −300 W — elle reste éteinte. Si la sonde s'allume sous
+etm3, le raisonnement est faux.
Prédiction secondaire, tout aussi engageante : la PAC de rang 2 ne changera PAS
d'état entre les deux versions (200 W comme −300 W sont sous P3 = 1500 W). Sans
la charge sonde, ce relevé aurait conclu « aucun effet observable » — à tort.
Raison sous-jacente établie avant mesure : les Things sont des relais GPIO, leur
ThingClass n'expose pas currentPower, donc RelayRouter::telemetry() retombe sur
le nominal commandé et currentPowerW == palier commandé en permanence. La
divergence entre les deux versions ne peut donc apparaître qu'en import net.
Volet 2 : comptage attendu R500=7, R1000=3, R2000=1 sur un aller ; la transition
1500→2000 bascule les trois relais à elle seule (3 des 11). Piège prédit — le
cliquet : sans plateaux à surplus NÉGATIF, le palier ne redescend jamais et le
relevé serait vide.
Protocole et ordre d'exécution inclus, dont la vérification obligatoire que +etm2
relit bien la configuration écrite par +etm3 avant de lancer le profil.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Expose autoconsommation/autonomie comme source canonique de l'energymanager,
pour remplacer le seam interim app-side (EnergyRatiosInterim.compute).
Voie (c) : méthode + notification sur le handler NymeaEnergy de NOTRE plugin
(déjà forké/buildé), calculées depuis EnergyManager::totalX() — interface
abstraite stable. PAS de champs sur Energy.PowerBalance → aucun fork du coeur
nymea (nymea-experience-plugin-energy) à rebaser perpétuellement.
- EnergyRatiosCalculator (GPL pur mesure) : Δ de cumuls, baseline minuit local,
reseed (1er appel / nouveau jour / non-monotone Δ<0), den<=0 -> n/a, clamp
[0,100]. Porté 1:1 du seam interim app.
- NymeaEnergy.GetEnergyRatios -> {o:selfConsumptionRate, o:autonomyRate} Double,
champ OMIS si n/a (jamais null).
- NymeaEnergy.EnergyRatiosChanged : meme forme, branchee sur powerBalanceChanged(),
emise uniquement quand une valeur (ou sa disponibilite) change.
- testEnergyRatiosAlignment : vecteurs joues 1:1 contre l'interim (seed, normal,
clamp-bas, den<=0->n/a, non-monotone, nouveau jour local).
- docs/INTERFACE_energyratios.md : contrat pour l'agent app (RPC a consommer).
Build prod 0/0. Suite simulation 25/0 (testLoadConfigRpc traverse le handler
modifie -> pas de regression).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vérification nymea : un integration-plugin ne peut pas piloter les things d'un autre
plugin (thingManager privé ; seuls cœur/règles/scripts/experience-plugins le font).
Donc la combinatoire watts→relais ne peut vivre que côté experience-plugin.
rév. 3 acte le déplacement de frontière :
- Optimiseur watt-pur (ne connaît jamais un relais) | ROUTEUR (watts→relais,
experience-plugin, a le ThingManager) | relais = things "power" nus.
- ECS multipalier : LoadConfig devient une LISTE de relais power (relays[] +
minOnS/minOffS) ; powerLevels DÉRIVÉS des combinaisons (source = les relais).
- Cas continu (EV/triac, EtmVariableLoadAdapter) inchangé.
- §10 IMPACT APP explicite : l'écran « Configurer » passe à une liste de relais +
picker findConfiguredThings("power") (UI-only) — l'agent app doit lire rév. 3
avant de toucher l'UI.
À déposer À L'IDENTIQUE dans etm-powersync-app (miroir manuel, hors de ce repo).
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>