Patrick Schurig 312a2484ae [3c-5] watchdog L2 : QTimer fraîcheur compteur + mode dégradé conservateur
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>
2026-06-08 16:43:28 +02:00

167 lines
7.3 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 *> &registeredEvChargers() 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 &currentDateTime) 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.
};