feat(etm): LoadConfig rév. 3 — forme relay-router (relays[]) + validation conditionnelle
LoadConfig gagne la forme "relay-router" (ECS multipalier rév. 3) : liste de relais
power (relays[] = [{thingId, powerW}]) + minOnS/minOffS. Coexiste avec la forme
"etmvariableload" (continu/dynamic ou multipalier natif). powerLevels NON stockés pour
le cas relais (DÉRIVÉS par le routeur en étape 3-4).
- LoadConfigRelay (struct) + relays/minOnS/minOffS dans LoadConfig (Q_GADGET + toMap/fromMap).
- isValid() CONDITIONNELLE au type (adapter/mode), pas "tous champs requis" :
relay-router → relays[] valides ; etmvariableload → powerLevels (fixed) | maxPowerW (dynamic).
- Schéma SET : TOUS les champs spécifiques en "o:" (powerLevels, maxPowerW, relays, minOnS,
minOffS, needs) — sinon nymea rejette une des deux formes avant le handler (bug objectRef
strict de T4, doublé en rév. 3). GET reste typé objectRef<LoadConfig>.
- testLoadConfigRpc étendu : SET des DEUX formes (etmvariableload + relay-router) tous deux
acceptés, round-trip relays[]/minOnS, rejets conditionnels (powerLevels sans 0 ; relays[] vide).
Build prod 0/0 ; suite config/L2/migrés verte.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
11f5c36b3a
commit
c86513e10f
@ -24,8 +24,41 @@ LoadConfigNeeds LoadConfigNeeds::fromMap(const QVariantMap &map)
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- LoadConfigRelay -------------------------------------------------------
|
||||||
|
|
||||||
|
QVariantMap LoadConfigRelay::toMap() const
|
||||||
|
{
|
||||||
|
QVariantMap m;
|
||||||
|
m.insert("thingId", thingId);
|
||||||
|
m.insert("powerW", powerW);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadConfigRelay LoadConfigRelay::fromMap(const QVariantMap &map)
|
||||||
|
{
|
||||||
|
LoadConfigRelay r;
|
||||||
|
r.thingId = map.value("thingId").toString();
|
||||||
|
r.powerW = map.value("powerW").toInt();
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- LoadConfig ------------------------------------------------------------
|
// ---- LoadConfig ------------------------------------------------------------
|
||||||
|
|
||||||
|
QVariantList LoadConfig::relays() const
|
||||||
|
{
|
||||||
|
QVariantList list;
|
||||||
|
for (const LoadConfigRelay &r : m_relays)
|
||||||
|
list.append(r.toMap());
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadConfig::setRelays(const QVariantList &v)
|
||||||
|
{
|
||||||
|
m_relays.clear();
|
||||||
|
for (const QVariant &item : v)
|
||||||
|
m_relays.append(LoadConfigRelay::fromMap(item.toMap()));
|
||||||
|
}
|
||||||
|
|
||||||
QVariantList LoadConfig::powerLevels() const
|
QVariantList LoadConfig::powerLevels() const
|
||||||
{
|
{
|
||||||
QVariantList list;
|
QVariantList list;
|
||||||
@ -48,23 +81,47 @@ bool LoadConfig::isValid(QString *error) const
|
|||||||
{
|
{
|
||||||
auto fail = [&](const QString &msg) { if (error) *error = msg; return false; };
|
auto fail = [&](const QString &msg) { if (error) *error = msg; return false; };
|
||||||
|
|
||||||
if (m_adapter != QStringLiteral("etmvariableload"))
|
// Validation CONDITIONNELLE au type (contrat rév. 3) — pas "tous champs requis".
|
||||||
return fail(QStringLiteral("adapter doit être \"etmvariableload\" (reçu: %1)").arg(m_adapter));
|
// Les deux formes (relay-router / etmvariableload) sont mutuellement exclusives ; le schéma
|
||||||
|
// JSON-RPC SET marque TOUS les champs spécifiques en optionnel, c'est ICI qu'on tranche.
|
||||||
if (m_id.isEmpty())
|
if (m_id.isEmpty())
|
||||||
return fail(QStringLiteral("id de charge vide"));
|
return fail(QStringLiteral("id de charge vide"));
|
||||||
if (m_mode != QStringLiteral("fixed") && m_mode != QStringLiteral("dynamic"))
|
if (m_mode != QStringLiteral("fixed") && m_mode != QStringLiteral("dynamic"))
|
||||||
return fail(QStringLiteral("mode doit être \"fixed\" ou \"dynamic\" (reçu: %1)").arg(m_mode));
|
return fail(QStringLiteral("mode doit être \"fixed\" ou \"dynamic\" (reçu: %1)").arg(m_mode));
|
||||||
|
|
||||||
if (m_mode == QStringLiteral("fixed")) {
|
if (m_adapter == QStringLiteral("relay-router")) {
|
||||||
if (m_powerLevels.isEmpty())
|
// Cas relais (rév. 3) : liste de relais power. powerLevels/maxPowerW DÉRIVÉS (ignorés ici).
|
||||||
return fail(QStringLiteral("mode fixed : powerLevels requis (non vide)"));
|
if (m_mode != QStringLiteral("fixed"))
|
||||||
if (m_powerLevels.first() != 0)
|
return fail(QStringLiteral("relay-router : mode doit être \"fixed\""));
|
||||||
return fail(QStringLiteral("mode fixed : powerLevels doit inclure 0"));
|
if (m_relays.isEmpty())
|
||||||
} else { // dynamic
|
return fail(QStringLiteral("relay-router : relays[] requis (non vide)"));
|
||||||
if (m_maxPowerW <= 0)
|
for (const LoadConfigRelay &r : m_relays) {
|
||||||
return fail(QStringLiteral("mode dynamic : maxPowerW requis (> 0)"));
|
if (r.thingId.isEmpty())
|
||||||
if (!m_powerLevels.isEmpty())
|
return fail(QStringLiteral("relay-router : un relais a un thingId vide"));
|
||||||
return fail(QStringLiteral("mode dynamic : powerLevels doit être absent"));
|
if (r.powerW <= 0)
|
||||||
|
return fail(QStringLiteral("relay-router : relais %1 a powerW ≤ 0").arg(r.thingId));
|
||||||
|
}
|
||||||
|
if (m_minOnS < 0 || m_minOffS < 0)
|
||||||
|
return fail(QStringLiteral("relay-router : minOnS/minOffS doivent être ≥ 0"));
|
||||||
|
|
||||||
|
} else if (m_adapter == QStringLiteral("etmvariableload")) {
|
||||||
|
// Cas thing unique : continu (dynamic) ou multipalier natif (fixed + powerLevels, legacy).
|
||||||
|
if (!m_relays.isEmpty())
|
||||||
|
return fail(QStringLiteral("etmvariableload : relays[] interdit (réservé à relay-router)"));
|
||||||
|
if (m_mode == QStringLiteral("fixed")) {
|
||||||
|
if (m_powerLevels.isEmpty())
|
||||||
|
return fail(QStringLiteral("etmvariableload fixed : powerLevels requis (non vide)"));
|
||||||
|
if (m_powerLevels.first() != 0)
|
||||||
|
return fail(QStringLiteral("etmvariableload fixed : powerLevels doit inclure 0"));
|
||||||
|
} else { // dynamic
|
||||||
|
if (m_maxPowerW <= 0)
|
||||||
|
return fail(QStringLiteral("etmvariableload dynamic : maxPowerW requis (> 0)"));
|
||||||
|
if (!m_powerLevels.isEmpty())
|
||||||
|
return fail(QStringLiteral("etmvariableload dynamic : powerLevels doit être absent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return fail(QStringLiteral("adapter inconnu \"%1\" (attendu \"relay-router\" ou \"etmvariableload\")").arg(m_adapter));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -84,6 +141,12 @@ QVariantMap LoadConfig::toMap() const
|
|||||||
const QVariantMap needs = m_needs.toMap();
|
const QVariantMap needs = m_needs.toMap();
|
||||||
if (!needs.isEmpty())
|
if (!needs.isEmpty())
|
||||||
m.insert("needs", needs);
|
m.insert("needs", needs);
|
||||||
|
// rév. 3 — relais (cas relay-router) : sérialisés seulement s'ils existent.
|
||||||
|
if (!m_relays.isEmpty()) {
|
||||||
|
m.insert("relays", relays());
|
||||||
|
m.insert("minOnS", m_minOnS);
|
||||||
|
m.insert("minOffS", m_minOffS);
|
||||||
|
}
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +162,9 @@ LoadConfig LoadConfig::fromMap(const QVariantMap &map)
|
|||||||
c.m_priority = map.value("priority").toInt();
|
c.m_priority = map.value("priority").toInt();
|
||||||
c.m_enabled = map.value("enabled", true).toBool();
|
c.m_enabled = map.value("enabled", true).toBool();
|
||||||
c.m_needs = LoadConfigNeeds::fromMap(map.value("needs").toMap());
|
c.m_needs = LoadConfigNeeds::fromMap(map.value("needs").toMap());
|
||||||
|
c.setRelays(map.value("relays").toList());
|
||||||
|
c.m_minOnS = map.value("minOnS").toInt();
|
||||||
|
c.m_minOffS = map.value("minOffS").toInt();
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,20 @@ private:
|
|||||||
};
|
};
|
||||||
Q_DECLARE_METATYPE(LoadConfigNeeds)
|
Q_DECLARE_METATYPE(LoadConfigNeeds)
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Un relais d'une charge \c relay-router (contrat rév. 3 §4) : un thing \c power + sa
|
||||||
|
* puissance nominale (W). Imbriqué dans \c LoadConfig.relays — vit côté ROUTEUR, ne franchit
|
||||||
|
* JAMAIS la frontière vers l'optimiseur (qui ne voit que des watts dérivés).
|
||||||
|
*/
|
||||||
|
struct LoadConfigRelay
|
||||||
|
{
|
||||||
|
QString thingId; //!< ThingId du thing implémentant l'interface \c power.
|
||||||
|
int powerW = 0; //!< Puissance nominale de ce relais (W).
|
||||||
|
|
||||||
|
QVariantMap toMap() const;
|
||||||
|
static LoadConfigRelay fromMap(const QVariantMap &map);
|
||||||
|
};
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Déclaration de config d'une charge pilotée — contrat etmvariableload §4.
|
* \brief Déclaration de config d'une charge pilotée — contrat etmvariableload §4.
|
||||||
*
|
*
|
||||||
@ -59,6 +73,10 @@ class LoadConfig
|
|||||||
Q_PROPERTY(int priority READ priority WRITE setPriority)
|
Q_PROPERTY(int priority READ priority WRITE setPriority)
|
||||||
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled)
|
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled)
|
||||||
Q_PROPERTY(LoadConfigNeeds needs READ needs WRITE setNeeds)
|
Q_PROPERTY(LoadConfigNeeds needs READ needs WRITE setNeeds)
|
||||||
|
// rév. 3 — cas relay-router (la combinatoire vit côté routeur, jamais chez l'optimiseur) :
|
||||||
|
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)
|
||||||
public:
|
public:
|
||||||
LoadConfig() {}
|
LoadConfig() {}
|
||||||
|
|
||||||
@ -86,8 +104,20 @@ public:
|
|||||||
LoadConfigNeeds needs() const { return m_needs; }
|
LoadConfigNeeds needs() const { return m_needs; }
|
||||||
void setNeeds(const LoadConfigNeeds &v) { m_needs = v; }
|
void setNeeds(const LoadConfigNeeds &v) { m_needs = v; }
|
||||||
|
|
||||||
//! Vrai si *dynamic* (powerLevels vide). Sinon *fixed*.
|
//! relays en QVariantList (introspection / sérialisation) ; typé via relaysList().
|
||||||
bool isDynamic() const { return m_powerLevels.isEmpty(); }
|
QVariantList relays() const;
|
||||||
|
void setRelays(const QVariantList &v);
|
||||||
|
QList<LoadConfigRelay> relaysList() const { return m_relays; }
|
||||||
|
|
||||||
|
int minOnS() const { return m_minOnS; }
|
||||||
|
void setMinOnS(int v) { m_minOnS = v; }
|
||||||
|
int minOffS() const { return m_minOffS; }
|
||||||
|
void setMinOffS(int v) { m_minOffS = v; }
|
||||||
|
|
||||||
|
//! Vrai si la charge est un routeur de relais (rév. 3 : combinatoire watts→relais côté routeur).
|
||||||
|
bool isRelayRouter() const { return m_adapter == QStringLiteral("relay-router"); }
|
||||||
|
//! Vrai si *dynamic* (modulation continue). Sinon *fixed*.
|
||||||
|
bool isDynamic() const { return m_mode == QStringLiteral("dynamic"); }
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Valide la cohérence (contrat §4/§6) : adapter, id, mode, paliers/plafond.
|
* \brief Valide la cohérence (contrat §4/§6) : adapter, id, mode, paliers/plafond.
|
||||||
@ -109,6 +139,9 @@ private:
|
|||||||
int m_priority = 0;
|
int m_priority = 0;
|
||||||
bool m_enabled = true;
|
bool m_enabled = true;
|
||||||
LoadConfigNeeds m_needs;
|
LoadConfigNeeds m_needs;
|
||||||
|
QList<LoadConfigRelay> m_relays; //!< rév. 3 — relais du routeur (cas relay-router).
|
||||||
|
int m_minOnS = 0;
|
||||||
|
int m_minOffS = 0;
|
||||||
};
|
};
|
||||||
Q_DECLARE_METATYPE(LoadConfig)
|
Q_DECLARE_METATYPE(LoadConfig)
|
||||||
|
|
||||||
|
|||||||
@ -153,23 +153,31 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
|||||||
params.clear(); returns.clear();
|
params.clear(); returns.clear();
|
||||||
description = "Replace the full set of controllable load configs. Rejected as a whole "
|
description = "Replace the full set of controllable load configs. Rejected as a whole "
|
||||||
"(energyError) if any entry is invalid; persisted and applied atomically.";
|
"(energyError) if any entry is invalid; persisted and applied atomically.";
|
||||||
// Schéma inline (pas objectRef<LoadConfig>) : le contrat §4 rend powerLevels conditionnel
|
// Schéma inline (pas objectRef<LoadConfig>) : le contrat rév. 3 a DEUX formes exclusives
|
||||||
// (absent si dynamic) et needs optionnel — objectRef rendrait TOUS les champs requis et
|
// (relay-router : relays[]+minOnS/minOffS — etmvariableload : powerLevels|maxPowerW).
|
||||||
// rejetterait l'entrée §4 de l'app. Les clés "o:" marquent l'optionnel. Le GET reste typé
|
// objectRef rendrait TOUS les champs requis → nymea rejetterait une des deux formes AVANT
|
||||||
// objectRef<LoadConfig> (pack émet toujours tous les champs).
|
// le handler. On marque donc TOUS les champs spécifiques "o:" ; la validation conditionnelle
|
||||||
|
// (par adapter/mode) vit dans LoadConfig::isValid(). Seuls id/label/adapter/mode/priority/
|
||||||
|
// enabled sont requis (présents dans les deux formes). GET reste typé objectRef<LoadConfig>.
|
||||||
QVariantMap needsItem;
|
QVariantMap needsItem;
|
||||||
needsItem.insert("o:dailyDeadline", enumValueName(String));
|
needsItem.insert("o:dailyDeadline", enumValueName(String));
|
||||||
needsItem.insert("o:minEnergyWhPerDay", enumValueName(Uint));
|
needsItem.insert("o:minEnergyWhPerDay", enumValueName(Uint));
|
||||||
|
QVariantMap relayItem;
|
||||||
|
relayItem.insert("thingId", enumValueName(String));
|
||||||
|
relayItem.insert("powerW", enumValueName(Uint));
|
||||||
QVariantMap loadItem;
|
QVariantMap loadItem;
|
||||||
loadItem.insert("id", enumValueName(String));
|
loadItem.insert("id", enumValueName(String));
|
||||||
loadItem.insert("label", enumValueName(String));
|
loadItem.insert("label", enumValueName(String));
|
||||||
loadItem.insert("adapter", enumValueName(String));
|
loadItem.insert("adapter", enumValueName(String));
|
||||||
loadItem.insert("mode", enumValueName(String));
|
loadItem.insert("mode", enumValueName(String));
|
||||||
loadItem.insert("o:powerLevels", QVariantList() << enumValueName(Uint));
|
|
||||||
loadItem.insert("maxPowerW", enumValueName(Uint));
|
|
||||||
loadItem.insert("priority", enumValueName(Uint));
|
loadItem.insert("priority", enumValueName(Uint));
|
||||||
loadItem.insert("enabled", enumValueName(Bool));
|
loadItem.insert("enabled", enumValueName(Bool));
|
||||||
|
loadItem.insert("o:powerLevels", QVariantList() << enumValueName(Uint)); // etmvariableload fixed
|
||||||
|
loadItem.insert("o:maxPowerW", enumValueName(Uint)); // etmvariableload dynamic
|
||||||
loadItem.insert("o:needs", needsItem);
|
loadItem.insert("o:needs", needsItem);
|
||||||
|
loadItem.insert("o:relays", QVariantList() << relayItem); // relay-router (rév. 3)
|
||||||
|
loadItem.insert("o:minOnS", enumValueName(Uint));
|
||||||
|
loadItem.insert("o:minOffS", enumValueName(Uint));
|
||||||
params.insert("loadConfigs", QVariantList() << loadItem);
|
params.insert("loadConfigs", QVariantList() << loadItem);
|
||||||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||||||
registerMethod("SetLoadConfig", description, params, returns, Types::PermissionScopeControlThings);
|
registerMethod("SetLoadConfig", description, params, returns, Types::PermissionScopeControlThings);
|
||||||
|
|||||||
@ -381,7 +381,9 @@ void Simulation::testLoadConfigRpc()
|
|||||||
QVariant resp = injectAndWait("NymeaEnergy.GetLoadConfig");
|
QVariant resp = injectAndWait("NymeaEnergy.GetLoadConfig");
|
||||||
QVERIFY(resp.toMap().value("params").toMap().value("loadConfigs").toList().isEmpty());
|
QVERIFY(resp.toMap().value("params").toMap().value("loadConfigs").toList().isEmpty());
|
||||||
|
|
||||||
// 2. SetLoadConfig : une fixed (thing réel) + une dynamic.
|
// 2. SetLoadConfig : les DEUX formes rév. 3 (mutuellement exclusives) doivent être acceptées
|
||||||
|
// par le MÊME schéma SET — sinon nymea rejette une forme avant le handler (bug objectRef
|
||||||
|
// strict de T4, doublé). Une etmvariableload fixed (thing réel) + une dynamic + une relay-router.
|
||||||
QVariantList loadConfigs;
|
QVariantList loadConfigs;
|
||||||
loadConfigs << QVariantMap{
|
loadConfigs << QVariantMap{
|
||||||
{"id", ecsId.toString()}, {"label", "Chauffe-eau"}, {"adapter", "etmvariableload"},
|
{"id", ecsId.toString()}, {"label", "Chauffe-eau"}, {"adapter", "etmvariableload"},
|
||||||
@ -392,16 +394,26 @@ void Simulation::testLoadConfigRpc()
|
|||||||
{"id", "{b033b212-1adb-4df0-ba2b-8fa477de52a2}"}, {"label", "Routeur PV"},
|
{"id", "{b033b212-1adb-4df0-ba2b-8fa477de52a2}"}, {"label", "Routeur PV"},
|
||||||
{"adapter", "etmvariableload"}, {"mode", "dynamic"}, {"maxPowerW", 3000},
|
{"adapter", "etmvariableload"}, {"mode", "dynamic"}, {"maxPowerW", 3000},
|
||||||
{"priority", 1}, {"enabled", true}};
|
{"priority", 1}, {"enabled", true}};
|
||||||
|
loadConfigs << QVariantMap{ // ← rév. 3 : relay-router (relays[])
|
||||||
|
{"id", "ecs-relais"}, {"label", "ECS relais"}, {"adapter", "relay-router"},
|
||||||
|
{"mode", "fixed"}, {"priority", 3}, {"enabled", true},
|
||||||
|
{"relays", QVariantList()
|
||||||
|
<< QVariantMap{{"thingId", "{aaaaaaaa-1111-2222-3333-444444444444}"}, {"powerW", 1000}}
|
||||||
|
<< QVariantMap{{"thingId", "{bbbbbbbb-1111-2222-3333-444444444444}"}, {"powerW", 1500}}},
|
||||||
|
{"minOnS", 60}, {"minOffS", 60}};
|
||||||
resp = injectAndWait("NymeaEnergy.SetLoadConfig", {{"loadConfigs", loadConfigs}});
|
resp = injectAndWait("NymeaEnergy.SetLoadConfig", {{"loadConfigs", loadConfigs}});
|
||||||
QCOMPARE(resp.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorNoError"));
|
QCOMPARE(resp.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorNoError"));
|
||||||
|
|
||||||
// 3. GetLoadConfig → round-trip des 2 entrées, champs §4 préservés (via pack/unpack Q_GADGET).
|
// 3. GetLoadConfig → round-trip des 3 entrées (les 2 formes), champs préservés (pack/unpack).
|
||||||
resp = injectAndWait("NymeaEnergy.GetLoadConfig");
|
resp = injectAndWait("NymeaEnergy.GetLoadConfig");
|
||||||
const QVariantList got = resp.toMap().value("params").toMap().value("loadConfigs").toList();
|
const QVariantList got = resp.toMap().value("params").toMap().value("loadConfigs").toList();
|
||||||
QCOMPARE(got.size(), 2);
|
QCOMPARE(got.size(), 3);
|
||||||
QVariantMap fixedGot;
|
QVariantMap fixedGot, relayGot;
|
||||||
for (const QVariant &v : got)
|
for (const QVariant &v : got) {
|
||||||
if (v.toMap().value("id").toString() == ecsId.toString()) fixedGot = v.toMap();
|
const QVariantMap m = v.toMap();
|
||||||
|
if (m.value("id").toString() == ecsId.toString()) fixedGot = m;
|
||||||
|
if (m.value("adapter").toString() == "relay-router") relayGot = m;
|
||||||
|
}
|
||||||
QVERIFY(!fixedGot.isEmpty());
|
QVERIFY(!fixedGot.isEmpty());
|
||||||
QCOMPARE(fixedGot.value("mode").toString(), QString("fixed"));
|
QCOMPARE(fixedGot.value("mode").toString(), QString("fixed"));
|
||||||
QCOMPARE(fixedGot.value("label").toString(), QString("Chauffe-eau"));
|
QCOMPARE(fixedGot.value("label").toString(), QString("Chauffe-eau"));
|
||||||
@ -409,6 +421,12 @@ void Simulation::testLoadConfigRpc()
|
|||||||
QCOMPARE(levels.size(), 3);
|
QCOMPARE(levels.size(), 3);
|
||||||
QCOMPARE(levels.last().toInt(), 2400);
|
QCOMPARE(levels.last().toInt(), 2400);
|
||||||
QCOMPARE(fixedGot.value("needs").toMap().value("dailyDeadline").toString(), QString("06:00"));
|
QCOMPARE(fixedGot.value("needs").toMap().value("dailyDeadline").toString(), QString("06:00"));
|
||||||
|
// Forme relay-router : relays[] + minOnS round-trip (jamais piloté ici — RelayRouter en étape 4).
|
||||||
|
QVERIFY(!relayGot.isEmpty());
|
||||||
|
QVariantList relays = relayGot.value("relays").toList();
|
||||||
|
QCOMPARE(relays.size(), 2);
|
||||||
|
QCOMPARE(relays.last().toMap().value("powerW").toInt(), 1500);
|
||||||
|
QCOMPARE(relayGot.value("minOnS").toInt(), 60);
|
||||||
|
|
||||||
// 4. Fichier persisté.
|
// 4. Fichier persisté.
|
||||||
QVERIFY(QFileInfo::exists(cfgPath));
|
QVERIFY(QFileInfo::exists(cfgPath));
|
||||||
@ -421,17 +439,27 @@ void Simulation::testLoadConfigRpc()
|
|||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
QCOMPARE(qRound(ecsThing->stateValue("powerSetpoint").toDouble()), 2400); // reliquat 3000 → palier 2400
|
QCOMPARE(qRound(ecsThing->stateValue("powerSetpoint").toDouble()), 2400); // reliquat 3000 → palier 2400
|
||||||
|
|
||||||
// 6. Config invalide (fixed sans 0) → rejet en bloc, energyError InvalidParameter.
|
// 6. Validation CONDITIONNELLE — rejet en bloc (energyError InvalidParameter) :
|
||||||
QVariantList bad;
|
// (a) etmvariableload fixed sans 0 dans powerLevels ; (b) relay-router avec relays[] vide.
|
||||||
bad << QVariantMap{
|
QVariantList badA;
|
||||||
|
badA << QVariantMap{
|
||||||
{"id", "{cccccccc-2222-3333-4444-555555555555}"}, {"label", "Bancal"}, {"adapter", "etmvariableload"},
|
{"id", "{cccccccc-2222-3333-4444-555555555555}"}, {"label", "Bancal"}, {"adapter", "etmvariableload"},
|
||||||
{"mode", "fixed"}, {"powerLevels", QVariantList() << 1200 << 2400}, {"maxPowerW", 2400},
|
{"mode", "fixed"}, {"powerLevels", QVariantList() << 1200 << 2400}, {"maxPowerW", 2400},
|
||||||
{"priority", 1}, {"enabled", true}};
|
{"priority", 1}, {"enabled", true}};
|
||||||
resp = injectAndWait("NymeaEnergy.SetLoadConfig", {{"loadConfigs", bad}});
|
resp = injectAndWait("NymeaEnergy.SetLoadConfig", {{"loadConfigs", badA}});
|
||||||
QCOMPARE(resp.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorInvalidParameter"));
|
QCOMPARE(resp.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorInvalidParameter"));
|
||||||
// Le rejet n'a rien écrasé : GetLoadConfig retourne toujours les 2 valides.
|
|
||||||
|
QVariantList badB;
|
||||||
|
badB << QVariantMap{
|
||||||
|
{"id", "ecs-vide"}, {"label", "ECS sans relais"}, {"adapter", "relay-router"},
|
||||||
|
{"mode", "fixed"}, {"priority", 1}, {"enabled", true},
|
||||||
|
{"relays", QVariantList()}}; // relays[] vide → invalide
|
||||||
|
resp = injectAndWait("NymeaEnergy.SetLoadConfig", {{"loadConfigs", badB}});
|
||||||
|
QCOMPARE(resp.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorInvalidParameter"));
|
||||||
|
|
||||||
|
// Aucun rejet n'a écrasé : GetLoadConfig retourne toujours les 3 valides.
|
||||||
resp = injectAndWait("NymeaEnergy.GetLoadConfig");
|
resp = injectAndWait("NymeaEnergy.GetLoadConfig");
|
||||||
QCOMPARE(resp.toMap().value("params").toMap().value("loadConfigs").toList().size(), 2);
|
QCOMPARE(resp.toMap().value("params").toMap().value("loadConfigs").toList().size(), 3);
|
||||||
|
|
||||||
qunsetenv("NYMEA_ENERGY_LOAD_CONFIG");
|
qunsetenv("NYMEA_ENERGY_LOAD_CONFIG");
|
||||||
QFile::remove(cfgPath);
|
QFile::remove(cfgPath);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user