[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:
parent
9017a880ac
commit
5f49e4ca3c
20
.clangd
Normal file
20
.clangd
Normal 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
3
.gitignore
vendored
@ -8,3 +8,6 @@ Makefile
|
||||
builddir/
|
||||
*_moc.cpp
|
||||
autogenerated/
|
||||
|
||||
# clangd — chemins absolus du poste local, ne pas versionner
|
||||
compile_commands.json
|
||||
|
||||
37
AGENTS.md
37
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
|
||||
|
||||
|
||||
@ -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 += \
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
94
energyplugin/etm/adapters/evadapter.cpp
Normal file
94
energyplugin/etm/adapters/evadapter.cpp
Normal 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;
|
||||
}
|
||||
72
energyplugin/etm/adapters/evadapter.h
Normal file
72
energyplugin/etm/adapters/evadapter.h
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
|
||||
104
energyplugin/etm/energyarbitrator.cpp
Normal file
104
energyplugin/etm/energyarbitrator.cpp
Normal 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 ¤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();
|
||||
}
|
||||
}
|
||||
103
energyplugin/etm/energyarbitrator.h
Normal file
103
energyplugin/etm/energyarbitrator.h
Normal 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 *> ®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<QString, EvAdapter *> m_adapters; //!< loadId (ThingId string) → EvAdapter*.
|
||||
};
|
||||
@ -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 \
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
134
energyplugin/etm/scheduler/rulebasedscheduler.cpp
Normal file
134
energyplugin/etm/scheduler/rulebasedscheduler.cpp
Normal 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;
|
||||
}
|
||||
71
energyplugin/etm/scheduler/rulebasedscheduler.h
Normal file
71
energyplugin/etm/scheduler/rulebasedscheduler.h
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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().
|
||||
};
|
||||
|
||||
@ -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(); }
|
||||
};
|
||||
|
||||
@ -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; // 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;
|
||||
|
||||
@ -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<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 ¤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<ThingId, EvCharger *> m_evChargers;
|
||||
|
||||
void executeChargingAction(EvCharger *evCharger, const ChargingAction &chargingAction, const QDateTime ¤tDateTime);
|
||||
|
||||
};
|
||||
|
||||
#endif // SMARTCHARGINGMANAGER_H
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user