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
170c406f2a
commit
e7a15c3c5d
@ -657,11 +657,161 @@ void Simulation::testSgReadySurplus()
|
|||||||
|
|
||||||
void Simulation::testEcsRelayTopologies()
|
void Simulation::testEcsRelayTopologies()
|
||||||
{
|
{
|
||||||
// [T2] DÉSACTIVÉ — les topologies relais (1 relais, 3 relais non-cascadé, mapping
|
#ifndef ETM_ARBITRATOR
|
||||||
// powerLevels↔combinaisons) sont désormais la responsabilité du THING etmvariableload, plus
|
QSKIP("testEcsRelayTopologies nécessite ETM_ARBITRATOR.");
|
||||||
// de l'energymanager (contrat rév. 2 §1/§3 : la combinatoire matérielle vit dans le thing).
|
#else
|
||||||
// Côté moteur, seul l'arrondi au powerLevels (Setpoint W) sera testé — TODO T3.
|
// [rév. 3] La combinatoire watts→relais vit DANS le RelayRouter (couche routeur, frontière
|
||||||
QSKIP("TODO T3 — la combinatoire relais part dans le thing ; le moteur ne teste que l'arrondi W.");
|
// 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()
|
void Simulation::run_data()
|
||||||
|
|||||||
@ -70,13 +70,15 @@ private slots:
|
|||||||
void testLoadConfigBuildsAdapters();
|
void testLoadConfigBuildsAdapters();
|
||||||
// [T4] Injection RPC end-to-end : NymeaEnergy.Get/SetLoadConfig (couche consommée par l'app).
|
// [T4] Injection RPC end-to-end : NymeaEnergy.Get/SetLoadConfig (couche consommée par l'app).
|
||||||
void testLoadConfigRpc();
|
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,
|
// 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é).
|
// + Volet 4 budget PARTAGÉ etmvariableload(ECS)↔PAC (waterfall unifié, inversion priorité).
|
||||||
void testSgReadySurplus();
|
void testSgReadySurplus();
|
||||||
|
|
||||||
// [T2] DÉSACTIVÉ (QSKIP) — la combinatoire relais part dans le thing etmvariableload ;
|
// [rév.3] Combinatoire watts→relais DANS le RelayRouter : paliers dérivés, off-before-on,
|
||||||
// le moteur ne testera plus que l'arrondi en watts. TODO T3.
|
// transition non-cascadée, et DÉDUPLICATION des niveaux (relais identiques fusionnés).
|
||||||
void testEcsRelayTopologies();
|
void testEcsRelayTopologies();
|
||||||
|
|
||||||
void printStates(Thing *thing);
|
void printStates(Thing *thing);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user