RuleBasedScheduler : buildSetpointAction (contrat rév. 2 §5) — fixed = arrondi au plus haut powerLevels ≤ budget ; dynamic = clamp(budget, 0, maxPowerW) ; recrédit currentPowerW de DÉBUT de cycle (invariant 8) ; résidu cascadé. Waterfall unifié : etmvariableload (Setpoint W) + sg-ready (State) triés par priorité, un seul budget. EnergyArbitrator : registerEtmVariableLoadAdapter, inclusion dans buildContext, dispatch Setpoint par loadId (l'EV reste au proxy amont). Infra de test : ThingClass mock etmVariableLoad (currentPowerW read / powerSetpoint write) + handler executeAction + helper addEtmVariableLoad — valide l'implémentabilité de l'interface avant le thing réel. Les scénarios simulation migrés arrivent en T4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
6.2 KiB
C++
121 lines
6.2 KiB
C++
// 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ègles GPL : EV (proxy amont) + waterfall surplus SG-Ready (PAC).
|
||
*
|
||
* \c getPlan() produit un plan à 1 créneau en DEUX temps :
|
||
*
|
||
* 1. **EV — proxy amont (beta, jusqu'à 3g)** : délègue la planification EV à
|
||
* \c planSurplusCharging() / \c planSpotMarketCharging() héritées de SmartChargingManager,
|
||
* relit \c m_chargingActions et les reformate en LoadAction(Setpoint) annotées d'un
|
||
* \c reason français. Le dispatch EV réel reste dans \c adjustEvChargers() amont.
|
||
*
|
||
* 2. **Waterfall non-EV** : un budget de surplus UNIQUE — net SIGNÉ
|
||
* \c (exportW − importW) − evReservedW — cascade par **priorité ASC** (rang, 1 = premier
|
||
* servi). En T2, seule la PAC \c sg-ready (\c buildSgReadyStateAction, kind State) y est
|
||
* traitée : anti-clignotement par **recrédit** de la conso allouée, **clamp lock-aware**
|
||
* \c minState/maxState (verrou \c minStateHold, protection court-cycling compresseur) calculé
|
||
* au MÊME \c ctx.timestamp que l'exécution (cf. \c ILoadAdapter). Ces LoadAction sont
|
||
* RÉELLEMENT dispatchées par \c EnergyArbitrator::applyActionsToAdapters().
|
||
*
|
||
* Les charges \c etmvariableload (ECS/routeur, kind Setpoint W) rejoindront ce waterfall avec
|
||
* la règle d'arrondi (contrat rév. 2 §5) en **T3**. À partir de **3g**, l'EV rejoint aussi le
|
||
* waterfall unifié (toutes charges classables ensemble).
|
||
*
|
||
* \invariant getPlan() retourne IMMÉDIATEMENT (AGENTS invariant 5) et toujours un Plan valide.
|
||
* \invariant Toute LoadAction a un \c reason non vide, en français.
|
||
* \invariant EV (étape 1) : priorité Deadline VE > Surplus PV > aWATTar > Min courant > Idle
|
||
* (iso-fonctionnel amont 3b). Non-EV (étape 2) : ordre = \c priority croissant.
|
||
*/
|
||
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 Retourne le plan pour le slot courant (EV proxy + waterfall non-EV).
|
||
*
|
||
* Étape 1 (EV) : \c runSpotMarketPlanning() + \c runSurplusPlanning() puis reformatage
|
||
* de \c scheduledActions() en LoadAction(Setpoint) — log [Arbitre], dispatch amont.
|
||
* Étape 2 (non-EV) : waterfall surplus sur la PAC \c sg-ready triée par priorité —
|
||
* LoadAction(State) réellement dispatchées (etmvariableload ECS/routeur arrive en T3).
|
||
*
|
||
* \param ctx SurplusContext courant. Utilisé : \c ctx.timestamp (temps de cycle / verrous),
|
||
* \c ctx.meter (surplus net signé), \c ctx.loads (déclarés, télémétrie, fenêtres de verrou).
|
||
* \return Plan à 1 créneau couvrant \c ctx.timestamp + 60 s.
|
||
*/
|
||
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;
|
||
|
||
/*!
|
||
* \brief Construit un LoadAction \c Setpoint (W) pour une charge \c etmvariableload
|
||
* (ECS résistif / routeur PV) par la règle d'arrondi du contrat rév. 2 §5.
|
||
*
|
||
* Recrédit anti-clignotement (Correction B / contrat §5) : le budget local =
|
||
* \c remainingSurplusW + \c lc.telemetry.currentPowerW (conso de DÉBUT de cycle, déjà
|
||
* soustraite de l'export mesuré — JAMAIS une relecture post-setpoint, invariant 8). Puis :
|
||
* - **fixed** (\c powerLevels non vide) : setpoint = plus haut palier déclaré ≤ budget ;
|
||
* - **dynamic** (\c powerLevels vide) : setpoint = \c clamp(budget, 0, maxPowerW).
|
||
* Le résidu \c budget − setpoint repart vers la charge suivante de la priorité (même cycle).
|
||
*
|
||
* \param lc Charge \c etmvariableload du SurplusContext.
|
||
* \param[in,out] remainingSurplusW Budget de surplus restant (W) ; mis à jour pour la suite.
|
||
* \return LoadAction kind=Setpoint, funding=Surplus, \c powerW = consigne, \c reason non vide.
|
||
*/
|
||
LoadAction buildSetpointAction(const LoadContext &lc, double &remainingSurplusW) const;
|
||
|
||
/*!
|
||
* \brief Construit un LoadAction "state" SG-Ready (PAC) par mapping SÉMANTIQUE du surplus.
|
||
*
|
||
* 4 états normés (qualitatifs, pas des paliers) : surplus abondant stable → 4 (forcé,
|
||
* hystérésis P4×1,2 entrée / P4×1,0 sortie) ; surplus durable → 3 (recommandation, ≥P3) ;
|
||
* sinon → 2 (normal, mains off). L'état 1 (effacement) n'est PAS déclenché par le surplus
|
||
* seul (déféré : signal tarif/réseau). Recrédit (correction B) sur la puissance allouée
|
||
* (déclaré, 0 pour 1/2). Clamp lock-aware via \c minState/maxState (court-cycling PAC).
|
||
*
|
||
* \param lc Charge SG-Ready (adapter == "sg-ready") du SurplusContext.
|
||
* \param[in,out] remainingSurplusW Budget de surplus restant (W), mis à jour pour la suite.
|
||
* \return LoadAction kind=State, funding=Surplus, \c reason français non vide.
|
||
*/
|
||
LoadAction buildSgReadyStateAction(const LoadContext &lc, double &remainingSurplusW) const;
|
||
|
||
EnergyArbitrator *m_arbitrator;
|
||
};
|