diff --git a/energyplugin/etm/types/loadconfig.cpp b/energyplugin/etm/types/loadconfig.cpp index 7c57864..4f3bce1 100644 --- a/energyplugin/etm/types/loadconfig.cpp +++ b/energyplugin/etm/types/loadconfig.cpp @@ -42,8 +42,67 @@ LoadConfigRelay LoadConfigRelay::fromMap(const QVariantMap &map) return r; } +// ---- LoadConfigSgReady ----------------------------------------------------- + +QVariantMap LoadConfigSgReadyState::toMap() const +{ + QVariantMap m; + m.insert("state", state); + m.insert("relays", QVariant(relays)); + m.insert("estimatedPowerW", estimatedPowerW); + return m; +} + +LoadConfigSgReadyState LoadConfigSgReadyState::fromMap(const QVariantMap &map) +{ + LoadConfigSgReadyState s; + s.state = map.value("state").toInt(); + for (const QVariant &v : map.value("relays").toList()) + s.relays.append(v.toString()); + s.estimatedPowerW = map.value("estimatedPowerW").toDouble(); + return s; +} + +QVariantMap LoadConfigSgReady::toMap() const +{ + QVariantMap m; + QVariantList l; + for (const LoadConfigSgReadyState &s : states) + l.append(s.toMap()); + m.insert("states", l); + m.insert("minStateHoldS", minStateHoldS); + return m; +} + +LoadConfigSgReady LoadConfigSgReady::fromMap(const QVariantMap &map) +{ + LoadConfigSgReady p; + for (const QVariant &v : map.value("states").toList()) + p.states.append(LoadConfigSgReadyState::fromMap(v.toMap())); + p.minStateHoldS = map.value("minStateHoldS").toInt(); + return p; +} + +bool LoadConfigSgReady::hasState(int s) const +{ + for (const LoadConfigSgReadyState &e : states) + if (e.state == s) + return true; + return false; +} + // ---- LoadConfig ------------------------------------------------------------ +QVariantMap LoadConfig::sgReady() const +{ + return m_sgReady.states.isEmpty() ? QVariantMap() : m_sgReady.toMap(); +} + +void LoadConfig::setSgReady(const QVariantMap &v) +{ + m_sgReady = LoadConfigSgReady::fromMap(v); +} + QVariantList LoadConfig::relays() const { QVariantList list; @@ -89,6 +148,53 @@ bool LoadConfig::isValid(QString *error) const if (m_mode != QStringLiteral("fixed") && m_mode != QStringLiteral("dynamic")) return fail(QStringLiteral("mode doit être \"fixed\" ou \"dynamic\" (reçu: %1)").arg(m_mode)); + // LM-300 — UNION DISCRIMINÉE par mécanisme. Le discriminant est \c adapter ; chaque + // mécanisme a UNE charge utile, et les charges utiles des autres mécanismes DOIVENT être + // absentes (LM-302 : les états invalides doivent être inexprimables). Ajouter un + // mécanisme = un type de charge utile + une branche ici, rien d'autre (LM-301). + const bool aRelays = !m_relays.isEmpty(); + const bool aSgReady = !m_sgReady.states.isEmpty(); + const bool aLevels = !m_powerLevels.isEmpty() || m_maxPowerW > 0; + + if (m_adapter == QStringLiteral("sg-ready")) { + if (aRelays) + return fail(QStringLiteral("sg-ready : relays[] interdit (charge utile d'un autre mécanisme)")); + if (aLevels) + return fail(QStringLiteral("sg-ready : powerLevels/maxPowerW interdits (charge utile d'un autre mécanisme)")); + if (!aSgReady) + return fail(QStringLiteral("sg-ready : charge utile sgReady.states[] requise (non vide)")); + if (m_mode != QStringLiteral("fixed")) + return fail(QStringLiteral("sg-ready : mode doit être \"fixed\" (4 états normés, pas une modulation)")); + if (m_sgReady.minStateHoldS < 0) + return fail(QStringLiteral("sg-ready : minStateHoldS doit être ≥ 0")); + + QList vus; + for (const LoadConfigSgReadyState &e : m_sgReady.states) { + if (e.state < 1 || e.state > 4) + return fail(QStringLiteral("sg-ready : état %1 hors plage (1-4 normés)").arg(e.state)); + if (vus.contains(e.state)) + return fail(QStringLiteral("sg-ready : état %1 déclaré deux fois").arg(e.state)); + vus.append(e.state); + if (e.estimatedPowerW < 0) + return fail(QStringLiteral("sg-ready : état %1 a estimatedPowerW < 0").arg(e.state)); + for (const QString &r : e.relays) + if (r.isEmpty()) + return fail(QStringLiteral("sg-ready : état %1 référence un thingId vide").arg(e.state)); + } + + // ECS-110 / SAFETY.md — l'état 2 (normal, mains off) est le REPLI SÛR du mode dégradé + // L2. Une PAC dont la configuration ne permet pas de l'exprimer est REFUSÉE, jamais + // construite : c'est le garde-fou qui remplace le Q_ASSERT disparu en release. + if (!m_sgReady.hasState(2)) + return fail(QStringLiteral("sg-ready : l'état 2 (normal) est OBLIGATOIRE — c'est le " + "repli sûr du mode dégradé L2, sans lui la charge ne peut " + "pas être mise en sécurité")); + return true; + } + + if (aSgReady) + return fail(QStringLiteral("%1 : sgReady interdit (charge utile du mécanisme sg-ready)").arg(m_adapter)); + if (m_adapter == QStringLiteral("relay-router")) { // Cas relais (rév. 3) : liste de relais power. powerLevels/maxPowerW DÉRIVÉS (ignorés ici). if (m_mode != QStringLiteral("fixed")) @@ -121,7 +227,8 @@ bool LoadConfig::isValid(QString *error) const } } else { - return fail(QStringLiteral("adapter inconnu \"%1\" (attendu \"relay-router\" ou \"etmvariableload\")").arg(m_adapter)); + return fail(QStringLiteral("mécanisme inconnu \"%1\" (attendu \"relay-router\", " + "\"etmvariableload\" ou \"sg-ready\")").arg(m_adapter)); } return true; } @@ -141,6 +248,9 @@ QVariantMap LoadConfig::toMap() const const QVariantMap needs = m_needs.toMap(); if (!needs.isEmpty()) m.insert("needs", needs); + // LM-300 — charge utile sg-ready, sérialisée seulement pour ce mécanisme. + if (!m_sgReady.states.isEmpty()) + m.insert("sgReady", m_sgReady.toMap()); // rév. 3 — relais (cas relay-router) : sérialisés seulement s'ils existent. if (!m_relays.isEmpty()) { m.insert("relays", relays()); @@ -163,6 +273,7 @@ LoadConfig LoadConfig::fromMap(const QVariantMap &map) c.m_enabled = map.value("enabled", true).toBool(); c.m_needs = LoadConfigNeeds::fromMap(map.value("needs").toMap()); c.setRelays(map.value("relays").toList()); + c.setSgReady(map.value("sgReady").toMap()); c.m_minOnS = map.value("minOnS").toInt(); c.m_minOffS = map.value("minOffS").toInt(); return c; diff --git a/energyplugin/etm/types/loadconfig.h b/energyplugin/etm/types/loadconfig.h index a320831..e6c9003 100644 --- a/energyplugin/etm/types/loadconfig.h +++ b/energyplugin/etm/types/loadconfig.h @@ -62,6 +62,47 @@ struct LoadConfigRelay static LoadConfigRelay fromMap(const QVariantMap &map); }; +/*! + * \brief Un état SG-Ready configuré (charge utile du mécanisme \c sg-ready). + * + * \c estimatedPowerW porte bien son nom : c'est une **ESTIMATION**, pas un engagement ni une + * mesure. Une PAC ne consomme pas la même chose à −5 °C et à +12 °C. Elle sert de base au + * recrédit budget et au choix d'état ; la traiter comme une puissance garantie ferait dériver + * l'allocation de tout le waterfall. + */ +struct LoadConfigSgReadyState +{ + int state = 0; //!< 1 = blocage · 2 = normal · 3 = recommandation · 4 = forcé. + QStringList relays; //!< ThingIds \c power à FERMER pour cet état (les autres ouverts). + double estimatedPowerW = 0; //!< Puissance ESTIMÉE (W) de cet état — jamais un engagement. + + QVariantMap toMap() const; + //! \brief Construit depuis la forme sérialisée. \param map Map d'un état. + //! \return État ; champs absents = valeurs par défaut, la validation est ailleurs. + static LoadConfigSgReadyState fromMap(const QVariantMap &map); +}; + +/*! + * \brief Charge utile du mécanisme \c sg-ready (LM-300 : union discriminée par mécanisme). + * + * \invariant L'**état 2** (normal, mains off) DOIT être exprimable : c'est le repli sûr du + * mode dégradé L2 (\c docs/SAFETY.md). Une configuration qui ne le permet pas est REFUSÉE + * par \c LoadConfig::isValid(), jamais construite — ECS-110 interdit de faire reposer cette + * garantie sur un \c Q_ASSERT, absent du binaire release. + */ +struct LoadConfigSgReady +{ + QList states; + int minStateHoldS = 0; //!< Maintien minimal d'état (s) — anti court-cycling. + + QVariantMap toMap() const; + //! \brief Construit depuis la forme sérialisée. \param map Map \c sgReady. + //! \return Charge utile NON validée — cf. \c LoadConfig::isValid(). + static LoadConfigSgReady fromMap(const QVariantMap &map); + //! \return Vrai si l'état \p s est déclaré avec un encodage. + bool hasState(int s) const; +}; + /*! * \brief Déclaration de config d'une charge pilotée — contrat etmvariableload §4. * @@ -89,6 +130,8 @@ class LoadConfig Q_PROPERTY(QVariantList relays READ relays WRITE setRelays) //!< [{thingId, powerW}] Q_PROPERTY(int minOnS READ minOnS WRITE setMinOnS) Q_PROPERTY(int minOffS READ minOffS WRITE setMinOffS) + //! LM-300 — charge utile du mécanisme \c sg-ready : {states:[{state,relays,estimatedPowerW}], minStateHoldS} + Q_PROPERTY(QVariantMap sgReady READ sgReady WRITE setSgReady) public: //! \brief Construit une config vide — \c adapter vaut "etmvariableload", \c enabled vrai. LoadConfig() {} @@ -156,6 +199,16 @@ public: //! \param v Durée minimale OFF (s), ≥ 0. void setMinOffS(int v) { m_minOffS = v; } + //! \return Charge utile \c sg-ready sérialisée ; vide hors de ce mécanisme. + QVariantMap sgReady() const; + //! \param v Charge utile \c sg-ready ; réservée au mécanisme du même nom. + void setSgReady(const QVariantMap &v); + //! \return La même charge utile, typée — source de construction du SgReadyAdapter. + LoadConfigSgReady sgReadyPayload() const { return m_sgReady; } + + //! \return Vrai si le mécanisme est \c sg-ready (PAC à 4 états normés). + bool isSgReady() const { return m_adapter == QStringLiteral("sg-ready"); } + //! Vrai si la charge est un routeur de relais (rév. 3 : combinatoire watts→relais côté routeur). //! \return Vrai si \c adapter == "relay-router" — décide de la classe construite au rebuild. bool isRelayRouter() const { return m_adapter == QStringLiteral("relay-router"); } @@ -190,6 +243,7 @@ private: bool m_enabled = true; LoadConfigNeeds m_needs; QList m_relays; //!< rév. 3 — relais du routeur (cas relay-router). + LoadConfigSgReady m_sgReady; //!< LM-300 — charge utile du mécanisme sg-ready. int m_minOnS = 0; int m_minOffS = 0; };