Deux champs optionnels de même forme sur LoadConfig — o:meterThingId, o:sensorThingId — et leur publication en télémétrie. Rien d'autre : l'arbitre ne s'en sert pas pour décider, et le §12 comme le §10 restent à faire. « Aucun effet sur les décisions » est vrai par CONSTRUCTION, pas par discipline : les adaptateurs ne voient jamais ces Things. attachMeasurements() les résout au moment de publier, dans les deux chemins de télémétrie — y compris le mode dégradé, où un exploitant veut précisément voir ce que la charge fait pendant que l'arbitre ne décide plus rien. measuredW est publié À CÔTÉ de allocatedW et jamais fondu dedans : l'un est ce que le waterfall a alloué, l'autre ce qu'un compteur a vu. Les confondre rendrait la somme des allocations irréconciliable avec budget.allocatedW — et injecter une mesure en retard sur la commande réintroduirait l'oscillation que le recrédit a supprimée. claimedThingIds() les exclut, et l'exclusion est maintenant commentée sur place : LM-201 vaut pour ce qui commande, ceux-ci mesurent. Sans ce commentaire, la symétrie avec relays[] invitait à les ajouter — ce qui ferait refuser un compteur divisionnaire partagé ou une sonde de pièce commune. spec 0.2.1 : la précondition de livraison du §10 (extension du délestage au waterfall, même lot ou avant, jamais après) est écrite comme CONDITION et non comme conseil — la protection de surcharge ne déleste que les bornes, et des planchers éco qui s'additionnent un matin sans soleil sur un branchement 6 kVA feraient disjoncter l'installation. Et LM-1008 tranche la cible de confort : saisie en degrés, frontière de l'arbitre en énergie, conversion à la couche domaine — et aucune conversion nécessaire aujourd'hui, un comparateur suffit, c'est l'arrêt qui compte et pas le dosage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
391 lines
21 KiB
C++
391 lines
21 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"
|
||
// Inclus (et non déclaré en avant) : m_builtFrom stocke des LoadConfig PAR VALEUR — c'est ce
|
||
// qui permet au rebuild incrémental (ECS-412) de comparer l'ancienne et la nouvelle config.
|
||
#include "types/loadconfig.h"
|
||
|
||
#include <QDateTime>
|
||
#include <QVariantMap>
|
||
|
||
class QTimer;
|
||
|
||
class EvAdapter;
|
||
class SgReadyAdapter;
|
||
class EtmVariableLoadAdapter;
|
||
class RelayRouter;
|
||
class ILoadAdapter;
|
||
class LoadConfigStore;
|
||
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 EtmVariableLoadAdapter (ECS/routeur, interface \c etmvariableload)
|
||
* pour inclusion dans le contexte et le dispatch \c Setpoint (W).
|
||
* \param adapter Adaptateur à enregistrer ; son \c descriptor().id doit être unique.
|
||
* Adopté comme enfant Qt de l'arbitre. Appelé par le test (setup) ou — en T4 — la
|
||
* construction depuis \c LoadConfig.
|
||
* \note Le dispatch distingue un \c Setpoint etmvariableload d'un \c Setpoint EV par le
|
||
* \c loadId : seul l'EV n'est PAS dans \c m_loadAdapters (proxy amont jusqu'à 3g).
|
||
*/
|
||
void registerEtmVariableLoadAdapter(EtmVariableLoadAdapter *adapter);
|
||
|
||
/*!
|
||
* \brief Enregistre un RelayRouter (ECS multipalier, rév. 3) dans la même table d'adaptateurs.
|
||
* \param adapter Routeur à enregistrer ; \c descriptor().id unique. Adopté enfant Qt.
|
||
* Appelé par le test (setup) ou — en prod — la construction depuis \c LoadConfig (relays[]).
|
||
*/
|
||
void registerRelayRouter(RelayRouter *adapter);
|
||
|
||
/*!
|
||
* \brief Branche le store de config charge pilotée : construit les adaptateurs
|
||
* \c etmvariableload depuis la config et les reconstruit à chaque \c changed().
|
||
*
|
||
* Les charges \c enabled==false ne sont PAS construites (exclues de l'arbitrage,
|
||
* contrat §9). Depuis le lot B-bis, **toutes** les charges arbitrées passent par ici — y
|
||
* compris la PAC SG-Ready, qui n'a plus d'exemplaire codé en dur.
|
||
* \param store Store persistant (propriété de l'appelant ; non adopté).
|
||
*/
|
||
void setLoadConfigStore(LoadConfigStore *store);
|
||
|
||
/*!
|
||
* \brief Lève le verrou de défaut d'une charge pilotée (ECS-410).
|
||
* \param loadId Identifiant logique de la charge.
|
||
* \return Vrai si la charge existe ; faux si l'identifiant est inconnu.
|
||
* \note Acte délibéré de l'opérateur, exposé par \c NymeaEnergy.ClearLoadFault. Le défaut
|
||
* ne se lève jamais seul : c'est ce qui empêche \c available de clignoter.
|
||
*/
|
||
bool clearLoadFault(const QString &loadId);
|
||
|
||
/*!
|
||
* \brief Mode dégradé L2 actif (compteur muet > 90 s) — override de SmartChargingManager.
|
||
* \return \c true tant que les consignes de repli L2 tiennent ; \c false en régime normal.
|
||
* \note Exposé dans la notification \c NymeaEnergy.ChargingSchedulesChanged (champ
|
||
* \c degradedMode), émise aussi aux transitions de ce flag.
|
||
*/
|
||
bool degradedMode() const override { return m_degradedMode; }
|
||
|
||
/*!
|
||
* \brief Enregistre une mesure fraîche du compteur à l'instant \p now (logique L2).
|
||
*
|
||
* Met à jour \c m_lastMeterUpdate et, si le mode dégradé était actif, en sort
|
||
* (\c degradedMode=false + notification). \p now = temps de cycle.
|
||
* \note Logique injectable (temps en paramètre) — en production appelée par le
|
||
* handler \c powerBalanceChanged ; en simulation/test appelée directement. Le
|
||
* déclencheur réel (signal) est câblé sous \c \#ifndef ENERGY_SIMULATION.
|
||
*/
|
||
void recordMeterUpdate(const QDateTime &now);
|
||
|
||
/*!
|
||
* \brief Évalue la fraîcheur du compteur à \p now et bascule en mode dégradé si muet >90 s.
|
||
*
|
||
* Si \c now − \c m_lastMeterUpdate > 90 s et pas déjà dégradé → \c applyDegradedMode().
|
||
* Appliqué à la TRANSITION uniquement (idempotent ensuite). \p now = temps de cycle.
|
||
* \note Logique injectable — en production appelée par \c onMeterWatchdogTick() (QTimer
|
||
* horloge murale, indépendant car le compteur muet fige aussi \c update()) ; en
|
||
* simulation/test appelée directement avec le temps simulé. Symétrique de
|
||
* \c simulationCallUpdate : déclencheur réel en prod, logique testable par injection.
|
||
*/
|
||
void evaluateMeterFreshness(const QDateTime &now);
|
||
|
||
/*!
|
||
* \brief Dernier instantané de télémétrie d'arbitrage — CE QUI A ÉTÉ PUBLIÉ, pas un recalcul.
|
||
*
|
||
* \return La charge utile de \c NymeaEnergy.GetLoadTelemetry : \c timestamp (fin du dernier
|
||
* cycle d'arbitrage), \c degradedMode, \c budget, \c loads[]. Vide tant qu'aucun cycle
|
||
* n'a eu lieu (démarrage) — voir la note ci-dessous.
|
||
*
|
||
* \note **Snapshot ≡ notification, par construction.** C'est le MÊME objet que celui émis
|
||
* par \c loadTelemetryChanged() ; il n'est pas recalculé à la demande. Un recalcul
|
||
* donnerait des verrous rafraîchis sous un \c timestamp périmé — un état qui n'a jamais
|
||
* existé, et que le client ne pourrait rapprocher d'aucune notification.
|
||
* \note **Vide avant le premier cycle.** Un instantané forgé au démarrage (budget nul,
|
||
* aucune charge) se lirait comme « cycle exécuté, rien à allouer » — exactement le
|
||
* mensonge que \c timestamp existe pour empêcher. La carte vide est la seule réponse
|
||
* vraie ; le handler la rend telle quelle, sans \c timestamp (règle « omis, jamais nul »).
|
||
*/
|
||
QVariantMap loadTelemetry() const { return m_telemetry; }
|
||
|
||
/*!
|
||
* \brief Battement de cœur : republie l'instantané COURANT sans condition ni recalcul.
|
||
*
|
||
* C'est ce qui rend un arbitre FIGÉ détectable : les notifications continuent d'arriver
|
||
* tandis que leur \c timestamp cesse d'avancer. Sans battement, « installation stable » et
|
||
* « moteur arrêté » se ressemblent — un silence, donc un silence ambigu (règle 7-c).
|
||
*
|
||
* \note Logique injectable (symétrique de \c evaluateMeterFreshness) : en production
|
||
* appelée par un \c QTimer d'horloge murale câblé sous \c \#ifndef ENERGY_SIMULATION ;
|
||
* en simulation/test appelée directement. Le \c QTimer ne peut pas dépendre du cycle
|
||
* d'arbitrage : c'est précisément l'arrêt de ce cycle qu'il sert à révéler.
|
||
* \note Le battement ne double pas une notification fraîche : chaque publication RELANCE
|
||
* le timer. La règle vit donc dans le timer, et cette méthode reste inconditionnelle —
|
||
* testable sans attendre une période entière.
|
||
*/
|
||
void publishTelemetryHeartbeat();
|
||
|
||
/*!
|
||
* \brief Vrai si deux configs décrivent le MÊME matériel (câblage, paliers, verrous).
|
||
*
|
||
* \param a Config ayant servi à construire l'adaptateur. \param b Nouvelle config.
|
||
* \return Vrai si l'adaptateur peut être conservé — ECS-412. Rang, besoins et libellé
|
||
* sont volontairement ignorés : ils se mettent à jour en place.
|
||
*
|
||
* \note **Publique à dessein.** Ce prédicat décide si une écriture coûte un geste
|
||
* MATÉRIEL — contacts ouverts par l'état sûr, verrous réarmés à froid — ou une simple
|
||
* mise à jour en place. C'est à ce titre un élément de contrat, que le client annonce
|
||
* à l'installateur avant d'enregistrer, et il est éprouvé comme tel
|
||
* (\c tests/auto/loadmodel). Statique et sans état : l'exposer n'ouvre rien.
|
||
*/
|
||
static bool sameHardware(const LoadConfig &a, const LoadConfig &b);
|
||
|
||
signals:
|
||
/*!
|
||
* \brief Émise quand l'état runtime d'arbitrage a CHANGÉ, ou au battement de cœur.
|
||
* \param telemetry Charge utile identique à celle de \c loadTelemetry().
|
||
* \note « Changé » exclut les grandeurs qui bougent en continu : les secondes restantes
|
||
* d'un verrou et les paramètres numériques d'un motif ne déclenchent rien, et une
|
||
* variation d'allocation inférieure au seuil non plus. Voir
|
||
* \c telemetrySignificantlyDiffers().
|
||
*/
|
||
void loadTelemetryChanged(const QVariantMap &telemetry);
|
||
|
||
|
||
|
||
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() (etmvariableload Setpoint W + SG-Ready State) + adjustEvChargers() (EV) → dispatch
|
||
*
|
||
* \param currentDateTime Instant courant (timer ou simulation).
|
||
*/
|
||
void update(const QDateTime ¤tDateTime) override;
|
||
|
||
private:
|
||
/*!
|
||
* \brief Construit le SurplusContext §5 : meter brut + loads EV + SG-Ready.
|
||
*
|
||
* \c ctx.meter.exportW = mesure brute du compteur (AGENTS invariant 8 — aucune
|
||
* déduction interne). La déduction evReservedW est faite dans le scheduler.
|
||
* \param now Temps de cycle (\c ctx.timestamp) : pose \c ctx.timestamp et sert de SOURCE
|
||
* UNIQUE à la fenêtre de verrou PAC (\c minState/maxState) calculée par le SgReadyAdapter —
|
||
* cohérence décision (scheduler) / exécution (applyAction). Les charges etmvariableload
|
||
* (ECS/routeur) n'ont pas de fenêtre côté moteur (verrous dans le thing).
|
||
*/
|
||
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 Les charges non-EV, elles, viennent toutes de \c LoadConfigStore.
|
||
*/
|
||
void syncAdapters();
|
||
|
||
/*!
|
||
* \brief Applique les actions d'un Slot aux LoadAdapters non-EV.
|
||
*
|
||
* Itère \c slot.actions et dispatche par \c loadId dans la table UNIQUE des charges
|
||
* arbitrées ; le polymorphisme \c ILoadAdapter absorbe le mécanisme. Les actions EV
|
||
* (\c Setpoint) restent dispatchées par \c adjustEvChargers() amont jusqu'à 3g — l'EV
|
||
* n'est pas dans cette table. L'adaptateur écrête/verrouille lui-même 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.
|
||
* \param now Temps de cycle (\c ctx.timestamp) — transmis aux adaptateurs pour une
|
||
* évaluation des verrous cohérente avec celle vue par le scheduler.
|
||
*/
|
||
void applyActionsToAdapters(const Slot &slot, const QDateTime &now);
|
||
|
||
|
||
/*!
|
||
* \brief (Re)construit \c m_loadAdapters depuis \c m_loadConfigStore (rév. 3).
|
||
* Crée, par config \c enabled==true, un \c SgReadyAdapter (si \c sgReady), un
|
||
* \c RelayRouter (si \c relays[]) ou un \c EtmVariableLoadAdapter (sinon) — une branche
|
||
* de fabrique par mécanisme, LM-301. La distinction de TYPE vit ici ; au-dessus tout est
|
||
* \c ILoadAdapter. Reconstruction INCRÉMENTALE (ECS-412) ; ce qui sort de la config passe
|
||
* par \c applySafeState() avant destruction (ECS-413). Appelé au branchement du store et
|
||
* à chaque \c LoadConfigStore::changed().
|
||
*/
|
||
void rebuildLoadAdapters();
|
||
|
||
/*!
|
||
* \brief Déclencheur RÉEL du watchdog L2 (SAFETY.md §L2) — slot de \c m_meterWatchdog
|
||
* (QTimer 30 s, horloge murale ; câblé sous \c \#ifndef ENERGY_SIMULATION).
|
||
*
|
||
* Délègue simplement à \c evaluateMeterFreshness(QDateTime::currentDateTime()) : la
|
||
* LOGIQUE (seuil 90 s, bascule en dégradé) est dans cette méthode injectable, le QTimer
|
||
* n'est que le battement. Indépendant des signaux compteur : reste actif précisément
|
||
* quand le compteur est muet (le signal \c powerBalanceChanged ne fire plus, et
|
||
* \c update() — piloté par le compteur — s'arrête aussi). Voir \c evaluateMeterFreshness().
|
||
*/
|
||
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);
|
||
|
||
/*!
|
||
* \brief Construit l'instantané de télémétrie du cycle qui vient de s'exécuter.
|
||
*
|
||
* \param plan Plan retourné par le scheduler — source de \c budget (jamais recalculé ici).
|
||
* \param slot Créneau courant : ses actions donnent l'ordre du waterfall ET les motifs.
|
||
* \param now Temps de cycle, publié tel quel comme \c timestamp.
|
||
* \note Les charges EV sont EXCLUES : décidées par l'amont en amont du waterfall (décision
|
||
* B), leur puissance est réservée dans \c budget.evReservedW, pas allouée depuis le
|
||
* budget publié. Les inclure ferait une somme \c loads[] qui ne retomberait pas sur
|
||
* \c budget.allocatedW.
|
||
*/
|
||
void buildTelemetry(const Plan &plan, const Slot &slot, const QDateTime &now);
|
||
|
||
/*!
|
||
* \brief Construit l'instantané du mode dégradé L2 et le publie sans condition.
|
||
* \param now Instant de la bascule — c'est bien une décision, elle se date.
|
||
* \note Sans cet appel, un client qui se connecte APRÈS la bascule ne lirait jamais
|
||
* \c degradedMode : \c update() rend la main avant toute publication tant que le repli
|
||
* tient. C'est la reprise à froid, et c'est la famille ECS-411.
|
||
*/
|
||
void buildDegradedTelemetry(const QDateTime &now);
|
||
|
||
/*!
|
||
* \brief Ajoute à une entrée de télémétrie les MESURES de la charge, si elle en déclare.
|
||
*
|
||
* §11 (LM-1101, LM-1103). Deux champs optionnels de \c LoadConfig — \c meterThingId et
|
||
* \c sensorThingId — sont résolus ici, au moment de PUBLIER, et nulle part ailleurs.
|
||
*
|
||
* \note **Les adaptateurs ne voient jamais ces Things.** C'est ce qui rend « aucun effet
|
||
* sur les décisions » vrai par construction plutôt que par discipline : la mesure ne
|
||
* traverse aucun chemin d'arbitrage, elle est lue au dernier moment et publiée.
|
||
* \note Omis, jamais nul : un Thing absent de la configuration nymea, ou un état que le
|
||
* Thing ne porte pas, ne produit **aucune clé** — pas un zéro, qui se lirait « mesuré 0 W »
|
||
* là où la vérité est « pas de mesure ».
|
||
*
|
||
* \param entry Entrée de charge en cours de construction, modifiée en place.
|
||
* \param loadId Identifiant de la charge, clé dans le magasin de configuration.
|
||
*/
|
||
void attachMeasurements(QVariantMap &entry, const QString &loadId) const;
|
||
|
||
/*!
|
||
* \brief Publie \c m_telemetry si elle diffère significativement de la dernière publiée.
|
||
* \param force Vrai pour publier sans condition (bascule de mode dégradé, battement).
|
||
*/
|
||
void publishTelemetry(bool force);
|
||
|
||
/*!
|
||
* \brief Vrai si deux instantanés diffèrent assez pour mériter une notification.
|
||
* \param a Dernier instantané publié. \param b Instantané courant.
|
||
* \return Vrai sur tout changement d'état QUALITATIF (mode dégradé, jeu de charges,
|
||
* disponibilité, code de défaut, nature du verrou, mécanisme, CODE de motif) ou sur une
|
||
* variation d'allocation dépassant le seuil.
|
||
* \note Sont volontairement IGNORÉS : les secondes restantes d'un verrou (elles décroissent
|
||
* à chaque seconde), et les paramètres numériques d'un motif (le budget bouge à chaque
|
||
* cycle). Les comparer ferait notifier en permanence et le seuil ne servirait à rien.
|
||
*/
|
||
static bool telemetrySignificantlyDiffers(const QVariantMap &a, const QVariantMap &b);
|
||
|
||
RuleBasedScheduler *m_scheduler = nullptr;
|
||
QHash<QString, EvAdapter *> m_adapters; //!< loadId (ThingId string) → EvAdapter*.
|
||
//! loadId → charge ARBITRÉE, tous mécanismes confondus (relay-router | etmvariableload |
|
||
//! sg-ready). Table UNIQUE, et c'est un invariant de contrat : toutes ses entrées viennent
|
||
//! de \c LoadConfigStore, donc l'ensemble publié par \c NymeaEnergy.GetLoadTelemetry est
|
||
//! exactement celui de \c GetLoadConfig. Une seconde table existait pour la PAC de banc
|
||
//! codée en dur ; elle a disparu avec elle (lot B-bis), et avec elle la possibilité qu'une
|
||
//! charge soit arbitrée sans être configurable.
|
||
QHash<QString, ILoadAdapter *> m_loadAdapters;
|
||
//! loadId → config AYANT SERVI à construire l'adaptateur. Permet le rebuild incrémental
|
||
//! (ECS-412) : on ne reconstruit que si le matériel a changé.
|
||
QHash<QString, LoadConfig> m_builtFrom;
|
||
ThingManager *m_tm = nullptr; //!< ThingManager (pour construire les adaptateurs config).
|
||
LoadConfigStore *m_loadConfigStore = nullptr; //!< Store config charge pilotée (non adopté).
|
||
|
||
// --- 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.
|
||
|
||
// --- Frontière de télémétrie (NymeaEnergy.GetLoadTelemetry / LoadTelemetryChanged) ---
|
||
QVariantMap m_telemetry; //!< Instantané courant ; vide avant le premier cycle.
|
||
QVariantMap m_publishedTelemetry; //!< Dernier instantané ÉMIS — base de comparaison du seuil.
|
||
QDateTime m_lastTelemetryPublish;//!< Horloge murale du dernier envoi (cadence du battement).
|
||
QTimer *m_telemetryHeartbeat = nullptr; //!< Battement 60 s, indépendant du cycle d'arbitrage.
|
||
};
|