// SPDX-License-Identifier: GPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-energy-plugin-nymea. * * nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * nymea-energy-plugin-nymea.s distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with nymea-energy-plugin-nymea. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "simulation.h" #include #include #include #include using namespace nymeaserver; #include "../../../energyplugin/smartchargingmanager.h" #include "../../mocks/spotmarketprovider/spotmarketdataprovidermock.h" #ifdef ETM_ARBITRATOR #include "../../../energyplugin/etm/energyarbitrator.h" #include "../../../energyplugin/etm/adapters/ecsrelayadapter.h" #include "../../../energyplugin/etm/adapters/sgreadyadapter.h" #endif #include #include #include #include #include #include #include #include #include #include "simulationtestpoint.h" void Simulation::testEcsSurplusPV() { #ifndef ETM_ARBITRATOR QSKIP("testEcsSurplusPV nécessite ETM_ARBITRATOR."); #else // Préambule harnais (comme run()) : DB + (ré)initialisation → expérience chargée et // arbitre FRAIS (pas d'adaptateur ECS résiduel d'un test précédent). cleanupTestCase(); m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite"; initTestCase(); EnergyArbitrator *arbitrator = dynamic_cast(m_experiencePlugin->smartChargingManager()); QVERIFY2(arbitrator, "smartChargingManager n'est pas un EnergyArbitrator (ETM_ARBITRATOR requis)"); ThingManager *thingManager = NymeaCore::instance()->thingManager(); // --- Root meter --- QUuid meterThingId = addMeter(); QVERIFY2(!meterThingId.isNull(), "meter thingId invalide"); m_experiencePlugin->energyManager()->setRootMeter(meterThingId); Thing *meterThing = thingManager->findConfiguredThing(meterThingId); QVERIFY(meterThing); meterThing->setStateValue("connected", true); // --- Deux relais ECS : palier 1 = relayA (1200 W), palier 2 = A+B (2400 W) --- QUuid relayAId = addPowerSwitch(1200, 26661); QUuid relayBId = addPowerSwitch(1200, 26662); QVERIFY(!relayAId.isNull() && !relayBId.isNull()); Thing *relayA = thingManager->findConfiguredThing(relayAId); Thing *relayB = thingManager->findConfiguredThing(relayBId); QVERIFY(relayA && relayB); // minOn = 300 s (protection compresseur) ; minOff = 0 (pas de délai de redémarrage ici). const int minOnS = 300; EcsRelayAdapter *ecs = new EcsRelayAdapter( thingManager, "ecs-surplus-test", "Chauffe-eau (test surplus)", QList({0, 1200, 2400}), QList>({ {}, {relayAId.toString()}, {relayAId.toString(), relayBId.toString()} }), minOnS, 0, 1, arbitrator); arbitrator->registerEcsAdapter(ecs); const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0)); // currentPower compteur : < 0 = export (surplus PV), > 0 = import (réseau). auto setMeterW = [&](double signedW){ meterThing->setStateValue("currentPower", signedW); }; auto cycle = [&](const QDateTime &now){ arbitrator->simulationCallUpdate(now); QCoreApplication::processEvents(); }; // ============ Régime 1 — cascade montante sur surplus (export) ============ setMeterW(-1000); cycle(t0); // 1000 W < palier 1 → éteint QCOMPARE(ecs->currentStage(), 0); QCOMPARE(relayA->stateValue("power").toBool(), false); setMeterW(-1500); cycle(t0); // 1500 W → palier 1 (relayA) QCOMPARE(ecs->currentStage(), 1); QCOMPARE(relayA->stateValue("power").toBool(), true); QCOMPARE(relayB->stateValue("power").toBool(), false); setMeterW(-2500); cycle(t0.addSecs(1)); // 2500 W → palier 2 (montée autorisée même sous minOn) QCOMPARE(ecs->currentStage(), 2); QCOMPARE(relayA->stateValue("power").toBool(), true); QCOMPARE(relayB->stateValue("power").toBool(), true); // ============ Régime 2 — anti-clignotement (recrédit, hors verrou) ============ // T0+400 : minOn écoulé → plancher de verrou = 0. PV 2500, ECS tire 2400 → export net 100. // budget = 100 + 2400 (recrédit conso) = 2500 → RESTE palier 2. Sans recrédit : 100 → éteint. // Le plancher étant 0, c'est bien le RECRÉDIT qui maintient le palier, pas le verrou. setMeterW(-100); cycle(t0.addSecs(400)); QCOMPARE(ecs->currentStage(), 2); // Redescente propre au palier 1 (import partiel, minOn écoulé) pour préparer le régime 3. setMeterW(600); cycle(t0.addSecs(401)); // import 600 → budget = -600 + 2400 = 1800 → palier 1 QCOMPARE(ecs->currentStage(), 1); QCOMPARE(relayB->stateValue("power").toBool(), false); // ============ Régime 3 — PROTECTION COMPRESSEUR : import < minOn → RESTE ============ // lastSwitch = T0+401. T0+501 (elapsed 100 < minOn 300) : PV 0, ECS tire 1200 → import 1200. // budget = -1200 + 1200 = 0 → le scheduler voudrait palier 0, MAIS plancher de verrou = 1. setMeterW(1200); cycle(t0.addSecs(501)); QCOMPARE(ecs->currentStage(), 1); // RESTE allumé — protection compresseur QCOMPARE(relayA->stateValue("power").toBool(), true); // ============ Régime 4 — minOn écoulé → DÉLESTE ============ // T0+801 (elapsed depuis T0+401 = 400 > minOn 300) : MÊME import 1200. Plancher = 0 → palier 0. // Seul le TEMPS SIMULÉ a changé entre régime 3 et 4 : si le seam de temps était faux // (horloge murale), la décision ne basculerait pas. Ce test prouve le seam ET la protection. setMeterW(1200); cycle(t0.addSecs(801)); QCOMPARE(ecs->currentStage(), 0); // DÉLESTE QCOMPARE(relayA->stateValue("power").toBool(), false); #endif } void Simulation::testMeterSilentFallback() { #ifndef ETM_ARBITRATOR QSKIP("testMeterSilentFallback nécessite ETM_ARBITRATOR."); #else cleanupTestCase(); m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite"; initTestCase(); EnergyArbitrator *arbitrator = dynamic_cast(m_experiencePlugin->smartChargingManager()); QVERIFY2(arbitrator, "smartChargingManager n'est pas un EnergyArbitrator (ETM_ARBITRATOR requis)"); ThingManager *thingManager = NymeaCore::instance()->thingManager(); // --- Root meter + un relais ECS (palier 1 = 2400 W) --- QUuid meterThingId = addMeter(); QVERIFY2(!meterThingId.isNull(), "meter thingId invalide"); m_experiencePlugin->energyManager()->setRootMeter(meterThingId); Thing *meterThing = thingManager->findConfiguredThing(meterThingId); QVERIFY(meterThing); meterThing->setStateValue("connected", true); QUuid relayAId = addPowerSwitch(2400, 26661); QVERIFY(!relayAId.isNull()); Thing *relayA = thingManager->findConfiguredThing(relayAId); QVERIFY(relayA); // minOn = 300 s (protection compresseur) ; minOff = 0. EcsRelayAdapter *ecs = new EcsRelayAdapter( thingManager, "ecs-fallback-test", "Chauffe-eau (test repli)", QList({0, 2400}), QList>({ {}, {relayAId.toString()} }), 300, 0, 1, arbitrator); arbitrator->registerEcsAdapter(ecs); const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0)); auto setMeterW = [&](double signedW){ meterThing->setStateValue("currentPower", signedW); }; auto cycle = [&](const QDateTime &now){ arbitrator->simulationCallUpdate(now); QCoreApplication::processEvents(); }; // --- ECS allumé sur surplus, compteur frais à T0 --- arbitrator->recordMeterUpdate(t0); setMeterW(-2500); cycle(t0); // surplus 2500 → palier 1 (lastSwitch ECS = T0) QCOMPARE(ecs->currentStage(), 1); QCOMPARE(relayA->stateValue("power").toBool(), true); QVERIFY(!arbitrator->degradedMode()); // --- Compteur muet > 90 s → mode dégradé ; le repli force=true coupe l'ECS MÊME sous minOn --- // (ECS commuté à T0, elapsed 91 s < minOn 300 → normalement verrouillé ; force=true bypasse.) arbitrator->evaluateMeterFreshness(t0.addSecs(91)); QCoreApplication::processEvents(); QVERIFY(arbitrator->degradedMode()); QCOMPARE(ecs->currentStage(), 0); // coupé malgré minOn (sécurité bypasse l'anti-flapping) QCOMPARE(relayA->stateValue("power").toBool(), false); // --- STABILITÉ : compteur toujours muet, plusieurs update() → l'ECS RESTE à 0 --- // (planification suspendue : pas de getPlan() sur cache mort qui rallumerait l'ECS.) // On garde même un faux surplus au compteur pour piéger une éventuelle replanification. setMeterW(-3000); foreach (int dt, QList({92, 120, 200, 280})) { cycle(t0.addSecs(dt)); QVERIFY2(arbitrator->degradedMode(), "degradedMode doit rester actif pendant le silence"); QCOMPARE(ecs->currentStage(), 0); QCOMPARE(relayA->stateValue("power").toBool(), false); } // --- REPRISE : le compteur re-parle → degradedMode retombe → recalcul normal --- arbitrator->recordMeterUpdate(t0.addSecs(300)); QVERIFY(!arbitrator->degradedMode()); // Preuve "recalcul, pas restauration d'ancienne consigne" : surplus FAIBLE → l'ECS // RESTE éteint (recalculé), il ne revient PAS à son ancien palier 1. setMeterW(-1000); cycle(t0.addSecs(301)); // 1000 W < palier 1 → éteint QCOMPARE(ecs->currentStage(), 0); // Puis surplus suffisant → l'ECS resuit le surplus normalement. setMeterW(-2500); cycle(t0.addSecs(302)); QCOMPARE(ecs->currentStage(), 1); QCOMPARE(relayA->stateValue("power").toBool(), true); #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 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(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>({ {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(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({0, 2400}), QList>({ {}, {ke.toString()} }), 0, 0, ecsPrio, arb); arb->registerEcsAdapter(ecs); SgReadyAdapter *pacWf = new SgReadyAdapter( tm2, "pac-wf", "PAC waterfall", QHash>({ {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 QTest::addColumn("simulationName"); QTest::addColumn("simulationTitle"); QTest::addColumn("databaseName"); QTest::addColumn("simulationStart"); QTest::addColumn("plugEvents"); QTest::addColumn("simulationHours"); QTest::addColumn("sampleRate"); QTest::addColumn("productionScaling"); QTest::addColumn("detailsStepStart"); QTest::addColumn("detailsStepStop"); QTest::addColumn("detailsStepList"); // Houshold info QTest::addColumn("phasePowerLimit"); QTest::addColumn("spotMarketEnabled"); QTest::addColumn("spotMarketResourceData"); QTest::addColumn("acquisitionTolerance"); QTest::addColumn("batteryLevelConsideration"); // ChargingInfo QTest::addColumn("targetPercentage"); QTest::addColumn("targetDateTime"); QTest::addColumn("chargingMode"); QTest::addColumn("carBatteryLevel"); QTest::addColumn("dailySpotMarketPercentage"); // Car information and states QTest::addColumn("carCapacity"); QTest::addColumn("carMinChargingCurrent"); QTest::addColumn("carPhaseCount"); // Energy storage information and states QTest::addColumn("energyStorageAvailable"); QTest::addColumn("energyStorageCapacity"); QTest::addColumn("energyStorageMaxChargingPower"); QTest::addColumn("energyStorageMaxDischargingPower"); QTest::addColumn("energyStorageInitialBatteyLevel"); // Charger initial states QTest::addColumn("chargerConnected"); QTest::addColumn("chargerPower"); QTest::addColumn("chargerPhases"); QTest::addColumn("canSwitchPhaseCount"); QTest::addColumn("chargerMaxChargingCurrent"); QTest::addColumn("chargerMaxChargingCurrentMaxValue"); QTest::addColumn("iterationTest"); bool runAllSimulations = true; bool runSpotmarketSimulation = runAllSimulations; bool run1PhaseSimulations = runAllSimulations; bool run2PhaseSimulations = runAllSimulations; bool run3PhaseSimulations = runAllSimulations; bool runPhaseSwitchingSimulations = runAllSimulations; // Simulations if (runSpotmarketSimulation) QTest::newRow("Spotmarket only") /* Simulation info */ << "simulation-spotmarket-only-1-phase-16A" // simulationName << "Simulation (1 phase, charger 16A max, only spot market)" // simulationTitle << ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC) << ChargerPlugEvents( { ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(7,0,0)), false), ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(17,30,0)), true), ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(7,0,0)), false), ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(17,0,0)), true) }) // pluggedInTime (UTC) << 48 // simulationHours << EnergyLogs::SampleRate1Min << 0.0 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList() // detailsStepList /* Houshold info */ << 32 // phase limit (A) << true // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(22,0,0)) // targetDateTime << "ChargingModeEco" // chargingMode << 20 // carBatteryLevel << 20 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 1 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << false // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 16 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 20) } }, { 250, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), } }, { 500, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), } }, { 1440, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) // Should have charged 20% in one day } }, { 2800, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), } }, { 2880, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 60) // Should have charged 20% in one day } } } ); if (runSpotmarketSimulation) QTest::newRow("Spotmarket and PV") /* Simulation info */ << "simulation-spotmarket-and-pv-1-phase-16A" // simulationName << "Simulation (1 phase, charger 16A max, spot market and PV)" // simulationTitle << ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC) << ChargerPlugEvents() << 48 // simulationHours << EnergyLogs::SampleRate1Min << 0.35 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList() // detailsStepList /* Houshold info */ << 32 // phase limit (A) << true // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(22,0,0)) // targetDateTime << "ChargingModeEco" // chargingMode << 20 // carBatteryLevel << 20 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 1 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << false // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 16 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 20) } }, { 250, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 500, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 580, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 750, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 780, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 810, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 1400, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 2000, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 46) // Should be at least 20% more than the day before... } }, { 2250, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), } }, { 2750, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), } } } ); if (runSpotmarketSimulation) QTest::newRow("Spotmarket with target time") /* Simulation info */ << "simulation-spotmarket-only-with-targettime-1-phase-16A" // simulationName << "Simulation (1 phase, charger 16A max, only spot market with target time)" // simulationTitle << ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC) << ChargerPlugEvents() // Car plug events << 48 // simulationHours << EnergyLogs::SampleRate1Min << 0.0 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList() // detailsStepList /* Houshold info */ << 32 // phase limit (A) << true // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(22,0,0)) // targetDateTime << "ChargingModeEcoWithTargetTime" // chargingMode << 20 // carBatteryLevel << 0 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 1 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << false // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 16 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 20) } }, { 700, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 1200, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 1400, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 1550, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 1700, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 2760, { // 22:00 SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100) } } } ); if (runSpotmarketSimulation) QTest::newRow("Spotmarket only with target time") /* Simulation info */ << "simulation-spotmarket-only-with-targettime-1-day-1-phase-16A" // simulationName << "Simulation (1 phase, charger 16A max, only spot market with target time single day)" // simulationTitle << ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0, 0, 0)) // simulationStart (UTC) << ChargerPlugEvents() << 32 // simulationHours << EnergyLogs::SampleRate1Min << 0.0 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList() // detailsStepList /* Houshold info */ << 32 // phase limit (A) << true // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(07, 0, 0)) // targetDateTime << "ChargingModeEcoWithTargetTime" // chargingMode << 50 // carBatteryLevel << 0 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 1 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << false // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 16 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 50) } }, { 250, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 61) } }, { 500, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), } }, { 1400, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), } } }); if (run1PhaseSimulations) QTest::newRow("Default") /* Simulation info */ << "simulation-1-phase-32A" // simulationName << "Simulation (1 phase, charger 32A max, target 22:00 100%" // simulationTitle << ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC) << ChargerPlugEvents() // pluggedInTime (UTC) << 18 // simulationHours << EnergyLogs::SampleRate1Min << 20.0 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList() // detailsStepList /* Houshold info */ << 32 // phase limit (A) << false // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime << "ChargingModeEcoWithTargetTime" // chargingMode << 40 // carBatteryLevel << 0 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 1 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << false // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 32 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) } }, { 163, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 170, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 444, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 7) , SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 520, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6) , SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 80) } }, { 700, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6) , SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 85) } }, { 900, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 30) , SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 88) } }, { 960, { // 22:00 SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 30) , SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100) } } } ); if (run1PhaseSimulations) QTest::newRow("Default") /* Simulation info */ << "simulation-1-phase-16A" // simulationName << "Simulation (1 phase, charger 16A max, target 22:00 100%)" // simulationTitle << ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(0,0,0)) // simulationStart (UTC) << ChargerPlugEvents( { ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)), true) }) // pluggedInTime (UTC) << 24 // simulationHours << EnergyLogs::SampleRate1Min << 20.0 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList() // detailsStepList /* Houshold info */ << 32 // phase limit (A) << false // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime << "ChargingModeEcoWithTargetTime" // chargingMode << 50 // carBatteryLevel << 0 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 1 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << false // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 16 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 50) } }, { 452, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 50) } }, { 467, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 51) } }, { 750, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 830, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 841, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 1000, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 1300, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 1440, { // 22:00 SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100) } } } ); if (run2PhaseSimulations) QTest::newRow("Kostal") /* Simulation info */ << "simulation-kostal-2-phase-16A" // simulationName << "Simulation (2 phase, charger 16A max, target 22:00 100%)" // simulationTitle << ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC) << ChargerPlugEvents( { ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(8,0,0)), true) }) // pluggedInTime (UTC) << 24 // simulationHours << EnergyLogs::SampleRate1Min << 1.0 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList() // detailsStepList /* Houshold info */ << 32 // phase limit (A) << false // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(22,0,0)) // targetDateTime << "ChargingModeEcoWithTargetTime" // chargingMode << 40 // carBatteryLevel << 0 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 2 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << false // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 16 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) } }, { 550, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 600, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 48) } }, { 820, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 823, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 847, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 80) } }, { 1050, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100) } } } ); if (run2PhaseSimulations) QTest::newRow("Kostal") /* Simulation info */ << "simulation-kostal-2-phase-16A-away-2-hours" // simulationName << "Simulation (2 phase, charger 16A max, target 22:00 100%)" // simulationTitle << ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC) << ChargerPlugEvents( { ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(12,0,0)), false), ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(14,00,0)), true, 10) }) // pluggedInTime (UTC) << 24 // simulationHours << EnergyLogs::SampleRate1Min << 1.0 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList() // detailsStepList /* Houshold info */ << 32 // phase limit (A) << false // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(22,0,0)) // targetDateTime << "ChargingModeEcoWithTargetTime" // chargingMode << 40 // carBatteryLevel << 0 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 2 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << false // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 16 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) } }, { 550, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 600, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 48) } }, { 1300, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 1440, { // 22:00 SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100) } } } ); if (run2PhaseSimulations) QTest::newRow("Default") /* Simulation info */ << "simulation-2-phase-16A" // simulationName << "Simulation (2 phase, charger 16A max, target 22:00 100%)" // simulationTitle << ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC) << ChargerPlugEvents() // pluggedInTime (UTC) << 18 // simulationHours << EnergyLogs::SampleRate1Min << 20.0 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList() // detailsStepList /* Houshold info */ << 32 // phase limit (A) << false // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime << "ChargingModeEcoWithTargetTime" // chargingMode << 40 // carBatteryLevel << 0 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 2 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << false // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 16 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) } }, { 850, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 900, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 960, { // 22:00 SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100) } } } ); if (run2PhaseSimulations) QTest::newRow("Default") /* Simulation info */ << "simulation-2-phase-32A" // simulationName << "Simulation (2 phase, charger 32A max, target 22:00 100%)" // simulationTitle << ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC) << ChargerPlugEvents() // pluggedInTime (UTC) << 18 // simulationHours << EnergyLogs::SampleRate1Min << 20.0 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList() // detailsStepList /* Houshold info */ << 32 // phase limit (A) << false // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime << "ChargingModeEcoWithTargetTime" // chargingMode << 40 // carBatteryLevel << 0 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 2 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << false // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 32 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) } }, { 100, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 300, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 61) } }, { 470, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 586, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 800, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 950, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 30), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 99) } }, { 960, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100) } } } ); if (run3PhaseSimulations) QTest::newRow("Default") /* Simulation info */ << "simulation-3-phase-16A" // simulationName << "Simulation (3 phase, charger 16A max, target 22:00 100%)" // simulationTitle << ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC) << ChargerPlugEvents() // pluggedInTime (UTC) << 18 // simulationHours << EnergyLogs::SampleRate1Min << 40.0 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList({200, 300, 400, 500}) // detailsStepList /* Houshold info */ << 32 // phase limit (A) << false // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime << "ChargingModeEcoWithTargetTime" // chargingMode << 40 // carBatteryLevel << 0 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 3 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << false // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 16 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) } }, { 200, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 11), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 57) } }, { 300, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 13), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 83) } }, { 400, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 11), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100) } }, { 700, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100) } } } ); if (run3PhaseSimulations) QTest::newRow("Default") /* Simulation info */ << "simulation-3-phase-32A" // simulationName << "Simulation (3 phase, charger 32A max, target 22:00 100%)" // simulationTitle << ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC) << ChargerPlugEvents() // pluggedInTime (UTC) << 18 // simulationHours << EnergyLogs::SampleRate1Min << 20.0 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList({ 480, 500, 600, 950, 960 }) // detailsStepList /* Houshold info */ << 32 // phase limit (A) << false // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime << "ChargingModeEcoWithTargetTime" // chargingMode << 40 // carBatteryLevel << 0 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 3 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << false // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 32 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 100, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) } }, { 200, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 300, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 400, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true) } }, { 480, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 500, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 82) } }, { 600, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false) } }, { 950, { SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 98) // Finish 10 min early } }, { 960, { // 22:00 SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100) } } } ); if (runPhaseSwitchingSimulations) QTest::newRow("Default") /* Simulation info */ << "simulation-phase-switching-16A" // simulationName << "Simulation (phase switching, charger 16A max, surplus only)" // simulationTitle << ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC) << ChargerPlugEvents() // pluggedInTime (UTC) << 18 // simulationHours << EnergyLogs::SampleRate1Min << 45.0 // productionScaling << 0 // detailsStepStart << 0 // detailsStepStop << DetailsStepList({80, 150, 310, 400, 470}) // detailsStepList /* Houshold info */ << 32 // phase limit (A) << false // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime << "ChargingModeEco" // chargingMode << 40 // carBatteryLevel << 0 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 3 // carPhaseCount /* Energy storage */ << false // energyStorageAvailable << 0 // energyStorageCapacity << 0.0 // energyStorageMaxChargingPower << 0.0 // energyStorageMaxDischargingPower << 50.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << true // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 16 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) } }, { 80, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) } }, { 150, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 10), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 50) } }, { 310, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 91) } }, { 400, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 13), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100) } }, { 470, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100) } }, } ); if (runPhaseSwitchingSimulations) QTest::newRow("Default") /* Simulation info */ << "simulation-energy-storage-phase-switching" // simulationName << "Simulation energy storage, phase switching" // simulationTitle << ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(0,0,0)) // simulationStart (UTC) << ChargerPlugEvents() // pluggedInTime (UTC) << 36 // simulationHours << EnergyLogs::SampleRate1Min << 45.0 // productionScaling << 0 // detailsStepStart << 10 // detailsStepStop << DetailsStepList({}) // detailsStepList /* Houshold info */ << 32 // phase limit (A) << false // spotMarketEnabled << ":/resources/dataset-1.json" // spotMarketResourceData << 0.5 // acquisitionTolerance << 0.9 // batteryLevelConsideration /* Charging Info */ << 100.0 // targetPercentage << EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime << "ChargingModeEco" // chargingMode << 40 // carBatteryLevel << 0 // dailySpotMarketPercentage /* Car settings */ << 50 // carCapacity << 6 // carMinChargingCurrent << 3 // carPhaseCount /* Energy storage */ << true // energyStorageAvailable << 12 // energyStorageCapacity << 5000.0 // energyStorageMaxChargingPower << 5000.0 // energyStorageMaxDischargingPower << 10.0 // energyStorageInitialBatteyLevel << true // chargerConnected << false // chargerPower << "ABC" // chargerPhases << true // canSwitchPhaseCount << 6 // chargerMaxChargingCurrent << 16 //chargerMaxChargingCurrentMaxValue << SimulationIterationTest ( { { 0, { SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6), SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false), SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) } } } ); } void Simulation::run() { QFETCH(QString, simulationName); QFETCH(QString, simulationTitle); QFETCH(QString, databaseName); QFETCH(QDateTime, simulationStart); QFETCH(ChargerPlugEvents, plugEvents); QFETCH(int, simulationHours); QFETCH(EnergyLogs::SampleRate, sampleRate); QFETCH(double, productionScaling); QFETCH(int, detailsStepStart); QFETCH(int, detailsStepStop); QFETCH(DetailsStepList, detailsStepList); QFETCH(int, phasePowerLimit); QFETCH(bool, spotMarketEnabled); QFETCH(QString, spotMarketResourceData); QFETCH(double, acquisitionTolerance); QFETCH(double, batteryLevelConsideration); QFETCH(double, targetPercentage); QFETCH(QDateTime, targetDateTime); QFETCH(QString, chargingMode); QFETCH(int, carBatteryLevel); QFETCH(int, dailySpotMarketPercentage); QFETCH(int, carCapacity); QFETCH(int, carMinChargingCurrent); QFETCH(int, carPhaseCount); QFETCH(bool, energyStorageAvailable); QFETCH(int, energyStorageCapacity); QFETCH(double, energyStorageMaxChargingPower); QFETCH(double, energyStorageMaxDischargingPower); QFETCH(double, energyStorageInitialBatteyLevel); QFETCH(bool, chargerConnected); QFETCH(bool, chargerPower); QFETCH(QString, chargerPhases); QFETCH(bool, canSwitchPhaseCount); QFETCH(int, chargerMaxChargingCurrent); QFETCH(int, chargerMaxChargingCurrentMaxValue); QFETCH(SimulationIterationTest, iterationTest); QStringList loggingDefaultList = { "*.debug=false", "Application.debug=true", "LogEngine.info=false", "Simulation.debug=true", "Experiences.debug=false", "NymeaEnergy.debug=false", "EnergyMocks.debug=false", "DBus.warning=false", }; QString loggingRulesDefault = loggingDefaultList.join("\n"); QStringList loggingDetailsList = { "*.debug=false", "Application.debug=true", "LogEngine.info=false", "Simulation.debug=true", "Experiences.debug=false", "NymeaEnergy.debug=true", "EnergyMocks.debug=false", "DBus.warning=false", }; QString loggingRulesDetails = loggingDetailsList.join("\n"); QStringList availableChargingModes; availableChargingModes << "ChargingModeNormal"; availableChargingModes << "ChargingModeEco"; availableChargingModes << "ChargingModeEcoWithTargetTime"; QVERIFY2(availableChargingModes.contains(chargingMode), "Unknown charging mode passed to the simulation. Please compair the list with the ChargingMode enum."); if (canSwitchPhaseCount) QVERIFY2(chargerPhases == "ABC", "If the charger supports phase count switching all 3 phases must be connected."); cleanupTestCase(); m_energyLogDbFilePath = databaseName; initTestCase(loggingRulesDefault); // Print simulation init details QLoggingCategory::setFilterRules(loggingRulesDefault); QVariant response; QVariantMap params; QNetworkReply *reply = nullptr; QSignalSpy packetSpy(m_mockTcpServer, &MockTcpServer::outgoingData);; Electricity::Phases chargerPhasesConverted = Electricity::convertPhasesFromString(chargerPhases); // Set phase power limit response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", phasePowerLimit}})); QVERIFY(response.toMap().value("params").toMap().value("energyError").toString() == "EnergyErrorNoError"); // Add mock spotmarket provider SpotMarketManager *spotMarketManager = m_experiencePlugin->spotMarketManager(); SpotMarketDataProviderMock *mockProvider = new SpotMarketDataProviderMock(nullptr, this); QVERIFY(mockProvider->prepareResourceData(spotMarketResourceData, simulationStart.toUTC())); QVERIFY(spotMarketManager->registerProvider(mockProvider)); QVERIFY(spotMarketManager->changeProvider(mockProvider->providerId())); // Enabke/disable spot market response = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", QVariantMap({ {"enabled", spotMarketEnabled }, {"providerId", mockProvider->providerId()} })); QCOMPARE(response.toMap().value("params").toMap().value("energyError").toString(), "EnergyErrorNoError"); QCOMPARE(m_experiencePlugin->spotMarketManager()->enabled(), spotMarketEnabled); // Set initial acquisition tolerance params.clear(); response.clear(); params.insert("acquisitionTolerance", acquisitionTolerance); response = injectAndWait("NymeaEnergy.SetAcquisitionTolerance", params); verifyEnergyError(response); // Set battery level consideration params.clear(); response.clear(); params.insert("batteryLevelConsideration", batteryLevelConsideration); response = injectAndWait("NymeaEnergy.SetBatteryLevelConsideration", params); verifyEnergyError(response); // Add mock meter QUuid meterThingId = addMeter(); QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId"); if (packetSpy.count() == 0) packetSpy.wait(); checkNotification(packetSpy, "Integrations.ThingAdded"); packetSpy.clear(); // Set it as root meter m_experiencePlugin->energyManager()->setRootMeter(meterThingId); // Make sure this is our root meter now response = injectAndWait("Energy.GetRootMeter"); QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId); packetSpy.clear(); // Add the charger QUuid evChargerId; if (canSwitchPhaseCount) { evChargerId = addChargerWithPhaseCountSwitching(chargerPhases, chargerMaxChargingCurrentMaxValue); } else { evChargerId = addCharger(chargerPhases, chargerMaxChargingCurrentMaxValue); } QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId"); if (packetSpy.count() == 0) packetSpy.wait(); checkNotification(packetSpy, "Integrations.ThingAdded"); // Add the car QUuid carThingId = addCar(); QVERIFY2(!carThingId.isNull(), "Did not receive valid ThingId"); if (packetSpy.count() == 0) packetSpy.wait(); checkNotification(packetSpy, "Integrations.ThingAdded"); // Add energy storage if available QUuid energyStorageThingId; if (energyStorageAvailable) { energyStorageThingId = addEnergyStorage(energyStorageCapacity, energyStorageMaxChargingPower, energyStorageMaxDischargingPower); QVERIFY2(!energyStorageThingId.isNull(), "Did not receive valid ThingId"); if (packetSpy.count() == 0) packetSpy.wait(); checkNotification(packetSpy, "Integrations.ThingAdded"); } // ============================================================================== // Set states of car and charger Thing *carThing = NymeaCore::instance()->thingManager()->findConfiguredThing(carThingId); QVERIFY2(carThing != nullptr, "Failed to find car thing"); carThing->setSettingValue(carThing->thingClass().settingsTypes().findByName("phaseCount").id(), carPhaseCount); carThing->setSettingValue(carThing->thingClass().settingsTypes().findByName("capacity").id(), carCapacity); carThing->setSettingValue(carThing->thingClass().settingsTypes().findByName("minChargingCurrent").id(), carMinChargingCurrent); carThing->setStateValue("batteryLevel", carBatteryLevel); Thing *chargerThing = NymeaCore::instance()->thingManager()->findConfiguredThing(evChargerId); QVERIFY2(chargerThing != nullptr, "Failed to find charger thing"); chargerThing->setStateValue("connected", chargerConnected); chargerThing->setStateValue("power", chargerPower); chargerThing->setStateValue("maxChargingCurrent", chargerMaxChargingCurrent); chargerThing->setStateValue("maxChargingCurrentMaxValue", chargerMaxChargingCurrentMaxValue); chargerThing->setStateValue("pluggedIn", true); // Initially always plugged in, the rest can be handeld using the plug events // This will update all internal states not set directly updateChargerMeter(chargerThing); Thing *meterThing = NymeaCore::instance()->thingManager()->findConfiguredThing(meterThingId); QVERIFY2(meterThing != nullptr, "Failed to find meter thing"); meterThing->setStateValue("connected", true); // Energy storage states Thing *energyStorageThing = nullptr; if (energyStorageAvailable) { energyStorageThing = NymeaCore::instance()->thingManager()->findConfiguredThing(energyStorageThingId); energyStorageThing->setStateValue("currentPower", 0); energyStorageThing->setStateValue("batteryLevel", energyStorageInitialBatteyLevel); energyStorageThing->setProperty("preciseBatteryLevel", energyStorageInitialBatteyLevel * 1.0); // For precise runtime calculations printStates(energyStorageThing); } // printStates(chargerThing); // printStates(carThing); // printStates(meterThing); // Set charging info with our charger and car, this should trigger the evaluation QVariantMap chargingInfoMap; chargingInfoMap.insert("evChargerId", evChargerId); chargingInfoMap.insert("assignedCarId", carThingId); chargingInfoMap.insert("chargingMode", chargingMode); chargingInfoMap.insert("endDateTime", targetDateTime.toMSecsSinceEpoch() / 1000); chargingInfoMap.insert("targetPercentage", targetPercentage); chargingInfoMap.insert("spotMarketChargingEnabled", spotMarketEnabled); chargingInfoMap.insert("dailySpotMarketPercentage", dailySpotMarketPercentage); response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", chargingInfoMap}})); QCOMPARE(response.toMap().value("status").toString(), QString("success")); QCOMPARE(response.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorNoError")); uint effectivePhaseCount; if (canSwitchPhaseCount) { effectivePhaseCount = chargerThing->stateValue("phaseCount").toUInt(); } else { effectivePhaseCount = qMin((uint)carPhaseCount, Electricity::getPhaseCount(chargerPhasesConverted)); } QString usedPhases; if (effectivePhaseCount >= 1) usedPhases.append("A"); if (effectivePhaseCount >= 2) usedPhases.append("B"); if (effectivePhaseCount >= 3) usedPhases.append("C"); chargerThing->setStateValue("usedPhases", usedPhases); // Note: we want to have the limit negative, so we see better where the limit is and why the chaging stopped double aquisitionToleranceLimit = -(effectivePhaseCount * carMinChargingCurrent * 230.0 * acquisitionTolerance); // Simulation output QDir simulationBaseDir = QDir(QDir::currentPath() + QDir::separator() + "simulations"); QDir workspaceDir = QDir(simulationBaseDir.absolutePath() + QDir::separator() + "workspace"); if (!workspaceDir.exists()) { //QVERIFY2(outputDir.removeRecursively(), "Failed to cleanup output dir"); QVERIFY2(workspaceDir.mkpath(workspaceDir.path()), "Failed to create results dir"); } QDir outputDir = QDir(workspaceDir.absolutePath() + QDir::separator() + simulationName); if (!outputDir.exists()) { // QVERIFY2(outputDir.removeRecursively(), "Failed to cleanup output dir"); QVERIFY2(outputDir.mkpath(outputDir.path()), "Failed to create output dir"); } QDir resultsDir = QDir(simulationBaseDir.absolutePath() + QDir::separator() + "results"); if (!resultsDir.exists()) { //QVERIFY2(outputDir.removeRecursively(), "Failed to cleanup output dir"); QVERIFY2(resultsDir.mkpath(resultsDir.path()), "Failed to create results dir"); } QFile gnuplotLogFile(outputDir.path() + QDir::separator() + "simulation.csv"); QVERIFY2(gnuplotLogFile.open(QIODevice::ReadWrite | QIODevice::Truncate), QString("Failed to open logfile for gnuplot" + gnuplotLogFile.fileName() + ": " + gnuplotLogFile.errorString()).toUtf8()); QTextStream gnuplotLogFileStream(&gnuplotLogFile); QFile gnuplotOriginalLogFile(outputDir.path() + QDir::separator() + "original.csv"); QVERIFY2(gnuplotOriginalLogFile.open(QIODevice::ReadWrite | QIODevice::Truncate), "Failed to open logfile for gnuplot"); QTextStream gnuplotOriginalLogFileStream(&gnuplotOriginalLogFile); // ============================================================================== // All set up, lets start simulating QDateTime simulationEnd = simulationStart.addSecs(simulationHours * 3600); PowerBalanceLogEntries powerBalanceLogs = m_experiencePlugin->energyManager()->logs()->powerBalanceLogs(sampleRate, simulationStart, simulationEnd); qCDebug(dcSimulation()) << "Simulation start" << simulationStart; qCDebug(dcSimulation()) << "Simulation end" << simulationEnd; qCDebug(dcSimulation()) << "Loaded" << powerBalanceLogs.count() << "log entries for that day"; // Simulation helpers int simulationProgress = 0; // Disable init details QLoggingCategory::setFilterRules(loggingRulesDefault); // Run the simulation for (int i = 0; i < powerBalanceLogs.count(); i++) { bool debugEnabled = (i >= detailsStepStart && i <= detailsStepStop) || detailsStepList.contains(i); const PowerBalanceLogEntry entry = powerBalanceLogs.at(i); QDateTime currentDateTime = entry.timestamp().toUTC(); // Calculate progress if (debugEnabled) { qCDebug(dcSimulation()) << "###############################################################################################################"; qCDebug(dcSimulation()) << "Step" << i << ":" << currentDateTime.toUTC().toString("yyyy.MM.dd hh:mm"); } effectivePhaseCount = chargerThing->stateValue("phaseCount").toUInt(); usedPhases = chargerThing->stateValue("usedPhases").toString(); // Update mocked spotmarket data provider mockProvider->setCurrentDataTime(currentDateTime.toUTC()); // Print simulation progress double simulationProgressPrecise = i * 100 / powerBalanceLogs.count(); if (simulationProgress != qRound(simulationProgressPrecise)) { simulationProgress = qRound(simulationProgressPrecise); if (simulationProgress % 10 == 0) { qCDebug(dcSimulation()) << simulationName << currentDateTime.toUTC().toString("hh:mm") << simulationProgress << "% (" << i << ")"; } } // Enable logs in the interesting simulation steps if (debugEnabled) { QLoggingCategory::setFilterRules(loggingRulesDetails); } else { QLoggingCategory::setFilterRules(loggingRulesDefault); } // Get the current situation befor running the simulation step double totalProduction = entry.production() * productionScaling; double totalProductionDifference = totalProduction - entry.production(); double scaledCurrentPower = entry.acquisition() + totalProductionDifference; //qCDebug(dcSimulation()) << "Scale production using" << productionScaling << entry.production() << "-->" << totalProduction << totalProductionDifference; //qCDebug(dcSimulation()) << "Scale aquisition using" << entry.acquisition() << "-->" << scaledCurrentPower; // Distribute the load before battery and charger on 3 phases double phaseLoad = scaledCurrentPower / 3; // -------------------- Charger // Handle plug events foreach(const ChargerPlugEvent &plugEvent, plugEvents) { if (plugEvent.dateTime.date() == currentDateTime.toUTC().date() && plugEvent.dateTime.time().hour() == currentDateTime.toUTC().time().hour() && plugEvent.dateTime.time().minute() == currentDateTime.toUTC().time().minute()) { // Plug event chargerThing->setStateValue("pluggedIn", plugEvent.pluggedIn); if (plugEvent.percentageUsed != 0) { double batteryLevel = carThing->stateValue("batteryLevel").toDouble(); batteryLevel -= plugEvent.percentageUsed; if (batteryLevel < 0) { batteryLevel = 0; } qCDebug(dcSimulation()) << "-->" << currentDateTime.toUTC().toString("hh:mm") << "New car battery level" << batteryLevel << "(old:" << carThing->stateValue("batteryLevel").toDouble() << ")"; carThing->setStateValue("batteryLevel", batteryLevel); } qCDebug(dcSimulation()) << "-->" << currentDateTime.toUTC().toString("hh:mm") << "Car has been" << (plugEvent.pluggedIn ? "plugged in" : "unplugged"); } } // Let the charger set all power, voltage etc.... updateChargerMeter(chargerThing); double chargerCurrentPower = chargerThing->stateValue("currentPower").toDouble(); double totalConsumption = chargerCurrentPower + entry.consumption(); // Totals before battery double totalCurrentPower = totalConsumption + totalProduction; // All producers and all consumers have been summed up, charge / discharge the battery and create a final total balance // -------------------- Energy storage double energyStorageCurrentPower = 0; uint energyStorageBatteryLevel = 0; // All consumers should have what they get, put the rest into or from the storage within limits if (energyStorageAvailable) { // Calculate the new battery level depending on the previouse step. double esCurrentPower = energyStorageThing->stateValue("currentPower").toDouble(); double esBatteryLevel = energyStorageThing->property("preciseBatteryLevel").toDouble(); double esCapacity = energyStorageThing->stateValue("capacity").toDouble(); // Let's caclulate the new percentage depending on the rate of the last minute... // We charged/discharged the last minute with energyStorageCurrentPower W double energyChargedDischargedkWh = esCurrentPower * 60 / 60 / 60 / 1000; double addedPercentage = energyChargedDischargedkWh * 100.0 / esCapacity; double newBatteryLevel = esBatteryLevel += addedPercentage; if (totalCurrentPower < 0 && newBatteryLevel < 100) { energyStorageCurrentPower = qMin(energyStorageMaxChargingPower, -totalCurrentPower); energyStorageBatteryLevel = newBatteryLevel; energyStorageThing->setProperty("preciseBatteryLevel", newBatteryLevel); setEnergyStorageStates(energyStorageBatteryLevel, energyStorageCurrentPower); } else if (totalCurrentPower > 0 && newBatteryLevel > 0) { energyStorageCurrentPower = - qMin(energyStorageMaxDischargingPower, totalCurrentPower); energyStorageBatteryLevel = newBatteryLevel; energyStorageThing->setProperty("preciseBatteryLevel", newBatteryLevel); setEnergyStorageStates(energyStorageBatteryLevel, energyStorageCurrentPower); } else { energyStorageBatteryLevel = newBatteryLevel; energyStorageThing->setProperty("preciseBatteryLevel", newBatteryLevel); setEnergyStorageStates(energyStorageBatteryLevel, energyStorageCurrentPower); } //qCDebug(dcSimulation()) << "Energy storage charged with" << energyStorageCurrentPower << "W" << addedPercentage << "% added to total" << energyStorageBatteryLevel << "%"; } // -------------------- Meter totalCurrentPower += energyStorageCurrentPower; phaseLoad += energyStorageCurrentPower / 3; // Add the charger power in the aproperiate phase QVariantMap phases = QVariantMap({ {"A", phaseLoad + chargerThing->stateValue("currentPowerPhaseA").toDouble()}, {"B", phaseLoad + chargerThing->stateValue("currentPowerPhaseB").toDouble()}, {"C", phaseLoad + chargerThing->stateValue("currentPowerPhaseC").toDouble()} }); reply = setMeterStates(phases, true); QSignalSpy setMeterStatesReplySpy(reply, &QNetworkReply::finished); if (setMeterStatesReplySpy.count() == 0) setMeterStatesReplySpy.wait(); QCOMPARE(reply->error(), QNetworkReply::NoError); // -------------------- Run charging manager update // Set charger information and pass them to the logic ThingPowerLogEntry chargerPowerEntry(currentDateTime.toUTC(), chargerThing->id(), chargerThing->stateValue("currentPower").toDouble(), 0, 0); m_experiencePlugin->smartChargingManager()->simulationCallUpdateManualSoCsWithMeter(sampleRate, chargerPowerEntry); // Update smart charging manager with the current root meter and charger situation m_experiencePlugin->smartChargingManager()->simulationCallUpdate(currentDateTime.toUTC()); // Fetch information after simulation iteration double carBatteryPercentage = carThing->stateValue("batteryLevel").toDouble(); int maxChargingCurrent = chargerThing->stateValue("maxChargingCurrent").toInt(); chargerPower = chargerThing->stateValue("power").toBool(); chargerCurrentPower = chargerThing->stateValue("currentPower").toDouble(); if (debugEnabled | iterationTest.contains(i)) { qCDebug(dcSimulation()) << "Step" << i; qCDebug(dcSimulation()) << "- Total power:" << totalCurrentPower << "Production:" << totalProduction << "Consumption:" << totalConsumption; if (energyStorageAvailable) { qCDebug(dcSimulation()) << "- Energy storage:" << energyStorageCurrentPower << energyStorageThing->property("preciseBatteryLevel").toDouble() << "%"; } qCDebug(dcSimulation()) << "- Meter phases: A:" << meterThing->stateValue("currentPowerPhaseA").toDouble() << "W | B:" << meterThing->stateValue("currentPowerPhaseB").toDouble() << "W | C:" << meterThing->stateValue("currentPowerPhaseC").toDouble() << "W"; qCDebug(dcSimulation()) << "- Charger:" << chargerCurrentPower << "[W] (" << maxChargingCurrent << "[A]" << (chargerCurrentPower ? "On)" : "Off )") << effectivePhaseCount << usedPhases; qCDebug(dcSimulation()) << "- Charger phases: A:" << chargerThing->stateValue("currentPowerPhaseA").toDouble() << "W | B:" << chargerThing->stateValue("currentPowerPhaseB").toDouble() << "W | C:" << chargerThing->stateValue("currentPowerPhaseC").toDouble() << "W"; qCDebug(dcSimulation()) << "- Car battery:" << carBatteryPercentage; qCDebug(dcSimulation()) << "--------------------------------"; // printStates(meterThing); // printStates(chargerThing); // printStates(carThing); } // Verify test points foreach(const SimulationTestPoint &testPoint, iterationTest.value(i)) { switch(testPoint.testType()) { case SimulationTestPoint::TestTypeCharging: QVERIFY2(chargerPower == testPoint.expectedValue().toBool(), qPrintable(QString("Simulation: %1 - %2 Step: %3 expected \"%4\" from the testpoint but is actually \"%5\"") .arg(simulationName) .arg(simulationTitle) .arg(i) .arg(testPoint.expectedValue().toBool() ? "true" : "false") .arg(chargerPower ? "true" : "false"))); break; case SimulationTestPoint::TestTypeMaxChargingCurrent: QVERIFY2(maxChargingCurrent == testPoint.expectedValue().toInt(), qPrintable(QString("Simulation: %1 - %2 Step: %3 expected \"%4\" from the testpoint but is actually \"%5\"") .arg(simulationName) .arg(simulationTitle) .arg(i) .arg(testPoint.expectedValue().toInt()) .arg(maxChargingCurrent))); break; case SimulationTestPoint::TestTypeStateOfCharge: QVERIFY2(qFuzzyCompare(carBatteryPercentage, testPoint.expectedValue().toDouble()), qPrintable(QString("Simulation: %1 - %2 Step: %3 expected \"%4\" from the testpoint but is actually \"%5\"") .arg(simulationName) .arg(simulationTitle) .arg(i) .arg(testPoint.expectedValue().toDouble()) .arg(carBatteryPercentage))); break; } } // -------------------- Data logging // Log the simulation data gnuplotLogFileStream << currentDateTime.toMSecsSinceEpoch() / 1000 << ", " << // 1 totalCurrentPower << ", " << // 2 totalProduction << ", " << // 3 totalConsumption << ", " << // 4 chargerCurrentPower << ", " << // 5 maxChargingCurrent << ", " << // 6 (chargerPower ? "1" : "0") << ", " << // 7 chargerThing->state("maxChargingCurrent").minValue().toDouble() * 230 * effectivePhaseCount << ", " << // 8 chargerThing->state("maxChargingCurrent").maxValue().toDouble() * 230 * effectivePhaseCount << ", " << // 9 phasePowerLimit * 230 * effectivePhaseCount << ", " << // 10 carBatteryPercentage << ", " << // 11 i << ", " << // 12 aquisitionToleranceLimit << ", " << // 13 (chargerThing->stateValue("pluggedIn").toBool() ? 10 : 0 ) << ", "; // 14 if (spotMarketEnabled) { const ScoreEntries weightedEntries = spotMarketManager->weightedScoreEntries(currentDateTime.date()); const ScoreEntry currentScore = weightedEntries.getScoreEntry(currentDateTime.toUTC()); QVERIFY(!currentScore.isNull()); gnuplotLogFileStream << currentScore.weighting() * 100 << ", "; // 15 gnuplotLogFileStream << currentScore.value() / 10.0 << ", "; // 16 Price } else { gnuplotLogFileStream << 0 << ", "; // 15 gnuplotLogFileStream << 0 << ", "; // 16 } if (energyStorageAvailable) { gnuplotLogFileStream << energyStorageCurrentPower << ", "; // 17 gnuplotLogFileStream << energyStorageBatteryLevel << ", "; // 18 } else { gnuplotLogFileStream << 0 << ", "; // 17 gnuplotLogFileStream << 0 << ", "; // 18 } gnuplotLogFileStream << "\n"; // Log Unchanged for raw data analysis gnuplotOriginalLogFileStream << currentDateTime.toMSecsSinceEpoch() / 1000 << ", " << // 1 scaledCurrentPower << ", " << // 2 totalProduction << ", " << // 3 entry.consumption() << ", " << // 4 phasePowerLimit * 230 * effectivePhaseCount << ", " << // 5 i << ", " << // 6 "\n"; } gnuplotLogFile.close(); gnuplotOriginalLogFile.close(); // Draw original data QStringList scriptLines; QStringList plotLines; // Plot with: 1h = 200 px int height = 800; int width = simulationHours * 200; QString originalImageName = simulationName + "-00-original.png"; QString simulationImageName = simulationName + "-01.png"; scriptLines.append("set term png size " + QString::number(width) + "," + QString::number(height)); scriptLines.append("set output '" + originalImageName + "'"); scriptLines.append("set datafile separator ','"); scriptLines.append(plotOriginalData(powerBalanceLogs.count())); if (spotMarketEnabled) { scriptLines.append("set term png size " + QString::number(width) + "," + QString::number(height * 2)); scriptLines.append("set output '" + simulationImageName + "'"); scriptLines.append("set datafile separator ','"); scriptLines.append("set multiplot layout 2,1"); scriptLines.append("set size 1,0.8"); scriptLines.append("set origin 0,0.2"); scriptLines.append(plotSimulation(simulationTitle, powerBalanceLogs.count())); scriptLines.append("set size 1,0.2"); scriptLines.append("set origin 0,0"); scriptLines.append(plotSpotMarketData(powerBalanceLogs.count())); scriptLines.append("unset multiplot"); } else { scriptLines.append("set term png size " + QString::number(width) + "," + QString::number(height)); scriptLines.append("set output '" + simulationImageName + "'"); scriptLines.append("set datafile separator ','"); scriptLines.append(plotSimulation(simulationTitle, powerBalanceLogs.count())); } // Write the gnuplot script QFile gnuplotScript(outputDir.path() + QDir::separator() + "script.gnuplot"); QVERIFY2(gnuplotScript.open(QIODevice::ReadWrite | QIODevice::Truncate), QString("Failed to open script file for gnuplot" + gnuplotScript.fileName() + ": " + gnuplotScript.errorString()).toUtf8()); QTextStream scriptStream(&gnuplotScript); foreach (const QString &line, scriptLines) scriptStream << line << "\n"; gnuplotScript.close(); // Write the executable gnuplot script scriptLines.clear(); scriptLines.append("set terminal wxt 1 persist"); scriptLines.append("set datafile separator ','"); if (spotMarketEnabled) { scriptLines.append("set multiplot layout 2,1"); scriptLines.append("set size 1,0.8"); scriptLines.append("set origin 0,0.2"); scriptLines.append(plotSimulation(simulationTitle, powerBalanceLogs.count())); scriptLines.append("set size 1,0.2"); scriptLines.append("set origin 0,0"); scriptLines.append(plotSpotMarketData(powerBalanceLogs.count())); scriptLines.append("unset multiplot"); } else { scriptLines.append(plotSimulation(simulationTitle, powerBalanceLogs.count())); } QString executableScriptName = simulationName + ".gnuplot"; QFile executableGnuplotScript(outputDir.path() + QDir::separator() + executableScriptName); QVERIFY2(executableGnuplotScript.open(QIODevice::ReadWrite | QIODevice::Truncate), QString("Failed to open logfile for gnuplot" + executableGnuplotScript.fileName() + ": " + executableGnuplotScript.errorString()).toUtf8()); QTextStream executableScriptStream(&executableGnuplotScript); foreach (const QString &line, scriptLines) executableScriptStream << line << "\n"; executableGnuplotScript.close(); QProcess gnuplotProcess; //gnuplotProcess.setEnvironment(QProcessEnvironment::systemEnvironment().toStringList()); gnuplotProcess.setProcessChannelMode(QProcess::MergedChannels); gnuplotProcess.setWorkingDirectory(outputDir.path()); gnuplotProcess.start("gnuplot", { "-c", "script.gnuplot"}); gnuplotProcess.waitForFinished(); qCDebug(dcSimulation()) << "gnuplot finished" << gnuplotProcess.arguments() << gnuplotProcess.workingDirectory() << gnuplotProcess.exitCode() << gnuplotProcess.exitStatus(); if (gnuplotProcess.exitCode() != 0) { qCDebug(dcSimulation()) << "error plotting data:\n" << qUtf8Printable(gnuplotProcess.readAll()); QVERIFY2(false, "plot process finished with error"); } // Copy resulting images to the simulations QFile::copy(outputDir.path() + QDir::separator() + originalImageName, resultsDir.path() + QDir::separator() + originalImageName); QFile::copy(outputDir.path() + QDir::separator() + simulationImageName, resultsDir.path() + QDir::separator() + simulationImageName); } void Simulation::printStates(Thing *thing) { qCDebug(dcSimulation()) << "Thing states for" << thing->name(); foreach (const StateType &stateType, thing->thingClass().stateTypes()) { qCDebug(dcSimulation()) << "-->" << stateType.name() << thing->stateValue(stateType.id()); } } void Simulation::updateChargerMeter(Thing *thing) { Action updateChargerAction(thing->thingClass().actionTypes().findByName("update").id(), thing->id()); NymeaCore::instance()->thingManager()->executeAction(updateChargerAction); } QStringList Simulation::plotOriginalData(int powerBalanceCount) { QStringList scriptLines; scriptLines.append("set title 'Original energy data'"); scriptLines.append("set grid"); scriptLines.append("set timefmt '%s'"); scriptLines.append("set xdata time"); scriptLines.append("set xtics 3600"); scriptLines.append("set format x '%H:%M'"); scriptLines.append("set xlabel 'Time'"); scriptLines.append("set ylabel '[W]'"); scriptLines.append("set style fill transparent solid 0.3"); scriptLines.append("set x2tics 100"); scriptLines.append("set xtics nomirror"); scriptLines.append("set x2label 'iterations'"); scriptLines.append("set x2range [0:" + QString::number(powerBalanceCount) + "]"); QStringList plotLines; plotLines.append("'original.csv' using 1:3 with boxes lt rgb '#7F75C23A' title 'Production'"); plotLines.append("'original.csv' using 1:4 with boxes lt rgb '#7F3590F3' title 'Consumption'"); plotLines.append("'original.csv' using 1:5 with line lt rgb 'orange' title 'House limit'"); plotLines.append("'original.csv' using 1:2 with line lt rgb 'red' title 'Meter'"); scriptLines.append("plot \\\n" + plotLines.join(", \\\n")); scriptLines.append(""); return scriptLines; } QStringList Simulation::plotSimulation(const QString &title, int powerBalanceCount) { QStringList scriptLines; scriptLines.append("set title '" + title + "'"); scriptLines.append("set grid"); scriptLines.append("set timefmt '%s'"); scriptLines.append("set xdata time"); scriptLines.append("set format x '%H:%M'"); scriptLines.append("set xtics 3600"); scriptLines.append("set xlabel 'time'"); scriptLines.append("set ylabel '[W]'"); scriptLines.append("set x2tics 100"); scriptLines.append("set xtics nomirror"); scriptLines.append("set x2label 'iterations'"); scriptLines.append("set x2range [0:" + QString::number(powerBalanceCount) + "]"); scriptLines.append("set style fill transparent solid 0.3"); scriptLines.append("set y2tics 10"); scriptLines.append("set ytics nomirror"); scriptLines.append("set y2label '[\%]'"); scriptLines.append("set y2range [0:100]"); QStringList plotLines; plotLines.append("'simulation.csv' using 1:9 title 'Charger range' w filledcurves x1 lc rgb '#fff0f0f0'"); plotLines.append("'simulation.csv' using 1:8 notitle w filledcurves x1 lc rgb '#ffffffff'"); plotLines.append("'simulation.csv' using 1:3 with boxes lt rgb '#0A75C23A' title 'Production'"); plotLines.append("'simulation.csv' using 1:4 with boxes lt rgb '#7F3590F3' title 'Consumption'"); plotLines.append("'simulation.csv' using 1:17 with boxes lt rgb '#7FA020F0' title 'Energy storage'"); plotLines.append("'simulation.csv' using 1:5 with boxes lt rgb '#7FF3DE8A' title 'Charger'"); // plotLines.append("'simulation.csv' using 1:9 with line lt rgb '#EABC01' title 'Charger max'"); // plotLines.append("'simulation.csv' using 1:8 with line lt rgb '#F6CAAF' title 'Charger min'"); plotLines.append("'simulation.csv' using 1:13 with line lt rgb 'green' title 'Acquisition Limit'"); plotLines.append("'simulation.csv' using 1:11 with line lt rgb 'black' axes x1y2 title 'Battery [\%]'"); plotLines.append("'simulation.csv' using 1:18 with line lt rgb 'purple ' axes x1y2 title 'Energy storage [\%]'"); plotLines.append("'simulation.csv' using 1:14 with line lt rgb 'purple' axes x1y2 title 'Car plugged in into charger'"); plotLines.append("'simulation.csv' using 1:10 with line lt rgb 'orange' title 'House limit'"); plotLines.append("'simulation.csv' using 1:2 with line lt rgb 'red' title 'Meter'"); scriptLines.append("plot \\\n" + plotLines.join(", \\\n")); scriptLines.append(""); return scriptLines; } QStringList Simulation::plotSpotMarketData(int powerBalanceCount) { QStringList scriptLines; scriptLines.append("set title 'Spot maket data'"); scriptLines.append("set grid"); scriptLines.append("set timefmt '%s'"); scriptLines.append("set xdata time"); scriptLines.append("set format x '%H:%M'"); scriptLines.append("set xtics 3600"); scriptLines.append("set xlabel 'Time'"); scriptLines.append("set ylabel 'Price [Cent/kWh]'"); scriptLines.append("set x2tics 100"); scriptLines.append("set xtics nomirror"); scriptLines.append("set x2label 'iterations'"); scriptLines.append("set x2range [0:" + QString::number(powerBalanceCount) + "]"); scriptLines.append("set y2tics"); scriptLines.append("set ytics nomirror"); scriptLines.append("set y2label '[\%]'"); scriptLines.append("set y2range [0:100]"); QStringList plotLines; plotLines.append("'simulation.csv' using 1:15 with boxes fs solid lt rgb '#fff0f0f0' axes x1y2 title 'Spotmarket scoring [%]'"); plotLines.append("'simulation.csv' using 1:16 with line lt rgb 'black' axes x1y1 title 'Price [Cent/kWh]'"); scriptLines.append("plot \\\n" + plotLines.join(", \\\n")); scriptLines.append(""); return scriptLines; } QTEST_MAIN(Simulation)