fix(optimiseur): token déclaré String, type contrôlé par le moteur ; GetPlan sous verrou (brief 4 T4)
Sous Qt 6, le validateur nymea ne contrôle pas le type des paramètres de base : la déclaration Variant ne protégeait de rien (contre-épreuve inerte), le contrôle du handler est la seule garde. Retour à String, plus lisible à l'introspection ; INTERFACE.md corrigé (seule une clé EN TROP fait recopier les paramètres au journal par nymea). testGetPlanShowsTheAppliedPowerUnderALock : sous le rule-based, un verrou minOn tient le relais ; GetPlan publie l'appliqué. Avec l'écrêtage amont retiré, c'est la relecture de l'appliqué qui protège seule (double mutation, elle mord). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7a32961b23
commit
e5926189e9
@ -575,10 +575,11 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
||||
// WRITE-ONLY (décision Q2) : aucune méthode ne relit le token. Admin : c'est un secret, et il
|
||||
// ouvre l'optimiseur à qui le détient — même niveau que les réglages d'installation du
|
||||
// handler restés au défaut nymea (SetPhasePowerLimit, SetBatteryLevelConsideration).
|
||||
// `token` est déclaré Variant, et son type vérifié ici : un paramètre que le VALIDATEUR nymea
|
||||
// refuse est recopié en entier dans un avertissement (« Call params: », dcJsonRpc) — un token
|
||||
// envoyé en nombre finirait au journal. Une clé en trop, elle, y finit quand même : le
|
||||
// validateur la refuse avant que le handler ne voie l'appel (INTERFACE.md).
|
||||
// Le TYPE de `token` est vérifié ici, pas par nymea : sous Qt 6, le validateur ne contrôle
|
||||
// plus le type des paramètres de base (jsonvalidator.cpp, #if QT_VERSION >= 6), un nombre
|
||||
// arrive donc au handler. Une clé EN TROP, elle, est refusée par le validateur, qui recopie
|
||||
// alors les paramètres dans un avertissement (« Call params: », dcJsonRpc) — token compris,
|
||||
// et hors de portée du plugin (INTERFACE.md).
|
||||
params.clear(); returns.clear();
|
||||
description = "Set the shared secret sent to the optimizer at handshake. WRITE-ONLY: no method "
|
||||
"returns it (GetOptimizerStatus has tokenConfigured). Stored in its own file, mode "
|
||||
@ -587,7 +588,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
||||
"refusal: EnergyErrorInvalidParameter, fault token-refused, nothing is changed. "
|
||||
"Send ONLY the 'token' key: nymea logs, as a warning, the params of a call whose "
|
||||
"keys it rejects.";
|
||||
params.insert("token", enumValueName(Variant));
|
||||
params.insert("token", enumValueName(String));
|
||||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||||
returns.insert("o:fault", enumValueName(String));
|
||||
registerMethod("SetOptimizerToken", description, params, returns, Types::PermissionScopeAdmin);
|
||||
|
||||
@ -115,6 +115,7 @@ private slots:
|
||||
void testEndpointIsSetByRpcAndAppliedHot();
|
||||
void testTokenIsWriteOnlyAndNeverLeaks();
|
||||
void testGetPlanPublishesThePlanInForce();
|
||||
void testGetPlanShowsTheAppliedPowerUnderALock();
|
||||
void testUpdateLearnedIsLoggedOnceAndIgnored();
|
||||
|
||||
private:
|
||||
@ -1332,6 +1333,36 @@ void TestSocketScheduler::testGetPlanPublishesThePlanInForce()
|
||||
QCOMPARE(rpc("GetPlan"), (QVariantMap{{"inForce", false}}));
|
||||
}
|
||||
|
||||
void TestSocketScheduler::testGetPlanShowsTheAppliedPowerUnderALock()
|
||||
{
|
||||
// Le créneau courant publie ce que l'adaptateur a APPLIQUÉ, pas ce que la stratégie a
|
||||
// demandé. Sous l'optimiseur les deux coïncident déjà dans le créneau (finaliser(), T3) ;
|
||||
// le cas qui les sépare est le rule-based face à un verrou interne à l'adaptateur.
|
||||
monter(false);
|
||||
QVariantList l;
|
||||
l.append(QVariantMap{{"id", "ecs"}, {"label", "ECS"}, {"adapter", "relay-router"}, {"mode", "fixed"},
|
||||
{"priority", 1}, {"enabled", true}, {"minOnS", 600}, {"minOffS", 0},
|
||||
{"relays", QVariantList() << QVariantMap{{"thingId", m_relais.toString()}, {"powerW", 1000}}}});
|
||||
QCOMPARE(injectAndWait("NymeaEnergy.SetLoadConfig", QVariantMap({{"loadConfigs", l}}))
|
||||
.toMap().value("params").toMap().value("energyError").toString(),
|
||||
QString("EnergyErrorNoError"));
|
||||
auto ecs = [this]() {
|
||||
for (const QVariant &a : rpc("GetPlan").value("slots").toList().first().toMap().value("actions").toList())
|
||||
if (a.toMap().value("loadId").toString() == QLatin1String("ecs"))
|
||||
return a.toMap();
|
||||
return QVariantMap();
|
||||
};
|
||||
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||
cycle(t0, -3000);
|
||||
QCOMPARE(qRound(ecs().value("powerW").toDouble()), 1000);
|
||||
Thing *relais = NymeaCore::instance()->thingManager()->findConfiguredThing(m_relais);
|
||||
QCOMPARE(relais->stateValue("power").toBool(), true);
|
||||
// Import : la stratégie veut couper, le verrou minOn (600 s) tient le relais fermé.
|
||||
cycle(t0.addSecs(60), 2500);
|
||||
QCOMPARE(relais->stateValue("power").toBool(), true);
|
||||
QCOMPARE(qRound(ecs().value("powerW").toDouble()), 1000);
|
||||
}
|
||||
|
||||
void TestSocketScheduler::testUpdateLearnedIsLoggedOnceAndIgnored()
|
||||
{
|
||||
// §8 : reçu, journalisé UNE fois, ignoré — la persistance est un lot ultérieur.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user