[3c-7b] testEcsSurplusPV : waterfall ECS + protection compresseur (seam de temps prouvé)
Test simulation autonome (arbitre frais via initTestCase) : 2 relais powerSwitch + EcsRelayAdapter minOn=300. 4 régimes pilotés par le temps simulé : cascade export 0→1→2 ; anti-clignotement (recrédit, hors verrou) ; import<minOn → RESTE (protection compresseur) ; import>minOn → déleste. Seul le temps simulé change entre les 2 derniers → prouve le seam de temps unifié ET la protection. Renommage ThingClass mockPowerSwitch→powerSwitch (collision symbole plugininfo vs energytestbase dans le binaire simulation). Suite simulation : 17 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3a8eb5da86
commit
dde967da41
@ -33,6 +33,10 @@ using namespace nymeaserver;
|
||||
|
||||
#include "../../../energyplugin/smartchargingmanager.h"
|
||||
#include "../../mocks/spotmarketprovider/spotmarketdataprovidermock.h"
|
||||
#ifdef ETM_ARBITRATOR
|
||||
#include "../../../energyplugin/etm/energyarbitrator.h"
|
||||
#include "../../../energyplugin/etm/adapters/ecsrelayadapter.h"
|
||||
#endif
|
||||
|
||||
#include <QHash>
|
||||
#include <QtMath>
|
||||
@ -41,11 +45,102 @@ using namespace nymeaserver;
|
||||
#include <QDateTime>
|
||||
#include <QSignalSpy>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include <nymeacore.h>
|
||||
|
||||
#include "simulationtestpoint.h"
|
||||
|
||||
void Simulation::testEcsSurplusPV()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("testEcsSurplusPV nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
// Préambule harnais (comme run()) : DB + (ré)initialisation → expérience chargée et
|
||||
// arbitre FRAIS (pas d'adaptateur ECS résiduel d'un test précédent).
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
|
||||
EnergyArbitrator *arbitrator = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||
QVERIFY2(arbitrator, "smartChargingManager n'est pas un EnergyArbitrator (ETM_ARBITRATOR requis)");
|
||||
|
||||
ThingManager *thingManager = NymeaCore::instance()->thingManager();
|
||||
|
||||
// --- Root meter ---
|
||||
QUuid meterThingId = addMeter();
|
||||
QVERIFY2(!meterThingId.isNull(), "meter thingId invalide");
|
||||
m_experiencePlugin->energyManager()->setRootMeter(meterThingId);
|
||||
Thing *meterThing = thingManager->findConfiguredThing(meterThingId);
|
||||
QVERIFY(meterThing);
|
||||
meterThing->setStateValue("connected", true);
|
||||
|
||||
// --- Deux relais ECS : palier 1 = relayA (1200 W), palier 2 = A+B (2400 W) ---
|
||||
QUuid relayAId = addPowerSwitch(1200, 26661);
|
||||
QUuid relayBId = addPowerSwitch(1200, 26662);
|
||||
QVERIFY(!relayAId.isNull() && !relayBId.isNull());
|
||||
Thing *relayA = thingManager->findConfiguredThing(relayAId);
|
||||
Thing *relayB = thingManager->findConfiguredThing(relayBId);
|
||||
QVERIFY(relayA && relayB);
|
||||
|
||||
// minOn = 300 s (protection compresseur) ; minOff = 0 (pas de délai de redémarrage ici).
|
||||
const int minOnS = 300;
|
||||
EcsRelayAdapter *ecs = new EcsRelayAdapter(
|
||||
thingManager, "ecs-surplus-test", "Chauffe-eau (test surplus)",
|
||||
QList<int>({0, 1200, 2400}),
|
||||
QList<QList<QString>>({ {}, {relayAId.toString()}, {relayAId.toString(), relayBId.toString()} }),
|
||||
minOnS, 0, 1, arbitrator);
|
||||
arbitrator->registerEcsAdapter(ecs);
|
||||
|
||||
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||
// currentPower compteur : < 0 = export (surplus PV), > 0 = import (réseau).
|
||||
auto setMeterW = [&](double signedW){ meterThing->setStateValue("currentPower", signedW); };
|
||||
auto cycle = [&](const QDateTime &now){ arbitrator->simulationCallUpdate(now); QCoreApplication::processEvents(); };
|
||||
|
||||
// ============ Régime 1 — cascade montante sur surplus (export) ============
|
||||
setMeterW(-1000); cycle(t0); // 1000 W < palier 1 → éteint
|
||||
QCOMPARE(ecs->currentStage(), 0);
|
||||
QCOMPARE(relayA->stateValue("power").toBool(), false);
|
||||
|
||||
setMeterW(-1500); cycle(t0); // 1500 W → palier 1 (relayA)
|
||||
QCOMPARE(ecs->currentStage(), 1);
|
||||
QCOMPARE(relayA->stateValue("power").toBool(), true);
|
||||
QCOMPARE(relayB->stateValue("power").toBool(), false);
|
||||
|
||||
setMeterW(-2500); cycle(t0.addSecs(1)); // 2500 W → palier 2 (montée autorisée même sous minOn)
|
||||
QCOMPARE(ecs->currentStage(), 2);
|
||||
QCOMPARE(relayA->stateValue("power").toBool(), true);
|
||||
QCOMPARE(relayB->stateValue("power").toBool(), true);
|
||||
|
||||
// ============ Régime 2 — anti-clignotement (recrédit, hors verrou) ============
|
||||
// T0+400 : minOn écoulé → plancher de verrou = 0. PV 2500, ECS tire 2400 → export net 100.
|
||||
// budget = 100 + 2400 (recrédit conso) = 2500 → RESTE palier 2. Sans recrédit : 100 → éteint.
|
||||
// Le plancher étant 0, c'est bien le RECRÉDIT qui maintient le palier, pas le verrou.
|
||||
setMeterW(-100); cycle(t0.addSecs(400));
|
||||
QCOMPARE(ecs->currentStage(), 2);
|
||||
|
||||
// Redescente propre au palier 1 (import partiel, minOn écoulé) pour préparer le régime 3.
|
||||
setMeterW(600); cycle(t0.addSecs(401)); // import 600 → budget = -600 + 2400 = 1800 → palier 1
|
||||
QCOMPARE(ecs->currentStage(), 1);
|
||||
QCOMPARE(relayB->stateValue("power").toBool(), false);
|
||||
|
||||
// ============ Régime 3 — PROTECTION COMPRESSEUR : import < minOn → RESTE ============
|
||||
// lastSwitch = T0+401. T0+501 (elapsed 100 < minOn 300) : PV 0, ECS tire 1200 → import 1200.
|
||||
// budget = -1200 + 1200 = 0 → le scheduler voudrait palier 0, MAIS plancher de verrou = 1.
|
||||
setMeterW(1200); cycle(t0.addSecs(501));
|
||||
QCOMPARE(ecs->currentStage(), 1); // RESTE allumé — protection compresseur
|
||||
QCOMPARE(relayA->stateValue("power").toBool(), true);
|
||||
|
||||
// ============ Régime 4 — minOn écoulé → DÉLESTE ============
|
||||
// T0+801 (elapsed depuis T0+401 = 400 > minOn 300) : MÊME import 1200. Plancher = 0 → palier 0.
|
||||
// Seul le TEMPS SIMULÉ a changé entre régime 3 et 4 : si le seam de temps était faux
|
||||
// (horloge murale), la décision ne basculerait pas. Ce test prouve le seam ET la protection.
|
||||
setMeterW(1200); cycle(t0.addSecs(801));
|
||||
QCOMPARE(ecs->currentStage(), 0); // DÉLESTE
|
||||
QCOMPARE(relayA->stateValue("power").toBool(), false);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::run_data()
|
||||
{
|
||||
// Simulation infos
|
||||
|
||||
@ -56,6 +56,10 @@ private slots:
|
||||
void run_data();
|
||||
void run();
|
||||
|
||||
// Waterfall ECS (EcsRelayAdapter) : cascade surplus, anti-clignotement (recrédit),
|
||||
// et protection compresseur (import < minOn → RESTE ; import > minOn → déleste).
|
||||
void testEcsSurplusPV();
|
||||
|
||||
void printStates(Thing *thing);
|
||||
void updateChargerMeter(Thing *thing);
|
||||
|
||||
|
||||
@ -359,7 +359,7 @@ void IntegrationPluginEnergyMocks::setupThing(ThingSetupInfo *info)
|
||||
|
||||
return;
|
||||
|
||||
} else if (thing->thingClassId() == mockPowerSwitchThingClassId) {
|
||||
} else if (thing->thingClassId() == powerSwitchThingClassId) {
|
||||
EnergyMockController *controller = new EnergyMockController(thing, this);
|
||||
ParamType paramType = thing->thingClass().paramTypes().findByName("port");
|
||||
quint16 port = thing->paramValue(paramType.id()).toUInt();
|
||||
@ -499,7 +499,7 @@ void IntegrationPluginEnergyMocks::executeAction(ThingActionInfo *info)
|
||||
}
|
||||
}
|
||||
|
||||
if (thing->thingClassId() == mockPowerSwitchThingClassId) {
|
||||
if (thing->thingClassId() == powerSwitchThingClassId) {
|
||||
if (actionType.name() == "power") {
|
||||
bool power = action.paramValue(actionType.paramTypes().findByName("power").id()).toBool();
|
||||
double nominal = thing->paramValue(thing->thingClass().paramTypes().findByName("nominalPower").id()).toDouble();
|
||||
|
||||
@ -818,7 +818,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mockPowerSwitch",
|
||||
"name": "powerSwitch",
|
||||
"displayName": "Mocked Power Switch (relais ECS)",
|
||||
"id": "841f8905-d1d7-4053-909f-01123b497747",
|
||||
"createMethods": ["user"],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user