Constat de banc du 2026-08-09 : un SetLoadConfig posant enabled: false sur une
charge alors au palier 3500 W détruisait l'adaptateur en laissant les TROIS RELAIS
FERMÉS, juste avant une intervention de câblage. Plus personne ne les commandait ;
ils y seraient restés indéfiniment.
applySafeState(now) est ajouté à ILoadAdapter, PURE VIRTUELLE : l'état sûr est
propre à chaque adaptateur et une formulation « tout couper » serait fausse.
- RelayRouter : tous relais ouverts
- EtmVariableLoadAdapter : consigne 0 W
- SgReadyAdapter : ÉTAT 2 (normal, mains off) — JAMAIS l'état 1. Bloquer
une PAC n'est pas la mettre en sécurité, c'est arrêter
le chauffage sans raison visible (SAFETY.md).
- EvAdapter : sans effet, il n'est pas construit depuis LoadConfig.
L'application passe par le chemin d'action NORMAL avec force = true, celui du
mode dégradé L2 : le mécanisme de contournement des verrous existait déjà.
ORDRE, et c'est le point qui dépendait d'ECS-410 : l'état sûr est appliqué AVANT
la destruction, et la destruction passe par deleteLater(). Les écritures d'ECS-410
sont asynchrones avec `this` en contexte de connexion — détruire immédiatement
couperait les acquittements en vol, et on ne saurait pas si la mise en sécurité a
abouti, précisément dans le cas où elle échoue.
PÉRIMÈTRE BORNÉ. Rien de tout cela à l'arrêt du plugin ni au redémarrage de
nymead : l'état doit y être CONSERVÉ, c'est ce qu'ECS-411 relit, et couper l'eau
chaude à chaque redémarrage de service serait une régression. La désactivation est
un acte délibéré de l'opérateur ; un redémarrage n'en est pas un. Les charges
CONSERVÉES par le rebuild incrémental ne passent pas par ce chemin.
Test testEcsDisableLeavesSafeState, avec son CAS NÉGATIF en premier : un rebuild
qui ne change que le rang ne coupe rien — sans lui, ECS-412 serait annulé et
chaque changement de priorité couperait la charge. Puis le cas positif :
désactivation, relais ouvert, et il le reste même sous surplus au cycle suivant.
Build amd64 0 erreur. Simulation : 17/17.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
132 lines
6.8 KiB
C++
132 lines
6.8 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**.
|
|
*/
|
|
virtual void 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;
|
|
};
|