ClearLoadFault est le SEUL levier de reprise à distance : ECS-410 pose un verrou collant que seuls ce RPC ou une reconstruction lèvent. L'arbitre journalisait la demande de l'opérateur, jamais son issue — si bien qu'appelé depuis l'étranger, on ne distinguait pas « le défaut a été levé » de « il n'y en avait pas » de « l'appel n'a pas atteint l'adaptateur ». L'opérateur relance trois fois et conclut que le système est cassé. L'issue est écrite dans l'ARBITRE, pas dans chaque adaptateur : une ligne là couvre les quatre d'un coup et ne peut pas être oubliée par un adaptateur futur ; répétée dans chacun, elle manquerait le cinquième. Les trois cas sont distincts à la lecture. ILoadAdapter::clearFault() rend un bool au lieu de void. Le type de retour force chaque implémentation, présente et future, à répondre à la question « un défaut a-t-il réellement été levé ». EvAdapter, qui n'a pas de mécanisme de défaut, le dit désormais explicitement plutôt que par un corps vide. Règle 7-d — un refus doit être au moins aussi visible que l'application correspondante. Le refus par verrou minStateHold de SgReadyAdapter était en qCDebug quand l'application, dix lignes plus bas, était en qCInfo. L'asymétrie est le défaut, pas le niveau : un resserrement de la journalisation — et on vient d'en faire un pour tenir deux semaines — aurait fait disparaître le refus AVANT le succès, ne laissant au journal que les décisions abouties. Or un refus est plus informatif qu'une application réussie : il dit qu'une décision a été prise et n'a pas été exécutée. Vérification faite sur les autres adaptateurs : c'était le seul cas. Les qCDebug restants sont symétriques, et le marqueur L2 par cycle reste en debug à dessein, son entrée étant en qCWarning et sa sortie en qCInfo. Le retour idempotent du routeur reste DIFFÉRÉ, et ira en qCDebug : c'est le cas le plus fréquent — plusieurs milliers de lignes par jour sur un budget ramené à 6,5 Mo — et son ambiguïté est partiellement couverte, le scheduler journalisant sa décision et l'armement à froid s'exécutant avant ce retour. Faible gain, coût réel : l'inverse des deux cas corrigés. Le test porte sur le TEXTE des trois issues et vérifie qu'elles sont deux à deux distinctes. Suite complète : 112 tests, 0 échec. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
143 lines
7.6 KiB
C++
143 lines
7.6 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
|
#pragma once
|
|
|
|
#include <QDateTime>
|
|
#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 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 \c reason 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 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 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 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(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 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;
|
|
};
|