feat(config): LM-300 — union discriminée, charge utile sg-ready
Première implémentation réelle du schéma de spec_loadmodel.md §3. Le champ `adapter`
devient le discriminant ; chaque mécanisme a UNE charge utile et valide la sienne :
relay-router → relays[]
etmvariableload → powerLevels[] | maxPowerW
sg-ready → sgReady { states[] { state, relays[], estimatedPowerW }, minStateHoldS }
isValid() rejette les charges utiles étrangères dans les deux sens (LM-302 : les états
invalides doivent être inexprimables), et refuse toute configuration sg-ready dépourvue
de l'état 2, avec un message explicite.
estimatedPowerW garde ce nom parce que c'en est une : une PAC ne consomme pas la même
chose à −5 °C et à +12 °C. Le champ sert à ordonner et à budgéter ; la mesure reste la
source de vérité (ECS-500).
C'est un format PERSISTÉ : la forme est ce qui coûte cher à rattraper une fois que le
banc et la beta auront écrit des données. D'où l'union plutôt qu'un troisième cas
particulier.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
42cb2f080b
commit
ef67231ed1
@ -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<int> 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;
|
||||
|
||||
@ -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<LoadConfigSgReadyState> 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<LoadConfigRelay> 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;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user