From 7709057335f84bbd7353b73124a5dce85b76003c Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Mon, 8 Jun 2026 13:34:42 +0200 Subject: [PATCH] =?UTF-8?q?[wip]=203c=20morceaux=200-2=20compil=C3=A9s=20+?= =?UTF-8?q?=20plan=203c=20valid=C3=A9=20dans=20AGENTS.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- AGENTS.md | 89 +++++++- energyplugin/etm/adapters/ecsrelayadapter.cpp | 191 ++++++++++++++++++ energyplugin/etm/adapters/ecsrelayadapter.h | 108 ++++++++++ energyplugin/etm/energyarbitrator.cpp | 41 +++- energyplugin/etm/energyarbitrator.h | 23 ++- energyplugin/etm/etm.pri | 2 + energyplugin/etm/types/loadaction.h | 10 + 7 files changed, 453 insertions(+), 11 deletions(-) create mode 100644 energyplugin/etm/adapters/ecsrelayadapter.cpp create mode 100644 energyplugin/etm/adapters/ecsrelayadapter.h diff --git a/AGENTS.md b/AGENTS.md index 7411a5f..07d8b09 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,7 @@ vers un gestionnaire d'énergie complet (EV, ECS, PAC SG-Ready, batterie). | 2 — design arbitre validé | ✅ FAITE | `074fa71` | | 3a — structs protocole + interfaces | ✅ FAITE | `4ae1939` | | 3b — EnergyArbitrator + scheduler + adapter | ✅ FAITE — iso-fonctionnalité prouvée | `5f49e4c`, `d8ebd65`, `[3b-iv]` | +| 3c — EcsRelayAdapter + waterfall ECS | 🔄 EN COURS | (wip) | **Détail 3b** : - `EnergyArbitrator : public SmartChargingManager` — justification dans `## DÉCISIONS DE DESIGN` @@ -28,11 +29,17 @@ vers un gestionnaire d'énergie complet (EV, ECS, PAC SG-Ready, batterie). - Tests charging : 57 lignes décisions identiques, diff = 0 ; 46/46 PASS ref ET ETM - [Arbitre] présents avec raisons françaises pour les 4 cas (idle, surplus PV, aWATTar, deadline) -**PROCHAINE ACTION — 3c** : -- `EcsRelayAdapter` (paliers 0/1/2) — premier adaptateur non-EV, premier `applyAction()` vivant -- Pipeline ETM waterfall (budget surplus → ECS, déduction `addedPower` EV) dans `EnergyArbitrator::update()` -- Watchdog L2 : `QTimer` 30 s, mode dégradé variante B, notification `degradedMode` (cf. SAFETY.md §L2) -- Scénarios simulation : `testEcsSurplusPV` (chauffe-eau sur surplus PV) + `testMeterSilentFallback` (compteur muet → repli) +**Détail 3c (morceaux déjà compilés, 0 erreur / 0 warning)** : +- **Morceau 0** — `LoadAction.force=false` (bypass verrous sécurité) ✅ +- **Morceau 1** — `EcsRelayAdapter` (.h + .cpp) : pilote N Things powerswitch, + `applyRelayStage()`, verrous `minOnS/minOffS`, bypass si `force==true` ✅ + - Enregistrement explicite via `EnergyArbitrator::registerEcsAdapter()` (tests + config) +- **Morceau 2** — `buildContext()` : `SurplusMeter` brut (`exportW = max(0, -meter->currentPower())`), + `loads[]` EV + ECS, `SurpusPv` déféré 3d ✅ + +**PROCHAINE ACTION — suite 3c** : +- Morceau 3 : waterfall ECS dans `RuleBasedScheduler::getPlan()` (voir PLAN 3C ci-dessous) +- Morceaux 4-7 : voir PLAN 3C **Remotes git** : - `origin` (`https://git.etm-powersync.fr/...`) = remote de travail — push normal @@ -41,6 +48,78 @@ vers un gestionnaire d'énergie complet (EV, ECS, PAC SG-Ready, batterie). --- +## PLAN 3C (validé, morceaux 0-2 déjà compilés) + +Plan approuvé par Patrick. Corrections A (double déduction EV) et B (anti-clignotement ECS) +**intégrées** dans le design ci-dessous. + +### Morceaux déjà compilés (0 erreur/warning) + +| # | Fichier(s) | Ce qui a été fait | +|---|-----------|-------------------| +| 0 | `types/loadaction.h` | `bool force = false` — bypass verrous sécurité | +| 1 | `adapters/ecsrelayadapter.h/.cpp` | Adaptateur N-paliers powerswitch, anti-rebond, `applyRelayStage()`, `etm.pri` | +| 2 | `energyarbitrator.h/.cpp` | `buildContext()` : `SurplusMeter` brut, `loads[]` EV+ECS, `registerEcsAdapter()`, `m_ecsAdapters` | + +### Morceaux à venir + +**3 — Waterfall ECS dans `RuleBasedScheduler::getPlan()`** + +Après la boucle EV proxy, ajouter : + +``` +// Déduction unique (correction A) — ctx.meter.exportW = mesure brute +evReservedW = Σ EV en charge dans slot.actions : max(0, commandedA×phases×230 − ev->currentPower()) +remainingSurplusW = max(0, ctx.meter.exportW − evReservedW) + +// Tri loads ECS par priorité DESC (200 = servi en premier) +pour chaque LoadContext lc où lc.adapter == "relay-stages" : + budgetCharge = remainingSurplusW + lc.telemetry.currentPowerW // correction B anti-clignotement + bestStage = palier le plus haut dont stages[i] ≤ budgetCharge + reason = "Surplus PV — ECS palier N (W)" ou "Surplus insuffisant — ECS éteint" + remainingSurplusW = remainingSurplusW + lc.telemetry.currentPowerW − stages[bestStage] + // Grid funding : dormant jusqu'à 3f (commente, n'implémente pas) +``` + +**4 — `syncAdapters()` extension + `applyActionsToAdapters(Slot)` dans `update()`** +- `syncAdapters()` : commentaire "découverte ECS via interface 'ecsrelay' déférée 3g" +- `applyActionsToAdapters(Slot)` : itère slot.actions, dispatche via `m_ecsAdapters` pour kind==Stage + +**5 — Watchdog L2** (cf. SAFETY.md §L2) +- `QTimer m_meterWatchdog` 30 s — picoté sur `powerBalanceChanged` SIGNAL pour `m_lastMeterUpdate` +- `onMeterWatchdogTick()` : si `now − m_lastMeterUpdate > 90 s` → `applyDegradedMode()` +- `applyDegradedMode()` : ECS stage 0 force=true + EV courant minimum, reason="Compteur muet..." + +**6 — `degradedMode()` + notification + INTERFACE.md** +- `virtual bool degradedMode() const` dans `SmartChargingManager` (retourne false, `// [ETM]`) +- Override dans `EnergyArbitrator` +- Champ `"degradedMode": bool` dans `ChargingSchedulesChanged` (additif, rétro-compatible) +- Mise à jour `docs/INTERFACE.md` + limite dans `docs/SAFETY.md` : "valeur strictement constante non détectée" + +**7 — Mock powerswitch + tests** +- Mock JSON : ThingClass `mockPowerSwitch` (état `power` bool, état `currentPower` double) +- `energytestbase.h` : `mockPowerSwitchThingClassId` +- `testEcsSurplusPV` : cas normal + cas "ECS déjà au palier 1, surplus stable → Y RESTE" (anti-clignotement) +- `testMeterSilentFallback` : compteur muet 90 s → mode dégradé → ECS off + +### Arithmétique du budget (corrections A + B) + +**Correction A — déduction EV unique, dans le scheduler** : +``` +exportW dans ctx.meter = mesure brute (invariant 8, protocole §5) +evReservedW = déduction dans getPlan() APRÈS proxy EV, avec m_chargingActions fraîches +Pas de déduction dans buildContext(). +``` +Exemple : PV 9 kW, EV stable 7360 W, export mesuré 1140 W → evReservedW=0, budget ECS=1140 W ✓ + +**Correction B — anti-clignotement par recrédit de la conso actuelle** : +``` +budgetCharge = remainingSurplusW + lc.telemetry.currentPowerW +``` +Identique à SCM ~l.1245 pour l'EV. Sans ce recrédit : ECS palier 1 → export chute → palier 0 → oscillation. + +--- + > ⚠️ Tout plan antérieur mentionnant « créer etm/ avec PowerSyncClient et > StaticHcHpProvider comme première étape » ou « injecter l'optimiseur dans > SmartChargingManager » est **INVALIDE et ABANDONNÉ**. Ne pas le reprendre, diff --git a/energyplugin/etm/adapters/ecsrelayadapter.cpp b/energyplugin/etm/adapters/ecsrelayadapter.cpp new file mode 100644 index 0000000..089a4aa --- /dev/null +++ b/energyplugin/etm/adapters/ecsrelayadapter.cpp @@ -0,0 +1,191 @@ +// 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; + + // Puissance mesurée = somme des relais actifs pour le stage courant + double power = 0; + 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) + power += t2->stateValue("currentPower").toDouble(); + } + // Si pas de powermetering dans le mock, on estime depuis les stages déclarés + if (power == 0 && m_currentStage > 0 && m_currentStage < m_stages.size()) + power = m_stages.at(m_currentStage); + + t.currentPowerW = power; + return t; +} + +LoadContext EcsRelayAdapter::toLoadContext() 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; + return ctx; +} + +LoadAction EcsRelayAdapter::applyAction(const LoadAction &action) +{ + 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 — bypassés si force == true (L2 watchdog) + if (!action.force && lockActive(newStage)) { + qCDebug(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label + << "— verrou anti-rebond actif, stage" << newStage << "ignoré."; + return action; + } + + qCDebug(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 = QDateTime::currentDateTime(); + m_lastActionAt = m_lastSwitch; + + LoadAction applied = action; + applied.stage = newStage; + applied.estimatedPowerW = m_stages.at(newStage); + return applied; +} + +// ---- privé --------------------------------------------------------------- + +bool EcsRelayAdapter::lockActive(int newStage) const +{ + if (!m_lastSwitch.isValid()) + return false; + + const int elapsed = static_cast(m_lastSwitch.secsTo(QDateTime::currentDateTime())); + + if (newStage > m_currentStage) { + // Passage à un palier supérieur : minOffS si l'on quitte l'état OFF (stage 0→n) + // ou simplement le délai de stabilisation + if (m_currentStage == 0 && elapsed < m_minOffS) + return true; + } else { + // Réduction de palier : minOnS depuis le dernier ON + if (m_currentStage > 0 && elapsed < m_minOnS) + return true; + } + return false; +} + +void EcsRelayAdapter::applyRelayStage(int stage) +{ + // Ensemble des relais ON pour le nouveau stage + const QSet wantOn = [&]() { + QSet s; + if (stage < m_relayMapping.size()) + for (const QString &id : m_relayMapping.at(stage)) + s.insert(id); + return s; + }(); + + // Union de tous les relais connus + QSet allRelays; + for (const auto &list : m_relayMapping) + for (const QString &id : list) + allRelays.insert(id); + + for (const QString &thingId : allRelays) { + Thing *relay = m_thingManager->findConfiguredThing(ThingId(thingId)); + if (!relay) { + qCWarning(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label + << "— relais non trouvé:" << thingId; + continue; + } + const bool targetOn = wantOn.contains(thingId); + 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(), targetOn)); + m_thingManager->executeAction(powerAction); + } else { + // Fallback mock : setStateValue direct (Things sans actionType "power") + relay->setStateValue("power", targetOn); + } + } +} diff --git a/energyplugin/etm/adapters/ecsrelayadapter.h b/energyplugin/etm/adapters/ecsrelayadapter.h new file mode 100644 index 0000000..46510b8 --- /dev/null +++ b/energyplugin/etm/adapters/ecsrelayadapter.h @@ -0,0 +1,108 @@ +// 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 Priorité dans le waterfall (200=deadline, 100=normal, 0=différable). + * \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. + * \return LoadContext incluant declared, limits et télémétrie ECS (stage, currentPowerW, lastSwitch). + */ + LoadContext toLoadContext() const override; + + /*! + * \brief Applique un changement de palier sur les relais. + * + * \param action LoadAction de kind Stage. Autres kinds : retour sans effet. + * \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. + */ + LoadAction applyAction(const LoadAction &action) override; + + /*! \brief Stage courant (0 = off). */ + int currentStage() const { return m_currentStage; } + +private: + bool lockActive(int newStage) 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 857e1c5..c4ae348 100644 --- a/energyplugin/etm/energyarbitrator.cpp +++ b/energyplugin/etm/energyarbitrator.cpp @@ -3,9 +3,11 @@ #include "energyarbitrator.h" #include "adapters/evadapter.h" +#include "adapters/ecsrelayadapter.h" #include "scheduler/rulebasedscheduler.h" #include "types/surpluscontext.h" #include "types/plan.h" +#include "../rootmeter.h" #include "plugininfo.h" @@ -51,6 +53,18 @@ 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::update(const QDateTime ¤tDateTime) { qCDebug(dcNymeaEnergy()) << "Updating smart charging"; @@ -88,10 +102,33 @@ void EnergyArbitrator::update(const QDateTime ¤tDateTime) SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const { - // 3b stub — seul le timestamp est renseigné. - // Contexte §5 complet (site/meter/pv/battery/loads) prévu en 3d. SurplusContext ctx; ctx.timestamp = now; + + // --- Compteur principal (AGENTS invariant 8 : mesure brute, aucune déduction) --- + RootMeter *meter = internalRootMeter(); + if (meter) { + // currentPower() < 0 → export ; > 0 → import (convention amont SCM l.1141) + const double p = meter->currentPower(); + ctx.meter.importW = qMax(0.0, p); + ctx.meter.exportW = qMax(0.0, -p); + ctx.meter.perPhaseA = { + meter->currentPhaseA(), + meter->currentPhaseB(), + meter->currentPhaseC() + }; + } + // SurplusPv : interface inverter — déféré (remplissage prévu en 3d) + // SurplusBattery : déféré 3f + + // --- loads[] : EV adapters --- + for (auto it = m_adapters.constBegin(); it != m_adapters.constEnd(); ++it) + ctx.loads.append(it.value()->toLoadContext()); + + // --- loads[] : ECS relay adapters --- + for (auto it = m_ecsAdapters.constBegin(); it != m_ecsAdapters.constEnd(); ++it) + ctx.loads.append(it.value()->toLoadContext()); + return ctx; } diff --git a/energyplugin/etm/energyarbitrator.h b/energyplugin/etm/energyarbitrator.h index 6455cfe..27dac63 100644 --- a/energyplugin/etm/energyarbitrator.h +++ b/energyplugin/etm/energyarbitrator.h @@ -8,6 +8,7 @@ #include "types/plan.h" class EvAdapter; +class EcsRelayAdapter; class RuleBasedScheduler; /*! @@ -70,6 +71,15 @@ 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); + protected: /*! * \brief Boucle principale ETM — surcharge SmartChargingManager::update(). @@ -87,17 +97,22 @@ protected: private: /*! - * \brief Construit un SurplusContext minimal (3b stub — timestamp seul). - * \note Contexte complet prévu en 3d (§5 complet avec site/meter/pv/battery/loads). + * \brief Construit le SurplusContext §5 : meter brut + loads EV + loads ECS. + * + * \c ctx.meter.exportW = mesure brute du compteur (AGENTS invariant 8 — aucune + * déduction interne). La déduction evReservedW est faite dans le scheduler. */ 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(). */ void syncAdapters(); - RuleBasedScheduler *m_scheduler = nullptr; - QHash m_adapters; //!< loadId (ThingId string) → EvAdapter*. + RuleBasedScheduler *m_scheduler = nullptr; + QHash m_adapters; //!< loadId (ThingId string) → EvAdapter*. + QHash m_ecsAdapters; //!< loadId → EcsRelayAdapter*. }; diff --git a/energyplugin/etm/etm.pri b/energyplugin/etm/etm.pri index b49f3cc..62b60c4 100644 --- a/energyplugin/etm/etm.pri +++ b/energyplugin/etm/etm.pri @@ -6,10 +6,12 @@ HEADERS += \ $$PWD/adapters/iloadadapter.h \ $$PWD/scheduler/ischeduler.h \ $$PWD/adapters/evadapter.h \ + $$PWD/adapters/ecsrelayadapter.h \ $$PWD/scheduler/rulebasedscheduler.h \ $$PWD/energyarbitrator.h \ SOURCES += \ $$PWD/adapters/evadapter.cpp \ + $$PWD/adapters/ecsrelayadapter.cpp \ $$PWD/scheduler/rulebasedscheduler.cpp \ $$PWD/energyarbitrator.cpp \ diff --git a/energyplugin/etm/types/loadaction.h b/energyplugin/etm/types/loadaction.h index b90efb9..45207bc 100644 --- a/energyplugin/etm/types/loadaction.h +++ b/energyplugin/etm/types/loadaction.h @@ -59,4 +59,14 @@ struct LoadAction { * Rempli par le scheduler ; peut être 0 si inconnu. */ double estimatedPowerW = 0; + + /*! + * \brief Forçage sécurité — bypasse les verrous anti-rebond (minOn/minOff). + * + * Positionné à \c true uniquement par \c applyDegradedMode() (L2 watchdog) + * et les contraintes de sécurité. Les adaptateurs doivent appliquer l'action + * immédiatement sans vérifier les verrous temporels. + * \warning Réservé à la sécurité. Ne jamais mettre à \c true dans un scheduler. + */ + bool force = false; };