[3e-5] testSgReadySurplus : montée + hystérésis + court-cycling + budget partagé ECS↔PAC
Test simulation autonome (mock powerSwitch 2 relais, encodage 2 bits). 4 volets : (1) montée 2→3→4 ; (2) hystérésis 3↔4 (zone morte P4×1,0–1,2, budget oscillant → reste 4) ; (3) court-cycling (gelé sous minStateHold, bascule au-delà via temps simulé) ; (4) budget PARTAGÉ ECS↔PAC : ordre priorité → service inverse (preuve waterfall unifié 3e). Suite simulation 19/19, 0 régression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b06ac15714
commit
d8079e84e0
@ -36,6 +36,7 @@ using namespace nymeaserver;
|
||||
#ifdef ETM_ARBITRATOR
|
||||
#include "../../../energyplugin/etm/energyarbitrator.h"
|
||||
#include "../../../energyplugin/etm/adapters/ecsrelayadapter.h"
|
||||
#include "../../../energyplugin/etm/adapters/sgreadyadapter.h"
|
||||
#endif
|
||||
|
||||
#include <QHash>
|
||||
@ -222,6 +223,138 @@ void Simulation::testMeterSilentFallback()
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testSgReadySurplus()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("testSgReadySurplus nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
// Encodage SG-Ready 2 bits (K1,K2) : 1=[K1] blocage · 2=[] normal · 3=[K2] reco · 4=[K1,K2] forcé.
|
||||
// estimatedPowerW déclaré : P3=1500, P4=3000. Hystérésis état 4 : entrée P4×1,2=3600, sortie P4×1,0=3000.
|
||||
const QHash<int, double> pacPower({ {1, 0.0}, {2, 0.0}, {3, 1500.0}, {4, 3000.0} });
|
||||
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||
|
||||
// ===================== Volets 1-3 : PAC seule =====================
|
||||
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");
|
||||
ThingManager *tm = NymeaCore::instance()->thingManager();
|
||||
|
||||
QUuid meterId = addMeter();
|
||||
m_experiencePlugin->energyManager()->setRootMeter(meterId);
|
||||
Thing *meter = tm->findConfiguredThing(meterId);
|
||||
QVERIFY(meter);
|
||||
meter->setStateValue("connected", true);
|
||||
|
||||
QUuid k1 = addPowerSwitch(0, 26661);
|
||||
QUuid k2 = addPowerSwitch(0, 26662);
|
||||
Thing *relayK1 = tm->findConfiguredThing(k1);
|
||||
Thing *relayK2 = tm->findConfiguredThing(k2);
|
||||
QVERIFY(relayK1 && relayK2);
|
||||
|
||||
SgReadyAdapter *pac = new SgReadyAdapter(
|
||||
tm, "pac-test", "PAC test",
|
||||
QHash<int, QList<QString>>({ {1, {k1.toString()}}, {2, {}},
|
||||
{3, {k2.toString()}}, {4, {k1.toString(), k2.toString()}} }),
|
||||
pacPower, 300, 1, arbitrator);
|
||||
arbitrator->registerSgReadyAdapter(pac);
|
||||
|
||||
auto setMeterW = [&](double signedW){ meter->setStateValue("currentPower", signedW); }; // <0 export
|
||||
auto cycle = [&](const QDateTime &now){ arbitrator->simulationCallUpdate(now); QCoreApplication::processEvents(); };
|
||||
|
||||
// --- Volet 1 : montée d'états 2 → 3 → 4 (mapping sémantique) ---
|
||||
setMeterW(-1000); cycle(t0); // budget 1000 < P3 → état 2 (normal)
|
||||
QCOMPARE(pac->currentState(), 2);
|
||||
setMeterW(-2000); cycle(t0); // budget 2000 ≥ P3 → état 3 (reco)
|
||||
QCOMPARE(pac->currentState(), 3);
|
||||
QCOMPARE(relayK2->stateValue("power").toBool(), true);
|
||||
QCOMPARE(relayK1->stateValue("power").toBool(), false);
|
||||
setMeterW(-2500); cycle(t0.addSecs(400)); // budget 2500+1500=4000 ≥ P4×1,2 → état 4 (hold écoulé)
|
||||
QCOMPARE(pac->currentState(), 4);
|
||||
QCOMPARE(relayK1->stateValue("power").toBool(), true);
|
||||
QCOMPARE(relayK2->stateValue("power").toBool(), true);
|
||||
|
||||
// --- Volet 2 : hystérésis 3↔4 (budget oscille dans la zone morte [P4×1,0 ; P4×1,2)) ---
|
||||
// hold écoulé à chaque cycle (lastSwitch=T0+400) → c'est la ZONE MORTE qui tient l'état 4, pas le verrou.
|
||||
setMeterW(-300); cycle(t0.addSecs(800)); // budget 300+3000=3300 ∈ [3000,3600) → reste 4
|
||||
QCOMPARE(pac->currentState(), 4);
|
||||
setMeterW(-100); cycle(t0.addSecs(1200)); // budget 3100 → reste 4
|
||||
QCOMPARE(pac->currentState(), 4);
|
||||
setMeterW(-500); cycle(t0.addSecs(1600)); // budget 3500 → reste 4
|
||||
QCOMPARE(pac->currentState(), 4);
|
||||
// En-dessous de P4×1,0 → sort enfin de l'état 4 (vers 3).
|
||||
setMeterW(200); cycle(t0.addSecs(2000)); // import 200 → budget -200+3000=2800 < 3000 → état 3
|
||||
QCOMPARE(pac->currentState(), 3);
|
||||
|
||||
// --- Volet 3 : protection court-cycling (changement avant minStateHold → GELÉ) ---
|
||||
// lastSwitch=T0+2000. À T0+2100 (elapsed 100 < hold 300) : surplus abondant mais GELÉ en 3.
|
||||
setMeterW(-3000); cycle(t0.addSecs(2100));
|
||||
QCOMPARE(pac->currentState(), 3); // gelé malgré budget ≥ P4×1,2 (protection compresseur)
|
||||
// À T0+2400 (elapsed 400 > hold) : MÊME surplus → bascule en 4. Seul le temps simulé a changé.
|
||||
setMeterW(-3000); cycle(t0.addSecs(2400));
|
||||
QCOMPARE(pac->currentState(), 4);
|
||||
|
||||
// ===================== Volet 4 : interaction budget PARTAGÉ ECS↔PAC =====================
|
||||
// Surplus 3000 W, ECS palier 1 = 2400 W, PAC P3 = 1500. Selon l'ordre de priorité,
|
||||
// l'un se sert et l'autre voit le RELIQUAT → preuve du waterfall unifié (un seul budget).
|
||||
// priority fixé à la création → on ré-initialise un arbitre frais par ordre testé.
|
||||
auto runSharedBudget = [&](int ecsPrio, int pacPrio, int &ecsStageOut, int &pacStateOut) {
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||
QVERIFY(arb);
|
||||
ThingManager *tm2 = NymeaCore::instance()->thingManager();
|
||||
|
||||
QUuid mId = addMeter();
|
||||
m_experiencePlugin->energyManager()->setRootMeter(mId);
|
||||
Thing *m2 = tm2->findConfiguredThing(mId);
|
||||
QVERIFY(m2);
|
||||
m2->setStateValue("connected", true);
|
||||
|
||||
QUuid ke = addPowerSwitch(0, 26663); // relais ECS
|
||||
QUuid j1 = addPowerSwitch(0, 26661); // relais PAC K1
|
||||
QUuid j2 = addPowerSwitch(0, 26662); // relais PAC K2
|
||||
|
||||
// ECS : 1 palier à 2400 W, verrous à 0 (on teste le partage de budget, pas l'anti-rebond).
|
||||
EcsRelayAdapter *ecs = new EcsRelayAdapter(
|
||||
tm2, "ecs-wf", "ECS waterfall",
|
||||
QList<int>({0, 2400}),
|
||||
QList<QList<QString>>({ {}, {ke.toString()} }),
|
||||
0, 0, ecsPrio, arb);
|
||||
arb->registerEcsAdapter(ecs);
|
||||
|
||||
SgReadyAdapter *pacWf = new SgReadyAdapter(
|
||||
tm2, "pac-wf", "PAC waterfall",
|
||||
QHash<int, QList<QString>>({ {1, {j1.toString()}}, {2, {}},
|
||||
{3, {j2.toString()}}, {4, {j1.toString(), j2.toString()}} }),
|
||||
pacPower, 300, pacPrio, arb);
|
||||
arb->registerSgReadyAdapter(pacWf);
|
||||
|
||||
m2->setStateValue("currentPower", -3000); // export 3000 W
|
||||
arb->simulationCallUpdate(t0);
|
||||
QCoreApplication::processEvents();
|
||||
ecsStageOut = ecs->currentStage();
|
||||
pacStateOut = pacWf->currentState();
|
||||
};
|
||||
|
||||
int ecsStage = -1, pacState = -1;
|
||||
|
||||
// ECS prioritaire (rang 1) : ECS se sert (2400) → reliquat 600 < P3 → PAC reste en NORMAL (2).
|
||||
runSharedBudget(/*ecsPrio*/ 1, /*pacPrio*/ 2, ecsStage, pacState);
|
||||
QCOMPARE(ecsStage, 1);
|
||||
QCOMPARE(pacState, 2);
|
||||
|
||||
// Priorités INVERSÉES — PAC prioritaire (rang 1) : PAC se sert (état 3, 1500) → reliquat
|
||||
// 1500 < 2400 → l'ECS reste éteint (palier 0). L'ordre de service s'inverse.
|
||||
runSharedBudget(/*ecsPrio*/ 2, /*pacPrio*/ 1, ecsStage, pacState);
|
||||
QCOMPARE(ecsStage, 0);
|
||||
QCOMPARE(pacState, 3);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::run_data()
|
||||
{
|
||||
// Simulation infos
|
||||
|
||||
@ -64,6 +64,10 @@ private slots:
|
||||
// planification suspendue (ECS reste 0 sur N cycles), reprise au retour du compteur.
|
||||
void testMeterSilentFallback();
|
||||
|
||||
// SG-Ready (PAC) : montée d'états sur surplus, hystérésis 3↔4, protection court-cycling,
|
||||
// et interaction budget PARTAGÉ ECS↔PAC (preuve du waterfall unifié 3e).
|
||||
void testSgReadySurplus();
|
||||
|
||||
void printStates(Thing *thing);
|
||||
void updateChargerMeter(Thing *thing);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user