[3e+] ECS non-cascadé : applyRelayStage off-before-on + tests 1 et 3 relais
applyRelayStage faisait déjà du set-cible complet (delta correct, gère le non-cascadé) : durcissement off-before-on (anti sur-puissance transitoire quand monter de palier éteint des relais, ex. 3 résistances 500/1000/2000 : 1500→2000 commute 3 relais) + intention documentée (comme SG-Ready). testEcsRelayTopologies : ECS simple 1 relais (on/off) + ECS 3 relais non-cascadé (transition 1500→2000 → set final r2000 SEUL, r500/r1000 coupés). Couvre les 2 topologies du test terrain vendredi. Suite simulation 20/20, plugin prod 0/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
51760a7f61
commit
dfdd9884d0
@ -172,7 +172,10 @@ bool EcsRelayAdapter::lockActive(int newStage, const QDateTime &now) const
|
||||
|
||||
void EcsRelayAdapter::applyRelayStage(int stage)
|
||||
{
|
||||
// Ensemble des relais ON pour le nouveau stage
|
||||
// Set de relais CIBLE du palier (delta complet : chaque relais connu est amené à son
|
||||
// état cible on/off, pas d'ajout incrémental). Gère les mappings NON-CASCADÉS où monter
|
||||
// d'un palier éteint des relais (ex. 3 résistances 500/1000/2000 W : 1500=[r500,r1000]
|
||||
// → 2000=[r2000] commute 3 relais).
|
||||
const QSet<QString> wantOn = [&]() {
|
||||
QSet<QString> s;
|
||||
if (stage < m_relayMapping.size())
|
||||
@ -181,28 +184,34 @@ void EcsRelayAdapter::applyRelayStage(int stage)
|
||||
return s;
|
||||
}();
|
||||
|
||||
// Union de tous les relais connus
|
||||
QSet<QString> allRelays;
|
||||
for (const auto &list : m_relayMapping)
|
||||
for (const QString &id : list)
|
||||
allRelays.insert(id);
|
||||
|
||||
for (const QString &thingId : allRelays) {
|
||||
auto writeRelay = [&](const QString &thingId, bool on) {
|
||||
Thing *relay = m_thingManager->findConfiguredThing(ThingId(thingId));
|
||||
if (!relay) {
|
||||
qCWarning(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label
|
||||
<< "— relais non trouvé:" << thingId;
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
const bool targetOn = wantOn.contains(thingId);
|
||||
StateType powerStateType = relay->thingClass().stateTypes().findByName("power");
|
||||
if (!powerStateType.id().isNull()) {
|
||||
Action powerAction(powerStateType.id(), relay->id(), Action::TriggeredByRule);
|
||||
powerAction.setParams(ParamList() << Param(powerStateType.id(), targetOn));
|
||||
powerAction.setParams(ParamList() << Param(powerStateType.id(), on));
|
||||
m_thingManager->executeAction(powerAction);
|
||||
} else {
|
||||
// Fallback mock : setStateValue direct (Things sans actionType "power")
|
||||
relay->setStateValue("power", targetOn);
|
||||
relay->setStateValue("power", on); // repli mock
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Intention (résistif : transitoire inoffensif, mais portée comme SG-Ready) : COUPER
|
||||
// d'abord les relais hors-cible, PUIS enclencher ceux de la cible → pas de sur-puissance
|
||||
// transitoire (somme des deux paliers) sur une transition non-cascadée.
|
||||
for (const QString &id : allRelays)
|
||||
if (!wantOn.contains(id))
|
||||
writeRelay(id, false);
|
||||
for (const QString &id : wantOn)
|
||||
writeRelay(id, true);
|
||||
}
|
||||
|
||||
@ -355,6 +355,103 @@ void Simulation::testSgReadySurplus()
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testEcsRelayTopologies()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("testEcsRelayTopologies nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||
|
||||
// Setup commun : arbitre frais + root meter. Retourne meter + thingManager via réf.
|
||||
auto freshSetup = [&](EnergyArbitrator *&arb, ThingManager *&tm, Thing *&meter) {
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||
QVERIFY(arb);
|
||||
tm = NymeaCore::instance()->thingManager();
|
||||
QUuid mId = addMeter();
|
||||
m_experiencePlugin->energyManager()->setRootMeter(mId);
|
||||
meter = tm->findConfiguredThing(mId);
|
||||
QVERIFY(meter);
|
||||
meter->setStateValue("connected", true);
|
||||
};
|
||||
|
||||
// ===================== Topologie 1 : ECS simple (1 relais, [0, 2000]) =====================
|
||||
{
|
||||
EnergyArbitrator *arb; ThingManager *tm; Thing *meter;
|
||||
freshSetup(arb, tm, meter);
|
||||
QUuid r = addPowerSwitch(2000, 26661);
|
||||
Thing *relay = tm->findConfiguredThing(r);
|
||||
QVERIFY(relay);
|
||||
EcsRelayAdapter *ecs = new EcsRelayAdapter(
|
||||
tm, "ecs-1relay", "ECS simple",
|
||||
QList<int>({0, 2000}),
|
||||
QList<QList<QString>>({ {}, {r.toString()} }),
|
||||
0, 0, 1, arb);
|
||||
arb->registerEcsAdapter(ecs);
|
||||
|
||||
meter->setStateValue("currentPower", -2500); // surplus 2500 → palier 1
|
||||
arb->simulationCallUpdate(t0); QCoreApplication::processEvents();
|
||||
QCOMPARE(ecs->currentStage(), 1);
|
||||
QCOMPARE(relay->stateValue("power").toBool(), true);
|
||||
|
||||
meter->setStateValue("currentPower", 1000); // import 1000 → palier 0 (off)
|
||||
arb->simulationCallUpdate(t0.addSecs(1)); QCoreApplication::processEvents();
|
||||
QCOMPARE(ecs->currentStage(), 0);
|
||||
QCOMPARE(relay->stateValue("power").toBool(), false);
|
||||
}
|
||||
|
||||
// ============= Topologie 2 : ECS 3 relais 500/1000/2000 W (mapping NON-CASCADÉ) =============
|
||||
{
|
||||
EnergyArbitrator *arb; ThingManager *tm; Thing *meter;
|
||||
freshSetup(arb, tm, meter);
|
||||
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);
|
||||
|
||||
// 8 niveaux binaires ; encodage bit0=r500, bit1=r1000, bit2=r2000.
|
||||
EcsRelayAdapter *ecs = new EcsRelayAdapter(
|
||||
tm, "ecs-3relay", "ECS 3 relais",
|
||||
QList<int>({0, 500, 1000, 1500, 2000, 2500, 3000, 3500}),
|
||||
QList<QList<QString>>({
|
||||
{}, // 0
|
||||
{r500.toString()}, // 500
|
||||
{r1000.toString()}, // 1000
|
||||
{r500.toString(), r1000.toString()}, // 1500
|
||||
{r2000.toString()}, // 2000
|
||||
{r500.toString(), r2000.toString()}, // 2500
|
||||
{r1000.toString(), r2000.toString()}, // 3000
|
||||
{r500.toString(), r1000.toString(), r2000.toString()}// 3500
|
||||
}),
|
||||
0, 0, 1, arb);
|
||||
arb->registerEcsAdapter(ecs);
|
||||
|
||||
// Surplus 1700 → palier 1500 = [r500, r1000] (r2000 éteint).
|
||||
meter->setStateValue("currentPower", -1700);
|
||||
arb->simulationCallUpdate(t0); QCoreApplication::processEvents();
|
||||
QCOMPARE(ecs->currentStage(), 3);
|
||||
QCOMPARE(t500->stateValue("power").toBool(), true);
|
||||
QCOMPARE(t1000->stateValue("power").toBool(), true);
|
||||
QCOMPARE(t2000->stateValue("power").toBool(), false);
|
||||
|
||||
// Transition NON-CASCADÉE 1500 → 2000 : à stage 3 l'ECS mesure 1500 W (r500+r1000),
|
||||
// export net 700 → budget 700+1500=2200 → palier 2000 = [r2000] SEUL.
|
||||
// Vérifie le set FINAL : r500 OFF, r1000 OFF, r2000 ON (commutation de 3 relais).
|
||||
meter->setStateValue("currentPower", -700);
|
||||
arb->simulationCallUpdate(t0.addSecs(1)); QCoreApplication::processEvents();
|
||||
QCOMPARE(ecs->currentStage(), 4);
|
||||
QCOMPARE(t500->stateValue("power").toBool(), false);
|
||||
QCOMPARE(t1000->stateValue("power").toBool(), false);
|
||||
QCOMPARE(t2000->stateValue("power").toBool(), true);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::run_data()
|
||||
{
|
||||
// Simulation infos
|
||||
|
||||
@ -68,6 +68,10 @@ private slots:
|
||||
// et interaction budget PARTAGÉ ECS↔PAC (preuve du waterfall unifié 3e).
|
||||
void testSgReadySurplus();
|
||||
|
||||
// ECS topologies : 1 relais (simple) + 3 relais valeurs différentes (mapping NON-CASCADÉ,
|
||||
// transition 1500→2000 qui commute 3 relais) — vérifie le set de relais final correct.
|
||||
void testEcsRelayTopologies();
|
||||
|
||||
void printStates(Thing *thing);
|
||||
void updateChargerMeter(Thing *thing);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user