33 Commits

Author SHA1 Message Date
Patrick Schurig
c2f08cb804 fix(3g-1): une borne sans véhicule branché est hors arbitrage
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
2026-08-27 12:52:21 +02:00
Patrick Schurig
93b257172c docs(relevé): 3g-2 sur machine — le doublet a disparu, et le banc a trouvé un trou
`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
2026-08-27 12:44:19 +02:00
Patrick Schurig
baf267a963 feat(3g-2): une charge, un commandeur
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
2026-08-27 10:43:25 +02:00
Patrick Schurig
0765672e62 feat(config): lot B-bis — la PAC entre en configuration — 1.15.2+etm15
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
2026-08-25 18:35:04 +02:00
Patrick Schurig
74465375c4 feat(telemetry): lot B — frontière de télémétrie d'arbitrage + o:domain — 1.15.2+etm14
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
2026-08-25 13:18:10 +02:00
Patrick Schurig
cd1a0c83a5 fix: ECS-410-b — l'issue de ClearLoadFault, et l'asymétrie refus/succès (règles 7-c, 7-d)
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>
2026-08-13 20:27:53 +02:00
Patrick Schurig
6acaee38f9 fix(relayrouter): ECS-411-b — la reprise s'annonce, et annonce ce qu'elle a lu
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>
2026-08-13 20:13:18 +02:00
Patrick Schurig
3c81f76da5 docs(doxygen): 14 → 6 avertissements, et correction de la note sur le graphe
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>
2026-08-11 06:54:56 +02:00
Patrick Schurig
67f7059dcc docs(doxygen): Graphviz activé + cible de génération reproductible
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>
2026-08-10 06:39:15 +02:00
Patrick Schurig
0cbc9480db docs(build): arbre vierge — lrelease avant le premier dpkg-buildpackage
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>
2026-08-09 12:38:30 +02:00
Patrick Schurig
81621b2bcc docs: emballement du banc — artefact, pas défaut ; ECS-303 non affaibli
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>
2026-08-09 09:59:28 +02:00
Patrick Schurig
9e37fdcbbb docs(agents): invariants BUILD — corriger la référence qt5 → qt6, d'après le build réel
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>
2026-08-08 14:57:27 +02:00
Patrick Schurig
e70a60c180 docs(etm): contrats Doxygen — types de config/contexte et adaptateurs stables
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>
2026-08-08 11:47:05 +02:00
Patrick Schurig
f6a4be5560 docs(agents): consolidation des zones périmées (12 zones, aucun code)
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>
2026-08-08 10:51:10 +02:00
Patrick Schurig
65f281cd5a docs: AGENTS.md — acte bascule Voie B (Provides/Conflicts/Replaces) + version générique
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 16:25:28 +02:00
Patrick Schurig
3afef0436d [packaging] renommage powersync-* + version 1.15.0+etm1 + invariants AGENTS
- debian/control : Source+Package → powersync-energy-plugin-nymea ;
  Provides/Conflicts/Replaces: nymea-energy-plugin-nymea (remplace l'amont) ;
  description + homepage ETM.
- debian/changelog : nouvelle entrée tête powersync-energy-plugin-nymea
  (1.15.0+etm1) trixie — plus de 1.13.0/jammy comme version courante.
- debian/nymea-energy-plugin-nymea.install.in → powersync-energy-plugin-nymea.install.in
  (déjà commité via git mv).
- AGENTS.md : section "INVARIANTS BUILD / PACKAGING" (nom powersync-*,
  version +etmN, cross arm64, tests hors deb, vérification post-build).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 14:50:40 +02:00
Patrick Schurig
6670bed6cc [3e+doc] clôture 3e + TEST_TERRAIN.md + point d'étape
docs/TEST_TERRAIN.md : procédure Palier 1 (14 tests T1-T14) pour le banc nymea-dev arm64
— §0 pré-vol (forçables [À LIRE SUR LA BOX]), §1 déploiement (cross-arm64→scp→dpkg,
logging NymeaEnergy.debug, déclaration adaptateurs codée dans energypluginnymea.cpp),
§2-3 ECS 1/3 relais (dont transition non-cascadée 1500→2000), §4 SG-Ready (montée/
atomicité/hystérésis), §5 watchdog L2, §6 interaction priorités, §7 EV optionnel.

AGENTS.md ÉTAT : 3e clôturée (testEcsRelayTopologies dfdd988), audit Doxygen (5→0),
TEST_TERRAIN créé ; déféré = passe README+contrats (force/minStage-maxStage/min-maxState/
degradedMode pas encore dans le protocole publié), Waveshare, V2C, 3f, 3g, config priorités,
arm64 CI, Doxyfile+CI. Prochaine action : test terrain vendredi puis passe contrats.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:31:47 +02:00
Patrick Schurig
e641f289db [3e-étape] ÉTAT : 3e FAITE (suite 19/19) + récap capacités moteur + liste différé
Point d'étape : EV proxy + ECS + SG-Ready sur budget unifié trié par priorité,
protection compresseur, watchdog L2/mode dégradé. Différé : Waveshare (session
dédiée Modbus), 3d/3f/3g, config priorités API+UI, arm64 CI, test terrain vendredi.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:46:27 +02:00
Patrick Schurig
093fa09b5e [3e-3] mapping sémantique SG-Ready + waterfall unifié ECS/SG-Ready
getPlan : cascade unique sur charges non-EV (relay-stages + sg-ready) triées par
priorité — budget de surplus partagé. buildSgReadyStateAction : mapping qualitatif
(4 forcé hyst. 1,2/1,0 ; 3 reco ≥P3 ; 2 normal mains off ; 1 jamais via surplus),
recrédit sur puissance allouée déclarée, clamp lock-aware minState/maxState.

AGENTS.md : ROADMAP config priorités utilisateur (acquis tri unifié ; manque 3g VE
+ couche config JSON-RPC/UI Flutter pour drag-and-drop à chaud). Build 0/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:19:54 +02:00
Patrick Schurig
cbee13e455 [3c-clôture] ÉTAT : 3c FAITE (suite 18/18 + charging 46/46), arm64 cross délégué CI
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:27:30 +02:00
Patrick Schurig
3a8eb5da86 [3c-docs] défauts minOn/minOff par type + séparation budget/verrou (protection compresseur)
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>
2026-06-09 21:26:38 +02:00
Patrick Schurig
f71e0405b4 [3c-6] degradedMode() + notification ChargingSchedulesChanged + invariant zéro-cloud
virtual degradedMode() dans SmartChargingManager (base false, [ETM] additif),
override EnergyArbitrator. Champ o:degradedMode (additif) dans la notification
NymeaEnergy.ChargingSchedulesChanged, émise aussi aux transitions du mode dégradé
(planif suspendue → push du flag via emit chargingSchedulesChanged()).
INTERFACE.md : champ degradedMode documenté.

SAFETY.md : notification réconciliée (ChargingSchedulesChanged, pas EnergyManagerChanged)
+ limite "valeur figée non détectée". Correction ZÉRO CLOUD : suppression de la section
"Alertes externes" / mécanisme n8n, remplacée par une signalisation 100% locale
(notification nymea in-app + buzzer/relais via règle nymea, aucun canal réseau sortant).
Invariant 10 "ZÉRO cloud" gravé dans AGENTS.md.

Build 0 erreur / 0 warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:04:09 +02:00
Patrick Schurig
312a2484ae [3c-5] watchdog L2 : QTimer fraîcheur compteur + mode dégradé conservateur
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>
2026-06-08 16:43:28 +02:00
Patrick Schurig
6298d5d42f [3c-3] waterfall ECS dans RuleBasedScheduler::getPlan() + tri priorité ASC
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>
2026-06-08 16:20:08 +02:00
Patrick Schurig
7709057335 [wip] 3c morceaux 0-2 compilés + plan 3c validé dans AGENTS.md
Morceaux 0-2 implémentés et compilés (0 erreur / 0 warning) :
- M0 : LoadAction.force=false (bypass verrous anti-rebond sécurité)
- M1 : EcsRelayAdapter (.h+.cpp) — N paliers powerswitch, anti-rebond, etm.pri
- M2 : buildContext() — SurplusMeter brut, loads EV+ECS, registerEcsAdapter()

AGENTS.md : section PLAN 3C ajoutée avec corrections A+B intégrées.
Corrections A (déduction EV unique dans scheduler) et B (recrédit conso
propre anti-clignotement) documentées avant implémentation morceau 3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 13:34:42 +02:00
Patrick Schurig
19951a1e3e [3c-prep] AGENTS.md : ajout watchdog L2 + testMeterSilentFallback à la DoD 3c
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:36:12 +02:00
Patrick Schurig
5bb6da0e9f [3b-iv] ETM_ARBITRATOR actif — iso-fonctionnalité prouvée (simulation + tests charging, diff décisions zéro écart)
- energyplugin/energyplugin.pri : décommente DEFINES += ETM_ARBITRATOR (flip actif)
- energyplugin/etm/energyarbitrator.cpp : ajoute qCDebug "Updating smart charging" en tête
  de update() — comparabilité des logs avec l'amont garantie
- AGENTS.md : 3b → ✅ FAITE, chiffres de preuve, prochaine action 3c

Preuve iso-fonctionnalité :
- Simulation : 226 lignes décisions (Theoretically/Surplus/Current load) — diff = 0
- Tests charging : 57 lignes décisions — diff = 0 ; 46/46 PASS ref ET ETM
- [Arbitre] présents avec raisons françaises (idle, surplus PV, aWATTar, deadline)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:29:34 +02:00
Patrick Schurig
c3fedfe36b [3b] décision B + modèle sécurité (AGENTS + SAFETY.md) + Doxygen proxy/inactif
- AGENTS.md : nouvelle entrée "3b révisé — délégation EV à l'amont" (beta hybride
  assumée, ETM réel en 3c, transplantation EV en 3g) ; modèle sécurité L0-L4
  avec double déclenchement verifyOverloadProtection documenté (signal ligne 127 +
  appel cyclique ligne 313 SCM.cpp).
- docs/SAFETY.md : document normatif 5 couches + signalisation locale optionnelle ;
  Variante B confirmée pour le repli L2 (EV au minimum + notification nymea +
  risque 1,4 kW accepté) ; table défaillances/couches corrigée (L1 ne couvre pas
  compteur hors ligne).
- energyarbitrator.cpp update() : commentaire explicitant la correspondance exacte
  avec l'ordre SCM (1-4 parent, ETM entre 4 et 7, planSpot+planSurplus via getPlan).
- rulebasedscheduler.h : Doxygen getPlan() marqué "PROXY AMONT POUR L'EV (beta)".
- evadapter.h : Doxygen applyAction() marqué "Inactif jusqu'à 3g".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 07:41:12 +02:00
Patrick Schurig
7d3fc6e5ea add adresse IP dans AGENTS.md 2026-06-08 07:23:55 +02:00
Patrick Schurig
08039a3542 [wip] état session — phases 0-3b, prochaine action 3b-iv, topologie remotes 2026-06-07 23:34:18 +02:00
Patrick Schurig
5f49e4ca3c [3b-wip] EnergyArbitrator + RuleBasedScheduler + EvAdapter (dispatch amont, ETM_ARBITRATOR désactivé)
- EnergyArbitrator : public SmartChargingManager — raison documentée dans AGENTS.md §DÉCISIONS DE DESIGN
- SmartChargingManager : protected slots + virtual update() + 3 accesseurs inline [ETM]
- RuleBasedScheduler::getPlan() wraps planSurplusCharging/planSpotMarketCharging, annote chaque action d'un reason français
- EvAdapter : ILoadAdapter concret pour evcharger — applyAction() implémenté, NON appelé en 3b (dispatch via adjustEvChargers() amont, iso-fonctionnel)
- ETM_ARBITRATOR : commenté dans .pro — ne s'active qu'après preuve iso-fonctionnelle (3b-iv)
- Doxygen \brief + invariants + contrats sur toutes les classes/méthodes publiques etm/ (DoD §5)
- plan.h : timeSlots (pas slots, mot-clé Qt) ; commentaire JSON sérialisation "slots" OPTIMIZER_PROTOCOL §6
- .clangd : flags de repli Qt/nymea pour clangd via symlink ~/Schreibtisch/
- compile_commands.json gitignore (chemins absolus locaux)
- Build : 0 erreurs, 0 warnings — libnymea_energypluginnymea.so 914 KB

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 23:16:49 +02:00
Patrick Schurig
9017a880ac [brief] AGENTS.md = version définitive (architecture+workflow), statut phases 0-3a, décision TARGET drop-in actée 2026-06-07 22:06:50 +02:00
Patrick Schurig
074fa71308 [brief] AGENTS.md définitif (arbitre+LoadAdapters), CLAUDE.md pointeur, protocole versionné 2026-06-07 21:32:12 +02:00