É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
254 lines
14 KiB
C++
254 lines
14 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
|
#pragma once
|
|
|
|
#include <QDateTime>
|
|
#include <QVariantMap>
|
|
#include "../types/loadaction.h"
|
|
#include "../types/loaddescriptor.h"
|
|
#include "../types/surpluscontext.h"
|
|
|
|
/*!
|
|
* \brief Vue runtime minimale exposée par un adaptateur à l'arbitre.
|
|
*/
|
|
struct LoadTelemetry {
|
|
double currentPowerW = 0; //!< Puissance mesurée (W).
|
|
bool available = true; //!< Faux si l'appareil nymea est absent ou en erreur.
|
|
QDateTime lastActionAt; //!< Dernier instant où applyAction() a produit un effet.
|
|
};
|
|
|
|
/*!
|
|
* \brief Mesure PROPRE de l'appareil piloté — §11, LM-1105/LM-1106.
|
|
*
|
|
* Distincte de \c LoadTelemetry::currentPowerW, et la distinction est tout l'objet du type.
|
|
* Ce champ-là est le **juge runtime du budget** : il vaut une mesure quand il en existe une et
|
|
* la puissance **commandée ou déclarée** sinon, parce que le recrédit anti-clignotement a
|
|
* besoin d'un nombre à chaque cycle. Celui-ci ne vaut **que** ce qu'un appareil a réellement
|
|
* mesuré, ou rien.
|
|
*
|
|
* \par Pourquoi une CAPACITÉ et non une valeur nulle
|
|
* \c available répond à « cette charge est-elle mesurable ? », question posée à la
|
|
* \c ThingClass, jamais à la valeur du moment. Un routeur au palier 0 mesure 0 W et reste
|
|
* mesurable ; sans ce booléen, les deux seraient indistinguables et la source publiée
|
|
* basculerait d'un cycle à l'autre sur une installation dont rien n'a changé.
|
|
*/
|
|
struct LoadMeasurement {
|
|
//! Vrai si les Things pilotés portent l'état de puissance — CAPACITÉ, pas valeur.
|
|
bool available = false;
|
|
//! Puissance réellement mesurée (W) ; sans objet quand \c available est faux.
|
|
double powerW = 0;
|
|
};
|
|
|
|
/*!
|
|
* \brief Vue runtime d'une charge destinée à la FRONTIÈRE DE TÉLÉMÉTRIE (RPC), et à elle seule.
|
|
*
|
|
* Distincte de \c LoadTelemetry (que l'arbitre lit pour DÉCIDER) et de \c LoadContext (ce que
|
|
* le scheduler reçoit). Elle porte ce qui n'a de sens que pour EXPLIQUER une décision à un
|
|
* humain : le défaut, le verrou en secondes restantes, et l'état du mécanisme.
|
|
*
|
|
* \par Des codes, jamais des phrases
|
|
* \c faultCode, \c lockKind et \c mechanismKind sont des CODES d'énumération fermée. La
|
|
* phrase est fabriquée par le client, dans la langue de son utilisateur — même raison que
|
|
* pour \c DecisionReason (cf. \c decisionreason.h).
|
|
*
|
|
* \par Des secondes, pas des watts
|
|
* \c lockMinPowerW / \c lockMaxPowerW (dans \c LoadContextTelemetry) sont un artefact du
|
|
* mécanisme d'allocation : illisibles pour un utilisateur. Le temps restant, lui, se lit.
|
|
* Ceci n'enfreint pas ECS-306 : cette exigence interdit qu'un index de PALIER franchisse la
|
|
* frontière d'ALLOCATION. Une valeur d'AFFICHAGE sortante est un autre chemin, et
|
|
* \c stageW / \c state sont des watts et des états normés, pas des index.
|
|
*
|
|
* \par Union discriminée par mécanisme
|
|
* \c mechanismKind discrimine ; \c mechanism porte la charge utile de la variante, et
|
|
* d'elle seule (LM-300). Ajouter un mécanisme = une variante ici et son test — rien d'autre.
|
|
*/
|
|
struct LoadRuntimeView {
|
|
//! Code de défaut ; VIDE = aucun défaut. Énumération fermée, cf. \c INTERFACE.md.
|
|
QString faultCode;
|
|
//! Verrou ACTIF maintenant : "minOn" | "minOff" | "minStateHold" ; vide = aucun.
|
|
QString lockKind;
|
|
//! Secondes restantes du verrou nommé par \c lockKind ; sans objet si \c lockKind est vide.
|
|
int lockRemainingS = 0;
|
|
//! Mécanisme : "relay" | "variable" | "sgReady" | "evcharger".
|
|
QString mechanismKind;
|
|
//! Charge utile du mécanisme (clés propres à la variante) ; jamais celle d'un autre.
|
|
QVariantMap mechanism;
|
|
};
|
|
|
|
/*!
|
|
* \brief Interface pure des adaptateurs de charge.
|
|
*
|
|
* Les implémentations concrètes héritent de QObject + ILoadAdapter et déclarent leurs
|
|
* propres signaux (telemetryChanged, descriptorChanged).
|
|
*
|
|
* \invariant Les adaptateurs EXÉCUTENT, ils ne décident pas (AGENTS règle 2).
|
|
* \invariant applyAction() écrête les valeurs selon les limites matérielles réelles
|
|
* (second filet après l'écrêtage de l'arbitre).
|
|
* \invariant applyAction() avec un \c reason.code vide doit être rejetée silencieusement.
|
|
* \invariant Les méthodes non-applyAction() retournent immédiatement (pas de I/O bloquant).
|
|
* \invariant **Temps = paramètre, jamais l'horloge.** Toute logique temporelle d'un
|
|
* adaptateur (verrous minOn/minOff, fenêtres, fraîcheur…) utilise EXCLUSIVEMENT le
|
|
* \c now (= \c ctx.timestamp) reçu en paramètre de \c toLoadContext()/applyAction().
|
|
* JAMAIS \c QDateTime::currentDateTime(). C'est cette source unique, partagée avec le
|
|
* scheduler, qui rend impossible toute divergence décision/exécution et qui rend la
|
|
* logique injectable en simulation. Contrat pour tout futur adaptateur (SgReady, Battery).
|
|
*/
|
|
class ILoadAdapter {
|
|
public:
|
|
virtual ~ILoadAdapter() = default;
|
|
|
|
/*!
|
|
* \brief Description statique de la charge : capacités, limites, priorité, needs.
|
|
* \return LoadDescriptor construit depuis la configuration matérielle.
|
|
* \note Peut être rappelé à chaque cycle — l'implémentation doit être légère.
|
|
*/
|
|
virtual LoadDescriptor descriptor() const = 0;
|
|
|
|
/*!
|
|
* \brief Télémétrie runtime (puissance, disponibilité, dernière action).
|
|
* \return LoadTelemetry issue de l'état courant de l'appareil nymea.
|
|
*/
|
|
virtual LoadTelemetry telemetry() const = 0;
|
|
|
|
/*!
|
|
* \brief Construit l'entrée §5 loads[] pour le SurplusContext.
|
|
* \param now Temps de cycle (\c ctx.timestamp). Source unique pour l'évaluation des
|
|
* verrous (minStage/maxStage) — JAMAIS \c QDateTime::currentDateTime() côté adaptateur,
|
|
* afin que décision (scheduler) et exécution (applyAction) partagent le même temps.
|
|
* \return LoadContext incluant declared, limits, needs et télémétrie type-spécifique.
|
|
*/
|
|
virtual LoadContext toLoadContext(const QDateTime &now) const = 0;
|
|
|
|
/*!
|
|
* \brief Applique l'action et retourne ce qui a réellement été envoyé au matériel.
|
|
*
|
|
* L'arbitre a déjà écrêté selon les limites et le budget — ceci est le second filet.
|
|
*
|
|
* \param action Action à appliquer. Doit avoir \c reason.code non vide.
|
|
* \param now Temps de cycle (\c ctx.timestamp) — MÊME source que toLoadContext(),
|
|
* pour que l'évaluation des verrous coïncide avec celle vue par le scheduler.
|
|
* \return L'action après écrêtage matériel (peut différer de l'entrée).
|
|
* \note Retour silencieux sans effet si \c action.reason.code est vide.
|
|
*/
|
|
virtual LoadAction applyAction(const LoadAction &action, const QDateTime &now) = 0;
|
|
|
|
/*!
|
|
* \brief Met à jour les champs de configuration qui ne touchent PAS le matériel.
|
|
*
|
|
* Sert au rebuild INCRÉMENTAL (ECS-412) : quand seul le rang de service ou les besoins
|
|
* changent, l'arbitre met à jour l'adaptateur en place au lieu de le détruire. Détruire
|
|
* réarmerait les verrous — sur un ballon thermodynamique à \c minOn de 300 à 600 s, un
|
|
* client qui réordonne ses priorités depuis l'app pourrait faire court-cycler son
|
|
* compresseur — et provoquerait des réécritures de relais inutiles.
|
|
*
|
|
* \param label Nouveau libellé d'affichage.
|
|
* \param priority Nouveau rang de service (ASC, 1 = premier servi).
|
|
* \param needs Nouveaux besoins déclarés.
|
|
* \warning NE DOIT jamais toucher au câblage, aux paliers ni aux verrous : ces
|
|
* changements-là exigent une vraie reconstruction.
|
|
* \note **Pure virtuelle à dessein**, sans implémentation par défaut. Un adaptateur qui
|
|
* n'est pas construit depuis \c LoadConfig n'a rien à mettre à jour, mais il doit le
|
|
* DÉCLARER : un défaut vide silencieux ferait qu'un futur adaptateur construit depuis
|
|
* la config ignorerait sans bruit les changements de rang.
|
|
*/
|
|
virtual void updateSoftConfig(const QString &label, int priority,
|
|
const LoadNeeds &needs) = 0;
|
|
|
|
/*!
|
|
* \brief Lève le verrou de défaut de la charge (ECS-410).
|
|
*
|
|
* Le défaut est COLLANT par conception — « cesser toute commande jusqu'à intervention ».
|
|
* Sa levée est un acte délibéré de l'opérateur, jamais une expiration : sans quoi
|
|
* \c available oscillerait d'un cycle à l'autre et le waterfall réagirait à chaque
|
|
* battement.
|
|
*
|
|
* \note **Pure virtuelle à dessein.** Tous les adaptateurs ne savent pas encore tomber en
|
|
* défaut — seul \c RelayRouter implémente l'échelle ECS-410 — mais chacun doit le
|
|
* DÉCLARER. La généralisation est portée par **ECS-414**.
|
|
*
|
|
* \return \c true si un défaut a RÉELLEMENT été levé, \c false s'il n'y en avait pas.
|
|
*
|
|
* \note **Pourquoi un booléen et non \c void** (règle 7-c d'AGENTS.md). C'est le seul
|
|
* levier de reprise à distance : ECS-410 pose un verrou collant que seuls ce RPC ou une
|
|
* reconstruction lèvent. Sans valeur de retour, l'arbitre ne peut pas distinguer
|
|
* « défaut levé » de « aucun défaut à lever », et un opérateur à distance ne peut pas
|
|
* savoir si son geste a agi — il relancera, puis conclura que le système est cassé.
|
|
* Le type de retour force chaque implémentation, présente et future, à répondre à la
|
|
* question ; une ligne de journal dans chaque adaptateur se serait oubliée à la
|
|
* cinquième.
|
|
*/
|
|
virtual bool clearFault() = 0;
|
|
|
|
/*!
|
|
* \brief Vue runtime destinée à la frontière de télémétrie RPC (\c GetLoadTelemetry).
|
|
*
|
|
* \param now Temps de cycle — MÊME source que \c toLoadContext()/\c applyAction(), afin
|
|
* que le verrou annoncé au client soit celui qui a réellement borné la décision.
|
|
* \return Défaut, verrou en secondes restantes, et charge utile du mécanisme.
|
|
*
|
|
* \note **Pure virtuelle à dessein**, sans implémentation par défaut. Un mécanisme ajouté
|
|
* qui hériterait d'un défaut vide sortirait de la télémétrie SANS BRUIT : le client
|
|
* verrait une charge sans mécanisme et sans verrou, indistinguable d'une charge libre.
|
|
* C'est précisément le silence ambigu de la règle 7-c. Le compilateur pose donc la
|
|
* question à chaque nouvel adaptateur.
|
|
* \note Cette méthode n'entre JAMAIS dans une décision : l'arbitre ne la lit que pour
|
|
* publier. Un adaptateur qui s'en servirait pour décider violerait la règle 2.
|
|
*/
|
|
virtual LoadRuntimeView runtimeView(const QDateTime &now) const = 0;
|
|
|
|
/*!
|
|
* \brief Cette charge est-elle mesurée par les appareils qu'elle pilote, et combien ?
|
|
*
|
|
* §11 — **LM-1105** : le moteur lit l'appareil qu'il pilote plutôt que d'exiger qu'on le
|
|
* désigne comme son propre compteur dans \c meterThingId. **LM-1106** : ce qui est publié
|
|
* est un couple {valeur, source}, et c'est cette méthode qui répond pour la source
|
|
* \c "device".
|
|
*
|
|
* \return \c available faux = **rien ne mesure cette charge** (\c source: "none" à la
|
|
* publication) ; vrai = \c powerW est une mesure réelle.
|
|
*
|
|
* \note **La question porte sur la ThingClass, jamais sur la valeur ni sur l'instant.** Un
|
|
* \c RelayRouter au palier 0 n'a aucun relais fermé : interroger les relais ACTIFS
|
|
* ferait osciller la source publiée d'un cycle à l'autre. Elle se pose sur TOUS les
|
|
* Things déclarés de la charge.
|
|
* \note **Et elle présuppose que le Thing piloté est TRAVERSÉ par la puissance.** Un relais
|
|
* de \c RelayRouter commute la charge, donc sa puissance EST celle de la charge ; un
|
|
* contact SG-Ready est un contact de SIGNAL — la PAC est alimentée ailleurs, et son
|
|
* courant ne passe jamais par lui. Un contact qui mesurerait quelque chose mesurerait sa
|
|
* propre bobine. \c SgReadyAdapter répond donc toujours « non mesurable », et c'est le
|
|
* seul mécanisme pour lequel \c meterThingId reste la seule voie.
|
|
* \note **Une mesure PARTIELLE n'est pas une mesure.** Là où plusieurs Things portent la
|
|
* charge, il faut qu'ils la portent TOUS : sommer les seuls relais mesurants
|
|
* publierait une puissance sous-évaluée, et l'écart commandé↔mesuré enverrait chercher
|
|
* une panne qui n'existe pas (LM-1102).
|
|
* \note **Pure virtuelle à dessein**, comme \c runtimeView() et pour la même raison : un
|
|
* mécanisme ajouté qui hériterait d'un défaut « non mesurable » sortirait de la mesure
|
|
* SANS BRUIT, et son écran afficherait « pas mesurable » avec l'autorité d'un constat.
|
|
* Le compilateur pose la question à chaque nouvel adaptateur.
|
|
* \note N'entre JAMAIS dans une décision : l'arbitre ne la lit que pour publier (règle 2).
|
|
*/
|
|
virtual LoadMeasurement deviceMeasurement() const = 0;
|
|
|
|
/*!
|
|
* \brief Amène le matériel dans l'ÉTAT SÛR de cet adaptateur (ECS-413).
|
|
*
|
|
* Appelé quand la charge est désactivée (\c enabled: false) ou retirée de la
|
|
* configuration — un acte DÉLIBÉRÉ de l'opérateur. Sans cela, l'adaptateur est détruit et
|
|
* le matériel reste dans son DERNIER ÉTAT COMMANDÉ : constaté au banc le 2026-08-09, trois
|
|
* relais laissés fermés juste avant une intervention de câblage.
|
|
*
|
|
* \param now Temps de cycle. Passe par le chemin d'action normal avec \c force = true,
|
|
* comme le mode dégradé L2 : l'arrêt prime sur les verrous.
|
|
*
|
|
* \warning **NE PAS appeler à l'arrêt du plugin ni au redémarrage de nymead.** L'état doit
|
|
* y être CONSERVÉ — c'est ce qu'ECS-411 relit au démarrage, et couper l'eau chaude à
|
|
* chaque redémarrage de service serait une régression. La distinction est
|
|
* intentionnelle : la désactivation est délibérée, un redémarrage ne l'est pas.
|
|
*
|
|
* \note L'état sûr est PROPRE à chaque adaptateur — relais ouverts pour un routeur,
|
|
* consigne 0 W pour une charge continue, mais **état 2** pour une PAC SG-Ready, jamais
|
|
* le blocage (\c docs/SAFETY.md). Une formulation « tout couper » serait fausse.
|
|
*/
|
|
virtual void applySafeState(const QDateTime &now) = 0;
|
|
};
|