From a23161b3b6c81c82d25f61e4fa777bfa8252c0cd Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Mon, 12 Oct 2026 12:48:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(spot):=20le=20fournisseur=20aWATTar=20sort?= =?UTF-8?q?=20du=20build=20livr=C3=A9=20(brief=203,=20T0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le fournisseur spot aWATTar était le seul flux sortant du moteur (invariant 10). Il n'est plus compilé que sous `qmake CONFIG+=etm_spotmarket` ; la source reste dans l'arbre pour la synchronisation amont. Sans l'option (build livré) : - `spotmarketdataproviderawattar.*` hors de SOURCES/HEADERS, enregistrement sous `#ifdef ETM_SPOTMARKET` ; - aucun `QNetworkAccessManager` : le plugin et l'expérience de simulation passent par le même point, `SpotMarketBuild::creer()` ; - `SetSpotMarketConfiguration` refuse l'activation avec `EnergyErrorInvalidParameter` + `fault: SPOT_MARKET_NOT_IN_BUILD` (code, pas de phrase). Désactiver reste accepté ; - `planSpotMarketCharging()` est inerte : `enabled()` ne peut plus devenir vrai. Tests : - simulation `testSpotMarketIsOutOfTheBuild` ; contre-épreuve faite (un QNetworkAccessManager réintroduit fait échouer le test) ; - spotmarket `shippedBuildHasNoProvider`, sur le .so réel ; les 4 tests qui lisent la liste aWATTar sont sautés hors option et passent avec l'option ; - `testDecisionReasonRendering` : ligne de rendu du nouveau code. Co-Authored-By: Claude Opus 5.5 --- energyplugin/energyplugin.pri | 16 +++- energyplugin/energypluginnymea.cpp | 6 +- energyplugin/etm/etm.pri | 2 + energyplugin/etm/spotmarketbuild.cpp | 19 ++++ energyplugin/etm/spotmarketbuild.h | 45 ++++++++++ energyplugin/etm/types/decisionreason.cpp | 5 ++ energyplugin/etm/types/decisionreason.h | 11 +++ energyplugin/nymeaenergyjsonhandler.cpp | 24 +++++- energyplugin/spotmarket/spotmarketmanager.cpp | 7 ++ .../experience/energyexperienceenergymock.cpp | 6 +- tests/auto/simulation/simulation.cpp | 86 ++++++++++++++++++- tests/auto/simulation/simulation.h | 2 + tests/auto/spotmarket/testspotmarket.cpp | 36 ++++++++ tests/auto/spotmarket/testspotmarket.h | 4 + 14 files changed, 260 insertions(+), 9 deletions(-) create mode 100644 energyplugin/etm/spotmarketbuild.cpp create mode 100644 energyplugin/etm/spotmarketbuild.h diff --git a/energyplugin/energyplugin.pri b/energyplugin/energyplugin.pri index 24de5be..89b6066 100644 --- a/energyplugin/energyplugin.pri +++ b/energyplugin/energyplugin.pri @@ -7,6 +7,20 @@ INCLUDEPATH += $$PWD # Uncommenter pour activer. Commité DÉSACTIVÉ jusqu'à preuve iso (3b-iv). DEFINES += ETM_ARBITRATOR +# [ETM] Fournisseur spot aWATTar — HORS DU BUILD LIVRÉ (invariant 10 : aucun flux sortant). +# C'était le seul appel réseau du moteur : une requête HTTPS vers api.awattar.{at,de}. La +# source reste dans l'arbre pour la synchronisation amont ; elle n'est compilée qu'avec +# `qmake CONFIG+=etm_spotmarket`. Sans l'option : aucun fournisseur enregistré, aucun +# QNetworkAccessManager construit, et l'activation par RPC est refusée avec un code +# (SPOT_MARKET_NOT_IN_BUILD). Le gestionnaire et ses fonctions de calcul restent compilés : +# SmartChargingManager (amont) en dépend, et les suites de test y injectent un fournisseur mock. +etm_spotmarket { + message("Fournisseur spot aWATTar COMPILÉ (CONFIG+=etm_spotmarket) — flux sortant actif") + DEFINES += ETM_SPOTMARKET + HEADERS += $$PWD/spotmarket/spotmarketdataproviderawattar.h + SOURCES += $$PWD/spotmarket/spotmarketdataproviderawattar.cpp +} + greaterThan(QT_MAJOR_VERSION, 5) { message("Building using Qt6 support") CONFIG *= c++17 @@ -37,7 +51,6 @@ HEADERS += \ $$PWD/smartchargingmanager.h \ $$PWD/etm/forecast/forecaststore.h \ $$PWD/spotmarket/spotmarketdataprovider.h \ - $$PWD/spotmarket/spotmarketdataproviderawattar.h \ $$PWD/spotmarket/spotmarketmanager.h \ $$PWD/types/chargingaction.h \ $$PWD/types/charginginfo.h \ @@ -58,7 +71,6 @@ SOURCES += \ $$PWD/smartchargingmanager.cpp \ $$PWD/etm/forecast/forecaststore.cpp \ $$PWD/spotmarket/spotmarketdataprovider.cpp \ - $$PWD/spotmarket/spotmarketdataproviderawattar.cpp \ $$PWD/spotmarket/spotmarketmanager.cpp \ $$PWD/types/chargingaction.cpp \ $$PWD/types/charginginfo.cpp \ diff --git a/energyplugin/energypluginnymea.cpp b/energyplugin/energypluginnymea.cpp index 50417e6..22a97e3 100644 --- a/energyplugin/energypluginnymea.cpp +++ b/energyplugin/energypluginnymea.cpp @@ -27,6 +27,7 @@ #include "nymeaenergyjsonhandler.h" #include "energymanagerconfiguration.h" #include "spotmarket/spotmarketmanager.h" +#include "etm/spotmarketbuild.h" // [ETM] fournisseur spot selon le build // [ETM] BEGIN — EnergyArbitrator flip. Remove block to revert to upstream SmartChargingManager. #ifdef ETM_ARBITRATOR @@ -47,8 +48,9 @@ void EnergyPluginNymea::init() qCDebug(dcNymeaEnergy()) << "Initializing nymea.energy plugin"; EnergyManagerConfiguration *configuration = new EnergyManagerConfiguration(this); - QNetworkAccessManager *networkManager = new QNetworkAccessManager(this); - SpotMarketManager *spotMarketManager = new SpotMarketManager(networkManager, this); + // [ETM] Aucun QNetworkAccessManager dans le build livré : le seul flux sortant du moteur + // était le fournisseur spot aWATTar, compilé seulement avec CONFIG+=etm_spotmarket. + SpotMarketManager *spotMarketManager = SpotMarketBuild::creer(this); LoadConfigStore *loadConfigStore = nullptr; diff --git a/energyplugin/etm/etm.pri b/energyplugin/etm/etm.pri index de11f61..4565c30 100644 --- a/energyplugin/etm/etm.pri +++ b/energyplugin/etm/etm.pri @@ -18,6 +18,7 @@ HEADERS += \ $$PWD/scheduler/rulebasedscheduler.h \ $$PWD/ratios/energyratioscalculator.h \ $$PWD/energyarbitrator.h \ + $$PWD/spotmarketbuild.h \ SOURCES += \ $$PWD/types/decisionreason.cpp \ @@ -32,3 +33,4 @@ SOURCES += \ $$PWD/adapters/relayrouter.cpp \ $$PWD/scheduler/rulebasedscheduler.cpp \ $$PWD/energyarbitrator.cpp \ + $$PWD/spotmarketbuild.cpp \ diff --git a/energyplugin/etm/spotmarketbuild.cpp b/energyplugin/etm/spotmarketbuild.cpp new file mode 100644 index 0000000..20ad557 --- /dev/null +++ b/energyplugin/etm/spotmarketbuild.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync +#include "spotmarketbuild.h" + +#include "../spotmarket/spotmarketmanager.h" + +#include + +SpotMarketManager *SpotMarketBuild::creer(QObject *parent) +{ +#ifdef ETM_SPOTMARKET + QNetworkAccessManager *reseau = new QNetworkAccessManager(parent); +#else + // AUCUN objet réseau. Le pointeur nul n'est lu que par les fournisseurs aWATTar, qui ne + // sont pas compilés : un fournisseur injecté par un test apporte le sien. + QNetworkAccessManager *reseau = nullptr; +#endif + return new SpotMarketManager(reseau, parent); +} diff --git a/energyplugin/etm/spotmarketbuild.h b/energyplugin/etm/spotmarketbuild.h new file mode 100644 index 0000000..9526b2b --- /dev/null +++ b/energyplugin/etm/spotmarketbuild.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync +#pragma once + +#include + +class SpotMarketManager; + +/*! + * \file spotmarketbuild.h + * \brief Le fournisseur spot selon le BUILD — un seul point de construction. + * + * \par Pourquoi un point unique + * Le plugin (\c EnergyPluginNymea::init()) et l'expérience de simulation construisent chacun + * leur \c SpotMarketManager. Si chacun décidait de son côté d'instancier ou non un + * \c QNetworkAccessManager, le test vérifierait la copie de la simulation, jamais celle qui + * part dans le paquet. Les deux passent donc par ici. + * + * \par Ce que l'option change + * Sans \c CONFIG+=etm_spotmarket (le build livré) : aucun \c QNetworkAccessManager, aucun + * fournisseur enregistré — le gestionnaire existe, mais \c setEnabled(true) est refusé faute + * de fournisseur, donc \c planSpotMarketCharging() rend la main à sa première ligne. + * Invariant 10 : le moteur ne contacte personne. + */ +namespace SpotMarketBuild { + +//! \return Vrai si le fournisseur aWATTar est compilé dans ce build (\c ETM_SPOTMARKET). +constexpr bool inclus() +{ +#ifdef ETM_SPOTMARKET + return true; +#else + return false; +#endif +} + +/*! + * \brief Construit le gestionnaire spot de ce build. + * \param parent Parent Qt du gestionnaire — et du \c QNetworkAccessManager quand il existe. + * \return Le gestionnaire. Sans l'option, il est construit avec un gestionnaire réseau + * **nul** : aucun objet réseau n'est créé, et aucun fournisseur ne peut s'en servir. + */ +SpotMarketManager *creer(QObject *parent); + +} diff --git a/energyplugin/etm/types/decisionreason.cpp b/energyplugin/etm/types/decisionreason.cpp index b9a35b4..3dcc472 100644 --- a/energyplugin/etm/types/decisionreason.cpp +++ b/energyplugin/etm/types/decisionreason.cpp @@ -230,6 +230,11 @@ QString renderFr(const DecisionReason &reason, const QString &label) "REFUSÉE ; regarder le plugin météo") .arg(p.value(QStringLiteral("cause")).toString()); + if (c == QLatin1String(DecisionCode::SpotMarketNotInBuild)) + // LE GESTE : aucun réglage ne l'active, c'est le paquet qui ne le contient pas. + return QStringLiteral("Marché spot non disponible dans ce build — aucun fournisseur " + "compilé (invariant 10, aucun flux sortant)"); + // --- Sécurité et cycle de vie ------------------------------------------ if (c == QLatin1String(DecisionCode::DegradedL2)) return QStringLiteral("Compteur muet depuis >90 s — consigne de repli (L2 watchdog)"); diff --git a/energyplugin/etm/types/decisionreason.h b/energyplugin/etm/types/decisionreason.h index 5441420..7ce5622 100644 --- a/energyplugin/etm/types/decisionreason.h +++ b/energyplugin/etm/types/decisionreason.h @@ -308,6 +308,17 @@ constexpr const char *MissingForecastSite = "MISSING_FORECAST_SITE"; */ constexpr const char *ForecastPayloadRefused = "FORECAST_PAYLOAD_REFUSED"; +/*! + * \brief Activation du marché spot demandée, mais AUCUN fournisseur n'est compilé dans ce build. + * params : aucun. + * + * \note Le fournisseur aWATTar est hors du build livré (invariant 10, \c CONFIG+=etm_spotmarket + * pour le réintégrer). Rendu par \c SetSpotMarketConfiguration à côté de + * \c EnergyErrorInvalidParameter : l'énumération amont n'a pas de valeur pour « fonction + * absente », et le moteur n'en invente pas — le code dit lequel des refus c'est. + */ +constexpr const char *SpotMarketNotInBuild = "SPOT_MARKET_NOT_IN_BUILD"; + // --- SG-Ready (PAC) --------------------------------------------------------- //! params: \c state — le verrou minStateHold maintient l'état (court-cycling compresseur). constexpr const char *LockMinStateHold = "LOCK_MIN_STATE_HOLD"; diff --git a/energyplugin/nymeaenergyjsonhandler.cpp b/energyplugin/nymeaenergyjsonhandler.cpp index 3fc8d88..1ed305b 100644 --- a/energyplugin/nymeaenergyjsonhandler.cpp +++ b/energyplugin/nymeaenergyjsonhandler.cpp @@ -26,11 +26,12 @@ #ifdef ETM_ARBITRATOR #include "etm/energyarbitrator.h" #include "etm/forecast/forecaststore.h" -#include "etm/types/decisionreason.h" #endif +#include "etm/types/decisionreason.h" // hors ETM_ARBITRATOR aussi : SPOT_MARKET_NOT_IN_BUILD #include "types/charginginfo.h" #include "smartchargingmanager.h" #include "spotmarket/spotmarketmanager.h" +#include "etm/spotmarketbuild.h" #include "etm/types/loadconfig.h" #include "etm/config/loadconfigstore.h" @@ -134,10 +135,15 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana registerMethod("GetAvailableSpotMarketProviders", description, params, returns, Types::PermissionScopeControlThings); params.clear(); returns.clear(); - description = "Set the current spotmarket configuration. If enabled, the provideId must be valid. You can get the available providers using GetAvailableSpotMarketProviders."; + description = "Set the current spotmarket configuration. If enabled, the provideId must be valid. You can get the available providers using GetAvailableSpotMarketProviders. " + "[ETM] The shipped build compiles NO spot market provider (no outbound " + "network flow): enabling is then refused with EnergyErrorInvalidParameter and " + "'fault' SPOT_MARKET_NOT_IN_BUILD - a code, never a sentence. Disabling is " + "always accepted."; params.insert("enabled", enumValueName(Bool)); params.insert("o:providerId", enumValueName(Uuid)); returns.insert("energyError", enumRef()); + returns.insert("o:fault", enumValueName(String)); // [ETM] SPOT_MARKET_NOT_IN_BUILD registerMethod("SetSpotMarketConfiguration", description, params, returns, Types::PermissionScopeControlThings); params.clear(); returns.clear(); @@ -747,6 +753,20 @@ JsonReply *NymeaEnergyJsonHandler::SetSpotMarketConfiguration(const QVariantMap EnergyManager::EnergyError error = EnergyManager::EnergyErrorNoError; + // [ETM] BEGIN — FONCTION ABSENTE DU BUILD, et le refus le dit par un code. + // Sans fournisseur compilé, les refus ci-dessous tomberaient quand même, mais sous un + // InvalidParameter indistinct : le client accuserait son providerId. Seul le cas « rien + // n'est enregistré » est visé — un fournisseur injecté (tests) garde le chemin amont. + if (enable && !SpotMarketBuild::inclus() && m_spotMarketManager->availableProviders().isEmpty()) { + qCWarning(dcNymeaEnergy()).noquote() + << QStringLiteral("SetSpotMarketConfiguration refusé — %1 (motif %2)") + .arg(renderFr({ DecisionCode::SpotMarketNotInBuild, {} })) + .arg(QLatin1String(DecisionCode::SpotMarketNotInBuild)); + return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}, + {"fault", QLatin1String(DecisionCode::SpotMarketNotInBuild)}}); + } + // [ETM] END + if (enable && providerId.isNull()) { qCWarning(dcNymeaEnergy()) << "Could not enable spot market because ther is no valid provider id given."; error = EnergyManager::EnergyErrorInvalidParameter; diff --git a/energyplugin/spotmarket/spotmarketmanager.cpp b/energyplugin/spotmarket/spotmarketmanager.cpp index 9f59fa2..aca8526 100644 --- a/energyplugin/spotmarket/spotmarketmanager.cpp +++ b/energyplugin/spotmarket/spotmarketmanager.cpp @@ -26,15 +26,22 @@ #include "../plugininfo.h" #include "../energysettings.h" +// [ETM] Le fournisseur aWATTar n'est compilé qu'avec CONFIG+=etm_spotmarket (energyplugin.pri). +#ifdef ETM_SPOTMARKET #include "spotmarketdataproviderawattar.h" +#endif SpotMarketManager::SpotMarketManager(QNetworkAccessManager *networkManager, QObject *parent) : QObject{parent}, m_networkManager{networkManager} { // Init known spot market providers + // [ETM] BEGIN — aucun fournisseur réseau hors CONFIG+=etm_spotmarket (invariant 10). +#ifdef ETM_SPOTMARKET registerProvider(new SpotMarketDataProviderAwattar(m_networkManager, SpotMarketDataProviderAwattar::AwattarCountryAustria, this)); registerProvider(new SpotMarketDataProviderAwattar(m_networkManager, SpotMarketDataProviderAwattar::AwattarCountryGermany, this)); +#endif + // [ETM] END // Load settings EnergySettings settings; diff --git a/tests/auto/simulation/experience/energyexperienceenergymock.cpp b/tests/auto/simulation/experience/energyexperienceenergymock.cpp index d3838e8..77798ab 100644 --- a/tests/auto/simulation/experience/energyexperienceenergymock.cpp +++ b/tests/auto/simulation/experience/energyexperienceenergymock.cpp @@ -32,6 +32,7 @@ #include "../../../energyplugin/nymeaenergyjsonhandler.h" #include "../../../energyplugin/energymanagerconfiguration.h" #include "../../../energyplugin/spotmarket/spotmarketmanager.h" +#include "../../../energyplugin/etm/spotmarketbuild.h" #ifdef ETM_ARBITRATOR #include "../../../energyplugin/etm/energyarbitrator.h" #include "../../../energyplugin/etm/config/loadconfigstore.h" @@ -83,8 +84,9 @@ void ExperiencePluginEnergyMock::init() // This is the init call of our nymea.energy plugin EnergyManagerConfiguration *configuration = new EnergyManagerConfiguration(this); - QNetworkAccessManager *networkManager = new QNetworkAccessManager(this); - m_spotMarketManager = new SpotMarketManager(networkManager, this); + // Même point de construction que la production (energypluginnymea.cpp) : c'est lui que + // testSpotMarketIsOutOfTheBuild vérifie, pas une copie propre à la simulation. + m_spotMarketManager = SpotMarketBuild::creer(this); LoadConfigStore *loadConfigStore = nullptr; // [ETM] BEGIN — flip identique à energypluginnymea.cpp #ifdef ETM_ARBITRATOR diff --git a/tests/auto/simulation/simulation.cpp b/tests/auto/simulation/simulation.cpp index 4ce24a7..40ff716 100644 --- a/tests/auto/simulation/simulation.cpp +++ b/tests/auto/simulation/simulation.cpp @@ -49,6 +49,10 @@ using namespace nymeaserver; #include "../../../energyplugin/etm/scheduler/rulebasedscheduler.h" #include "../../../energyplugin/etm/types/decisionreason.h" #endif +#include "../../../energyplugin/etm/types/decisionreason.h" +#include "../../../energyplugin/etm/spotmarketbuild.h" +#include "../../../energyplugin/spotmarket/spotmarketmanager.h" +#include #include #include @@ -4137,6 +4141,11 @@ void Simulation::testDecisionReasonRendering() QStringLiteral("Charge désactivée — mise en état sûr (0 W) avant retrait (ECS-413)") }, { { DecisionCode::SafeStateSgReady }, QString(), QStringLiteral("Charge désactivée — mise en état sûr (état 2) avant retrait (ECS-413)") }, + + // --- Marché spot hors du build livré (brief 3, T0) --- + { { DecisionCode::SpotMarketNotInBuild }, QString(), + QStringLiteral("Marché spot non disponible dans ce build — aucun fournisseur compilé " + "(invariant 10, aucun flux sortant)") }, }; QSet codesCouverts; @@ -4162,7 +4171,9 @@ void Simulation::testDecisionReasonRendering() // la garantie « tout code a une ligne de rendu éprouvée » ne tenait plus. DecisionCode::BatteryReserve, DecisionCode::BelowMinPower, // 3g-2 - DecisionCode::EvGridStart, DecisionCode::PhaseLimit }; + DecisionCode::EvGridStart, DecisionCode::PhaseLimit, + // brief 3, T0 + DecisionCode::SpotMarketNotInBuild }; for (const QString &code : catalogue) QVERIFY2(codesCouverts.contains(code), qUtf8Printable(QStringLiteral("code %1 sans ligne de rendu attendue").arg(code))); @@ -7067,6 +7078,79 @@ void Simulation::testTheSnapshotServesTheChannelNeverTheTable() #endif } +void Simulation::testSpotMarketIsOutOfTheBuild() +{ + // BRIEF 3, T0 — le fournisseur spot aWATTar est HORS du build livré (invariant 10). + // + // C'était le seul flux sortant du moteur. La source reste dans l'arbre (synchronisation + // amont) mais n'est compilée qu'avec CONFIG+=etm_spotmarket. Ce test tient les trois + // promesses du build SANS l'option : aucun objet réseau, aucune activation possible, et un + // refus qui DIT pourquoi — par un code, pas par une phrase. + if (SpotMarketBuild::inclus()) + QSKIP("build CONFIG+=etm_spotmarket : le fournisseur aWATTar est compilé, rien à vérifier ici."); + + cleanupTestCase(); + m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite"; + initTestCase(); + + // 1. AUCUN QNetworkAccessManager. L'expérience de simulation construit son gestionnaire spot + // par SpotMarketBuild::creer(), le MÊME point que EnergyPluginNymea::init() : c'est donc + // le chemin de production qui est vérifié, pas une copie. + QVERIFY2(m_experiencePlugin->findChildren().isEmpty(), + "un QNetworkAccessManager a été construit alors que le build n'a aucun fournisseur"); + + SpotMarketManager *spot = m_experiencePlugin->spotMarketManager(); + QVERIFY(spot); + QVERIFY2(spot->availableProviders().isEmpty(), "aucun fournisseur ne doit être enregistré"); + + // 2. L'ACTIVATION EST REFUSÉE — et sans plantage : avant [upstream-fix], setEnabled(true) sur + // une liste vide déréférençait un fournisseur nul. C'est le chemin d'une box dont + // energy.conf porte encore SpotMarket/enabled=true d'un build antérieur. + spot->setEnabled(true); + QVERIFY2(!spot->enabled(), "le marché spot s'est activé sans fournisseur"); + QVERIFY(!spot->available()); + + // 3. planSpotMarketCharging() est INERTE : sa première ligne rend la main sur !enabled(), et + // le point 2 établit que enabled() ne peut pas devenir vrai. On l'appelle quand même, pour + // que le jour où cette garde bouge, ce test passe par le chemin. +#ifdef ETM_ARBITRATOR + EnergyArbitrator *arb = dynamic_cast(m_experiencePlugin->smartChargingManager()); + QVERIFY(arb); + arb->runSpotMarketPlanning(QDateTime::currentDateTimeUtc()); + for (auto it = arb->scheduledActions().constBegin(); it != arb->scheduledActions().constEnd(); ++it) + QVERIFY2(!it.value().contains(ChargingAction::ChargingActionIssuerSpotMarketCharging), + "une action spot a été planifiée par un build sans fournisseur"); +#endif + + // 4. LE RPC DIT POURQUOI. Un client ancien qui envoie l'identifiant aWATTar (Autriche) + // recevait InvalidParameter indistinct et accusait son providerId. Le code nomme la cause. + QVariantMap rep = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", + {{"enabled", true}, + {"providerId", QUuid("5196b3cc-b2ee-46d6-b63a-7af2cf70ba67")}}) + .toMap().value("params").toMap(); + QCOMPARE(rep.value("energyError").toString(), QStringLiteral("EnergyErrorInvalidParameter")); + QCOMPARE(rep.value("fault").toString(), QString::fromLatin1(DecisionCode::SpotMarketNotInBuild)); + // Sans providerId : même refus, même code. + rep = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", {{"enabled", true}}) + .toMap().value("params").toMap(); + QCOMPARE(rep.value("fault").toString(), QString::fromLatin1(DecisionCode::SpotMarketNotInBuild)); + QVERIFY(!spot->enabled()); + + // 5. Désactiver reste accepté, sans motif : c'est l'état du build, le confirmer n'est pas + // une erreur. + rep = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", {{"enabled", false}}) + .toMap().value("params").toMap(); + QCOMPARE(rep.value("energyError").toString(), QStringLiteral("EnergyErrorNoError")); + QVERIFY(!rep.contains("fault")); + + const QVariantMap conf = injectAndWait("NymeaEnergy.GetSpotMarketConfiguration") + .toMap().value("params").toMap(); + QCOMPARE(conf.value("enabled").toBool(), false); + QCOMPARE(conf.value("available").toBool(), false); + QVERIFY(injectAndWait("NymeaEnergy.GetAvailableSpotMarketProviders") + .toMap().value("params").toMap().value("providers").toList().isEmpty()); +} + void Simulation::testEveryGetterReturnsWhatItDeclares() { // LE TROU QUE LE 2026-09-09 A OUVERT. diff --git a/tests/auto/simulation/simulation.h b/tests/auto/simulation/simulation.h index 6d19108..62ecba2 100644 --- a/tests/auto/simulation/simulation.h +++ b/tests/auto/simulation/simulation.h @@ -209,6 +209,8 @@ private slots: void testARefusedStateIsDistinguishableFromAnApplied(); void testL4ShedsAPacAndSaysItForcedTheLock(); void testDerogationTakesTheHeadButNotTheSafety(); + //! Brief 3 T0 — aucun objet réseau, aucune activation, un refus codé. + void testSpotMarketIsOutOfTheBuild(); //! TOUTE méthode Get* du namespace rend ce que sa déclaration promet. void testEveryGetterReturnsWhatItDeclares(); //! Le snapshot sert la DÉCISION : le canal, jamais la table — et l'absence n'est pas zéro. diff --git a/tests/auto/spotmarket/testspotmarket.cpp b/tests/auto/spotmarket/testspotmarket.cpp index 068aa86..d1f2cd7 100644 --- a/tests/auto/spotmarket/testspotmarket.cpp +++ b/tests/auto/spotmarket/testspotmarket.cpp @@ -28,6 +28,15 @@ #include "../../../energyplugin/smartchargingmanager.h" #include "../../mocks/spotmarketprovider/spotmarketdataprovidermock.h" +#include "../../../energyplugin/etm/spotmarketbuild.h" // [ETM] +#include "../../../energyplugin/etm/types/decisionreason.h" // [ETM] + +// [ETM] Les tests qui lisent la liste des fournisseurs supposent aWATTar compilé. Dans le build +// livré il ne l'est pas (invariant 10) : ils sont SAUTÉS, et shippedBuildHasNoProvider() vérifie +// à leur place ce que le build livré promet. +#define ETM_REQUIERT_AWATTAR() \ + if (!SpotMarketBuild::inclus()) \ + QSKIP("fournisseur aWATTar hors du build (CONFIG+=etm_spotmarket pour le compiler)") TestSpotmarket::TestSpotmarket(QObject *parent): EnergyTestBase(parent) @@ -35,8 +44,32 @@ TestSpotmarket::TestSpotmarket(QObject *parent): qRegisterMetaType(); } +void TestSpotmarket::shippedBuildHasNoProvider() +{ + // [ETM] Brief 3 T0. Cette suite charge le plugin RÉEL (libnymea_energypluginnymea.so), pas + // l'expérience de simulation : c'est donc le binaire du paquet qui répond ici. + if (SpotMarketBuild::inclus()) + QSKIP("build CONFIG+=etm_spotmarket : le fournisseur aWATTar est compilé."); + + QVariant response = injectAndWait("NymeaEnergy.GetAvailableSpotMarketProviders"); + QVERIFY2(response.toMap().value("params").toMap().value("providers").toList().isEmpty(), + "le build livré ne doit enregistrer aucun fournisseur"); + + QVariantMap params; + params.insert("enabled", true); + params.insert("providerId", QUuid("5196b3cc-b2ee-46d6-b63a-7af2cf70ba67")); // aWATTar AT + response = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", params); + const QVariantMap rep = response.toMap().value("params").toMap(); + QCOMPARE(rep.value("energyError").toString(), QStringLiteral("EnergyErrorInvalidParameter")); + QCOMPARE(rep.value("fault").toString(), QString::fromLatin1(DecisionCode::SpotMarketNotInBuild)); + + response = injectAndWait("NymeaEnergy.GetSpotMarketConfiguration"); + QCOMPARE(response.toMap().value("params").toMap().value("enabled").toBool(), false); +} + void TestSpotmarket::getAvailableProviders() { + ETM_REQUIERT_AWATTAR(); QVariant response = injectAndWait("NymeaEnergy.GetAvailableSpotMarketProviders"); QVariantList providerList = response.toMap().value("params").toMap().value("providers").toList(); QVERIFY2(providerList.count() > 0, "Available providers list is empty."); @@ -51,6 +84,7 @@ void TestSpotmarket::getAvailableProviders() void TestSpotmarket::getSpotmarketEntriesAndChargingSchedules() { + ETM_REQUIERT_AWATTAR(); QVariant response, notification; QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);; @@ -214,6 +248,7 @@ void TestSpotmarket::addCharingInfo_data() void TestSpotmarket::addCharingInfo() { + ETM_REQUIERT_AWATTAR(); QFETCH(bool, addingCharger); QFETCH(bool, addingCar); QFETCH(bool, carValid); @@ -334,6 +369,7 @@ void TestSpotmarket::setConfiguration_data() void TestSpotmarket::setConfiguration() { + ETM_REQUIERT_AWATTAR(); QFETCH(bool, enabled); QFETCH(bool, useValidProvider); QFETCH(bool, useNullProvider); diff --git a/tests/auto/spotmarket/testspotmarket.h b/tests/auto/spotmarket/testspotmarket.h index 9d08fea..d678679 100644 --- a/tests/auto/spotmarket/testspotmarket.h +++ b/tests/auto/spotmarket/testspotmarket.h @@ -34,6 +34,10 @@ public: explicit TestSpotmarket(QObject *parent = nullptr); private slots: + // [ETM] Brief 3 T0 — le plugin RÉEL (.so) sans CONFIG+=etm_spotmarket : aucun fournisseur, + // activation refusée avec SPOT_MARKET_NOT_IN_BUILD. + void shippedBuildHasNoProvider(); + void getAvailableProviders(); void getSpotmarketEntriesAndChargingSchedules();