Étape 4 du chantier de documentation. tools/ci-quality.sh porte les trois
vérifications qui se posent au même endroit — elles partagent la même sortie
(doc-generated/) et la même commande de réparation, les séparer ferait trois jobs
qui régénèrent trois fois la même chose :
1. l'index des règles est VRAI (code de retour de gen-rules.py) ;
2. la doc Doxygen est complète — 0 avertissement, DoD §5 ;
3. l'index est REPRODUCTIBLE : deux passes, un seul résultat.
PAS EN SERVICE, et l'extension le garantit : ci/gitea-workflow.yml.disabled ne
déclenche rien tant qu'il n'est pas déplacé dans .gitea/workflows/. La raison est
datée dans le fichier — forge muette, plus de trente commits en attente ici et
plus de soixante côté app ; un job activé à l'aveugle échouerait au moment précis
où tout le monde pousse, et le premier réflexe serait de le désactiver.
CHAQUE ÉCHEC DONNE LA COMMANDE À LANCER. Un job qui annonce « l'index est
périmé » sans dire quoi faire se contourne en le désactivant : c'est le chemin de
moindre effort, et il gagne toujours.
VÉRIFIÉ AVANT D'ÊTRE DÉCLARÉ PRÊT, et ça ne passait pas du premier coup.
— Les 26 avertissements Doxygen de ce matin sont CORRIGÉS, pas contournés par un
cliquet sur un compte de référence : c'était de la dette DoD-5 (retours et
paramètres non documentés dans relayrouter.h, energyarbitrator.h, loadconfig.h,
les trois clearFault(), ILoadAdapter::updateSoftConfig), plus deux que j'avais
introduits — un \rule{} dans un titre \par, et un \param energyLogs qui ne
correspond à aucun argument. Le seuil est donc ZÉRO, le seul qui ne rote pas.
— Et le script lui-même échouait sur un dépôt SAIN : `grep -c ... || echo 0`
affiche 0 PUIS sort en 1 quand il ne trouve rien, si bien que le « || » empile
un second zéro et que le test entier casse. Corrigé ici et dans gen-doc.sh, qui
portait le même piège. Une commande de mesure dont l'échec produit une lecture
fausse — le motif de la semaine, appliqué à l'outil.
CE QUI N'EST PAS VÉRIFIÉ, et pourquoi : « régénérer et refuser si ça diffère de
ce qui est commité » n'a rien à comparer, doc-generated/ étant dans .gitignore.
Le risque qu'un tel diff attraperait — un artefact commité qui dérive de sa
source — a été SUPPRIMÉ en ne le commitant pas, pas déplacé. Ce qui reste à
garantir est que l'index soit vrai (contrôle 1) et déterministe (contrôle 3).
État au 2026-08-29 : ./tools/ci-quality.sh → RC=0, 113 règles, 0 avertissement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
199 lines
10 KiB
C++
199 lines
10 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 ThingActionInfo;
|
||
|
||
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 minPowerW Plancher de modulation (W) — §12 / LM-1201. `0` = aucun plancher.
|
||
* Sous cette valeur la charge ne prend RIEN : le domaine de la
|
||
* consigne est `{0} ∪ [minPowerW, maxPowerW]`. **Déclaré**, parce
|
||
* qu'aucune donnée du thing ne permet de le deviner.
|
||
* \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 minPowerW,
|
||
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.
|
||
* \return LoadTelemetry ; \c currentPowerW vaut 0 si le thing est absent ou n'expose
|
||
* pas l'état \c currentPowerW (jamais de puissance fantôme).
|
||
*/
|
||
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 Met à jour libellé, rang et besoins sans reconstruire (ECS-412).
|
||
//! \param label Nouveau libellé — il part au journal DÈS ce cycle.
|
||
//! \param priority Nouveau rang de service. \param needs Nouveaux besoins.
|
||
void updateSoftConfig(const QString &label, int priority, const LoadNeeds &needs) override
|
||
{ m_label = label; m_priority = priority; m_needs = needs; }
|
||
|
||
//! \brief Sans effet : cet adaptateur n'implémente pas encore l'échelle de défaut ECS-410.
|
||
//! La généralisation est portée par **ECS-414** (lot de mise en configuration
|
||
//! du SgReadyAdapter). Déclaré explicitement plutôt qu'hérité d'un défaut vide.
|
||
//! \brief Lève le verrou de défaut (ECS-414). La consigne réelle est RELUE, pas supposée.
|
||
//! \return Vrai si un défaut a réellement été levé ; faux s'il n'y en avait pas.
|
||
bool clearFault() override;
|
||
|
||
//! \return Vrai si la charge est en défaut (plus aucune consigne écrite).
|
||
bool faulted() const { return m_faulted; }
|
||
|
||
/*!
|
||
* \brief Vue runtime pour la télémétrie RPC : consigne, pourcentage, défaut.
|
||
* \param now Inutilisé — cet adaptateur n'a AUCUNE fenêtre de verrou côté moteur :
|
||
* l'anti-rebond vit dans le thing. Le client lit donc \c lock absent, ce qui est la
|
||
* vérité, et non un verrou nul qui laisserait croire à une fenêtre expirée.
|
||
* \return \c mechanismKind "variable" ; \c mechanism \c {setpointW, percent, maxPowerW}.
|
||
*/
|
||
LoadRuntimeView runtimeView(const QDateTime &now) const override;
|
||
|
||
/*!
|
||
* \brief Mesure propre de la charge pilotée (§11 / LM-1105).
|
||
* \return La puissance que le Thing rapporte lui-même, quand sa ThingClass porte
|
||
* \c currentPowerW ; « non mesurable » sinon.
|
||
* \note **Ne retombe PAS sur la consigne en vol**, contrairement à \c telemetry(). Celle-ci
|
||
* doit rendre un nombre à chaque cycle — le recrédit anti-clignotement en dépend — et
|
||
* annonce donc la plus haute consigne possible quand rien ne mesure (ECS-414). Ici, une
|
||
* consigne n'est pas une mesure : la publier comme telle produirait un écart
|
||
* commandé↔mesuré identiquement nul (LM-1106).
|
||
*/
|
||
LoadMeasurement deviceMeasurement() const override;
|
||
|
||
//! \brief État sûr : consigne **0 W**, en \c force = true (ECS-413). \param now Temps de cycle.
|
||
void applySafeState(const QDateTime &now) override;
|
||
|
||
/*!
|
||
* \brief Dernière consigne (W) effectivement écrite (avant écrêtage thing).
|
||
* \return Consigne commandée en W, bornée à \c maxPowerW ; 0 tant qu'aucune action
|
||
* n'a été appliquée. C'est le COMMANDÉ, pas le mesuré.
|
||
*/
|
||
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),
|
||
//! et suit son acquittement (ECS-414, modèle d'ECS-410).
|
||
void writeSetpoint(double powerW);
|
||
//! Verdict quand l'écriture est acquittée ; fait avancer l'échelle d'un cran.
|
||
void settleTransition();
|
||
|
||
//! Barreaux de l'échelle. Plancher de cet adaptateur : consigne 0 W.
|
||
enum Phase { PhaseNominale, PhaseRepli, PhasePlancher, PhaseDefaut };
|
||
|
||
//! 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_minPowerW; //!< Plancher de modulation (W) ; 0 = aucun (§12).
|
||
int m_priority;
|
||
LoadNeeds m_needs;
|
||
|
||
double m_currentSetpointW = 0; //!< Dernière consigne écrite (W).
|
||
|
||
// --- ECS-414 : suivi asynchrone (modèle d'ECS-410) ----------------------------------
|
||
int m_pending = 0;
|
||
bool m_writeFailed = false;
|
||
double m_setpointPrev = 0;
|
||
double m_setpointTarget = 0;
|
||
Phase m_phase = PhaseNominale;
|
||
bool m_faulted = false;
|
||
QDateTime m_lastActionAt;
|
||
};
|