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.