// SPDX-License-Identifier: GPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-energy-plugin-nymea. * * nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * nymea-energy-plugin-nymea.s distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with nymea-energy-plugin-nymea. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #ifndef SMARTCHARGINGMANAGER_H #define SMARTCHARGINGMANAGER_H #include #include #include "energymanagerconfiguration.h" #include "types/charginginfo.h" #include "types/chargingaction.h" #include "types/chargingschedule.h" #include "types/chargingprocessinfo.h" #include "spotmarket/spotmarketmanager.h" // from libnymea #include #include // from libnymea-energy #include class EvCharger; class RootMeter; // Minimum charging current (A) applied when EcoWithMinCurrent is active and no surplus is available static constexpr uint EcoMinChargingCurrent = 6; class SmartChargingManager : public QObject { Q_OBJECT public: explicit SmartChargingManager(EnergyManager *energyManager, ThingManager *thingManager, SpotMarketManager *spotMarketManager, EnergyManagerConfiguration *configuration, QObject *parent = nullptr); //! \brief [ETM] Gestionnaire de Things, pour les consommateurs qui doivent interroger une //! ThingClass sans passer par NymeaCore (frontière RPC notamment). //! \return Pointeur non-propriétaire ; jamais nul après construction. ThingManager *thingManager() const { return m_thingManager; } uint phasePowerLimit() const; void setPhasePowerLimit(uint phasePowerLimit); double acquisitionTolerance() const; void setAcquisitionTolerance(double acquisitionTolerance); double batteryLevelConsideration() const; void setBatteryLevelConsideration(double batteryLevelConsideration); bool lockOnUnplug() const; void setLockOnUnplug(bool lockOnUnplug); ChargingInfos chargingInfos() const; ChargingInfo chargingInfo(const ThingId &evChargerId) const; EnergyManager::EnergyError setChargingInfo(const ChargingInfo &chargingInfo); ChargingSchedules chargingSchedules() const; /*! * \brief [ETM] Mode dégradé L2 — le compteur principal est muet depuis plus de 90 s. * * Base amont : toujours faux (l'amont n'a pas de watchdog). Redéfini par * \c EnergyArbitrator, qui le pose depuis \c applyDegradedMode() (SAFETY.md §L2). * Exposé publiquement pour la notification JSON-RPC \c ChargingSchedulesChanged. * * \return Vrai si la planification est suspendue et les consignes de repli posées. * La sécurité L4 (\c verifyOverloadProtection) reste active dans cet état. */ virtual bool degradedMode() const { return false; } SpotMarketManager *spotMarketManager() const; #ifdef ENERGY_SIMULATION void simulationCallUpdate(const QDateTime &dateTime); void simulationCallUpdateManualSoCsWithMeter(EnergyLogs::SampleRate sampleRate, const ThingPowerLogEntry &entry); #endif signals: void phasePowerLimitChanged(int phasePowerLimit); void acquisitionToleranceChanged(double acquisitionTolerance); void batteryLevelConsiderationChanged(double batteryLevelConsideration); void lockOnUnplugChanged(bool lockOnUnplug); void chargingInfoAdded(const ChargingInfo &chargingInfo); void chargingInfoRemoved(const ThingId &evChargerThingId); void chargingInfoChanged(const ChargingInfo &chargingInfo); void chargingSchedulesChanged(); #ifdef ENERGY_SIMULATION 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. // // INVENTAIRE des ajouts ETM de ce fichier — ils vivent HORS de energyplugin/etm/ et // échappent donc au périmètre du Doxyfile ; ils sont documentés à la main, et la // définition de fait d'AGENTS.md les couvre explicitement. Toute nouvelle marque // [ETM] dans un en-tête amont DOIT être ajoutée ici : // // 1. degradedMode() — ajout ETM, virtuel, documenté ci-dessus. // 2. update() — virtual ajouté (le corps reste amont). // 3. prepareInformation, planSpotMarketCharging, planSurplusCharging, // adjustEvChargers, updateManualSoCsWithoutMeter, verifyOverloadProtection, // verifyOverloadProtectionRecovery, executeChargingAction // — visibilité seule (private → protected). // Sémantique amont inchangée : NON documentés // ici, la doc appartient à l'amont. // 4. internalEvChargers(), internalChargingActions(), internalRootMeter(), // internalProcessInfo(), internalHasProcessInfo() // — accesseurs ETM neufs, documentés ci-dessous. // 5. bestPhaseCountFor(), phasesForCount() — passe-plats ETM vers deux règles amont // privées, documentés ci-dessous. // 7. setEvChargingState() — écriture ETM d'un état RUNTIME, documentée // ci-dessous. Pas un réglage : jamais persistée. // 6. evSurplusPlannedByWaterfall(), evCommandedByWaterfall() // — crochets ETM, virtuels, documentés ci-dessous. // Ce sont EUX qui garantissent « une charge, un // commandeur » : sans eux, planSurplusCharging() // et adjustEvChargers() re-décident après le // waterfall. Deux crochets et non un : ils sont // interrogés à deux moments du cycle où la // réponse n'est pas la même (voir leur doc). 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 /*! * \brief [ETM 3g-1] Nombre de phases le plus adapté à un courant de surplus donné. * \note `private → protected`. La règle de bascule 1↔3 phases est la MÊME que le VE soit * décidé par le proxy ou par le waterfall ; la dupliquer dans l'arbitre en ferait deux, * et elles divergeraient. C'est la seule raison de cet élargissement de visibilité. */ uint bestPhaseCountFor(EvCharger *evCharger, double surplusAmpere) { return getBestPhaseCount(evCharger, surplusAmpere); } /*! * \brief [ETM 3g-2] Masque de phases correspondant à un nombre de phases. * \note `private → protected`, même raison que \c bestPhaseCountFor : l'arbitre en a * besoin pour interroger \c RootMeter::calculateAllowanceAmpere() avec le MÊME masque * que le proxy. Redériver le masque dans l'arbitre en ferait deux versions. */ Electricity::Phases phasesForCount(uint phaseCount) { return getAscendingPhasesForCount(phaseCount); } /*! * \brief [ETM 3g-2] Cette borne est-elle déjà commandée par le waterfall ce cycle ? * * \return Faux dans l'amont : sans arbitre ETM, \c adjustEvChargers() est le seul * commandeur et rien ne change. \c EnergyArbitrator le surcharge. * * \par Pourquoi ce crochet existe * 3g-1 a fait entrer les bornes dans le waterfall mais a laissé tourner le chemin * d'exécution du proxy : l'arbitre décidait, publiait un motif juste, et * \c adjustEvChargers() commandait autre chose une milliseconde plus tard (relevé * machine du 2026-08-26, §3.3). **Deux commandeurs sur un même organe** — c'est la * règle absolue 1 d'AGENTS.md, appliquée à l'exécution au lieu du budget. * * Ce que le crochet EXCLUT du proxy est exactement ce que le waterfall a commandé : * les bornes servies au surplus. Celles qui restent au proxy — échéance et spot market, * financées au réseau (DESIGN_3g §3.1) — continuent d'y passer, allowance de compteur * comprise. */ virtual bool evCommandedByWaterfall(EvCharger *evCharger) const { Q_UNUSED(evCharger) return false; } /*! * \brief [ETM 3g-2] Le surplus de cette borne sera-t-il décidé par le waterfall ? * * \return Faux dans l'amont. \c EnergyArbitrator le surcharge par le filtre d'entrée du * waterfall (voiture assignée + mode non manuel). * * \note **Distinct de \c evCommandedByWaterfall, et la différence est temporelle.** * \c planSurplusCharging() tourne AVANT que le waterfall n'ait décidé : à cet instant * on ne peut savoir que si la borne lui APPARTIENT, pas ce qu'il en aura fait. Une * borne qui lui appartient peut encore sortir par l'échéance — mais celle-ci est * traitée à l'étape 2 de la même fonction, qui la retire de la liste avant l'étape 3. * \c adjustEvChargers(), lui, tourne APRÈS : il interroge le crochet plus strict, sans * quoi une borne servie par l'échéance ne serait plus commandée du tout. */ virtual bool evSurplusPlannedByWaterfall(EvCharger *evCharger) const { Q_UNUSED(evCharger) return false; } /*! * \brief [ETM 3g-2] Met à jour l'état de charge PUBLIÉ d'une borne, et notifie si changé. * * \param evChargerId Borne concernée. \param state Nouvel état de charge. * * Reproduit **exactement** ce que fait \c adjustEvChargers() pour les bornes qu'il * commande : écriture en mémoire, notification \c chargingInfoChanged sur transition * seulement, et **aucune persistance** — \c chargingState est un état runtime, pas un * réglage (il n'a d'ailleurs pas d'accesseur en écriture au schéma RPC). * * Existe parce que les bornes servies par le waterfall ne traversent plus * \c adjustEvChargers() : sans elle, \c chargingState resterait figé sur sa dernière * valeur et l'app afficherait « en charge » une borne que l'arbitre vient d'éteindre. * * \note Ne PAS passer par \c setChargingInfo() pour cela : cette méthode valide, * persiste et relance \c update() — appelée depuis un cycle en cours, elle récurse. */ void setEvChargingState(const ThingId &evChargerId, ChargingInfo::ChargingState state); // [ETM] Read-only state accessors — inline, no copies, no logic. /*! * \brief [ETM] Bornes VE enregistrées, en lecture seule. * \return Référence sur la table interne — aucune copie. Valide tant que le manager * vit ; ne pas conserver au-delà d'un cycle \c update(). */ const QHash &internalEvChargers() const { return m_evChargers; } // [ETM] new /*! * \brief [ETM] Consignes de charge du cycle courant, en lecture seule. * \return Référence sur la table remplie par \c planSurplusCharging() / * \c planSpotMarketCharging(). N'a de sens qu'APRÈS ces appels dans le même cycle. */ const QHash &internalChargingActions() const { return m_chargingActions; } // [ETM] new /*! * \brief [ETM] Compteur principal du site, en lecture seule. * \return Pointeur non-propriétaire ; **peut être nul** si aucun root meter n'est * configuré — tout appelant doit le tester. */ RootMeter *internalRootMeter() const { return m_rootMeter; } // [ETM] new /*! * \brief [ETM 3g-2] Informations de processus d'une borne pour le cycle courant. * * \param evCharger Borne. \return Copie de son \c ChargingProcessInfo ; valeurs par * défaut si la borne n'a pas été préparée ce cycle. * * \note N'a de sens qu'APRÈS \c prepareInformation() dans le même cycle — position 2 de * \c update(), donc avant toute construction de contexte. * \note Existe pour que l'arbitre n'ait pas à REDÉRIVER le nombre de phases possible. * \c EvCharger::canSetPhaseCount() dit seulement que la borne PORTE l'état * \c desiredPhaseCount ; la vraie question — la borne ET la voiture acceptent-elles 1 et * 3 phases — est tranchée ici, une fois, avec le véhicule branché. Les deux prédicats ont * divergé en 3g-1 : une borne monophasée sans voiture triphasée s'est vue viser 3 phases, * et sous surcharge cela commandait 3 × 6 A là où le proxy commandait 1 × 6 A. */ ChargingProcessInfo internalProcessInfo(EvCharger *evCharger) const { return m_processInfos.value(evCharger); } // [ETM] new /*! * \brief [ETM · LM-1208-b] Cette borne a-t-elle été PRÉPARÉE ce cycle ? * * \param evCharger Borne. * \return Vrai si \c prepareInformation() lui a construit un \c ChargingProcessInfo * pendant ce cycle ; faux sinon — notamment quand aucune voiture ne lui est assignée, * cas où le filtre d'entrée de \c prepareInformation() l'écarte. * * \note Existe parce que \c internalProcessInfo() ne sait pas dire « absent » : il rend un * objet par DÉFAUT, dont les valeurs — 1 phase, pas de bascule — sont des réponses * parfaitement plausibles qu'aucun appelant ne peut distinguer d'une préparation réelle. * \note Le cas est devenu RÉEL avec **LM-1208-b** : une borne sans voiture assignée entre * dans le waterfall, alors que \c prepareInformation() continue de l'écarter. Elle est * donc arbitrée sans \c ChargingProcessInfo, et * \c EnergyArbitrator::evTargetPhases() doit le SAVOIR plutôt que de le déduire de * valeurs par défaut — sans quoi une borne triphasée est annoncée monophasée et le * budget du cycle est divisé par trois. */ bool internalHasProcessInfo(EvCharger *evCharger) const { return m_processInfos.contains(evCharger); } // [ETM] new // [ETM] END private slots: void updateManualSoCsWithMeter(EnergyLogs::SampleRate sampleRate, const ThingPowerLogEntry &entry); void onThingAdded(Thing *thing); void onThingRemoved(const ThingId &thingId); void onActionExecuted(const Action &action, Thing::ThingError status); void onChargingModeChanged(const ThingId &evChargerId, const ChargingInfo &chargingInfo); private: void setupRootMeter(Thing *thing); void setupEvCharger(Thing *thing); void setupPluggedInHandlers(const Thing *thing); void storeChargingInfo(const ChargingInfo &chargingInfo); void storeManualChargingParameters(const ThingId &evChargerId, bool enabled, int maxChargingCurrent, uint desiredPhaseCount); bool manualChargingEnabled(const ThingId &evChargerId) const; uint manualMaxChargingCurrent(const ThingId &evChargerId) const; uint manualDesiredPhaseCount(const ThingId &evChargerId) const; Electricity::Phases getAscendingPhasesForCount(uint phaseCount); uint getBestPhaseCount(EvCharger *evCharger, double surplusAmpere); QString chargerPhaseKey(EvCharger *evCharger) const; EnergyManager *m_energyManager = nullptr; ThingManager *m_thingManager = nullptr; SpotMarketManager *m_spotMarketManager = nullptr; EnergyManagerConfiguration *m_configuration = nullptr; QHash m_chargingSchedules; QHash m_processInfos; QHash m_chargingActions; QDateTime m_lastSpotMarketPlanning; // Overload protection QHash m_overloadProtectionActive; uint m_phasePowerConsumptionLimit = 25; double m_acquisitionTolerance = 0.5; double m_batteryLevelConsideration = 0.9; QHash m_chargingInfos; RootMeter *m_rootMeter = nullptr; QHash m_evChargers; }; #endif // SMARTCHARGINGMANAGER_H