// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync #pragma once #include #include "../types/loadaction.h" #include "../types/loaddescriptor.h" #include "../types/surpluscontext.h" /*! * \brief Vue runtime minimale exposée par un adaptateur à l'arbitre. */ struct LoadTelemetry { double currentPowerW = 0; //!< Puissance mesurée (W). bool available = true; //!< Faux si l'appareil nymea est absent ou en erreur. QDateTime lastActionAt; //!< Dernier instant où applyAction() a produit un effet. }; /*! * \brief Interface pure des adaptateurs de charge. * * Les implémentations concrètes héritent de QObject + ILoadAdapter et déclarent leurs * propres signaux (telemetryChanged, descriptorChanged). * * \invariant Les adaptateurs EXÉCUTENT, ils ne décident pas (AGENTS règle 2). * \invariant applyAction() écrête les valeurs selon les limites matérielles réelles * (second filet après l'écrêtage de l'arbitre). * \invariant applyAction() avec \c reason vide doit être rejetée silencieusement. * \invariant Les méthodes non-applyAction() retournent immédiatement (pas de I/O bloquant). * \invariant **Temps = paramètre, jamais l'horloge.** Toute logique temporelle d'un * adaptateur (verrous minOn/minOff, fenêtres, fraîcheur…) utilise EXCLUSIVEMENT le * \c now (= \c ctx.timestamp) reçu en paramètre de \c toLoadContext()/applyAction(). * JAMAIS \c QDateTime::currentDateTime(). C'est cette source unique, partagée avec le * scheduler, qui rend impossible toute divergence décision/exécution et qui rend la * logique injectable en simulation. Contrat pour tout futur adaptateur (SgReady, Battery). */ class ILoadAdapter { public: virtual ~ILoadAdapter() = default; /*! * \brief Description statique de la charge : capacités, limites, priorité, needs. * \return LoadDescriptor construit depuis la configuration matérielle. * \note Peut être rappelé à chaque cycle — l'implémentation doit être légère. */ virtual LoadDescriptor descriptor() const = 0; /*! * \brief Télémétrie runtime (puissance, disponibilité, dernière action). * \return LoadTelemetry issue de l'état courant de l'appareil nymea. */ virtual LoadTelemetry telemetry() const = 0; /*! * \brief Construit l'entrée §5 loads[] pour le SurplusContext. * \param now Temps de cycle (\c ctx.timestamp). Source unique pour l'évaluation des * verrous (minStage/maxStage) — JAMAIS \c QDateTime::currentDateTime() côté adaptateur, * afin que décision (scheduler) et exécution (applyAction) partagent le même temps. * \return LoadContext incluant declared, limits, needs et télémétrie type-spécifique. */ virtual LoadContext toLoadContext(const QDateTime &now) const = 0; /*! * \brief Applique l'action et retourne ce qui a réellement été envoyé au matériel. * * L'arbitre a déjà écrêté selon les limites et le budget — ceci est le second filet. * * \param action Action à appliquer. Doit avoir \c reason non vide. * \param now Temps de cycle (\c ctx.timestamp) — MÊME source que toLoadContext(), * pour que l'évaluation des verrous coïncide avec celle vue par le scheduler. * \return L'action après écrêtage matériel (peut différer de l'entrée). * \note Retour silencieux sans effet si \c action.reason est vide. */ virtual LoadAction applyAction(const LoadAction &action, const QDateTime &now) = 0; /*! * \brief Met à jour les champs de configuration qui ne touchent PAS le matériel. * * Sert au rebuild INCRÉMENTAL (ECS-412) : quand seul le rang de service ou les besoins * changent, l'arbitre met à jour l'adaptateur en place au lieu de le détruire. Détruire * réarmerait les verrous — sur un ballon thermodynamique à \c minOn de 300 à 600 s, un * client qui réordonne ses priorités depuis l'app pourrait faire court-cycler son * compresseur — et provoquerait des réécritures de relais inutiles. * * \param priority Nouveau rang de service (ASC, 1 = premier servi). * \param needs Nouveaux besoins déclarés. * \warning NE DOIT jamais toucher au câblage, aux paliers ni aux verrous : ces * changements-là exigent une vraie reconstruction. * \note **Pure virtuelle à dessein**, sans implémentation par défaut. Un adaptateur qui * n'est pas construit depuis \c LoadConfig n'a rien à mettre à jour, mais il doit le * DÉCLARER : un défaut vide silencieux ferait qu'un futur adaptateur construit depuis * la config ignorerait sans bruit les changements de rang. */ virtual void updateSoftConfig(int priority, const LoadNeeds &needs) = 0; /*! * \brief Lève le verrou de défaut de la charge (ECS-410). * * Le défaut est COLLANT par conception — « cesser toute commande jusqu'à intervention ». * Sa levée est un acte délibéré de l'opérateur, jamais une expiration : sans quoi * \c available oscillerait d'un cycle à l'autre et le waterfall réagirait à chaque * battement. * * \note **Pure virtuelle à dessein.** Tous les adaptateurs ne savent pas encore tomber en * défaut — seul \c RelayRouter implémente l'échelle ECS-410 — mais chacun doit le * DÉCLARER. La généralisation est portée par **ECS-414**. */ virtual void clearFault() = 0; /*! * \brief Amène le matériel dans l'ÉTAT SÛR de cet adaptateur (ECS-413). * * Appelé quand la charge est désactivée (\c enabled: false) ou retirée de la * configuration — un acte DÉLIBÉRÉ de l'opérateur. Sans cela, l'adaptateur est détruit et * le matériel reste dans son DERNIER ÉTAT COMMANDÉ : constaté au banc le 2026-08-09, trois * relais laissés fermés juste avant une intervention de câblage. * * \param now Temps de cycle. Passe par le chemin d'action normal avec \c force = true, * comme le mode dégradé L2 : l'arrêt prime sur les verrous. * * \warning **NE PAS appeler à l'arrêt du plugin ni au redémarrage de nymead.** L'état doit * y être CONSERVÉ — c'est ce qu'ECS-411 relit au démarrage, et couper l'eau chaude à * chaque redémarrage de service serait une régression. La distinction est * intentionnelle : la désactivation est délibérée, un redémarrage ne l'est pas. * * \note L'état sûr est PROPRE à chaque adaptateur — relais ouverts pour un routeur, * consigne 0 W pour une charge continue, mais **état 2** pour une PAC SG-Ready, jamais * le blocage (\c docs/SAFETY.md). Une formulation « tout couper » serait fausse. */ virtual void applySafeState(const QDateTime &now) = 0; };