// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync #pragma once #include #include #include #include #include #include "iloadadapter.h" class Thing; class ThingManager; /*! * \brief Adaptateur SG-Ready (PAC) — interface "sg-ready", action \c kind:State. * * Pilote une pompe à chaleur via 2 contacts SG-Ready (encodage 2 bits → 4 états NORMÉS) : * 1 = blocage (EVU-Sperre) · 2 = normal (mains off : la PAC décide) * 3 = recommandation (surplus) · 4 = forcé (boost) * * Les 4 états ne sont PAS des paliers de puissance : ils sont qualitatifs, la PAC les * interprète selon SA logique. \c m_stateRelays[état] = ThingIds powerswitch à mettre ON * pour cet état (encodage câblé par l'installateur ; les autres relais sont OFF). * * \invariant applyAction() rejette silencieusement toute action dont \c reason est vide. * \invariant applyAction() applique le verrou \c minStateHoldS (protection court-cycling * compresseur) SAUF si \c action.force == true (réservé L2 watchdog → état 2). * \invariant L'état est écrêté à l'ensemble \c m_states avant envoi matériel. * \invariant Seul le kind State est traité ; les autres kinds retournent sans effet. * * \par Contrat d'atomicité (transport déporté Shelly/Modbus à venir) * Une transition d'état commute parfois 2 relais (ex. 2→4 : 00→11). Les contacts * doivent être écrits **aussi atomiquement que possible**, et l'ORDRE de commutation * doit éviter tout **état actif parasite** : on passe par le transitoire le plus DOUX * (neutre = état 2, sinon recommandation = état 3) plutôt que par blocage (1) ou forcé * (4). \c applyStateRelays() choisit cet ordre. En GPIO local le transitoire dure des µs, * mais l'intention est portée par l'adaptateur pour rester correcte sur un bus lent. */ class SgReadyAdapter : public QObject, public ILoadAdapter { Q_OBJECT public: /*! * \brief Constructeur. * \param thingManager Gestionnaire nymea pour résoudre les ThingIds. * \param id Identifiant logique de la charge. * \param label Nom lisible (logs, app). * \param stateRelays état → liste de ThingIds powerswitch ON (encodage 2 bits SG-Ready). * \param estimatedPowerW Puissance estimée (W) par état (déclaré installateur, approx.). * \param minStateHoldS Durée minimale de maintien d'état (s) — protection court-cycling. * \param priority Rang dans le waterfall (protocole §5 : valeur plus BASSE = servi en premier). * \param parent Propriétaire Qt. */ explicit SgReadyAdapter(ThingManager *thingManager, const QString &id, const QString &label, const QHash> &stateRelays, const QHash &estimatedPowerW, int minStateHoldS, int priority, QObject *parent = nullptr); LoadDescriptor descriptor() const override; /*! * \brief Télémétrie runtime. \c currentPowerW = puissance ALLOUÉE de l'état courant * (\c declared.estimatedPowerW : 0 pour états 1/2, P3/P4 pour 3/4). C'est la base du * recrédit budget — PAS la conso mesurée de la PAC (l'état 2 autonome est déjà au * compteur, invariant 8 : la recréditer double-compterait). */ LoadTelemetry telemetry() const override; /*! * \brief Construit l'entrée loads[] §5 (adapter="sg-ready"). * \param now Temps de cycle (\c ctx.timestamp) — source unique de la fenêtre minState/maxState. */ LoadContext toLoadContext(const QDateTime &now) const override; /*! * \brief Applique un changement d'état SG-Ready (2 relais, transition atomique-douce). * \param action LoadAction de kind State. Autres kinds : retour sans effet. * \param now Temps de cycle — MÊME source que toLoadContext() (verrou + lastSwitch). * \return L'action après écrêtage (state borné à l'ensemble déclaré). */ LoadAction applyAction(const LoadAction &action, const QDateTime &now) override; /*! \brief État SG-Ready courant (1-4). */ int currentState() const { return m_currentState; } private: /*! * \brief Fenêtre d'états autorisée à \p now par le verrou minStateHold (symétrique). * \param[out] minState / maxState Gel total (== \c m_currentState) si \c minStateHold * non écoulé ; sinon [min, max] des états déclarés. */ void lockWindow(const QDateTime &now, int &minState, int &maxState) const; bool lockActive(int newState, const QDateTime &now) const; //! Applique l'ensemble de relais de \p toState en passant par le transitoire le plus //! doux (cf. contrat d'atomicité). \p fromState = état courant (pour l'ordre). void applyStateRelays(int fromState, int toState); //! Rang de nocivité d'un état comme TRANSITOIRE (2 neutre < 3 reco < 1 blocage < 4 forcé). static int transientHarm(int state); //! État correspondant à un ensemble de relais ON (-1 si aucun état ne correspond). int stateForRelays(const QList &onRelays) const; QSet allRelays() const; ThingManager *m_thingManager; QString m_id; QString m_label; QHash> m_stateRelays; //!< état → ThingIds ON. QHash m_estimatedPowerW; //!< état → W estimés (déclaré). QList m_states; //!< États déclarés triés croissants. int m_minStateHoldS; int m_priority; int m_currentState = 2; //!< Démarrage en NORMAL (mains off). QDateTime m_lastSwitch; //!< Dernier changement d'état (null = jamais). QDateTime m_lastActionAt; };