// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync #pragma once #include #include #include #include #include "iloadadapter.h" class Thing; class ThingManager; /*! * \brief Adaptateur charge à puissance pilotable — interface \c etmvariableload (contrat * INTERFACE_etmvariableload.md). Action \c kind:Setpoint en WATTS. * * Couvre les charges dont la consigne est une puissance : ECS multipalier, routeur PV / * triac, résistance modulable. \b Pas la PAC SG-Ready (modèle à états, \c SgReadyAdapter). * * \par Frontière (contrat §1 / §5) — non négociable * L'energymanager raisonne UNIQUEMENT en watts. \b Aucune résistance, relais, combinaison * ni index de palier matériel ne traverse cet adaptateur. La connaissance matérielle (quelle * résistance, quel triac, sur quelle phase), l'anti-rebond (minOn/minOff) et l'équilibrage * de phases vivent ENTIÈREMENT dans le \b thing qui implémente l'interface. Cet adaptateur * se contente d'écrire une consigne \c powerSetpoint (W) et de relire \c currentPowerW. * * \par States nymea consommés sur le thing cible (\c m_id) * - \c currentPowerW (read) : puissance réellement appliquée — juge runtime (contrat §4). * - \c powerSetpoint (write) : consigne demandée par l'energymanager. * - \c maxPowerW / \c powerLevels (read) : déclarés par le thing pour le wizard ; la * planification utilise la DÉCLARATION de config (passée au constructeur, contrat §4), * pas ces states — l'adaptateur n'écrase jamais la déclaration. * * \invariant applyAction() rejette silencieusement toute action dont \c reason est vide. * \invariant Seul le kind \c Setpoint est traité ; les autres kinds retournent sans effet. * \invariant La consigne est écrêtée à \c [0, maxPowerW] avant envoi matériel (second filet * après l'écrêtage de l'arbitre). L'arrondi au \c powerLevels (mode *fixed*) est la * responsabilité du SCHEDULER (contrat §4), pas de l'adaptateur. * \invariant **Temps = paramètre, jamais l'horloge** (cf. \c ILoadAdapter) : aucune logique * temporelle ici (les verrous vivent dans le thing) — \c now sert uniquement aux estampilles. * * \note T1 (squelette) : l'adaptateur consomme l'interface côté moteur. La déclaration de * l'interface nymea \c etmvariableload et le driver thing relèvent d'une session device dédiée. */ class EtmVariableLoadAdapter : public QObject, public ILoadAdapter { Q_OBJECT public: /*! * \brief Constructeur. * \param thingManager Gestionnaire nymea pour résoudre \p id en Thing. * \param id ThingId (string) du thing implémentant \c etmvariableload, et * identifiant logique de la charge. * \param label Nom lisible (logs, app). * \param powerLevels Paliers atteignables en W (DÉCLARÉS, triés croissants, \c 0 inclus) * en mode *fixed* ; liste vide ⇒ *dynamic* (modulation 0..maxPowerW). * \param maxPowerW Plafond physique (W) — contrat §2. * \param priority Rang dans le waterfall (contrat §5 / OPTIMIZER_PROTOCOL §5) : valeur * plus BASSE = servi en premier (rang 1 = premier servi). * \param needs Besoins déclarés (échéances, énergie min) — exposés au scheduler * via \c descriptor().needs. Vide par défaut. * \param parent Propriétaire Qt. */ explicit EtmVariableLoadAdapter(ThingManager *thingManager, const QString &id, const QString &label, const QList &powerLevels, int maxPowerW, int priority, const LoadNeeds &needs = LoadNeeds(), QObject *parent = nullptr); /*! * \brief Description statique : adapter="etmvariableload", powerLevels, maxPowerW, priority. * \return LoadDescriptor déclaratif (référence de planification, contrat §4). */ LoadDescriptor descriptor() const override; /*! * \brief Télémétrie runtime. \c currentPowerW = state \c currentPowerW MESURÉ du thing * (juge de quantification, contrat §4) ; \c available faux si le thing est absent. */ LoadTelemetry telemetry() const override; /*! * \brief Construit l'entrée loads[] §5 (adapter="etmvariableload"). * \param now Temps de cycle (\c ctx.timestamp) — estampille uniquement ; aucune fenêtre de * verrou côté moteur (l'anti-rebond vit dans le thing, contrat §2). * \return LoadContext incluant declared (powerLevels/maxPowerW) et \c currentPowerW. */ LoadContext toLoadContext(const QDateTime &now) const override; /*! * \brief Écrit la consigne \c powerSetpoint (W) sur le thing. * \param action LoadAction de kind \c Setpoint ; consigne lue dans \c action.powerW. Autres * kinds : retour sans effet. * \param now Temps de cycle (\c ctx.timestamp) — estampille \c m_lastActionAt. * \return L'action après écrêtage matériel (\c powerW borné à [0, maxPowerW]). * * \invariant Si \c action.reason est vide → retour sans effet (log warning). * \invariant \c action.force == true (repli L2 : \c setPowerSetpoint(0)) est transmis tel * quel ; le bypass anti-rebond est honoré par le thing, pas ici. */ LoadAction applyAction(const LoadAction &action, const QDateTime &now) override; /*! \brief Dernière consigne (W) effectivement écrite (avant écrêtage thing). */ double currentSetpointW() const { return m_currentSetpointW; } private: //! Vrai si aucun palier déclaré n'est fourni ⇒ modulation continue (contrat §2). bool isDynamic() const { return m_powerLevels.isEmpty(); } //! Écrit le state \c powerSetpoint (W) via executeAction (repli \c setStateValue pour mock). void writeSetpoint(double powerW); //! Lit le state \c currentPowerW du thing cible (0 si absent / non exposé). double readCurrentPowerW() const; ThingManager *m_thingManager; QString m_id; QString m_label; QList m_powerLevels; //!< Paliers W déclarés (vide = dynamic). int m_maxPowerW; int m_priority; LoadNeeds m_needs; double m_currentSetpointW = 0; //!< Dernière consigne écrite (W). QDateTime m_lastActionAt; };