From dde967da414f1bc549a62647d5292dbb69bf31c6 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Tue, 9 Jun 2026 21:52:14 +0200 Subject: [PATCH] =?UTF-8?q?[3c-7b]=20testEcsSurplusPV=20:=20waterfall=20EC?= =?UTF-8?q?S=20+=20protection=20compresseur=20(seam=20de=20temps=20prouv?= =?UTF-8?q?=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) ; importminOn → 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) --- tests/auto/simulation/simulation.cpp | 95 +++++++++++++++++++ tests/auto/simulation/simulation.h | 4 + .../integrationpluginenergymocks.cpp | 4 +- .../integrationpluginenergymocks.json | 2 +- 4 files changed, 102 insertions(+), 3 deletions(-) diff --git a/tests/auto/simulation/simulation.cpp b/tests/auto/simulation/simulation.cpp index a6b9c48..7b0c1e0 100644 --- a/tests/auto/simulation/simulation.cpp +++ b/tests/auto/simulation/simulation.cpp @@ -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 #include @@ -41,11 +45,102 @@ using namespace nymeaserver; #include #include #include +#include #include #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(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({0, 1200, 2400}), + QList>({ {}, {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 diff --git a/tests/auto/simulation/simulation.h b/tests/auto/simulation/simulation.h index 64cf46f..ace9e86 100644 --- a/tests/auto/simulation/simulation.h +++ b/tests/auto/simulation/simulation.h @@ -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); diff --git a/tests/mocks/plugins/energymocks/integrationpluginenergymocks.cpp b/tests/mocks/plugins/energymocks/integrationpluginenergymocks.cpp index 8d50880..3467395 100644 --- a/tests/mocks/plugins/energymocks/integrationpluginenergymocks.cpp +++ b/tests/mocks/plugins/energymocks/integrationpluginenergymocks.cpp @@ -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(); diff --git a/tests/mocks/plugins/energymocks/integrationpluginenergymocks.json b/tests/mocks/plugins/energymocks/integrationpluginenergymocks.json index 97d8b1e..65d3eb7 100644 --- a/tests/mocks/plugins/energymocks/integrationpluginenergymocks.json +++ b/tests/mocks/plugins/energymocks/integrationpluginenergymocks.json @@ -818,7 +818,7 @@ ] }, { - "name": "mockPowerSwitch", + "name": "powerSwitch", "displayName": "Mocked Power Switch (relais ECS)", "id": "841f8905-d1d7-4053-909f-01123b497747", "createMethods": ["user"],