// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync #include "rulebasedscheduler.h" #include "../energyarbitrator.h" #include "../../evcharger.h" #include "../../types/chargingaction.h" #include "../../types/charginginfo.h" #include "plugininfo.h" #include RuleBasedScheduler::RuleBasedScheduler(EnergyArbitrator *arbitrator, QObject *parent) : QObject(parent) , m_arbitrator(arbitrator) { } Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx) { // Planification (même logique que l'amont — écrit dans m_chargingActions) m_arbitrator->runSpotMarketPlanning(ctx.timestamp); m_arbitrator->runSurplusPlanning(ctx.timestamp); Slot slot; slot.from = ctx.timestamp; slot.to = ctx.timestamp.addSecs(60); const auto &cas = m_arbitrator->scheduledActions(); const auto &evs = m_arbitrator->registeredEvChargers(); // Même priorité que adjustEvChargers() — iso-fonctionnel 3b for (auto it = evs.constBegin(); it != evs.constEnd(); ++it) { EvCharger *ev = it.value(); if (!ev->available() || !ev->pluggedIn()) continue; const ChargingActions &actions = cas.value(ev); LoadAction la; if (actions.value(ChargingAction::ChargingActionIssuerTimeRequirement).chargingEnabled()) { la = buildTimeRequirementAction( ev, actions.value(ChargingAction::ChargingActionIssuerTimeRequirement)); } else if (actions.value(ChargingAction::ChargingActionIssuerSurplusCharging).chargingEnabled()) { const auto &ca = actions.value(ChargingAction::ChargingActionIssuerSurplusCharging); la.loadId = ev->thing()->id().toString(); la.kind = LoadAction::Setpoint; la.funding = LoadAction::Surplus; la.chargingEnabled = true; la.currentA = ca.maxChargingCurrent(); la.phaseCount = ca.desiredPhaseCount(); la.reason = QStringLiteral("Surplus PV disponible — recharge solaire"); la.estimatedPowerW = la.currentA * 230.0 * la.phaseCount; } else if (actions.value(ChargingAction::ChargingActionIssuerSpotMarketCharging).chargingEnabled()) { const auto &ca = actions.value(ChargingAction::ChargingActionIssuerSpotMarketCharging); la.loadId = ev->thing()->id().toString(); la.kind = LoadAction::Setpoint; la.funding = LoadAction::Grid; la.chargingEnabled = true; la.currentA = ca.maxChargingCurrent(); la.phaseCount = ca.desiredPhaseCount(); la.reason = QStringLiteral("Tarif aWATTar favorable — recharge heure creuse"); la.estimatedPowerW = la.currentA * 230.0 * la.phaseCount; } else { const ChargingInfo::ChargingMode mode = m_arbitrator->chargingInfo(ev->id()).chargingMode(); if (mode == ChargingInfo::ChargingModeEcoWithMinCurrent || mode == ChargingInfo::ChargingModeEcoMinWithTargetTime) { la = buildMinCurrentAction(ev); } else { la = buildIdleAction(ev); } } slot.actions.append(la); } Plan plan; plan.planId = QUuid::createUuid().toString(QUuid::WithoutBraces); plan.strategy = QStringLiteral("rule-based"); plan.timeSlots.append(slot); return plan; } LoadAction RuleBasedScheduler::buildTimeRequirementAction(EvCharger *ev, const ChargingAction &ca) const { // Le courant final est affiné par adjustEvChargers() (allowance root-meter). // En 3b on log la valeur brute de la planification — iso-fonctionnel. LoadAction la; la.loadId = ev->thing()->id().toString(); la.kind = LoadAction::Setpoint; la.funding = LoadAction::Grid; la.chargingEnabled = true; la.currentA = ca.maxChargingCurrent(); la.phaseCount = ca.desiredPhaseCount(); la.reason = QStringLiteral("Deadline VE approchante — recharge prioritaire"); la.estimatedPowerW = la.currentA * 230.0 * la.phaseCount; return la; } LoadAction RuleBasedScheduler::buildMinCurrentAction(EvCharger *ev) const { const uint minA = qMax(EcoMinChargingCurrent, ev->maxChargingCurrentMinValue()); const uint phases = ev->phaseCount(); LoadAction la; la.loadId = ev->thing()->id().toString(); la.kind = LoadAction::Setpoint; la.funding = LoadAction::Surplus; la.chargingEnabled = true; la.currentA = minA; la.phaseCount = phases; la.reason = QStringLiteral("Aucun surplus — courant minimum maintenu (mode EcoMin)"); la.estimatedPowerW = la.currentA * 230.0 * la.phaseCount; return la; } LoadAction RuleBasedScheduler::buildIdleAction(EvCharger *ev) const { LoadAction la; la.loadId = ev->thing()->id().toString(); la.kind = LoadAction::Setpoint; la.funding = LoadAction::Surplus; la.chargingEnabled = false; la.currentA = 0; la.phaseCount = 0; la.reason = QStringLiteral("Aucun surplus disponible — recharge suspendue"); la.estimatedPowerW = 0; return la; }