diff --git a/energyplugin/energypluginnymea.cpp b/energyplugin/energypluginnymea.cpp index a6031de..b07e2c5 100644 --- a/energyplugin/energypluginnymea.cpp +++ b/energyplugin/energypluginnymea.cpp @@ -31,7 +31,6 @@ // [ETM] BEGIN — EnergyArbitrator flip. Remove block to revert to upstream SmartChargingManager. #ifdef ETM_ARBITRATOR #include "etm/energyarbitrator.h" -#include "etm/adapters/ecsrelayadapter.h" #include "etm/adapters/sgreadyadapter.h" #endif // [ETM] END @@ -55,20 +54,11 @@ void EnergyPluginNymea::init() qCDebug(dcNymeaEnergy()) << "ETM_ARBITRATOR actif — EnergyArbitrator chargé."; EnergyArbitrator *chargingManager = new EnergyArbitrator(energyManager(), thingManager(), spotMarketManager, configuration, this); - // §1.d — Banc hems : adaptateurs ECS 3 relais + SG-Ready PAC (ThingIds briefing terrain). + // §1.d — Banc hems : SG-Ready PAC (ThingIds briefing terrain). + // NB (T2) : l'ECS banc (ex-EcsRelayAdapter relay-stages) est retiré — l'ECS passe en + // etmvariableload (Setpoint W) et sera construit depuis LoadConfig en T4. La PAC reste à états. { ThingManager *tm = thingManager(); - const QString R500 = "{8538782f-2c8c-4a30-bfce-c8140f791c9b}"; - const QString R1000 = "{2ebe6bef-829b-4695-9d11-ddb4c16c5448}"; - const QString R2000 = "{b033b212-1adb-4df0-ba2b-8fa477de52a2}"; - auto *ecs = new EcsRelayAdapter( - tm, "ecs-terrain", "ECS banc", - QList({0, 500, 1000, 1500, 2000, 2500, 3000, 3500}), - QList>({ {}, {R500}, {R1000}, {R500,R1000}, - {R2000}, {R500,R2000}, {R1000,R2000}, {R500,R1000,R2000} }), - 60, 60, 1, chargingManager); - chargingManager->registerEcsAdapter(ecs); - const QString K1 = "{beaf92e1-aedc-4b84-9ce4-e423648638cc}"; const QString K2 = "{bf236e64-5ae7-4bf1-82a6-4ede03de75a6}"; auto *pac = new SgReadyAdapter( diff --git a/energyplugin/etm/adapters/ecsrelayadapter.cpp b/energyplugin/etm/adapters/ecsrelayadapter.cpp deleted file mode 100644 index d04f4cb..0000000 --- a/energyplugin/etm/adapters/ecsrelayadapter.cpp +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync - -#include "ecsrelayadapter.h" -#include "plugininfo.h" - -#include -#include -#include -#include -#include - -EcsRelayAdapter::EcsRelayAdapter(ThingManager *thingManager, - const QString &id, - const QString &label, - const QList &stages, - const QList> &relayMapping, - int minOnS, - int minOffS, - int priority, - QObject *parent) - : QObject(parent) - , m_thingManager(thingManager) - , m_id(id) - , m_label(label) - , m_stages(stages) - , m_relayMapping(relayMapping) - , m_minOnS(minOnS) - , m_minOffS(minOffS) - , m_priority(priority) -{ - Q_ASSERT(!m_stages.isEmpty() && m_stages.first() == 0); - Q_ASSERT(m_relayMapping.size() == m_stages.size()); -} - -LoadDescriptor EcsRelayAdapter::descriptor() const -{ - LoadDescriptor d; - d.id = m_id; - d.label = m_label; - d.adapter = QStringLiteral("relay-stages"); - d.priority = m_priority; - - d.declared.stages = m_stages; - d.limits.minOnS = m_minOnS; - d.limits.minOffS = m_minOffS; - - d.supportedKinds = { LoadAction::Stage }; - return d; -} - -LoadTelemetry EcsRelayAdapter::telemetry() const -{ - LoadTelemetry t; - t.available = true; - t.lastActionAt = m_lastActionAt; - - // Source de currentPowerW (consommée par le recrédit anti-clignotement du waterfall, - // RuleBasedScheduler::buildEcsStageAction — correction B) : - // - MESURÉE dès qu'au moins un relais du stage expose un state "currentPower" : - // somme des mesures. Un thermostat interne coupé (relais ON mais 0 W réel) renvoie - // alors 0 → recrédit 0, jamais de puissance fantôme. - // - DÉCLARÉE (repli) : stages[stage] UNIQUEMENT si aucun relais actif ne mesure - // (relais nus / mock sans powermetering). Approximation = palier à sa nominale. - double power = 0; - bool metered = false; - const QList &activeRelays = m_currentStage < m_relayMapping.size() - ? m_relayMapping.at(m_currentStage) - : QList(); - for (const QString &thingId : activeRelays) { - Thing *t2 = m_thingManager->findConfiguredThing(ThingId(thingId)); - if (!t2) - continue; - if (!t2->thingClass().stateTypes().findByName("currentPower").id().isNull()) { - metered = true; - power += t2->stateValue("currentPower").toDouble(); - } - } - // Repli déclaré : seulement si la mesure n'est pas disponible (pas si elle vaut 0). - if (!metered && m_currentStage > 0 && m_currentStage < m_stages.size()) - power = m_stages.at(m_currentStage); - - t.currentPowerW = power; - return t; -} - -LoadContext EcsRelayAdapter::toLoadContext(const QDateTime &now) const -{ - LoadContext ctx; - ctx.id = m_id; - ctx.adapter = QStringLiteral("relay-stages"); - ctx.label = m_label; - ctx.priority = m_priority; - ctx.declared = descriptor().declared; - ctx.limits = descriptor().limits; - - const LoadTelemetry tel = telemetry(); - ctx.telemetry.currentPowerW = tel.currentPowerW; - ctx.telemetry.stage = m_currentStage; - ctx.telemetry.lastSwitch = m_lastSwitch; - // Fenêtre de verrou évaluée au temps de cycle : le scheduler y borne son choix - // (plancher = puissance engagée non-coupable sous minOn ; plafond = redémarrage minOff). - lockWindow(now, ctx.telemetry.minStage, ctx.telemetry.maxStage); - return ctx; -} - -LoadAction EcsRelayAdapter::applyAction(const LoadAction &action, const QDateTime &now) -{ - if (action.kind != LoadAction::Stage) - return action; - - if (action.reason.isEmpty()) { - qCWarning(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label - << "— LoadAction sans reason rejetée."; - return action; - } - - const int newStage = qBound(0, action.stage, m_stages.size() - 1); - - if (newStage == m_currentStage) - return action; // Aucun changement → idempotent - - // Verrous anti-rebond évalués au temps de cycle (même fenêtre que le scheduler) — - // bypassés si force == true (L2 watchdog). - if (!action.force && lockActive(newStage, now)) { - qCDebug(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label - << "— verrou anti-rebond actif, stage" << newStage << "ignoré."; - return action; - } - - qCInfo(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label - << "→ stage" << newStage - << "(" << (m_currentStage < m_stages.size() ? m_stages.at(m_currentStage) : 0) << "W" - << "→" << m_stages.at(newStage) << "W)" - << "|" << action.reason; - - applyRelayStage(newStage); - - m_currentStage = newStage; - m_lastSwitch = now; // estampille au temps de cycle (cohérent avec la fenêtre de verrou) - m_lastActionAt = now; - - LoadAction applied = action; - applied.stage = newStage; - applied.estimatedPowerW = m_stages.at(newStage); - return applied; -} - -// ---- privé --------------------------------------------------------------- - -void EcsRelayAdapter::lockWindow(const QDateTime &now, int &minStage, int &maxStage) const -{ - const int topStage = m_stages.size() - 1; - const bool valid = m_lastSwitch.isValid(); - const qint64 elapsed = valid ? m_lastSwitch.secsTo(now) : 0; - - // Plancher : si ON et minOn non écoulé → interdit de descendre sous le palier courant - // (puissance engagée non-coupable, protection compresseur / anti court-cycling). - minStage = (m_currentStage > 0 && valid && elapsed < m_minOnS) ? m_currentStage : 0; - - // Plafond : si à l'arrêt et minOff non écoulé → interdit de redémarrer. - maxStage = (m_currentStage == 0 && valid && elapsed < m_minOffS) ? 0 : topStage; -} - -bool EcsRelayAdapter::lockActive(int newStage, const QDateTime &now) const -{ - // MÊME calcul que la fenêtre exposée au scheduler → décision et exécution coïncident. - int minStage, maxStage; - lockWindow(now, minStage, maxStage); - return newStage < minStage || newStage > maxStage; -} - -void EcsRelayAdapter::applyRelayStage(int stage) -{ - // Set de relais CIBLE du palier (delta complet : chaque relais connu est amené à son - // état cible on/off, pas d'ajout incrémental). Gère les mappings NON-CASCADÉS où monter - // d'un palier éteint des relais (ex. 3 résistances 500/1000/2000 W : 1500=[r500,r1000] - // → 2000=[r2000] commute 3 relais). - const QSet wantOn = [&]() { - QSet s; - if (stage < m_relayMapping.size()) - for (const QString &id : m_relayMapping.at(stage)) - s.insert(id); - return s; - }(); - - QSet allRelays; - for (const auto &list : m_relayMapping) - for (const QString &id : list) - allRelays.insert(id); - - auto writeRelay = [&](const QString &thingId, bool on) { - Thing *relay = m_thingManager->findConfiguredThing(ThingId(thingId)); - if (!relay) { - qCWarning(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label - << "— relais non trouvé:" << thingId; - return; - } - StateType powerStateType = relay->thingClass().stateTypes().findByName("power"); - if (!powerStateType.id().isNull()) { - Action powerAction(powerStateType.id(), relay->id(), Action::TriggeredByRule); - powerAction.setParams(ParamList() << Param(powerStateType.id(), on)); - m_thingManager->executeAction(powerAction); - } else { - relay->setStateValue("power", on); // repli mock - } - }; - - // Intention (résistif : transitoire inoffensif, mais portée comme SG-Ready) : COUPER - // d'abord les relais hors-cible, PUIS enclencher ceux de la cible → pas de sur-puissance - // transitoire (somme des deux paliers) sur une transition non-cascadée. - for (const QString &id : allRelays) - if (!wantOn.contains(id)) - writeRelay(id, false); - for (const QString &id : wantOn) - writeRelay(id, true); -} diff --git a/energyplugin/etm/adapters/ecsrelayadapter.h b/energyplugin/etm/adapters/ecsrelayadapter.h deleted file mode 100644 index e5d9610..0000000 --- a/energyplugin/etm/adapters/ecsrelayadapter.h +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync -#pragma once - -#include -#include -#include -#include -#include "iloadadapter.h" - -class Thing; -class ThingManager; - -/*! - * \brief Adaptateur pour chauffe-eau ou tout relais N paliers (interface relay-stages). - * - * Pilote en production N Things powerswitch nymea : \c m_relayMapping[stage] contient - * la liste des ThingIds à mettre ON pour ce palier (les autres sont mis OFF). - * - * Exemple — chauffe-eau 2400W, 2 résistances Waveshare : - * stage 0 : {} → A=OFF, B=OFF - * stage 1 : {"thingId-A"} → A=ON, B=OFF (1200 W) - * stage 2 : {"thingId-A", "thingId-B"} → A=ON, B=ON (2400 W) - * - * \invariant applyAction() rejette silencieusement toute action dont \c reason est vide. - * \invariant applyAction() applique les verrous anti-rebond \c minOnS / \c minOffS - * SAUF si \c action.force == true (réservé L2 watchdog). - * \invariant Le stage est écrêté à [0, stages().size()-1] avant envoi matériel. - * \invariant Seul le kind Stage est traité ; les autres kinds retournent sans effet. - */ -class EcsRelayAdapter : public QObject, public ILoadAdapter -{ - Q_OBJECT -public: - /*! - * \brief Constructeur. - * \param thingManager Gestionnaire nymea pour résoudre les ThingIds en Things. - * \param id Identifiant logique de la charge (ThingId de l'objet ECS dans nymea, - * ou identifiant arbitraire unique pour le mock). - * \param label Nom lisible affiché dans les logs et l'app. - * \param stages Puissances en W par palier, index 0 = off : [0, 1200, 2400]. - * \param relayMapping relayMapping[i] = liste de ThingIds powerswitch ON pour le palier i. - * \param minOnS Durée minimale ON (s) — anti-rebond. - * \param minOffS Durée minimale OFF (s) — anti-rebond. - * \param priority Rang dans le waterfall (OPTIMIZER_PROTOCOL §5) : valeur plus BASSE - * = servi en premier (rang 1 = premier servi). - * \param parent Propriétaire Qt. - */ - explicit EcsRelayAdapter(ThingManager *thingManager, - const QString &id, - const QString &label, - const QList &stages, - const QList> &relayMapping, - int minOnS, - int minOffS, - int priority, - QObject *parent = nullptr); - - /*! - * \brief Description statique de la charge. - * \return LoadDescriptor avec adapter="relay-stages", stages, minOnS/minOffS, priority. - */ - LoadDescriptor descriptor() const override; - - /*! - * \brief Télémétrie runtime : puissance mesurée, stage courant, lastSwitch. - * \return LoadTelemetry avec currentPowerW issu de la somme des Things actifs. - */ - LoadTelemetry telemetry() const override; - - /*! - * \brief Construit l'entrée loads[] §5 du SurplusContext. - * \param now Temps de cycle (\c ctx.timestamp) — source unique pour minStage/maxStage. - * \return LoadContext incluant declared, limits et télémétrie ECS (stage, currentPowerW, - * lastSwitch, et la fenêtre de verrou \c minStage/maxStage évaluée à \p now). - */ - LoadContext toLoadContext(const QDateTime &now) const override; - - /*! - * \brief Applique un changement de palier sur les relais. - * - * \param action LoadAction de kind Stage. Autres kinds : retour sans effet. - * \param now Temps de cycle (\c ctx.timestamp) — MÊME source que toLoadContext(), - * utilisée pour l'évaluation des verrous et l'estampille \c m_lastSwitch. - * \return L'action après écrêtage (stage borné à [0, stages.size()-1]). - * - * \invariant Si \c action.reason est vide → retour sans effet (log warning). - * \invariant Si verrous anti-rebond actifs ET \c action.force == false → retour sans effet (log). - * \invariant Si \c action.force == true → bypass verrous (L2 watchdog uniquement). - * \invariant Toute modification de stage met à jour \c m_lastSwitch (= \p now). - */ - LoadAction applyAction(const LoadAction &action, const QDateTime &now) override; - - /*! \brief Stage courant (0 = off). */ - int currentStage() const { return m_currentStage; } - -private: - /*! - * \brief Fenêtre de paliers autorisée à l'instant \p now par les verrous minOn/minOff. - * \param now Temps de cycle. - * \param[out] minStage Plancher : palier courant si minOn non écoulé (puissance engagée - * non-coupable), sinon 0. - * \param[out] maxStage Plafond : 0 si à l'arrêt et minOff non écoulé (redémarrage interdit), - * sinon le palier le plus haut. - */ - void lockWindow(const QDateTime &now, int &minStage, int &maxStage) const; - - bool lockActive(int newStage, const QDateTime &now) const; - void applyRelayStage(int stage); - - ThingManager *m_thingManager; - QString m_id; - QString m_label; - QList m_stages; //!< Puissances W par palier, [0]=off. - QList> m_relayMapping; //!< ThingIds ON par palier. - int m_minOnS; - int m_minOffS; - int m_priority; - - int m_currentStage = 0; - QDateTime m_lastSwitch; //!< Dernier changement de palier (null = jamais). - QDateTime m_lastActionAt; -}; diff --git a/energyplugin/etm/energyarbitrator.cpp b/energyplugin/etm/energyarbitrator.cpp index 5dd1460..f9b06d9 100644 --- a/energyplugin/etm/energyarbitrator.cpp +++ b/energyplugin/etm/energyarbitrator.cpp @@ -3,7 +3,6 @@ #include "energyarbitrator.h" #include "adapters/evadapter.h" -#include "adapters/ecsrelayadapter.h" #include "adapters/sgreadyadapter.h" #include "scheduler/rulebasedscheduler.h" #include "types/surpluscontext.h" @@ -85,18 +84,6 @@ RootMeter *EnergyArbitrator::registeredRootMeter() const return internalRootMeter(); } -void EnergyArbitrator::registerEcsAdapter(EcsRelayAdapter *adapter) -{ - const QString id = adapter->descriptor().id; - if (m_ecsAdapters.contains(id)) { - qCWarning(dcNymeaEnergy()) << "[EnergyArbitrator] EcsRelayAdapter déjà enregistré:" << id; - return; - } - adapter->setParent(this); - m_ecsAdapters[id] = adapter; - qCDebug(dcNymeaEnergy()) << "[EnergyArbitrator] EcsRelayAdapter enregistré:" << adapter->descriptor().label; -} - void EnergyArbitrator::registerSgReadyAdapter(SgReadyAdapter *adapter) { const QString id = adapter->descriptor().id; @@ -150,7 +137,7 @@ void EnergyArbitrator::update(const QDateTime ¤tDateTime) } // 7 : dispatch matériel (même position que l'amont — m_chargingActions rempli par getPlan()) - applyActionsToAdapters(slot, currentDateTime); // ECS (kind==Stage) → m_ecsAdapters + applyActionsToAdapters(slot, currentDateTime); // PAC (kind==State) → m_sgReadyAdapters adjustEvChargers(currentDateTime); // EV (kind==Setpoint) → proxy amont jusqu'à 3g } @@ -179,9 +166,7 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const for (auto it = m_adapters.constBegin(); it != m_adapters.constEnd(); ++it) ctx.loads.append(it.value()->toLoadContext(now)); - // --- loads[] : ECS relay adapters --- - for (auto it = m_ecsAdapters.constBegin(); it != m_ecsAdapters.constEnd(); ++it) - ctx.loads.append(it.value()->toLoadContext(now)); + // --- loads[] : etmvariableload adapters (ECS/routeur) → câblés en T4 (config) --- // --- loads[] : SG-Ready adapters (PAC) --- for (auto it = m_sgReadyAdapters.constBegin(); it != m_sgReadyAdapters.constEnd(); ++it) @@ -209,14 +194,7 @@ void EnergyArbitrator::applyActionsToAdapters(const Slot &slot, const QDateTime { for (const LoadAction &action : slot.actions) { // L'adaptateur applique, écrête et verrouille — il ne décide pas (règle 2). - if (action.kind == LoadAction::Stage) { - EcsRelayAdapter *adapter = m_ecsAdapters.value(action.loadId); - if (adapter) - adapter->applyAction(action, now); - else - qCWarning(dcNymeaEnergy()) << "[Arbitre] action Stage sans adaptateur ECS:" << action.loadId; - - } else if (action.kind == LoadAction::State) { + if (action.kind == LoadAction::State) { SgReadyAdapter *adapter = m_sgReadyAdapters.value(action.loadId); if (adapter) adapter->applyAction(action, now); @@ -224,6 +202,7 @@ void EnergyArbitrator::applyActionsToAdapters(const Slot &slot, const QDateTime qCWarning(dcNymeaEnergy()) << "[Arbitre] action State sans adaptateur SG-Ready:" << action.loadId; } // EV (Setpoint) : dispatché par adjustEvChargers() amont jusqu'à 3g. + // etmvariableload (Setpoint W) : dispatch via m_etmVariableLoadAdapters → câblé en T3/T4. } } @@ -267,16 +246,8 @@ void EnergyArbitrator::applyDegradedMode(const QDateTime &now) const QString reason = QStringLiteral("Compteur muet depuis >90 s — consigne de repli (L2 watchdog)"); - // ECS : tous paliers coupés (stage 0), force=true → bypass des verrous anti-rebond. - for (EcsRelayAdapter *adapter : m_ecsAdapters) { - LoadAction la; - la.loadId = adapter->descriptor().id; - la.kind = LoadAction::Stage; - la.stage = 0; - la.force = true; - la.reason = reason; - adapter->applyAction(la, now); // force=true → bypass verrous ; now = temps de cycle - } + // etmvariableload (ECS/routeur) : repli setPowerSetpoint(0) force=true (contrat rév. 2 §9) — + // câblé avec m_etmVariableLoadAdapters en T4 (config). En attendant, EV + PAC ci-dessous. // EV : repli CONSERVATEUR — n'initie aucune charge. On clampe seulement une charge // DÉJÀ en cours au courant minimum (force=true, bypass lock). Une borne branchée mais diff --git a/energyplugin/etm/energyarbitrator.h b/energyplugin/etm/energyarbitrator.h index e84903e..b68930b 100644 --- a/energyplugin/etm/energyarbitrator.h +++ b/energyplugin/etm/energyarbitrator.h @@ -12,7 +12,6 @@ class QTimer; class EvAdapter; -class EcsRelayAdapter; class SgReadyAdapter; class RuleBasedScheduler; @@ -76,15 +75,6 @@ public: */ RootMeter *registeredRootMeter() const; - /*! - * \brief Enregistre un EcsRelayAdapter pour inclusion dans le contexte et le dispatch. - * - * Appelé par le test (setup) ou la configuration de production. - * L'adaptateur est adopté comme enfant Qt de l'arbitre. - * \param adapter Adaptateur à enregistrer. Son \c descriptor().id doit être unique. - */ - void registerEcsAdapter(EcsRelayAdapter *adapter); - /*! * \brief Enregistre un SgReadyAdapter (PAC) pour inclusion dans le contexte et le dispatch. * \param adapter Adaptateur à enregistrer ; son \c descriptor().id doit être unique. @@ -133,7 +123,7 @@ protected: * 3. verifyOverloadProtection() + verifyOverloadProtectionRecovery() * (si \c m_degradedMode actif : retour immédiat — planification/dispatch suspendus, L2) * 4. m_scheduler->getPlan() → log des decisionReason - * 5. applyActionsToAdapters() (ECS Stage + SG-Ready State) + adjustEvChargers() (EV) → dispatch + * 5. applyActionsToAdapters() (SG-Ready State) + adjustEvChargers() (EV) → dispatch * * \param currentDateTime Instant courant (timer ou simulation). */ @@ -141,32 +131,32 @@ protected: private: /*! - * \brief Construit le SurplusContext §5 : meter brut + loads EV + ECS + SG-Ready. + * \brief Construit le SurplusContext §5 : meter brut + loads EV + SG-Ready. * * \c ctx.meter.exportW = mesure brute du compteur (AGENTS invariant 8 — aucune * déduction interne). La déduction evReservedW est faite dans le scheduler. * \param now Temps de cycle (\c ctx.timestamp) : pose \c ctx.timestamp et sert de SOURCE - * UNIQUE aux fenêtres de verrou (\c minStage/maxStage, \c minState/maxState) calculées - * par chaque adaptateur — cohérence décision (scheduler) / exécution (applyAction). + * UNIQUE à la fenêtre de verrou PAC (\c minState/maxState) calculée par le SgReadyAdapter — + * cohérence décision (scheduler) / exécution (applyAction). Les charges etmvariableload + * (ECS/routeur) n'ont pas de fenêtre côté moteur (verrous dans le thing). */ SurplusContext buildContext(const QDateTime &now) const; /*! * \brief Synchronise m_adapters avec les EvCharger actuellement enregistrés. * Crée les adapters manquants, supprime les adapters obsolètes. - * \note Découverte ECS via interface 'ecsrelay' ThingManager — déféré 3g config. - * En beta, les EcsRelayAdapters sont enregistrés via registerEcsAdapter(). + * \note Découverte etmvariableload/SG-Ready via config — câblé en T4. */ void syncAdapters(); /*! - * \brief Applique les actions d'un Slot aux LoadAdapters non-EV (ECS + SG-Ready). + * \brief Applique les actions d'un Slot aux LoadAdapters non-EV. * - * Itère \c slot.actions et dispatche selon le kind : \c Stage → \c m_ecsAdapters - * (EcsRelayAdapter) ; \c State → \c m_sgReadyAdapters (SgReadyAdapter). Les actions EV - * (\c Setpoint) restent dispatchées par \c adjustEvChargers() amont jusqu'à 3g. - * L'adaptateur écrête/verrouille lui-même (anti-rebond) et ignore toute action sans - * \c reason ou de kind non supporté — aucune décision ici (règle 2). + * Itère \c slot.actions et dispatche selon le kind : \c State → \c m_sgReadyAdapters + * (SgReadyAdapter, PAC). Les actions EV (\c Setpoint) restent dispatchées par + * \c adjustEvChargers() amont jusqu'à 3g ; les charges etmvariableload (\c Setpoint W) + * seront dispatchées via leur adaptateur en T3/T4. L'adaptateur écrête/verrouille lui-même + * et ignore toute action sans \c reason ou de kind non supporté — aucune décision ici (règle 2). * \param slot Créneau courant retourné par le scheduler. * \param now Temps de cycle (\c ctx.timestamp) — transmis aux adaptateurs pour une * évaluation des verrous cohérente avec celle vue par le scheduler. @@ -202,7 +192,6 @@ private: RuleBasedScheduler *m_scheduler = nullptr; QHash m_adapters; //!< loadId (ThingId string) → EvAdapter*. - QHash m_ecsAdapters; //!< loadId → EcsRelayAdapter*. QHash m_sgReadyAdapters; //!< loadId → SgReadyAdapter* (PAC). // --- L2 watchdog fraîcheur compteur (SAFETY.md §L2) --- diff --git a/energyplugin/etm/etm.pri b/energyplugin/etm/etm.pri index 4c157a0..6de4fec 100644 --- a/energyplugin/etm/etm.pri +++ b/energyplugin/etm/etm.pri @@ -6,7 +6,6 @@ HEADERS += \ $$PWD/adapters/iloadadapter.h \ $$PWD/scheduler/ischeduler.h \ $$PWD/adapters/evadapter.h \ - $$PWD/adapters/ecsrelayadapter.h \ $$PWD/adapters/sgreadyadapter.h \ $$PWD/adapters/etmvariableloadadapter.h \ $$PWD/scheduler/rulebasedscheduler.h \ @@ -14,7 +13,6 @@ HEADERS += \ SOURCES += \ $$PWD/adapters/evadapter.cpp \ - $$PWD/adapters/ecsrelayadapter.cpp \ $$PWD/adapters/sgreadyadapter.cpp \ $$PWD/adapters/etmvariableloadadapter.cpp \ $$PWD/scheduler/rulebasedscheduler.cpp \ diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.cpp b/energyplugin/etm/scheduler/rulebasedscheduler.cpp index cb56dc8..604bc83 100644 --- a/energyplugin/etm/scheduler/rulebasedscheduler.cpp +++ b/energyplugin/etm/scheduler/rulebasedscheduler.cpp @@ -96,23 +96,22 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx) // transitoire sous minOn est géré par le verrou de l'adaptateur, pas par le budget.) double remainingSurplusW = (ctx.meter.exportW - ctx.meter.importW) - evReservedW; - // Charges pilotables non-EV (ECS paliers + SG-Ready) triées par priorité ASCENDANTE : - // rang 1 = premier servi (OPTIMIZER_PROTOCOL §5 + annexe C — la priorité est un rang). - // Le budget de surplus est UNIQUE et cascade à travers TOUTES ces charges par priorité. + // Charges pilotables non-EV triées par priorité ASCENDANTE : rang 1 = premier servi + // (OPTIMIZER_PROTOCOL §5 + annexe C — la priorité est un rang). Le budget de surplus est + // UNIQUE et cascade à travers ces charges par priorité. + // + // T2 : seule la PAC SG-Ready (kind State) est traitée ici. Les charges etmvariableload + // (ECS/routeur, kind Setpoint W) rejoindront ce waterfall avec la règle d'arrondi en T3. QList nonEvLoads; for (const LoadContext &lc : ctx.loads) { - if (lc.adapter == QStringLiteral("relay-stages") || lc.adapter == QStringLiteral("sg-ready")) + if (lc.adapter == QStringLiteral("sg-ready")) nonEvLoads.append(lc); } std::sort(nonEvLoads.begin(), nonEvLoads.end(), [](const LoadContext &a, const LoadContext &b) { return a.priority < b.priority; }); - for (const LoadContext &lc : nonEvLoads) { - if (lc.adapter == QStringLiteral("sg-ready")) - slot.actions.append(buildSgReadyStateAction(lc, remainingSurplusW)); - else - slot.actions.append(buildEcsStageAction(lc, remainingSurplusW)); - } + for (const LoadContext &lc : nonEvLoads) + slot.actions.append(buildSgReadyStateAction(lc, remainingSurplusW)); // Grid funding (ECS/PAC) : dormant jusqu'à 3f (waterfall réseau) — non implémenté ici. @@ -171,58 +170,6 @@ LoadAction RuleBasedScheduler::buildIdleAction(EvCharger *ev) const return la; } -LoadAction RuleBasedScheduler::buildEcsStageAction(const LoadContext &lc, - double &remainingSurplusW) const -{ - // Correction B (anti-clignotement) : on recrédite la conso actuelle de l'ECS au budget. - // Sans ce recrédit, allumer un palier ferait chuter l'export mesuré → palier 0 → oscillation. - const double budgetChargeW = remainingSurplusW + lc.telemetry.currentPowerW; - - // Palier déclaré le plus haut qui tient dans le budget. stages[0] == 0 par construction - // (Q_ASSERT EcsRelayAdapter). - const QList &stages = lc.declared.stages; - const int topStage = stages.size() - 1; - int budgetStage = 0; - for (int i = 0; i < stages.size(); ++i) { - if (stages.at(i) <= budgetChargeW) - budgetStage = i; - } - - // Verrou (minOn/minOff) : le palier choisi est borné par la fenêtre déclarée par - // l'adaptateur (calculée au MÊME ctx.timestamp). Une charge verrouillée ON garde son - // palier (puissance engagée non-coupable) ; à l'arrêt sous minOff elle ne redémarre pas. - const int lo = qBound(0, lc.telemetry.minStage, topStage); - const int hi = qBound(lo, lc.telemetry.maxStage, topStage); - const int bestStage = qBound(lo, budgetStage, hi); - - LoadAction la; - la.loadId = lc.id; - la.kind = LoadAction::Stage; - la.funding = LoadAction::Surplus; - la.stage = bestStage; - la.estimatedPowerW = stages.at(bestStage); - - if (bestStage > budgetStage) - // Maintenu au-dessus du budget : protection compresseur (minOn non écoulé). - la.reason = QStringLiteral("Verrou minOn — %1 maintenu palier %2 (%3 W, surplus %4 W)") - .arg(lc.label).arg(bestStage).arg(stages.at(bestStage)).arg(qRound(budgetChargeW)); - else if (bestStage < budgetStage) - // Empêché de monter/redémarrer par minOff malgré le surplus. - la.reason = QStringLiteral("Verrou minOff — %1 maintenu palier %2 (redémarrage trop tôt)") - .arg(lc.label).arg(bestStage); - else if (bestStage > 0) - la.reason = QStringLiteral("Surplus PV %1 W — %2 palier %3 (%4 W)") - .arg(qRound(budgetChargeW)).arg(lc.label) - .arg(bestStage).arg(stages.at(bestStage)); - else - la.reason = QStringLiteral("Surplus insuffisant (%1 W) — %2 éteint") - .arg(qRound(budgetChargeW)).arg(lc.label); - - // Budget restant pour les charges ECS suivantes (rang supérieur / priorité plus basse). - remainingSurplusW = budgetChargeW - la.estimatedPowerW; - return la; -} - LoadAction RuleBasedScheduler::buildSgReadyStateAction(const LoadContext &lc, double &remainingSurplusW) const { diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.h b/energyplugin/etm/scheduler/rulebasedscheduler.h index 5a0f8c3..d304b3c 100644 --- a/energyplugin/etm/scheduler/rulebasedscheduler.h +++ b/energyplugin/etm/scheduler/rulebasedscheduler.h @@ -10,7 +10,7 @@ class ChargingAction; class EnergyArbitrator; /*! - * \brief Planificateur règles GPL : EV (proxy amont) + waterfall surplus ECS / SG-Ready. + * \brief Planificateur règles GPL : EV (proxy amont) + waterfall surplus SG-Ready (PAC). * * \c getPlan() produit un plan à 1 créneau en DEUX temps : * @@ -19,16 +19,17 @@ class EnergyArbitrator; * relit \c m_chargingActions et les reformate en LoadAction(Setpoint) annotées d'un * \c reason français. Le dispatch EV réel reste dans \c adjustEvChargers() amont. * - * 2. **Waterfall non-EV (3c/3e)** : un budget de surplus UNIQUE — net SIGNÉ + * 2. **Waterfall non-EV** : un budget de surplus UNIQUE — net SIGNÉ * \c (exportW − importW) − evReservedW — cascade par **priorité ASC** (rang, 1 = premier - * servi) à travers les charges \c relay-stages (ECS, \c buildEcsStageAction) ET - * \c sg-ready (PAC, \c buildSgReadyStateAction). Anti-clignotement par **recrédit** de la - * conso allouée ; **clamp lock-aware** \c minStage/maxStage et \c minState/maxState (verrous - * minOn/minOff/minStateHold, protection compresseur) — la fenêtre est calculée au MÊME - * \c ctx.timestamp que l'exécution (cf. \c ILoadAdapter). Ces LoadAction sont RÉELLEMENT - * dispatchées par \c EnergyArbitrator::applyActionsToAdapters(). + * servi). En T2, seule la PAC \c sg-ready (\c buildSgReadyStateAction, kind State) y est + * traitée : anti-clignotement par **recrédit** de la conso allouée, **clamp lock-aware** + * \c minState/maxState (verrou \c minStateHold, protection court-cycling compresseur) calculé + * au MÊME \c ctx.timestamp que l'exécution (cf. \c ILoadAdapter). Ces LoadAction sont + * RÉELLEMENT dispatchées par \c EnergyArbitrator::applyActionsToAdapters(). * - * À partir de **3g** : l'EV rejoindra le waterfall unifié (toutes charges classables ensemble). + * Les charges \c etmvariableload (ECS/routeur, kind Setpoint W) rejoindront ce waterfall avec + * la règle d'arrondi (contrat rév. 2 §5) en **T3**. À partir de **3g**, l'EV rejoint aussi le + * waterfall unifié (toutes charges classables ensemble). * * \invariant getPlan() retourne IMMÉDIATEMENT (AGENTS invariant 5) et toujours un Plan valide. * \invariant Toute LoadAction a un \c reason non vide, en français. @@ -51,8 +52,8 @@ public: * * Étape 1 (EV) : \c runSpotMarketPlanning() + \c runSurplusPlanning() puis reformatage * de \c scheduledActions() en LoadAction(Setpoint) — log [Arbitre], dispatch amont. - * Étape 2 (non-EV) : waterfall surplus sur les charges \c relay-stages / \c sg-ready - * triées par priorité — LoadAction(Stage/State) réellement dispatchées. + * Étape 2 (non-EV) : waterfall surplus sur la PAC \c sg-ready triée par priorité — + * LoadAction(State) réellement dispatchées (etmvariableload ECS/routeur arrive en T3). * * \param ctx SurplusContext courant. Utilisé : \c ctx.timestamp (temps de cycle / verrous), * \c ctx.meter (surplus net signé), \c ctx.loads (déclarés, télémétrie, fenêtres de verrou). @@ -83,21 +84,6 @@ private: */ LoadAction buildIdleAction(EvCharger *ev) const; - /*! - * \brief Construit un LoadAction "stage" ECS par cascade de surplus (waterfall §6). - * - * Retient le palier déclaré le plus haut dont la puissance tient dans le budget courant. - * Correction B (anti-clignotement) : recrédite d'abord \c lc.telemetry.currentPowerW au - * budget (la conso actuelle de l'ECS est déjà soustraite de l'export mesuré), puis - * décrémente \c remainingSurplusW de la puissance du palier retenu. - * - * \param lc Charge ECS (adapter == "relay-stages") du SurplusContext. - * \param[in,out] remainingSurplusW Budget de surplus restant (W) ; mis à jour pour la - * charge ECS suivante (priorité inférieure / rang supérieur). - * \return LoadAction kind=Stage, funding=Surplus, \c reason français non vide. - */ - LoadAction buildEcsStageAction(const LoadContext &lc, double &remainingSurplusW) const; - /*! * \brief Construit un LoadAction "state" SG-Ready (PAC) par mapping SÉMANTIQUE du surplus. * diff --git a/energyplugin/etm/types/loadaction.h b/energyplugin/etm/types/loadaction.h index 45207bc..f2c00e9 100644 --- a/energyplugin/etm/types/loadaction.h +++ b/energyplugin/etm/types/loadaction.h @@ -16,8 +16,10 @@ * \c currentA, \c phaseCount sont significatifs. */ struct LoadAction { - /*! \brief Type d'action : consigne continue, palier discret, état ou contrainte. */ - enum Kind { Setpoint, Stage, State, Constraint }; + /*! \brief Type d'action : consigne en watts (etmvariableload), état SG-Ready ou contrainte. + * \note Le kind \c Stage (index de palier ECS) a été RETIRÉ (contrat etmvariableload rév. 2 + * §6) : l'ECS est piloté en watts via \c Setpoint, l'index de palier vit dans le thing. */ + enum Kind { Setpoint, State, Constraint }; /*! \brief Financement interne : Surplus (PV) ou Grid (réseau). Non sérialisé. */ enum Funding { Surplus, Grid }; /*! \brief Source d'énergie pour Setpoint batterie : "solar" ou "grid". */ @@ -38,9 +40,6 @@ struct LoadAction { double powerW = 0; Source source = Solar; - // --- Stage relay-stages (ECS) --- - int stage = 0; //!< Index de palier (0 = off, 1 = 1er palier, ...). - // --- State sg-ready --- int state = 0; //!< État SG-Ready (1-4). diff --git a/energyplugin/etm/types/loaddescriptor.h b/energyplugin/etm/types/loaddescriptor.h index e3ffce6..e1c91d8 100644 --- a/energyplugin/etm/types/loaddescriptor.h +++ b/energyplugin/etm/types/loaddescriptor.h @@ -19,10 +19,6 @@ struct LoadDeclared { double maxA = 0; //!< Courant maximum (A). int phases = 0; //!< Nombre de phases disponibles (1 ou 3). - // --- relay-stages (ECS) --- - //! Puissances en W par palier : [0, 1200, 2400] — index 0 = off. - QList stages; - // --- etmvariableload (charge à puissance pilotable : EV / ECS résistif / routeur PV) --- //! Paliers atteignables en W (contrat etmvariableload §3), triés croissants, \c 0 inclus. //! Présent en mode *fixed*. Vide ⇒ modulation continue (*dynamic*, borne \c maxPowerW). @@ -86,7 +82,7 @@ struct LoadNeeds { struct LoadDescriptor { QString id; //!< ThingId de la charge (string). QString label; //!< Nom lisible (affiché dans les logs). - //! Type d'adaptateur : "evcharger"|"relay-stages"|"sg-ready"|"battery". + //! Type d'adaptateur : "evcharger"|"etmvariableload"|"sg-ready"|"battery". QString adapter; int priority = 0; LoadDeclared declared; diff --git a/energyplugin/etm/types/surpluscontext.h b/energyplugin/etm/types/surpluscontext.h index ef86960..0c12bad 100644 --- a/energyplugin/etm/types/surpluscontext.h +++ b/energyplugin/etm/types/surpluscontext.h @@ -59,13 +59,9 @@ struct LoadContextTelemetry { bool pluggedIn = false; bool charging = false; double sessionWh = 0; //!< Énergie chargée dans la session courante (Wh). - // --- relay-stages --- - int stage = 0; - //! Fenêtre de paliers autorisée MAINTENANT par les verrous (calculée au temps de cycle). - //! Le scheduler y borne son choix : minStage = plancher (verrou minOn, puissance - //! engagée non-coupable) ; maxStage = plafond (verrou minOff, redémarrage interdit). - int minStage = 0; - int maxStage = 0; + // --- etmvariableload (EV/ECS/routeur) : pas de fenêtre de verrou côté moteur — + // l'anti-rebond (minOn/minOff) vit dans le thing (contrat rév. 2 §3). Seul + // currentPowerW (ci-dessus) sert au résidu de la règle d'arrondi (contrat §5). // --- sg-ready --- int state = 0; //! Fenêtre d'états autorisée MAINTENANT par le verrou minStateHold (protection @@ -91,7 +87,7 @@ struct LoadLearned { */ struct LoadContext { QString id; - QString adapter; //!< "evcharger"|"relay-stages"|"sg-ready"|"battery". + QString adapter; //!< "evcharger"|"etmvariableload"|"sg-ready"|"battery". QString label; int priority = 0; LoadDeclared declared;