85 Commits

Author SHA1 Message Date
Patrick Schurig
fa7ee442d3 docs: resserrer la conclusion du volet 1 + piège du rootmeter (ECS-504)
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>
2026-08-09 08:54:32 +02:00
Patrick Schurig
cf49b35baf docs(banc): relevé ECS-306 — volet 1 mesuré, prédiction confirmée
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>
2026-08-09 08:48:26 +02:00
Patrick Schurig
8c868dfd4a deb: bump 1.15.2+etm4 (ECS-412 — blocage circulaire du verrou à froid)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 08:28:45 +02:00
Patrick Schurig
6beee20f43 fix(etm): ECS-412 — armement à froid TRANSITOIRE (blocage circulaire corrigé)
Défaut constaté AU BANC le 2026-08-09, pas en test : une charge sonde de rang 3
est restée figée à 0 W sous 4 kW de surplus disponible.

Mécanisme. lockWindow() traitait un m_lastSwitch nul comme sentinelle
« elapsed = 0 » à CHAQUE cycle. Une charge démarrant au palier 0 avec
minOffS > 0 voyait donc offHeld vrai en permanence, maxStage forcé à 0 : elle ne
pouvait jamais s'enclencher, donc jamais commuter, donc jamais valider
m_lastSwitch. Blocage circulaire. L'intention d'ECS-412 — armer plutôt que
purger — était juste ; mon écriture rendait l'armement DÉFINITIF au lieu de
transitoire.

Portée réelle : toute charge relay-router démarrant relais ouverts avec
minOffS > 0 était gelée. Le chauffe-eau du banc (minOffS = 60) n'y échappait que
parce que ses relais étaient déjà fermés et qu'ECS-411 lui faisait reprendre un
palier non nul — au premier démarrage sur installation froide, il aurait été
bloqué lui aussi.

Correction : armement PARESSEUX. applyAction(), chemin non-const qui reçoit le
temps de cycle, estampille m_lastSwitch au premier now s'il est invalide. Le
verrou expire alors après sa durée configurée. Le repli « elapsed = 0 » ne
couvre plus que les cycles précédant la première action. L'invariant « temps =
paramètre, jamais l'horloge » est préservé : aucune horloge n'est lue, et le
contrat d'ILoadAdapter n'est pas modifié — passer now au constructeur aurait
changé l'interface pour un cas particulier.

Symétrie vérifiée : relais fermés au départ → ECS-411 donne un palier non nul,
c'est minOn qui s'arme ; relais ouverts → palier 0, c'est minOff. Les deux sont
désormais transitoires de la même façon.

Test — le vrai livrable : testEcsColdStartLockExpires. Palier 0 au départ,
minOffS = 120, budget de 5000 W. La charge reste éteinte à t0 et à t0+119, puis
s'enclenche à t0+121. Couvre aussi l'expiration de la fenêtre exposée au
scheduler (lockMaxPowerW : 0 pendant le verrou, 1000 après). Aucun test ne
combinait « palier 0 au départ » et « minOffS > 0 » — c'était le trou exact.

spec_ecs.md : ECS-412 précise que l'armement est transitoire, d'une durée égale
au verrou configuré, posé paresseusement, avec le tableau de symétrie et le
renvoi au test.

Build amd64 0 erreur. Simulation : 15/15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 08:24:41 +02:00
Patrick Schurig
793b76b6f7 docs(banc): prédiction ECS-306 et commutations — publiée AVANT la mesure
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>
2026-08-09 08:05:49 +02:00
Patrick Schurig
1996d7da7c docs(spec): INST-104 — .75 réglée pour émuler l'installation de référence
La configuration de la box de test déclarait 2 relais à 600/1200 W ; elle en
déclare désormais 3 à 500/1000/2000, posés par SetLoadConfig (RPC, rebuild à
chaud). Trois relais GPIO, Relay1/2/3 ; Relay4/5 restent au SG-Ready.

Formulation retenue : « réglée pour ÉMULER l'installation de référence », pas
« alignée sur le câblage réel ». La différence n'est pas rhétorique — elle dit
que la vérification physique en attente (INST-102) porte sur l'installation
CLIENTE, pas sur les broches de .75. Mesurer le banc à la pince ne confirmerait
que le banc.

Fait établi au passage : le câblage de .75 était DÉJÀ documenté dans
docs/TEST_TERRAIN.md:24-26 sous les noms R500/R1000/R2000, broches BCM 5/6/13,
avec les mêmes ThingId. La configuration à 600/1200 était donc un reliquat,
contredit par la documentation du banc elle-même — ce n'est pas la spec qui a été
imposée au banc, c'est le banc qui a été remis d'accord avec sa propre doc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 14:57:51 +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
791d272311 deb: bump 1.15.2+etm3 (étape 1 — ECS-306, ECS-411, ECS-412)
Version alignée sur la box cible : nymea 1.15.2, plugin installé 1.15.2+etm2.
Numérotation contiguë — aucun +etm3 n'a jamais été distribué.

Le fichier réel est debian-qt5/changelog : debian -> debian-qt6, et
debian-qt6/changelog -> ../debian-qt5/changelog. Un seul changelog pour les deux
arbres de packaging, écrit via debian/changelog.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 14:36:38 +02:00
Patrick Schurig
fa35ff9ba2 docs(spec): spec_ecs 0.5.2 — câblage 500/1000/2000 et DÉCOUPLAGE des puissances
Correction. La 0.5.1 avait corrigé le câblage en 500/1000/1500 sur la foi du
simulateur du banc ; le simulateur ne reflète pas l'installation. Le câblage réel
est 500/1000/2000. Quatrième révision de cette donnée.

Les trois consignations reviennent à l'état de la 0.5.0 :
  - ECS-302 NON exercé — pondération binaire, aucune redondance d'encodage. Il
    perd son caractère de préalable structurel et redescend derrière ECS-305 dans
    l'étape 3 ;
  - ECS-303 RÉTABLI, mais reformulé sans puissances : « hystérésis élargie aux
    frontières de recombinaison », déclenchée dès qu'un passage de palier impose
    de basculer plus de deux relais ;
  - 8 valeurs distinctes, plafond 3500 W, usure 7/3/1 par balayage.

DÉCOUPLAGE — c'est le vrai objet de cette version. Les puissances ne vivent plus
que dans un bloc unique, §4.0 « Installation de référence », qui porte les trois
valeurs, leur source et leur date, ainsi qu'un tableau « ce que cette
installation exerce et ce qu'elle n'exerce pas ». Aucune exigence ne contient
plus de puissance : celles dont le rang ou la portée en dépendent le disent par
renvoi. Vérifié — hors §4.0, il ne subsiste que le changelog, le journal et
l'illustration P = U²/R d'ECS-500.

Motif : ce paramètre a fait quatre allers-retours, chacun coûtant la réécriture
de trois exigences. Le cinquième ne coûtera qu'un paragraphe.

Source inscrite telle qu'elle est : DÉCLARATION DE L'UTILISATEUR, NON MESURÉE.
Une vérification physique — plaque signalétique ou pince ampèremétrique — est en
attente et reste un préalable à l'ouverture de l'étape 3.

Note ajoutée sous ECS-411 : sur cette installation les encodages sont uniques,
donc le modèle d'état ne peut pas diverger du physique. Sur un câblage à
encodages multiples, une correspondance par puissance seule renverrait un palier
dont l'encodage canonique diffère de l'ensemble réellement fermé — la reprise
doit alors restituer l'ensemble de relais, pas seulement l'index.

Le simulateur du banc sera aligné sur cette installation dans un lot SÉPARÉ, sur
le dépôt etm-powersync-hems-sim.

Aucun code touché.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 13:34:43 +02:00
Patrick Schurig
3846a2e29c fix(etm): ECS-306 — le budget décrémenté suit le palier réellement applicable
Régression contre une décision documentée : AGENTS.md, section « Verrous
minOn/minOff », énonce que le scheduler clampe et décrémente le budget au palier
réel. Le canal qui le permettait — minStage/maxStage dans LoadContextTelemetry —
a été retiré en rév. 2/3. L'écrêtage a continué d'avoir lieu dans l'adaptateur,
mais son résultat ne revenait plus à l'arbitre : pendant toute la fenêtre minOn,
les charges de priorité suivante recevaient un résidu SURESTIMÉ et
l'installation soutirait au réseau.

Règle visée : la 4 — « bornes par adaptateur écrêtent TOUTE sortie de
stratégie ». Pas la 1 : il n'y a qu'un décideur, le défaut est un décalage de
comptabilité entre décision et exécution.

Le canal est restauré EN WATTS, pas en index de palier : lockMinPowerW /
lockMaxPowerW dans LoadContextTelemetry, remplis par RelayRouter::toLoadContext()
depuis lockWindow(), convertis via la table de paliers. La frontière rév. 3 tient
— aucun identifiant de relais ni index de combinaison ne remonte. Un
lockMaxPowerW négatif signifie « aucun plafond », et non « plafond nul ».

buildSetpointAction() applique ces bornes AVANT de décrémenter le budget, donc le
résidu passé aux charges suivantes tient compte de la puissance engagée — quitte
à devenir négatif, ce qui est la réponse correcte. Le decisionReason distingue ce
cas : « Verrou minOn — X maintenue à N W (puissance engagée, budget M W) ; le
résidu en tient compte ».

Corriger dans le cycle, pas au cycle suivant : remonter le palier appliqué après
coup n'aurait rattrapé l'erreur qu'au tour d'après.

Test : testEcsBudgetUnderLock — deux charges classées, la première verrouillée à
2000 W sous un budget de 500 W ; la seconde reste à 0 parce que le résidu tient
compte des 2000 W engagés.

Build amd64 0 erreur. Simulation : 14/14.

Réf. specs/spec_ecs.md §3 ECS-306 (0.5.1).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 12:47:55 +02:00
Patrick Schurig
701759ddbb fix(etm): ECS-411 — reprise du palier depuis l'état réel des relais
m_currentStage valait 0 à la construction alors que des contacts peuvent être
fermés : le moteur croyait 0 W pendant que le ballon tirait sa puissance. Même
famille de défaut qu'ECS-410 — annoncer une puissance non appliquée.

deduceStageFromThings() lit l'état réel des Things au constructeur, en trois
temps :

  1. correspondance EXACTE de l'ensemble de relais fermés avec un palier — cas
     nominal ;
  2. à défaut, correspondance par PUISSANCE. Ce cas se produit avec les encodages
     dédupliqués : deux combinaisons de même puissance existent, une seule est
     conservée dans la table. Toute somme de sous-ensemble figure nécessairement
     dans m_levels, qui est construit de ces sommes — la reprise aboutit donc
     toujours, et le premier applyAction() normalise l'encodage des relais ;
  3. hors table (troncature à MaxRelays) : palier MAXIMAL, jamais 0.

Un relais introuvable est supposé FERMÉ, avec avertissement. Le principe est
constant sur les trois branches : ne jamais annoncer moins que ce qui peut être
appliqué. Sous-estimer est le défaut qu'ECS-411 corrige ; surestimer ne fait que
retarder une montée en puissance.

Le repli initial sur le palier 0 — écrit dans un premier jet — reproduisait
exactement le défaut visé et a été corrigé avant ce commit.

Test : testEcsRestartRecovery — trois cas, dont une combinaison à deux relais
(R500+R1500 = 2000 W) et le cas tout-ouvert.

Build amd64 0 erreur. Simulation : 13/13.

Réf. specs/spec_ecs.md §5 ECS-411 (0.5.1).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 12:45:15 +02:00
Patrick Schurig
c9b8e63f89 fix(etm): ECS-412 — rebuild incrémental et armement du verrou à froid
Cause racine, pas symptôme. rebuildLoadAdapters() détruisait TOUS les adaptateurs
à chaque SetLoadConfig, réarmant leurs verrous. Sur un ballon thermodynamique à
minOn de 300-600 s, un client qui réordonne ses priorités depuis l'app pouvait
faire court-cycler son compresseur. C'est de la protection matérielle.

L'arbitre mémorise désormais la config ayant servi à construire chaque adaptateur
(m_builtFrom) et ne reconstruit que si le MATÉRIEL a changé — type, câblage,
paliers, plafond, verrous (sameHardware()). Un changement de rang ou de besoins
passe par updateSoftConfig(), en place : ni m_currentStage ni m_lastSwitch ne
bougent, aucun relais n'est réécrit. Le log distingue créées / mises à jour /
inchangées / retirées.

updateSoftConfig est PURE VIRTUELLE, sans implémentation par défaut. Un défaut
vide silencieux ferait qu'un futur adaptateur construit depuis LoadConfig
ignorerait sans bruit les changements de rang ; là, le compilateur force la
décision. EvAdapter et SgReadyAdapter la déclarent sans effet, avec le motif.

Démarrage à froid : après un redémarrage de nymead, m_lastSwitch est
irrécupérable. lockWindow() traite désormais un horodatage nul comme
« commutation venant d'avoir lieu » (elapsed = 0), donc verrou ARMÉ pour sa durée
configurée. L'écriture naturelle (`valid && elapsed < minOnS`) fait l'inverse et
laisserait une boucle de redémarrage court-circuiter la protection compresseur
quand elle est la plus nécessaire. Armer n'est pas verrouiller
inconditionnellement : avec une durée nulle, `0 < 0` est faux et le verrou reste
inactif — un premier jet qui forçait le verrou a fait tomber trois tests
existants, qui avaient raison.

Test : testEcsRebuildPreservesLock — SetLoadConfig pendant une fenêtre de verrou
active, le relais reste fermé ; le délestage reprend une fois minOn écoulé.

Build amd64 0 erreur. Simulation : 12/12.

Réf. specs/spec_ecs.md §3 ECS-412 (0.5.1).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 12:42:56 +02:00
Patrick Schurig
1011a6ccd7 docs(spec): spec_ecs 0.5.1 — câblage réel 500/1000/1500, les trois consignations s'inversent
La 0.5.0 consignait 500/1000/2000 sur la foi de la consigne d'étape 1. Le
simulateur du banc contredit cette donnée ; arbitrage : le simulateur reflète
l'installation, la donnée était fausse.

Câblage : 500/1000/1500 W, additif, 7 valeurs distinctes de 0 à 3000 W, avec UNE
redondance — 1500 W admet {R1500} et {R500+R1000}.

Trois corrections :

1. ECS-302 est EXERCÉ. Il redevient préalable structurel de l'étape 3 et reprend
   son rang devant tout ce qui s'appuiera sur m_relayMapping.

2. ECS-303 SUPPRIMÉ. Le « point dur » 1500→2000 n'est pas une exigence
   d'hystérésis, c'est une conséquence directe d'ECS-302 non traité :
   relayrouter.cpp:58 retient la première combinaison rencontrée (masque le plus
   bas), donc {R500+R1000} pour 1500 W, et jette {R1500}. La transition vers
   2000 W = {R500+R1500} demande alors d'ouvrir R1000 et de fermer R1500, avec le
   creux qu'impose l'ordre coupure-avant-fermeture. Avec {R1500} pour 1500 W, la
   même transition ne coûterait QU'UNE commutation : fermer R500. Aucune entrée
   ECS-303 ni ECS-605 de ce côté.

3. 7 valeurs et plafond 3000 W, non 8 et 3500. ECS-305 reste valable, comptes
   corrigés : par balayage complet R500 commute 5 fois, R1000 3 fois, R1500 une
   seule (vérifié sur l'encodage effectivement retenu par le code actuel).

RÉSERVE INSCRITE : la source est le simulateur (sim/sim_ecs_router.py:38, sommé
dans _power_w(), confirmé en vol sur MQTT), PAS une mesure physique. Ce chiffre a
déjà changé trois fois et il commande désormais l'ordre de l'étape 3 : il doit
être confirmé sur plaque signalétique ou au pince ampèremétrique AVANT
l'ouverture de cette étape. Consigné en §13-3 et en garde-fou sous l'étape 3.

Aucun code touché.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 12:15:40 +02:00
Patrick Schurig
1e80a83df9 docs(spec): spec_ecs 0.5.0 — §13-1/§13-2 tranchés, étape 1 ouverte, câblage confirmé
Précondition de la consigne d'étape 1 : les deux arbitrages étaient RELEVÉS au
journal, pas TRANCHÉS. Ils le sont maintenant, datés, dans les termes reçus.

§13-1 CLOS — le schéma « ARCHITECTURE CIBLE » d'AGENTS.md a été redessiné par le
lot de consolidation (f6a4be5) : RelayRouter et EtmVariableLoadAdapter y
figurent, le kind Stage en est retiré, SocketScheduler et BatteryAdapter sont
marqués non écrits.

§13-2 CLOS, lecture (b) — ECS-412 réduit à m_lastSwitch et redirigé vers sa cause
racine (rebuild incrémental : ne reconstruire que ce qui a changé) ; ECS-411
remonté en étape 1. L'étape 1 porte donc TROIS exigences : ECS-306, ECS-411,
ECS-412. Ajouté explicitement : au démarrage à froid, m_lastSwitch étant
irrécupérable, le verrou DOIT être ARMÉ et non purgé — l'implémentation naturelle
(QDateTime nul = verrou inactif) fait l'inverse et court-circuiterait la
protection compresseur sur une boucle de redémarrage.

Câblage du banc confirmé 500/1000/2000 (pondération binaire, 8 paliers de 0 à
3500 W au pas de 500, aucune redondance). Trois conséquences consignées :
  - ECS-302 n'est PAS exercé par ce matériel — un seul encodage par palier. La
    restructuration de m_relayMapping perd sa justification immédiate et cesse
    d'être un préalable structurel de l'étape 3.
  - ECS-305 monte en importance : sur un balayage complet R500 commute 7 fois,
    R1000 3 fois, R2000 une seule. C'est la seule mesure d'usure utile.
  - ECS-303 CRÉÉ — point dur 1500→2000 W : les trois relais basculent d'un coup,
    donc trou de puissance à chaque franchissement avec l'ordre
    coupure-avant-fermeture. La parade est un seuil d'hystérésis élargi à cette
    frontière, pas un réordonnancement. À MESURER au banc avant de coder.

Aucun code touché.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 11:56:53 +02:00
Patrick Schurig
41d3a329e4 docs(etm): contrats Doxygen — 25 derniers éléments hors fichiers différés
Complète la couverture : 31 → 6 avertissements. Les 6 restants sont exactement
les deux fichiers différés jusqu'à l'étape 3 (relayrouter.h 3,
energyarbitrator.h 3, ce dernier étant la cible d'ECS-412).

energyratioscalculator.h traité EN PREMIER, pour la raison qui le distingue :
c'était le seul endroit où l'absence de documentation cachait une règle métier.
La règle était en fait écrite — mais en commentaires « // », invisibles à
doxygen, donc invérifiables. Conversion en blocs Doxygen SANS réécriture du
fond : les trois cas de reseed de la baseline (premier appel, changement de jour
local, compteur non monotone) et la garantie « dénominateur ≤ 0 → n/a, jamais de
NaN » deviennent des invariants opposables. Précisé au passage que le paramètre
`now` doit être en heure LOCALE — passer de l'UTC déplacerait la frontière de
journée.

Reste du lot : loadaction.h (quels champs sont significatifs selon le kind),
plan.h ([from, to[ et l'interdiction de retourner un plan invalide),
loaddescriptor.h, loadconfigstore.h (tolérance de chargement : un fichier absent
n'est pas une erreur, une entrée invalide est ignorée seule).

Build amd64 0 erreur. Simulation : 7/7, dont testEnergyRatiosAlignment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 11:47:19 +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
0fefbc509e fix(doc): deux défauts Doxygen réels dans etm/
Corrections, pas ajouts. Les deux n'apparaissent que sous une configuration
générant réellement une sortie (cf. Doxyfile) — ils étaient invisibles jusqu'ici.

- loadconfigstore.h : « <LoadConfig §4> » dans le bloc de tête était interprété
  comme une balise xml/html non supportée, ce qui tronquait la description du
  format fichier. Reformulé sans chevrons.

- sgreadyadapter.h : lockWindow() documentait « \param[out] minState / maxState »
  en une seule directive ; doxygen n'en reconnaissait aucune des deux et
  considérait « now » et « maxState » comme non documentés. Scindé en trois
  directives, une par paramètre.

Aucun changement de comportement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 11:46:42 +02:00
Patrick Schurig
06e01363d9 build(doc): Doxyfile de vérification (HTML + XML), non bloquant
Périmètre energyplugin/etm/, EXTRACT_ALL=NO — sans quoi doxygen documente tout
d'office et n'avertit de rien.

GENERATE_HTML et GENERATE_XML restent à YES, et c'est structurel : le nombre
d'avertissements dépend des générateurs actifs. Sans aucune sortie, doxygen
1.9.8 signale « not documented » des membres qui le sont (mesuré sur le même
arbre : 163 sans sortie, 167 en XML seul, 109 en LaTeX, 113 en HTML+XML). Seule
la configuration HTML+XML donne un comptage honnête. L'avertissement est écrit
en tête du fichier.

XML pour une éventuelle chaîne Breathe/Sphinx — coût mesuré ~180 ms, 73
fichiers, 1,5 Mo. Note : l'amont nymea documente en qdoc, pas en doxygen ;
nymea-docs n'a pas pu être consulté.

WARN_AS_ERROR=NO : le job CI ne devient bloquant qu'à la clôture de l'étape 3
de specs/spec_ecs.md (relayrouter.h et le noyau de calcul sont différés
jusque-là). Aucun job CI créé dans ce commit.

doc-generated/ (OUTPUT_DIRECTORY) ajouté au .gitignore.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 11:46:16 +02:00
Patrick Schurig
3f6aae0f4b chore(gitignore): artefacts de build des tests (19 → 4 entrées non suivies)
Les artefacts produits par `qmake6 CONFIG+=build_tests && make` polluaient
`git status` en permanence. Ajoutés : qrc_*.cpp, target_wrapper.sh, les trois
binaires de test (chemins explicites — ces cibles n'ont pas d'extension), le
plugininfo généré du mock, build-check/ et .cache/.

Portée du motif plugininfo volontairement limitée à
tests/mocks/plugins/energymocks/ : energyplugin/plugininfo.h est SUIVI, un motif
global aurait été trompeur.

Vérifié : aucun fichier suivi n'est nouvellement ignoré
(`git ls-files | git check-ignore --stdin` → vide).

Restent non suivis, volontairement — fichiers de travail, pas des artefacts :
BRIEF_agent_energymanager.md, consignes/, jsonRPC.txt, logs-3biv/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 10:53:21 +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
9624b6ec87 docs(specs): spec_ecs 0.4.3 + spec_loadmodel 0.1.1
Deux documents normatifs, préalables au lot AGENTS.md qui les référence.

spec_ecs.md 0.4.3 — spécification ECS multi-palier, écrite après audit du code :
  - 0.4.1 : statut réel des binaires de test, portée du point de gouvernance
    AGENTS.md, citation de règle dans ECS-306, recouvrement ECS-411/ECS-412 ;
  - 0.4.2 : étape 2 « type domaine » → « noyau de calcul » (collision avec LM-100) ;
  - 0.4.3 : ECS-110 étendu — la validation ne doit pas reposer sur Q_ASSERT,
    absent du binaire release (QT_NO_DEBUG).

spec_loadmodel.md 0.1.1 — modèle de charges (domaine × mécanisme), intention de
conception. Ne déclenche aucun travail.

Aucun code touché.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 10:50:50 +02:00
Patrick Schurig
c27ccd317d docs(recap): ratios poussé sur feature/beta-rulebased (landing-silo à jour ad385ca)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:43:51 +02:00
Patrick Schurig
08175f258b docs(recap): commit ratios 444b13d (committé, non poussé) — reprise 2026-07-03
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:38:07 +02:00
Patrick Schurig
444b13dbf8 feat(ratios): NymeaEnergy.GetEnergyRatios + EnergyRatiosChanged (voie c)
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>
2026-07-01 07:56:35 +02:00
Patrick Schurig
ad385ca8fc deb: bump 1.15.2+etm2 (rév. 3 — frontière optimiseur↔routeur, RelayRouter)
Build contre nymea 1.15.2. Paquet rév. 3 pour test live RelayRouter sur hems (testing).
(changelog réel = debian-qt5/changelog ; debian → debian-qt6 → ../debian-qt5/changelog.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:39:51 +02:00
Patrick Schurig
4d2e457ed2 test(etm): restaurer testEcsRelayTopologies côté routeur + test config→RelayRouter (rév. 3)
testEcsRelayTopologies réécrit pour le RelayRouter (la combinatoire vit maintenant dans le
routeur, plus dans le thing) :
- 1 relais (dégénéré [0, 2000]) ;
- 3 relais 500/1000/2000 NON-CASCADÉ : paliers dérivés [0,500,..,3500], arrondi watts→combo,
  transition 1500→2000={r2000} seul (off-before-on, commute 3 relais) ;
- DÉDUPLICATION : deux relais identiques 1000 W → niveaux fusionnés [0,1000,2000] (3, pas 4),
  le palier 1000 n'utilise qu'un relais — prouve que la dérivation dédoublonne.

testLoadConfigRelayRouter (nouveau) : chaîne COMPLÈTE SetConfigs(relays[]) → store.changed →
rebuild → RelayRouter → cycle surplus → commutation relais → currentPowerW (pas les pièces
isolées).

Build prod 0/0 ; suite complète verte (8/8 tests à 3 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:00:21 +02:00
Patrick Schurig
88626cfd56 feat(etm): câblage RelayRouter + repli L2 unifié (rév. 3) — routeur vivant, sécurité fermée
L'arbitre construit et pilote les routeurs ; le repli L2 couvre les DEUX types ENSEMBLE
(atomique : pas d'intermédiaire avec charge active sans repli).

- Map unique m_loadAdapters (QHash<QString, ILoadAdapter*>) : RelayRouter + EtmVariableLoadAdapter
  cohabitent par polymorphisme. registerRelayRouter() ajouté.
- rebuildLoadAdapters() : la distinction de TYPE vit ICI — RelayRouter si relays[], sinon
  EtmVariableLoadAdapter. Au-dessus tout est ILoadAdapter (Setpoint W).
- Dispatch Setpoint AGNOSTIQUE au type : routage par loadId, le polymorphisme absorbe (pas de
  if(RelayRouter)). Le scheduler distingue Setpoint (etmvariableload + relay-router) vs State
  (sg-ready) — nature de l'action, pas classe concrète.
- Repli L2 : applyDegradedMode boucle sur TOUS les ILoadAdapter → Setpoint(0) force=true. Pour
  le RelayRouter, coupe TOUS les relais (bypass minOn). Ferme le trou T2 pour le routeur.
- testMeterSilentFallback étendu au cas RELAIS : compteur muet → 2 relais OFF force=true,
  restent OFF sur N cycles. Certifie la sécurité relais.

Build prod 0/0 ; suite (config/L2/migrés) verte. Étape qui rouvre le déploiement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:40:28 +02:00
Patrick Schurig
e16aca4d1a feat(etm): RelayRouter — couche routeur watts→relais (contrat rév. 3)
Restauration de l'ex-EcsRelayAdapter (b7bfd58) repositionné en ROUTEUR distinct, sous
l'optimiseur watt-pur. ILoadAdapter consommant Setpoint(W) au lieu de Stage :

- Paliers DÉRIVÉS : toutes les combinaisons (sommes de sous-ensembles) des relais, dédupliquées,
  triées, 0 inclus — source de vérité unique = les relais (descriptor().powerLevels/maxPowerW).
- applyAction(Setpoint W) : mappe powerW → combinaison la plus haute ≤ powerW, applique les
  verrous minOn/minOff EN INTERNE (clamp, plus exposés au scheduler), commute via
  ThingManager::executeAction (interface "power"), off-before-on. force=true → bypass (repli L2).
- currentPowerW : mesuré si relais expose "currentPower", sinon nominal commandé (GPIO bool nu).
- LoadContext : watts uniquement (powerLevels/currentPowerW) — aucun relais ne franchit la frontière.

Classe INERTE à ce commit (non câblée à l'arbitre) : aucune charge ne l'utilise, donc aucune
charge sans repli L2. Câblage + repli L2 = étape 4 (atomiquement). Build prod 0/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:03:17 +02:00
Patrick Schurig
5585a5c483 feat(etm): LoadConfig rév. 3 — forme relay-router (relays[]) + validation conditionnelle
LoadConfig gagne la forme "relay-router" (ECS multipalier rév. 3) : liste de relais
power (relays[] = [{thingId, powerW}]) + minOnS/minOffS. Coexiste avec la forme
"etmvariableload" (continu/dynamic ou multipalier natif). powerLevels NON stockés pour
le cas relais (DÉRIVÉS par le routeur en étape 3-4).

- LoadConfigRelay (struct) + relays/minOnS/minOffS dans LoadConfig (Q_GADGET + toMap/fromMap).
- isValid() CONDITIONNELLE au type (adapter/mode), pas "tous champs requis" :
  relay-router → relays[] valides ; etmvariableload → powerLevels (fixed) | maxPowerW (dynamic).
- Schéma SET : TOUS les champs spécifiques en "o:" (powerLevels, maxPowerW, relays, minOnS,
  minOffS, needs) — sinon nymea rejette une des deux formes avant le handler (bug objectRef
  strict de T4, doublé en rév. 3). GET reste typé objectRef<LoadConfig>.
- testLoadConfigRpc étendu : SET des DEUX formes (etmvariableload + relay-router) tous deux
  acceptés, round-trip relays[]/minOnS, rejets conditionnels (powerLevels sans 0 ; relays[] vide).

Build prod 0/0 ; suite config/L2/migrés verte.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:49:52 +02:00
Patrick Schurig
1f976f0189 docs(contract): INTERFACE charge pilotée rév. 3 — frontière optimiseur↔routeur
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>
2026-06-28 11:17:39 +02:00
Patrick Schurig
7184fe4e1d feat(etm): config charge pilotée persistée (Get/SetLoadConfig) + repli L2
Pont moteur↔app : déclaration des charges etmvariableload persistée et éditable,
construction des adaptateurs à chaud depuis la config (remplace le registre en dur).

- LoadConfig / LoadConfigNeeds (Q_GADGET typés, introspectables) — forme mot pour mot
  du LoadDescriptor §4 (jonction inter-repos avec l'app).
- LoadConfigStore : persistance atomique /var/lib/nymea/energy-load-configuration.json,
  validation en bloc, signal changed().
- Handler NymeaEnergy.GetLoadConfig / SetLoadConfig (+ notif LoadConfigChanged). GET typé
  objectRef<LoadConfig> ; SET schéma inline o: (powerLevels/needs conditionnels §4).
- EnergyArbitrator : setLoadConfigStore + rebuildEtmVariableLoadAdapters (enabled==true
  seulement, §9) ; repli L2 = setPowerSetpoint(0) force=true sur tout etmvariableload
  (ferme le trou sécurité ouvert en T2).
- Tests simulation : migration ECS→etmvariableload (arrondi fixed/dynamic, recrédit,
  délestage, round-trip powerSetpoint), budget partagé etmvariableload↔PAC, watchdog L2
  réactivé, persistance + construction depuis config + injection RPC end-to-end.

PAC SG-Ready reste hors config (§8, gelée). Les 5 tâches du brief sont couvertes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:51:38 +02:00
Patrick Schurig
3cb79f5918 feat(etm): règle d'arrondi etmvariableload dans le waterfall + infra de test
RuleBasedScheduler : buildSetpointAction (contrat rév. 2 §5) — fixed = arrondi au plus
haut powerLevels ≤ budget ; dynamic = clamp(budget, 0, maxPowerW) ; recrédit currentPowerW
de DÉBUT de cycle (invariant 8) ; résidu cascadé. Waterfall unifié : etmvariableload
(Setpoint W) + sg-ready (State) triés par priorité, un seul budget.

EnergyArbitrator : registerEtmVariableLoadAdapter, inclusion dans buildContext, dispatch
Setpoint par loadId (l'EV reste au proxy amont).

Infra de test : ThingClass mock etmVariableLoad (currentPowerW read / powerSetpoint write)
+ handler executeAction + helper addEtmVariableLoad — valide l'implémentabilité de
l'interface avant le thing réel. Les scénarios simulation migrés arrivent en T4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:47:58 +02:00
Patrick Schurig
51006745ed refactor(etm): retirer le kind Stage (ECS→Setpoint), chemin State PAC conservé
Contrat etmvariableload rév. 2 §6 : LoadAction garde DEUX kinds (Setpoint W + état
SG-Ready). On supprime UNIQUEMENT le kind Stage (index de palier ECS) ; l'ECS passera
en etmvariableload (Setpoint W) en T3.

- LoadAction : enum Kind { Setpoint, State, Constraint } (retrait Stage + champ stage).
- SurplusContext : retrait stage/minStage/maxStage de LoadContextTelemetry. state/minState/
  maxState (PAC) CONSERVÉS.
- LoadDescriptor : retrait du champ stages (relay-stages orphelin).
- RuleBasedScheduler : retrait buildEcsStageAction ; waterfall = sg-ready uniquement.
- EnergyArbitrator : retrait registerEcsAdapter / m_ecsAdapters / dispatch Stage /
  boucle contexte ECS / repli L2 ECS. Chemin State (PAC) intact.
- Suppression de EcsRelayAdapter ; retrait du registre en dur ECS (energypluginnymea).

NB : entre ce commit et T4 le repli L2 charge pilotée est absent (recâblé en T4) —
commit intermédiaire d'une branche feature, jamais déployé tel quel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:23:19 +02:00
Patrick Schurig
b7bfd58139 feat(etm): adaptateur etmvariableload (interface charge pilotée, Setpoint W)
Côté energymanager : EtmVariableLoadAdapter consomme l'interface etmvariableload
(states currentPowerW read / powerSetpoint write) pour les charges à puissance
pilotable (EV / ECS résistif / routeur PV). La combinatoire matérielle, l'anti-rebond
et les phases vivent dans le thing (contrat §1/§3) — l'adaptateur n'écrit qu'un
setpoint W et relit currentPowerW.

- LoadDescriptor : champs additifs powerLevels / maxPowerW (déclaration §3/§5).
- Contrat partagé versionné : docs/INTERFACE_etmvariableload.md (rév. 2).
- La déclaration de l'interface nymee + le driver thing relèvent d'une session device.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:06:16 +02:00
Patrick Schurig
c43449cafe log: promouvoir transitions ECS/SgReady de qCDebug → qCInfo
Visible avec NymeaEnergy.info=true dans nymead.conf sans activer debug global.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 06:27:00 +02:00
Patrick Schurig
9242a6e28c deb(qt6): fix install.in — chemin qm installé (debian/tmp) pas chemin source
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 16:46:54 +02:00
Patrick Schurig
54c378c550 deb: basculer sur debian-qt6 (hems = Qt6) + powersync-* + Provides/Conflicts/Replaces
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 16:39:29 +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
dd3503ab52 deb: bump vers 1.15.2+etm1 (build contre nymea 1.15.2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 16:17:39 +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
f7cb1ba8af [build] exclure nymea-energy-tests du build deb
debian/rules : DH_OPTIONS=-N nymea-energy-tests — les binaires de test
n'existent pas dans le build deb (ils nécessitent CONFIG+=build_tests).
L'exclusion est globale sur tous les outils dh, pas de paquet vide produit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 14:26:50 +02:00
Patrick Schurig
45b5e8af16 [build] .pro racine : tests sous CONFIG+=build_tests (pas dans le build deb)
libnymea-tests:arm64 1.15.0 n'exporte pas enableNotifications — le link
des tests dans dpkg-buildpackage échoue. Politique : les tests ne font pas
partie du paquet de prod ; ils sont réservés au CI/dev (CONFIG+=build_tests).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 14:15:50 +02:00
Patrick Schurig
67b32be92f [deploy] §1.d banc hems : EcsRelayAdapter + SgReadyAdapter dans init()
Enregistre les adaptateurs ECS (3 relais R500/R1000/R2000) et SG-Ready (K1/K2)
avec les ThingIds réels du banc hems dans EnergyPluginNymea::init(), compilé
uniquement sous ETM_ARBITRATOR. Bloquant pour le cross-build arm64 Palier 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 13:11:59 +02:00
Patrick Schurig
380823dc9d [doc] TEST_TERRAIN.md : helper relay() JSON-RPC + seuils SG-Ready + pré-vol §3.5
TÂCHE A — relay() réécrit (JSON-RPC complet) :
- JSONRPC.Hello obligatoire avant tout appel
- auth optionnelle : Users.Authenticate → token (NYMEA_USER/NYMEA_PASS)
- framing par id : boucle sur lignes newline-delimited, ignore notifications push
- résolution stateTypeId 'power' via GetThingClasses (les states GetThings ne portent
  pas le nom, seulement le stateTypeId — l'ancien s.get('name') ne matchait jamais)
- note ACCEPTANCE : tester relay isolément avant T1

TÂCHE B — seuils SG-Ready réels (rulebasedscheduler.cpp kForceMargin=1.2) :
- table seuils en en-tête §4 : état 3 ≥1500, état 4 ≥3600, zone morte ≥3000
- T7 : arithmétique du recrédit explicitée (2500+1500=4000≥3600, pas d'incohérence)
- T9 : annotations (budget ≈ XXXX) supprimées → sémantique surplus brut

Batch précédent (session antérieure rejetée) :
- ThingIds réels R500/R1000/R2000/K1/K2 + BOX/MPORT remplis partout
- root@$BOX → etm@$BOX + sudo (§1.b, §1.c, helpers, logs())
- §3.5 pré-vol SG-Ready : polarité K1/K2 + gpioget nymead arrêté + multimètre NO-COM

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 12:56:45 +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
dfdd9884d0 [3e+] ECS non-cascadé : applyRelayStage off-before-on + tests 1 et 3 relais
applyRelayStage faisait déjà du set-cible complet (delta correct, gère le non-cascadé) :
durcissement off-before-on (anti sur-puissance transitoire quand monter de palier éteint
des relais, ex. 3 résistances 500/1000/2000 : 1500→2000 commute 3 relais) + intention
documentée (comme SG-Ready).

testEcsRelayTopologies : ECS simple 1 relais (on/off) + ECS 3 relais non-cascadé
(transition 1500→2000 → set final r2000 SEUL, r500/r1000 coupés). Couvre les 2 topologies
du test terrain vendredi. Suite simulation 20/20, plugin prod 0/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:09:24 +02:00
Patrick Schurig
51760a7f61 [doc] audit Doxygen : \param 'now' + docs périmées après refactors 3c/3e
Audit manuel (doxygen non installé, pas de Doxyfile). 5 findings corrigés :
- EvAdapter::applyAction : \param now manquant (param partiellement documenté → warning) ;
  toLoadContext : \param now ajouté.
- EnergyArbitrator::buildContext : mention SG-Ready + \param now (source unique verrous).
- applyActionsToAdapters : dispatch State→SG-Ready documenté (était ECS/Stage seul).
- onMeterWatchdogTick : doc alignée sur le refactor 7c (délègue à evaluateMeterFreshness,
  QTimer sous #ifndef ENERGY_SIMULATION).
- RuleBasedScheduler (classe + getPlan) : décrivait seulement le proxy EV → ajout du
  waterfall non-EV (budget net signé, priorité ASC, recrédit, clamp lock-aware) et
  correction "seul ctx.timestamp utilisé" (faux : meter + loads aussi).

Concepts 3c/3e vérifiés documentés : seam de temps/lockWindow, minStage/maxStage,
atomicité 2 bits (transientHarm), mode dégradé L2, waterfall unifié + ordre EV→ECS/SG-Ready,
hystérésis SG-Ready. Build 0/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:55:31 +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
d8079e84e0 [3e-5] testSgReadySurplus : montée + hystérésis + court-cycling + budget partagé ECS↔PAC
Test simulation autonome (mock powerSwitch 2 relais, encodage 2 bits). 4 volets :
(1) montée 2→3→4 ; (2) hystérésis 3↔4 (zone morte P4×1,0–1,2, budget oscillant →
reste 4) ; (3) court-cycling (gelé sous minStateHold, bascule au-delà via temps simulé) ;
(4) budget PARTAGÉ ECS↔PAC : ordre priorité → service inverse (preuve waterfall unifié 3e).
Suite simulation 19/19, 0 régression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:43:02 +02:00
Patrick Schurig
b06ac15714 [3e-4] arbitre : registerSgReadyAdapter + dispatch State + mode dégradé → état 2
registerSgReadyAdapter + m_sgReadyAdapters ; buildContext inclut les PAC ;
applyActionsToAdapters dispatche kind==State → m_sgReadyAdapters. Mode dégradé L2 :
SG-Ready → état 2 (NORMAL, mains off, force=true), JAMAIS état 1 (blocage). SAFETY.md
table L2 corrigée (état 2, pas 1). Build 0/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:25:09 +02:00