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>
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>
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>
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>
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>
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>
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>
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>
Adaptateur sg-ready (kind:State) : pilote N relais signal (stateRelays par état),
lockWindow symétrique (minStateHold, gel total — protection court-cycling), seam de
temps unifié (toLoadContext(now)/applyAction(now)). currentPowerW = puissance allouée
déclarée (pas mesurée → recrédit correct, anti double-comptage état 2).
Atomicité 2 bits : applyStateRelays commute d'abord le relais au transitoire le plus
doux (neutre/reco) puis les autres → jamais de blocage/forcé parasite. Contrat documenté
(transport déporté Shelly/Modbus). État initial = 2 (mains off). Build 0/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Test simulation autonome (arbitre frais via initTestCase) : 2 relais powerSwitch +
EcsRelayAdapter minOn=300. 4 régimes pilotés par le temps simulé :
cascade export 0→1→2 ; anti-clignotement (recrédit, hors verrou) ; import<minOn → RESTE
(protection compresseur) ; import>minOn → déleste. Seul le temps simulé change entre les
2 derniers → prouve le seam de temps unifié ET la protection.
Renommage ThingClass mockPowerSwitch→powerSwitch (collision symbole plugininfo vs
energytestbase dans le binaire simulation). Suite simulation : 17 passed, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Décision Patrick : délestage strict au budget, MAIS minOn/minOff (protection
compresseur, anti court-cycling) bornent le palier via l'adaptateur, pas le budget.
Paramètres par charge (config installateur, jamais hardcodé) + défauts indicatifs
par type (résistif / thermodynamique-PAC / SG-Ready). Note seam de temps unifié.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Itère slot.actions, dispatche les kind==Stage vers m_ecsAdapters (position 7,
avant adjustEvChargers). EV (Setpoint) reste sur le proxy amont jusqu'à 3g.
Build 0 erreur / 0 warning.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Corrections A (déduction EV unique) et B (anti-clignotement) intégrées.
Tri priorité ascendant (rang 1 = premier servi, OPTIMIZER_PROTOCOL §5/annexe C) —
corrige l'inversion du PLAN 3C et 3 doc-comments (plan.h, loaddescriptor.h,
ecsrelayadapter.h). Build 0 erreur / 0 warning.
telemetry() ECS : currentPowerW MESURÉE si au moins un relais expose "currentPower"
(thermostat coupé → 0, pas de fantôme), DÉCLARÉE en repli seulement sans comptage.
Dette evadapter.cpp priority=100 (ancienne convention) inscrite en 3g.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- tests/mocks/plugins/energymocks/integrationpluginenergymocks.json : maxChargingCurrent
"type": "uint" → "type": "double" sur les 3 Thing classes (charger, chargerPhaseSwitching,
simpleCharger) — nymea 1.15 a changé le type de l'interface evcharger.
- energyplugin/energyplugin.pri : INCLUDEPATH += $$PWD — rend energyplugin/ accessible
aux consommateurs du .pri (simulation, tests) qui compilent les sources ETM.
- energyplugin/energyplugin.pro : le toggle ETM_ARBITRATOR déplacé dans energyplugin.pri
(unique point de contrôle propagé à plugin ET simulation).
- tests/auto/simulation/experience/energyexperienceenergymock.cpp : même flip que
energypluginnymea.cpp — instancie EnergyArbitrator si ETM_ARBITRATOR est défini.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>