feat(spot): le fournisseur aWATTar sort du build livré (brief 3, T0)
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 <noreply@anthropic.com>
This commit is contained in:
parent
9574d4b266
commit
a23161b3b6
@ -7,6 +7,20 @@ INCLUDEPATH += $$PWD
|
|||||||
# Uncommenter pour activer. Commité DÉSACTIVÉ jusqu'à preuve iso (3b-iv).
|
# Uncommenter pour activer. Commité DÉSACTIVÉ jusqu'à preuve iso (3b-iv).
|
||||||
DEFINES += ETM_ARBITRATOR
|
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) {
|
greaterThan(QT_MAJOR_VERSION, 5) {
|
||||||
message("Building using Qt6 support")
|
message("Building using Qt6 support")
|
||||||
CONFIG *= c++17
|
CONFIG *= c++17
|
||||||
@ -37,7 +51,6 @@ HEADERS += \
|
|||||||
$$PWD/smartchargingmanager.h \
|
$$PWD/smartchargingmanager.h \
|
||||||
$$PWD/etm/forecast/forecaststore.h \
|
$$PWD/etm/forecast/forecaststore.h \
|
||||||
$$PWD/spotmarket/spotmarketdataprovider.h \
|
$$PWD/spotmarket/spotmarketdataprovider.h \
|
||||||
$$PWD/spotmarket/spotmarketdataproviderawattar.h \
|
|
||||||
$$PWD/spotmarket/spotmarketmanager.h \
|
$$PWD/spotmarket/spotmarketmanager.h \
|
||||||
$$PWD/types/chargingaction.h \
|
$$PWD/types/chargingaction.h \
|
||||||
$$PWD/types/charginginfo.h \
|
$$PWD/types/charginginfo.h \
|
||||||
@ -58,7 +71,6 @@ SOURCES += \
|
|||||||
$$PWD/smartchargingmanager.cpp \
|
$$PWD/smartchargingmanager.cpp \
|
||||||
$$PWD/etm/forecast/forecaststore.cpp \
|
$$PWD/etm/forecast/forecaststore.cpp \
|
||||||
$$PWD/spotmarket/spotmarketdataprovider.cpp \
|
$$PWD/spotmarket/spotmarketdataprovider.cpp \
|
||||||
$$PWD/spotmarket/spotmarketdataproviderawattar.cpp \
|
|
||||||
$$PWD/spotmarket/spotmarketmanager.cpp \
|
$$PWD/spotmarket/spotmarketmanager.cpp \
|
||||||
$$PWD/types/chargingaction.cpp \
|
$$PWD/types/chargingaction.cpp \
|
||||||
$$PWD/types/charginginfo.cpp \
|
$$PWD/types/charginginfo.cpp \
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
#include "nymeaenergyjsonhandler.h"
|
#include "nymeaenergyjsonhandler.h"
|
||||||
#include "energymanagerconfiguration.h"
|
#include "energymanagerconfiguration.h"
|
||||||
#include "spotmarket/spotmarketmanager.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.
|
// [ETM] BEGIN — EnergyArbitrator flip. Remove block to revert to upstream SmartChargingManager.
|
||||||
#ifdef ETM_ARBITRATOR
|
#ifdef ETM_ARBITRATOR
|
||||||
@ -47,8 +48,9 @@ void EnergyPluginNymea::init()
|
|||||||
qCDebug(dcNymeaEnergy()) << "Initializing nymea.energy plugin";
|
qCDebug(dcNymeaEnergy()) << "Initializing nymea.energy plugin";
|
||||||
|
|
||||||
EnergyManagerConfiguration *configuration = new EnergyManagerConfiguration(this);
|
EnergyManagerConfiguration *configuration = new EnergyManagerConfiguration(this);
|
||||||
QNetworkAccessManager *networkManager = new QNetworkAccessManager(this);
|
// [ETM] Aucun QNetworkAccessManager dans le build livré : le seul flux sortant du moteur
|
||||||
SpotMarketManager *spotMarketManager = new SpotMarketManager(networkManager, this);
|
// était le fournisseur spot aWATTar, compilé seulement avec CONFIG+=etm_spotmarket.
|
||||||
|
SpotMarketManager *spotMarketManager = SpotMarketBuild::creer(this);
|
||||||
|
|
||||||
LoadConfigStore *loadConfigStore = nullptr;
|
LoadConfigStore *loadConfigStore = nullptr;
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ HEADERS += \
|
|||||||
$$PWD/scheduler/rulebasedscheduler.h \
|
$$PWD/scheduler/rulebasedscheduler.h \
|
||||||
$$PWD/ratios/energyratioscalculator.h \
|
$$PWD/ratios/energyratioscalculator.h \
|
||||||
$$PWD/energyarbitrator.h \
|
$$PWD/energyarbitrator.h \
|
||||||
|
$$PWD/spotmarketbuild.h \
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
$$PWD/types/decisionreason.cpp \
|
$$PWD/types/decisionreason.cpp \
|
||||||
@ -32,3 +33,4 @@ SOURCES += \
|
|||||||
$$PWD/adapters/relayrouter.cpp \
|
$$PWD/adapters/relayrouter.cpp \
|
||||||
$$PWD/scheduler/rulebasedscheduler.cpp \
|
$$PWD/scheduler/rulebasedscheduler.cpp \
|
||||||
$$PWD/energyarbitrator.cpp \
|
$$PWD/energyarbitrator.cpp \
|
||||||
|
$$PWD/spotmarketbuild.cpp \
|
||||||
|
|||||||
19
energyplugin/etm/spotmarketbuild.cpp
Normal file
19
energyplugin/etm/spotmarketbuild.cpp
Normal file
@ -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 <QNetworkAccessManager>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
45
energyplugin/etm/spotmarketbuild.h
Normal file
45
energyplugin/etm/spotmarketbuild.h
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
@ -230,6 +230,11 @@ QString renderFr(const DecisionReason &reason, const QString &label)
|
|||||||
"REFUSÉE ; regarder le plugin météo")
|
"REFUSÉE ; regarder le plugin météo")
|
||||||
.arg(p.value(QStringLiteral("cause")).toString());
|
.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 ------------------------------------------
|
// --- Sécurité et cycle de vie ------------------------------------------
|
||||||
if (c == QLatin1String(DecisionCode::DegradedL2))
|
if (c == QLatin1String(DecisionCode::DegradedL2))
|
||||||
return QStringLiteral("Compteur muet depuis >90 s — consigne de repli (L2 watchdog)");
|
return QStringLiteral("Compteur muet depuis >90 s — consigne de repli (L2 watchdog)");
|
||||||
|
|||||||
@ -308,6 +308,17 @@ constexpr const char *MissingForecastSite = "MISSING_FORECAST_SITE";
|
|||||||
*/
|
*/
|
||||||
constexpr const char *ForecastPayloadRefused = "FORECAST_PAYLOAD_REFUSED";
|
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) ---------------------------------------------------------
|
// --- SG-Ready (PAC) ---------------------------------------------------------
|
||||||
//! params: \c state — le verrou minStateHold maintient l'état (court-cycling compresseur).
|
//! params: \c state — le verrou minStateHold maintient l'état (court-cycling compresseur).
|
||||||
constexpr const char *LockMinStateHold = "LOCK_MIN_STATE_HOLD";
|
constexpr const char *LockMinStateHold = "LOCK_MIN_STATE_HOLD";
|
||||||
|
|||||||
@ -26,11 +26,12 @@
|
|||||||
#ifdef ETM_ARBITRATOR
|
#ifdef ETM_ARBITRATOR
|
||||||
#include "etm/energyarbitrator.h"
|
#include "etm/energyarbitrator.h"
|
||||||
#include "etm/forecast/forecaststore.h"
|
#include "etm/forecast/forecaststore.h"
|
||||||
#include "etm/types/decisionreason.h"
|
|
||||||
#endif
|
#endif
|
||||||
|
#include "etm/types/decisionreason.h" // hors ETM_ARBITRATOR aussi : SPOT_MARKET_NOT_IN_BUILD
|
||||||
#include "types/charginginfo.h"
|
#include "types/charginginfo.h"
|
||||||
#include "smartchargingmanager.h"
|
#include "smartchargingmanager.h"
|
||||||
#include "spotmarket/spotmarketmanager.h"
|
#include "spotmarket/spotmarketmanager.h"
|
||||||
|
#include "etm/spotmarketbuild.h"
|
||||||
#include "etm/types/loadconfig.h"
|
#include "etm/types/loadconfig.h"
|
||||||
#include "etm/config/loadconfigstore.h"
|
#include "etm/config/loadconfigstore.h"
|
||||||
|
|
||||||
@ -134,10 +135,15 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
|||||||
registerMethod("GetAvailableSpotMarketProviders", description, params, returns, Types::PermissionScopeControlThings);
|
registerMethod("GetAvailableSpotMarketProviders", description, params, returns, Types::PermissionScopeControlThings);
|
||||||
|
|
||||||
params.clear(); returns.clear();
|
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("enabled", enumValueName(Bool));
|
||||||
params.insert("o:providerId", enumValueName(Uuid));
|
params.insert("o:providerId", enumValueName(Uuid));
|
||||||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||||||
|
returns.insert("o:fault", enumValueName(String)); // [ETM] SPOT_MARKET_NOT_IN_BUILD
|
||||||
registerMethod("SetSpotMarketConfiguration", description, params, returns, Types::PermissionScopeControlThings);
|
registerMethod("SetSpotMarketConfiguration", description, params, returns, Types::PermissionScopeControlThings);
|
||||||
|
|
||||||
params.clear(); returns.clear();
|
params.clear(); returns.clear();
|
||||||
@ -747,6 +753,20 @@ JsonReply *NymeaEnergyJsonHandler::SetSpotMarketConfiguration(const QVariantMap
|
|||||||
|
|
||||||
EnergyManager::EnergyError error = EnergyManager::EnergyErrorNoError;
|
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()) {
|
if (enable && providerId.isNull()) {
|
||||||
qCWarning(dcNymeaEnergy()) << "Could not enable spot market because ther is no valid provider id given.";
|
qCWarning(dcNymeaEnergy()) << "Could not enable spot market because ther is no valid provider id given.";
|
||||||
error = EnergyManager::EnergyErrorInvalidParameter;
|
error = EnergyManager::EnergyErrorInvalidParameter;
|
||||||
|
|||||||
@ -26,15 +26,22 @@
|
|||||||
#include "../plugininfo.h"
|
#include "../plugininfo.h"
|
||||||
#include "../energysettings.h"
|
#include "../energysettings.h"
|
||||||
|
|
||||||
|
// [ETM] Le fournisseur aWATTar n'est compilé qu'avec CONFIG+=etm_spotmarket (energyplugin.pri).
|
||||||
|
#ifdef ETM_SPOTMARKET
|
||||||
#include "spotmarketdataproviderawattar.h"
|
#include "spotmarketdataproviderawattar.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
SpotMarketManager::SpotMarketManager(QNetworkAccessManager *networkManager, QObject *parent) :
|
SpotMarketManager::SpotMarketManager(QNetworkAccessManager *networkManager, QObject *parent) :
|
||||||
QObject{parent},
|
QObject{parent},
|
||||||
m_networkManager{networkManager}
|
m_networkManager{networkManager}
|
||||||
{
|
{
|
||||||
// Init known spot market providers
|
// 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::AwattarCountryAustria, this));
|
||||||
registerProvider(new SpotMarketDataProviderAwattar(m_networkManager, SpotMarketDataProviderAwattar::AwattarCountryGermany, this));
|
registerProvider(new SpotMarketDataProviderAwattar(m_networkManager, SpotMarketDataProviderAwattar::AwattarCountryGermany, this));
|
||||||
|
#endif
|
||||||
|
// [ETM] END
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
EnergySettings settings;
|
EnergySettings settings;
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
#include "../../../energyplugin/nymeaenergyjsonhandler.h"
|
#include "../../../energyplugin/nymeaenergyjsonhandler.h"
|
||||||
#include "../../../energyplugin/energymanagerconfiguration.h"
|
#include "../../../energyplugin/energymanagerconfiguration.h"
|
||||||
#include "../../../energyplugin/spotmarket/spotmarketmanager.h"
|
#include "../../../energyplugin/spotmarket/spotmarketmanager.h"
|
||||||
|
#include "../../../energyplugin/etm/spotmarketbuild.h"
|
||||||
#ifdef ETM_ARBITRATOR
|
#ifdef ETM_ARBITRATOR
|
||||||
#include "../../../energyplugin/etm/energyarbitrator.h"
|
#include "../../../energyplugin/etm/energyarbitrator.h"
|
||||||
#include "../../../energyplugin/etm/config/loadconfigstore.h"
|
#include "../../../energyplugin/etm/config/loadconfigstore.h"
|
||||||
@ -83,8 +84,9 @@ void ExperiencePluginEnergyMock::init()
|
|||||||
|
|
||||||
// This is the init call of our nymea.energy plugin
|
// This is the init call of our nymea.energy plugin
|
||||||
EnergyManagerConfiguration *configuration = new EnergyManagerConfiguration(this);
|
EnergyManagerConfiguration *configuration = new EnergyManagerConfiguration(this);
|
||||||
QNetworkAccessManager *networkManager = new QNetworkAccessManager(this);
|
// Même point de construction que la production (energypluginnymea.cpp) : c'est lui que
|
||||||
m_spotMarketManager = new SpotMarketManager(networkManager, this);
|
// testSpotMarketIsOutOfTheBuild vérifie, pas une copie propre à la simulation.
|
||||||
|
m_spotMarketManager = SpotMarketBuild::creer(this);
|
||||||
LoadConfigStore *loadConfigStore = nullptr;
|
LoadConfigStore *loadConfigStore = nullptr;
|
||||||
// [ETM] BEGIN — flip identique à energypluginnymea.cpp
|
// [ETM] BEGIN — flip identique à energypluginnymea.cpp
|
||||||
#ifdef ETM_ARBITRATOR
|
#ifdef ETM_ARBITRATOR
|
||||||
|
|||||||
@ -49,6 +49,10 @@ using namespace nymeaserver;
|
|||||||
#include "../../../energyplugin/etm/scheduler/rulebasedscheduler.h"
|
#include "../../../energyplugin/etm/scheduler/rulebasedscheduler.h"
|
||||||
#include "../../../energyplugin/etm/types/decisionreason.h"
|
#include "../../../energyplugin/etm/types/decisionreason.h"
|
||||||
#endif
|
#endif
|
||||||
|
#include "../../../energyplugin/etm/types/decisionreason.h"
|
||||||
|
#include "../../../energyplugin/etm/spotmarketbuild.h"
|
||||||
|
#include "../../../energyplugin/spotmarket/spotmarketmanager.h"
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
@ -4137,6 +4141,11 @@ void Simulation::testDecisionReasonRendering()
|
|||||||
QStringLiteral("Charge désactivée — mise en état sûr (0 W) avant retrait (ECS-413)") },
|
QStringLiteral("Charge désactivée — mise en état sûr (0 W) avant retrait (ECS-413)") },
|
||||||
{ { DecisionCode::SafeStateSgReady }, QString(),
|
{ { DecisionCode::SafeStateSgReady }, QString(),
|
||||||
QStringLiteral("Charge désactivée — mise en état sûr (état 2) avant retrait (ECS-413)") },
|
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<QString> codesCouverts;
|
QSet<QString> codesCouverts;
|
||||||
@ -4162,7 +4171,9 @@ void Simulation::testDecisionReasonRendering()
|
|||||||
// la garantie « tout code a une ligne de rendu éprouvée » ne tenait plus.
|
// la garantie « tout code a une ligne de rendu éprouvée » ne tenait plus.
|
||||||
DecisionCode::BatteryReserve, DecisionCode::BelowMinPower,
|
DecisionCode::BatteryReserve, DecisionCode::BelowMinPower,
|
||||||
// 3g-2
|
// 3g-2
|
||||||
DecisionCode::EvGridStart, DecisionCode::PhaseLimit };
|
DecisionCode::EvGridStart, DecisionCode::PhaseLimit,
|
||||||
|
// brief 3, T0
|
||||||
|
DecisionCode::SpotMarketNotInBuild };
|
||||||
for (const QString &code : catalogue)
|
for (const QString &code : catalogue)
|
||||||
QVERIFY2(codesCouverts.contains(code),
|
QVERIFY2(codesCouverts.contains(code),
|
||||||
qUtf8Printable(QStringLiteral("code %1 sans ligne de rendu attendue").arg(code)));
|
qUtf8Printable(QStringLiteral("code %1 sans ligne de rendu attendue").arg(code)));
|
||||||
@ -7067,6 +7078,79 @@ void Simulation::testTheSnapshotServesTheChannelNeverTheTable()
|
|||||||
#endif
|
#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<QNetworkAccessManager *>().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<EnergyArbitrator *>(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()
|
void Simulation::testEveryGetterReturnsWhatItDeclares()
|
||||||
{
|
{
|
||||||
// LE TROU QUE LE 2026-09-09 A OUVERT.
|
// LE TROU QUE LE 2026-09-09 A OUVERT.
|
||||||
|
|||||||
@ -209,6 +209,8 @@ private slots:
|
|||||||
void testARefusedStateIsDistinguishableFromAnApplied();
|
void testARefusedStateIsDistinguishableFromAnApplied();
|
||||||
void testL4ShedsAPacAndSaysItForcedTheLock();
|
void testL4ShedsAPacAndSaysItForcedTheLock();
|
||||||
void testDerogationTakesTheHeadButNotTheSafety();
|
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.
|
//! TOUTE méthode Get* du namespace rend ce que sa déclaration promet.
|
||||||
void testEveryGetterReturnsWhatItDeclares();
|
void testEveryGetterReturnsWhatItDeclares();
|
||||||
//! Le snapshot sert la DÉCISION : le canal, jamais la table — et l'absence n'est pas zéro.
|
//! Le snapshot sert la DÉCISION : le canal, jamais la table — et l'absence n'est pas zéro.
|
||||||
|
|||||||
@ -28,6 +28,15 @@
|
|||||||
|
|
||||||
#include "../../../energyplugin/smartchargingmanager.h"
|
#include "../../../energyplugin/smartchargingmanager.h"
|
||||||
#include "../../mocks/spotmarketprovider/spotmarketdataprovidermock.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):
|
TestSpotmarket::TestSpotmarket(QObject *parent):
|
||||||
EnergyTestBase(parent)
|
EnergyTestBase(parent)
|
||||||
@ -35,8 +44,32 @@ TestSpotmarket::TestSpotmarket(QObject *parent):
|
|||||||
qRegisterMetaType<TimeFrames>();
|
qRegisterMetaType<TimeFrames>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
void TestSpotmarket::getAvailableProviders()
|
||||||
{
|
{
|
||||||
|
ETM_REQUIERT_AWATTAR();
|
||||||
QVariant response = injectAndWait("NymeaEnergy.GetAvailableSpotMarketProviders");
|
QVariant response = injectAndWait("NymeaEnergy.GetAvailableSpotMarketProviders");
|
||||||
QVariantList providerList = response.toMap().value("params").toMap().value("providers").toList();
|
QVariantList providerList = response.toMap().value("params").toMap().value("providers").toList();
|
||||||
QVERIFY2(providerList.count() > 0, "Available providers list is empty.");
|
QVERIFY2(providerList.count() > 0, "Available providers list is empty.");
|
||||||
@ -51,6 +84,7 @@ void TestSpotmarket::getAvailableProviders()
|
|||||||
|
|
||||||
void TestSpotmarket::getSpotmarketEntriesAndChargingSchedules()
|
void TestSpotmarket::getSpotmarketEntriesAndChargingSchedules()
|
||||||
{
|
{
|
||||||
|
ETM_REQUIERT_AWATTAR();
|
||||||
QVariant response, notification;
|
QVariant response, notification;
|
||||||
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
||||||
|
|
||||||
@ -214,6 +248,7 @@ void TestSpotmarket::addCharingInfo_data()
|
|||||||
|
|
||||||
void TestSpotmarket::addCharingInfo()
|
void TestSpotmarket::addCharingInfo()
|
||||||
{
|
{
|
||||||
|
ETM_REQUIERT_AWATTAR();
|
||||||
QFETCH(bool, addingCharger);
|
QFETCH(bool, addingCharger);
|
||||||
QFETCH(bool, addingCar);
|
QFETCH(bool, addingCar);
|
||||||
QFETCH(bool, carValid);
|
QFETCH(bool, carValid);
|
||||||
@ -334,6 +369,7 @@ void TestSpotmarket::setConfiguration_data()
|
|||||||
|
|
||||||
void TestSpotmarket::setConfiguration()
|
void TestSpotmarket::setConfiguration()
|
||||||
{
|
{
|
||||||
|
ETM_REQUIERT_AWATTAR();
|
||||||
QFETCH(bool, enabled);
|
QFETCH(bool, enabled);
|
||||||
QFETCH(bool, useValidProvider);
|
QFETCH(bool, useValidProvider);
|
||||||
QFETCH(bool, useNullProvider);
|
QFETCH(bool, useNullProvider);
|
||||||
|
|||||||
@ -34,6 +34,10 @@ public:
|
|||||||
explicit TestSpotmarket(QObject *parent = nullptr);
|
explicit TestSpotmarket(QObject *parent = nullptr);
|
||||||
|
|
||||||
private slots:
|
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 getAvailableProviders();
|
||||||
|
|
||||||
void getSpotmarketEntriesAndChargingSchedules();
|
void getSpotmarketEntriesAndChargingSchedules();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user