// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-energy-plugin-nymea.
*
* nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-energy-plugin-nymea.s distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-energy-plugin-nymea. If not, see .
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "simulation.h"
#include
#include
#include
#include
using namespace nymeaserver;
#include "../../../energyplugin/smartchargingmanager.h"
#include "../../mocks/spotmarketprovider/spotmarketdataprovidermock.h"
#ifdef ETM_ARBITRATOR
#include "../../../energyplugin/etm/energyarbitrator.h"
#include "../../../energyplugin/etm/adapters/sgreadyadapter.h"
// [T3] ecsrelayadapter.h retiré : l'ECS est piloté en watts via EtmVariableLoadAdapter
// (interface etmvariableload, kind Setpoint). La combinatoire relais vit dans le thing.
#include "../../../energyplugin/etm/adapters/etmvariableloadadapter.h"
#include "../../../energyplugin/etm/types/loadconfig.h"
#include "../../../energyplugin/etm/config/loadconfigstore.h"
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "simulationtestpoint.h"
void Simulation::testEcsSurplusPV()
{
#ifndef ETM_ARBITRATOR
QSKIP("testEcsSurplusPV nécessite ETM_ARBITRATOR.");
#else
// [T3] ECS piloté en WATTS via l'interface etmvariableload (contrat rév. 2 §3/§5) : l'arbitre
// arrondit le surplus au powerLevels déclaré (fixed) ou clampe à maxPowerW (dynamic), écrit
// powerSetpoint, et recrédite currentPowerW (début de cycle) pour l'anti-clignotement. La
// combinatoire matérielle (relais/triac) vit dans le thing — invisible côté moteur.
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
// =================== FIXED : paliers déclarés [0, 1200, 2400] ===================
cleanupTestCase();
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
initTestCase();
EnergyArbitrator *arbitrator = dynamic_cast(m_experiencePlugin->smartChargingManager());
QVERIFY2(arbitrator, "smartChargingManager n'est pas un EnergyArbitrator (ETM_ARBITRATOR requis)");
ThingManager *thingManager = NymeaCore::instance()->thingManager();
QUuid meterThingId = addMeter();
QVERIFY(!meterThingId.isNull());
m_experiencePlugin->energyManager()->setRootMeter(meterThingId);
Thing *meterThing = thingManager->findConfiguredThing(meterThingId);
QVERIFY(meterThing);
meterThing->setStateValue("connected", true);
QUuid ecsId = addEtmVariableLoad(27001);
QVERIFY(!ecsId.isNull());
Thing *ecsThing = thingManager->findConfiguredThing(ecsId);
QVERIFY(ecsThing);
EtmVariableLoadAdapter *ecs = new EtmVariableLoadAdapter(
thingManager, ecsId.toString(), "ECS variable",
QList({0, 1200, 2400}), 2400, 1, LoadNeeds(), arbitrator);
arbitrator->registerEtmVariableLoadAdapter(ecs);
auto setMeterW = [&](double signedW){ meterThing->setStateValue("currentPower", signedW); }; // <0 = export
auto setLoadW = [&](double w){ ecsThing->setStateValue("currentPowerW", w); }; // mesure réelle simulée
auto cycle = [&](const QDateTime &now){ arbitrator->simulationCallUpdate(now); QCoreApplication::processEvents(); };
// --- Cascade montante (la charge ne tire encore rien : currentPowerW = 0) ---
setLoadW(0);
setMeterW(-1000); cycle(t0); // budget 1000 < 1200 → 0 W
QCOMPARE(qRound(ecs->currentSetpointW()), 0);
setMeterW(-1500); cycle(t0); // budget 1500 → palier 1200
QCOMPARE(qRound(ecs->currentSetpointW()), 1200);
QCOMPARE(qRound(ecsThing->stateValue("powerSetpoint").toDouble()), 1200); // round-trip interface
setMeterW(-2500); cycle(t0); // budget 2500 → palier 2400
QCOMPARE(qRound(ecs->currentSetpointW()), 2400);
QCOMPARE(qRound(ecsThing->stateValue("powerSetpoint").toDouble()), 2400);
// --- Anti-clignotement (recrédit) : la charge tire 2400, PV 2500 → export net 100.
// budget = 100 + 2400 (recrédit currentPowerW début de cycle) = 2500 → RESTE 2400.
// Sans recrédit : 100 → 0 → oscillation. C'est précisément le test du recrédit. ---
setLoadW(2400);
setMeterW(-100); cycle(t0);
QCOMPARE(qRound(ecs->currentSetpointW()), 2400);
// --- Délestage sur import : charge tire 2400, import 600 → budget = -600 + 2400 = 1800 → 1200. ---
setMeterW(600); cycle(t0);
QCOMPARE(qRound(ecs->currentSetpointW()), 1200);
QCOMPARE(qRound(ecsThing->stateValue("powerSetpoint").toDouble()), 1200);
// =================== DYNAMIC : modulation continue, maxPowerW 3000, sans powerLevels ===================
cleanupTestCase();
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
initTestCase();
EnergyArbitrator *arb2 = dynamic_cast(m_experiencePlugin->smartChargingManager());
QVERIFY(arb2);
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 rId = addEtmVariableLoad(27002);
Thing *rThing = tm2->findConfiguredThing(rId);
QVERIFY(rThing);
rThing->setStateValue("currentPowerW", 0);
EtmVariableLoadAdapter *router = new EtmVariableLoadAdapter(
tm2, rId.toString(), "Routeur PV", QList(), 3000, 1, LoadNeeds(), arb2); // powerLevels vide → dynamic
arb2->registerEtmVariableLoadAdapter(router);
m2->setStateValue("currentPower", -2000); arb2->simulationCallUpdate(t0); QCoreApplication::processEvents();
QCOMPARE(qRound(router->currentSetpointW()), 2000); // clamp(2000, 0, 3000)
m2->setStateValue("currentPower", -4000); arb2->simulationCallUpdate(t0); QCoreApplication::processEvents();
QCOMPARE(qRound(router->currentSetpointW()), 3000); // plafonné à maxPowerW
m2->setStateValue("currentPower", 500); arb2->simulationCallUpdate(t0); QCoreApplication::processEvents();
QCOMPARE(qRound(router->currentSetpointW()), 0); // import → 0
#endif
}
void Simulation::testMeterSilentFallback()
{
#ifndef ETM_ARBITRATOR
QSKIP("testMeterSilentFallback nécessite ETM_ARBITRATOR.");
#else
// [T4] Repli L2 charge pilotée recâblé : compteur muet >90 s → setPowerSetpoint(0) force=true
// sur l'etmvariableload, planif suspendue (reste 0 sur N cycles), reprise au retour compteur.
cleanupTestCase();
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
initTestCase();
EnergyArbitrator *arbitrator = dynamic_cast(m_experiencePlugin->smartChargingManager());
QVERIFY2(arbitrator, "smartChargingManager n'est pas un EnergyArbitrator (ETM_ARBITRATOR requis)");
ThingManager *thingManager = NymeaCore::instance()->thingManager();
QUuid meterThingId = addMeter();
QVERIFY(!meterThingId.isNull());
m_experiencePlugin->energyManager()->setRootMeter(meterThingId);
Thing *meterThing = thingManager->findConfiguredThing(meterThingId);
QVERIFY(meterThing);
meterThing->setStateValue("connected", true);
QUuid ecsId = addEtmVariableLoad(27010);
QVERIFY(!ecsId.isNull());
Thing *ecsThing = thingManager->findConfiguredThing(ecsId);
QVERIFY(ecsThing);
EtmVariableLoadAdapter *ecs = new EtmVariableLoadAdapter(
thingManager, ecsId.toString(), "ECS repli",
QList({0, 2400}), 2400, 1, LoadNeeds(), arbitrator);
arbitrator->registerEtmVariableLoadAdapter(ecs);
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
auto setMeterW = [&](double signedW){ meterThing->setStateValue("currentPower", signedW); };
auto setLoadW = [&](double w){ ecsThing->setStateValue("currentPowerW", w); };
auto cycle = [&](const QDateTime &now){ arbitrator->simulationCallUpdate(now); QCoreApplication::processEvents(); };
// ECS servi sur surplus, compteur frais à T0.
arbitrator->recordMeterUpdate(t0);
setLoadW(0);
setMeterW(-2500); cycle(t0); // budget 2500 → 2400
QCOMPARE(qRound(ecs->currentSetpointW()), 2400);
QVERIFY(!arbitrator->degradedMode());
// Compteur muet > 90 s → mode dégradé : setPowerSetpoint(0) force=true.
setLoadW(2400); // la charge tirait
arbitrator->evaluateMeterFreshness(t0.addSecs(91));
QCoreApplication::processEvents();
QVERIFY(arbitrator->degradedMode());
QCOMPARE(qRound(ecs->currentSetpointW()), 0);
QCOMPARE(qRound(ecsThing->stateValue("powerSetpoint").toDouble()), 0);
// STABILITÉ : muet, plusieurs cycles, faux surplus piège → l'ECS RESTE à 0 (planif suspendue).
setMeterW(-3000);
foreach (int dt, QList({92, 120, 200, 280})) {
cycle(t0.addSecs(dt));
QVERIFY2(arbitrator->degradedMode(), "degradedMode doit rester actif pendant le silence");
QCOMPARE(qRound(ecs->currentSetpointW()), 0);
}
// REPRISE : compteur reparle → degradedMode retombe → recalcul (pas de restauration d'ancienne consigne).
arbitrator->recordMeterUpdate(t0.addSecs(300));
QVERIFY(!arbitrator->degradedMode());
setLoadW(0);
setMeterW(-1000); cycle(t0.addSecs(301)); // 1000 < 2400 → reste 0
QCOMPARE(qRound(ecs->currentSetpointW()), 0);
setMeterW(-2500); cycle(t0.addSecs(302)); // surplus suffisant → 2400
QCOMPARE(qRound(ecs->currentSetpointW()), 2400);
#endif
}
void Simulation::testLoadConfigPersistence()
{
#ifndef ETM_ARBITRATOR
QSKIP("testLoadConfigPersistence nécessite ETM_ARBITRATOR.");
#else
// Persistance §4 : SetConfigs → fichier écrit → relecture par un store neuf → round-trip
// mot pour mot (id/label/mode/powerLevels/maxPowerW/priority/enabled/needs).
const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-persist.json";
QFile::remove(cfgPath);
qputenv("NYMEA_ENERGY_LOAD_CONFIG", cfgPath.toUtf8());
const QVariantMap entry{
{"id", "{11111111-2222-3333-4444-555555555555}"},
{"label", "Chauffe-eau"},
{"adapter", "etmvariableload"},
{"mode", "fixed"},
{"powerLevels", QVariantList() << 0 << 600 << 1200},
{"maxPowerW", 1200},
{"priority", 2},
{"enabled", true},
{"needs", QVariantMap{{"dailyDeadline", "06:00"}, {"minEnergyWhPerDay", 4000}}}
};
LoadConfigs cfgs;
cfgs.append(LoadConfig::fromMap(entry));
{
LoadConfigStore store1;
QString err;
QVERIFY2(store1.setConfigs(cfgs, &err), err.toUtf8());
}
QVERIFY(QFileInfo::exists(cfgPath));
LoadConfigStore store2; // relit le fichier
QCOMPARE(store2.configs().count(), 1);
const LoadConfig r = store2.configs().first();
QCOMPARE(r.label(), QString("Chauffe-eau"));
QCOMPARE(r.mode(), QString("fixed"));
QCOMPARE(r.powerLevelsInt(), QList({0, 600, 1200}));
QCOMPARE(r.maxPowerW(), 1200);
QCOMPARE(r.priority(), 2);
QVERIFY(r.enabled());
QCOMPARE(r.needs().dailyDeadline(), QString("06:00"));
QCOMPARE(r.needs().minEnergyWhPerDay(), 4000);
// Validation : une config fixed sans 0 dans powerLevels est rejetée EN BLOC (rien persisté).
QVariantMap badEntry = entry;
badEntry["powerLevels"] = QVariantList() << 600 << 1200; // pas de 0
LoadConfigs bad;
bad.append(LoadConfig::fromMap(badEntry));
LoadConfigStore store3;
QString err2;
QVERIFY(!store3.setConfigs(bad, &err2));
QVERIFY(!err2.isEmpty());
qunsetenv("NYMEA_ENERGY_LOAD_CONFIG");
QFile::remove(cfgPath);
#endif
}
void Simulation::testLoadConfigBuildsAdapters()
{
#ifndef ETM_ARBITRATOR
QSKIP("testLoadConfigBuildsAdapters nécessite ETM_ARBITRATOR.");
#else
// SetConfigs → store.changed → l'arbitre (re)construit les adaptateurs etmvariableload.
// enabled==true servi par le surplus ; enabled==false JAMAIS piloté (exclu, contrat §9).
cleanupTestCase();
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
initTestCase();
EnergyArbitrator *arbitrator = dynamic_cast(m_experiencePlugin->smartChargingManager());
QVERIFY(arbitrator);
ThingManager *thingManager = NymeaCore::instance()->thingManager();
QUuid meterId = addMeter();
m_experiencePlugin->energyManager()->setRootMeter(meterId);
Thing *meter = thingManager->findConfiguredThing(meterId);
QVERIFY(meter);
meter->setStateValue("connected", true);
QUuid id1 = addEtmVariableLoad(27020); // enabled
QUuid id2 = addEtmVariableLoad(27021); // disabled
Thing *t1 = thingManager->findConfiguredThing(id1);
Thing *t2 = thingManager->findConfiguredThing(id2);
QVERIFY(t1 && t2);
t1->setStateValue("currentPowerW", 0);
t2->setStateValue("currentPowerW", 0);
const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-build.json";
QFile::remove(cfgPath);
qputenv("NYMEA_ENERGY_LOAD_CONFIG", cfgPath.toUtf8());
LoadConfigStore *store = new LoadConfigStore(arbitrator);
arbitrator->setLoadConfigStore(store);
LoadConfigs cfgs;
cfgs.append(LoadConfig::fromMap(QVariantMap{
{"id", id1.toString()}, {"label", "ECS actif"}, {"adapter", "etmvariableload"},
{"mode", "fixed"}, {"powerLevels", QVariantList() << 0 << 2400}, {"maxPowerW", 2400},
{"priority", 1}, {"enabled", true}}));
cfgs.append(LoadConfig::fromMap(QVariantMap{
{"id", id2.toString()}, {"label", "ECS désactivé"}, {"adapter", "etmvariableload"},
{"mode", "fixed"}, {"powerLevels", QVariantList() << 0 << 1200}, {"maxPowerW", 1200},
{"priority", 2}, {"enabled", false}}));
QString err;
QVERIFY2(store->setConfigs(cfgs, &err), err.toUtf8()); // persiste + changed → rebuild
// Surplus large (5000 W) : assez pour servir les DEUX si elles étaient actives.
meter->setStateValue("currentPower", -5000);
arbitrator->simulationCallUpdate(utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0)));
QCoreApplication::processEvents();
QCOMPARE(qRound(t1->stateValue("powerSetpoint").toDouble()), 2400); // enabled → servi
QCOMPARE(qRound(t2->stateValue("powerSetpoint").toDouble()), 0); // disabled → jamais piloté
qunsetenv("NYMEA_ENERGY_LOAD_CONFIG");
QFile::remove(cfgPath);
#endif
}
void Simulation::testLoadConfigRpc()
{
#ifndef ETM_ARBITRATOR
QSKIP("testLoadConfigRpc nécessite ETM_ARBITRATOR.");
#else
// End-to-end JSON-RPC (la couche que l'app consomme) : GetLoadConfig vide → SetLoadConfig
// (fixed réelle + dynamic) → energyError NoError → GetLoadConfig round-trip → effet RÉEL sur
// le thing (RPC → store → changed → arbitre reconstruit → adaptateur → powerSetpoint) →
// rejet d'une config invalide.
const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-rpc.json";
QFile::remove(cfgPath);
qputenv("NYMEA_ENERGY_LOAD_CONFIG", cfgPath.toUtf8()); // avant initTestCase → store sur ce chemin
cleanupTestCase();
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
initTestCase();
EnergyArbitrator *arbitrator = dynamic_cast(m_experiencePlugin->smartChargingManager());
QVERIFY(arbitrator);
ThingManager *thingManager = NymeaCore::instance()->thingManager();
QUuid meterId = addMeter();
m_experiencePlugin->energyManager()->setRootMeter(meterId);
Thing *meter = thingManager->findConfiguredThing(meterId);
QVERIFY(meter);
meter->setStateValue("connected", true);
QUuid ecsId = addEtmVariableLoad(27030); // thing réel piloté par la config fixed
Thing *ecsThing = thingManager->findConfiguredThing(ecsId);
QVERIFY(ecsThing);
ecsThing->setStateValue("currentPowerW", 0);
// 1. GetLoadConfig initial → vide.
QVariant resp = injectAndWait("NymeaEnergy.GetLoadConfig");
QVERIFY(resp.toMap().value("params").toMap().value("loadConfigs").toList().isEmpty());
// 2. SetLoadConfig : les DEUX formes rév. 3 (mutuellement exclusives) doivent être acceptées
// par le MÊME schéma SET — sinon nymea rejette une forme avant le handler (bug objectRef
// strict de T4, doublé). Une etmvariableload fixed (thing réel) + une dynamic + une relay-router.
QVariantList loadConfigs;
loadConfigs << QVariantMap{
{"id", ecsId.toString()}, {"label", "Chauffe-eau"}, {"adapter", "etmvariableload"},
{"mode", "fixed"}, {"powerLevels", QVariantList() << 0 << 1200 << 2400}, {"maxPowerW", 2400},
{"priority", 2}, {"enabled", true},
{"needs", QVariantMap{{"dailyDeadline", "06:00"}, {"minEnergyWhPerDay", 4000}}}};
loadConfigs << QVariantMap{
{"id", "{b033b212-1adb-4df0-ba2b-8fa477de52a2}"}, {"label", "Routeur PV"},
{"adapter", "etmvariableload"}, {"mode", "dynamic"}, {"maxPowerW", 3000},
{"priority", 1}, {"enabled", true}};
loadConfigs << QVariantMap{ // ← rév. 3 : relay-router (relays[])
{"id", "ecs-relais"}, {"label", "ECS relais"}, {"adapter", "relay-router"},
{"mode", "fixed"}, {"priority", 3}, {"enabled", true},
{"relays", QVariantList()
<< QVariantMap{{"thingId", "{aaaaaaaa-1111-2222-3333-444444444444}"}, {"powerW", 1000}}
<< QVariantMap{{"thingId", "{bbbbbbbb-1111-2222-3333-444444444444}"}, {"powerW", 1500}}},
{"minOnS", 60}, {"minOffS", 60}};
resp = injectAndWait("NymeaEnergy.SetLoadConfig", {{"loadConfigs", loadConfigs}});
QCOMPARE(resp.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorNoError"));
// 3. GetLoadConfig → round-trip des 3 entrées (les 2 formes), champs préservés (pack/unpack).
resp = injectAndWait("NymeaEnergy.GetLoadConfig");
const QVariantList got = resp.toMap().value("params").toMap().value("loadConfigs").toList();
QCOMPARE(got.size(), 3);
QVariantMap fixedGot, relayGot;
for (const QVariant &v : got) {
const QVariantMap m = v.toMap();
if (m.value("id").toString() == ecsId.toString()) fixedGot = m;
if (m.value("adapter").toString() == "relay-router") relayGot = m;
}
QVERIFY(!fixedGot.isEmpty());
QCOMPARE(fixedGot.value("mode").toString(), QString("fixed"));
QCOMPARE(fixedGot.value("label").toString(), QString("Chauffe-eau"));
QVariantList levels = fixedGot.value("powerLevels").toList();
QCOMPARE(levels.size(), 3);
QCOMPARE(levels.last().toInt(), 2400);
QCOMPARE(fixedGot.value("needs").toMap().value("dailyDeadline").toString(), QString("06:00"));
// Forme relay-router : relays[] + minOnS round-trip (jamais piloté ici — RelayRouter en étape 4).
QVERIFY(!relayGot.isEmpty());
QVariantList relays = relayGot.value("relays").toList();
QCOMPARE(relays.size(), 2);
QCOMPARE(relays.last().toMap().value("powerW").toInt(), 1500);
QCOMPARE(relayGot.value("minOnS").toInt(), 60);
// 4. Fichier persisté.
QVERIFY(QFileInfo::exists(cfgPath));
// 5. EFFET RÉEL : le Set RPC a reconstruit les adaptateurs → un cycle surplus pilote le thing.
// Cascade du waterfall unifié construit DEPUIS la config RPC : surplus 6000 → routeur dynamic
// (rang 1) prend 3000 (clamp maxPowerW), reliquat 3000 → ECS fixed (rang 2) prend le palier 2400.
meter->setStateValue("currentPower", -6000);
arbitrator->simulationCallUpdate(utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0)));
QCoreApplication::processEvents();
QCOMPARE(qRound(ecsThing->stateValue("powerSetpoint").toDouble()), 2400); // reliquat 3000 → palier 2400
// 6. Validation CONDITIONNELLE — rejet en bloc (energyError InvalidParameter) :
// (a) etmvariableload fixed sans 0 dans powerLevels ; (b) relay-router avec relays[] vide.
QVariantList badA;
badA << QVariantMap{
{"id", "{cccccccc-2222-3333-4444-555555555555}"}, {"label", "Bancal"}, {"adapter", "etmvariableload"},
{"mode", "fixed"}, {"powerLevels", QVariantList() << 1200 << 2400}, {"maxPowerW", 2400},
{"priority", 1}, {"enabled", true}};
resp = injectAndWait("NymeaEnergy.SetLoadConfig", {{"loadConfigs", badA}});
QCOMPARE(resp.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorInvalidParameter"));
QVariantList badB;
badB << QVariantMap{
{"id", "ecs-vide"}, {"label", "ECS sans relais"}, {"adapter", "relay-router"},
{"mode", "fixed"}, {"priority", 1}, {"enabled", true},
{"relays", QVariantList()}}; // relays[] vide → invalide
resp = injectAndWait("NymeaEnergy.SetLoadConfig", {{"loadConfigs", badB}});
QCOMPARE(resp.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorInvalidParameter"));
// Aucun rejet n'a écrasé : GetLoadConfig retourne toujours les 3 valides.
resp = injectAndWait("NymeaEnergy.GetLoadConfig");
QCOMPARE(resp.toMap().value("params").toMap().value("loadConfigs").toList().size(), 3);
qunsetenv("NYMEA_ENERGY_LOAD_CONFIG");
QFile::remove(cfgPath);
#endif
}
void Simulation::testSgReadySurplus()
{
#ifndef ETM_ARBITRATOR
QSKIP("testSgReadySurplus nécessite ETM_ARBITRATOR.");
#else
// Encodage SG-Ready 2 bits (K1,K2) : 1=[K1] blocage · 2=[] normal · 3=[K2] reco · 4=[K1,K2] forcé.
// estimatedPowerW déclaré : P3=1500, P4=3000. Hystérésis état 4 : entrée P4×1,2=3600, sortie P4×1,0=3000.
const QHash pacPower({ {1, 0.0}, {2, 0.0}, {3, 1500.0}, {4, 3000.0} });
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
// ===================== Volets 1-3 : PAC seule =====================
cleanupTestCase();
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
initTestCase();
EnergyArbitrator *arbitrator = dynamic_cast(m_experiencePlugin->smartChargingManager());
QVERIFY2(arbitrator, "smartChargingManager n'est pas un EnergyArbitrator");
ThingManager *tm = NymeaCore::instance()->thingManager();
QUuid meterId = addMeter();
m_experiencePlugin->energyManager()->setRootMeter(meterId);
Thing *meter = tm->findConfiguredThing(meterId);
QVERIFY(meter);
meter->setStateValue("connected", true);
QUuid k1 = addPowerSwitch(0, 26661);
QUuid k2 = addPowerSwitch(0, 26662);
Thing *relayK1 = tm->findConfiguredThing(k1);
Thing *relayK2 = tm->findConfiguredThing(k2);
QVERIFY(relayK1 && relayK2);
SgReadyAdapter *pac = new SgReadyAdapter(
tm, "pac-test", "PAC test",
QHash>({ {1, {k1.toString()}}, {2, {}},
{3, {k2.toString()}}, {4, {k1.toString(), k2.toString()}} }),
pacPower, 300, 1, arbitrator);
arbitrator->registerSgReadyAdapter(pac);
auto setMeterW = [&](double signedW){ meter->setStateValue("currentPower", signedW); }; // <0 export
auto cycle = [&](const QDateTime &now){ arbitrator->simulationCallUpdate(now); QCoreApplication::processEvents(); };
// --- Volet 1 : montée d'états 2 → 3 → 4 (mapping sémantique) ---
setMeterW(-1000); cycle(t0); // budget 1000 < P3 → état 2 (normal)
QCOMPARE(pac->currentState(), 2);
setMeterW(-2000); cycle(t0); // budget 2000 ≥ P3 → état 3 (reco)
QCOMPARE(pac->currentState(), 3);
QCOMPARE(relayK2->stateValue("power").toBool(), true);
QCOMPARE(relayK1->stateValue("power").toBool(), false);
setMeterW(-2500); cycle(t0.addSecs(400)); // budget 2500+1500=4000 ≥ P4×1,2 → état 4 (hold écoulé)
QCOMPARE(pac->currentState(), 4);
QCOMPARE(relayK1->stateValue("power").toBool(), true);
QCOMPARE(relayK2->stateValue("power").toBool(), true);
// --- Volet 2 : hystérésis 3↔4 (budget oscille dans la zone morte [P4×1,0 ; P4×1,2)) ---
// hold écoulé à chaque cycle (lastSwitch=T0+400) → c'est la ZONE MORTE qui tient l'état 4, pas le verrou.
setMeterW(-300); cycle(t0.addSecs(800)); // budget 300+3000=3300 ∈ [3000,3600) → reste 4
QCOMPARE(pac->currentState(), 4);
setMeterW(-100); cycle(t0.addSecs(1200)); // budget 3100 → reste 4
QCOMPARE(pac->currentState(), 4);
setMeterW(-500); cycle(t0.addSecs(1600)); // budget 3500 → reste 4
QCOMPARE(pac->currentState(), 4);
// En-dessous de P4×1,0 → sort enfin de l'état 4 (vers 3).
setMeterW(200); cycle(t0.addSecs(2000)); // import 200 → budget -200+3000=2800 < 3000 → état 3
QCOMPARE(pac->currentState(), 3);
// --- Volet 3 : protection court-cycling (changement avant minStateHold → GELÉ) ---
// lastSwitch=T0+2000. À T0+2100 (elapsed 100 < hold 300) : surplus abondant mais GELÉ en 3.
setMeterW(-3000); cycle(t0.addSecs(2100));
QCOMPARE(pac->currentState(), 3); // gelé malgré budget ≥ P4×1,2 (protection compresseur)
// À T0+2400 (elapsed 400 > hold) : MÊME surplus → bascule en 4. Seul le temps simulé a changé.
setMeterW(-3000); cycle(t0.addSecs(2400));
QCOMPARE(pac->currentState(), 4);
// ===================== Volet 4 : budget PARTAGÉ ECS(etmvariableload)↔PAC =====================
// [T3] Surplus 3000 W ; ECS palier 2400 W (etmvariableload, kind Setpoint), PAC P3 = 1500.
// Selon l'ordre de priorité, l'un se sert et l'autre voit le RELIQUAT → waterfall unifié
// (un seul budget cascade à travers etmvariableload ET sg-ready, triés par priorité).
auto runSharedBudget = [&](int ecsPrio, int pacPrio, double &ecsSetpointOut, int &pacStateOut) {
cleanupTestCase();
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
initTestCase();
EnergyArbitrator *arb = dynamic_cast(m_experiencePlugin->smartChargingManager());
QVERIFY(arb);
ThingManager *tm2 = NymeaCore::instance()->thingManager();
QUuid mId = addMeter();
m_experiencePlugin->energyManager()->setRootMeter(mId);
Thing *m2 = tm2->findConfiguredThing(mId);
QVERIFY(m2);
m2->setStateValue("connected", true);
QUuid eId = addEtmVariableLoad(27003); // ECS etmvariableload
QUuid j1 = addPowerSwitch(0, 26661); // relais PAC K1
QUuid j2 = addPowerSwitch(0, 26662); // relais PAC K2
Thing *eThing = tm2->findConfiguredThing(eId);
QVERIFY(eThing);
eThing->setStateValue("currentPowerW", 0);
// ECS : 1 palier à 2400 W (fixed). La charge ne tire encore rien (currentPowerW=0).
EtmVariableLoadAdapter *ecsWf = new EtmVariableLoadAdapter(
tm2, eId.toString(), "ECS waterfall", QList({0, 2400}), 2400, ecsPrio, LoadNeeds(), arb);
arb->registerEtmVariableLoadAdapter(ecsWf);
SgReadyAdapter *pacWf = new SgReadyAdapter(
tm2, "pac-wf", "PAC waterfall",
QHash>({ {1, {j1.toString()}}, {2, {}},
{3, {j2.toString()}}, {4, {j1.toString(), j2.toString()}} }),
pacPower, 300, pacPrio, arb);
arb->registerSgReadyAdapter(pacWf);
m2->setStateValue("currentPower", -3000); // export 3000 W
arb->simulationCallUpdate(t0);
QCoreApplication::processEvents();
ecsSetpointOut = ecsWf->currentSetpointW();
pacStateOut = pacWf->currentState();
};
double ecsSetpoint = -1;
int pacState = -1;
// ECS prioritaire (rang 1) : ECS se sert (2400) → reliquat 600 < P3 → PAC reste NORMAL (2).
runSharedBudget(/*ecsPrio*/ 1, /*pacPrio*/ 2, ecsSetpoint, pacState);
QCOMPARE(qRound(ecsSetpoint), 2400);
QCOMPARE(pacState, 2);
// Priorités INVERSÉES — PAC prioritaire (rang 1) : PAC se sert (état 3, 1500) → reliquat
// 1500 < 2400 → l'ECS reste à 0 W. L'ordre de service s'inverse (même budget unique).
runSharedBudget(/*ecsPrio*/ 2, /*pacPrio*/ 1, ecsSetpoint, pacState);
QCOMPARE(qRound(ecsSetpoint), 0);
QCOMPARE(pacState, 3);
#endif
}
void Simulation::testEcsRelayTopologies()
{
// [T2] DÉSACTIVÉ — les topologies relais (1 relais, 3 relais non-cascadé, mapping
// powerLevels↔combinaisons) sont désormais la responsabilité du THING etmvariableload, plus
// de l'energymanager (contrat rév. 2 §1/§3 : la combinatoire matérielle vit dans le thing).
// Côté moteur, seul l'arrondi au powerLevels (Setpoint W) sera testé — TODO T3.
QSKIP("TODO T3 — la combinatoire relais part dans le thing ; le moteur ne teste que l'arrondi W.");
}
void Simulation::run_data()
{
// Simulation infos
QTest::addColumn("simulationName");
QTest::addColumn("simulationTitle");
QTest::addColumn("databaseName");
QTest::addColumn("simulationStart");
QTest::addColumn("plugEvents");
QTest::addColumn("simulationHours");
QTest::addColumn("sampleRate");
QTest::addColumn("productionScaling");
QTest::addColumn("detailsStepStart");
QTest::addColumn("detailsStepStop");
QTest::addColumn("detailsStepList");
// Houshold info
QTest::addColumn("phasePowerLimit");
QTest::addColumn("spotMarketEnabled");
QTest::addColumn("spotMarketResourceData");
QTest::addColumn("acquisitionTolerance");
QTest::addColumn("batteryLevelConsideration");
// ChargingInfo
QTest::addColumn("targetPercentage");
QTest::addColumn("targetDateTime");
QTest::addColumn("chargingMode");
QTest::addColumn("carBatteryLevel");
QTest::addColumn("dailySpotMarketPercentage");
// Car information and states
QTest::addColumn("carCapacity");
QTest::addColumn("carMinChargingCurrent");
QTest::addColumn("carPhaseCount");
// Energy storage information and states
QTest::addColumn("energyStorageAvailable");
QTest::addColumn("energyStorageCapacity");
QTest::addColumn("energyStorageMaxChargingPower");
QTest::addColumn("energyStorageMaxDischargingPower");
QTest::addColumn("energyStorageInitialBatteyLevel");
// Charger initial states
QTest::addColumn("chargerConnected");
QTest::addColumn("chargerPower");
QTest::addColumn("chargerPhases");
QTest::addColumn("canSwitchPhaseCount");
QTest::addColumn("chargerMaxChargingCurrent");
QTest::addColumn("chargerMaxChargingCurrentMaxValue");
QTest::addColumn("iterationTest");
bool runAllSimulations = true;
bool runSpotmarketSimulation = runAllSimulations;
bool run1PhaseSimulations = runAllSimulations;
bool run2PhaseSimulations = runAllSimulations;
bool run3PhaseSimulations = runAllSimulations;
bool runPhaseSwitchingSimulations = runAllSimulations;
// Simulations
if (runSpotmarketSimulation)
QTest::newRow("Spotmarket only")
/* Simulation info */
<< "simulation-spotmarket-only-1-phase-16A" // simulationName
<< "Simulation (1 phase, charger 16A max, only spot market)" // simulationTitle
<< ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents( { ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(7,0,0)), false),
ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(17,30,0)), true),
ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(7,0,0)), false),
ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(17,0,0)), true)
}) // pluggedInTime (UTC)
<< 48 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 0.0 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList() // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< true // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEco" // chargingMode
<< 20 // carBatteryLevel
<< 20 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 1 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< false // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 16 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 20)
}
},
{
250, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
}
},
{
500, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
}
},
{
1440, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) // Should have charged 20% in one day
}
},
{
2800, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
}
},
{
2880, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 60) // Should have charged 20% in one day
}
}
} );
if (runSpotmarketSimulation)
QTest::newRow("Spotmarket and PV")
/* Simulation info */
<< "simulation-spotmarket-and-pv-1-phase-16A" // simulationName
<< "Simulation (1 phase, charger 16A max, spot market and PV)" // simulationTitle
<< ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents()
<< 48 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 0.35 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList() // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< true // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEco" // chargingMode
<< 20 // carBatteryLevel
<< 20 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 1 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< false // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 16 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 20)
}
},
{
250, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
500, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
580, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
750, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
780, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
810, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
1400, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
2000, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 46) // Should be at least 20% more than the day before...
}
},
{
2250, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
}
},
{
2750, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
}
}
} );
if (runSpotmarketSimulation)
QTest::newRow("Spotmarket with target time")
/* Simulation info */
<< "simulation-spotmarket-only-with-targettime-1-phase-16A" // simulationName
<< "Simulation (1 phase, charger 16A max, only spot market with target time)" // simulationTitle
<< ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents() // Car plug events
<< 48 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 0.0 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList() // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< true // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEcoWithTargetTime" // chargingMode
<< 20 // carBatteryLevel
<< 0 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 1 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< false // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 16 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 20)
}
},
{
700, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
1200, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
1400, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
1550, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
1700, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
2760, { // 22:00
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
}
}
} );
if (runSpotmarketSimulation)
QTest::newRow("Spotmarket only with target time")
/* Simulation info */
<< "simulation-spotmarket-only-with-targettime-1-day-1-phase-16A" // simulationName
<< "Simulation (1 phase, charger 16A max, only spot market with target time single day)" // simulationTitle
<< ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0, 0, 0)) // simulationStart (UTC)
<< ChargerPlugEvents()
<< 32 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 0.0 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList() // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< true // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(07, 0, 0)) // targetDateTime
<< "ChargingModeEcoWithTargetTime" // chargingMode
<< 50 // carBatteryLevel
<< 0 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 1 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< false // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 16 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 50)
}
},
{
250, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 61)
}
},
{
500, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
}
},
{
1400, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
}
}
});
if (run1PhaseSimulations)
QTest::newRow("Default")
/* Simulation info */
<< "simulation-1-phase-32A" // simulationName
<< "Simulation (1 phase, charger 32A max, target 22:00 100%" // simulationTitle
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents() // pluggedInTime (UTC)
<< 18 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 20.0 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList() // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< false // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEcoWithTargetTime" // chargingMode
<< 40 // carBatteryLevel
<< 0 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 1 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< false // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 32 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
}
},
{
163, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
170, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
444, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 7) ,
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
520, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6) ,
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 80)
}
},
{
700, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6) ,
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 85)
}
},
{
900, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 30) ,
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 88)
}
},
{
960, { // 22:00
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 30) ,
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
}
}
} );
if (run1PhaseSimulations)
QTest::newRow("Default")
/* Simulation info */
<< "simulation-1-phase-16A" // simulationName
<< "Simulation (1 phase, charger 16A max, target 22:00 100%)" // simulationTitle
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(0,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents( { ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)), true) }) // pluggedInTime (UTC)
<< 24 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 20.0 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList() // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< false // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEcoWithTargetTime" // chargingMode
<< 50 // carBatteryLevel
<< 0 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 1 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< false // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 16 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 50)
}
},
{
452, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 50)
}
},
{
467, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 51)
}
},
{
750, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
830, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
841, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
1000, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
1300, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
1440, { // 22:00
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
}
}
} );
if (run2PhaseSimulations)
QTest::newRow("Kostal")
/* Simulation info */
<< "simulation-kostal-2-phase-16A" // simulationName
<< "Simulation (2 phase, charger 16A max, target 22:00 100%)" // simulationTitle
<< ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents( { ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(8,0,0)), true) }) // pluggedInTime (UTC)
<< 24 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 1.0 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList() // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< false // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEcoWithTargetTime" // chargingMode
<< 40 // carBatteryLevel
<< 0 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 2 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< false // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 16 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
}
},
{
550, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
600, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 48)
}
},
{
820, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
823, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
847, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 80)
}
},
{
1050, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
}
}
} );
if (run2PhaseSimulations)
QTest::newRow("Kostal")
/* Simulation info */
<< "simulation-kostal-2-phase-16A-away-2-hours" // simulationName
<< "Simulation (2 phase, charger 16A max, target 22:00 100%)" // simulationTitle
<< ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents( {
ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(12,0,0)), false),
ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(14,00,0)), true, 10)
}) // pluggedInTime (UTC)
<< 24 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 1.0 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList() // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< false // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEcoWithTargetTime" // chargingMode
<< 40 // carBatteryLevel
<< 0 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 2 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< false // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 16 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
}
},
{
550, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
600, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 48)
}
},
{
1300, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
1440, { // 22:00
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
}
}
} );
if (run2PhaseSimulations)
QTest::newRow("Default")
/* Simulation info */
<< "simulation-2-phase-16A" // simulationName
<< "Simulation (2 phase, charger 16A max, target 22:00 100%)" // simulationTitle
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents() // pluggedInTime (UTC)
<< 18 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 20.0 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList() // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< false // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEcoWithTargetTime" // chargingMode
<< 40 // carBatteryLevel
<< 0 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 2 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< false // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 16 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
}
},
{
850, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
900, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
960, { // 22:00
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
}
}
} );
if (run2PhaseSimulations)
QTest::newRow("Default")
/* Simulation info */
<< "simulation-2-phase-32A" // simulationName
<< "Simulation (2 phase, charger 32A max, target 22:00 100%)" // simulationTitle
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents() // pluggedInTime (UTC)
<< 18 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 20.0 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList() // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< false // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEcoWithTargetTime" // chargingMode
<< 40 // carBatteryLevel
<< 0 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 2 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< false // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 32 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
}
},
{
100, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
300, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 61)
}
},
{
470, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
586, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
800, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
950, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 30),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 99)
}
},
{
960, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
}
}
} );
if (run3PhaseSimulations)
QTest::newRow("Default")
/* Simulation info */
<< "simulation-3-phase-16A" // simulationName
<< "Simulation (3 phase, charger 16A max, target 22:00 100%)" // simulationTitle
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents() // pluggedInTime (UTC)
<< 18 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 40.0 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList({200, 300, 400, 500}) // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< false // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEcoWithTargetTime" // chargingMode
<< 40 // carBatteryLevel
<< 0 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 3 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< false // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 16 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
}
},
{
200, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 11),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 57)
}
},
{
300, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 13),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 83)
}
},
{
400, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 11),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
}
},
{
700, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
}
}
} );
if (run3PhaseSimulations)
QTest::newRow("Default")
/* Simulation info */
<< "simulation-3-phase-32A" // simulationName
<< "Simulation (3 phase, charger 32A max, target 22:00 100%)" // simulationTitle
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents() // pluggedInTime (UTC)
<< 18 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 20.0 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList({ 480, 500, 600, 950, 960 }) // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< false // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEcoWithTargetTime" // chargingMode
<< 40 // carBatteryLevel
<< 0 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 3 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< false // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 32 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
100, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
}
},
{
200, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
300, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
400, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
}
},
{
480, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
500, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 82)
}
},
{
600, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
}
},
{
950, {
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 98) // Finish 10 min early
}
},
{
960, { // 22:00
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
}
}
} );
if (runPhaseSwitchingSimulations)
QTest::newRow("Default")
/* Simulation info */
<< "simulation-phase-switching-16A" // simulationName
<< "Simulation (phase switching, charger 16A max, surplus only)" // simulationTitle
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents() // pluggedInTime (UTC)
<< 18 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 45.0 // productionScaling
<< 0 // detailsStepStart
<< 0 // detailsStepStop
<< DetailsStepList({80, 150, 310, 400, 470}) // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< false // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEco" // chargingMode
<< 40 // carBatteryLevel
<< 0 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 3 // carPhaseCount
/* Energy storage */
<< false // energyStorageAvailable
<< 0 // energyStorageCapacity
<< 0.0 // energyStorageMaxChargingPower
<< 0.0 // energyStorageMaxDischargingPower
<< 50.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< true // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 16 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
}
},
{
80, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
}
},
{
150, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 10),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 50)
}
},
{
310, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 91)
}
},
{
400, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 13),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
}
},
{
470, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
}
},
} );
if (runPhaseSwitchingSimulations)
QTest::newRow("Default")
/* Simulation info */
<< "simulation-energy-storage-phase-switching" // simulationName
<< "Simulation energy storage, phase switching" // simulationTitle
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(0,0,0)) // simulationStart (UTC)
<< ChargerPlugEvents() // pluggedInTime (UTC)
<< 36 // simulationHours
<< EnergyLogs::SampleRate1Min
<< 45.0 // productionScaling
<< 0 // detailsStepStart
<< 10 // detailsStepStop
<< DetailsStepList({}) // detailsStepList
/* Houshold info */
<< 32 // phase limit (A)
<< false // spotMarketEnabled
<< ":/resources/dataset-1.json" // spotMarketResourceData
<< 0.5 // acquisitionTolerance
<< 0.9 // batteryLevelConsideration
/* Charging Info */
<< 100.0 // targetPercentage
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
<< "ChargingModeEco" // chargingMode
<< 40 // carBatteryLevel
<< 0 // dailySpotMarketPercentage
/* Car settings */
<< 50 // carCapacity
<< 6 // carMinChargingCurrent
<< 3 // carPhaseCount
/* Energy storage */
<< true // energyStorageAvailable
<< 12 // energyStorageCapacity
<< 5000.0 // energyStorageMaxChargingPower
<< 5000.0 // energyStorageMaxDischargingPower
<< 10.0 // energyStorageInitialBatteyLevel
<< true // chargerConnected
<< false // chargerPower
<< "ABC" // chargerPhases
<< true // canSwitchPhaseCount
<< 6 // chargerMaxChargingCurrent
<< 16 //chargerMaxChargingCurrentMaxValue
<< SimulationIterationTest ( {
{
0, {
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
}
}
} );
}
void Simulation::run()
{
QFETCH(QString, simulationName);
QFETCH(QString, simulationTitle);
QFETCH(QString, databaseName);
QFETCH(QDateTime, simulationStart);
QFETCH(ChargerPlugEvents, plugEvents);
QFETCH(int, simulationHours);
QFETCH(EnergyLogs::SampleRate, sampleRate);
QFETCH(double, productionScaling);
QFETCH(int, detailsStepStart);
QFETCH(int, detailsStepStop);
QFETCH(DetailsStepList, detailsStepList);
QFETCH(int, phasePowerLimit);
QFETCH(bool, spotMarketEnabled);
QFETCH(QString, spotMarketResourceData);
QFETCH(double, acquisitionTolerance);
QFETCH(double, batteryLevelConsideration);
QFETCH(double, targetPercentage);
QFETCH(QDateTime, targetDateTime);
QFETCH(QString, chargingMode);
QFETCH(int, carBatteryLevel);
QFETCH(int, dailySpotMarketPercentage);
QFETCH(int, carCapacity);
QFETCH(int, carMinChargingCurrent);
QFETCH(int, carPhaseCount);
QFETCH(bool, energyStorageAvailable);
QFETCH(int, energyStorageCapacity);
QFETCH(double, energyStorageMaxChargingPower);
QFETCH(double, energyStorageMaxDischargingPower);
QFETCH(double, energyStorageInitialBatteyLevel);
QFETCH(bool, chargerConnected);
QFETCH(bool, chargerPower);
QFETCH(QString, chargerPhases);
QFETCH(bool, canSwitchPhaseCount);
QFETCH(int, chargerMaxChargingCurrent);
QFETCH(int, chargerMaxChargingCurrentMaxValue);
QFETCH(SimulationIterationTest, iterationTest);
QStringList loggingDefaultList = {
"*.debug=false",
"Application.debug=true",
"LogEngine.info=false",
"Simulation.debug=true",
"Experiences.debug=false",
"NymeaEnergy.debug=false",
"EnergyMocks.debug=false",
"DBus.warning=false",
};
QString loggingRulesDefault = loggingDefaultList.join("\n");
QStringList loggingDetailsList = {
"*.debug=false",
"Application.debug=true",
"LogEngine.info=false",
"Simulation.debug=true",
"Experiences.debug=false",
"NymeaEnergy.debug=true",
"EnergyMocks.debug=false",
"DBus.warning=false",
};
QString loggingRulesDetails = loggingDetailsList.join("\n");
QStringList availableChargingModes;
availableChargingModes << "ChargingModeNormal";
availableChargingModes << "ChargingModeEco";
availableChargingModes << "ChargingModeEcoWithTargetTime";
QVERIFY2(availableChargingModes.contains(chargingMode), "Unknown charging mode passed to the simulation. Please compair the list with the ChargingMode enum.");
if (canSwitchPhaseCount)
QVERIFY2(chargerPhases == "ABC", "If the charger supports phase count switching all 3 phases must be connected.");
cleanupTestCase();
m_energyLogDbFilePath = databaseName;
initTestCase(loggingRulesDefault);
// Print simulation init details
QLoggingCategory::setFilterRules(loggingRulesDefault);
QVariant response; QVariantMap params;
QNetworkReply *reply = nullptr;
QSignalSpy packetSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
Electricity::Phases chargerPhasesConverted = Electricity::convertPhasesFromString(chargerPhases);
// Set phase power limit
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", phasePowerLimit}}));
QVERIFY(response.toMap().value("params").toMap().value("energyError").toString() == "EnergyErrorNoError");
// Add mock spotmarket provider
SpotMarketManager *spotMarketManager = m_experiencePlugin->spotMarketManager();
SpotMarketDataProviderMock *mockProvider = new SpotMarketDataProviderMock(nullptr, this);
QVERIFY(mockProvider->prepareResourceData(spotMarketResourceData, simulationStart.toUTC()));
QVERIFY(spotMarketManager->registerProvider(mockProvider));
QVERIFY(spotMarketManager->changeProvider(mockProvider->providerId()));
// Enabke/disable spot market
response = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", QVariantMap({ {"enabled", spotMarketEnabled }, {"providerId", mockProvider->providerId()} }));
QCOMPARE(response.toMap().value("params").toMap().value("energyError").toString(), "EnergyErrorNoError");
QCOMPARE(m_experiencePlugin->spotMarketManager()->enabled(), spotMarketEnabled);
// Set initial acquisition tolerance
params.clear(); response.clear();
params.insert("acquisitionTolerance", acquisitionTolerance);
response = injectAndWait("NymeaEnergy.SetAcquisitionTolerance", params);
verifyEnergyError(response);
// Set battery level consideration
params.clear(); response.clear();
params.insert("batteryLevelConsideration", batteryLevelConsideration);
response = injectAndWait("NymeaEnergy.SetBatteryLevelConsideration", params);
verifyEnergyError(response);
// Add mock meter
QUuid meterThingId = addMeter();
QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
if (packetSpy.count() == 0) packetSpy.wait();
checkNotification(packetSpy, "Integrations.ThingAdded");
packetSpy.clear();
// Set it as root meter
m_experiencePlugin->energyManager()->setRootMeter(meterThingId);
// Make sure this is our root meter now
response = injectAndWait("Energy.GetRootMeter");
QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
packetSpy.clear();
// Add the charger
QUuid evChargerId;
if (canSwitchPhaseCount) {
evChargerId = addChargerWithPhaseCountSwitching(chargerPhases, chargerMaxChargingCurrentMaxValue);
} else {
evChargerId = addCharger(chargerPhases, chargerMaxChargingCurrentMaxValue);
}
QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId");
if (packetSpy.count() == 0) packetSpy.wait();
checkNotification(packetSpy, "Integrations.ThingAdded");
// Add the car
QUuid carThingId = addCar();
QVERIFY2(!carThingId.isNull(), "Did not receive valid ThingId");
if (packetSpy.count() == 0) packetSpy.wait();
checkNotification(packetSpy, "Integrations.ThingAdded");
// Add energy storage if available
QUuid energyStorageThingId;
if (energyStorageAvailable) {
energyStorageThingId = addEnergyStorage(energyStorageCapacity, energyStorageMaxChargingPower, energyStorageMaxDischargingPower);
QVERIFY2(!energyStorageThingId.isNull(), "Did not receive valid ThingId");
if (packetSpy.count() == 0) packetSpy.wait();
checkNotification(packetSpy, "Integrations.ThingAdded");
}
// ==============================================================================
// Set states of car and charger
Thing *carThing = NymeaCore::instance()->thingManager()->findConfiguredThing(carThingId);
QVERIFY2(carThing != nullptr, "Failed to find car thing");
carThing->setSettingValue(carThing->thingClass().settingsTypes().findByName("phaseCount").id(), carPhaseCount);
carThing->setSettingValue(carThing->thingClass().settingsTypes().findByName("capacity").id(), carCapacity);
carThing->setSettingValue(carThing->thingClass().settingsTypes().findByName("minChargingCurrent").id(), carMinChargingCurrent);
carThing->setStateValue("batteryLevel", carBatteryLevel);
Thing *chargerThing = NymeaCore::instance()->thingManager()->findConfiguredThing(evChargerId);
QVERIFY2(chargerThing != nullptr, "Failed to find charger thing");
chargerThing->setStateValue("connected", chargerConnected);
chargerThing->setStateValue("power", chargerPower);
chargerThing->setStateValue("maxChargingCurrent", chargerMaxChargingCurrent);
chargerThing->setStateValue("maxChargingCurrentMaxValue", chargerMaxChargingCurrentMaxValue);
chargerThing->setStateValue("pluggedIn", true); // Initially always plugged in, the rest can be handeld using the plug events
// This will update all internal states not set directly
updateChargerMeter(chargerThing);
Thing *meterThing = NymeaCore::instance()->thingManager()->findConfiguredThing(meterThingId);
QVERIFY2(meterThing != nullptr, "Failed to find meter thing");
meterThing->setStateValue("connected", true);
// Energy storage states
Thing *energyStorageThing = nullptr;
if (energyStorageAvailable) {
energyStorageThing = NymeaCore::instance()->thingManager()->findConfiguredThing(energyStorageThingId);
energyStorageThing->setStateValue("currentPower", 0);
energyStorageThing->setStateValue("batteryLevel", energyStorageInitialBatteyLevel);
energyStorageThing->setProperty("preciseBatteryLevel", energyStorageInitialBatteyLevel * 1.0); // For precise runtime calculations
printStates(energyStorageThing);
}
// printStates(chargerThing);
// printStates(carThing);
// printStates(meterThing);
// Set charging info with our charger and car, this should trigger the evaluation
QVariantMap chargingInfoMap;
chargingInfoMap.insert("evChargerId", evChargerId);
chargingInfoMap.insert("assignedCarId", carThingId);
chargingInfoMap.insert("chargingMode", chargingMode);
chargingInfoMap.insert("endDateTime", targetDateTime.toMSecsSinceEpoch() / 1000);
chargingInfoMap.insert("targetPercentage", targetPercentage);
chargingInfoMap.insert("spotMarketChargingEnabled", spotMarketEnabled);
chargingInfoMap.insert("dailySpotMarketPercentage", dailySpotMarketPercentage);
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", chargingInfoMap}}));
QCOMPARE(response.toMap().value("status").toString(), QString("success"));
QCOMPARE(response.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorNoError"));
uint effectivePhaseCount;
if (canSwitchPhaseCount) {
effectivePhaseCount = chargerThing->stateValue("phaseCount").toUInt();
} else {
effectivePhaseCount = qMin((uint)carPhaseCount, Electricity::getPhaseCount(chargerPhasesConverted));
}
QString usedPhases;
if (effectivePhaseCount >= 1)
usedPhases.append("A");
if (effectivePhaseCount >= 2)
usedPhases.append("B");
if (effectivePhaseCount >= 3)
usedPhases.append("C");
chargerThing->setStateValue("usedPhases", usedPhases);
// Note: we want to have the limit negative, so we see better where the limit is and why the chaging stopped
double aquisitionToleranceLimit = -(effectivePhaseCount * carMinChargingCurrent * 230.0 * acquisitionTolerance);
// Simulation output
QDir simulationBaseDir = QDir(QDir::currentPath() + QDir::separator() + "simulations");
QDir workspaceDir = QDir(simulationBaseDir.absolutePath() + QDir::separator() + "workspace");
if (!workspaceDir.exists()) {
//QVERIFY2(outputDir.removeRecursively(), "Failed to cleanup output dir");
QVERIFY2(workspaceDir.mkpath(workspaceDir.path()), "Failed to create results dir");
}
QDir outputDir = QDir(workspaceDir.absolutePath() + QDir::separator() + simulationName);
if (!outputDir.exists()) {
// QVERIFY2(outputDir.removeRecursively(), "Failed to cleanup output dir");
QVERIFY2(outputDir.mkpath(outputDir.path()), "Failed to create output dir");
}
QDir resultsDir = QDir(simulationBaseDir.absolutePath() + QDir::separator() + "results");
if (!resultsDir.exists()) {
//QVERIFY2(outputDir.removeRecursively(), "Failed to cleanup output dir");
QVERIFY2(resultsDir.mkpath(resultsDir.path()), "Failed to create results dir");
}
QFile gnuplotLogFile(outputDir.path() + QDir::separator() + "simulation.csv");
QVERIFY2(gnuplotLogFile.open(QIODevice::ReadWrite | QIODevice::Truncate), QString("Failed to open logfile for gnuplot" + gnuplotLogFile.fileName() + ": " + gnuplotLogFile.errorString()).toUtf8());
QTextStream gnuplotLogFileStream(&gnuplotLogFile);
QFile gnuplotOriginalLogFile(outputDir.path() + QDir::separator() + "original.csv");
QVERIFY2(gnuplotOriginalLogFile.open(QIODevice::ReadWrite | QIODevice::Truncate), "Failed to open logfile for gnuplot");
QTextStream gnuplotOriginalLogFileStream(&gnuplotOriginalLogFile);
// ==============================================================================
// All set up, lets start simulating
QDateTime simulationEnd = simulationStart.addSecs(simulationHours * 3600);
PowerBalanceLogEntries powerBalanceLogs = m_experiencePlugin->energyManager()->logs()->powerBalanceLogs(sampleRate, simulationStart, simulationEnd);
qCDebug(dcSimulation()) << "Simulation start" << simulationStart;
qCDebug(dcSimulation()) << "Simulation end" << simulationEnd;
qCDebug(dcSimulation()) << "Loaded" << powerBalanceLogs.count() << "log entries for that day";
// Simulation helpers
int simulationProgress = 0;
// Disable init details
QLoggingCategory::setFilterRules(loggingRulesDefault);
// Run the simulation
for (int i = 0; i < powerBalanceLogs.count(); i++) {
bool debugEnabled = (i >= detailsStepStart && i <= detailsStepStop) || detailsStepList.contains(i);
const PowerBalanceLogEntry entry = powerBalanceLogs.at(i);
QDateTime currentDateTime = entry.timestamp().toUTC();
// Calculate progress
if (debugEnabled) {
qCDebug(dcSimulation()) << "###############################################################################################################";
qCDebug(dcSimulation()) << "Step" << i << ":" << currentDateTime.toUTC().toString("yyyy.MM.dd hh:mm");
}
effectivePhaseCount = chargerThing->stateValue("phaseCount").toUInt();
usedPhases = chargerThing->stateValue("usedPhases").toString();
// Update mocked spotmarket data provider
mockProvider->setCurrentDataTime(currentDateTime.toUTC());
// Print simulation progress
double simulationProgressPrecise = i * 100 / powerBalanceLogs.count();
if (simulationProgress != qRound(simulationProgressPrecise)) {
simulationProgress = qRound(simulationProgressPrecise);
if (simulationProgress % 10 == 0) {
qCDebug(dcSimulation()) << simulationName << currentDateTime.toUTC().toString("hh:mm") << simulationProgress << "% (" << i << ")";
}
}
// Enable logs in the interesting simulation steps
if (debugEnabled) {
QLoggingCategory::setFilterRules(loggingRulesDetails);
} else {
QLoggingCategory::setFilterRules(loggingRulesDefault);
}
// Get the current situation befor running the simulation step
double totalProduction = entry.production() * productionScaling;
double totalProductionDifference = totalProduction - entry.production();
double scaledCurrentPower = entry.acquisition() + totalProductionDifference;
//qCDebug(dcSimulation()) << "Scale production using" << productionScaling << entry.production() << "-->" << totalProduction << totalProductionDifference;
//qCDebug(dcSimulation()) << "Scale aquisition using" << entry.acquisition() << "-->" << scaledCurrentPower;
// Distribute the load before battery and charger on 3 phases
double phaseLoad = scaledCurrentPower / 3;
// -------------------- Charger
// Handle plug events
foreach(const ChargerPlugEvent &plugEvent, plugEvents) {
if (plugEvent.dateTime.date() == currentDateTime.toUTC().date() &&
plugEvent.dateTime.time().hour() == currentDateTime.toUTC().time().hour() &&
plugEvent.dateTime.time().minute() == currentDateTime.toUTC().time().minute()) {
// Plug event
chargerThing->setStateValue("pluggedIn", plugEvent.pluggedIn);
if (plugEvent.percentageUsed != 0) {
double batteryLevel = carThing->stateValue("batteryLevel").toDouble();
batteryLevel -= plugEvent.percentageUsed;
if (batteryLevel < 0) {
batteryLevel = 0;
}
qCDebug(dcSimulation()) << "-->" << currentDateTime.toUTC().toString("hh:mm") << "New car battery level" << batteryLevel << "(old:" << carThing->stateValue("batteryLevel").toDouble() << ")";
carThing->setStateValue("batteryLevel", batteryLevel);
}
qCDebug(dcSimulation()) << "-->" << currentDateTime.toUTC().toString("hh:mm") << "Car has been" << (plugEvent.pluggedIn ? "plugged in" : "unplugged");
}
}
// Let the charger set all power, voltage etc....
updateChargerMeter(chargerThing);
double chargerCurrentPower = chargerThing->stateValue("currentPower").toDouble();
double totalConsumption = chargerCurrentPower + entry.consumption();
// Totals before battery
double totalCurrentPower = totalConsumption + totalProduction;
// All producers and all consumers have been summed up, charge / discharge the battery and create a final total balance
// -------------------- Energy storage
double energyStorageCurrentPower = 0;
uint energyStorageBatteryLevel = 0;
// All consumers should have what they get, put the rest into or from the storage within limits
if (energyStorageAvailable) {
// Calculate the new battery level depending on the previouse step.
double esCurrentPower = energyStorageThing->stateValue("currentPower").toDouble();
double esBatteryLevel = energyStorageThing->property("preciseBatteryLevel").toDouble();
double esCapacity = energyStorageThing->stateValue("capacity").toDouble();
// Let's caclulate the new percentage depending on the rate of the last minute...
// We charged/discharged the last minute with energyStorageCurrentPower W
double energyChargedDischargedkWh = esCurrentPower * 60 / 60 / 60 / 1000;
double addedPercentage = energyChargedDischargedkWh * 100.0 / esCapacity;
double newBatteryLevel = esBatteryLevel += addedPercentage;
if (totalCurrentPower < 0 && newBatteryLevel < 100) {
energyStorageCurrentPower = qMin(energyStorageMaxChargingPower, -totalCurrentPower);
energyStorageBatteryLevel = newBatteryLevel;
energyStorageThing->setProperty("preciseBatteryLevel", newBatteryLevel);
setEnergyStorageStates(energyStorageBatteryLevel, energyStorageCurrentPower);
} else if (totalCurrentPower > 0 && newBatteryLevel > 0) {
energyStorageCurrentPower = - qMin(energyStorageMaxDischargingPower, totalCurrentPower);
energyStorageBatteryLevel = newBatteryLevel;
energyStorageThing->setProperty("preciseBatteryLevel", newBatteryLevel);
setEnergyStorageStates(energyStorageBatteryLevel, energyStorageCurrentPower);
} else {
energyStorageBatteryLevel = newBatteryLevel;
energyStorageThing->setProperty("preciseBatteryLevel", newBatteryLevel);
setEnergyStorageStates(energyStorageBatteryLevel, energyStorageCurrentPower);
}
//qCDebug(dcSimulation()) << "Energy storage charged with" << energyStorageCurrentPower << "W" << addedPercentage << "% added to total" << energyStorageBatteryLevel << "%";
}
// -------------------- Meter
totalCurrentPower += energyStorageCurrentPower;
phaseLoad += energyStorageCurrentPower / 3;
// Add the charger power in the aproperiate phase
QVariantMap phases = QVariantMap({ {"A", phaseLoad + chargerThing->stateValue("currentPowerPhaseA").toDouble()},
{"B", phaseLoad + chargerThing->stateValue("currentPowerPhaseB").toDouble()},
{"C", phaseLoad + chargerThing->stateValue("currentPowerPhaseC").toDouble()} });
reply = setMeterStates(phases, true);
QSignalSpy setMeterStatesReplySpy(reply, &QNetworkReply::finished);
if (setMeterStatesReplySpy.count() == 0) setMeterStatesReplySpy.wait();
QCOMPARE(reply->error(), QNetworkReply::NoError);
// -------------------- Run charging manager update
// Set charger information and pass them to the logic
ThingPowerLogEntry chargerPowerEntry(currentDateTime.toUTC(), chargerThing->id(), chargerThing->stateValue("currentPower").toDouble(), 0, 0);
m_experiencePlugin->smartChargingManager()->simulationCallUpdateManualSoCsWithMeter(sampleRate, chargerPowerEntry);
// Update smart charging manager with the current root meter and charger situation
m_experiencePlugin->smartChargingManager()->simulationCallUpdate(currentDateTime.toUTC());
// Fetch information after simulation iteration
double carBatteryPercentage = carThing->stateValue("batteryLevel").toDouble();
int maxChargingCurrent = chargerThing->stateValue("maxChargingCurrent").toInt();
chargerPower = chargerThing->stateValue("power").toBool();
chargerCurrentPower = chargerThing->stateValue("currentPower").toDouble();
if (debugEnabled | iterationTest.contains(i)) {
qCDebug(dcSimulation()) << "Step" << i;
qCDebug(dcSimulation()) << "- Total power:" << totalCurrentPower << "Production:" << totalProduction << "Consumption:" << totalConsumption;
if (energyStorageAvailable) {
qCDebug(dcSimulation()) << "- Energy storage:" << energyStorageCurrentPower << energyStorageThing->property("preciseBatteryLevel").toDouble() << "%";
}
qCDebug(dcSimulation()) << "- Meter phases: A:" << meterThing->stateValue("currentPowerPhaseA").toDouble() << "W | B:"
<< meterThing->stateValue("currentPowerPhaseB").toDouble() << "W | C:" << meterThing->stateValue("currentPowerPhaseC").toDouble() << "W";
qCDebug(dcSimulation()) << "- Charger:" << chargerCurrentPower << "[W] (" << maxChargingCurrent << "[A]" << (chargerCurrentPower ? "On)" : "Off )") << effectivePhaseCount << usedPhases;
qCDebug(dcSimulation()) << "- Charger phases: A:" << chargerThing->stateValue("currentPowerPhaseA").toDouble() << "W | B:"
<< chargerThing->stateValue("currentPowerPhaseB").toDouble() << "W | C:" << chargerThing->stateValue("currentPowerPhaseC").toDouble() << "W";
qCDebug(dcSimulation()) << "- Car battery:" << carBatteryPercentage;
qCDebug(dcSimulation()) << "--------------------------------";
// printStates(meterThing);
// printStates(chargerThing);
// printStates(carThing);
}
// Verify test points
foreach(const SimulationTestPoint &testPoint, iterationTest.value(i)) {
switch(testPoint.testType()) {
case SimulationTestPoint::TestTypeCharging:
QVERIFY2(chargerPower == testPoint.expectedValue().toBool(),
qPrintable(QString("Simulation: %1 - %2 Step: %3 expected \"%4\" from the testpoint but is actually \"%5\"")
.arg(simulationName)
.arg(simulationTitle)
.arg(i)
.arg(testPoint.expectedValue().toBool() ? "true" : "false")
.arg(chargerPower ? "true" : "false")));
break;
case SimulationTestPoint::TestTypeMaxChargingCurrent:
QVERIFY2(maxChargingCurrent == testPoint.expectedValue().toInt(),
qPrintable(QString("Simulation: %1 - %2 Step: %3 expected \"%4\" from the testpoint but is actually \"%5\"")
.arg(simulationName)
.arg(simulationTitle)
.arg(i)
.arg(testPoint.expectedValue().toInt())
.arg(maxChargingCurrent)));
break;
case SimulationTestPoint::TestTypeStateOfCharge:
QVERIFY2(qFuzzyCompare(carBatteryPercentage, testPoint.expectedValue().toDouble()),
qPrintable(QString("Simulation: %1 - %2 Step: %3 expected \"%4\" from the testpoint but is actually \"%5\"")
.arg(simulationName)
.arg(simulationTitle)
.arg(i)
.arg(testPoint.expectedValue().toDouble())
.arg(carBatteryPercentage)));
break;
}
}
// -------------------- Data logging
// Log the simulation data
gnuplotLogFileStream << currentDateTime.toMSecsSinceEpoch() / 1000 << ", " << // 1
totalCurrentPower << ", " << // 2
totalProduction << ", " << // 3
totalConsumption << ", " << // 4
chargerCurrentPower << ", " << // 5
maxChargingCurrent << ", " << // 6
(chargerPower ? "1" : "0") << ", " << // 7
chargerThing->state("maxChargingCurrent").minValue().toDouble() * 230 * effectivePhaseCount << ", " << // 8
chargerThing->state("maxChargingCurrent").maxValue().toDouble() * 230 * effectivePhaseCount << ", " << // 9
phasePowerLimit * 230 * effectivePhaseCount << ", " << // 10
carBatteryPercentage << ", " << // 11
i << ", " << // 12
aquisitionToleranceLimit << ", " << // 13
(chargerThing->stateValue("pluggedIn").toBool() ? 10 : 0 ) << ", "; // 14
if (spotMarketEnabled) {
const ScoreEntries weightedEntries = spotMarketManager->weightedScoreEntries(currentDateTime.date());
const ScoreEntry currentScore = weightedEntries.getScoreEntry(currentDateTime.toUTC());
QVERIFY(!currentScore.isNull());
gnuplotLogFileStream << currentScore.weighting() * 100 << ", "; // 15
gnuplotLogFileStream << currentScore.value() / 10.0 << ", "; // 16 Price
} else {
gnuplotLogFileStream << 0 << ", "; // 15
gnuplotLogFileStream << 0 << ", "; // 16
}
if (energyStorageAvailable) {
gnuplotLogFileStream << energyStorageCurrentPower << ", "; // 17
gnuplotLogFileStream << energyStorageBatteryLevel << ", "; // 18
} else {
gnuplotLogFileStream << 0 << ", "; // 17
gnuplotLogFileStream << 0 << ", "; // 18
}
gnuplotLogFileStream << "\n";
// Log Unchanged for raw data analysis
gnuplotOriginalLogFileStream << currentDateTime.toMSecsSinceEpoch() / 1000 << ", " << // 1
scaledCurrentPower << ", " << // 2
totalProduction << ", " << // 3
entry.consumption() << ", " << // 4
phasePowerLimit * 230 * effectivePhaseCount << ", " << // 5
i << ", " << // 6
"\n";
}
gnuplotLogFile.close();
gnuplotOriginalLogFile.close();
// Draw original data
QStringList scriptLines;
QStringList plotLines;
// Plot with: 1h = 200 px
int height = 800;
int width = simulationHours * 200;
QString originalImageName = simulationName + "-00-original.png";
QString simulationImageName = simulationName + "-01.png";
scriptLines.append("set term png size " + QString::number(width) + "," + QString::number(height));
scriptLines.append("set output '" + originalImageName + "'");
scriptLines.append("set datafile separator ','");
scriptLines.append(plotOriginalData(powerBalanceLogs.count()));
if (spotMarketEnabled) {
scriptLines.append("set term png size " + QString::number(width) + "," + QString::number(height * 2));
scriptLines.append("set output '" + simulationImageName + "'");
scriptLines.append("set datafile separator ','");
scriptLines.append("set multiplot layout 2,1");
scriptLines.append("set size 1,0.8");
scriptLines.append("set origin 0,0.2");
scriptLines.append(plotSimulation(simulationTitle, powerBalanceLogs.count()));
scriptLines.append("set size 1,0.2");
scriptLines.append("set origin 0,0");
scriptLines.append(plotSpotMarketData(powerBalanceLogs.count()));
scriptLines.append("unset multiplot");
} else {
scriptLines.append("set term png size " + QString::number(width) + "," + QString::number(height));
scriptLines.append("set output '" + simulationImageName + "'");
scriptLines.append("set datafile separator ','");
scriptLines.append(plotSimulation(simulationTitle, powerBalanceLogs.count()));
}
// Write the gnuplot script
QFile gnuplotScript(outputDir.path() + QDir::separator() + "script.gnuplot");
QVERIFY2(gnuplotScript.open(QIODevice::ReadWrite | QIODevice::Truncate),
QString("Failed to open script file for gnuplot" + gnuplotScript.fileName() + ": " + gnuplotScript.errorString()).toUtf8());
QTextStream scriptStream(&gnuplotScript);
foreach (const QString &line, scriptLines)
scriptStream << line << "\n";
gnuplotScript.close();
// Write the executable gnuplot script
scriptLines.clear();
scriptLines.append("set terminal wxt 1 persist");
scriptLines.append("set datafile separator ','");
if (spotMarketEnabled) {
scriptLines.append("set multiplot layout 2,1");
scriptLines.append("set size 1,0.8");
scriptLines.append("set origin 0,0.2");
scriptLines.append(plotSimulation(simulationTitle, powerBalanceLogs.count()));
scriptLines.append("set size 1,0.2");
scriptLines.append("set origin 0,0");
scriptLines.append(plotSpotMarketData(powerBalanceLogs.count()));
scriptLines.append("unset multiplot");
} else {
scriptLines.append(plotSimulation(simulationTitle, powerBalanceLogs.count()));
}
QString executableScriptName = simulationName + ".gnuplot";
QFile executableGnuplotScript(outputDir.path() + QDir::separator() + executableScriptName);
QVERIFY2(executableGnuplotScript.open(QIODevice::ReadWrite | QIODevice::Truncate),
QString("Failed to open logfile for gnuplot" + executableGnuplotScript.fileName() + ": " + executableGnuplotScript.errorString()).toUtf8());
QTextStream executableScriptStream(&executableGnuplotScript);
foreach (const QString &line, scriptLines)
executableScriptStream << line << "\n";
executableGnuplotScript.close();
QProcess gnuplotProcess;
//gnuplotProcess.setEnvironment(QProcessEnvironment::systemEnvironment().toStringList());
gnuplotProcess.setProcessChannelMode(QProcess::MergedChannels);
gnuplotProcess.setWorkingDirectory(outputDir.path());
gnuplotProcess.start("gnuplot", { "-c", "script.gnuplot"});
gnuplotProcess.waitForFinished();
qCDebug(dcSimulation()) << "gnuplot finished" << gnuplotProcess.arguments() << gnuplotProcess.workingDirectory() << gnuplotProcess.exitCode() << gnuplotProcess.exitStatus();
if (gnuplotProcess.exitCode() != 0) {
qCDebug(dcSimulation()) << "error plotting data:\n" << qUtf8Printable(gnuplotProcess.readAll());
QVERIFY2(false, "plot process finished with error");
}
// Copy resulting images to the simulations
QFile::copy(outputDir.path() + QDir::separator() + originalImageName, resultsDir.path() + QDir::separator() + originalImageName);
QFile::copy(outputDir.path() + QDir::separator() + simulationImageName, resultsDir.path() + QDir::separator() + simulationImageName);
}
void Simulation::printStates(Thing *thing)
{
qCDebug(dcSimulation()) << "Thing states for" << thing->name();
foreach (const StateType &stateType, thing->thingClass().stateTypes()) {
qCDebug(dcSimulation()) << "-->" << stateType.name() << thing->stateValue(stateType.id());
}
}
void Simulation::updateChargerMeter(Thing *thing)
{
Action updateChargerAction(thing->thingClass().actionTypes().findByName("update").id(), thing->id());
NymeaCore::instance()->thingManager()->executeAction(updateChargerAction);
}
QStringList Simulation::plotOriginalData(int powerBalanceCount)
{
QStringList scriptLines;
scriptLines.append("set title 'Original energy data'");
scriptLines.append("set grid");
scriptLines.append("set timefmt '%s'");
scriptLines.append("set xdata time");
scriptLines.append("set xtics 3600");
scriptLines.append("set format x '%H:%M'");
scriptLines.append("set xlabel 'Time'");
scriptLines.append("set ylabel '[W]'");
scriptLines.append("set style fill transparent solid 0.3");
scriptLines.append("set x2tics 100");
scriptLines.append("set xtics nomirror");
scriptLines.append("set x2label 'iterations'");
scriptLines.append("set x2range [0:" + QString::number(powerBalanceCount) + "]");
QStringList plotLines;
plotLines.append("'original.csv' using 1:3 with boxes lt rgb '#7F75C23A' title 'Production'");
plotLines.append("'original.csv' using 1:4 with boxes lt rgb '#7F3590F3' title 'Consumption'");
plotLines.append("'original.csv' using 1:5 with line lt rgb 'orange' title 'House limit'");
plotLines.append("'original.csv' using 1:2 with line lt rgb 'red' title 'Meter'");
scriptLines.append("plot \\\n" + plotLines.join(", \\\n"));
scriptLines.append("");
return scriptLines;
}
QStringList Simulation::plotSimulation(const QString &title, int powerBalanceCount)
{
QStringList scriptLines;
scriptLines.append("set title '" + title + "'");
scriptLines.append("set grid");
scriptLines.append("set timefmt '%s'");
scriptLines.append("set xdata time");
scriptLines.append("set format x '%H:%M'");
scriptLines.append("set xtics 3600");
scriptLines.append("set xlabel 'time'");
scriptLines.append("set ylabel '[W]'");
scriptLines.append("set x2tics 100");
scriptLines.append("set xtics nomirror");
scriptLines.append("set x2label 'iterations'");
scriptLines.append("set x2range [0:" + QString::number(powerBalanceCount) + "]");
scriptLines.append("set style fill transparent solid 0.3");
scriptLines.append("set y2tics 10");
scriptLines.append("set ytics nomirror");
scriptLines.append("set y2label '[\%]'");
scriptLines.append("set y2range [0:100]");
QStringList plotLines;
plotLines.append("'simulation.csv' using 1:9 title 'Charger range' w filledcurves x1 lc rgb '#fff0f0f0'");
plotLines.append("'simulation.csv' using 1:8 notitle w filledcurves x1 lc rgb '#ffffffff'");
plotLines.append("'simulation.csv' using 1:3 with boxes lt rgb '#0A75C23A' title 'Production'");
plotLines.append("'simulation.csv' using 1:4 with boxes lt rgb '#7F3590F3' title 'Consumption'");
plotLines.append("'simulation.csv' using 1:17 with boxes lt rgb '#7FA020F0' title 'Energy storage'");
plotLines.append("'simulation.csv' using 1:5 with boxes lt rgb '#7FF3DE8A' title 'Charger'");
// plotLines.append("'simulation.csv' using 1:9 with line lt rgb '#EABC01' title 'Charger max'");
// plotLines.append("'simulation.csv' using 1:8 with line lt rgb '#F6CAAF' title 'Charger min'");
plotLines.append("'simulation.csv' using 1:13 with line lt rgb 'green' title 'Acquisition Limit'");
plotLines.append("'simulation.csv' using 1:11 with line lt rgb 'black' axes x1y2 title 'Battery [\%]'");
plotLines.append("'simulation.csv' using 1:18 with line lt rgb 'purple ' axes x1y2 title 'Energy storage [\%]'");
plotLines.append("'simulation.csv' using 1:14 with line lt rgb 'purple' axes x1y2 title 'Car plugged in into charger'");
plotLines.append("'simulation.csv' using 1:10 with line lt rgb 'orange' title 'House limit'");
plotLines.append("'simulation.csv' using 1:2 with line lt rgb 'red' title 'Meter'");
scriptLines.append("plot \\\n" + plotLines.join(", \\\n"));
scriptLines.append("");
return scriptLines;
}
QStringList Simulation::plotSpotMarketData(int powerBalanceCount)
{
QStringList scriptLines;
scriptLines.append("set title 'Spot maket data'");
scriptLines.append("set grid");
scriptLines.append("set timefmt '%s'");
scriptLines.append("set xdata time");
scriptLines.append("set format x '%H:%M'");
scriptLines.append("set xtics 3600");
scriptLines.append("set xlabel 'Time'");
scriptLines.append("set ylabel 'Price [Cent/kWh]'");
scriptLines.append("set x2tics 100");
scriptLines.append("set xtics nomirror");
scriptLines.append("set x2label 'iterations'");
scriptLines.append("set x2range [0:" + QString::number(powerBalanceCount) + "]");
scriptLines.append("set y2tics");
scriptLines.append("set ytics nomirror");
scriptLines.append("set y2label '[\%]'");
scriptLines.append("set y2range [0:100]");
QStringList plotLines;
plotLines.append("'simulation.csv' using 1:15 with boxes fs solid lt rgb '#fff0f0f0' axes x1y2 title 'Spotmarket scoring [%]'");
plotLines.append("'simulation.csv' using 1:16 with line lt rgb 'black' axes x1y1 title 'Price [Cent/kWh]'");
scriptLines.append("plot \\\n" + plotLines.join(", \\\n"));
scriptLines.append("");
return scriptLines;
}
QTEST_MAIN(Simulation)