Côté energymanager : EtmVariableLoadAdapter consomme l'interface etmvariableload (states currentPowerW read / powerSetpoint write) pour les charges à puissance pilotable (EV / ECS résistif / routeur PV). La combinatoire matérielle, l'anti-rebond et les phases vivent dans le thing (contrat §1/§3) — l'adaptateur n'écrit qu'un setpoint W et relit currentPowerW. - LoadDescriptor : champs additifs powerLevels / maxPowerW (déclaration §3/§5). - Contrat partagé versionné : docs/INTERFACE_etmvariableload.md (rév. 2). - La déclaration de l'interface nymee + le driver thing relèvent d'une session device. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
131 lines
6.5 KiB
C++
131 lines
6.5 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
|
#pragma once
|
|
|
|
#include <QObject>
|
|
#include <QDateTime>
|
|
#include <QList>
|
|
#include <QString>
|
|
#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<int> &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<int> 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;
|
|
};
|