[3b-wip] EnergyArbitrator + RuleBasedScheduler + EvAdapter (dispatch amont, ETM_ARBITRATOR désactivé)

- 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 <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-06-07 23:16:49 +02:00
parent 9017a880ac
commit 5f49e4ca3c
19 changed files with 910 additions and 119 deletions

20
.clangd Normal file
View File

@ -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

3
.gitignore vendored
View File

@ -8,3 +8,6 @@ Makefile
builddir/
*_moc.cpp
autogenerated/
# clangd — chemins absolus du poste local, ne pas versionner
compile_commands.json

View File

@ -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

View File

@ -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 += \

View File

@ -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);
}

View File

@ -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<int>(m_charger->phaseCount());
d.limits.chargingEnabledLockS = static_cast<int>(m_charger->chargingEnabledLockDuration());
d.limits.currentChangeLockS = static_cast<int>(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<uint>(
qBound(static_cast<double>(minA), action.currentA, static_cast<double>(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;
}

View File

@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
#pragma once
#include <QObject>
#include <QDateTime>
#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;
};

View File

@ -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 é 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;
};

View File

@ -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<EvCharger *, ChargingActions> &EnergyArbitrator::scheduledActions() const
{
return internalChargingActions();
}
void EnergyArbitrator::doExecuteChargingAction(EvCharger *charger,
const ChargingAction &action,
const QDateTime &now)
{
executeChargingAction(charger, action, now);
}
const QHash<ThingId, EvCharger *> &EnergyArbitrator::registeredEvChargers() const
{
return internalEvChargers();
}
RootMeter *EnergyArbitrator::registeredRootMeter() const
{
return internalRootMeter();
}
void EnergyArbitrator::update(const QDateTime &currentDateTime)
{
// 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();
}
}

View File

@ -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<EvCharger *, ChargingActions> &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<ThingId, EvCharger *> &registeredEvChargers() 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 &currentDateTime) 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<QString, EvAdapter *> m_adapters; //!< loadId (ThingId string) → EvAdapter*.
};

View File

@ -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 \

View File

@ -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;
};

View File

@ -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 <QUuid>
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;
}

View File

@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
#pragma once
#include <QObject>
#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;
};

View File

@ -4,33 +4,59 @@
#include <QString>
// 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;
};

View File

@ -7,48 +7,70 @@
#include <QString>
#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<int> 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<LoadAction::Kind> 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<LoadAction::Kind> supportedKinds; //!< Kinds acceptés par applyAction().
};

View File

@ -7,27 +7,47 @@
#include <QString>
#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<LoadAction> actions; // ordonnées par priorité de LoadDescriptor
QList<LoadAction> 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<Slot> 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<Slot> 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(); }
};

View File

@ -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<double> phaseLimitA; // [63.0, 63.0, 63.0]
double contractedPowerW = 0; //!< Puissance souscrite (W).
QList<double> phaseLimitA; //!< Limite par phase (A) : [63, 63, 63].
};
/*! \brief Mesures du compteur principal (rootmeter). */
struct SurplusMeter {
double importW = 0;
double exportW = 0;
QList<double> 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<double> 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<TariffEntry> next;
QList<TariffEntry> 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; // 01 ; < 0.7 → "profil en apprentissage"
//! Confiance 01 ; < 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;

View File

@ -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 &currentDateTime); // [ETM] virtual added
void prepareInformation(const QDateTime &currentDateTime); // [ETM] private → protected
void planSpotMarketCharging(const QDateTime &currentDateTime); // [ETM] private → protected
void planSurplusCharging(const QDateTime &currentDateTime); // [ETM] private → protected
void adjustEvChargers(const QDateTime &currentDateTime); // [ETM] private → protected
void updateManualSoCsWithoutMeter(const QDateTime &currentDateTime); // [ETM] private → protected
void verifyOverloadProtection(const QDateTime &currentDateTime); // [ETM] private → protected
void verifyOverloadProtectionRecovery(const QDateTime &currentDateTime); // [ETM] private → protected
protected:
void executeChargingAction(EvCharger *evCharger, const ChargingAction &chargingAction, const QDateTime &currentDateTime); // [ETM] private → protected
// [ETM] Read-only state accessors — inline, no copies, no logic.
const QHash<ThingId, EvCharger *> &internalEvChargers() const { return m_evChargers; } // [ETM] new
const QHash<EvCharger *, ChargingActions> &internalChargingActions() const { return m_chargingActions; } // [ETM] new
RootMeter *internalRootMeter() const { return m_rootMeter; } // [ETM] new
// [ETM] END
private slots:
void update(const QDateTime &currentDateTime);
// 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 &currentDateTime);
void planSpotMarketCharging(const QDateTime &currentDateTime);
void planSurplusCharging(const QDateTime &currentDateTime);
void adjustEvChargers(const QDateTime &currentDateTime);
void updateManualSoCsWithMeter(EnergyLogs::SampleRate sampleRate, const ThingPowerLogEntry &entry);
void updateManualSoCsWithoutMeter(const QDateTime &currentDateTime);
void verifyOverloadProtection(const QDateTime &currentDateTime);
void verifyOverloadProtectionRecovery(const QDateTime &currentDateTime);
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<ThingId, EvCharger *> m_evChargers;
void executeChargingAction(EvCharger *evCharger, const ChargingAction &chargingAction, const QDateTime &currentDateTime);
};
#endif // SMARTCHARGINGMANAGER_H