// 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" #include "../types/loadconfig.h" // LoadConfigRelay class Thing; class ThingManager; /*! * \brief ROUTEUR de relais (contrat rév. 3) — traduit un \c Setpoint(W) de l'optimiseur en * combinaison de things \c power, côté experience-plugin. * * \par Frontière rév. 3 * L'optimiseur (scheduler) est **watt-pur** : il émet \c LoadAction{Setpoint, powerW}. Le * `RelayRouter` est la couche **sous** l'optimiseur qui connaît les relais : il mappe * \c powerW → la combinaison atteignable la plus haute ≤ \c powerW, applique l'anti-rebond * (\c minOn/minOff) **ici** (plus dans le scheduler), commute les relais via * \c ThingManager::executeAction (interface \c power), et agrège \c currentPowerW. **Aucun * relais, aucun index de combinaison ne remonte dans le \c LoadContext** (seuls les watts * dérivés `powerLevels`/`maxPowerW` y figurent). * * \par Paliers DÉRIVÉS * Les \c powerLevels (et \c maxPowerW) sont **calculés** depuis la liste de relais : toutes * les sommes de sous-ensembles atteignables, dédupliquées, triées, \c 0 inclus. **Source de * vérité unique = les relais** (l'app affiche le même calcul, l'optimiseur les lit pour * l'arrondi/résidu). * * \invariant \c supportedKinds == { Setpoint }. Autres kinds : retour sans effet. * \invariant applyAction() rejette silencieusement toute action dont \c reason est vide. * \invariant Verrous \c minOn/minOff appliqués en INTERNE (clamp), bypassés si \c force==true * (repli L2). **Temps = paramètre** (cf. \c ILoadAdapter) : \c now reçu, jamais l'horloge. * \invariant Transition relais en **off-before-on** : coupe d'abord les relais hors-cible. */ class RelayRouter : public QObject, public ILoadAdapter { Q_OBJECT public: /*! * \brief Constructeur. * \param thingManager Résout les ThingIds des relais et exécute les actions \c power. * \param id Identifiant LOGIQUE de la charge (rév. 3 : plus un thingId unique). * \param label Nom lisible (logs, app). * \param relays Liste \c {thingId, powerW} des relais \c power. Les paliers atteignables * sont dérivés de toutes leurs combinaisons. * \param minOnS/minOffS Verrous anti-rebond (protection relais/compresseur), appliqués ici. * \param priority Rang dans le waterfall (1 = servi en premier). * \param needs Besoins déclarés (exposés via descriptor().needs). * \param parent Propriétaire Qt. */ explicit RelayRouter(ThingManager *thingManager, const QString &id, const QString &label, const QList &relays, int minOnS, int minOffS, int priority, const LoadNeeds &needs = LoadNeeds(), QObject *parent = nullptr); //! \return LoadDescriptor : adapter="relay-router", powerLevels/maxPowerW DÉRIVÉS, needs. LoadDescriptor descriptor() const override; //! \return currentPowerW = somme des \c currentPower des relais ON (mesuré), sinon nominal commandé. LoadTelemetry telemetry() const override; //! \return LoadContext §5 (watts uniquement : powerLevels, currentPowerW — aucun relais). LoadContext toLoadContext(const QDateTime &now) const override; /*! * \brief Applique un \c Setpoint(W) : mappe en combinaison de relais, clampe par les verrous, * commute (off-before-on), publie l'état. * \param action LoadAction kind \c Setpoint (\c powerW). Autres kinds : retour sans effet. * \param now Temps de cycle (verrous + estampille). * \return L'action après écrêtage (\c powerW = palier réellement appliqué). */ LoadAction applyAction(const LoadAction &action, const QDateTime &now) override; //! \brief Palier courant (0 = tout coupé). int currentStage() const { return m_currentStage; } //! \brief Puissance (W) du palier courant. double currentSetpointW() const { return m_currentStage < m_levels.size() ? m_levels.at(m_currentStage) : 0.0; } private: //! Palier le plus haut dont la puissance ≤ \p powerW (≥ 0). int stageForPower(double powerW) const; //! Fenêtre de paliers autorisée à \p now par minOn/minOff (verrou INTERNE, plus exposé). void lockWindow(const QDateTime &now, int &minStage, int &maxStage) const; void applyRelayStage(int stage); ThingManager *m_thingManager; QString m_id; QString m_label; QList m_levels; //!< Paliers W dérivés, triés, [0]=0. QList> m_relayMapping; //!< ThingIds ON par palier (dérivé des combinaisons). int m_minOnS; int m_minOffS; int m_priority; LoadNeeds m_needs; int m_currentStage = 0; QDateTime m_lastSwitch; //!< Dernier changement (null = jamais). QDateTime m_lastActionAt; };