From b06ac1571479c3e99efba911b3deb48cb84b16f3 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Tue, 9 Jun 2026 23:25:09 +0200 Subject: [PATCH] =?UTF-8?q?[3e-4]=20arbitre=20:=20registerSgReadyAdapter?= =?UTF-8?q?=20+=20dispatch=20State=20+=20mode=20d=C3=A9grad=C3=A9=20?= =?UTF-8?q?=E2=86=92=20=C3=A9tat=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/SAFETY.md | 2 +- energyplugin/etm/energyarbitrator.cpp | 56 +++++++++++++++++++++------ energyplugin/etm/energyarbitrator.h | 15 +++++-- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/docs/SAFETY.md b/docs/SAFETY.md index a81841f..d6c7587 100644 --- a/docs/SAFETY.md +++ b/docs/SAFETY.md @@ -61,7 +61,7 @@ repli **conservatrice** : le repli n'INITIE rien, il borne ce qui tourne déjà. | EV branché mais **pas en charge** | Inchangé — reste off (off possiblement volontaire : HC/spot à venir) | | EV débranché | Aucune action | | ECS | Relais coupé (palier 0, `force=true`) | -| SG-Ready PAC | État 1 (normal) | +| SG-Ready PAC | État **2** (normal — mains off, la PAC chauffe selon son thermostat). JAMAIS état 1 (blocage) : bloquer une PAC sous compteur muet = maison qui ne chauffe plus sans raison visible. | | Batterie | Aucune charge réseau (surplus uniquement, plafonné à 0 si compteur muet) | « Maintenu » ≠ « démarré » : le mode dégradé ne force jamais l'activation d'une charge. diff --git a/energyplugin/etm/energyarbitrator.cpp b/energyplugin/etm/energyarbitrator.cpp index ba6d1cf..5dd1460 100644 --- a/energyplugin/etm/energyarbitrator.cpp +++ b/energyplugin/etm/energyarbitrator.cpp @@ -4,6 +4,7 @@ #include "energyarbitrator.h" #include "adapters/evadapter.h" #include "adapters/ecsrelayadapter.h" +#include "adapters/sgreadyadapter.h" #include "scheduler/rulebasedscheduler.h" #include "types/surpluscontext.h" #include "types/plan.h" @@ -96,6 +97,18 @@ void EnergyArbitrator::registerEcsAdapter(EcsRelayAdapter *adapter) qCDebug(dcNymeaEnergy()) << "[EnergyArbitrator] EcsRelayAdapter enregistré:" << adapter->descriptor().label; } +void EnergyArbitrator::registerSgReadyAdapter(SgReadyAdapter *adapter) +{ + const QString id = adapter->descriptor().id; + if (m_sgReadyAdapters.contains(id)) { + qCWarning(dcNymeaEnergy()) << "[EnergyArbitrator] SgReadyAdapter déjà enregistré:" << id; + return; + } + adapter->setParent(this); + m_sgReadyAdapters[id] = adapter; + qCDebug(dcNymeaEnergy()) << "[EnergyArbitrator] SgReadyAdapter enregistré:" << adapter->descriptor().label; +} + void EnergyArbitrator::update(const QDateTime ¤tDateTime) { qCDebug(dcNymeaEnergy()) << "Updating smart charging"; @@ -170,6 +183,10 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const for (auto it = m_ecsAdapters.constBegin(); it != m_ecsAdapters.constEnd(); ++it) ctx.loads.append(it.value()->toLoadContext(now)); + // --- loads[] : SG-Ready adapters (PAC) --- + for (auto it = m_sgReadyAdapters.constBegin(); it != m_sgReadyAdapters.constEnd(); ++it) + ctx.loads.append(it.value()->toLoadContext(now)); + return ctx; } @@ -191,17 +208,22 @@ void EnergyArbitrator::syncAdapters() void EnergyArbitrator::applyActionsToAdapters(const Slot &slot, const QDateTime &now) { for (const LoadAction &action : slot.actions) { - // EV (Setpoint) : dispatché par adjustEvChargers() amont jusqu'à 3g. - if (action.kind != LoadAction::Stage) - continue; + // 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; - EcsRelayAdapter *adapter = m_ecsAdapters.value(action.loadId); - if (!adapter) { - qCWarning(dcNymeaEnergy()) << "[Arbitre] action Stage sans adaptateur ECS:" << action.loadId; - continue; + } else if (action.kind == LoadAction::State) { + SgReadyAdapter *adapter = m_sgReadyAdapters.value(action.loadId); + if (adapter) + adapter->applyAction(action, now); + else + qCWarning(dcNymeaEnergy()) << "[Arbitre] action State sans adaptateur SG-Ready:" << action.loadId; } - // L'adaptateur applique, écrête et verrouille (anti-rebond) — il ne décide pas. - adapter->applyAction(action, now); + // EV (Setpoint) : dispatché par adjustEvChargers() amont jusqu'à 3g. } } @@ -266,6 +288,18 @@ void EnergyArbitrator::applyDegradedMode(const QDateTime &now) ev->setMaxChargingCurrent(ev->maxChargingCurrentMinValue(), now, true); } - // SG-Ready (état 1) / Batterie (aucune charge réseau) : repli ajouté avec leurs - // adaptateurs (3e/3f). Le flag degradedMode + notification client arrivent en 3c-6. + // SG-Ready (PAC) : repli en état 2 (NORMAL — mains off), JAMAIS état 1 (blocage). + // Sous compteur muet on cesse de piloter : la PAC chauffe selon son propre thermostat + // (la bloquer = maison qui ne chauffe plus sans raison visible). force=true → bypass minStateHold. + for (SgReadyAdapter *adapter : m_sgReadyAdapters) { + LoadAction la; + la.loadId = adapter->descriptor().id; + la.kind = LoadAction::State; + la.state = 2; + la.force = true; + la.reason = reason; + adapter->applyAction(la, now); + } + + // Batterie (aucune charge réseau) : repli ajouté avec son adaptateur (3f). } diff --git a/energyplugin/etm/energyarbitrator.h b/energyplugin/etm/energyarbitrator.h index da30745..fb7c48f 100644 --- a/energyplugin/etm/energyarbitrator.h +++ b/energyplugin/etm/energyarbitrator.h @@ -13,6 +13,7 @@ class QTimer; class EvAdapter; class EcsRelayAdapter; +class SgReadyAdapter; class RuleBasedScheduler; /*! @@ -84,6 +85,13 @@ public: */ 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. + * Adopté comme enfant Qt de l'arbitre. Appelé par le test (setup) ou la config production. + */ + void registerSgReadyAdapter(SgReadyAdapter *adapter); + /*! * \brief Mode dégradé L2 actif (compteur muet > 90 s) — override de SmartChargingManager. * \return \c true tant que les consignes de repli L2 tiennent ; \c false en régime normal. @@ -125,7 +133,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) + adjustEvChargers() (EV) → dispatch matériel + * 5. applyActionsToAdapters() (ECS Stage + SG-Ready State) + adjustEvChargers() (EV) → dispatch * * \param currentDateTime Instant courant (timer ou simulation). */ @@ -189,8 +197,9 @@ private: void applyDegradedMode(const QDateTime &now); RuleBasedScheduler *m_scheduler = nullptr; - QHash m_adapters; //!< loadId (ThingId string) → EvAdapter*. - QHash m_ecsAdapters; //!< loadId → EcsRelayAdapter*. + 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) --- QTimer *m_meterWatchdog = nullptr; //!< Tick 30 s, indépendant des signaux compteur.