QTimer 30s indépendant des signaux ; m_lastMeterUpdate picoté sur powerBalanceChanged.
Silence >90s → mode dégradé (appliqué à la TRANSITION uniquement) :
- ECS palier 0 force=true ;
- EV : clamp courant minimum SEULEMENT si déjà en charge (pas d'activation forcée ;
"jamais 0 A si branché" relève du failsafe L1, pas du repli logiciel).
update() suspend la planification + le dispatch tant que m_degradedMode (sécurité L4
en position 3 reste active) → pas de rallumage sur le cache d'un compteur mort, pas
d'oscillation. Reprise au retour du compteur.
SAFETY.md §L2 : nuance maintenu/démarré + suspension planification. AGENTS.md morceau 7 :
exiger ECS reste à 0 sur plusieurs cycles. SG-Ready/Batterie déférés 3e/3f ;
flag degradedMode exposé en 3c-6. Build 0 erreur / 0 warning.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
167 lines
7.3 KiB
C++
167 lines
7.3 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||
#pragma once
|
||
|
||
#include "../smartchargingmanager.h"
|
||
#include "scheduler/ischeduler.h"
|
||
#include "types/surpluscontext.h"
|
||
#include "types/plan.h"
|
||
|
||
#include <QDateTime>
|
||
|
||
class QTimer;
|
||
|
||
class EvAdapter;
|
||
class EcsRelayAdapter;
|
||
class RuleBasedScheduler;
|
||
|
||
/*!
|
||
* \brief Arbitre central ETM — remplace SmartChargingManager::update() (ETM_ARBITRATOR).
|
||
*
|
||
* Hérite de SmartChargingManager pour conserver la compatibilité API complète avec
|
||
* NymeaEnergyJsonHandler sans modifier le code amont.
|
||
* Seul update() est surchargé : préparation → sécurité → planificateur → adapters.
|
||
*
|
||
* \invariant UN seul arbitre : EnergyArbitrator décide, les EvAdapter exécutent (règle 1).
|
||
* \invariant verifyOverloadProtection() est toujours appelée avant la planification (règle 4).
|
||
* \invariant Toute LoadAction transmise aux adapters a un \c reason non vide (règle 7).
|
||
* \invariant L'absence du root meter n'empêche pas le démarrage — cycle ignoré silencieusement.
|
||
*/
|
||
class EnergyArbitrator : public SmartChargingManager
|
||
{
|
||
Q_OBJECT
|
||
public:
|
||
explicit EnergyArbitrator(EnergyManager *energyManager, ThingManager *thingManager,
|
||
SpotMarketManager *spotMarketManager,
|
||
EnergyManagerConfiguration *configuration,
|
||
QObject *parent = nullptr);
|
||
|
||
/*!
|
||
* \brief Déclenche planSurplusCharging() (protégée) — appelé par RuleBasedScheduler.
|
||
* \param now Instant courant du cycle.
|
||
*/
|
||
void runSurplusPlanning(const QDateTime &now);
|
||
|
||
/*!
|
||
* \brief Déclenche planSpotMarketCharging() (protégée) — appelé par RuleBasedScheduler.
|
||
* \param now Instant courant du cycle.
|
||
*/
|
||
void runSpotMarketPlanning(const QDateTime &now);
|
||
|
||
/*!
|
||
* \brief Actions planifiées (résultat de runSurplus/SpotMarket).
|
||
* \return Référence constante vers la table EvCharger* → ChargingActions.
|
||
* \note Valide seulement après runSurplusPlanning() / runSpotMarketPlanning().
|
||
*/
|
||
const QHash<EvCharger *, ChargingActions> &scheduledActions() const;
|
||
|
||
/*!
|
||
* \brief Pont d'exécution pour EvAdapter — délègue à executeChargingAction() protégée.
|
||
* \param charger Borne EV cible.
|
||
* \param action ChargingAction à appliquer.
|
||
* \param now Instant de l'action (pour les locks anti-rebond).
|
||
*/
|
||
void doExecuteChargingAction(EvCharger *charger, const ChargingAction &action, const QDateTime &now);
|
||
|
||
/*!
|
||
* \brief Liste des EvCharger enregistrés (lecture seule).
|
||
* \return Table ThingId → EvCharger*.
|
||
*/
|
||
const QHash<ThingId, EvCharger *> ®isteredEvChargers() const;
|
||
|
||
/*!
|
||
* \brief Root meter courant.
|
||
* \return Pointeur ou nullptr si aucun compteur principal n'est enregistré.
|
||
*/
|
||
RootMeter *registeredRootMeter() const;
|
||
|
||
/*!
|
||
* \brief Enregistre un EcsRelayAdapter pour inclusion dans le contexte et le dispatch.
|
||
*
|
||
* Appelé par le test (setup) ou la configuration de production.
|
||
* L'adaptateur est adopté comme enfant Qt de l'arbitre.
|
||
* \param adapter Adaptateur à enregistrer. Son \c descriptor().id doit être unique.
|
||
*/
|
||
void registerEcsAdapter(EcsRelayAdapter *adapter);
|
||
|
||
protected:
|
||
/*!
|
||
* \brief Boucle principale ETM — surcharge SmartChargingManager::update().
|
||
*
|
||
* Ordre garanti :
|
||
* 1. updateManualSoCsWithoutMeter()
|
||
* 2. prepareInformation()
|
||
* 3. verifyOverloadProtection() + verifyOverloadProtectionRecovery()
|
||
* (si \c m_degradedMode actif : retour immédiat — planification/dispatch suspendus, L2)
|
||
* 4. m_scheduler->getPlan() → log des decisionReason
|
||
* 5. applyActionsToAdapters() (ECS Stage) + adjustEvChargers() (EV) → dispatch matériel
|
||
*
|
||
* \param currentDateTime Instant courant (timer ou simulation).
|
||
*/
|
||
void update(const QDateTime ¤tDateTime) override;
|
||
|
||
private:
|
||
/*!
|
||
* \brief Construit le SurplusContext §5 : meter brut + loads EV + loads ECS.
|
||
*
|
||
* \c ctx.meter.exportW = mesure brute du compteur (AGENTS invariant 8 — aucune
|
||
* déduction interne). La déduction evReservedW est faite dans le scheduler.
|
||
*/
|
||
SurplusContext buildContext(const QDateTime &now) const;
|
||
|
||
/*!
|
||
* \brief Synchronise m_adapters avec les EvCharger actuellement enregistrés.
|
||
* Crée les adapters manquants, supprime les adapters obsolètes.
|
||
* \note Découverte ECS via interface 'ecsrelay' ThingManager — déféré 3g config.
|
||
* En beta, les EcsRelayAdapters sont enregistrés via registerEcsAdapter().
|
||
*/
|
||
void syncAdapters();
|
||
|
||
/*!
|
||
* \brief Applique les actions d'un Slot aux LoadAdapters non-EV (ECS en 3c).
|
||
*
|
||
* Itère \c slot.actions et dispatche chaque action \c kind==Stage vers le
|
||
* EcsRelayAdapter correspondant (\c m_ecsAdapters[action.loadId]). Les actions EV
|
||
* (\c kind==Setpoint) restent dispatchées par \c adjustEvChargers() amont jusqu'à 3g.
|
||
* L'adaptateur écrête/verrouille lui-même (anti-rebond) et ignore toute action sans
|
||
* \c reason ou de kind non supporté — aucune décision ici (règle 2).
|
||
* \param slot Créneau courant retourné par le scheduler.
|
||
*/
|
||
void applyActionsToAdapters(const Slot &slot);
|
||
|
||
/*!
|
||
* \brief Tick du watchdog L2 (SAFETY.md §L2) — piloté par \c m_meterWatchdog (QTimer 30 s).
|
||
*
|
||
* Si \c QDateTime::currentDateTime() − \c m_lastMeterUpdate dépasse 90 s, déclenche
|
||
* \c applyDegradedMode(). Indépendant des signaux compteur : reste actif précisément
|
||
* quand le compteur est muet (le signal \c powerBalanceChanged ne fire plus).
|
||
* \note Tant que \c m_lastMeterUpdate est invalide (aucune mesure reçue depuis le
|
||
* démarrage), aucun mode dégradé n'est déclenché (invariant root meter absent).
|
||
*/
|
||
void onMeterWatchdogTick();
|
||
|
||
/*!
|
||
* \brief Applique les consignes de repli L2 (SAFETY.md §L2, Variante B).
|
||
*
|
||
* Repli CONSERVATEUR (n'initie aucune charge) : ECS → palier 0 \c force=true (bypass
|
||
* anti-rebond) ; EV en charge → clamp courant minimum borne ; EV branché non chargeant
|
||
* ou débranché → aucune action (planification en cours respectée ; "jamais 0 A si
|
||
* branché" relève du failsafe L1). SG-Ready/Batterie : repli ajouté à l'arrivée de
|
||
* leurs adaptateurs (3e/3f). Positionne \c m_degradedMode.
|
||
*
|
||
* \note Appelé une seule fois à la TRANSITION vers le mode dégradé. Ensuite \c update()
|
||
* suspend la planification, donc les consignes tiennent sans ré-émission par tick.
|
||
* \param now Instant courant (locks anti-rebond des bornes EV).
|
||
*/
|
||
void applyDegradedMode(const QDateTime &now);
|
||
|
||
RuleBasedScheduler *m_scheduler = nullptr;
|
||
QHash<QString, EvAdapter *> m_adapters; //!< loadId (ThingId string) → EvAdapter*.
|
||
QHash<QString, EcsRelayAdapter *> m_ecsAdapters; //!< loadId → EcsRelayAdapter*.
|
||
|
||
// --- L2 watchdog fraîcheur compteur (SAFETY.md §L2) ---
|
||
QTimer *m_meterWatchdog = nullptr; //!< Tick 30 s, indépendant des signaux compteur.
|
||
QDateTime m_lastMeterUpdate; //!< Horodatage du dernier powerBalanceChanged.
|
||
bool m_degradedMode = false; //!< Vrai si les consignes de repli L2 sont actives.
|
||
};
|