- 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>
135 lines
5.3 KiB
C++
135 lines
5.3 KiB
C++
// 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;
|
|
}
|