test(etm): restaurer testEcsRelayTopologies côté routeur + test config→RelayRouter (rév. 3)
testEcsRelayTopologies réécrit pour le RelayRouter (la combinatoire vit maintenant dans le
routeur, plus dans le thing) :
- 1 relais (dégénéré [0, 2000]) ;
- 3 relais 500/1000/2000 NON-CASCADÉ : paliers dérivés [0,500,..,3500], arrondi watts→combo,
transition 1500→2000={r2000} seul (off-before-on, commute 3 relais) ;
- DÉDUPLICATION : deux relais identiques 1000 W → niveaux fusionnés [0,1000,2000] (3, pas 4),
le palier 1000 n'utilise qu'un relais — prouve que la dérivation dédoublonne.
testLoadConfigRelayRouter (nouveau) : chaîne COMPLÈTE SetConfigs(relays[]) → store.changed →
rebuild → RelayRouter → cycle surplus → commutation relais → currentPowerW (pas les pièces
isolées).
Build prod 0/0 ; suite complète verte (8/8 tests à 3 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
88626cfd56
commit
4d2e457ed2
@ -657,11 +657,161 @@ void Simulation::testSgReadySurplus()
|
||||
|
||||
void Simulation::testEcsRelayTopologies()
|
||||
{
|
||||
// [T2] DÉSACTIVÉ — les topologies relais (1 relais, 3 relais non-cascadé, mapping
|
||||
// powerLevels↔combinaisons) sont désormais la responsabilité du THING etmvariableload, plus
|
||||
// de l'energymanager (contrat rév. 2 §1/§3 : la combinatoire matérielle vit dans le thing).
|
||||
// Côté moteur, seul l'arrondi au powerLevels (Setpoint W) sera testé — TODO T3.
|
||||
QSKIP("TODO T3 — la combinatoire relais part dans le thing ; le moteur ne teste que l'arrondi W.");
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("testEcsRelayTopologies nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
// [rév. 3] La combinatoire watts→relais vit DANS le RelayRouter (couche routeur, frontière
|
||||
// déplacée). On teste directement applyAction(Setpoint W) : paliers DÉRIVÉS, arrondi à la
|
||||
// combinaison ≤ setpoint, off-before-on non-cascadé, et DÉDUPLICATION des niveaux.
|
||||
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||
auto sp = [&](double w) {
|
||||
LoadAction a;
|
||||
a.kind = LoadAction::Setpoint; a.funding = LoadAction::Surplus;
|
||||
a.powerW = w; a.reason = QStringLiteral("test topo");
|
||||
return a;
|
||||
};
|
||||
auto freshSetup = [&](ThingManager *&tm, QObject *&owner) {
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||
QVERIFY(arb);
|
||||
tm = NymeaCore::instance()->thingManager();
|
||||
owner = arb;
|
||||
};
|
||||
|
||||
// ===================== Topologie 1 : 1 relais (dégénéré [0, 2000]) =====================
|
||||
{
|
||||
ThingManager *tm; QObject *owner; freshSetup(tm, owner);
|
||||
QUuid r = addPowerSwitch(2000, 26661);
|
||||
Thing *relay = tm->findConfiguredThing(r);
|
||||
QVERIFY(relay);
|
||||
RelayRouter *ecs = new RelayRouter(tm, "ecs-1", "ECS 1 relais",
|
||||
QList<LoadConfigRelay>({ {r.toString(), 2000} }), 0, 0, 1, LoadNeeds(), owner);
|
||||
QCOMPARE(ecs->descriptor().declared.powerLevels, QList<int>({0, 2000}));
|
||||
|
||||
ecs->applyAction(sp(2500), t0); // 2500 → palier 2000
|
||||
QCOMPARE(qRound(ecs->currentSetpointW()), 2000);
|
||||
QCOMPARE(relay->stateValue("power").toBool(), true);
|
||||
ecs->applyAction(sp(1000), t0.addSecs(1)); // 1000 < 2000 → 0
|
||||
QCOMPARE(qRound(ecs->currentSetpointW()), 0);
|
||||
QCOMPARE(relay->stateValue("power").toBool(), false);
|
||||
}
|
||||
|
||||
// ============= Topologie 2 : 3 relais 500/1000/2000 (NON-CASCADÉ, off-before-on) =============
|
||||
{
|
||||
ThingManager *tm; QObject *owner; freshSetup(tm, owner);
|
||||
QUuid r500 = addPowerSwitch(500, 26661);
|
||||
QUuid r1000 = addPowerSwitch(1000, 26662);
|
||||
QUuid r2000 = addPowerSwitch(2000, 26663);
|
||||
Thing *t500 = tm->findConfiguredThing(r500);
|
||||
Thing *t1000 = tm->findConfiguredThing(r1000);
|
||||
Thing *t2000 = tm->findConfiguredThing(r2000);
|
||||
QVERIFY(t500 && t1000 && t2000);
|
||||
RelayRouter *ecs = new RelayRouter(tm, "ecs-3", "ECS 3 relais",
|
||||
QList<LoadConfigRelay>({ {r500.toString(), 500}, {r1000.toString(), 1000}, {r2000.toString(), 2000} }),
|
||||
0, 0, 1, LoadNeeds(), owner);
|
||||
// 8 niveaux dérivés (2^3 combinaisons toutes distinctes).
|
||||
QCOMPARE(ecs->descriptor().declared.powerLevels, QList<int>({0, 500, 1000, 1500, 2000, 2500, 3000, 3500}));
|
||||
|
||||
// 1700 → palier 1500 = {r500, r1000} (r2000 OFF).
|
||||
ecs->applyAction(sp(1700), t0);
|
||||
QCOMPARE(qRound(ecs->currentSetpointW()), 1500);
|
||||
QCOMPARE(t500->stateValue("power").toBool(), true);
|
||||
QCOMPARE(t1000->stateValue("power").toBool(), true);
|
||||
QCOMPARE(t2000->stateValue("power").toBool(), false);
|
||||
|
||||
// Transition NON-CASCADÉE 1500 → 2000 = {r2000} SEUL : commute 3 relais (off-before-on).
|
||||
ecs->applyAction(sp(2000), t0.addSecs(1));
|
||||
QCOMPARE(qRound(ecs->currentSetpointW()), 2000);
|
||||
QCOMPARE(t500->stateValue("power").toBool(), false);
|
||||
QCOMPARE(t1000->stateValue("power").toBool(), false);
|
||||
QCOMPARE(t2000->stateValue("power").toBool(), true);
|
||||
}
|
||||
|
||||
// ============= Topologie 3 : DÉDUPLICATION (deux relais identiques 1000 W) =============
|
||||
{
|
||||
ThingManager *tm; QObject *owner; freshSetup(tm, owner);
|
||||
QUuid rA = addPowerSwitch(1000, 26661);
|
||||
QUuid rB = addPowerSwitch(1000, 26662);
|
||||
Thing *tA = tm->findConfiguredThing(rA);
|
||||
Thing *tB = tm->findConfiguredThing(rB);
|
||||
QVERIFY(tA && tB);
|
||||
RelayRouter *ecs = new RelayRouter(tm, "ecs-dup", "ECS dédup",
|
||||
QList<LoadConfigRelay>({ {rA.toString(), 1000}, {rB.toString(), 1000} }), 0, 0, 1, LoadNeeds(), owner);
|
||||
// 4 combinaisons MAIS deux donnent 1000 W → FUSIONNÉES : 3 niveaux, pas 4.
|
||||
QCOMPARE(ecs->descriptor().declared.powerLevels, QList<int>({0, 1000, 2000}));
|
||||
|
||||
// 1200 → palier 1000 = UN SEUL relais (le premier de la combinaison dédupliquée).
|
||||
ecs->applyAction(sp(1200), t0);
|
||||
QCOMPARE(qRound(ecs->currentSetpointW()), 1000);
|
||||
QCOMPARE(tA->stateValue("power").toBool(), true);
|
||||
QCOMPARE(tB->stateValue("power").toBool(), false);
|
||||
// 2200 → palier 2000 = les deux.
|
||||
ecs->applyAction(sp(2200), t0.addSecs(1));
|
||||
QCOMPARE(qRound(ecs->currentSetpointW()), 2000);
|
||||
QCOMPARE(tA->stateValue("power").toBool(), true);
|
||||
QCOMPARE(tB->stateValue("power").toBool(), true);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testLoadConfigRelayRouter()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("testLoadConfigRelayRouter nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
// Chaîne COMPLÈTE rév. 3 : SetConfigs(relays[]) → store.changed → rebuild construit un
|
||||
// RelayRouter → cycle surplus → arrondi scheduler → routeur → commutation relais → currentPowerW.
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
EnergyArbitrator *arbitrator = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||
QVERIFY(arbitrator);
|
||||
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 rA = addPowerSwitch(1000, 26661);
|
||||
QUuid rB = addPowerSwitch(1500, 26662);
|
||||
Thing *relayA = tm->findConfiguredThing(rA);
|
||||
Thing *relayB = tm->findConfiguredThing(rB);
|
||||
QVERIFY(relayA && relayB);
|
||||
|
||||
const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-relayrouter.json";
|
||||
QFile::remove(cfgPath);
|
||||
qputenv("NYMEA_ENERGY_LOAD_CONFIG", cfgPath.toUtf8());
|
||||
LoadConfigStore *store = new LoadConfigStore(arbitrator);
|
||||
arbitrator->setLoadConfigStore(store);
|
||||
|
||||
LoadConfigs cfgs;
|
||||
cfgs.append(LoadConfig::fromMap(QVariantMap{
|
||||
{"id", "ecs-relais"}, {"label", "ECS relais"}, {"adapter", "relay-router"}, {"mode", "fixed"},
|
||||
{"priority", 1}, {"enabled", true},
|
||||
{"relays", QVariantList()
|
||||
<< QVariantMap{{"thingId", rA.toString()}, {"powerW", 1000}}
|
||||
<< QVariantMap{{"thingId", rB.toString()}, {"powerW", 1500}}},
|
||||
{"minOnS", 0}, {"minOffS", 0}}));
|
||||
QString err;
|
||||
QVERIFY2(store->setConfigs(cfgs, &err), err.toUtf8()); // persiste + changed → rebuild → RelayRouter
|
||||
|
||||
// Surplus 2500 → paliers dérivés [0,1000,1500,2500] → palier 2500 (rA+rB) → les deux ON.
|
||||
meter->setStateValue("currentPower", -2500);
|
||||
arbitrator->simulationCallUpdate(utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0)));
|
||||
QCoreApplication::processEvents();
|
||||
QCOMPARE(relayA->stateValue("power").toBool(), true);
|
||||
QCOMPARE(relayB->stateValue("power").toBool(), true);
|
||||
// currentPowerW remonte (mock : relais ON → currentPower = nominal) → source du recrédit.
|
||||
QCOMPARE(qRound(relayA->stateValue("currentPower").toDouble()), 1000);
|
||||
QCOMPARE(qRound(relayB->stateValue("currentPower").toDouble()), 1500);
|
||||
|
||||
qunsetenv("NYMEA_ENERGY_LOAD_CONFIG");
|
||||
QFile::remove(cfgPath);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::run_data()
|
||||
|
||||
@ -70,13 +70,15 @@ private slots:
|
||||
void testLoadConfigBuildsAdapters();
|
||||
// [T4] Injection RPC end-to-end : NymeaEnergy.Get/SetLoadConfig (couche consommée par l'app).
|
||||
void testLoadConfigRpc();
|
||||
// [rév.3] Chaîne complète : SetConfigs(relays[]) → rebuild → RelayRouter → cycle → commutation relais.
|
||||
void testLoadConfigRelayRouter();
|
||||
|
||||
// SG-Ready (PAC) : montée d'états sur surplus, hystérésis 3↔4, protection court-cycling,
|
||||
// + Volet 4 budget PARTAGÉ etmvariableload(ECS)↔PAC (waterfall unifié, inversion priorité).
|
||||
void testSgReadySurplus();
|
||||
|
||||
// [T2] DÉSACTIVÉ (QSKIP) — la combinatoire relais part dans le thing etmvariableload ;
|
||||
// le moteur ne testera plus que l'arrondi en watts. TODO T3.
|
||||
// [rév.3] Combinatoire watts→relais DANS le RelayRouter : paliers dérivés, off-before-on,
|
||||
// transition non-cascadée, et DÉDUPLICATION des niveaux (relais identiques fusionnés).
|
||||
void testEcsRelayTopologies();
|
||||
|
||||
void printStates(Thing *thing);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user