Défaut de 3g-1, resté latent parce que sa sortie n'atteignait que la télémétrie. Il est corrigé à part du lot 3g-2 pour pouvoir être relu, contesté ou rétroporté sans lui. EvAdapter interrogeait EvCharger::canSetPhaseCount(), qui dit seulement que la borne PORTE l'état desiredPhaseCount. La vraie question — la borne ET la voiture acceptent-elles 1 et 3 phases — est tranchée une fois par cycle par prepareInformation(), qui en tire canSwitchPhaseCount et le couple min/maxPossiblePhaseCount. Les deux prédicats ont divergé : là où le proxy appliquait `qMin(carPhaseCount, getBestPhaseCount(...))`, 3g-1 n'a repris que l'appel. Conséquence : une borne dont la combinaison ne permet qu'une phase se voyait viser 3 phases. Son plancher publié triplait (1,4 kW → 4,1 kW) sans que rien ne puisse jamais le franchir — la borne restait éternellement « sous son plancher », avec un motif juste et une cause fausse. Second défaut, dans applyAction() : le nombre de phases demandé était borné par m_charger->phaseCount(), qui est l'état COURANT de la borne. Une borne en monophasé ne pouvait donc jamais être commandée en triphasé, et le plancher publié pour trois phases n'aurait de toute façon pas pu être servi. evTargetPhases() cesse d'être un passe-plat et lit les bornes du manager, via un accesseur [ETM] read-only de plus (internalProcessInfo). L'arbitre ne redérive rien : une seconde version de cette règle divergerait de la première au premier ajustement, ce qui est exactement ce qui vient d'arriver. Suite charging 16/16, simulation `run` 16/16, testLoadTelemetryRpc vert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
239 lines
11 KiB
C++
239 lines
11 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.
|
||
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); }
|
||
|
||
// [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
|