diff --git a/energyplugin/etm/adapters/sgreadyadapter.cpp b/energyplugin/etm/adapters/sgreadyadapter.cpp index 354a3ba..b9b7de4 100644 --- a/energyplugin/etm/adapters/sgreadyadapter.cpp +++ b/energyplugin/etm/adapters/sgreadyadapter.cpp @@ -33,8 +33,21 @@ SgReadyAdapter::SgReadyAdapter(ThingManager *thingManager, { m_states = m_stateRelays.keys(); std::sort(m_states.begin(), m_states.end()); - Q_ASSERT(!m_states.isEmpty()); - Q_ASSERT(m_stateRelays.contains(2)); // état 2 (normal) = repli sûr obligatoire + + // ECS-110 — la présence de l'état 2 NE DOIT PAS reposer sur un Q_ASSERT : QT_NO_DEBUG est + // défini en release, l'assertion disparaît du binaire livré. Tant que les états venaient + // d'une construction codée en dur elle ne pouvait pas échouer ; dès qu'ils viennent de la + // configuration, elle serait le SEUL garde-fou sur le repli sûr du mode dégradé L2. + // + // La vérification vit donc dans LoadConfig::isValid() (refus explicite, message FR) et le + // contrôle ci-dessous n'est qu'un dernier filet pour les constructions directes (tests, + // enregistrement codé en dur) : il PARLE au lieu de disparaître. + m_usable = !m_states.isEmpty() && m_stateRelays.contains(2); + if (!m_usable) + qCCritical(dcNymeaEnergy()) << "[SgReadyAdapter]" << m_label + << "— encodage INUTILISABLE : l'état 2 (normal) n'est pas" + << "exprimable. C'est le repli sûr du mode dégradé L2 ;" + << "l'adaptateur refusera toute commande."; } LoadDescriptor SgReadyAdapter::descriptor() const @@ -56,7 +69,7 @@ LoadDescriptor SgReadyAdapter::descriptor() const LoadTelemetry SgReadyAdapter::telemetry() const { LoadTelemetry t; - t.available = !m_faulted; // ECS-414 : sort de l'ARBITRAGE, pas de la COMPTABILITÉ. + t.available = m_usable && !m_faulted; // ECS-414 : sort de l'ARBITRAGE, pas de la COMPTABILITÉ. t.lastActionAt = m_lastActionAt; // Base du recrédit budget = puissance ALLOUÉE de l'état (déclaré), pas la conso mesurée // (états 1/2 → 0 ; états 3/4 → P3/P4). Cf. invariant 8. @@ -82,7 +95,7 @@ LoadContext SgReadyAdapter::toLoadContext(const QDateTime &now) const ctx.limits = descriptor().limits; ctx.telemetry.currentPowerW = telemetry().currentPowerW; - ctx.telemetry.available = !m_faulted; + ctx.telemetry.available = m_usable && !m_faulted; ctx.telemetry.state = m_currentState; ctx.telemetry.lastSwitch = m_lastSwitch; if (m_faulted) { @@ -107,6 +120,12 @@ LoadAction SgReadyAdapter::applyAction(const LoadAction &action, const QDateTime return action; } + if (!m_usable) { + LoadAction refused = action; + refused.state = m_currentState; + return refused; // encodage sans état 2 : aucune commande, cf. constructeur + } + // ECS-414 — en défaut, plus aucune commande, y compris forcée : trois tentatives ont // déjà échoué, en réémettre masquerait l'état sans rien réparer. if (m_faulted) { diff --git a/energyplugin/etm/adapters/sgreadyadapter.h b/energyplugin/etm/adapters/sgreadyadapter.h index 93fb417..6335884 100644 --- a/energyplugin/etm/adapters/sgreadyadapter.h +++ b/energyplugin/etm/adapters/sgreadyadapter.h @@ -119,6 +119,14 @@ public: */ void applySafeState(const QDateTime &now) override; + /*! \brief Contacts que cet adaptateur revendique, tous états confondus. + * + * Sert à détecter qu'une PAC de configuration (LM-300) et une PAC enregistrée en dur + * commandent le même matériel — situation où deux adaptateurs se disputeraient les mêmes + * contacts et où le budget compterait la charge deux fois. + */ + QSet claimedRelays() const { return allRelays(); } + /*! * \brief État SG-Ready courant (1-4). * \return Dernier état réellement commuté ; valeur initiale tant qu'aucune transition @@ -188,6 +196,10 @@ private: int m_stateTarget = 2; Phase m_phase = PhaseNominale; bool m_faulted = false; + //! Faux si l'encodage ne permet pas d'exprimer l'état 2 — repli sûr du mode dégradé L2. + //! L'adaptateur refuse alors TOUTE commande, et le dit (ECS-110 : jamais un Q_ASSERT, + //! qui disparaît du binaire release). + bool m_usable = true; QDateTime m_lastSwitch; //!< Dernier changement d'état (null = jamais). QDateTime m_lastActionAt; };