13 Commits

Author SHA1 Message Date
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
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
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
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
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
83d5ad9ed7 [3e-1] types SG-Ready : declared.states + estimatedPowerW(état) + telemetry min/maxState
LoadDeclared : states[1-4] + estimatedPowerW QHash<int,double> (déclaré installateur,
approximatif ; états 1/2 ≈ 0 pour l'allocation surplus, invariant 8).
LoadContextTelemetry : minState/maxState (fenêtre verrou minStateHold, parallèle à
minStage/maxStage ECS). Build 0 erreur / 0 warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:42:33 +02:00
Patrick Schurig
5d67dc943d [3c-3-fix] waterfall ECS : surplus net signé + clamp lock-aware (protection compresseur)
Bug : exportW clampé à max(0,-p) AVANT recrédit → sur-crédit en import (ECS
restait allumé sur le réseau, ne délestait jamais). Fix : surplus net SIGNÉ
(exportW - importW). Régime export inchangé.

Le délestage strict est borné par minOn/minOff (protection compresseur, pas confort) :
l'adaptateur expose minStage/maxStage (fenêtre de verrou évaluée au temps de cycle),
le scheduler clampe bestStage et décrémente au palier réel → budget correct pour les
charges suivantes (puissance verrouillée = engagée non-coupable).

Seam de temps unifié : now=ctx.timestamp partagé par toLoadContext()/applyAction() ;
lockWindow() est l'unique calcul, lockActive() en dérive (décision==exécution).
Interface ILoadAdapter étendue (now) + contrat "temps=paramètre, jamais l'horloge"
documenté pour les futurs adaptateurs. EvAdapter aligné. Build 0 erreur / 0 warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:25:22 +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
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
4ae1939f93 [3a] structs protocole + interfaces LoadAdapter/Scheduler (zéro comportement)
LoadAction (kind+funding+§6 fields), LoadDescriptor, SurplusContext (§5),
Plan/Slot, ILoadAdapter, IScheduler — noms de champs = OPTIMIZER_PROTOCOL.md.
energyplugin.pri inclut etm/etm.pri. Build Qt6 vert, aucun fichier upstream touché.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 22:02:18 +02:00