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