From e5926189e9fbf5828bc95c7a3e963e14adb53cda Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sat, 17 Oct 2026 09:47:09 +0200 Subject: [PATCH] =?UTF-8?q?fix(optimiseur):=20token=20d=C3=A9clar=C3=A9=20?= =?UTF-8?q?String,=20type=20contr=C3=B4l=C3=A9=20par=20le=20moteur=20;=20G?= =?UTF-8?q?etPlan=20sous=20verrou=20(brief=204=20T4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- energyplugin/nymeaenergyjsonhandler.cpp | 11 ++++--- .../socketscheduler/testsocketscheduler.cpp | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/energyplugin/nymeaenergyjsonhandler.cpp b/energyplugin/nymeaenergyjsonhandler.cpp index 0bc0ef3..f4949c3 100644 --- a/energyplugin/nymeaenergyjsonhandler.cpp +++ b/energyplugin/nymeaenergyjsonhandler.cpp @@ -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()); returns.insert("o:fault", enumValueName(String)); registerMethod("SetOptimizerToken", description, params, returns, Types::PermissionScopeAdmin); diff --git a/tests/auto/socketscheduler/testsocketscheduler.cpp b/tests/auto/socketscheduler/testsocketscheduler.cpp index 83d2094..63232c0 100644 --- a/tests/auto/socketscheduler/testsocketscheduler.cpp +++ b/tests/auto/socketscheduler/testsocketscheduler.cpp @@ -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.