From dfdd9884d056f9e6d3bdcffac7785d583ba14d91 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Wed, 10 Jun 2026 00:09:24 +0200 Subject: [PATCH] =?UTF-8?q?[3e+]=20ECS=20non-cascad=C3=A9=20:=20applyRelay?= =?UTF-8?q?Stage=20off-before-on=20+=20tests=201=20et=203=20relais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- energyplugin/etm/adapters/ecsrelayadapter.cpp | 27 ++++-- tests/auto/simulation/simulation.cpp | 97 +++++++++++++++++++ tests/auto/simulation/simulation.h | 4 + 3 files changed, 119 insertions(+), 9 deletions(-) diff --git a/energyplugin/etm/adapters/ecsrelayadapter.cpp b/energyplugin/etm/adapters/ecsrelayadapter.cpp index 7b1b8da..379454b 100644 --- a/energyplugin/etm/adapters/ecsrelayadapter.cpp +++ b/energyplugin/etm/adapters/ecsrelayadapter.cpp @@ -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 wantOn = [&]() { QSet s; if (stage < m_relayMapping.size()) @@ -181,28 +184,34 @@ void EcsRelayAdapter::applyRelayStage(int stage) return s; }(); - // Union de tous les relais connus QSet 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); } diff --git a/tests/auto/simulation/simulation.cpp b/tests/auto/simulation/simulation.cpp index 0407c96..cd0f528 100644 --- a/tests/auto/simulation/simulation.cpp +++ b/tests/auto/simulation/simulation.cpp @@ -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(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({0, 2000}), + QList>({ {}, {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({0, 500, 1000, 1500, 2000, 2500, 3000, 3500}), + QList>({ + {}, // 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 diff --git a/tests/auto/simulation/simulation.h b/tests/auto/simulation/simulation.h index 5827493..6090452 100644 --- a/tests/auto/simulation/simulation.h +++ b/tests/auto/simulation/simulation.h @@ -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);