From 5f49e4ca3c3aab064d7d341663200046975cd012 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sun, 7 Jun 2026 23:16:49 +0200 Subject: [PATCH] =?UTF-8?q?[3b-wip]=20EnergyArbitrator=20+=20RuleBasedSche?= =?UTF-8?q?duler=20+=20EvAdapter=20(dispatch=20amont,=20ETM=5FARBITRATOR?= =?UTF-8?q?=20d=C3=A9sactiv=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .clangd | 20 +++ .gitignore | 3 + AGENTS.md | 37 +++++ energyplugin/energyplugin.pro | 4 + energyplugin/energypluginnymea.cpp | 14 +- energyplugin/etm/adapters/evadapter.cpp | 94 ++++++++++++ energyplugin/etm/adapters/evadapter.h | 72 ++++++++++ energyplugin/etm/adapters/iloadadapter.h | 55 +++++-- energyplugin/etm/energyarbitrator.cpp | 104 ++++++++++++++ energyplugin/etm/energyarbitrator.h | 103 ++++++++++++++ energyplugin/etm/etm.pri | 8 ++ energyplugin/etm/scheduler/ischeduler.h | 24 +++- .../etm/scheduler/rulebasedscheduler.cpp | 134 ++++++++++++++++++ .../etm/scheduler/rulebasedscheduler.h | 71 ++++++++++ energyplugin/etm/types/loadaction.h | 60 +++++--- energyplugin/etm/types/loaddescriptor.h | 88 +++++++----- energyplugin/etm/types/plan.h | 36 +++-- energyplugin/etm/types/surpluscontext.h | 63 ++++---- energyplugin/smartchargingmanager.h | 39 ++--- 19 files changed, 910 insertions(+), 119 deletions(-) create mode 100644 .clangd create mode 100644 energyplugin/etm/adapters/evadapter.cpp create mode 100644 energyplugin/etm/adapters/evadapter.h create mode 100644 energyplugin/etm/energyarbitrator.cpp create mode 100644 energyplugin/etm/energyarbitrator.h create mode 100644 energyplugin/etm/scheduler/rulebasedscheduler.cpp create mode 100644 energyplugin/etm/scheduler/rulebasedscheduler.h diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..5c5dc11 --- /dev/null +++ b/.clangd @@ -0,0 +1,20 @@ +# Flags de repli pour les headers ouverts hors contexte compile_commands.json +# (notamment quand le repo est ouvert via le symlink ~/Schreibtisch/ alors que +# compile_commands.json contient les chemins réels ~/projects/). +# Ces flags s'appliquent en fallback ; les entrées compile_commands.json ont priorité. +CompileFlags: + Add: + - -std=c++17 + - -Ienergyplugin + - -I/usr/include/nymea + - -I/usr/include/nymea-energy + - -I/usr/include/x86_64-linux-gnu/qt6 + - -I/usr/include/x86_64-linux-gnu/qt6/QtCore + - -I/usr/include/x86_64-linux-gnu/qt6/QtGui + - -I/usr/include/x86_64-linux-gnu/qt6/QtNetwork + - -I/usr/lib/x86_64-linux-gnu/qt6/mkspecs/linux-g++ + - -DQT_CORE_LIB + - -DQT_NETWORK_LIB + - -DQT_GUI_LIB + - -DQT_PLUGIN + - -D_REENTRANT diff --git a/.gitignore b/.gitignore index b607d8c..fd0f909 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ Makefile builddir/ *_moc.cpp autogenerated/ + +# clangd — chemins absolus du poste local, ne pas versionner +compile_commands.json diff --git a/AGENTS.md b/AGENTS.md index 5f9e321..f81d2cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -100,6 +100,39 @@ avant validation du design de la phase. 3f. BatteryAdapter (constraints + charge réseau plafonnée). - **Bugs upstream** : au fil de l'eau, commits `[upstream-fix]` séparés. +## DÉCISIONS DE DESIGN (écarts et justifications) + +### 3b-iii — EnergyArbitrator hérite de SmartChargingManager + +**Design validé en session** : "nouvelle classe dans etm/, n'étend pas SmartChargingManager". + +**Écart implémenté** : `EnergyArbitrator : public SmartChargingManager`. + +**Justification** : + +1. **Contrainte NymeaEnergyJsonHandler** : ce handler amont prend un + `SmartChargingManager*` dans son constructeur. + Sans héritage, toute solution propre (interface commune, pointeur générique) + nécessiterait de modifier `nymeaenergyjsonhandler.h/.cpp` — violation de la règle + "Modifier le code amont uniquement pour corriger des bugs". + +2. **verifyOverloadProtection() intacte** : héritée bit-pour-bit, connectée aux mêmes + signaux via le constructeur du parent. Zéro risque de régression sur la sécurité. + +3. **simulationCallUpdate() polymorphe** : appelle `update()` virtuel → redirige + automatiquement vers `EnergyArbitrator::update()`. Les tests amont passent sans + modification. + +4. **Minimal upstream diff** : seuls les attributs `protected`/`virtual` changent dans + `smartchargingmanager.h` (marqués `// [ETM]`). Zéro logique upstream modifiée. + +**Risque accepté** : `EnergyArbitrator` a accès à l'état privé de SCM via les +accesseurs `internal*`. La discipline AGENTS (LoadAdapters exécutent, ne décident pas ; +un seul arbitre) compense. Si SCM était refactorisé en amont pour exposer une interface +publique propre, l'héritage pourrait être remplacé par composition. + +--- + ## DÉFINITION DE FAIT (par étape de phase 3) 1. Compile amd64 et cross arm64. @@ -107,6 +140,10 @@ avant validation du design de la phase. `docker-simulation.sh` + `tests/auto` hérités sont le banc de test). 3. `decisionReason` visibles dans les logs de simulation. 4. Aucune régression des tests amont existants. +5. Toute classe/méthode **publique** de `etm/` porte un commentaire Doxygen : + `\brief`, `\param`, `\return`, et surtout le **contrat de comportement** + (invariants, écrêtage, hypothèses que l'appelant peut faire). + Les headers 3a servent de modèle — les convertir au format Doxygen lors du passage 3b. ## RÉFÉRENCES diff --git a/energyplugin/energyplugin.pro b/energyplugin/energyplugin.pro index 375880d..0f5c03d 100644 --- a/energyplugin/energyplugin.pro +++ b/energyplugin/energyplugin.pro @@ -7,6 +7,10 @@ PKGCONFIG += nymea nymea-energy QT *= network +# [ETM] Activate ETM arbitrator — replaces SmartChargingManager::update() with EnergyArbitrator. +# Uncomment to enable. Committed DISABLED until iso-functional proof (3b-iv). +# DEFINES += ETM_ARBITRATOR + include(energyplugin.pri) HEADERS += \ diff --git a/energyplugin/energypluginnymea.cpp b/energyplugin/energypluginnymea.cpp index b8f3133..6a31bc5 100644 --- a/energyplugin/energypluginnymea.cpp +++ b/energyplugin/energypluginnymea.cpp @@ -28,6 +28,12 @@ #include "energymanagerconfiguration.h" #include "spotmarket/spotmarketmanager.h" +// [ETM] BEGIN — EnergyArbitrator flip. Remove block to revert to upstream SmartChargingManager. +#ifdef ETM_ARBITRATOR +#include "etm/energyarbitrator.h" +#endif +// [ETM] END + #include "plugininfo.h" EnergyPluginNymea::EnergyPluginNymea(QObject *parent) : EnergyPlugin(parent) @@ -41,8 +47,14 @@ void EnergyPluginNymea::init() EnergyManagerConfiguration *configuration = new EnergyManagerConfiguration(this); QNetworkAccessManager *networkManager = new QNetworkAccessManager(this); - SpotMarketManager *spotMarketManager = new SpotMarketManager(networkManager, this); + +#ifdef ETM_ARBITRATOR + qCDebug(dcNymeaEnergy()) << "ETM_ARBITRATOR actif — EnergyArbitrator chargé."; + EnergyArbitrator *chargingManager = new EnergyArbitrator(energyManager(), thingManager(), spotMarketManager, configuration, this); +#else SmartChargingManager *chargingManager = new SmartChargingManager(energyManager(), thingManager(), spotMarketManager, configuration, this); +#endif + jsonRpcServer()->registerExperienceHandler(new NymeaEnergyJsonHandler(spotMarketManager, chargingManager, this), 0, 8); } diff --git a/energyplugin/etm/adapters/evadapter.cpp b/energyplugin/etm/adapters/evadapter.cpp new file mode 100644 index 0000000..1cde634 --- /dev/null +++ b/energyplugin/etm/adapters/evadapter.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync + +#include "evadapter.h" +#include "../energyarbitrator.h" +#include "../../evcharger.h" +#include "../../types/chargingaction.h" + +#include "plugininfo.h" + +EvAdapter::EvAdapter(EvCharger *evCharger, EnergyArbitrator *parent) + : QObject(parent) + , m_charger(evCharger) + , m_parent(parent) +{ +} + +LoadDescriptor EvAdapter::descriptor() const +{ + LoadDescriptor d; + d.id = m_charger->thing()->id().toString(); + d.label = m_charger->name(); + d.adapter = QStringLiteral("evcharger"); + d.priority = 100; + + d.declared.minA = m_charger->maxChargingCurrentMinValue(); + d.declared.maxA = m_charger->maxChargingCurrentMaxValue(); + d.declared.phases = static_cast(m_charger->phaseCount()); + + d.limits.chargingEnabledLockS = static_cast(m_charger->chargingEnabledLockDuration()); + d.limits.currentChangeLockS = static_cast(m_charger->chargingCurrentLockDuration()); + + d.supportedKinds = { LoadAction::Setpoint }; + return d; +} + +LoadTelemetry EvAdapter::telemetry() const +{ + LoadTelemetry t; + t.currentPowerW = m_charger->currentPower(); + t.available = m_charger->available(); + t.lastActionAt = m_lastActionAt; + return t; +} + +LoadContext EvAdapter::toLoadContext() const +{ + LoadContext ctx; + ctx.id = m_charger->thing()->id().toString(); + ctx.adapter = QStringLiteral("evcharger"); + ctx.label = m_charger->name(); + ctx.declared = descriptor().declared; + ctx.limits = descriptor().limits; + + ctx.telemetry.currentPowerW = m_charger->currentPower(); + ctx.telemetry.pluggedIn = m_charger->pluggedIn(); + ctx.telemetry.charging = m_charger->charging(); + return ctx; +} + +LoadAction EvAdapter::applyAction(const LoadAction &action) +{ + if (action.kind != LoadAction::Setpoint) + return action; + + if (action.reason.isEmpty()) { + qCWarning(dcNymeaEnergy()) << "[EvAdapter]" << m_charger->name() + << "— LoadAction sans reason rejetée."; + return action; + } + + const uint minA = m_charger->maxChargingCurrentMinValue(); + const uint maxA = m_charger->maxChargingCurrentMaxValue(); + const uint clampedA = static_cast( + qBound(static_cast(minA), action.currentA, static_cast(maxA))); + + const uint phases = (m_charger->canSetPhaseCount() && action.phaseCount > 0) + ? qBound(1u, action.phaseCount, m_charger->phaseCount()) + : m_charger->phaseCount(); + + const auto issuer = (action.funding == LoadAction::Surplus) + ? ChargingAction::ChargingActionIssuerSurplusCharging + : ChargingAction::ChargingActionIssuerTimeRequirement; + + ChargingAction ca(action.chargingEnabled, clampedA, phases, issuer, false); + const QDateTime now = QDateTime::currentDateTimeUtc(); + m_parent->doExecuteChargingAction(m_charger, ca, now); + m_lastActionAt = now; + + LoadAction applied = action; + applied.currentA = clampedA; + applied.phaseCount = phases; + return applied; +} diff --git a/energyplugin/etm/adapters/evadapter.h b/energyplugin/etm/adapters/evadapter.h new file mode 100644 index 0000000..ffefd09 --- /dev/null +++ b/energyplugin/etm/adapters/evadapter.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync +#pragma once + +#include +#include +#include "iloadadapter.h" + +class EvCharger; +class EnergyArbitrator; + +/*! + * \brief Adaptateur pour une borne de recharge VE (interface evcharger nymea). + * + * Traduit les LoadAction(Setpoint) en appels matériels via + * EnergyArbitrator::doExecuteChargingAction() — le seul chemin d'exécution. + * + * \invariant applyAction() rejette silencieusement toute LoadAction dont \c reason est vide. + * \invariant currentA est écrêté à [maxChargingCurrentMinValue, maxChargingCurrentMaxValue]. + * \invariant phaseCount est écrêté selon canSetPhaseCount() et phaseCount() du EvCharger. + * \invariant Les kinds autres que Setpoint sont retournés sans effet. + */ +class EvAdapter : public QObject, public ILoadAdapter +{ + Q_OBJECT +public: + /*! + * \brief Constructeur. + * \param evCharger Borne VE à piloter (doit rester valide tant que l'adaptateur existe). + * \param parent EnergyArbitrator propriétaire — utilisé pour l'exécution matérielle. + */ + explicit EvAdapter(EvCharger *evCharger, EnergyArbitrator *parent); + + /*! + * \brief Retourne la description statique de la charge. + * \return LoadDescriptor construit depuis les capacités actuelles du EvCharger. + * \note Recalculé à chaque appel depuis l'état nymea du Thing. + */ + LoadDescriptor descriptor() const override; + + /*! + * \brief Retourne la télémétrie runtime (puissance mesurée, disponibilité). + * \return LoadTelemetry avec currentPowerW, available et lastActionAt. + */ + LoadTelemetry telemetry() const override; + + /*! + * \brief Construit l'entrée loads[] §5 du SurplusContext. + * \return LoadContext incluant declared, limits, needs et télémétrie EV. + */ + LoadContext toLoadContext() const override; + + /*! + * \brief Applique une consigne Setpoint sur la borne VE. + * + * \param action LoadAction de kind Setpoint. Autres kinds : retour sans effet. + * \return L'action après écrêtage matériel (currentA, phaseCount bornés). + * + * \invariant action.reason non vide requis — log warning et retour sans effet sinon. + * \invariant currentA écrêté à [minValue, maxValue] avant envoi à executeChargingAction. + * \invariant phaseCount ajusté selon canSetPhaseCount() du EvCharger. + */ + LoadAction applyAction(const LoadAction &action) override; + + /*! \brief Borne VE sous-jacente (lecture). */ + EvCharger *evCharger() const { return m_charger; } + +private: + EvCharger *m_charger; + EnergyArbitrator *m_parent; + QDateTime m_lastActionAt; +}; diff --git a/energyplugin/etm/adapters/iloadadapter.h b/energyplugin/etm/adapters/iloadadapter.h index 5aa1cac..3a4bb25 100644 --- a/energyplugin/etm/adapters/iloadadapter.h +++ b/energyplugin/etm/adapters/iloadadapter.h @@ -7,31 +7,58 @@ #include "../types/loaddescriptor.h" #include "../types/surpluscontext.h" -// Vue runtime minimale qu'un adaptateur expose à l'arbitre. +/*! + * \brief Vue runtime minimale exposée par un adaptateur à l'arbitre. + */ struct LoadTelemetry { - double currentPowerW = 0; - bool available = true; // faux si l'appareil nymea est absent/erreur - QDateTime lastActionAt; + double currentPowerW = 0; //!< Puissance mesurée (W). + bool available = true; //!< Faux si l'appareil nymea est absent ou en erreur. + QDateTime lastActionAt; //!< Dernier instant où applyAction() a produit un effet. }; -// Interface pure — les implémentations concrètes héritent de QObject + ILoadAdapter. -// Signaux (telemetryChanged, descriptorChanged) déclarés dans les classes concrètes. +/*! + * \brief Interface pure des adaptateurs de charge. + * + * Les implémentations concrètes héritent de QObject + ILoadAdapter et déclarent leurs + * propres signaux (telemetryChanged, descriptorChanged). + * + * \invariant Les adaptateurs EXÉCUTENT, ils ne décident pas (AGENTS règle 2). + * \invariant applyAction() écrête les valeurs selon les limites matérielles réelles + * (second filet après l'écrêtage de l'arbitre). + * \invariant applyAction() avec \c reason vide doit être rejetée silencieusement. + * \invariant Les méthodes non-applyAction() retournent immédiatement (pas de I/O bloquant). + */ class ILoadAdapter { public: virtual ~ILoadAdapter() = default; - // Déclaration statique : capacités, limites, priorité, needs. - virtual LoadDescriptor descriptor() const = 0; + /*! + * \brief Description statique de la charge : capacités, limites, priorité, needs. + * \return LoadDescriptor construit depuis la configuration matérielle. + * \note Peut être rappelé à chaque cycle — l'implémentation doit être légère. + */ + virtual LoadDescriptor descriptor() const = 0; - // Télémétrie runtime (courant, disponibilité, dernière action). + /*! + * \brief Télémétrie runtime (puissance, disponibilité, dernière action). + * \return LoadTelemetry issue de l'état courant de l'appareil nymea. + */ virtual LoadTelemetry telemetry() const = 0; - // Construit l'entrée §5 loads[] pour SurplusContext. - // Inclut declared, telemetry type-spécifique, learned courant. + /*! + * \brief Construit l'entrée §5 loads[] pour le SurplusContext. + * \return LoadContext incluant declared, limits, needs et télémétrie type-spécifique. + */ virtual LoadContext toLoadContext() const = 0; - // Applique l'action. Retourne ce qui a réellement été appliqué - // (après écrêtage matériel). L'arbitre a déjà écrêté selon les limites - // et le budget — c'est le second filet. + /*! + * \brief Applique l'action et retourne ce qui a réellement été envoyé au matériel. + * + * L'arbitre a déjà écrêté selon les limites et le budget — ceci est le second filet. + * + * \param action Action à appliquer. Doit avoir \c reason non vide. + * \return L'action après écrêtage matériel (peut différer de l'entrée). + * \note Retour silencieux sans effet si \c action.reason est vide. + */ virtual LoadAction applyAction(const LoadAction &action) = 0; }; diff --git a/energyplugin/etm/energyarbitrator.cpp b/energyplugin/etm/energyarbitrator.cpp new file mode 100644 index 0000000..ba1a2e3 --- /dev/null +++ b/energyplugin/etm/energyarbitrator.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync + +#include "energyarbitrator.h" +#include "adapters/evadapter.h" +#include "scheduler/rulebasedscheduler.h" +#include "types/surpluscontext.h" +#include "types/plan.h" + +#include "plugininfo.h" + +EnergyArbitrator::EnergyArbitrator(EnergyManager *em, ThingManager *tm, + SpotMarketManager *sm, + EnergyManagerConfiguration *conf, + QObject *parent) + : SmartChargingManager(em, tm, sm, conf, parent) + , m_scheduler(new RuleBasedScheduler(this, this)) +{ + qCDebug(dcNymeaEnergy()) << "[EnergyArbitrator] Arbitre ETM initialisé."; +} + +void EnergyArbitrator::runSurplusPlanning(const QDateTime &now) +{ + planSurplusCharging(now); +} + +void EnergyArbitrator::runSpotMarketPlanning(const QDateTime &now) +{ + planSpotMarketCharging(now); +} + +const QHash &EnergyArbitrator::scheduledActions() const +{ + return internalChargingActions(); +} + +void EnergyArbitrator::doExecuteChargingAction(EvCharger *charger, + const ChargingAction &action, + const QDateTime &now) +{ + executeChargingAction(charger, action, now); +} + +const QHash &EnergyArbitrator::registeredEvChargers() const +{ + return internalEvChargers(); +} + +RootMeter *EnergyArbitrator::registeredRootMeter() const +{ + return internalRootMeter(); +} + +void EnergyArbitrator::update(const QDateTime ¤tDateTime) +{ + // 1-4 : préparation + sécurité (ordre garanti, identique à l'amont) + updateManualSoCsWithoutMeter(currentDateTime); + prepareInformation(currentDateTime); + verifyOverloadProtection(currentDateTime); + verifyOverloadProtectionRecovery(currentDateTime); + + // 5 : planification via IScheduler (RuleBasedScheduler pour l'instant) + syncAdapters(); + SurplusContext ctx = buildContext(currentDateTime); + Plan plan = m_scheduler->getPlan(ctx); + Slot slot = plan.slotCovering(currentDateTime); + + // 6 : log des decisionReason (DoD 3b) + for (const LoadAction &action : slot.actions) { + qCInfo(dcNymeaEnergy()) << "[Arbitre]" + << action.loadId << "→" << action.reason + << "| activé:" << action.chargingEnabled + << "| courant:" << action.currentA << "A" + << "| phases:" << action.phaseCount + << "| stratégie:" << plan.strategy; + } + + // 7 : dispatch matériel + mise à jour des états de charge (via amont iso-fonctionnel) + adjustEvChargers(currentDateTime); +} + +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; + return ctx; +} + +void EnergyArbitrator::syncAdapters() +{ + // Crée les adapters manquants + for (auto it = internalEvChargers().constBegin(); it != internalEvChargers().constEnd(); ++it) { + const QString id = it.key().toString(); + if (!m_adapters.contains(id)) + m_adapters[id] = new EvAdapter(it.value(), this); + } + // Supprime les adapters obsolètes + for (const QString &id : m_adapters.keys()) { + if (!internalEvChargers().contains(ThingId(id))) + m_adapters.take(id)->deleteLater(); + } +} diff --git a/energyplugin/etm/energyarbitrator.h b/energyplugin/etm/energyarbitrator.h new file mode 100644 index 0000000..6455cfe --- /dev/null +++ b/energyplugin/etm/energyarbitrator.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync +#pragma once + +#include "../smartchargingmanager.h" +#include "scheduler/ischeduler.h" +#include "types/surpluscontext.h" +#include "types/plan.h" + +class EvAdapter; +class RuleBasedScheduler; + +/*! + * \brief Arbitre central ETM — remplace SmartChargingManager::update() (ETM_ARBITRATOR). + * + * Hérite de SmartChargingManager pour conserver la compatibilité API complète avec + * NymeaEnergyJsonHandler sans modifier le code amont. + * Seul update() est surchargé : préparation → sécurité → planificateur → adapters. + * + * \invariant UN seul arbitre : EnergyArbitrator décide, les EvAdapter exécutent (règle 1). + * \invariant verifyOverloadProtection() est toujours appelée avant la planification (règle 4). + * \invariant Toute LoadAction transmise aux adapters a un \c reason non vide (règle 7). + * \invariant L'absence du root meter n'empêche pas le démarrage — cycle ignoré silencieusement. + */ +class EnergyArbitrator : public SmartChargingManager +{ + Q_OBJECT +public: + explicit EnergyArbitrator(EnergyManager *energyManager, ThingManager *thingManager, + SpotMarketManager *spotMarketManager, + EnergyManagerConfiguration *configuration, + QObject *parent = nullptr); + + /*! + * \brief Déclenche planSurplusCharging() (protégée) — appelé par RuleBasedScheduler. + * \param now Instant courant du cycle. + */ + void runSurplusPlanning(const QDateTime &now); + + /*! + * \brief Déclenche planSpotMarketCharging() (protégée) — appelé par RuleBasedScheduler. + * \param now Instant courant du cycle. + */ + void runSpotMarketPlanning(const QDateTime &now); + + /*! + * \brief Actions planifiées (résultat de runSurplus/SpotMarket). + * \return Référence constante vers la table EvCharger* → ChargingActions. + * \note Valide seulement après runSurplusPlanning() / runSpotMarketPlanning(). + */ + const QHash &scheduledActions() const; + + /*! + * \brief Pont d'exécution pour EvAdapter — délègue à executeChargingAction() protégée. + * \param charger Borne EV cible. + * \param action ChargingAction à appliquer. + * \param now Instant de l'action (pour les locks anti-rebond). + */ + void doExecuteChargingAction(EvCharger *charger, const ChargingAction &action, const QDateTime &now); + + /*! + * \brief Liste des EvCharger enregistrés (lecture seule). + * \return Table ThingId → EvCharger*. + */ + const QHash ®isteredEvChargers() const; + + /*! + * \brief Root meter courant. + * \return Pointeur ou nullptr si aucun compteur principal n'est enregistré. + */ + RootMeter *registeredRootMeter() const; + +protected: + /*! + * \brief Boucle principale ETM — surcharge SmartChargingManager::update(). + * + * Ordre garanti : + * 1. updateManualSoCsWithoutMeter() + * 2. prepareInformation() + * 3. verifyOverloadProtection() + verifyOverloadProtectionRecovery() + * 4. m_scheduler->getPlan() → log des decisionReason + * 5. adjustEvChargers() → dispatch matériel + mise à jour états + * + * \param currentDateTime Instant courant (timer ou simulation). + */ + void update(const QDateTime ¤tDateTime) override; + +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). + */ + 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. + */ + void syncAdapters(); + + RuleBasedScheduler *m_scheduler = nullptr; + QHash m_adapters; //!< loadId (ThingId string) → EvAdapter*. +}; diff --git a/energyplugin/etm/etm.pri b/energyplugin/etm/etm.pri index 561d156..b49f3cc 100644 --- a/energyplugin/etm/etm.pri +++ b/energyplugin/etm/etm.pri @@ -5,3 +5,11 @@ HEADERS += \ $$PWD/types/plan.h \ $$PWD/adapters/iloadadapter.h \ $$PWD/scheduler/ischeduler.h \ + $$PWD/adapters/evadapter.h \ + $$PWD/scheduler/rulebasedscheduler.h \ + $$PWD/energyarbitrator.h \ + +SOURCES += \ + $$PWD/adapters/evadapter.cpp \ + $$PWD/scheduler/rulebasedscheduler.cpp \ + $$PWD/energyarbitrator.cpp \ diff --git a/energyplugin/etm/scheduler/ischeduler.h b/energyplugin/etm/scheduler/ischeduler.h index a679560..af177f6 100644 --- a/energyplugin/etm/scheduler/ischeduler.h +++ b/energyplugin/etm/scheduler/ischeduler.h @@ -5,15 +5,27 @@ #include "../types/surpluscontext.h" #include "../types/plan.h" -// Interface pure — implémentations concrètes héritent de QObject + IScheduler. -// -// Règle : getPlan() DOIT retourner immédiatement (modèle cache §AGENTS invariant 5). -// SocketScheduler renvoie son dernier plan en cache et recalcule en fond. -// SocketScheduler embarque un RuleBasedScheduler comme fallback et renvoie -// TOUJOURS un Plan exploitable — l'abstain du protocole n'atteint jamais l'arbitre. +/*! + * \brief Interface pure du planificateur d'énergie. + * + * Les implémentations concrètes héritent de QObject + IScheduler. + * + * \invariant getPlan() retourne IMMÉDIATEMENT (modèle cache, AGENTS invariant 5). + * SocketScheduler retourne son dernier plan en cache et recalcule en arrière-plan. + * \invariant getPlan() retourne TOUJOURS un Plan valide (isValid() == true). + * SocketScheduler embarque un RuleBasedScheduler en fallback — jamais d'abstain + * qui remonterait à l'arbitre (AGENTS règle 6). + * \invariant Toute LoadAction du Plan retourné a \c reason non vide, en français. + */ class IScheduler { public: virtual ~IScheduler() = default; + /*! + * \brief Calcule le plan d'optimisation à partir du contexte courant. + * \param ctx Contexte surplus (site, compteur, PV, batterie, charges, tarif). + * \return Plan avec au moins un Slot — jamais Plan::isValid() == false. + * \note Retourne immédiatement depuis le cache ; le recalcul est asynchrone. + */ virtual Plan getPlan(const SurplusContext &ctx) = 0; }; diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.cpp b/energyplugin/etm/scheduler/rulebasedscheduler.cpp new file mode 100644 index 0000000..363abe2 --- /dev/null +++ b/energyplugin/etm/scheduler/rulebasedscheduler.cpp @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync + +#include "rulebasedscheduler.h" +#include "../energyarbitrator.h" +#include "../../evcharger.h" +#include "../../types/chargingaction.h" +#include "../../types/charginginfo.h" + +#include "plugininfo.h" +#include + +RuleBasedScheduler::RuleBasedScheduler(EnergyArbitrator *arbitrator, QObject *parent) + : QObject(parent) + , m_arbitrator(arbitrator) +{ +} + +Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx) +{ + // Planification (même logique que l'amont — écrit dans m_chargingActions) + m_arbitrator->runSpotMarketPlanning(ctx.timestamp); + m_arbitrator->runSurplusPlanning(ctx.timestamp); + + Slot slot; + slot.from = ctx.timestamp; + slot.to = ctx.timestamp.addSecs(60); + + const auto &cas = m_arbitrator->scheduledActions(); + const auto &evs = m_arbitrator->registeredEvChargers(); + + // Même priorité que adjustEvChargers() — iso-fonctionnel 3b + for (auto it = evs.constBegin(); it != evs.constEnd(); ++it) { + EvCharger *ev = it.value(); + if (!ev->available() || !ev->pluggedIn()) + continue; + + const ChargingActions &actions = cas.value(ev); + LoadAction la; + + if (actions.value(ChargingAction::ChargingActionIssuerTimeRequirement).chargingEnabled()) { + la = buildTimeRequirementAction( + ev, actions.value(ChargingAction::ChargingActionIssuerTimeRequirement)); + + } else if (actions.value(ChargingAction::ChargingActionIssuerSurplusCharging).chargingEnabled()) { + const auto &ca = actions.value(ChargingAction::ChargingActionIssuerSurplusCharging); + la.loadId = ev->thing()->id().toString(); + la.kind = LoadAction::Setpoint; + la.funding = LoadAction::Surplus; + la.chargingEnabled = true; + la.currentA = ca.maxChargingCurrent(); + la.phaseCount = ca.desiredPhaseCount(); + la.reason = QStringLiteral("Surplus PV disponible — recharge solaire"); + la.estimatedPowerW = la.currentA * 230.0 * la.phaseCount; + + } else if (actions.value(ChargingAction::ChargingActionIssuerSpotMarketCharging).chargingEnabled()) { + const auto &ca = actions.value(ChargingAction::ChargingActionIssuerSpotMarketCharging); + la.loadId = ev->thing()->id().toString(); + la.kind = LoadAction::Setpoint; + la.funding = LoadAction::Grid; + la.chargingEnabled = true; + la.currentA = ca.maxChargingCurrent(); + la.phaseCount = ca.desiredPhaseCount(); + la.reason = QStringLiteral("Tarif aWATTar favorable — recharge heure creuse"); + la.estimatedPowerW = la.currentA * 230.0 * la.phaseCount; + + } else { + const ChargingInfo::ChargingMode mode = + m_arbitrator->chargingInfo(ev->id()).chargingMode(); + if (mode == ChargingInfo::ChargingModeEcoWithMinCurrent + || mode == ChargingInfo::ChargingModeEcoMinWithTargetTime) { + la = buildMinCurrentAction(ev); + } else { + la = buildIdleAction(ev); + } + } + + slot.actions.append(la); + } + + Plan plan; + plan.planId = QUuid::createUuid().toString(QUuid::WithoutBraces); + plan.strategy = QStringLiteral("rule-based"); + plan.timeSlots.append(slot); + return plan; +} + +LoadAction RuleBasedScheduler::buildTimeRequirementAction(EvCharger *ev, + const ChargingAction &ca) const +{ + // Le courant final est affiné par adjustEvChargers() (allowance root-meter). + // En 3b on log la valeur brute de la planification — iso-fonctionnel. + LoadAction la; + la.loadId = ev->thing()->id().toString(); + la.kind = LoadAction::Setpoint; + la.funding = LoadAction::Grid; + la.chargingEnabled = true; + la.currentA = ca.maxChargingCurrent(); + la.phaseCount = ca.desiredPhaseCount(); + la.reason = QStringLiteral("Deadline VE approchante — recharge prioritaire"); + la.estimatedPowerW = la.currentA * 230.0 * la.phaseCount; + return la; +} + +LoadAction RuleBasedScheduler::buildMinCurrentAction(EvCharger *ev) const +{ + const uint minA = qMax(EcoMinChargingCurrent, ev->maxChargingCurrentMinValue()); + const uint phases = ev->phaseCount(); + + LoadAction la; + la.loadId = ev->thing()->id().toString(); + la.kind = LoadAction::Setpoint; + la.funding = LoadAction::Surplus; + la.chargingEnabled = true; + la.currentA = minA; + la.phaseCount = phases; + la.reason = QStringLiteral("Aucun surplus — courant minimum maintenu (mode EcoMin)"); + la.estimatedPowerW = la.currentA * 230.0 * la.phaseCount; + return la; +} + +LoadAction RuleBasedScheduler::buildIdleAction(EvCharger *ev) const +{ + LoadAction la; + la.loadId = ev->thing()->id().toString(); + la.kind = LoadAction::Setpoint; + la.funding = LoadAction::Surplus; + la.chargingEnabled = false; + la.currentA = 0; + la.phaseCount = 0; + la.reason = QStringLiteral("Aucun surplus disponible — recharge suspendue"); + la.estimatedPowerW = 0; + return la; +} diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.h b/energyplugin/etm/scheduler/rulebasedscheduler.h new file mode 100644 index 0000000..33ed7ec --- /dev/null +++ b/energyplugin/etm/scheduler/rulebasedscheduler.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync +#pragma once + +#include +#include "ischeduler.h" + +class EvCharger; +class ChargingAction; +class EnergyArbitrator; + +/*! + * \brief Planificateur réglementaire basé sur les règles GPL (EV surplus + aWATTar). + * + * En phase 3b, wraps la logique de SmartChargingManager (planSurplusCharging + + * planSpotMarketCharging) et l'expose via IScheduler en annotant chaque action + * d'un \c reason en français. + * + * \invariant getPlan() retourne IMMÉDIATEMENT (AGENTS invariant 5). + * \invariant getPlan() retourne toujours un Plan valide (isValid() == true). + * \invariant Toute LoadAction a un \c reason non vide, en français. + * \invariant Priorité : Deadline VE > Surplus PV > aWATTar > Min courant > Idle. + * Identique à adjustEvChargers() amont (iso-fonctionnel 3b). + */ +class RuleBasedScheduler : public QObject, public IScheduler +{ + Q_OBJECT +public: + /*! + * \brief Constructeur. + * \param arbitrator Arbitre propriétaire — fournit l'accès à la planification et à l'état. + * \param parent Propriétaire Qt. + */ + explicit RuleBasedScheduler(EnergyArbitrator *arbitrator, QObject *parent = nullptr); + + /*! + * \brief Calcule le plan d'action pour le slot courant. + * + * Appelle les méthodes de planification héritées (surplus + spot market) puis + * traduit les ChargingActions résultantes en LoadActions annotées d'un \c reason. + * + * \param ctx SurplusContext courant. En 3b : seul \c ctx.timestamp est utilisé. + * \return Plan avec un Slot couvrant les 60 secondes à venir depuis ctx.timestamp. + */ + Plan getPlan(const SurplusContext &ctx) override; + +private: + /*! + * \brief Construit un LoadAction pour le cas "délai requis" (TimeRequirement). + * \param ev EvCharger concerné. + * \param ca ChargingAction planifiée (courant et phases déjà calculés par planSurplusCharging). + * \return LoadAction avec funding=Grid et reason "Deadline VE". + */ + LoadAction buildTimeRequirementAction(EvCharger *ev, const ChargingAction &ca) const; + + /*! + * \brief Construit un LoadAction "courant minimum" pour les modes EcoMin. + * \param ev EvCharger concerné. + * \return LoadAction avec funding=Surplus, chargingEnabled=true, currentA=min. + */ + LoadAction buildMinCurrentAction(EvCharger *ev) const; + + /*! + * \brief Construit un LoadAction "idle" (recharge désactivée, aucun surplus). + * \param ev EvCharger concerné. + * \return LoadAction avec chargingEnabled=false et reason appropriée. + */ + LoadAction buildIdleAction(EvCharger *ev) const; + + EnergyArbitrator *m_arbitrator; +}; diff --git a/energyplugin/etm/types/loadaction.h b/energyplugin/etm/types/loadaction.h index b2c4456..b90efb9 100644 --- a/energyplugin/etm/types/loadaction.h +++ b/energyplugin/etm/types/loadaction.h @@ -4,33 +4,59 @@ #include -// Noms de champs : identiques à OPTIMIZER_PROTOCOL.md §6 (font autorité). -// `funding` est interne à l'arbitre — absent du JSON protocole. +/*! + * \brief Action typée émise par l'arbitre vers un ILoadAdapter. + * + * Noms de champs identiques à OPTIMIZER_PROTOCOL.md §6 (fait autorité). + * \c funding est interne à l'arbitre — absent du JSON protocole socket. + * + * \invariant \c reason doit être non vide et en français (AGENTS invariant 7). + * Un adaptateur doit rejeter toute action avec \c reason vide. + * \invariant Pour kind == Setpoint / evcharger : seuls \c chargingEnabled, + * \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 }; - enum Funding { Surplus, Grid }; // interne, non sérialisé - enum Source { Solar, GridSource }; // "solar"|"grid" — Setpoint battery - enum Permission { Allow, Forbid }; // "allow"|"forbid" — 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". */ + enum Source { Solar, GridSource }; + /*! \brief Permission de charge/décharge pour Constraint batterie. */ + enum Permission { Allow, Forbid }; - QString loadId; + QString loadId; //!< ThingId de la charge cible (string). Kind kind = Setpoint; Funding funding = Surplus; - // Setpoint — evcharger + // --- Setpoint evcharger --- bool chargingEnabled = false; - double currentA = 0; - uint phaseCount = 0; - // Setpoint — battery + double currentA = 0; //!< Courant consigne (A), écrêté par l'adaptateur. + uint phaseCount = 0; //!< Nombre de phases (1 ou 3, 0 = inchangé). + + // --- Setpoint battery --- double powerW = 0; Source source = Solar; - // Stage — relay-stages (ECS) - int stage = 0; - // State — sg-ready - int state = 0; - // Constraint — battery v1 + + // --- 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). + + // --- Constraint battery --- Permission charge = Allow; Permission discharge = Allow; - QString reason; // obligatoire, non vide, français (AGENTS invariant 7) - double estimatedPowerW = 0; // hint pour comptabilité arbitre (rempli par le scheduler) + /*! + * \brief Motif de la décision, non vide, en français. + * Obligatoire (invariant 7). L'adaptateur rejette silencieusement si vide. + */ + QString reason; + + /*! + * \brief Puissance estimée (W) — hint pour la comptabilité budget de l'arbitre. + * Rempli par le scheduler ; peut être 0 si inconnu. + */ + double estimatedPowerW = 0; }; diff --git a/energyplugin/etm/types/loaddescriptor.h b/energyplugin/etm/types/loaddescriptor.h index f4ebb2b..9c981e7 100644 --- a/energyplugin/etm/types/loaddescriptor.h +++ b/energyplugin/etm/types/loaddescriptor.h @@ -7,48 +7,70 @@ #include #include "loadaction.h" -// Capacités déclarées par l'installateur (plaque signalétique, câblage). -// Noms de champs : OPTIMIZER_PROTOCOL.md §5 loads[].declared +/*! + * \brief Capacités déclarées par l'installateur (plaque signalétique, câblage). + * Correspond à OPTIMIZER_PROTOCOL.md §5 loads[].declared (noms identiques). + * \note Les champs inutilisés pour un type d'adaptateur restent à leur valeur par défaut. + */ struct LoadDeclared { - // evcharger - double minA = 0; - double maxA = 0; - int phases = 0; - // relay-stages (ECS) — puissances en W : [0, 1200, 2400] + // --- evcharger --- + double minA = 0; //!< Courant minimum (A). + 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; - // battery - double maxChargeW = 0; - double maxDischargeW = 0; - double capacityWh = 0; - int reserveSocPercent = 0; - // sg-ready : états toujours 1-4, pas de déclaration nécessaire + + // --- battery --- + double maxChargeW = 0; //!< Puissance max de charge (W). + double maxDischargeW = 0; //!< Puissance max de décharge (W). + double capacityWh = 0; //!< Capacité totale (Wh). + int reserveSocPercent = 0; //!< SOC de réserve (%) — non déchargeable. + + // sg-ready : états toujours 1-4, pas de déclaration nécessaire. }; -// OPTIMIZER_PROTOCOL.md §5 loads[].limits +/*! + * \brief Contraintes anti-rebond et lock temporel d'une charge. + * Correspond à OPTIMIZER_PROTOCOL.md §5 loads[].limits. + */ struct LoadLimits { - int minOnS = 0; - int minOffS = 0; - int chargingEnabledLockS = 0; // evcharger - int currentChangeLockS = 0; // evcharger - int minStateHoldS = 0; // sg-ready + int minOnS = 0; //!< Durée minimale ON (s). + int minOffS = 0; //!< Durée minimale OFF (s). + int chargingEnabledLockS = 0; //!< Lock on/off (s) — evcharger. + int currentChangeLockS = 0; //!< Lock changement courant (s) — evcharger. + int minStateHoldS = 0; //!< Durée minimale maintien état (s) — sg-ready. }; -// OPTIMIZER_PROTOCOL.md §5 loads[].needs +/*! + * \brief Besoins énergétiques déclarés par l'utilisateur pour une charge. + * Correspond à OPTIMIZER_PROTOCOL.md §5 loads[].needs. + */ struct LoadNeeds { - int targetSocPercent = 0; - QDateTime deadline; - QString dailyDeadline; // "18:00" - int minEnergyWhPerDay = 0; + int targetSocPercent = 0; //!< SOC cible (%) — EV / batterie. + QDateTime deadline; //!< Échéance absolue de recharge. + QString dailyDeadline; //!< Heure limite quotidienne, format "HH:MM". + int minEnergyWhPerDay = 0; //!< Énergie minimale par jour (Wh). }; -// Déclaration statique qu'un ILoadAdapter expose à l'arbitre. +/*! + * \brief Description statique complète d'une charge, exposée par ILoadAdapter. + * + * L'arbitre lit ce descripteur une fois par cycle pour construire le SurplusContext. + * Les valeurs doivent refléter la configuration matérielle réelle (non les setpoints). + * + * \note \c priority : entier positif, valeur plus haute = traité en premier. + * Valeurs suggérées : 200 deadline, 100 normal, 50 confort, 0 stockage différable. + */ struct LoadDescriptor { - QString id; - QString label; - QString adapter; // "evcharger"|"relay-stages"|"sg-ready"|"battery" - int priority = 0; - LoadDeclared declared; - LoadLimits limits; - LoadNeeds needs; - QList supportedKinds; + QString id; //!< ThingId de la charge (string). + QString label; //!< Nom lisible (affiché dans les logs). + //! Type d'adaptateur : "evcharger"|"relay-stages"|"sg-ready"|"battery". + QString adapter; + int priority = 0; + LoadDeclared declared; + LoadLimits limits; + LoadNeeds needs; + QList supportedKinds; //!< Kinds acceptés par applyAction(). }; diff --git a/energyplugin/etm/types/plan.h b/energyplugin/etm/types/plan.h index a349505..3cfeb48 100644 --- a/energyplugin/etm/types/plan.h +++ b/energyplugin/etm/types/plan.h @@ -7,27 +7,47 @@ #include #include "loadaction.h" -// OPTIMIZER_PROTOCOL.md §6 (noms de champs identiques). +// Structures miroirs de OPTIMIZER_PROTOCOL.md §6 (noms de champs identiques). +/*! + * \brief Créneau d'un plan — contient les LoadAction à appliquer pendant [from, to[. + * + * \invariant Les actions sont ordonnées par priorité décroissante (LoadDescriptor.priority). + * \invariant Un Slot vide (actions vide) est valide — signifie "aucune action ce créneau". + */ struct Slot { QDateTime from; QDateTime to; - QList actions; // ordonnées par priorité de LoadDescriptor + QList actions; }; +/*! + * \brief Plan d'optimisation retourné par IScheduler::getPlan(). + * + * \invariant \c isValid() == true après tout appel à getPlan() (invariant IScheduler). + * \invariant \c planId est unique par plan généré (UUID ou compteur). + */ struct Plan { - QString planId; - QString strategy; - QList slots; + QString planId; //!< Identifiant unique (UUID string). + QString strategy; //!< "rule-based" | "socket" | "socket-fallback". + //! Créneaux du plan. Nommé \c timeSlots (pas \c slots, mot-clé Qt). + //! Sérialisation JSON : sous le nom "slots" — OPTIMIZER_PROTOCOL.md §6 + //! fait autorité sur le nom de fil, le renommage est purement interne C++. + QList timeSlots; - // Créneau couvrant dt — créneau vide (from==to==invalid) si aucun. + /*! + * \brief Retourne le Slot couvrant \p dt. + * \param dt Instant à couvrir. + * \return Slot dont from ≤ dt < to, ou Slot vide (from/to invalides) si aucun ne correspond. + */ Slot slotCovering(const QDateTime &dt) const { - for (const Slot &s : slots) { + for (const Slot &s : timeSlots) { if (dt >= s.from && dt < s.to) return s; } return {}; } - bool isValid() const { return !slots.isEmpty(); } + /*! \brief Vrai si le plan contient au moins un Slot. */ + bool isValid() const { return !timeSlots.isEmpty(); } }; diff --git a/energyplugin/etm/types/surpluscontext.h b/energyplugin/etm/types/surpluscontext.h index 5b44e49..c5c75c7 100644 --- a/energyplugin/etm/types/surpluscontext.h +++ b/energyplugin/etm/types/surpluscontext.h @@ -9,70 +9,79 @@ // Structures miroirs de OPTIMIZER_PROTOCOL.md §5 (noms de champs identiques). +/*! \brief Paramètres fixes du site (contrat réseau, limite par phase). */ struct SurplusSite { - double contractedPowerW = 0; - QList phaseLimitA; // [63.0, 63.0, 63.0] + double contractedPowerW = 0; //!< Puissance souscrite (W). + QList phaseLimitA; //!< Limite par phase (A) : [63, 63, 63]. }; +/*! \brief Mesures du compteur principal (rootmeter). */ struct SurplusMeter { - double importW = 0; - double exportW = 0; - QList perPhaseA; + double importW = 0; //!< Puissance importée depuis le réseau (W, ≥ 0). + double exportW = 0; //!< Puissance exportée vers le réseau (W, ≥ 0). + QList perPhaseA; //!< Courant par phase (A). }; +/*! \brief Production PV courante. */ struct SurplusPv { - double currentW = 0; + double currentW = 0; //!< Puissance PV mesurée (W, ≥ 0). }; +/*! \brief État courant du système de stockage (batterie). */ struct SurplusBattery { bool present = false; double socPercent = 0; - double powerW = 0; + double powerW = 0; //!< Positif = charge, négatif = décharge. double capacityWh = 0; int reserveSocPercent = 0; double maxChargeW = 0; double maxDischargeW = 0; }; +/*! \brief Entrée tarifaire (créneau HP/HC ou spot market). */ struct TariffEntry { QDateTime from; - QString label; - double priceCtkWh = 0; + QString label; //!< "HP", "HC", "Tempo-Rouge", ... + double priceCtkWh = 0; //!< Prix en ct€/kWh. }; +/*! \brief Contexte tarifaire : créneau courant + prochains créneaux. */ struct SurplusTariff { QString provider; TariffEntry current; - QList next; + QList next; //!< Créneaux suivants, ordre chronologique. }; -// Télémétrie d'une charge dans le contexte §5 loads[].telemetry +/*! \brief Télémétrie d'une charge dans le contexte §5 loads[].telemetry. */ struct LoadContextTelemetry { - double currentPowerW = 0; - // evcharger + double currentPowerW = 0; //!< Puissance mesurée (W). + // --- evcharger --- bool pluggedIn = false; bool charging = false; - double sessionWh = 0; - // relay-stages + double sessionWh = 0; //!< Énergie chargée dans la session courante (Wh). + // --- relay-stages --- int stage = 0; - // sg-ready + // --- sg-ready --- int state = 0; - // battery / electricvehicle + // --- battery / electricvehicle --- double socPercent = 0; - QDateTime lastSwitch; + QDateTime lastSwitch; //!< Dernier changement d'état. }; -// Données apprises par l'optimiseur §8 (renvoyées pour persistance) +/*! \brief Données apprises par l'optimiseur §8 (renvoyées pour persistance). */ struct LoadLearned { double dailyEnergyWh = 0; - double confidence = 0.0; // 0–1 ; < 0.7 → "profil en apprentissage" + //! Confiance 0–1 ; < 0.7 = "profil en apprentissage". + double confidence = 0.0; }; -// Entrée loads[] dans le SurplusContext envoyé au scheduler. -// Construit par l'arbitre depuis ILoadAdapter::descriptor() + telemetry(). +/*! + * \brief Entrée loads[] du SurplusContext envoyé au scheduler. + * Construit par l'arbitre depuis ILoadAdapter::descriptor() + toLoadContext(). + */ struct LoadContext { QString id; - QString adapter; // "evcharger"|"relay-stages"|"sg-ready"|"battery" + QString adapter; //!< "evcharger"|"relay-stages"|"sg-ready"|"battery". QString label; int priority = 0; LoadDeclared declared; @@ -82,7 +91,13 @@ struct LoadContext { LoadLimits limits; }; -// OPTIMIZER_PROTOCOL.md §5 — transmis au scheduler à chaque cycle. +/*! + * \brief Contexte complet transmis au scheduler à chaque cycle (OPTIMIZER_PROTOCOL §5). + * + * \invariant \c timestamp correspond à l'instant du début du cycle. + * \invariant \c pv.currentW est la PV mesurée brute — JAMAIS le net après pilotage + * (AGENTS invariant 8 : pas de boucle de feedback). + */ struct SurplusContext { QDateTime timestamp; SurplusSite site; diff --git a/energyplugin/smartchargingmanager.h b/energyplugin/smartchargingmanager.h index 2ec115a..3bb2847 100644 --- a/energyplugin/smartchargingmanager.h +++ b/energyplugin/smartchargingmanager.h @@ -93,21 +93,30 @@ signals: void chargingUpdated(); #endif +// [ETM] BEGIN — SmartChargingManager protected API for EnergyArbitrator (etm/). +// All changes below are visibility-only (private → protected / virtual added). +// Zero logic change. Revert by deleting this block and restoring private slots. +protected slots: + virtual void update(const QDateTime ¤tDateTime); // [ETM] virtual added + void prepareInformation(const QDateTime ¤tDateTime); // [ETM] private → protected + void planSpotMarketCharging(const QDateTime ¤tDateTime); // [ETM] private → protected + void planSurplusCharging(const QDateTime ¤tDateTime); // [ETM] private → protected + void adjustEvChargers(const QDateTime ¤tDateTime); // [ETM] private → protected + void updateManualSoCsWithoutMeter(const QDateTime ¤tDateTime); // [ETM] private → protected + void verifyOverloadProtection(const QDateTime ¤tDateTime); // [ETM] private → protected + void verifyOverloadProtectionRecovery(const QDateTime ¤tDateTime); // [ETM] private → protected + +protected: + void executeChargingAction(EvCharger *evCharger, const ChargingAction &chargingAction, const QDateTime ¤tDateTime); // [ETM] private → protected + + // [ETM] Read-only state accessors — inline, no copies, no logic. + const QHash &internalEvChargers() const { return m_evChargers; } // [ETM] new + const QHash &internalChargingActions() const { return m_chargingActions; } // [ETM] new + RootMeter *internalRootMeter() const { return m_rootMeter; } // [ETM] new +// [ETM] END + private slots: - void update(const QDateTime ¤tDateTime); - - // Don't call these methods out of place. it's only meant to keep the otherwise long update() code tidy. - // Call update() if you want to trigger the smarties. - void prepareInformation(const QDateTime ¤tDateTime); - void planSpotMarketCharging(const QDateTime ¤tDateTime); - void planSurplusCharging(const QDateTime ¤tDateTime); - void adjustEvChargers(const QDateTime ¤tDateTime); void updateManualSoCsWithMeter(EnergyLogs::SampleRate sampleRate, const ThingPowerLogEntry &entry); - void updateManualSoCsWithoutMeter(const QDateTime ¤tDateTime); - - void verifyOverloadProtection(const QDateTime ¤tDateTime); - void verifyOverloadProtectionRecovery(const QDateTime ¤tDateTime); - void onThingAdded(Thing *thing); void onThingRemoved(const ThingId &thingId); void onActionExecuted(const Action &action, Thing::ThingError status); @@ -129,7 +138,7 @@ private: QString chargerPhaseKey(EvCharger *evCharger) const; EnergyManager *m_energyManager = nullptr; - ThingManager *m_thingManager = nullptr; + ThingManager *m_thingManager = nullptr; SpotMarketManager *m_spotMarketManager = nullptr; EnergyManagerConfiguration *m_configuration = nullptr; @@ -152,8 +161,6 @@ private: RootMeter *m_rootMeter = nullptr; QHash m_evChargers; - void executeChargingAction(EvCharger *evCharger, const ChargingAction &chargingAction, const QDateTime ¤tDateTime); - }; #endif // SMARTCHARGINGMANAGER_H