Patrick Schurig dfdd9884d0 [3e+] ECS non-cascadé : applyRelayStage off-before-on + tests 1 et 3 relais
applyRelayStage faisait déjà du set-cible complet (delta correct, gère le non-cascadé) :
durcissement off-before-on (anti sur-puissance transitoire quand monter de palier éteint
des relais, ex. 3 résistances 500/1000/2000 : 1500→2000 commute 3 relais) + intention
documentée (comme SG-Ready).

testEcsRelayTopologies : ECS simple 1 relais (on/off) + ECS 3 relais non-cascadé
(transition 1500→2000 → set final r2000 SEUL, r500/r1000 coupés). Couvre les 2 topologies
du test terrain vendredi. Suite simulation 20/20, plugin prod 0/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:09:24 +02:00

2585 lines
118 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "simulation.h"
#include <hardware/electricity.h>
#include <servers/mocktcpserver.h>
#include <experiences/experiencemanager.h>
#include <experiences/experienceplugin.h>
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 <QHash>
#include <QtMath>
#include <QtGlobal>
#include <QProcess>
#include <QDateTime>
#include <QSignalSpy>
#include <QProcessEnvironment>
#include <QCoreApplication>
#include <nymeacore.h>
#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<EnergyArbitrator *>(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<int>({0, 1200, 2400}),
QList<QList<QString>>({ {}, {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<EnergyArbitrator *>(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<int>({0, 2400}),
QList<QList<QString>>({ {}, {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<int>({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<int, double> 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<EnergyArbitrator *>(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<int, QList<QString>>({ {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<EnergyArbitrator *>(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<int>({0, 2400}),
QList<QList<QString>>({ {}, {ke.toString()} }),
0, 0, ecsPrio, arb);
arb->registerEcsAdapter(ecs);
SgReadyAdapter *pacWf = new SgReadyAdapter(
tm2, "pac-wf", "PAC waterfall",
QHash<int, QList<QString>>({ {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::testEcsRelayTopologies()
{
#ifndef ETM_ARBITRATOR
QSKIP("testEcsRelayTopologies nécessite ETM_ARBITRATOR.");
#else
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
// Setup commun : arbitre frais + root meter. Retourne meter + thingManager via réf.
auto freshSetup = [&](EnergyArbitrator *&arb, ThingManager *&tm, Thing *&meter) {
cleanupTestCase();
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
initTestCase();
arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
QVERIFY(arb);
tm = NymeaCore::instance()->thingManager();
QUuid mId = addMeter();
m_experiencePlugin->energyManager()->setRootMeter(mId);
meter = tm->findConfiguredThing(mId);
QVERIFY(meter);
meter->setStateValue("connected", true);
};
// ===================== Topologie 1 : ECS simple (1 relais, [0, 2000]) =====================
{
EnergyArbitrator *arb; ThingManager *tm; Thing *meter;
freshSetup(arb, tm, meter);
QUuid r = addPowerSwitch(2000, 26661);
Thing *relay = tm->findConfiguredThing(r);
QVERIFY(relay);
EcsRelayAdapter *ecs = new EcsRelayAdapter(
tm, "ecs-1relay", "ECS simple",
QList<int>({0, 2000}),
QList<QList<QString>>({ {}, {r.toString()} }),
0, 0, 1, arb);
arb->registerEcsAdapter(ecs);
meter->setStateValue("currentPower", -2500); // surplus 2500 → palier 1
arb->simulationCallUpdate(t0); QCoreApplication::processEvents();
QCOMPARE(ecs->currentStage(), 1);
QCOMPARE(relay->stateValue("power").toBool(), true);
meter->setStateValue("currentPower", 1000); // import 1000 → palier 0 (off)
arb->simulationCallUpdate(t0.addSecs(1)); QCoreApplication::processEvents();
QCOMPARE(ecs->currentStage(), 0);
QCOMPARE(relay->stateValue("power").toBool(), false);
}
// ============= Topologie 2 : ECS 3 relais 500/1000/2000 W (mapping NON-CASCADÉ) =============
{
EnergyArbitrator *arb; ThingManager *tm; Thing *meter;
freshSetup(arb, tm, meter);
QUuid r500 = addPowerSwitch(500, 26661);
QUuid r1000 = addPowerSwitch(1000, 26662);
QUuid r2000 = addPowerSwitch(2000, 26663);
Thing *t500 = tm->findConfiguredThing(r500);
Thing *t1000 = tm->findConfiguredThing(r1000);
Thing *t2000 = tm->findConfiguredThing(r2000);
QVERIFY(t500 && t1000 && t2000);
// 8 niveaux binaires ; encodage bit0=r500, bit1=r1000, bit2=r2000.
EcsRelayAdapter *ecs = new EcsRelayAdapter(
tm, "ecs-3relay", "ECS 3 relais",
QList<int>({0, 500, 1000, 1500, 2000, 2500, 3000, 3500}),
QList<QList<QString>>({
{}, // 0
{r500.toString()}, // 500
{r1000.toString()}, // 1000
{r500.toString(), r1000.toString()}, // 1500
{r2000.toString()}, // 2000
{r500.toString(), r2000.toString()}, // 2500
{r1000.toString(), r2000.toString()}, // 3000
{r500.toString(), r1000.toString(), r2000.toString()}// 3500
}),
0, 0, 1, arb);
arb->registerEcsAdapter(ecs);
// Surplus 1700 → palier 1500 = [r500, r1000] (r2000 éteint).
meter->setStateValue("currentPower", -1700);
arb->simulationCallUpdate(t0); QCoreApplication::processEvents();
QCOMPARE(ecs->currentStage(), 3);
QCOMPARE(t500->stateValue("power").toBool(), true);
QCOMPARE(t1000->stateValue("power").toBool(), true);
QCOMPARE(t2000->stateValue("power").toBool(), false);
// Transition NON-CASCADÉE 1500 → 2000 : à stage 3 l'ECS mesure 1500 W (r500+r1000),
// export net 700 → budget 700+1500=2200 → palier 2000 = [r2000] SEUL.
// Vérifie le set FINAL : r500 OFF, r1000 OFF, r2000 ON (commutation de 3 relais).
meter->setStateValue("currentPower", -700);
arb->simulationCallUpdate(t0.addSecs(1)); QCoreApplication::processEvents();
QCOMPARE(ecs->currentStage(), 4);
QCOMPARE(t500->stateValue("power").toBool(), false);
QCOMPARE(t1000->stateValue("power").toBool(), false);
QCOMPARE(t2000->stateValue("power").toBool(), true);
}
#endif
}
void Simulation::run_data()
{
// Simulation infos
QTest::addColumn<QString>("simulationName");
QTest::addColumn<QString>("simulationTitle");
QTest::addColumn<QString>("databaseName");
QTest::addColumn<QDateTime>("simulationStart");
QTest::addColumn<ChargerPlugEvents>("plugEvents");
QTest::addColumn<int>("simulationHours");
QTest::addColumn<EnergyLogs::SampleRate>("sampleRate");
QTest::addColumn<double>("productionScaling");
QTest::addColumn<int>("detailsStepStart");
QTest::addColumn<int>("detailsStepStop");
QTest::addColumn<DetailsStepList>("detailsStepList");
// Houshold info
QTest::addColumn<int>("phasePowerLimit");
QTest::addColumn<bool>("spotMarketEnabled");
QTest::addColumn<QString>("spotMarketResourceData");
QTest::addColumn<double>("acquisitionTolerance");
QTest::addColumn<double>("batteryLevelConsideration");
// ChargingInfo
QTest::addColumn<double>("targetPercentage");
QTest::addColumn<QDateTime>("targetDateTime");
QTest::addColumn<QString>("chargingMode");
QTest::addColumn<int>("carBatteryLevel");
QTest::addColumn<int>("dailySpotMarketPercentage");
// Car information and states
QTest::addColumn<int>("carCapacity");
QTest::addColumn<int>("carMinChargingCurrent");
QTest::addColumn<int>("carPhaseCount");
// Energy storage information and states
QTest::addColumn<bool>("energyStorageAvailable");
QTest::addColumn<int>("energyStorageCapacity");
QTest::addColumn<double>("energyStorageMaxChargingPower");
QTest::addColumn<double>("energyStorageMaxDischargingPower");
QTest::addColumn<double>("energyStorageInitialBatteyLevel");
// Charger initial states
QTest::addColumn<bool>("chargerConnected");
QTest::addColumn<bool>("chargerPower");
QTest::addColumn<QString>("chargerPhases");
QTest::addColumn<bool>("canSwitchPhaseCount");
QTest::addColumn<int>("chargerMaxChargingCurrent");
QTest::addColumn<int>("chargerMaxChargingCurrentMaxValue");
QTest::addColumn<SimulationIterationTest>("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)