Troisième défaut latent de 3g-1, et le seul des trois qui touche la sécurité.
Corrigé à part du lot 3g-2, pour pouvoir être relu ou rétroporté seul.
L'écrêtage d'une consigne EV par la limite de phase vivait dans
planSurplusCharging() : `newValue = qMin(newRawValue, allowanceOnRootMeter)`.
En transplantant la décision de surplus dans le waterfall, 3g-1 a laissé cet
écrêtage derrière lui. Sans conséquence tant que rien n'était exécuté — mais
la décision du waterfall est censée l'être, et alors plus rien ne borne
l'allocation avant qu'elle ne parte au matériel. Le seul filet restant serait
verifyOverloadProtection(), APRÈS coup.
Le plafond redevient donc une borne DÉCLARÉE de l'adaptateur
(LoadContext.declared.maxPowerW), ce qui le place là où la règle 4 d'AGENTS le
veut : le waterfall ne peut pas allouer au-delà, au lieu d'être rattrapé.
Deux points de sécurité tenus explicitement :
- Le plafond ne descend JAMAIS sous le plancher de la borne. C'est la règle
de l'amont reprise mot pour mot (`if (newValueInt <= minValue) newValueInt
= minValue`) et la répartition des rôles qu'elle porte : la limite de
phase RABAISSE la borne à son minimum, elle ne la coupe pas. Couper est le
geste de la couche L4, qui tourne en temps réel sur powerBalanceChanged et
garde le dernier mot après le dispatch. Écrêter jusqu'à 0 ici ferait
décider la coupure par la stratégie.
- L'allowance est la place LIBRE, sans rendre à la borne ce qu'elle tire
déjà. Le retour serait plus juste en régime établi, mais ferait disparaître
la réponse à la surcharge : sur une phase en dépassement, la place libre
est NÉGATIVE, et c'est ce signe qui ramène la borne à son minimum.
Nouveau motif PHASE_LIMIT {limitW, requiredW} : quand la place libre tombe
sous ce que la borne exige, c'est la limite de phase qui interdit, pas le
budget. Dire « budget sous le plancher » enverrait attendre le soleil là où
il faut regarder l'abonnement et les autres consommateurs — deux causes, deux
gestes (règle 7-b).
Suite simulation `run` 16/16, testOverloadProtectionEcoMode vert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
250 lines
12 KiB
C++
250 lines
12 KiB
C++
// 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 <https://www.gnu.org/licenses/>.
|
||
*
|
||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||
|
||
#ifndef SMARTCHARGINGMANAGER_H
|
||
#define SMARTCHARGINGMANAGER_H
|
||
|
||
#include <QObject>
|
||
#include <QTimer>
|
||
|
||
#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 <integrations/thingmanager.h>
|
||
#include <hardware/electricity.h>
|
||
|
||
// from libnymea-energy
|
||
#include <energymanager.h>
|
||
|
||
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);
|
||
|
||
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() — accesseurs ETM neufs, documentés ci-dessous.
|
||
// 5. bestPhaseCountFor(), phasesForCount() — passe-plats ETM vers deux règles amont
|
||
// privées, documentés ci-dessous.
|
||
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); }
|
||
|
||
// [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<ThingId, EvCharger *> &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<EvCharger *, ChargingActions> &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
|
||
// [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<EvCharger *, ChargingSchedules> m_chargingSchedules;
|
||
QHash<EvCharger *, ChargingProcessInfo> m_processInfos;
|
||
QHash<EvCharger *, ChargingActions> m_chargingActions;
|
||
|
||
QDateTime m_lastSpotMarketPlanning;
|
||
|
||
// Overload protection
|
||
QHash<EvCharger *, bool> m_overloadProtectionActive;
|
||
|
||
uint m_phasePowerConsumptionLimit = 25;
|
||
|
||
double m_acquisitionTolerance = 0.5;
|
||
double m_batteryLevelConsideration = 0.9;
|
||
|
||
QHash<ThingId, ChargingInfo> m_chargingInfos;
|
||
|
||
RootMeter *m_rootMeter = nullptr;
|
||
QHash<ThingId, EvCharger *> m_evChargers;
|
||
|
||
};
|
||
|
||
#endif // SMARTCHARGINGMANAGER_H
|