Pont moteur↔app : déclaration des charges etmvariableload persistée et éditable, construction des adaptateurs à chaud depuis la config (remplace le registre en dur). - LoadConfig / LoadConfigNeeds (Q_GADGET typés, introspectables) — forme mot pour mot du LoadDescriptor §4 (jonction inter-repos avec l'app). - LoadConfigStore : persistance atomique /var/lib/nymea/energy-load-configuration.json, validation en bloc, signal changed(). - Handler NymeaEnergy.GetLoadConfig / SetLoadConfig (+ notif LoadConfigChanged). GET typé objectRef<LoadConfig> ; SET schéma inline o: (powerLevels/needs conditionnels §4). - EnergyArbitrator : setLoadConfigStore + rebuildEtmVariableLoadAdapters (enabled==true seulement, §9) ; repli L2 = setPowerSetpoint(0) force=true sur tout etmvariableload (ferme le trou sécurité ouvert en T2). - Tests simulation : migration ECS→etmvariableload (arrondi fixed/dynamic, recrédit, délestage, round-trip powerSetpoint), budget partagé etmvariableload↔PAC, watchdog L2 réactivé, persistance + construction depuis config + injection RPC end-to-end. PAC SG-Ready reste hors config (§8, gelée). Les 5 tâches du brief sont couvertes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2713 lines
124 KiB
C++
2713 lines
124 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||
*
|
||
* Copyright (C) 2013 - 2024, nymea GmbH
|
||
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
|
||
*
|
||
* This file is part of nymea-energy-plugin-nymea.
|
||
*
|
||
* nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* nymea-energy-plugin-nymea.s distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU General Public License
|
||
* along with nymea-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
|
||
*
|
||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||
|
||
#include "simulation.h"
|
||
|
||
#include <hardware/electricity.h>
|
||
#include <servers/mocktcpserver.h>
|
||
#include <experiences/experiencemanager.h>
|
||
#include <experiences/experienceplugin.h>
|
||
|
||
using namespace nymeaserver;
|
||
|
||
#include "../../../energyplugin/smartchargingmanager.h"
|
||
#include "../../mocks/spotmarketprovider/spotmarketdataprovidermock.h"
|
||
#ifdef ETM_ARBITRATOR
|
||
#include "../../../energyplugin/etm/energyarbitrator.h"
|
||
#include "../../../energyplugin/etm/adapters/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 <QDir>
|
||
#include <QFile>
|
||
#include <QFileInfo>
|
||
|
||
#include <QHash>
|
||
#include <QtMath>
|
||
#include <QtGlobal>
|
||
#include <QProcess>
|
||
#include <QDateTime>
|
||
#include <QSignalSpy>
|
||
#include <QProcessEnvironment>
|
||
#include <QCoreApplication>
|
||
|
||
#include <nymeacore.h>
|
||
|
||
#include "simulationtestpoint.h"
|
||
|
||
void Simulation::testEcsSurplusPV()
|
||
{
|
||
#ifndef ETM_ARBITRATOR
|
||
QSKIP("testEcsSurplusPV nécessite ETM_ARBITRATOR.");
|
||
#else
|
||
// [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<EnergyArbitrator *>(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<int>({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<EnergyArbitrator *>(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<int>(), 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<EnergyArbitrator *>(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<int>({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<int>({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<int>({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<EnergyArbitrator *>(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<EnergyArbitrator *>(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 : une fixed (thing réel) + une dynamic.
|
||
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}};
|
||
resp = injectAndWait("NymeaEnergy.SetLoadConfig", {{"loadConfigs", loadConfigs}});
|
||
QCOMPARE(resp.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorNoError"));
|
||
|
||
// 3. GetLoadConfig → round-trip des 2 entrées, champs §4 préservés (via pack/unpack Q_GADGET).
|
||
resp = injectAndWait("NymeaEnergy.GetLoadConfig");
|
||
const QVariantList got = resp.toMap().value("params").toMap().value("loadConfigs").toList();
|
||
QCOMPARE(got.size(), 2);
|
||
QVariantMap fixedGot;
|
||
for (const QVariant &v : got)
|
||
if (v.toMap().value("id").toString() == ecsId.toString()) fixedGot = v.toMap();
|
||
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"));
|
||
|
||
// 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. Config invalide (fixed sans 0) → rejet en bloc, energyError InvalidParameter.
|
||
QVariantList bad;
|
||
bad << 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", bad}});
|
||
QCOMPARE(resp.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorInvalidParameter"));
|
||
// Le rejet n'a rien écrasé : GetLoadConfig retourne toujours les 2 valides.
|
||
resp = injectAndWait("NymeaEnergy.GetLoadConfig");
|
||
QCOMPARE(resp.toMap().value("params").toMap().value("loadConfigs").toList().size(), 2);
|
||
|
||
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<int, double> pacPower({ {1, 0.0}, {2, 0.0}, {3, 1500.0}, {4, 3000.0} });
|
||
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||
|
||
// ===================== Volets 1-3 : PAC seule =====================
|
||
cleanupTestCase();
|
||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||
initTestCase();
|
||
|
||
EnergyArbitrator *arbitrator = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||
QVERIFY2(arbitrator, "smartChargingManager n'est pas un EnergyArbitrator");
|
||
ThingManager *tm = NymeaCore::instance()->thingManager();
|
||
|
||
QUuid meterId = addMeter();
|
||
m_experiencePlugin->energyManager()->setRootMeter(meterId);
|
||
Thing *meter = tm->findConfiguredThing(meterId);
|
||
QVERIFY(meter);
|
||
meter->setStateValue("connected", true);
|
||
|
||
QUuid k1 = addPowerSwitch(0, 26661);
|
||
QUuid k2 = addPowerSwitch(0, 26662);
|
||
Thing *relayK1 = tm->findConfiguredThing(k1);
|
||
Thing *relayK2 = tm->findConfiguredThing(k2);
|
||
QVERIFY(relayK1 && relayK2);
|
||
|
||
SgReadyAdapter *pac = new SgReadyAdapter(
|
||
tm, "pac-test", "PAC test",
|
||
QHash<int, QList<QString>>({ {1, {k1.toString()}}, {2, {}},
|
||
{3, {k2.toString()}}, {4, {k1.toString(), k2.toString()}} }),
|
||
pacPower, 300, 1, arbitrator);
|
||
arbitrator->registerSgReadyAdapter(pac);
|
||
|
||
auto setMeterW = [&](double signedW){ meter->setStateValue("currentPower", signedW); }; // <0 export
|
||
auto cycle = [&](const QDateTime &now){ arbitrator->simulationCallUpdate(now); QCoreApplication::processEvents(); };
|
||
|
||
// --- Volet 1 : montée d'états 2 → 3 → 4 (mapping sémantique) ---
|
||
setMeterW(-1000); cycle(t0); // budget 1000 < P3 → état 2 (normal)
|
||
QCOMPARE(pac->currentState(), 2);
|
||
setMeterW(-2000); cycle(t0); // budget 2000 ≥ P3 → état 3 (reco)
|
||
QCOMPARE(pac->currentState(), 3);
|
||
QCOMPARE(relayK2->stateValue("power").toBool(), true);
|
||
QCOMPARE(relayK1->stateValue("power").toBool(), false);
|
||
setMeterW(-2500); cycle(t0.addSecs(400)); // budget 2500+1500=4000 ≥ P4×1,2 → état 4 (hold écoulé)
|
||
QCOMPARE(pac->currentState(), 4);
|
||
QCOMPARE(relayK1->stateValue("power").toBool(), true);
|
||
QCOMPARE(relayK2->stateValue("power").toBool(), true);
|
||
|
||
// --- Volet 2 : hystérésis 3↔4 (budget oscille dans la zone morte [P4×1,0 ; P4×1,2)) ---
|
||
// hold écoulé à chaque cycle (lastSwitch=T0+400) → c'est la ZONE MORTE qui tient l'état 4, pas le verrou.
|
||
setMeterW(-300); cycle(t0.addSecs(800)); // budget 300+3000=3300 ∈ [3000,3600) → reste 4
|
||
QCOMPARE(pac->currentState(), 4);
|
||
setMeterW(-100); cycle(t0.addSecs(1200)); // budget 3100 → reste 4
|
||
QCOMPARE(pac->currentState(), 4);
|
||
setMeterW(-500); cycle(t0.addSecs(1600)); // budget 3500 → reste 4
|
||
QCOMPARE(pac->currentState(), 4);
|
||
// En-dessous de P4×1,0 → sort enfin de l'état 4 (vers 3).
|
||
setMeterW(200); cycle(t0.addSecs(2000)); // import 200 → budget -200+3000=2800 < 3000 → état 3
|
||
QCOMPARE(pac->currentState(), 3);
|
||
|
||
// --- Volet 3 : protection court-cycling (changement avant minStateHold → GELÉ) ---
|
||
// lastSwitch=T0+2000. À T0+2100 (elapsed 100 < hold 300) : surplus abondant mais GELÉ en 3.
|
||
setMeterW(-3000); cycle(t0.addSecs(2100));
|
||
QCOMPARE(pac->currentState(), 3); // gelé malgré budget ≥ P4×1,2 (protection compresseur)
|
||
// À T0+2400 (elapsed 400 > hold) : MÊME surplus → bascule en 4. Seul le temps simulé a changé.
|
||
setMeterW(-3000); cycle(t0.addSecs(2400));
|
||
QCOMPARE(pac->currentState(), 4);
|
||
|
||
// ===================== Volet 4 : 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<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||
QVERIFY(arb);
|
||
ThingManager *tm2 = NymeaCore::instance()->thingManager();
|
||
|
||
QUuid mId = addMeter();
|
||
m_experiencePlugin->energyManager()->setRootMeter(mId);
|
||
Thing *m2 = tm2->findConfiguredThing(mId);
|
||
QVERIFY(m2);
|
||
m2->setStateValue("connected", true);
|
||
|
||
QUuid 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<int>({0, 2400}), 2400, ecsPrio, LoadNeeds(), arb);
|
||
arb->registerEtmVariableLoadAdapter(ecsWf);
|
||
|
||
SgReadyAdapter *pacWf = new SgReadyAdapter(
|
||
tm2, "pac-wf", "PAC waterfall",
|
||
QHash<int, QList<QString>>({ {1, {j1.toString()}}, {2, {}},
|
||
{3, {j2.toString()}}, {4, {j1.toString(), j2.toString()}} }),
|
||
pacPower, 300, pacPrio, arb);
|
||
arb->registerSgReadyAdapter(pacWf);
|
||
|
||
m2->setStateValue("currentPower", -3000); // export 3000 W
|
||
arb->simulationCallUpdate(t0);
|
||
QCoreApplication::processEvents();
|
||
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<QString>("simulationName");
|
||
QTest::addColumn<QString>("simulationTitle");
|
||
QTest::addColumn<QString>("databaseName");
|
||
QTest::addColumn<QDateTime>("simulationStart");
|
||
QTest::addColumn<ChargerPlugEvents>("plugEvents");
|
||
QTest::addColumn<int>("simulationHours");
|
||
QTest::addColumn<EnergyLogs::SampleRate>("sampleRate");
|
||
QTest::addColumn<double>("productionScaling");
|
||
QTest::addColumn<int>("detailsStepStart");
|
||
QTest::addColumn<int>("detailsStepStop");
|
||
QTest::addColumn<DetailsStepList>("detailsStepList");
|
||
|
||
// Houshold info
|
||
QTest::addColumn<int>("phasePowerLimit");
|
||
QTest::addColumn<bool>("spotMarketEnabled");
|
||
QTest::addColumn<QString>("spotMarketResourceData");
|
||
QTest::addColumn<double>("acquisitionTolerance");
|
||
QTest::addColumn<double>("batteryLevelConsideration");
|
||
|
||
// ChargingInfo
|
||
QTest::addColumn<double>("targetPercentage");
|
||
QTest::addColumn<QDateTime>("targetDateTime");
|
||
QTest::addColumn<QString>("chargingMode");
|
||
QTest::addColumn<int>("carBatteryLevel");
|
||
QTest::addColumn<int>("dailySpotMarketPercentage");
|
||
|
||
// Car information and states
|
||
QTest::addColumn<int>("carCapacity");
|
||
QTest::addColumn<int>("carMinChargingCurrent");
|
||
QTest::addColumn<int>("carPhaseCount");
|
||
|
||
// Energy storage information and states
|
||
QTest::addColumn<bool>("energyStorageAvailable");
|
||
QTest::addColumn<int>("energyStorageCapacity");
|
||
QTest::addColumn<double>("energyStorageMaxChargingPower");
|
||
QTest::addColumn<double>("energyStorageMaxDischargingPower");
|
||
QTest::addColumn<double>("energyStorageInitialBatteyLevel");
|
||
|
||
// Charger initial states
|
||
QTest::addColumn<bool>("chargerConnected");
|
||
QTest::addColumn<bool>("chargerPower");
|
||
QTest::addColumn<QString>("chargerPhases");
|
||
QTest::addColumn<bool>("canSwitchPhaseCount");
|
||
QTest::addColumn<int>("chargerMaxChargingCurrent");
|
||
QTest::addColumn<int>("chargerMaxChargingCurrentMaxValue");
|
||
|
||
QTest::addColumn<SimulationIterationTest>("iterationTest");
|
||
|
||
bool runAllSimulations = true;
|
||
|
||
bool runSpotmarketSimulation = runAllSimulations;
|
||
bool run1PhaseSimulations = runAllSimulations;
|
||
bool run2PhaseSimulations = runAllSimulations;
|
||
bool run3PhaseSimulations = runAllSimulations;
|
||
bool runPhaseSwitchingSimulations = runAllSimulations;
|
||
|
||
// Simulations
|
||
if (runSpotmarketSimulation)
|
||
QTest::newRow("Spotmarket only")
|
||
|
||
/* Simulation info */
|
||
|
||
<< "simulation-spotmarket-only-1-phase-16A" // simulationName
|
||
<< "Simulation (1 phase, charger 16A max, only spot market)" // simulationTitle
|
||
<< ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC)
|
||
|
||
<< ChargerPlugEvents( { ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(7,0,0)), false),
|
||
ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(17,30,0)), true),
|
||
ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(7,0,0)), false),
|
||
ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(17,0,0)), true)
|
||
}) // pluggedInTime (UTC)
|
||
|
||
<< 48 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 0.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList() // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< true // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEco" // chargingMode
|
||
<< 20 // carBatteryLevel
|
||
<< 20 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 1 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< false // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 16 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 20)
|
||
}
|
||
},
|
||
{
|
||
250, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
}
|
||
},
|
||
{
|
||
500, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
}
|
||
},
|
||
{
|
||
1440, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40) // Should have charged 20% in one day
|
||
}
|
||
},
|
||
{
|
||
2800, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
}
|
||
},
|
||
{
|
||
2880, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 60) // Should have charged 20% in one day
|
||
}
|
||
}
|
||
} );
|
||
|
||
if (runSpotmarketSimulation)
|
||
QTest::newRow("Spotmarket and PV")
|
||
|
||
/* Simulation info */
|
||
|
||
<< "simulation-spotmarket-and-pv-1-phase-16A" // simulationName
|
||
<< "Simulation (1 phase, charger 16A max, spot market and PV)" // simulationTitle
|
||
<< ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC)
|
||
<< ChargerPlugEvents()
|
||
|
||
<< 48 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 0.35 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList() // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< true // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEco" // chargingMode
|
||
<< 20 // carBatteryLevel
|
||
<< 20 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 1 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< false // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 16 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 20)
|
||
}
|
||
},
|
||
{
|
||
250, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
500, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
580, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
750, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
780, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
810, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
1400, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
2000, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 46) // Should be at least 20% more than the day before...
|
||
}
|
||
},
|
||
{
|
||
2250, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
}
|
||
},
|
||
{
|
||
2750, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
}
|
||
}
|
||
} );
|
||
|
||
if (runSpotmarketSimulation)
|
||
QTest::newRow("Spotmarket with target time")
|
||
|
||
/* Simulation info */
|
||
|
||
<< "simulation-spotmarket-only-with-targettime-1-phase-16A" // simulationName
|
||
<< "Simulation (1 phase, charger 16A max, only spot market with target time)" // simulationTitle
|
||
<< ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC)
|
||
<< ChargerPlugEvents() // Car plug events
|
||
|
||
<< 48 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 0.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList() // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< true // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEcoWithTargetTime" // chargingMode
|
||
<< 20 // carBatteryLevel
|
||
<< 0 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 1 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< false // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 16 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 20)
|
||
}
|
||
},
|
||
{
|
||
700, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
1200, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
1400, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
1550, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
1700, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
2760, { // 22:00
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
|
||
}
|
||
}
|
||
} );
|
||
|
||
|
||
if (runSpotmarketSimulation)
|
||
QTest::newRow("Spotmarket only with target time")
|
||
|
||
/* Simulation info */
|
||
|
||
<< "simulation-spotmarket-only-with-targettime-1-day-1-phase-16A" // simulationName
|
||
<< "Simulation (1 phase, charger 16A max, only spot market with target time single day)" // simulationTitle
|
||
<< ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0, 0, 0)) // simulationStart (UTC)
|
||
<< ChargerPlugEvents()
|
||
|
||
<< 32 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 0.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList() // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< true // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 15), QTime(07, 0, 0)) // targetDateTime
|
||
<< "ChargingModeEcoWithTargetTime" // chargingMode
|
||
<< 50 // carBatteryLevel
|
||
<< 0 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 1 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< false // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 16 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 50)
|
||
}
|
||
},
|
||
{
|
||
250, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 61)
|
||
}
|
||
},
|
||
{
|
||
500, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
}
|
||
},
|
||
{
|
||
1400, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
}
|
||
}
|
||
});
|
||
|
||
if (run1PhaseSimulations)
|
||
QTest::newRow("Default")
|
||
/* Simulation info */
|
||
<< "simulation-1-phase-32A" // simulationName
|
||
<< "Simulation (1 phase, charger 32A max, target 22:00 100%" // simulationTitle
|
||
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC)
|
||
<< ChargerPlugEvents() // pluggedInTime (UTC)
|
||
<< 18 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 20.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList() // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< false // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEcoWithTargetTime" // chargingMode
|
||
<< 40 // carBatteryLevel
|
||
<< 0 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 1 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< false // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 32 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
|
||
}
|
||
},
|
||
{
|
||
163, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
170, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
444, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 7) ,
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
520, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6) ,
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 80)
|
||
}
|
||
},
|
||
{
|
||
700, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6) ,
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 85)
|
||
}
|
||
},
|
||
{
|
||
900, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 30) ,
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 88)
|
||
}
|
||
},
|
||
{
|
||
960, { // 22:00
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 30) ,
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
|
||
}
|
||
}
|
||
} );
|
||
|
||
if (run1PhaseSimulations)
|
||
QTest::newRow("Default")
|
||
/* Simulation info */
|
||
<< "simulation-1-phase-16A" // simulationName
|
||
<< "Simulation (1 phase, charger 16A max, target 22:00 100%)" // simulationTitle
|
||
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(0,0,0)) // simulationStart (UTC)
|
||
<< ChargerPlugEvents( { ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)), true) }) // pluggedInTime (UTC)
|
||
<< 24 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 20.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList() // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< false // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEcoWithTargetTime" // chargingMode
|
||
<< 50 // carBatteryLevel
|
||
<< 0 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 1 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< false // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 16 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 50)
|
||
}
|
||
},
|
||
{
|
||
452, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 50)
|
||
}
|
||
},
|
||
{
|
||
467, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 51)
|
||
}
|
||
},
|
||
{
|
||
750, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
830, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
841, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
1000, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
1300, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
1440, { // 22:00
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
|
||
}
|
||
}
|
||
} );
|
||
|
||
if (run2PhaseSimulations)
|
||
QTest::newRow("Kostal")
|
||
/* Simulation info */
|
||
<< "simulation-kostal-2-phase-16A" // simulationName
|
||
<< "Simulation (2 phase, charger 16A max, target 22:00 100%)" // simulationTitle
|
||
<< ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC)
|
||
<< ChargerPlugEvents( { ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(8,0,0)), true) }) // pluggedInTime (UTC)
|
||
<< 24 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 1.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList() // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< false // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEcoWithTargetTime" // chargingMode
|
||
<< 40 // carBatteryLevel
|
||
<< 0 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 2 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< false // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 16 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
|
||
}
|
||
},
|
||
{
|
||
550, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
600, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 48)
|
||
}
|
||
},
|
||
{
|
||
820, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
823, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
847, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 80)
|
||
}
|
||
},
|
||
{
|
||
1050, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
|
||
}
|
||
}
|
||
} );
|
||
|
||
if (run2PhaseSimulations)
|
||
QTest::newRow("Kostal")
|
||
/* Simulation info */
|
||
<< "simulation-kostal-2-phase-16A-away-2-hours" // simulationName
|
||
<< "Simulation (2 phase, charger 16A max, target 22:00 100%)" // simulationTitle
|
||
<< ":/databases/2022-08-12-kostal-energylogs.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(0,0,0)) // simulationStart (UTC)
|
||
|
||
<< ChargerPlugEvents( {
|
||
ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(12,0,0)), false),
|
||
ChargerPlugEvent(EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(14,00,0)), true, 10)
|
||
}) // pluggedInTime (UTC)
|
||
<< 24 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 1.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList() // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< false // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 8, 14), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEcoWithTargetTime" // chargingMode
|
||
<< 40 // carBatteryLevel
|
||
<< 0 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 2 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< false // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 16 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
|
||
}
|
||
},
|
||
{
|
||
550, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
600, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 48)
|
||
}
|
||
},
|
||
{
|
||
1300, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
1440, { // 22:00
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
|
||
}
|
||
}
|
||
} );
|
||
|
||
if (run2PhaseSimulations)
|
||
QTest::newRow("Default")
|
||
/* Simulation info */
|
||
<< "simulation-2-phase-16A" // simulationName
|
||
<< "Simulation (2 phase, charger 16A max, target 22:00 100%)" // simulationTitle
|
||
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC)
|
||
<< ChargerPlugEvents() // pluggedInTime (UTC)
|
||
<< 18 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 20.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList() // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< false // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEcoWithTargetTime" // chargingMode
|
||
<< 40 // carBatteryLevel
|
||
<< 0 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 2 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< false // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 16 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
|
||
}
|
||
},
|
||
{
|
||
850, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
900, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
960, { // 22:00
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
|
||
}
|
||
}
|
||
} );
|
||
|
||
if (run2PhaseSimulations)
|
||
QTest::newRow("Default")
|
||
/* Simulation info */
|
||
<< "simulation-2-phase-32A" // simulationName
|
||
<< "Simulation (2 phase, charger 32A max, target 22:00 100%)" // simulationTitle
|
||
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC)
|
||
<< ChargerPlugEvents() // pluggedInTime (UTC)
|
||
<< 18 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 20.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList() // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< false // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEcoWithTargetTime" // chargingMode
|
||
<< 40 // carBatteryLevel
|
||
<< 0 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 2 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< false // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 32 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
|
||
}
|
||
},
|
||
{
|
||
100, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
300, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 61)
|
||
}
|
||
},
|
||
{
|
||
470, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
586, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
800, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
950, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 30),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 99)
|
||
}
|
||
},
|
||
{
|
||
960, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
|
||
}
|
||
}
|
||
} );
|
||
|
||
if (run3PhaseSimulations)
|
||
QTest::newRow("Default")
|
||
/* Simulation info */
|
||
<< "simulation-3-phase-16A" // simulationName
|
||
<< "Simulation (3 phase, charger 16A max, target 22:00 100%)" // simulationTitle
|
||
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC)
|
||
<< ChargerPlugEvents() // pluggedInTime (UTC)
|
||
<< 18 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 40.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList({200, 300, 400, 500}) // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< false // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEcoWithTargetTime" // chargingMode
|
||
<< 40 // carBatteryLevel
|
||
<< 0 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 3 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< false // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 16 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
|
||
}
|
||
},
|
||
{
|
||
200, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 11),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 57)
|
||
}
|
||
},
|
||
{
|
||
300, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 13),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 83)
|
||
}
|
||
},
|
||
{
|
||
400, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 11),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
|
||
}
|
||
},
|
||
{
|
||
700, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
|
||
}
|
||
}
|
||
} );
|
||
|
||
|
||
if (run3PhaseSimulations)
|
||
QTest::newRow("Default")
|
||
/* Simulation info */
|
||
<< "simulation-3-phase-32A" // simulationName
|
||
<< "Simulation (3 phase, charger 32A max, target 22:00 100%)" // simulationTitle
|
||
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC)
|
||
<< ChargerPlugEvents() // pluggedInTime (UTC)
|
||
<< 18 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 20.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList({ 480, 500, 600, 950, 960 }) // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< false // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEcoWithTargetTime" // chargingMode
|
||
<< 40 // carBatteryLevel
|
||
<< 0 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 3 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< false // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 32 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
100, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
|
||
}
|
||
},
|
||
{
|
||
200, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
300, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
400, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true)
|
||
}
|
||
},
|
||
{
|
||
480, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
500, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 82)
|
||
}
|
||
},
|
||
{
|
||
600, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false)
|
||
}
|
||
},
|
||
{
|
||
950, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 98) // Finish 10 min early
|
||
}
|
||
},
|
||
{
|
||
960, { // 22:00
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
|
||
}
|
||
}
|
||
} );
|
||
|
||
if (runPhaseSwitchingSimulations)
|
||
QTest::newRow("Default")
|
||
/* Simulation info */
|
||
<< "simulation-phase-switching-16A" // simulationName
|
||
<< "Simulation (phase switching, charger 16A max, surplus only)" // simulationTitle
|
||
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(6,0,0)) // simulationStart (UTC)
|
||
<< ChargerPlugEvents() // pluggedInTime (UTC)
|
||
<< 18 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 45.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 0 // detailsStepStop
|
||
<< DetailsStepList({80, 150, 310, 400, 470}) // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< false // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEco" // chargingMode
|
||
<< 40 // carBatteryLevel
|
||
<< 0 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 3 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< false // energyStorageAvailable
|
||
<< 0 // energyStorageCapacity
|
||
<< 0.0 // energyStorageMaxChargingPower
|
||
<< 0.0 // energyStorageMaxDischargingPower
|
||
<< 50.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< true // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 16 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
|
||
}
|
||
},
|
||
{
|
||
80, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
|
||
}
|
||
},
|
||
{
|
||
150, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 10),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 50)
|
||
}
|
||
},
|
||
{
|
||
310, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 16),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 91)
|
||
}
|
||
},
|
||
{
|
||
400, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 13),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
|
||
}
|
||
},
|
||
{
|
||
470, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 9),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, true),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 100)
|
||
}
|
||
},
|
||
} );
|
||
|
||
if (runPhaseSwitchingSimulations)
|
||
QTest::newRow("Default")
|
||
/* Simulation info */
|
||
<< "simulation-energy-storage-phase-switching" // simulationName
|
||
<< "Simulation energy storage, phase switching" // simulationTitle
|
||
<< ":/databases/2022-06-28-energylogs-micha.sqlite" // databaseName
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(0,0,0)) // simulationStart (UTC)
|
||
<< ChargerPlugEvents() // pluggedInTime (UTC)
|
||
<< 36 // simulationHours
|
||
<< EnergyLogs::SampleRate1Min
|
||
<< 45.0 // productionScaling
|
||
<< 0 // detailsStepStart
|
||
<< 10 // detailsStepStop
|
||
<< DetailsStepList({}) // detailsStepList
|
||
|
||
/* Houshold info */
|
||
<< 32 // phase limit (A)
|
||
<< false // spotMarketEnabled
|
||
<< ":/resources/dataset-1.json" // spotMarketResourceData
|
||
<< 0.5 // acquisitionTolerance
|
||
<< 0.9 // batteryLevelConsideration
|
||
|
||
/* Charging Info */
|
||
<< 100.0 // targetPercentage
|
||
<< EnergyTestBase::utcDateTime(QDate(2022, 6, 27), QTime(22,0,0)) // targetDateTime
|
||
<< "ChargingModeEco" // chargingMode
|
||
<< 40 // carBatteryLevel
|
||
<< 0 // dailySpotMarketPercentage
|
||
|
||
/* Car settings */
|
||
<< 50 // carCapacity
|
||
<< 6 // carMinChargingCurrent
|
||
<< 3 // carPhaseCount
|
||
|
||
/* Energy storage */
|
||
<< true // energyStorageAvailable
|
||
<< 12 // energyStorageCapacity
|
||
<< 5000.0 // energyStorageMaxChargingPower
|
||
<< 5000.0 // energyStorageMaxDischargingPower
|
||
<< 10.0 // energyStorageInitialBatteyLevel
|
||
|
||
<< true // chargerConnected
|
||
<< false // chargerPower
|
||
<< "ABC" // chargerPhases
|
||
<< true // canSwitchPhaseCount
|
||
<< 6 // chargerMaxChargingCurrent
|
||
<< 16 //chargerMaxChargingCurrentMaxValue
|
||
|
||
<< SimulationIterationTest ( {
|
||
{
|
||
0, {
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeMaxChargingCurrent, 6),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeCharging, false),
|
||
SimulationTestPoint(SimulationTestPoint::TestTypeStateOfCharge, 40)
|
||
}
|
||
}
|
||
} );
|
||
}
|
||
|
||
void Simulation::run()
|
||
{
|
||
QFETCH(QString, simulationName);
|
||
QFETCH(QString, simulationTitle);
|
||
QFETCH(QString, databaseName);
|
||
QFETCH(QDateTime, simulationStart);
|
||
QFETCH(ChargerPlugEvents, plugEvents);
|
||
QFETCH(int, simulationHours);
|
||
QFETCH(EnergyLogs::SampleRate, sampleRate);
|
||
QFETCH(double, productionScaling);
|
||
QFETCH(int, detailsStepStart);
|
||
QFETCH(int, detailsStepStop);
|
||
QFETCH(DetailsStepList, detailsStepList);
|
||
|
||
QFETCH(int, phasePowerLimit);
|
||
QFETCH(bool, spotMarketEnabled);
|
||
QFETCH(QString, spotMarketResourceData);
|
||
QFETCH(double, acquisitionTolerance);
|
||
QFETCH(double, batteryLevelConsideration);
|
||
|
||
QFETCH(double, targetPercentage);
|
||
QFETCH(QDateTime, targetDateTime);
|
||
QFETCH(QString, chargingMode);
|
||
QFETCH(int, carBatteryLevel);
|
||
QFETCH(int, dailySpotMarketPercentage);
|
||
|
||
QFETCH(int, carCapacity);
|
||
QFETCH(int, carMinChargingCurrent);
|
||
QFETCH(int, carPhaseCount);
|
||
|
||
QFETCH(bool, energyStorageAvailable);
|
||
QFETCH(int, energyStorageCapacity);
|
||
QFETCH(double, energyStorageMaxChargingPower);
|
||
QFETCH(double, energyStorageMaxDischargingPower);
|
||
QFETCH(double, energyStorageInitialBatteyLevel);
|
||
|
||
QFETCH(bool, chargerConnected);
|
||
QFETCH(bool, chargerPower);
|
||
QFETCH(QString, chargerPhases);
|
||
QFETCH(bool, canSwitchPhaseCount);
|
||
QFETCH(int, chargerMaxChargingCurrent);
|
||
QFETCH(int, chargerMaxChargingCurrentMaxValue);
|
||
|
||
QFETCH(SimulationIterationTest, iterationTest);
|
||
|
||
|
||
QStringList loggingDefaultList = {
|
||
"*.debug=false",
|
||
"Application.debug=true",
|
||
"LogEngine.info=false",
|
||
"Simulation.debug=true",
|
||
"Experiences.debug=false",
|
||
"NymeaEnergy.debug=false",
|
||
"EnergyMocks.debug=false",
|
||
"DBus.warning=false",
|
||
};
|
||
QString loggingRulesDefault = loggingDefaultList.join("\n");
|
||
|
||
QStringList loggingDetailsList = {
|
||
"*.debug=false",
|
||
"Application.debug=true",
|
||
"LogEngine.info=false",
|
||
"Simulation.debug=true",
|
||
"Experiences.debug=false",
|
||
"NymeaEnergy.debug=true",
|
||
"EnergyMocks.debug=false",
|
||
"DBus.warning=false",
|
||
};
|
||
QString loggingRulesDetails = loggingDetailsList.join("\n");
|
||
|
||
QStringList availableChargingModes;
|
||
availableChargingModes << "ChargingModeNormal";
|
||
availableChargingModes << "ChargingModeEco";
|
||
availableChargingModes << "ChargingModeEcoWithTargetTime";
|
||
QVERIFY2(availableChargingModes.contains(chargingMode), "Unknown charging mode passed to the simulation. Please compair the list with the ChargingMode enum.");
|
||
|
||
if (canSwitchPhaseCount)
|
||
QVERIFY2(chargerPhases == "ABC", "If the charger supports phase count switching all 3 phases must be connected.");
|
||
|
||
cleanupTestCase();
|
||
m_energyLogDbFilePath = databaseName;
|
||
initTestCase(loggingRulesDefault);
|
||
|
||
// Print simulation init details
|
||
QLoggingCategory::setFilterRules(loggingRulesDefault);
|
||
|
||
QVariant response; QVariantMap params;
|
||
QNetworkReply *reply = nullptr;
|
||
QSignalSpy packetSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
||
|
||
Electricity::Phases chargerPhasesConverted = Electricity::convertPhasesFromString(chargerPhases);
|
||
|
||
// Set phase power limit
|
||
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", phasePowerLimit}}));
|
||
QVERIFY(response.toMap().value("params").toMap().value("energyError").toString() == "EnergyErrorNoError");
|
||
|
||
// Add mock spotmarket provider
|
||
SpotMarketManager *spotMarketManager = m_experiencePlugin->spotMarketManager();
|
||
SpotMarketDataProviderMock *mockProvider = new SpotMarketDataProviderMock(nullptr, this);
|
||
QVERIFY(mockProvider->prepareResourceData(spotMarketResourceData, simulationStart.toUTC()));
|
||
QVERIFY(spotMarketManager->registerProvider(mockProvider));
|
||
QVERIFY(spotMarketManager->changeProvider(mockProvider->providerId()));
|
||
|
||
// Enabke/disable spot market
|
||
response = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", QVariantMap({ {"enabled", spotMarketEnabled }, {"providerId", mockProvider->providerId()} }));
|
||
QCOMPARE(response.toMap().value("params").toMap().value("energyError").toString(), "EnergyErrorNoError");
|
||
QCOMPARE(m_experiencePlugin->spotMarketManager()->enabled(), spotMarketEnabled);
|
||
|
||
// Set initial acquisition tolerance
|
||
params.clear(); response.clear();
|
||
params.insert("acquisitionTolerance", acquisitionTolerance);
|
||
response = injectAndWait("NymeaEnergy.SetAcquisitionTolerance", params);
|
||
verifyEnergyError(response);
|
||
|
||
// Set battery level consideration
|
||
params.clear(); response.clear();
|
||
params.insert("batteryLevelConsideration", batteryLevelConsideration);
|
||
response = injectAndWait("NymeaEnergy.SetBatteryLevelConsideration", params);
|
||
verifyEnergyError(response);
|
||
|
||
// Add mock meter
|
||
QUuid meterThingId = addMeter();
|
||
QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
|
||
if (packetSpy.count() == 0) packetSpy.wait();
|
||
checkNotification(packetSpy, "Integrations.ThingAdded");
|
||
packetSpy.clear();
|
||
|
||
// Set it as root meter
|
||
m_experiencePlugin->energyManager()->setRootMeter(meterThingId);
|
||
|
||
// Make sure this is our root meter now
|
||
response = injectAndWait("Energy.GetRootMeter");
|
||
QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
||
packetSpy.clear();
|
||
|
||
// Add the charger
|
||
QUuid evChargerId;
|
||
if (canSwitchPhaseCount) {
|
||
evChargerId = addChargerWithPhaseCountSwitching(chargerPhases, chargerMaxChargingCurrentMaxValue);
|
||
} else {
|
||
evChargerId = addCharger(chargerPhases, chargerMaxChargingCurrentMaxValue);
|
||
}
|
||
|
||
QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId");
|
||
if (packetSpy.count() == 0) packetSpy.wait();
|
||
checkNotification(packetSpy, "Integrations.ThingAdded");
|
||
|
||
// Add the car
|
||
QUuid carThingId = addCar();
|
||
QVERIFY2(!carThingId.isNull(), "Did not receive valid ThingId");
|
||
if (packetSpy.count() == 0) packetSpy.wait();
|
||
checkNotification(packetSpy, "Integrations.ThingAdded");
|
||
|
||
// Add energy storage if available
|
||
QUuid energyStorageThingId;
|
||
if (energyStorageAvailable) {
|
||
energyStorageThingId = addEnergyStorage(energyStorageCapacity, energyStorageMaxChargingPower, energyStorageMaxDischargingPower);
|
||
QVERIFY2(!energyStorageThingId.isNull(), "Did not receive valid ThingId");
|
||
if (packetSpy.count() == 0) packetSpy.wait();
|
||
checkNotification(packetSpy, "Integrations.ThingAdded");
|
||
}
|
||
|
||
// ==============================================================================
|
||
// Set states of car and charger
|
||
|
||
Thing *carThing = NymeaCore::instance()->thingManager()->findConfiguredThing(carThingId);
|
||
QVERIFY2(carThing != nullptr, "Failed to find car thing");
|
||
carThing->setSettingValue(carThing->thingClass().settingsTypes().findByName("phaseCount").id(), carPhaseCount);
|
||
carThing->setSettingValue(carThing->thingClass().settingsTypes().findByName("capacity").id(), carCapacity);
|
||
carThing->setSettingValue(carThing->thingClass().settingsTypes().findByName("minChargingCurrent").id(), carMinChargingCurrent);
|
||
carThing->setStateValue("batteryLevel", carBatteryLevel);
|
||
|
||
Thing *chargerThing = NymeaCore::instance()->thingManager()->findConfiguredThing(evChargerId);
|
||
QVERIFY2(chargerThing != nullptr, "Failed to find charger thing");
|
||
chargerThing->setStateValue("connected", chargerConnected);
|
||
chargerThing->setStateValue("power", chargerPower);
|
||
chargerThing->setStateValue("maxChargingCurrent", chargerMaxChargingCurrent);
|
||
chargerThing->setStateValue("maxChargingCurrentMaxValue", chargerMaxChargingCurrentMaxValue);
|
||
chargerThing->setStateValue("pluggedIn", true); // Initially always plugged in, the rest can be handeld using the plug events
|
||
|
||
// This will update all internal states not set directly
|
||
updateChargerMeter(chargerThing);
|
||
|
||
Thing *meterThing = NymeaCore::instance()->thingManager()->findConfiguredThing(meterThingId);
|
||
QVERIFY2(meterThing != nullptr, "Failed to find meter thing");
|
||
meterThing->setStateValue("connected", true);
|
||
|
||
// Energy storage states
|
||
Thing *energyStorageThing = nullptr;
|
||
if (energyStorageAvailable) {
|
||
energyStorageThing = NymeaCore::instance()->thingManager()->findConfiguredThing(energyStorageThingId);
|
||
energyStorageThing->setStateValue("currentPower", 0);
|
||
energyStorageThing->setStateValue("batteryLevel", energyStorageInitialBatteyLevel);
|
||
energyStorageThing->setProperty("preciseBatteryLevel", energyStorageInitialBatteyLevel * 1.0); // For precise runtime calculations
|
||
printStates(energyStorageThing);
|
||
}
|
||
|
||
// printStates(chargerThing);
|
||
// printStates(carThing);
|
||
// printStates(meterThing);
|
||
|
||
// Set charging info with our charger and car, this should trigger the evaluation
|
||
QVariantMap chargingInfoMap;
|
||
chargingInfoMap.insert("evChargerId", evChargerId);
|
||
chargingInfoMap.insert("assignedCarId", carThingId);
|
||
chargingInfoMap.insert("chargingMode", chargingMode);
|
||
chargingInfoMap.insert("endDateTime", targetDateTime.toMSecsSinceEpoch() / 1000);
|
||
chargingInfoMap.insert("targetPercentage", targetPercentage);
|
||
chargingInfoMap.insert("spotMarketChargingEnabled", spotMarketEnabled);
|
||
chargingInfoMap.insert("dailySpotMarketPercentage", dailySpotMarketPercentage);
|
||
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", chargingInfoMap}}));
|
||
QCOMPARE(response.toMap().value("status").toString(), QString("success"));
|
||
QCOMPARE(response.toMap().value("params").toMap().value("energyError").toString(), QString("EnergyErrorNoError"));
|
||
|
||
uint effectivePhaseCount;
|
||
if (canSwitchPhaseCount) {
|
||
effectivePhaseCount = chargerThing->stateValue("phaseCount").toUInt();
|
||
} else {
|
||
effectivePhaseCount = qMin((uint)carPhaseCount, Electricity::getPhaseCount(chargerPhasesConverted));
|
||
}
|
||
|
||
QString usedPhases;
|
||
if (effectivePhaseCount >= 1)
|
||
usedPhases.append("A");
|
||
|
||
if (effectivePhaseCount >= 2)
|
||
usedPhases.append("B");
|
||
|
||
if (effectivePhaseCount >= 3)
|
||
usedPhases.append("C");
|
||
|
||
chargerThing->setStateValue("usedPhases", usedPhases);
|
||
|
||
|
||
// Note: we want to have the limit negative, so we see better where the limit is and why the chaging stopped
|
||
double aquisitionToleranceLimit = -(effectivePhaseCount * carMinChargingCurrent * 230.0 * acquisitionTolerance);
|
||
|
||
// Simulation output
|
||
QDir simulationBaseDir = QDir(QDir::currentPath() + QDir::separator() + "simulations");
|
||
|
||
QDir workspaceDir = QDir(simulationBaseDir.absolutePath() + QDir::separator() + "workspace");
|
||
if (!workspaceDir.exists()) {
|
||
//QVERIFY2(outputDir.removeRecursively(), "Failed to cleanup output dir");
|
||
QVERIFY2(workspaceDir.mkpath(workspaceDir.path()), "Failed to create results dir");
|
||
}
|
||
|
||
QDir outputDir = QDir(workspaceDir.absolutePath() + QDir::separator() + simulationName);
|
||
if (!outputDir.exists()) {
|
||
// QVERIFY2(outputDir.removeRecursively(), "Failed to cleanup output dir");
|
||
QVERIFY2(outputDir.mkpath(outputDir.path()), "Failed to create output dir");
|
||
}
|
||
|
||
QDir resultsDir = QDir(simulationBaseDir.absolutePath() + QDir::separator() + "results");
|
||
if (!resultsDir.exists()) {
|
||
//QVERIFY2(outputDir.removeRecursively(), "Failed to cleanup output dir");
|
||
QVERIFY2(resultsDir.mkpath(resultsDir.path()), "Failed to create results dir");
|
||
}
|
||
|
||
QFile gnuplotLogFile(outputDir.path() + QDir::separator() + "simulation.csv");
|
||
QVERIFY2(gnuplotLogFile.open(QIODevice::ReadWrite | QIODevice::Truncate), QString("Failed to open logfile for gnuplot" + gnuplotLogFile.fileName() + ": " + gnuplotLogFile.errorString()).toUtf8());
|
||
QTextStream gnuplotLogFileStream(&gnuplotLogFile);
|
||
|
||
QFile gnuplotOriginalLogFile(outputDir.path() + QDir::separator() + "original.csv");
|
||
QVERIFY2(gnuplotOriginalLogFile.open(QIODevice::ReadWrite | QIODevice::Truncate), "Failed to open logfile for gnuplot");
|
||
QTextStream gnuplotOriginalLogFileStream(&gnuplotOriginalLogFile);
|
||
|
||
|
||
// ==============================================================================
|
||
// All set up, lets start simulating
|
||
|
||
QDateTime simulationEnd = simulationStart.addSecs(simulationHours * 3600);
|
||
|
||
PowerBalanceLogEntries powerBalanceLogs = m_experiencePlugin->energyManager()->logs()->powerBalanceLogs(sampleRate, simulationStart, simulationEnd);
|
||
qCDebug(dcSimulation()) << "Simulation start" << simulationStart;
|
||
qCDebug(dcSimulation()) << "Simulation end" << simulationEnd;
|
||
qCDebug(dcSimulation()) << "Loaded" << powerBalanceLogs.count() << "log entries for that day";
|
||
|
||
// Simulation helpers
|
||
int simulationProgress = 0;
|
||
|
||
// Disable init details
|
||
QLoggingCategory::setFilterRules(loggingRulesDefault);
|
||
|
||
// Run the simulation
|
||
for (int i = 0; i < powerBalanceLogs.count(); i++) {
|
||
|
||
bool debugEnabled = (i >= detailsStepStart && i <= detailsStepStop) || detailsStepList.contains(i);
|
||
const PowerBalanceLogEntry entry = powerBalanceLogs.at(i);
|
||
QDateTime currentDateTime = entry.timestamp().toUTC();
|
||
|
||
// Calculate progress
|
||
if (debugEnabled) {
|
||
qCDebug(dcSimulation()) << "###############################################################################################################";
|
||
qCDebug(dcSimulation()) << "Step" << i << ":" << currentDateTime.toUTC().toString("yyyy.MM.dd hh:mm");
|
||
}
|
||
|
||
effectivePhaseCount = chargerThing->stateValue("phaseCount").toUInt();
|
||
usedPhases = chargerThing->stateValue("usedPhases").toString();
|
||
|
||
// Update mocked spotmarket data provider
|
||
mockProvider->setCurrentDataTime(currentDateTime.toUTC());
|
||
|
||
// Print simulation progress
|
||
double simulationProgressPrecise = i * 100 / powerBalanceLogs.count();
|
||
if (simulationProgress != qRound(simulationProgressPrecise)) {
|
||
simulationProgress = qRound(simulationProgressPrecise);
|
||
if (simulationProgress % 10 == 0) {
|
||
qCDebug(dcSimulation()) << simulationName << currentDateTime.toUTC().toString("hh:mm") << simulationProgress << "% (" << i << ")";
|
||
}
|
||
}
|
||
|
||
// Enable logs in the interesting simulation steps
|
||
if (debugEnabled) {
|
||
QLoggingCategory::setFilterRules(loggingRulesDetails);
|
||
} else {
|
||
QLoggingCategory::setFilterRules(loggingRulesDefault);
|
||
}
|
||
|
||
// Get the current situation befor running the simulation step
|
||
double totalProduction = entry.production() * productionScaling;
|
||
double totalProductionDifference = totalProduction - entry.production();
|
||
double scaledCurrentPower = entry.acquisition() + totalProductionDifference;
|
||
//qCDebug(dcSimulation()) << "Scale production using" << productionScaling << entry.production() << "-->" << totalProduction << totalProductionDifference;
|
||
//qCDebug(dcSimulation()) << "Scale aquisition using" << entry.acquisition() << "-->" << scaledCurrentPower;
|
||
|
||
// Distribute the load before battery and charger on 3 phases
|
||
double phaseLoad = scaledCurrentPower / 3;
|
||
|
||
// -------------------- Charger
|
||
|
||
// Handle plug events
|
||
foreach(const ChargerPlugEvent &plugEvent, plugEvents) {
|
||
if (plugEvent.dateTime.date() == currentDateTime.toUTC().date() &&
|
||
plugEvent.dateTime.time().hour() == currentDateTime.toUTC().time().hour() &&
|
||
plugEvent.dateTime.time().minute() == currentDateTime.toUTC().time().minute()) {
|
||
|
||
// Plug event
|
||
chargerThing->setStateValue("pluggedIn", plugEvent.pluggedIn);
|
||
if (plugEvent.percentageUsed != 0) {
|
||
double batteryLevel = carThing->stateValue("batteryLevel").toDouble();
|
||
batteryLevel -= plugEvent.percentageUsed;
|
||
if (batteryLevel < 0) {
|
||
batteryLevel = 0;
|
||
}
|
||
qCDebug(dcSimulation()) << "-->" << currentDateTime.toUTC().toString("hh:mm") << "New car battery level" << batteryLevel << "(old:" << carThing->stateValue("batteryLevel").toDouble() << ")";
|
||
carThing->setStateValue("batteryLevel", batteryLevel);
|
||
}
|
||
qCDebug(dcSimulation()) << "-->" << currentDateTime.toUTC().toString("hh:mm") << "Car has been" << (plugEvent.pluggedIn ? "plugged in" : "unplugged");
|
||
}
|
||
}
|
||
|
||
// Let the charger set all power, voltage etc....
|
||
updateChargerMeter(chargerThing);
|
||
double chargerCurrentPower = chargerThing->stateValue("currentPower").toDouble();
|
||
|
||
double totalConsumption = chargerCurrentPower + entry.consumption();
|
||
|
||
// Totals before battery
|
||
double totalCurrentPower = totalConsumption + totalProduction;
|
||
|
||
// All producers and all consumers have been summed up, charge / discharge the battery and create a final total balance
|
||
|
||
// -------------------- Energy storage
|
||
|
||
double energyStorageCurrentPower = 0;
|
||
uint energyStorageBatteryLevel = 0;
|
||
|
||
// All consumers should have what they get, put the rest into or from the storage within limits
|
||
|
||
if (energyStorageAvailable) {
|
||
|
||
// Calculate the new battery level depending on the previouse step.
|
||
|
||
double esCurrentPower = energyStorageThing->stateValue("currentPower").toDouble();
|
||
double esBatteryLevel = energyStorageThing->property("preciseBatteryLevel").toDouble();
|
||
double esCapacity = energyStorageThing->stateValue("capacity").toDouble();
|
||
|
||
// Let's caclulate the new percentage depending on the rate of the last minute...
|
||
|
||
// We charged/discharged the last minute with energyStorageCurrentPower W
|
||
double energyChargedDischargedkWh = esCurrentPower * 60 / 60 / 60 / 1000;
|
||
double addedPercentage = energyChargedDischargedkWh * 100.0 / esCapacity;
|
||
double newBatteryLevel = esBatteryLevel += addedPercentage;
|
||
|
||
if (totalCurrentPower < 0 && newBatteryLevel < 100) {
|
||
energyStorageCurrentPower = qMin(energyStorageMaxChargingPower, -totalCurrentPower);
|
||
energyStorageBatteryLevel = newBatteryLevel;
|
||
energyStorageThing->setProperty("preciseBatteryLevel", newBatteryLevel);
|
||
setEnergyStorageStates(energyStorageBatteryLevel, energyStorageCurrentPower);
|
||
} else if (totalCurrentPower > 0 && newBatteryLevel > 0) {
|
||
energyStorageCurrentPower = - qMin(energyStorageMaxDischargingPower, totalCurrentPower);
|
||
energyStorageBatteryLevel = newBatteryLevel;
|
||
energyStorageThing->setProperty("preciseBatteryLevel", newBatteryLevel);
|
||
setEnergyStorageStates(energyStorageBatteryLevel, energyStorageCurrentPower);
|
||
} else {
|
||
energyStorageBatteryLevel = newBatteryLevel;
|
||
energyStorageThing->setProperty("preciseBatteryLevel", newBatteryLevel);
|
||
setEnergyStorageStates(energyStorageBatteryLevel, energyStorageCurrentPower);
|
||
}
|
||
|
||
//qCDebug(dcSimulation()) << "Energy storage charged with" << energyStorageCurrentPower << "W" << addedPercentage << "% added to total" << energyStorageBatteryLevel << "%";
|
||
}
|
||
|
||
// -------------------- Meter
|
||
|
||
totalCurrentPower += energyStorageCurrentPower;
|
||
phaseLoad += energyStorageCurrentPower / 3;
|
||
|
||
// Add the charger power in the aproperiate phase
|
||
QVariantMap phases = QVariantMap({ {"A", phaseLoad + chargerThing->stateValue("currentPowerPhaseA").toDouble()},
|
||
{"B", phaseLoad + chargerThing->stateValue("currentPowerPhaseB").toDouble()},
|
||
{"C", phaseLoad + chargerThing->stateValue("currentPowerPhaseC").toDouble()} });
|
||
|
||
reply = setMeterStates(phases, true);
|
||
QSignalSpy setMeterStatesReplySpy(reply, &QNetworkReply::finished);
|
||
if (setMeterStatesReplySpy.count() == 0) setMeterStatesReplySpy.wait();
|
||
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
||
|
||
// -------------------- Run charging manager update
|
||
|
||
// Set charger information and pass them to the logic
|
||
ThingPowerLogEntry chargerPowerEntry(currentDateTime.toUTC(), chargerThing->id(), chargerThing->stateValue("currentPower").toDouble(), 0, 0);
|
||
m_experiencePlugin->smartChargingManager()->simulationCallUpdateManualSoCsWithMeter(sampleRate, chargerPowerEntry);
|
||
|
||
// Update smart charging manager with the current root meter and charger situation
|
||
m_experiencePlugin->smartChargingManager()->simulationCallUpdate(currentDateTime.toUTC());
|
||
|
||
// Fetch information after simulation iteration
|
||
double carBatteryPercentage = carThing->stateValue("batteryLevel").toDouble();
|
||
int maxChargingCurrent = chargerThing->stateValue("maxChargingCurrent").toInt();
|
||
chargerPower = chargerThing->stateValue("power").toBool();
|
||
chargerCurrentPower = chargerThing->stateValue("currentPower").toDouble();
|
||
|
||
|
||
if (debugEnabled | iterationTest.contains(i)) {
|
||
qCDebug(dcSimulation()) << "Step" << i;
|
||
qCDebug(dcSimulation()) << "- Total power:" << totalCurrentPower << "Production:" << totalProduction << "Consumption:" << totalConsumption;
|
||
if (energyStorageAvailable) {
|
||
qCDebug(dcSimulation()) << "- Energy storage:" << energyStorageCurrentPower << energyStorageThing->property("preciseBatteryLevel").toDouble() << "%";
|
||
}
|
||
qCDebug(dcSimulation()) << "- Meter phases: A:" << meterThing->stateValue("currentPowerPhaseA").toDouble() << "W | B:"
|
||
<< meterThing->stateValue("currentPowerPhaseB").toDouble() << "W | C:" << meterThing->stateValue("currentPowerPhaseC").toDouble() << "W";
|
||
qCDebug(dcSimulation()) << "- Charger:" << chargerCurrentPower << "[W] (" << maxChargingCurrent << "[A]" << (chargerCurrentPower ? "On)" : "Off )") << effectivePhaseCount << usedPhases;
|
||
qCDebug(dcSimulation()) << "- Charger phases: A:" << chargerThing->stateValue("currentPowerPhaseA").toDouble() << "W | B:"
|
||
<< chargerThing->stateValue("currentPowerPhaseB").toDouble() << "W | C:" << chargerThing->stateValue("currentPowerPhaseC").toDouble() << "W";
|
||
qCDebug(dcSimulation()) << "- Car battery:" << carBatteryPercentage;
|
||
qCDebug(dcSimulation()) << "--------------------------------";
|
||
|
||
// printStates(meterThing);
|
||
// printStates(chargerThing);
|
||
// printStates(carThing);
|
||
|
||
}
|
||
|
||
// Verify test points
|
||
foreach(const SimulationTestPoint &testPoint, iterationTest.value(i)) {
|
||
switch(testPoint.testType()) {
|
||
case SimulationTestPoint::TestTypeCharging:
|
||
QVERIFY2(chargerPower == testPoint.expectedValue().toBool(),
|
||
qPrintable(QString("Simulation: %1 - %2 Step: %3 expected \"%4\" from the testpoint but is actually \"%5\"")
|
||
.arg(simulationName)
|
||
.arg(simulationTitle)
|
||
.arg(i)
|
||
.arg(testPoint.expectedValue().toBool() ? "true" : "false")
|
||
.arg(chargerPower ? "true" : "false")));
|
||
|
||
break;
|
||
case SimulationTestPoint::TestTypeMaxChargingCurrent:
|
||
QVERIFY2(maxChargingCurrent == testPoint.expectedValue().toInt(),
|
||
qPrintable(QString("Simulation: %1 - %2 Step: %3 expected \"%4\" from the testpoint but is actually \"%5\"")
|
||
.arg(simulationName)
|
||
.arg(simulationTitle)
|
||
.arg(i)
|
||
.arg(testPoint.expectedValue().toInt())
|
||
.arg(maxChargingCurrent)));
|
||
|
||
break;
|
||
case SimulationTestPoint::TestTypeStateOfCharge:
|
||
QVERIFY2(qFuzzyCompare(carBatteryPercentage, testPoint.expectedValue().toDouble()),
|
||
qPrintable(QString("Simulation: %1 - %2 Step: %3 expected \"%4\" from the testpoint but is actually \"%5\"")
|
||
.arg(simulationName)
|
||
.arg(simulationTitle)
|
||
.arg(i)
|
||
.arg(testPoint.expectedValue().toDouble())
|
||
.arg(carBatteryPercentage)));
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
// -------------------- Data logging
|
||
|
||
// Log the simulation data
|
||
gnuplotLogFileStream << currentDateTime.toMSecsSinceEpoch() / 1000 << ", " << // 1
|
||
totalCurrentPower << ", " << // 2
|
||
totalProduction << ", " << // 3
|
||
totalConsumption << ", " << // 4
|
||
chargerCurrentPower << ", " << // 5
|
||
maxChargingCurrent << ", " << // 6
|
||
(chargerPower ? "1" : "0") << ", " << // 7
|
||
chargerThing->state("maxChargingCurrent").minValue().toDouble() * 230 * effectivePhaseCount << ", " << // 8
|
||
chargerThing->state("maxChargingCurrent").maxValue().toDouble() * 230 * effectivePhaseCount << ", " << // 9
|
||
phasePowerLimit * 230 * effectivePhaseCount << ", " << // 10
|
||
carBatteryPercentage << ", " << // 11
|
||
i << ", " << // 12
|
||
aquisitionToleranceLimit << ", " << // 13
|
||
(chargerThing->stateValue("pluggedIn").toBool() ? 10 : 0 ) << ", "; // 14
|
||
if (spotMarketEnabled) {
|
||
const ScoreEntries weightedEntries = spotMarketManager->weightedScoreEntries(currentDateTime.date());
|
||
const ScoreEntry currentScore = weightedEntries.getScoreEntry(currentDateTime.toUTC());
|
||
QVERIFY(!currentScore.isNull());
|
||
gnuplotLogFileStream << currentScore.weighting() * 100 << ", "; // 15
|
||
gnuplotLogFileStream << currentScore.value() / 10.0 << ", "; // 16 Price
|
||
} else {
|
||
gnuplotLogFileStream << 0 << ", "; // 15
|
||
gnuplotLogFileStream << 0 << ", "; // 16
|
||
}
|
||
if (energyStorageAvailable) {
|
||
gnuplotLogFileStream << energyStorageCurrentPower << ", "; // 17
|
||
gnuplotLogFileStream << energyStorageBatteryLevel << ", "; // 18
|
||
} else {
|
||
gnuplotLogFileStream << 0 << ", "; // 17
|
||
gnuplotLogFileStream << 0 << ", "; // 18
|
||
}
|
||
gnuplotLogFileStream << "\n";
|
||
|
||
|
||
|
||
// Log Unchanged for raw data analysis
|
||
gnuplotOriginalLogFileStream << currentDateTime.toMSecsSinceEpoch() / 1000 << ", " << // 1
|
||
scaledCurrentPower << ", " << // 2
|
||
totalProduction << ", " << // 3
|
||
entry.consumption() << ", " << // 4
|
||
phasePowerLimit * 230 * effectivePhaseCount << ", " << // 5
|
||
i << ", " << // 6
|
||
"\n";
|
||
}
|
||
|
||
gnuplotLogFile.close();
|
||
gnuplotOriginalLogFile.close();
|
||
|
||
// Draw original data
|
||
QStringList scriptLines;
|
||
QStringList plotLines;
|
||
|
||
// Plot with: 1h = 200 px
|
||
int height = 800;
|
||
int width = simulationHours * 200;
|
||
|
||
QString originalImageName = simulationName + "-00-original.png";
|
||
QString simulationImageName = simulationName + "-01.png";
|
||
|
||
scriptLines.append("set term png size " + QString::number(width) + "," + QString::number(height));
|
||
scriptLines.append("set output '" + originalImageName + "'");
|
||
scriptLines.append("set datafile separator ','");
|
||
scriptLines.append(plotOriginalData(powerBalanceLogs.count()));
|
||
|
||
if (spotMarketEnabled) {
|
||
scriptLines.append("set term png size " + QString::number(width) + "," + QString::number(height * 2));
|
||
scriptLines.append("set output '" + simulationImageName + "'");
|
||
scriptLines.append("set datafile separator ','");
|
||
|
||
scriptLines.append("set multiplot layout 2,1");
|
||
|
||
scriptLines.append("set size 1,0.8");
|
||
scriptLines.append("set origin 0,0.2");
|
||
scriptLines.append(plotSimulation(simulationTitle, powerBalanceLogs.count()));
|
||
|
||
scriptLines.append("set size 1,0.2");
|
||
scriptLines.append("set origin 0,0");
|
||
scriptLines.append(plotSpotMarketData(powerBalanceLogs.count()));
|
||
|
||
scriptLines.append("unset multiplot");
|
||
|
||
} else {
|
||
scriptLines.append("set term png size " + QString::number(width) + "," + QString::number(height));
|
||
scriptLines.append("set output '" + simulationImageName + "'");
|
||
scriptLines.append("set datafile separator ','");
|
||
scriptLines.append(plotSimulation(simulationTitle, powerBalanceLogs.count()));
|
||
}
|
||
|
||
// Write the gnuplot script
|
||
QFile gnuplotScript(outputDir.path() + QDir::separator() + "script.gnuplot");
|
||
QVERIFY2(gnuplotScript.open(QIODevice::ReadWrite | QIODevice::Truncate),
|
||
QString("Failed to open script file for gnuplot" + gnuplotScript.fileName() + ": " + gnuplotScript.errorString()).toUtf8());
|
||
QTextStream scriptStream(&gnuplotScript);
|
||
foreach (const QString &line, scriptLines)
|
||
scriptStream << line << "\n";
|
||
|
||
gnuplotScript.close();
|
||
|
||
|
||
// Write the executable gnuplot script
|
||
|
||
scriptLines.clear();
|
||
|
||
scriptLines.append("set terminal wxt 1 persist");
|
||
scriptLines.append("set datafile separator ','");
|
||
|
||
if (spotMarketEnabled) {
|
||
|
||
scriptLines.append("set multiplot layout 2,1");
|
||
|
||
scriptLines.append("set size 1,0.8");
|
||
scriptLines.append("set origin 0,0.2");
|
||
scriptLines.append(plotSimulation(simulationTitle, powerBalanceLogs.count()));
|
||
|
||
scriptLines.append("set size 1,0.2");
|
||
scriptLines.append("set origin 0,0");
|
||
scriptLines.append(plotSpotMarketData(powerBalanceLogs.count()));
|
||
|
||
scriptLines.append("unset multiplot");
|
||
|
||
} else {
|
||
scriptLines.append(plotSimulation(simulationTitle, powerBalanceLogs.count()));
|
||
}
|
||
|
||
QString executableScriptName = simulationName + ".gnuplot";
|
||
QFile executableGnuplotScript(outputDir.path() + QDir::separator() + executableScriptName);
|
||
QVERIFY2(executableGnuplotScript.open(QIODevice::ReadWrite | QIODevice::Truncate),
|
||
QString("Failed to open logfile for gnuplot" + executableGnuplotScript.fileName() + ": " + executableGnuplotScript.errorString()).toUtf8());
|
||
QTextStream executableScriptStream(&executableGnuplotScript);
|
||
foreach (const QString &line, scriptLines)
|
||
executableScriptStream << line << "\n";
|
||
|
||
executableGnuplotScript.close();
|
||
|
||
|
||
|
||
QProcess gnuplotProcess;
|
||
//gnuplotProcess.setEnvironment(QProcessEnvironment::systemEnvironment().toStringList());
|
||
gnuplotProcess.setProcessChannelMode(QProcess::MergedChannels);
|
||
gnuplotProcess.setWorkingDirectory(outputDir.path());
|
||
gnuplotProcess.start("gnuplot", { "-c", "script.gnuplot"});
|
||
gnuplotProcess.waitForFinished();
|
||
qCDebug(dcSimulation()) << "gnuplot finished" << gnuplotProcess.arguments() << gnuplotProcess.workingDirectory() << gnuplotProcess.exitCode() << gnuplotProcess.exitStatus();
|
||
if (gnuplotProcess.exitCode() != 0) {
|
||
qCDebug(dcSimulation()) << "error plotting data:\n" << qUtf8Printable(gnuplotProcess.readAll());
|
||
QVERIFY2(false, "plot process finished with error");
|
||
}
|
||
|
||
// Copy resulting images to the simulations
|
||
QFile::copy(outputDir.path() + QDir::separator() + originalImageName, resultsDir.path() + QDir::separator() + originalImageName);
|
||
QFile::copy(outputDir.path() + QDir::separator() + simulationImageName, resultsDir.path() + QDir::separator() + simulationImageName);
|
||
}
|
||
|
||
void Simulation::printStates(Thing *thing)
|
||
{
|
||
qCDebug(dcSimulation()) << "Thing states for" << thing->name();
|
||
foreach (const StateType &stateType, thing->thingClass().stateTypes()) {
|
||
qCDebug(dcSimulation()) << "-->" << stateType.name() << thing->stateValue(stateType.id());
|
||
}
|
||
}
|
||
|
||
void Simulation::updateChargerMeter(Thing *thing)
|
||
{
|
||
Action updateChargerAction(thing->thingClass().actionTypes().findByName("update").id(), thing->id());
|
||
NymeaCore::instance()->thingManager()->executeAction(updateChargerAction);
|
||
}
|
||
|
||
QStringList Simulation::plotOriginalData(int powerBalanceCount)
|
||
{
|
||
QStringList scriptLines;
|
||
scriptLines.append("set title 'Original energy data'");
|
||
scriptLines.append("set grid");
|
||
|
||
scriptLines.append("set timefmt '%s'");
|
||
scriptLines.append("set xdata time");
|
||
scriptLines.append("set xtics 3600");
|
||
scriptLines.append("set format x '%H:%M'");
|
||
|
||
scriptLines.append("set xlabel 'Time'");
|
||
scriptLines.append("set ylabel '[W]'");
|
||
|
||
scriptLines.append("set style fill transparent solid 0.3");
|
||
|
||
scriptLines.append("set x2tics 100");
|
||
scriptLines.append("set xtics nomirror");
|
||
scriptLines.append("set x2label 'iterations'");
|
||
scriptLines.append("set x2range [0:" + QString::number(powerBalanceCount) + "]");
|
||
|
||
QStringList plotLines;
|
||
plotLines.append("'original.csv' using 1:3 with boxes lt rgb '#7F75C23A' title 'Production'");
|
||
plotLines.append("'original.csv' using 1:4 with boxes lt rgb '#7F3590F3' title 'Consumption'");
|
||
plotLines.append("'original.csv' using 1:5 with line lt rgb 'orange' title 'House limit'");
|
||
plotLines.append("'original.csv' using 1:2 with line lt rgb 'red' title 'Meter'");
|
||
scriptLines.append("plot \\\n" + plotLines.join(", \\\n"));
|
||
scriptLines.append("");
|
||
return scriptLines;
|
||
}
|
||
|
||
QStringList Simulation::plotSimulation(const QString &title, int powerBalanceCount)
|
||
{
|
||
QStringList scriptLines;
|
||
scriptLines.append("set title '" + title + "'");
|
||
scriptLines.append("set grid");
|
||
|
||
scriptLines.append("set timefmt '%s'");
|
||
scriptLines.append("set xdata time");
|
||
scriptLines.append("set format x '%H:%M'");
|
||
scriptLines.append("set xtics 3600");
|
||
|
||
scriptLines.append("set xlabel 'time'");
|
||
scriptLines.append("set ylabel '[W]'");
|
||
|
||
scriptLines.append("set x2tics 100");
|
||
scriptLines.append("set xtics nomirror");
|
||
scriptLines.append("set x2label 'iterations'");
|
||
scriptLines.append("set x2range [0:" + QString::number(powerBalanceCount) + "]");
|
||
|
||
scriptLines.append("set style fill transparent solid 0.3");
|
||
|
||
scriptLines.append("set y2tics 10");
|
||
scriptLines.append("set ytics nomirror");
|
||
scriptLines.append("set y2label '[\%]'");
|
||
scriptLines.append("set y2range [0:100]");
|
||
|
||
QStringList plotLines;
|
||
plotLines.append("'simulation.csv' using 1:9 title 'Charger range' w filledcurves x1 lc rgb '#fff0f0f0'");
|
||
plotLines.append("'simulation.csv' using 1:8 notitle w filledcurves x1 lc rgb '#ffffffff'");
|
||
|
||
plotLines.append("'simulation.csv' using 1:3 with boxes lt rgb '#0A75C23A' title 'Production'");
|
||
plotLines.append("'simulation.csv' using 1:4 with boxes lt rgb '#7F3590F3' title 'Consumption'");
|
||
plotLines.append("'simulation.csv' using 1:17 with boxes lt rgb '#7FA020F0' title 'Energy storage'");
|
||
plotLines.append("'simulation.csv' using 1:5 with boxes lt rgb '#7FF3DE8A' title 'Charger'");
|
||
|
||
// plotLines.append("'simulation.csv' using 1:9 with line lt rgb '#EABC01' title 'Charger max'");
|
||
// plotLines.append("'simulation.csv' using 1:8 with line lt rgb '#F6CAAF' title 'Charger min'");
|
||
|
||
plotLines.append("'simulation.csv' using 1:13 with line lt rgb 'green' title 'Acquisition Limit'");
|
||
plotLines.append("'simulation.csv' using 1:11 with line lt rgb 'black' axes x1y2 title 'Battery [\%]'");
|
||
plotLines.append("'simulation.csv' using 1:18 with line lt rgb 'purple ' axes x1y2 title 'Energy storage [\%]'");
|
||
plotLines.append("'simulation.csv' using 1:14 with line lt rgb 'purple' axes x1y2 title 'Car plugged in into charger'");
|
||
plotLines.append("'simulation.csv' using 1:10 with line lt rgb 'orange' title 'House limit'");
|
||
plotLines.append("'simulation.csv' using 1:2 with line lt rgb 'red' title 'Meter'");
|
||
|
||
scriptLines.append("plot \\\n" + plotLines.join(", \\\n"));
|
||
scriptLines.append("");
|
||
return scriptLines;
|
||
}
|
||
|
||
QStringList Simulation::plotSpotMarketData(int powerBalanceCount)
|
||
{
|
||
QStringList scriptLines;
|
||
scriptLines.append("set title 'Spot maket data'");
|
||
scriptLines.append("set grid");
|
||
|
||
scriptLines.append("set timefmt '%s'");
|
||
scriptLines.append("set xdata time");
|
||
scriptLines.append("set format x '%H:%M'");
|
||
scriptLines.append("set xtics 3600");
|
||
|
||
scriptLines.append("set xlabel 'Time'");
|
||
scriptLines.append("set ylabel 'Price [Cent/kWh]'");
|
||
scriptLines.append("set x2tics 100");
|
||
scriptLines.append("set xtics nomirror");
|
||
scriptLines.append("set x2label 'iterations'");
|
||
scriptLines.append("set x2range [0:" + QString::number(powerBalanceCount) + "]");
|
||
|
||
scriptLines.append("set y2tics");
|
||
scriptLines.append("set ytics nomirror");
|
||
scriptLines.append("set y2label '[\%]'");
|
||
scriptLines.append("set y2range [0:100]");
|
||
|
||
QStringList plotLines;
|
||
plotLines.append("'simulation.csv' using 1:15 with boxes fs solid lt rgb '#fff0f0f0' axes x1y2 title 'Spotmarket scoring [%]'");
|
||
plotLines.append("'simulation.csv' using 1:16 with line lt rgb 'black' axes x1y1 title 'Price [Cent/kWh]'");
|
||
scriptLines.append("plot \\\n" + plotLines.join(", \\\n"));
|
||
scriptLines.append("");
|
||
return scriptLines;
|
||
}
|
||
|
||
|
||
QTEST_MAIN(Simulation)
|