counts est publié par niveau : { destination → watts }, Σ counts == targetW
toujours. Clés opaques et jamais recyclées, publié même à une seule destination
et même à zéro — un counts vide satisferait l'identité en perdant la destination,
sur le cas le plus fréquent.
L'identité ne passe plus par funding : elle somme des COMPTEURS, plus des
origines. EV_GRID_START y entre sans exception. funding et counts ne se dérivent
pas l'un de l'autre — l'un dit d'où viennent les watts, l'autre où ils sont
comptés.
LA GARANTIE EST UNE MACHINE. Sept sites construisent une LoadAction ; vérifier
qu'ils renseignent tous counts en RELISANT le code se referait à chaque site
ajouté. testCountsSumToTarget relit la charge utile — toutes les charges, trois
régimes dont l'import où tout vaut 0 — et constate le résultat.
ET LA CONTRE-ÉPREUVE A TROUVÉ UN TROU DANS LE TEST LUI-MÊME. Amputer le partage
d'EV_GRID_START le laissait PASSER : la borne, auto-provisionnée en queue par
LM-1209, ne voyait jamais de budget, et le régime n'était pas exercé. Corrigé —
rang 1, et EV_GRID_START en PREMIER régime, parce qu'une fois la borne en charge
son recrédit porte le budget bien au-dessus du plancher et le démarrage sous
tolérance ne peut plus se produire. Plus une assertion qui EXIGE que le régime
ait été exercé, sinon le test ne prouve rien du partage.
Deux fois dans ce lot, la contre-épreuve a corrigé le test plutôt que le code :
la première fois elle avait échoué au MONTAGE (NYMEA_PLUGINS_PATH omis) et ne
prouvait rien non plus. Un garde-fou qu'on n'a pas vu échouer POUR LA BONNE
RAISON ne vaut pas mieux qu'un garde-fou qu'on n'a pas vu échouer.
GetChargingInfos filtre sur evChargerId. Le paramètre était déclaré au schéma,
documenté « for all or a single EV charger », et Q_UNUSED : accepté, sans effet,
sans erreur. Des trois issues c'était la pire — un paramètre absent se voit, un
paramètre refusé se voit, un paramètre ignoré ne se voit pas. Identifiant inconnu
→ liste vide, jamais une erreur, et la liste vide est non ambiguë puisque toute
borne configurée a une entrée.
Balayage du namespace : sur 20 méthodes, 9 déclarent des paramètres et
GetChargingInfos était la seule à en ignorer un. Mon premier balayage avait
conclu « aucune méthode ne déclare de paramètre » — regex fausse ; refait en
croisant, pour chaque méthode, les params.insert de sa déclaration avec les
params.value de son corps.
Le garde-fou doxygen de ci-quality a par ailleurs attrapé ma propre insertion :
le bloc CountKey s'était glissé entre le commentaire de LoadAction et la
structure, qui perdait sa documentation.
simulation 34/34, charging 17/17, loadmodel 20/20, spotmarket 7/7, amd64 0/0,
doxygen 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2174 lines
88 KiB
C++
2174 lines
88 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 "testcharging.h"
|
|
|
|
#include <hardware/electricity.h>
|
|
|
|
TestCharging::TestCharging(QObject *parent):
|
|
EnergyTestBase(parent)
|
|
{
|
|
|
|
}
|
|
|
|
void TestCharging::testAddCharger()
|
|
{
|
|
// Add
|
|
QUuid chargerThingId = addCharger("ABC", 16);
|
|
QVERIFY2(!chargerThingId.isNull(), "Did not receive valid ThingId");
|
|
|
|
// Remove
|
|
QVariant response = removeDevice(chargerThingId);
|
|
verifyThingError(response);
|
|
}
|
|
|
|
void TestCharging::testAddSimpleCharger()
|
|
{
|
|
// Add
|
|
QUuid chargerThingId = addSimpleCharger(20);
|
|
QVERIFY2(!chargerThingId.isNull(), "Did not receive valid ThingId");
|
|
|
|
// Remove
|
|
QVariant response = removeDevice(chargerThingId);
|
|
verifyThingError(response);
|
|
}
|
|
|
|
void TestCharging::testAddCar()
|
|
{
|
|
// Add
|
|
QUuid carThingId = addCar();
|
|
QVERIFY2(!carThingId.isNull(), "Did not receive valid ThingId");
|
|
|
|
// Remove
|
|
QVariant response = removeDevice(carThingId);
|
|
verifyThingError(response);
|
|
}
|
|
|
|
void TestCharging::testAddMeter()
|
|
{
|
|
QVariant response, notification;
|
|
|
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
|
|
|
// Make sure there is no root meter
|
|
response = injectAndWait("Energy.GetRootMeter");
|
|
QVERIFY(response.toMap().value("params").toMap().isEmpty());
|
|
QVERIFY(response.toMap().value("status").toString() == "success");
|
|
|
|
// Add mock meter
|
|
QUuid meterThingId = addMeter();
|
|
QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
|
|
|
|
// Check notification Energy.RootMeterChanged containing our added meter thing id
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "Energy.RootMeterChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
|
|
// Make sure this is our root meter now
|
|
response = injectAndWait("Energy.GetRootMeter");
|
|
QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
|
|
// Undo all steps
|
|
|
|
// Remove meter
|
|
notificationSpy.clear();
|
|
response = removeDevice(meterThingId);
|
|
qDebug() << response.toMap();
|
|
|
|
// Check notification Energy.RootMeterChanged containing our added meter thing id
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "Energy.RootMeterChanged");
|
|
QVERIFY(notification.toMap().value("params").toMap().isEmpty());
|
|
|
|
// Make sure there is no root meter
|
|
response = injectAndWait("Energy.GetRootMeter");
|
|
QVERIFY(response.toMap().value("params").toMap().isEmpty());
|
|
QVERIFY(response.toMap().value("status").toString() == "success");
|
|
}
|
|
|
|
void TestCharging::getSetPhaseLimits()
|
|
{
|
|
QVariant response, notification;
|
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
|
|
|
uint phasePowerLimit = 0;
|
|
uint newPhasePowerLimit = 30;
|
|
|
|
response = injectAndWait("NymeaEnergy.GetPhasePowerLimit");
|
|
phasePowerLimit = response.toMap().value("params").toMap().value("phasePowerLimit").toUInt();
|
|
// Initially 0 since we have no things at all set up
|
|
if (phasePowerLimit == newPhasePowerLimit) {
|
|
newPhasePowerLimit++;
|
|
}
|
|
|
|
notificationSpy.clear();
|
|
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", newPhasePowerLimit}}));
|
|
QVERIFY(response.toMap().value("params").toMap().value("energyError").toString() == "EnergyErrorNoError");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "NymeaEnergy.PhasePowerLimitChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("phasePowerLimit").toUInt(), newPhasePowerLimit);
|
|
|
|
response = injectAndWait("NymeaEnergy.GetPhasePowerLimit");
|
|
phasePowerLimit = response.toMap().value("params").toMap().value("phasePowerLimit").toUInt();
|
|
QCOMPARE(phasePowerLimit, newPhasePowerLimit);
|
|
|
|
|
|
notificationSpy.clear();
|
|
newPhasePowerLimit = 30000;
|
|
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", newPhasePowerLimit}}));
|
|
QVERIFY(response.toMap().value("params").toMap().value("energyError").toString() == "EnergyErrorNoError");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "NymeaEnergy.PhasePowerLimitChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("phasePowerLimit").toUInt(), newPhasePowerLimit);
|
|
|
|
response = injectAndWait("NymeaEnergy.GetPhasePowerLimit");
|
|
phasePowerLimit = response.toMap().value("params").toMap().value("phasePowerLimit").toUInt();
|
|
QCOMPARE(phasePowerLimit, newPhasePowerLimit);
|
|
}
|
|
|
|
|
|
void TestCharging::getSetLockOnUnplug_data()
|
|
{
|
|
QTest::addColumn<bool>("lockOnUnplug");
|
|
|
|
QTest::newRow("Enable") << true;
|
|
QTest::newRow("Disable") << false;
|
|
}
|
|
|
|
void TestCharging::getSetLockOnUnplug()
|
|
{
|
|
QFETCH(bool, lockOnUnplug);
|
|
|
|
QVariantMap params;
|
|
QVariant response, notification;
|
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
|
|
|
// Set initial lockOnUnplug to oposit, so we get the changed notification for sure
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
params.insert("lockOnUnplug", !lockOnUnplug);
|
|
response = injectAndWait("NymeaEnergy.SetLockOnUnplug", params);
|
|
verifyEnergyError(response);
|
|
|
|
// Set actual lockOnUnplug
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
params.insert("lockOnUnplug", lockOnUnplug);
|
|
response = injectAndWait("NymeaEnergy.SetLockOnUnplug", params);
|
|
verifyEnergyError(response);
|
|
|
|
// If the value has changed, verify the notification
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "NymeaEnergy.LockOnUnplugChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("lockOnUnplug").toDouble(), lockOnUnplug);
|
|
|
|
// Get acquisition tolerance
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("NymeaEnergy.GetLockOnUnplug", params);
|
|
QVERIFY2(response.toMap().value("params").toMap().contains("lockOnUnplug"), "Did not get aquisition tolerance.");
|
|
QCOMPARE(response.toMap().value("params").toMap().value("lockOnUnplug").toBool(), lockOnUnplug);
|
|
}
|
|
|
|
void TestCharging::getSetAcquisitionTolerance_data()
|
|
{
|
|
QTest::addColumn<double>("initialAcquisitionTolerance");
|
|
QTest::addColumn<double>("acquisitionTolerance");
|
|
QTest::addColumn<EnergyManager::EnergyError>("expectedError");
|
|
|
|
QTest::newRow("Default") << 0.1 << 0.5 << EnergyManager::EnergyErrorNoError;
|
|
QTest::newRow("Unchanged") << 0.5 << 0.5 << EnergyManager::EnergyErrorNoError;
|
|
QTest::newRow("< 0.0") << 0.5 << -1.0 << EnergyManager::EnergyErrorInvalidParameter;
|
|
QTest::newRow("> 1.0") << 0.5 << 2.0 << EnergyManager::EnergyErrorInvalidParameter;
|
|
}
|
|
|
|
void TestCharging::getSetAcquisitionTolerance()
|
|
{
|
|
QFETCH(double, initialAcquisitionTolerance);
|
|
QFETCH(double, acquisitionTolerance);
|
|
QFETCH(EnergyManager::EnergyError, expectedError);
|
|
|
|
QVariantMap params;
|
|
QVariant response, notification;
|
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
|
|
|
// Set initial acquisition tolerance
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
params.insert("acquisitionTolerance", initialAcquisitionTolerance);
|
|
response = injectAndWait("NymeaEnergy.SetAcquisitionTolerance", params);
|
|
verifyEnergyError(response);
|
|
|
|
bool testNotificationReceived = (initialAcquisitionTolerance != acquisitionTolerance);
|
|
|
|
// Set acquisition tolerance
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
params.insert("acquisitionTolerance", acquisitionTolerance);
|
|
response = injectAndWait("NymeaEnergy.SetAcquisitionTolerance", params);
|
|
verifyEnergyError(response, expectedError);
|
|
|
|
// If the value has changed, verify the notification
|
|
if (testNotificationReceived && expectedError == EnergyManager::EnergyErrorNoError) {
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "NymeaEnergy.AcquisitionToleranceChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("acquisitionTolerance").toDouble(), acquisitionTolerance);
|
|
}
|
|
|
|
// Get acquisition tolerance
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("NymeaEnergy.GetAcquisitionTolerance", params);
|
|
QVERIFY2(response.toMap().value("params").toMap().contains("acquisitionTolerance"), "Did not get aquisition tolerance.");
|
|
}
|
|
|
|
void TestCharging::getSetBatteryLevelConsideration_data()
|
|
{
|
|
QTest::addColumn<double>("initialBatteryLevelConsideration");
|
|
QTest::addColumn<double>("batteryLevelConsideration");
|
|
QTest::addColumn<EnergyManager::EnergyError>("expectedError");
|
|
|
|
QTest::newRow("Default") << 0.1 << 0.5 << EnergyManager::EnergyErrorNoError;
|
|
QTest::newRow("Unchanged") << 0.5 << 0.5 << EnergyManager::EnergyErrorNoError;
|
|
QTest::newRow("< 0.0") << 0.5 << -1.0 << EnergyManager::EnergyErrorInvalidParameter;
|
|
QTest::newRow("> 1.0") << 0.5 << 2.0 << EnergyManager::EnergyErrorInvalidParameter;
|
|
}
|
|
|
|
void TestCharging::getSetBatteryLevelConsideration()
|
|
{
|
|
QFETCH(double, initialBatteryLevelConsideration);
|
|
QFETCH(double, batteryLevelConsideration);
|
|
QFETCH(EnergyManager::EnergyError, expectedError);
|
|
|
|
QVariantMap params;
|
|
QVariant response, notification;
|
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
|
|
|
// Set initial acquisition tolerance
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
params.insert("batteryLevelConsideration", initialBatteryLevelConsideration);
|
|
response = injectAndWait("NymeaEnergy.SetBatteryLevelConsideration", params);
|
|
verifyEnergyError(response);
|
|
|
|
bool testNotificationReceived = (initialBatteryLevelConsideration != batteryLevelConsideration);
|
|
|
|
// Set acquisition tolerance
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
params.insert("batteryLevelConsideration", batteryLevelConsideration);
|
|
response = injectAndWait("NymeaEnergy.SetBatteryLevelConsideration", params);
|
|
verifyEnergyError(response, expectedError);
|
|
|
|
// If the value has changed, verify the notification
|
|
if (testNotificationReceived && expectedError == EnergyManager::EnergyErrorNoError) {
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "NymeaEnergy.BatteryLevelConsiderationChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("batteryLevelConsideration").toDouble(), batteryLevelConsideration);
|
|
}
|
|
|
|
// Get acquisition tolerance
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("NymeaEnergy.GetBatteryLevelConsideration", params);
|
|
QVERIFY2(response.toMap().value("params").toMap().contains("batteryLevelConsideration"), "Did not get battery level consideration.");
|
|
}
|
|
|
|
void TestCharging::addChargingInfo_data()
|
|
{
|
|
QTest::addColumn<bool>("addingCharger");
|
|
QTest::addColumn<bool>("addingCar");
|
|
QTest::addColumn<bool>("carValid");
|
|
QTest::addColumn<QString>("chargingMode");
|
|
QTest::addColumn<QDateTime>("endDateTime");
|
|
QTest::addColumn<QList<int>>("repeatDays");
|
|
QTest::addColumn<int>("targetPercentage");
|
|
QTest::addColumn<double>("acquisitionTolerance");
|
|
QTest::addColumn<bool>("jsonSuccess");
|
|
QTest::addColumn<EnergyManager::EnergyError>("expectedError");
|
|
|
|
QDateTime endDateTime = QDateTime::currentDateTime().addDays(1);
|
|
endDateTime.setTime(QTime(7, 0));
|
|
QTest::newRow("Default") << true << true << true << "ChargingModeNormal" << endDateTime << QList<int>{} << 100 << 0.5 << true << EnergyManager::EnergyErrorNoError;
|
|
QTest::newRow("Default Eco") << true << true << true << "ChargingModeEco" << endDateTime << QList<int>{} << 100 << 0.5 << true << EnergyManager::EnergyErrorNoError;
|
|
QTest::newRow("Invalid mode") << true << true << true << "ChargingModeBlablaa" << endDateTime << QList<int>{} << 100 << 0.5 << false << EnergyManager::EnergyErrorNoError;
|
|
QTest::newRow("Invalid charger") << false << true << true << "ChargingModeNormal" << endDateTime << QList<int>{} << 100 << 0.5 << true << EnergyManager::EnergyErrorInvalidParameter;
|
|
QTest::newRow("with repeatdays") << true << false << true << "ChargingModeNormal" << endDateTime << QList<int>{1,3,6} << 100 << 0.5 << true << EnergyManager::EnergyErrorNoError;
|
|
QTest::newRow("Invalid repeatDays") << true << false << true << "ChargingModeNormal" << endDateTime << QList<int>{0,1,2} << 100 << 0.5 << true << EnergyManager::EnergyErrorInvalidParameter;
|
|
QTest::newRow("Invalid car") << true << true << false << "ChargingModeEco" << endDateTime << QList<int>{} << 100 << 0.5 << true << EnergyManager::EnergyErrorInvalidParameter;
|
|
QTest::newRow("Invalid %") << true << true << true << "ChargingModeNormal" << endDateTime << QList<int>{} << 200 << 0.5 << true << EnergyManager::EnergyErrorInvalidParameter;
|
|
}
|
|
|
|
void TestCharging::addChargingInfo()
|
|
{
|
|
QFETCH(bool, addingCharger);
|
|
QFETCH(bool, addingCar);
|
|
QFETCH(bool, carValid);
|
|
QFETCH(QString, chargingMode);
|
|
QFETCH(QDateTime, endDateTime);
|
|
QFETCH(QList<int>, repeatDays);
|
|
QFETCH(int, targetPercentage);
|
|
QFETCH(double, acquisitionTolerance);
|
|
QFETCH(bool, jsonSuccess);
|
|
QFETCH(EnergyManager::EnergyError, expectedError);
|
|
|
|
QVariant response, notification;
|
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
|
|
|
// Set phase power limit
|
|
uint phasePowerLimit = 25000;
|
|
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", phasePowerLimit}}));
|
|
verifyEnergyError(response);
|
|
|
|
removeDevices();
|
|
|
|
// Add mock meter
|
|
QUuid meterThingId = addMeter();
|
|
QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "Energy.RootMeterChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
|
|
// Make sure this is our root meter now
|
|
response = injectAndWait("Energy.GetRootMeter");
|
|
QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
notificationSpy.clear();
|
|
|
|
// Add the charger
|
|
QUuid evChargerId;
|
|
if (addingCharger) {
|
|
evChargerId = addCharger("ABC", 16);
|
|
QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
} else {
|
|
evChargerId = QUuid::createUuid();
|
|
}
|
|
|
|
// Add the car
|
|
QUuid assignedCarId;
|
|
if (addingCar) {
|
|
assignedCarId = addCar();
|
|
QVERIFY2(!assignedCarId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
}
|
|
|
|
if (!carValid)
|
|
assignedCarId = QUuid::createUuid();
|
|
|
|
// Set aquisition tolerance
|
|
QVariantMap params;
|
|
params.insert("acquisitionTolerance", acquisitionTolerance);
|
|
response = injectAndWait("NymeaEnergy.SetAcquisitionTolerance", params);
|
|
verifyEnergyError(response);
|
|
notificationSpy.clear();
|
|
|
|
// Set charging info with our charger and car
|
|
QVariantMap chargingInfoMap;
|
|
chargingInfoMap.insert("evChargerId", evChargerId);
|
|
if (!assignedCarId.isNull())
|
|
chargingInfoMap.insert("assignedCarId", assignedCarId);
|
|
|
|
chargingInfoMap.insert("chargingMode", chargingMode);
|
|
chargingInfoMap.insert("endDateTime", endDateTime.toMSecsSinceEpoch() / 1000);
|
|
QVariantList repDaysList;
|
|
foreach (int day, repeatDays) {
|
|
repDaysList.append(day);
|
|
}
|
|
chargingInfoMap.insert("repeatDays", repDaysList);
|
|
chargingInfoMap.insert("targetPercentage", targetPercentage);
|
|
//qCDebug(dcTests()) << "Sending charging info:" << qUtf8Printable(QJsonDocument::fromVariant(chargingInfoMap).toJson(QJsonDocument::Indented));
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", chargingInfoMap}}));
|
|
|
|
if (jsonSuccess) {
|
|
QCOMPARE(response.toMap().value("status").toString(), QString("success"));
|
|
verifyEnergyError(response, expectedError);
|
|
|
|
// Get the charging infos
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("NymeaEnergy.GetChargingInfos", params);
|
|
QVERIFY2(response.toMap().value("params").toMap().contains("chargingInfos"), "Did not get charginginfos.");
|
|
QVariantList chargingInfos = response.toMap().value("params").toMap().value("chargingInfos").toList();
|
|
qDebug() << chargingInfos;
|
|
|
|
if (expectedError == EnergyManager::EnergyErrorNoError) {
|
|
QCOMPARE(chargingInfos.count(), 1);
|
|
}
|
|
|
|
} else {
|
|
QCOMPARE(response.toMap().value("status").toString(), QString("error"));
|
|
}
|
|
}
|
|
|
|
void TestCharging::setChargingInfoReplacesWholesale()
|
|
{
|
|
// [ETM] Le banc a montré le piège le 2026-08-26 : un appel ne portant que « chargingMode »
|
|
// a remonté targetPercentage de 0 à 80 — le DÉFAUT du constructeur — sous
|
|
// EnergyErrorNoError, alors que la description de la méthode promettait
|
|
// « Only given properties will be set, others will be untouched ».
|
|
// La description dit désormais la vérité (l'objet REMPLACE le stocké). Ce test épingle le
|
|
// comportement qu'elle annonce : sans lui, la promesse redeviendrait fausse en silence.
|
|
QVariant response;
|
|
|
|
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", 25000}}));
|
|
verifyEnergyError(response);
|
|
|
|
removeDevices();
|
|
|
|
QUuid meterThingId = addMeter();
|
|
QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId for the meter");
|
|
|
|
QUuid evChargerId = addCharger("ABC", 16);
|
|
QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId for the charger");
|
|
|
|
QUuid carId = addCar();
|
|
QVERIFY2(!carId.isNull(), "Did not receive valid ThingId for the car");
|
|
|
|
auto storedInfo = [this, evChargerId]() -> QVariantMap {
|
|
QVariant reply = injectAndWait("NymeaEnergy.GetChargingInfos", QVariantMap());
|
|
foreach (const QVariant &entry, reply.toMap().value("params").toMap().value("chargingInfos").toList()) {
|
|
if (entry.toMap().value("evChargerId").toUuid() == evChargerId)
|
|
return entry.toMap();
|
|
}
|
|
return QVariantMap();
|
|
};
|
|
|
|
// 1. Un objet complet, dont AUCUNE valeur n'est un défaut.
|
|
QVariantMap full;
|
|
full.insert("evChargerId", evChargerId);
|
|
full.insert("assignedCarId", carId);
|
|
full.insert("chargingMode", "ChargingModeEco");
|
|
full.insert("repeatDays", QVariantList() << 1 << 3);
|
|
full.insert("targetPercentage", 55);
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", full}}));
|
|
verifyEnergyError(response);
|
|
|
|
QVariantMap stored = storedInfo();
|
|
QVERIFY2(!stored.isEmpty(), "Charging info was not stored at all");
|
|
QCOMPARE(stored.value("targetPercentage").toUInt(), 55u);
|
|
QCOMPARE(stored.value("repeatDays").toList().count(), 2);
|
|
QCOMPARE(stored.value("assignedCarId").toUuid(), carId);
|
|
|
|
// 2. Un appel PARTIEL — le geste que la description autorisait par écrit. Il aboutit
|
|
// (EnergyErrorNoError) et les champs omis retombent sur leur DÉFAUT : ils ne sont pas
|
|
// préservés. C'est exactement ce qu'a vu le banc.
|
|
QVariantMap partial;
|
|
partial.insert("evChargerId", evChargerId);
|
|
partial.insert("chargingMode", "ChargingModeNormal");
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", partial}}));
|
|
verifyEnergyError(response);
|
|
|
|
stored = storedInfo();
|
|
QCOMPARE(stored.value("chargingMode").toString(), QString("ChargingModeNormal"));
|
|
QCOMPARE(stored.value("targetPercentage").toUInt(), 80u); // défaut, PAS les 55 posés en 1.
|
|
QVERIFY2(stored.value("repeatDays").toList().isEmpty(), "repeatDays survived a partial call");
|
|
QVERIFY2(stored.value("assignedCarId").toUuid().isNull(), "assignedCarId survived a partial call");
|
|
|
|
// 3. Le remède que la description prescrit : lire, modifier, renvoyer complet. Les champs
|
|
// non touchés survivent — y compris quand la charge utile relue porte « chargingState »,
|
|
// propriété en lecture seule, que la méthode doit ignorer sans faillir.
|
|
QVariantMap reposted = full;
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", reposted}}));
|
|
verifyEnergyError(response);
|
|
|
|
QVariantMap roundTrip = storedInfo();
|
|
QVERIFY2(roundTrip.contains("chargingState"), "GetChargingInfos no longer publishes chargingState");
|
|
// Un mode SANS échéance : avec une échéance, le moteur retomberait de lui-même sur
|
|
// ChargingModeEco faute d'endDateTime valide, et le test mesurerait ce repli au lieu de
|
|
// l'aller-retour.
|
|
roundTrip.insert("chargingMode", "ChargingModeEcoWithMinCurrent");
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", roundTrip}}));
|
|
verifyEnergyError(response);
|
|
|
|
stored = storedInfo();
|
|
QCOMPARE(stored.value("chargingMode").toString(), QString("ChargingModeEcoWithMinCurrent"));
|
|
QCOMPARE(stored.value("targetPercentage").toUInt(), 55u);
|
|
QCOMPARE(stored.value("assignedCarId").toUuid(), carId);
|
|
}
|
|
|
|
void TestCharging::testEcoMode_data()
|
|
{
|
|
// Houshold info
|
|
QTest::addColumn<int>("phasePowerLimit");
|
|
|
|
// ChargingInfo
|
|
QTest::addColumn<double>("acquisitionTolerance");
|
|
QTest::addColumn<int>("targetPercentage");
|
|
QTest::addColumn<int>("hoursLeftToTargetTime");
|
|
|
|
// Car information and states
|
|
QTest::addColumn<int>("carBatteryLevel");
|
|
QTest::addColumn<int>("carCapacity");
|
|
QTest::addColumn<int>("carMinChargingCurrent");
|
|
QTest::addColumn<int>("carPhaseCount");
|
|
|
|
// Charger information and states
|
|
QTest::addColumn<bool>("chargerConnected");
|
|
QTest::addColumn<bool>("chargerPower");
|
|
QTest::addColumn<QString>("chargerPhases");
|
|
QTest::addColumn<int>("chargerMaxChargingCurrent");
|
|
QTest::addColumn<int>("chargerMaxChargingCurrentMaxValue");
|
|
|
|
// Current meter power states
|
|
QTest::addColumn<QVariantMap>("meterPhasesPower");
|
|
|
|
// Desired result on the evCharger
|
|
QTest::addColumn<int>("expectedChargerMaxChargingCurrent");
|
|
QTest::addColumn<bool>("expectedChargerPower");
|
|
|
|
QTest::newRow("Default: 1 phase, p: 4kW, 16A, ON")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 100 // targetPercentage
|
|
<< 6 // hoursLeftToTargetTime
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 1 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "A" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 16 //chargerMaxChargingCurrentMaxValue
|
|
|
|
<< QVariantMap({ {"A", 0000}, // W
|
|
{"B", 2000}, // W
|
|
{"C", -6000} }) // W
|
|
|
|
<< 16 // expectedChargerMaxChargingCurrent
|
|
<< true; // expectedChargerPower
|
|
|
|
|
|
QTest::newRow("Default: 1 phase, p: 2kW, 7A, ON")
|
|
<< 16 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 100 // targetPercentage
|
|
<< 6 // hoursLeftToTargetTime
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 1 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "A" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 16 //chargerMaxChargingCurrentMaxValue
|
|
|
|
<< QVariantMap({ {"A", 2000}, // W
|
|
{"B", -2000}, // W
|
|
{"C", -2000} }) // W
|
|
|
|
<< 7 // expectedChargerMaxChargingCurrent
|
|
<< true; // expectedChargerPower
|
|
|
|
|
|
QTest::newRow("Default: 1 phase, p: -6kW, 13A, ON")
|
|
<< 16 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 100 // targetPercentage
|
|
<< 6 // hoursLeftToTargetTime
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 1 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "A" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 32 //chargerMaxChargingCurrentMaxValue
|
|
|
|
<< QVariantMap({ {"A", -3000}, // W
|
|
{"B", 3000}, // W
|
|
{"C", -3000} }) // W
|
|
|
|
<< 13 // expectedChargerMaxChargingCurrent
|
|
<< true; // expectedChargerPower
|
|
|
|
QTest::newRow("Default: 2 phase, p: -8kW, 17A, ON")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 100 // targetPercentage
|
|
<< 6 // hoursLeftToTargetTime
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 2 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "AB" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 32 //chargerMaxChargingCurrentMaxValue
|
|
|
|
<< QVariantMap({ {"A", -4000}, // W
|
|
{"B", -5000}, // W
|
|
{"C", 1000} }) // W
|
|
|
|
<< 17 // expectedChargerMaxChargingCurrent
|
|
<< true; // expectedChargerPower
|
|
|
|
QTest::newRow("Default: 3 phase, p: -3kW, 6A, ON")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 100 // targetPercentage
|
|
<< 6 // hoursLeftToTargetTime
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 3 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "ABC" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 32 //chargerMaxChargingCurrentMaxValue
|
|
|
|
<< QVariantMap({ {"A", -1000}, // W
|
|
{"B", -1000}, // W
|
|
{"C", -1000} }) // W
|
|
|
|
<< 6 // expectedChargerMaxChargingCurrent
|
|
<< true; // expectedChargerPower
|
|
}
|
|
|
|
void TestCharging::testEcoMode()
|
|
{
|
|
QFETCH(int, phasePowerLimit);
|
|
|
|
QFETCH(double, acquisitionTolerance);
|
|
QFETCH(int, targetPercentage);
|
|
QFETCH(int, hoursLeftToTargetTime);
|
|
|
|
QFETCH(int, carBatteryLevel);
|
|
QFETCH(int, carCapacity);
|
|
QFETCH(int, carMinChargingCurrent);
|
|
QFETCH(int, carPhaseCount);
|
|
|
|
QFETCH(bool, chargerConnected);
|
|
QFETCH(bool, chargerPower);
|
|
QFETCH(QString, chargerPhases);
|
|
QFETCH(int, chargerMaxChargingCurrent);
|
|
QFETCH(int, chargerMaxChargingCurrentMaxValue);
|
|
|
|
QFETCH(QVariantMap, meterPhasesPower);
|
|
|
|
QFETCH(int, expectedChargerMaxChargingCurrent);
|
|
QFETCH(bool, expectedChargerPower);
|
|
|
|
QVariantMap params;
|
|
QVariant response, notification;
|
|
QNetworkReply *reply = nullptr;
|
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
|
|
|
// Set phase power limit
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", phasePowerLimit}}));
|
|
QVERIFY(response.toMap().value("params").toMap().value("energyError").toString() == "EnergyErrorNoError");
|
|
|
|
// Add mock meter
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid meterThingId = addMeter();
|
|
QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "Energy.RootMeterChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
|
|
// Make sure this is our root meter now
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("Energy.GetRootMeter");
|
|
QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
notificationSpy.clear();
|
|
|
|
// Set the meter values first so we have the desired execution o charging info changed, and not an extra iteration with 0 W on root meter
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setMeterStates(meterPhasesPower);
|
|
QSignalSpy setStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setStatesReplySpy.count() == 0) setStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// No evaluation, there is no meter configured yet
|
|
|
|
// Add the charger
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid evChargerId = addCharger(chargerPhases, chargerMaxChargingCurrentMaxValue);
|
|
QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Add the car
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid assignedCarId = addCar();
|
|
QVERIFY2(!assignedCarId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Set aquisition tolerance
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
params.insert("acquisitionTolerance", acquisitionTolerance);
|
|
response = injectAndWait("NymeaEnergy.SetAcquisitionTolerance", params);
|
|
verifyEnergyError(response);
|
|
|
|
// Set states of car
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setCarStates(carBatteryLevel, carCapacity, carMinChargingCurrent, carPhaseCount);
|
|
QSignalSpy setCarStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setCarStatesReplySpy.count() == 0) setCarStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// Set states of the charger
|
|
|
|
uint effectivePhaseCount = qMin((uint)carPhaseCount, Electricity::getPhaseCount(Electricity::convertPhasesFromString(chargerPhases)));
|
|
QString usedPhases;
|
|
if (effectivePhaseCount >= 1)
|
|
usedPhases.append("A");
|
|
|
|
if (effectivePhaseCount >= 2)
|
|
usedPhases.append("B");
|
|
|
|
if (effectivePhaseCount >= 3)
|
|
usedPhases.append("C");
|
|
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setChargerStates(chargerConnected, chargerPower, true, usedPhases, chargerMaxChargingCurrent, chargerMaxChargingCurrentMaxValue);
|
|
QSignalSpy setChargerStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setChargerStatesReplySpy.count() == 0) setChargerStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
|
|
|
|
// Set charging info with our charger and car, this should trigger the evaluation
|
|
QVariantMap chargingInfoMap;
|
|
chargingInfoMap.insert("evChargerId", evChargerId);
|
|
if (!assignedCarId.isNull())
|
|
chargingInfoMap.insert("assignedCarId", assignedCarId);
|
|
|
|
chargingInfoMap.insert("chargingMode", "ChargingModeEcoWithTargetTime");
|
|
chargingInfoMap.insert("endDateTime", QDateTime::currentDateTime().addSecs(hoursLeftToTargetTime * 3600).toMSecsSinceEpoch() / 1000);
|
|
chargingInfoMap.insert("targetPercentage", targetPercentage);
|
|
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", chargingInfoMap}}));
|
|
verifyEnergyError(response);
|
|
|
|
// Verify if the charger has been set correctly
|
|
reply = getActionHistory(m_mockChargerDefaultPort);
|
|
QSignalSpy actionHistoryReplySpy(reply, &QNetworkReply::finished);
|
|
if (actionHistoryReplySpy.count() == 0) actionHistoryReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
QByteArray data = reply->readAll();
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
|
|
QVariantList actionHistory = jsonDoc.toVariant().toList();
|
|
//qCDebug(dcTests()) << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
|
|
|
|
// Make sure we got the correct actions
|
|
if (expectedChargerMaxChargingCurrent != 0)
|
|
QVERIFY(verifyActionExecuted(actionHistory, "maxChargingCurrent"));
|
|
|
|
QVERIFY(verifyActionExecuted(actionHistory, "power"));
|
|
|
|
if (expectedChargerMaxChargingCurrent != 0)
|
|
QCOMPARE(getLastValueFromExecutedAction(actionHistory, "maxChargingCurrent", "maxChargingCurrent"), QVariant(expectedChargerMaxChargingCurrent));
|
|
|
|
QCOMPARE(getLastValueFromExecutedAction(actionHistory, "power", "power"), QVariant(expectedChargerPower));
|
|
|
|
removeDevices();
|
|
}
|
|
|
|
void TestCharging::testEcoModePhaseSwitching_data()
|
|
{
|
|
// Houshold info
|
|
QTest::addColumn<int>("phasePowerLimit");
|
|
|
|
// ChargingInfo
|
|
QTest::addColumn<double>("acquisitionTolerance");
|
|
QTest::addColumn<int>("targetPercentage");
|
|
QTest::addColumn<int>("hoursLeftToTargetTime");
|
|
|
|
// Car information and states
|
|
QTest::addColumn<int>("carBatteryLevel");
|
|
QTest::addColumn<int>("carCapacity");
|
|
QTest::addColumn<int>("carMinChargingCurrent");
|
|
QTest::addColumn<int>("carPhaseCount");
|
|
|
|
// Charger information and states
|
|
QTest::addColumn<bool>("chargerConnected");
|
|
QTest::addColumn<bool>("chargerPower");
|
|
QTest::addColumn<QString>("chargerPhases");
|
|
QTest::addColumn<int>("chargerMaxChargingCurrent");
|
|
QTest::addColumn<int>("chargerMaxChargingCurrentMaxValue");
|
|
QTest::addColumn<int>("chargerDesiredPhaseCount");
|
|
|
|
// Current meter power states
|
|
QTest::addColumn<QVariantMap>("meterPhasesPower");
|
|
|
|
// Desired result on the evCharger
|
|
QTest::addColumn<int>("expectedChargerMaxChargingCurrent");
|
|
QTest::addColumn<bool>("expectedChargerPower");
|
|
QTest::addColumn<int>("expectedChargerDesiredPhaseCount");
|
|
|
|
|
|
QTest::newRow("1 phase, p: 3,8kW, 10A, ON, No phase switch, no aquisition")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 1.0 // acquisitionTolerance
|
|
<< 100 // targetPercentage
|
|
<< 6 // hoursLeftToTargetTime
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 3 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< true // chargerPower
|
|
<< "ABC" // chargerPhases
|
|
<< 10 // chargerMaxChargingCurrent
|
|
<< 16 // chargerMaxChargingCurrentMaxValue
|
|
<< 1 // chargerDesiredPhaseCount
|
|
|
|
// meterPhasesPower [W]
|
|
<< QVariantMap({ {"A", 2300},
|
|
{"B", -1900},
|
|
{"C", -2000} })
|
|
|
|
<< 16 // expectedChargerMaxChargingCurrent
|
|
<< true // expectedChargerPower
|
|
<< 1; // expectedChargerDesiredPhaseCount
|
|
|
|
|
|
QTest::newRow("1 phase, p: 3,8kW, 6A, ON, Switch 3 phase with aquisition")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 100 // targetPercentage
|
|
<< 6 // hoursLeftToTargetTime
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 3 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "ABC" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 16 // chargerMaxChargingCurrentMaxValue
|
|
<< 1 // chargerDesiredPhaseCount
|
|
|
|
// meterPhasesPower [W]
|
|
<< QVariantMap({ {"A", 0000},
|
|
{"B", 2000},
|
|
{"C", -5800} })
|
|
|
|
<< 6 // expectedChargerMaxChargingCurrent
|
|
<< true // expectedChargerPower
|
|
<< 3; // expectedChargerDesiredPhaseCount
|
|
|
|
|
|
QTest::newRow("3 phase, p: 4kW, 6A, ON, Switch 1 -> 3")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 100 // targetPercentage
|
|
<< 6 // hoursLeftToTargetTime
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 3 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "ABC" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 16 // chargerMaxChargingCurrentMaxValue
|
|
<< 1 // chargerDesiredPhaseCount
|
|
|
|
// meterPhasesPower [W]
|
|
<< QVariantMap({ {"A", 0000},
|
|
{"B", 2000},
|
|
{"C", -6000} })
|
|
|
|
<< 6 // expectedChargerMaxChargingCurrent
|
|
<< true // expectedChargerPower
|
|
<< 3; // expectedChargerDesiredPhaseCount
|
|
|
|
QTest::newRow("3 phase, p: 800W, 6A, ON, Switch 3 -> 1")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 100 // targetPercentage
|
|
<< 6 // hoursLeftToTargetTime
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 3 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "ABC" // chargerPhases
|
|
<< 10 // chargerMaxChargingCurrent
|
|
<< 16 // chargerMaxChargingCurrentMaxValue
|
|
<< 3 // chargerDesiredPhaseCount
|
|
|
|
// meterPhasesPower [W]
|
|
<< QVariantMap({ {"A", 0000},
|
|
{"B", 900},
|
|
{"C", -1700} })
|
|
|
|
<< 6 // expectedChargerMaxChargingCurrent
|
|
<< true // expectedChargerPower
|
|
<< 1; // expectedChargerDesiredPhaseCount
|
|
|
|
QTest::newRow("3 phase charger, p: 1,38kW, 6A, ON, Start 1-phase")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.01 // acquisitionTolerance
|
|
<< 100 // targetPercentage
|
|
<< 6 // hoursLeftToTargetTime
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 3 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "ABC" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 16 // chargerMaxChargingCurrentMaxValue
|
|
<< 3 // chargerDesiredPhaseCount
|
|
|
|
// meterPhasesPower [W]
|
|
<< QVariantMap({ {"A", -1400},
|
|
{"B", 2000},
|
|
{"C", -2000} })
|
|
|
|
<< 6 // expectedChargerMaxChargingCurrent
|
|
<< true // expectedChargerPower
|
|
<< 1; // expectedChargerDesiredPhaseCount
|
|
|
|
QTest::newRow("3 phase charger, p: 200W, 6A, ON, Start 1-phase")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.01 // acquisitionTolerance
|
|
<< 100 // targetPercentage
|
|
<< 6 // hoursLeftToTargetTime
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 3 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "ABC" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 16 // chargerMaxChargingCurrentMaxValue
|
|
<< 3 // chargerDesiredPhaseCount
|
|
|
|
// meterPhasesPower [W]
|
|
<< QVariantMap({ {"A", -200},
|
|
{"B", 2000},
|
|
{"C", -2000} })
|
|
|
|
<< 6 // expectedChargerMaxChargingCurrent
|
|
<< true // expectedChargerPower
|
|
<< 1; // expectedChargerDesiredPhaseCount
|
|
|
|
QTest::newRow("3 phase charger, p: 3,7kW, 6A, ON, Start 3-phase")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.01 // acquisitionTolerance
|
|
<< 100 // targetPercentage
|
|
<< 6 // hoursLeftToTargetTime
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 3 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "ABC" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 16 // chargerMaxChargingCurrentMaxValue
|
|
<< 3 // chargerDesiredPhaseCount
|
|
|
|
// meterPhasesPower [W]
|
|
<< QVariantMap({ {"A", -3700},
|
|
{"B", 2000},
|
|
{"C", -2000} })
|
|
|
|
<< 6 // expectedChargerMaxChargingCurrent
|
|
<< true // expectedChargerPower
|
|
<< 3; // expectedChargerDesiredPhaseCount
|
|
}
|
|
|
|
void TestCharging::testEcoModePhaseSwitching()
|
|
{
|
|
QFETCH(int, phasePowerLimit);
|
|
|
|
QFETCH(double, acquisitionTolerance);
|
|
QFETCH(int, targetPercentage);
|
|
QFETCH(int, hoursLeftToTargetTime);
|
|
|
|
QFETCH(int, carBatteryLevel);
|
|
QFETCH(int, carCapacity);
|
|
QFETCH(int, carMinChargingCurrent);
|
|
QFETCH(int, carPhaseCount);
|
|
|
|
QFETCH(bool, chargerConnected);
|
|
QFETCH(bool, chargerPower);
|
|
QFETCH(QString, chargerPhases);
|
|
QFETCH(int, chargerMaxChargingCurrent);
|
|
QFETCH(int, chargerMaxChargingCurrentMaxValue);
|
|
QFETCH(int, chargerDesiredPhaseCount);
|
|
|
|
QFETCH(QVariantMap, meterPhasesPower);
|
|
|
|
QFETCH(int, expectedChargerMaxChargingCurrent);
|
|
QFETCH(bool, expectedChargerPower);
|
|
QFETCH(int, expectedChargerDesiredPhaseCount);
|
|
|
|
Q_UNUSED(expectedChargerDesiredPhaseCount)
|
|
|
|
QVariantMap params;
|
|
QVariant response, notification;
|
|
QNetworkReply *reply = nullptr;
|
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
|
|
|
// Set phase power limit
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", phasePowerLimit}}));
|
|
QVERIFY(response.toMap().value("params").toMap().value("energyError").toString() == "EnergyErrorNoError");
|
|
|
|
// Add mock meter
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid meterThingId = addMeter();
|
|
QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "Energy.RootMeterChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
|
|
// Make sure this is our root meter now
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("Energy.GetRootMeter");
|
|
QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
notificationSpy.clear();
|
|
|
|
// Set the meter values first so we have the desired execution o charging info changed, and not an extra iteration with 0 W on root meter
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setMeterStates(meterPhasesPower);
|
|
QSignalSpy setStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setStatesReplySpy.count() == 0) setStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// No evaluation, there is no meter configured yet
|
|
|
|
// Add the charger
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid evChargerId = addChargerWithPhaseCountSwitching(chargerPhases, chargerMaxChargingCurrentMaxValue);
|
|
QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Add the car
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid assignedCarId = addCar();
|
|
QVERIFY2(!assignedCarId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Set aquisition tolerance
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
params.insert("acquisitionTolerance", acquisitionTolerance);
|
|
response = injectAndWait("NymeaEnergy.SetAcquisitionTolerance", params);
|
|
verifyEnergyError(response);
|
|
|
|
// Set states of car
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setCarStates(carBatteryLevel, carCapacity, carMinChargingCurrent, carPhaseCount);
|
|
QSignalSpy setCarStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setCarStatesReplySpy.count() == 0) setCarStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// Set states of the charger
|
|
|
|
uint effectivePhaseCount = qMin((uint)carPhaseCount, Electricity::getPhaseCount(Electricity::convertPhasesFromString(chargerPhases)));
|
|
QString usedPhases;
|
|
if (effectivePhaseCount >= 1)
|
|
usedPhases.append("A");
|
|
|
|
if (effectivePhaseCount >= 2)
|
|
usedPhases.append("B");
|
|
|
|
if (effectivePhaseCount >= 3)
|
|
usedPhases.append("C");
|
|
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setChargerWithPhaseCountSwitchingStates(chargerConnected, chargerPower, true, usedPhases, chargerMaxChargingCurrent, chargerMaxChargingCurrentMaxValue, chargerDesiredPhaseCount);
|
|
QSignalSpy setChargerStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setChargerStatesReplySpy.count() == 0) setChargerStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// Set charging info with our charger and car, this should trigger the evaluation
|
|
QVariantMap chargingInfoMap;
|
|
chargingInfoMap.insert("evChargerId", evChargerId);
|
|
if (!assignedCarId.isNull())
|
|
chargingInfoMap.insert("assignedCarId", assignedCarId);
|
|
|
|
chargingInfoMap.insert("chargingMode", "ChargingModeEcoWithTargetTime");
|
|
chargingInfoMap.insert("endDateTime", QDateTime::currentDateTime().addSecs(hoursLeftToTargetTime * 3600).toMSecsSinceEpoch() / 1000);
|
|
chargingInfoMap.insert("targetPercentage", targetPercentage);
|
|
|
|
// Trigger evaluation
|
|
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", chargingInfoMap}}));
|
|
verifyEnergyError(response);
|
|
|
|
// Verify if the charger has been set correctly
|
|
reply = getActionHistory(m_mockChargerWithPhaseCountSwitchingDefaultPort);
|
|
QSignalSpy actionHistoryReplySpy(reply, &QNetworkReply::finished);
|
|
if (actionHistoryReplySpy.count() == 0) actionHistoryReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
QByteArray data = reply->readAll();
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
|
|
QVariantList actionHistory = jsonDoc.toVariant().toList();
|
|
//qCDebug(dcTests()) << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
|
|
|
|
// Make sure we got the correct actions
|
|
if (expectedChargerMaxChargingCurrent != 0)
|
|
QVERIFY(verifyActionExecuted(actionHistory, "maxChargingCurrent"));
|
|
|
|
QVERIFY(verifyActionExecuted(actionHistory, "power"));
|
|
|
|
if (expectedChargerMaxChargingCurrent != 0)
|
|
QCOMPARE(getLastValueFromExecutedAction(actionHistory, "maxChargingCurrent", "maxChargingCurrent"), QVariant(expectedChargerMaxChargingCurrent));
|
|
|
|
if (expectedChargerDesiredPhaseCount != chargerDesiredPhaseCount)
|
|
QCOMPARE(getLastValueFromExecutedAction(actionHistory, "desiredPhaseCount", "desiredPhaseCount"), QVariant(expectedChargerDesiredPhaseCount));
|
|
|
|
QCOMPARE(getLastValueFromExecutedAction(actionHistory, "power", "power"), QVariant(expectedChargerPower));
|
|
|
|
removeDevices();
|
|
}
|
|
|
|
void TestCharging::testBatteryLevelConsideration_data()
|
|
{
|
|
// Houshold info
|
|
QTest::addColumn<int>("phasePowerLimit");
|
|
|
|
// ChargingInfo
|
|
QTest::addColumn<double>("acquisitionTolerance");
|
|
QTest::addColumn<double>("batteryLevelConsideration");
|
|
|
|
// Car information and states
|
|
QTest::addColumn<int>("carBatteryLevel");
|
|
QTest::addColumn<int>("carCapacity");
|
|
QTest::addColumn<int>("carMinChargingCurrent");
|
|
QTest::addColumn<int>("carPhaseCount");
|
|
|
|
// Charger information and states
|
|
QTest::addColumn<bool>("chargerConnected");
|
|
QTest::addColumn<bool>("chargerPower");
|
|
QTest::addColumn<QString>("chargerPhases");
|
|
QTest::addColumn<int>("chargerMaxChargingCurrent");
|
|
QTest::addColumn<int>("chargerMaxChargingCurrentMaxValue");
|
|
QTest::addColumn<int>("chargerDesiredPhaseCount");
|
|
|
|
// Current meter power states
|
|
QTest::addColumn<QVariantMap>("meterPhasesPower");
|
|
|
|
// Current energy storage states
|
|
QTest::addColumn<double>("energyStorageBatteryLevel");
|
|
QTest::addColumn<double>("energyStorageCurrentPower");
|
|
|
|
// Desired result on the evCharger
|
|
QTest::addColumn<int>("expectedChargerMaxChargingCurrent"); // 0 for Unchanged / not executed
|
|
QTest::addColumn<bool>("expectedChargerPower");
|
|
QTest::addColumn<int>("expectedChargerDesiredPhaseCount");
|
|
|
|
QTest::newRow("Default: 1 phase, battery: 80% 3,6kW -> 16A, ON, 1P")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 0.8 // batteryLevelConsideration
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 1 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "A" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 16 // chargerMaxChargingCurrentMaxValue
|
|
<< 1 // chargerDesiredPhaseCount
|
|
|
|
// meterPhasesPower [W]
|
|
<< QVariantMap({ {"A", 0},
|
|
{"B", 0 },
|
|
{"C", 0} })
|
|
|
|
<< 80.0 // energyStorageBatteryLevel
|
|
<< 3680.0 // energyStorageCurrentPower
|
|
|
|
<< 16 // expectedChargerMaxChargingCurrent
|
|
<< true // expectedChargerPower
|
|
<< 1; // expectedChargerDesiredPhaseCount
|
|
|
|
|
|
QTest::newRow("Default: 1 phase, battery: 79% 3,6kW -> 6A, OFF, 1P")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 0.8 // batteryLevelConsideration
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 1 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "A" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 16 // chargerMaxChargingCurrentMaxValue
|
|
<< 1 // chargerDesiredPhaseCount
|
|
|
|
// meterPhasesPower [W]
|
|
<< QVariantMap({ {"A", 0},
|
|
{"B", 0 },
|
|
{"C", 0} })
|
|
|
|
<< 79.0 // energyStorageBatteryLevel
|
|
<< 3680.0 // energyStorageCurrentPower
|
|
|
|
<< 0 // expectedChargerMaxChargingCurrent
|
|
<< false // expectedChargerPower
|
|
<< 1; // expectedChargerDesiredPhaseCount
|
|
|
|
|
|
QTest::newRow("Default: 1 phase, battery: 81% -600W -> 6A, OFF, 1P")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 0.8 // batteryLevelConsideration
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 1 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "A" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 16 // chargerMaxChargingCurrentMaxValue
|
|
<< 1 // chargerDesiredPhaseCount
|
|
|
|
// meterPhasesPower [W]
|
|
<< QVariantMap({ {"A", -600},
|
|
{"B", 0 },
|
|
{"C", 0} })
|
|
|
|
<< 81.0 // energyStorageBatteryLevel
|
|
<< -600.0 // energyStorageCurrentPower
|
|
|
|
<< 0 // expectedChargerMaxChargingCurrent
|
|
<< false // expectedChargerPower
|
|
<< 1; // expectedChargerDesiredPhaseCount
|
|
|
|
|
|
QTest::newRow("Default: 1 phase, battery: 81% -600W -> 6A, OFF, 1P")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 0.8 // batteryLevelConsideration
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 1 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< false // chargerPower
|
|
<< "A" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 16 // chargerMaxChargingCurrentMaxValue
|
|
<< 1 // chargerDesiredPhaseCount
|
|
|
|
// meterPhasesPower [W]
|
|
<< QVariantMap({ {"A", -600},
|
|
{"B", 0 },
|
|
{"C", 0} })
|
|
|
|
<< 81.5 // energyStorageBatteryLevel
|
|
<< 100.0 // energyStorageCurrentPower
|
|
|
|
<< 6 // expectedChargerMaxChargingCurrent
|
|
<< true // expectedChargerPower
|
|
<< 1; // expectedChargerDesiredPhaseCount
|
|
|
|
|
|
}
|
|
|
|
void TestCharging::testBatteryLevelConsideration()
|
|
{
|
|
QFETCH(int, phasePowerLimit);
|
|
|
|
QFETCH(double, acquisitionTolerance);
|
|
QFETCH(double, batteryLevelConsideration);
|
|
|
|
QFETCH(int, carBatteryLevel);
|
|
QFETCH(int, carCapacity);
|
|
QFETCH(int, carMinChargingCurrent);
|
|
QFETCH(int, carPhaseCount);
|
|
|
|
QFETCH(bool, chargerConnected);
|
|
QFETCH(bool, chargerPower);
|
|
QFETCH(QString, chargerPhases);
|
|
QFETCH(int, chargerMaxChargingCurrent);
|
|
QFETCH(int, chargerMaxChargingCurrentMaxValue);
|
|
QFETCH(int, chargerDesiredPhaseCount);
|
|
|
|
QFETCH(QVariantMap, meterPhasesPower);
|
|
|
|
QFETCH(double, energyStorageBatteryLevel);
|
|
QFETCH(double, energyStorageCurrentPower);
|
|
|
|
QFETCH(int, expectedChargerMaxChargingCurrent); // 0 for Unchanged / not executed
|
|
QFETCH(bool, expectedChargerPower);
|
|
QFETCH(int, expectedChargerDesiredPhaseCount);
|
|
|
|
Q_UNUSED(expectedChargerDesiredPhaseCount)
|
|
|
|
QVariantMap params;
|
|
QVariant response, notification;
|
|
QNetworkReply *reply = nullptr;
|
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
|
|
|
// Set phase power limit
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", phasePowerLimit}}));
|
|
QVERIFY(response.toMap().value("params").toMap().value("energyError").toString() == "EnergyErrorNoError");
|
|
|
|
// Add mock meter
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid meterThingId = addMeter();
|
|
QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "Energy.RootMeterChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
|
|
// Make sure this is our root meter now
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("Energy.GetRootMeter");
|
|
QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
notificationSpy.clear();
|
|
|
|
|
|
// Set the meter values first so we have the desired execution o charging info changed, and not an extra iteration with 0 W on root meter
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setMeterStates(meterPhasesPower);
|
|
QSignalSpy setStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setStatesReplySpy.count() == 0) setStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// No evaluation, there is no meter configured yet
|
|
|
|
// Add the charger
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid evChargerId = addChargerWithPhaseCountSwitching(chargerPhases, chargerMaxChargingCurrentMaxValue);
|
|
QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Add the car
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid assignedCarId = addCar();
|
|
QVERIFY2(!assignedCarId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Add energy storage
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid energyStorageId = addEnergyStorage();
|
|
QVERIFY2(!energyStorageId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Set aquisition tolerance
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
params.insert("acquisitionTolerance", acquisitionTolerance);
|
|
response = injectAndWait("NymeaEnergy.SetAcquisitionTolerance", params);
|
|
verifyEnergyError(response);
|
|
|
|
// Set battery level consideration
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
params.insert("batteryLevelConsideration", batteryLevelConsideration);
|
|
response = injectAndWait("NymeaEnergy.SetBatteryLevelConsideration", params);
|
|
verifyEnergyError(response);
|
|
|
|
// Set states of car
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setCarStates(carBatteryLevel, carCapacity, carMinChargingCurrent, carPhaseCount);
|
|
QSignalSpy setCarStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setCarStatesReplySpy.count() == 0) setCarStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// Set states of the charger
|
|
|
|
uint effectivePhaseCount = qMin((uint)carPhaseCount, Electricity::getPhaseCount(Electricity::convertPhasesFromString(chargerPhases)));
|
|
QString usedPhases;
|
|
if (effectivePhaseCount >= 1)
|
|
usedPhases.append("A");
|
|
|
|
if (effectivePhaseCount >= 2)
|
|
usedPhases.append("B");
|
|
|
|
if (effectivePhaseCount >= 3)
|
|
usedPhases.append("C");
|
|
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setChargerWithPhaseCountSwitchingStates(chargerConnected, chargerPower, true, usedPhases, chargerMaxChargingCurrent, chargerMaxChargingCurrentMaxValue, chargerDesiredPhaseCount);
|
|
QSignalSpy setChargerStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setChargerStatesReplySpy.count() == 0) setChargerStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
|
|
// Set energy storage
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setEnergyStorageStates(energyStorageBatteryLevel, energyStorageCurrentPower);
|
|
QSignalSpy setEnergyStorageStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setEnergyStorageStatesReplySpy.count() == 0) setEnergyStorageStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
|
|
// Set charging info with our charger and car, this should trigger the evaluation
|
|
QVariantMap chargingInfoMap;
|
|
chargingInfoMap.insert("evChargerId", evChargerId);
|
|
if (!assignedCarId.isNull())
|
|
chargingInfoMap.insert("assignedCarId", assignedCarId);
|
|
|
|
chargingInfoMap.insert("chargingMode", "ChargingModeEco");
|
|
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", chargingInfoMap}}));
|
|
verifyEnergyError(response);
|
|
|
|
|
|
// Verify if the charger has been set correctly
|
|
reply = getActionHistory(m_mockChargerWithPhaseCountSwitchingDefaultPort);
|
|
QSignalSpy actionHistoryReplySpy(reply, &QNetworkReply::finished);
|
|
if (actionHistoryReplySpy.count() == 0) actionHistoryReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
QByteArray data = reply->readAll();
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
|
|
QVariantList actionHistory = jsonDoc.toVariant().toList();
|
|
//qCDebug(dcTests()) << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
|
|
|
|
// Make sure we got the correct actions
|
|
if (expectedChargerMaxChargingCurrent != 0)
|
|
QVERIFY(verifyActionExecuted(actionHistory, "maxChargingCurrent"));
|
|
|
|
QVERIFY(verifyActionExecuted(actionHistory, "power"));
|
|
|
|
if (expectedChargerMaxChargingCurrent != 0)
|
|
QCOMPARE(getLastValueFromExecutedAction(actionHistory, "maxChargingCurrent", "maxChargingCurrent"), QVariant(expectedChargerMaxChargingCurrent));
|
|
|
|
QCOMPARE(getLastValueFromExecutedAction(actionHistory, "power", "power"), QVariant(expectedChargerPower));
|
|
|
|
removeDevices();
|
|
}
|
|
|
|
void TestCharging::batteryLevelConsiderationStopCharging_data()
|
|
{
|
|
// Houshold info
|
|
QTest::addColumn<int>("phasePowerLimit");
|
|
|
|
// ChargingConfiguration
|
|
QTest::addColumn<double>("acquisitionTolerance");
|
|
QTest::addColumn<double>("batteryLevelConsideration");
|
|
|
|
// Car information and states
|
|
QTest::addColumn<int>("carBatteryLevel");
|
|
QTest::addColumn<int>("carCapacity");
|
|
QTest::addColumn<int>("carMinChargingCurrent");
|
|
QTest::addColumn<int>("carPhaseCount");
|
|
|
|
// Charger information and states
|
|
QTest::addColumn<bool>("chargerConnected");
|
|
QTest::addColumn<bool>("chargerPower");
|
|
QTest::addColumn<QString>("chargerPhases");
|
|
QTest::addColumn<int>("chargerMaxChargingCurrent");
|
|
QTest::addColumn<int>("chargerMaxChargingCurrentMaxValue");
|
|
QTest::addColumn<int>("chargerDesiredPhaseCount");
|
|
|
|
// Current meter power states
|
|
QTest::addColumn<QVariantMap>("meterPhasesPower");
|
|
|
|
// Current energy storage states
|
|
QTest::addColumn<double>("energyStorageBatteryLevel");
|
|
QTest::addColumn<double>("energyStorageCurrentPower");
|
|
|
|
QTest::newRow("Default:")
|
|
<< 32 // phase limit (A)
|
|
|
|
<< 0.5 // acquisitionTolerance
|
|
<< 0.8 // batteryLevelConsideration
|
|
|
|
<< 80 // carBatteryLevel
|
|
<< 48 // carCapacity
|
|
<< 6 // carMinChargingCurrent
|
|
<< 1 // carPhaseCount
|
|
|
|
<< true // chargerConnected
|
|
<< true // chargerPower
|
|
<< "ABC" // chargerPhases
|
|
<< 6 // chargerMaxChargingCurrent
|
|
<< 16 // chargerMaxChargingCurrentMaxValue
|
|
<< 1 // chargerDesiredPhaseCount
|
|
|
|
// meterPhasesPower [W]
|
|
<< QVariantMap({ {"A", 0.0},
|
|
{"B", 0 },
|
|
{"C", 0} })
|
|
|
|
<< 20.0 // energyStorageBatteryLevel
|
|
<< 0.0; // energyStorageCurrentPower
|
|
|
|
}
|
|
|
|
void TestCharging::batteryLevelConsiderationStopCharging()
|
|
{
|
|
QFETCH(int, phasePowerLimit);
|
|
|
|
QFETCH(double, acquisitionTolerance);
|
|
QFETCH(double, batteryLevelConsideration);
|
|
|
|
QFETCH(int, carBatteryLevel);
|
|
QFETCH(int, carCapacity);
|
|
QFETCH(int, carMinChargingCurrent);
|
|
QFETCH(int, carPhaseCount);
|
|
|
|
QFETCH(bool, chargerConnected);
|
|
QFETCH(bool, chargerPower);
|
|
QFETCH(QString, chargerPhases);
|
|
QFETCH(int, chargerMaxChargingCurrent);
|
|
QFETCH(int, chargerMaxChargingCurrentMaxValue);
|
|
QFETCH(int, chargerDesiredPhaseCount);
|
|
|
|
QFETCH(QVariantMap, meterPhasesPower);
|
|
|
|
QFETCH(double, energyStorageBatteryLevel);
|
|
QFETCH(double, energyStorageCurrentPower);
|
|
|
|
|
|
QVariantMap params;
|
|
QVariant response, notification;
|
|
QNetworkReply *reply = nullptr;
|
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
|
|
|
// Set phase power limit
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", phasePowerLimit}}));
|
|
QVERIFY(response.toMap().value("params").toMap().value("energyError").toString() == "EnergyErrorNoError");
|
|
|
|
// Add mock meter
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid meterThingId = addMeter();
|
|
QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "Energy.RootMeterChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
|
|
// Make sure this is our root meter now
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("Energy.GetRootMeter");
|
|
QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
notificationSpy.clear();
|
|
|
|
|
|
// Set the meter values first so we have the desired execution o charging configuration changed, and not an extra iteration with 0 W on root meter
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setMeterStates(meterPhasesPower);
|
|
QSignalSpy setStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setStatesReplySpy.count() == 0) setStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// No evaluation, there is no meter configured yet
|
|
|
|
// Add the charger
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid evChargerId = addChargerWithPhaseCountSwitching(chargerPhases, chargerMaxChargingCurrentMaxValue);
|
|
QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Add the car
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid assignedCarId = addCar();
|
|
QVERIFY2(!assignedCarId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Add energy storage
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid energyStorageId = addEnergyStorage();
|
|
QVERIFY2(!energyStorageId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Set aquisition tolerance
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
params.insert("acquisitionTolerance", acquisitionTolerance);
|
|
response = injectAndWait("NymeaEnergy.SetAcquisitionTolerance", params);
|
|
verifyEnergyError(response);
|
|
|
|
// Set battery level consideration
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
params.insert("batteryLevelConsideration", batteryLevelConsideration);
|
|
response = injectAndWait("NymeaEnergy.SetBatteryLevelConsideration", params);
|
|
verifyEnergyError(response);
|
|
|
|
// Set states of car
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setCarStates(carBatteryLevel, carCapacity, carMinChargingCurrent, carPhaseCount);
|
|
QSignalSpy setCarStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setCarStatesReplySpy.count() == 0) setCarStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
|
|
// Set states of the charger
|
|
uint effectivePhaseCount = qMin((uint)carPhaseCount, Electricity::getPhaseCount(Electricity::convertPhasesFromString(chargerPhases)));
|
|
QString usedPhases;
|
|
if (effectivePhaseCount >= 1)
|
|
usedPhases.append("A");
|
|
|
|
if (effectivePhaseCount >= 2)
|
|
usedPhases.append("B");
|
|
|
|
if (effectivePhaseCount >= 3)
|
|
usedPhases.append("C");
|
|
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setChargerWithPhaseCountSwitchingStates(chargerConnected, chargerPower, true, usedPhases, chargerMaxChargingCurrent, chargerMaxChargingCurrentMaxValue, chargerDesiredPhaseCount);
|
|
QSignalSpy setChargerStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setChargerStatesReplySpy.count() == 0) setChargerStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// Set energy storage
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setEnergyStorageStates(energyStorageBatteryLevel, energyStorageCurrentPower);
|
|
QSignalSpy setEnergyStorageStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setEnergyStorageStatesReplySpy.count() == 0) setEnergyStorageStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// Make sure we are in normal mode
|
|
QVariantMap chargingInfoMap;
|
|
chargingInfoMap.insert("evChargerId", evChargerId);
|
|
chargingInfoMap.insert("assignedCarId", assignedCarId);
|
|
chargingInfoMap.insert("chargingMode", "ChargingModeNormal");
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", chargingInfoMap}}));
|
|
verifyEnergyError(response);
|
|
|
|
|
|
// Set charging info with our charger and car, this should trigger the evaluation
|
|
chargingInfoMap.clear();
|
|
chargingInfoMap.insert("evChargerId", evChargerId);
|
|
chargingInfoMap.insert("assignedCarId", assignedCarId);
|
|
chargingInfoMap.insert("chargingMode", "ChargingModeEco");
|
|
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", chargingInfoMap}}));
|
|
verifyEnergyError(response);
|
|
|
|
// Verify if the charger has been set correctly
|
|
reply = getActionHistory(m_mockChargerWithPhaseCountSwitchingDefaultPort);
|
|
QSignalSpy actionHistoryReplySpy(reply, &QNetworkReply::finished);
|
|
if (actionHistoryReplySpy.count() == 0) actionHistoryReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
QByteArray data = reply->readAll();
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
|
|
QVariantList actionHistory = jsonDoc.toVariant().toList();
|
|
qCDebug(dcTests()) << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
|
|
|
|
// Make sure we got the correct actions
|
|
QVERIFY(verifyActionExecuted(actionHistory, "power"));
|
|
QCOMPARE(getLastValueFromExecutedAction(actionHistory, "power", "power"), false);
|
|
}
|
|
|
|
|
|
void TestCharging::testOverloadProtectionManualMode_data()
|
|
{
|
|
// Houshold info
|
|
QTest::addColumn<int>("phasePowerLimit");
|
|
|
|
// Car
|
|
QTest::addColumn<int>("carPhaseCount");
|
|
|
|
// Charger information and states
|
|
QTest::addColumn<bool>("chargerPower");
|
|
QTest::addColumn<QString>("chargerPhases");
|
|
QTest::addColumn<int>("chargerMaxChargingCurrent");
|
|
QTest::addColumn<int>("chargerMaxChargingCurrentMaxValue");
|
|
|
|
// Current meter power states
|
|
QTest::addColumn<QVariantMap>("meterPhasesPower");
|
|
QTest::addColumn<QVariantMap>("triggerMeterPhasesPower");
|
|
|
|
// Desired result on the evCharger
|
|
QTest::addColumn<int>("expectedChargerMaxChargingCurrent");
|
|
QTest::addColumn<bool>("expectedChargerPower");
|
|
|
|
QTest::newRow("Default: 1 phase, 16A -> 10A")
|
|
<< 32 // phase limit (A) 7360 W
|
|
|
|
<< 1 // carPhaseCount
|
|
|
|
<< true // chargerPower
|
|
<< "A" // chargerPhases
|
|
<< 16 // chargerMaxChargingCurrent
|
|
<< 16 //chargerMaxChargingCurrentMaxValue
|
|
|
|
<< QVariantMap({{"A", 4680}, // W
|
|
{"B", 2000}, // W
|
|
{"C", -6000} }) // W
|
|
|
|
<< QVariantMap({ {"A", 8680}, // W
|
|
{"B", 2000}, // W
|
|
{"C", -6000}}) // W
|
|
|
|
<< 10 // expectedChargerMaxChargingCurrent
|
|
<< true; // expectedChargerPower
|
|
|
|
|
|
QTest::newRow("Default: 1 phase, 16A -> OFF")
|
|
<< 32 // phase limit (A) 7360 W
|
|
|
|
<< 1 // carPhaseCount
|
|
|
|
<< true // chargerPower
|
|
<< "A" // chargerPhases
|
|
<< 16 // chargerMaxChargingCurrent
|
|
<< 16 //chargerMaxChargingCurrentMaxValue
|
|
|
|
<< QVariantMap({{"A", 4680}, // W
|
|
{"B", 2000}, // W
|
|
{"C", -6000}}) // W
|
|
|
|
<< QVariantMap({{"A", 9820}, // W
|
|
{"B", 2000}, // W
|
|
{"C", -6000}}) // W
|
|
|
|
<< 16 // expectedChargerMaxChargingCurrent
|
|
<< false; // expectedChargerPower
|
|
|
|
QTest::newRow("Default: 3 phase, 16A -> 10A")
|
|
<< 32 // phase limit (A) 7360 W
|
|
|
|
<< 3 // carPhaseCount
|
|
|
|
<< true // chargerPower
|
|
<< "ABC" // chargerPhases
|
|
<< 16 // chargerMaxChargingCurrent
|
|
<< 16 //chargerMaxChargingCurrentMaxValue
|
|
|
|
<< QVariantMap({{"A", 4680}, // W
|
|
{"B", 2000}, // W
|
|
{"C", 3200}}) // W
|
|
|
|
<< QVariantMap({{"A", 4680}, // W
|
|
{"B", 8280}, // W
|
|
{"C", 3200}}) // W
|
|
|
|
<< 12 // expectedChargerMaxChargingCurrent
|
|
<< true; // expectedChargerPower
|
|
}
|
|
|
|
void TestCharging::testOverloadProtectionManualMode()
|
|
{
|
|
QFETCH(int, phasePowerLimit);
|
|
|
|
QFETCH(int, carPhaseCount);
|
|
|
|
QFETCH(bool, chargerPower);
|
|
QFETCH(QString, chargerPhases);
|
|
QFETCH(int, chargerMaxChargingCurrent);
|
|
QFETCH(int, chargerMaxChargingCurrentMaxValue);
|
|
|
|
QFETCH(QVariantMap, meterPhasesPower);
|
|
QFETCH(QVariantMap, triggerMeterPhasesPower);
|
|
|
|
QFETCH(int, expectedChargerMaxChargingCurrent);
|
|
QFETCH(bool, expectedChargerPower);
|
|
|
|
QVariantMap params;
|
|
QVariant response, notification;
|
|
QNetworkReply *reply = nullptr;
|
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
|
|
|
// Set phase power limit
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", phasePowerLimit}}));
|
|
QVERIFY(response.toMap().value("params").toMap().value("energyError").toString() == "EnergyErrorNoError");
|
|
|
|
// Add mock meter
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid meterThingId = addMeter();
|
|
QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "Energy.RootMeterChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
|
|
// Make sure this is our root meter now
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("Energy.GetRootMeter");
|
|
QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
notificationSpy.clear();
|
|
|
|
// Set the meter values first so we have the desired execution o charging info changed, and not an extra iteration with 0 W on root meter
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setMeterStates(meterPhasesPower);
|
|
QSignalSpy setStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setStatesReplySpy.count() == 0) setStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// No evaluation, there is no meter configured yet
|
|
|
|
// Add the charger
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid evChargerId = addCharger(chargerPhases, chargerMaxChargingCurrentMaxValue);
|
|
QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Add the car
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid assignedCarId = addCar();
|
|
QVERIFY2(!assignedCarId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Set states of the charger
|
|
|
|
uint effectivePhaseCount = qMin((uint)carPhaseCount, Electricity::getPhaseCount(Electricity::convertPhasesFromString(chargerPhases)));
|
|
QString usedPhases;
|
|
if (effectivePhaseCount >= 1)
|
|
usedPhases.append("A");
|
|
|
|
if (effectivePhaseCount >= 2)
|
|
usedPhases.append("B");
|
|
|
|
if (effectivePhaseCount >= 3)
|
|
usedPhases.append("C");
|
|
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setChargerStates(true, chargerPower, true, usedPhases, chargerMaxChargingCurrent, chargerMaxChargingCurrentMaxValue);
|
|
QSignalSpy setChargerStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setChargerStatesReplySpy.count() == 0) setChargerStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// Set charging info with our charger and car, this should trigger the evaluation
|
|
QVariantMap chargingInfoMap;
|
|
chargingInfoMap.insert("evChargerId", evChargerId);
|
|
if (!assignedCarId.isNull())
|
|
chargingInfoMap.insert("assignedCarId", assignedCarId);
|
|
|
|
chargingInfoMap.insert("chargingMode", "ChargingModeNormal");
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", chargingInfoMap}}));
|
|
verifyEnergyError(response);
|
|
|
|
|
|
// Now add the trigger load on the meter to trigger the overload protection
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setMeterStates(triggerMeterPhasesPower);
|
|
QSignalSpy setMeterStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setMeterStatesReplySpy.count() == 0) setMeterStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// Veify overload protection active
|
|
|
|
// Verify if the charger has been set correctly
|
|
reply = getActionHistory(m_mockChargerDefaultPort);
|
|
QSignalSpy actionHistoryReplySpy(reply, &QNetworkReply::finished);
|
|
if (actionHistoryReplySpy.count() == 0) actionHistoryReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
QByteArray data = reply->readAll();
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
|
|
QVariantList actionHistory = jsonDoc.toVariant().toList();
|
|
//qCDebug(dcTests()) << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
|
|
|
|
// Make sure we got the correct actions
|
|
if (expectedChargerMaxChargingCurrent != 0)
|
|
QVERIFY(verifyActionExecuted(actionHistory, "maxChargingCurrent"));
|
|
|
|
QVERIFY(verifyActionExecuted(actionHistory, "power"));
|
|
|
|
if (expectedChargerMaxChargingCurrent != 0)
|
|
QCOMPARE(getLastValueFromExecutedAction(actionHistory, "maxChargingCurrent", "maxChargingCurrent"), QVariant(expectedChargerMaxChargingCurrent));
|
|
|
|
QCOMPARE(getLastValueFromExecutedAction(actionHistory, "power", "power"), QVariant(expectedChargerPower));
|
|
|
|
removeDevices();
|
|
}
|
|
|
|
void TestCharging::testOverloadProtectionEcoMode_data()
|
|
{
|
|
// Houshold info
|
|
QTest::addColumn<int>("phasePowerLimit");
|
|
|
|
// Car
|
|
QTest::addColumn<int>("carPhaseCount");
|
|
|
|
// Charger information and states
|
|
QTest::addColumn<bool>("chargerPower");
|
|
QTest::addColumn<QString>("chargerPhases");
|
|
QTest::addColumn<int>("chargerMaxChargingCurrent");
|
|
QTest::addColumn<int>("chargerMaxChargingCurrentMaxValue");
|
|
|
|
// Current meter power states
|
|
QTest::addColumn<QVariantMap>("meterPhasesPower");
|
|
QTest::addColumn<QVariantMap>("triggerMeterPhasesPower");
|
|
|
|
// Desired result on the evCharger
|
|
QTest::addColumn<int>("expectedChargerMaxChargingCurrent");
|
|
QTest::addColumn<bool>("expectedChargerPower");
|
|
|
|
QTest::newRow("Default: 1 phase, 16A -> 10A")
|
|
<< 32 // phase limit (A) 7360 W
|
|
|
|
<< 3 // carPhaseCount
|
|
|
|
<< true // chargerPower
|
|
<< "ABC" // chargerPhases
|
|
<< 16 // chargerMaxChargingCurrent
|
|
<< 16 //chargerMaxChargingCurrentMaxValue
|
|
|
|
<< QVariantMap({{"A", 4680}, // W
|
|
{"B", 2000}, // W
|
|
{"C", -6000} }) // W
|
|
|
|
<< QVariantMap({ {"A", 8680}, // W
|
|
{"B", 2000}, // W
|
|
{"C", -6000}}) // W
|
|
|
|
<< 6 // expectedChargerMaxChargingCurrent
|
|
<< true; // expectedChargerPower
|
|
}
|
|
|
|
void TestCharging::testOverloadProtectionEcoMode()
|
|
{
|
|
QFETCH(int, phasePowerLimit);
|
|
|
|
QFETCH(int, carPhaseCount);
|
|
|
|
QFETCH(bool, chargerPower);
|
|
QFETCH(QString, chargerPhases);
|
|
QFETCH(int, chargerMaxChargingCurrent);
|
|
QFETCH(int, chargerMaxChargingCurrentMaxValue);
|
|
|
|
QFETCH(QVariantMap, meterPhasesPower);
|
|
QFETCH(QVariantMap, triggerMeterPhasesPower);
|
|
|
|
QFETCH(int, expectedChargerMaxChargingCurrent);
|
|
QFETCH(bool, expectedChargerPower);
|
|
|
|
QVariantMap params;
|
|
QVariant response, notification;
|
|
QNetworkReply *reply = nullptr;
|
|
QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;
|
|
|
|
// Set phase power limit
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", phasePowerLimit}}));
|
|
QVERIFY(response.toMap().value("params").toMap().value("energyError").toString() == "EnergyErrorNoError");
|
|
|
|
// Add mock meter
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid meterThingId = addMeter();
|
|
QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
notification = checkNotification(notificationSpy, "Energy.RootMeterChanged");
|
|
QCOMPARE(notification.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
|
|
// Make sure this is our root meter now
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
response = injectAndWait("Energy.GetRootMeter");
|
|
QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
|
|
notificationSpy.clear();
|
|
|
|
// Set the meter values first so we have the desired execution o charging info changed, and not an extra iteration with 0 W on root meter
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setMeterStates(meterPhasesPower);
|
|
QSignalSpy setStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setStatesReplySpy.count() == 0) setStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// No evaluation, there is no meter configured yet
|
|
|
|
// Add the charger
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid evChargerId = addCharger(chargerPhases, chargerMaxChargingCurrentMaxValue);
|
|
QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Add the car
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
QUuid assignedCarId = addCar();
|
|
QVERIFY2(!assignedCarId.isNull(), "Did not receive valid ThingId");
|
|
if (notificationSpy.count() == 0) notificationSpy.wait();
|
|
checkNotification(notificationSpy, "Integrations.ThingAdded");
|
|
|
|
// Set states of the charger
|
|
|
|
uint effectivePhaseCount = qMin((uint)carPhaseCount, Electricity::getPhaseCount(Electricity::convertPhasesFromString(chargerPhases)));
|
|
QString usedPhases;
|
|
if (effectivePhaseCount >= 1)
|
|
usedPhases.append("A");
|
|
|
|
if (effectivePhaseCount >= 2)
|
|
usedPhases.append("B");
|
|
|
|
if (effectivePhaseCount >= 3)
|
|
usedPhases.append("C");
|
|
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setChargerStates(true, chargerPower, true, usedPhases, chargerMaxChargingCurrent, chargerMaxChargingCurrentMaxValue);
|
|
QSignalSpy setChargerStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setChargerStatesReplySpy.count() == 0) setChargerStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// Set charging info with our charger and car, this should trigger the evaluation
|
|
QVariantMap chargingInfoMap;
|
|
chargingInfoMap.insert("evChargerId", evChargerId);
|
|
if (!assignedCarId.isNull())
|
|
chargingInfoMap.insert("assignedCarId", assignedCarId);
|
|
|
|
chargingInfoMap.insert("chargingMode", "ChargingModeEco");
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", chargingInfoMap}}));
|
|
verifyEnergyError(response);
|
|
|
|
// Now add the trigger load on the meter to trigger the overload protection
|
|
QTest::qSleep(1500);
|
|
params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
|
|
reply = setMeterStates(triggerMeterPhasesPower);
|
|
QSignalSpy setMeterStatesReplySpy(reply, &QNetworkReply::finished);
|
|
if (setMeterStatesReplySpy.count() == 0) setMeterStatesReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
|
|
// Verify overload protection active
|
|
// Verify if the charger has been set correctly
|
|
reply = getActionHistory(m_mockChargerDefaultPort);
|
|
QSignalSpy actionHistoryReplySpy(reply, &QNetworkReply::finished);
|
|
if (actionHistoryReplySpy.count() == 0) actionHistoryReplySpy.wait();
|
|
QCOMPARE(reply->error(), QNetworkReply::NoError);
|
|
QByteArray data = reply->readAll();
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
|
|
QVariantList actionHistory = jsonDoc.toVariant().toList();
|
|
//qCDebug(dcTests()) << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
|
|
|
|
// Make sure we got the correct actions
|
|
if (expectedChargerMaxChargingCurrent != 0)
|
|
QVERIFY(verifyActionExecuted(actionHistory, "maxChargingCurrent"));
|
|
|
|
QVERIFY(verifyActionExecuted(actionHistory, "power"));
|
|
|
|
if (expectedChargerMaxChargingCurrent != 0)
|
|
QCOMPARE(getLastValueFromExecutedAction(actionHistory, "maxChargingCurrent", "maxChargingCurrent"), QVariant(expectedChargerMaxChargingCurrent));
|
|
|
|
QCOMPARE(getLastValueFromExecutedAction(actionHistory, "power", "power"), QVariant(expectedChargerPower));
|
|
|
|
removeDevices();
|
|
}
|
|
|
|
|
|
QTEST_MAIN(TestCharging)
|
|
|
|
void TestCharging::getChargingInfosFiltersOnCharger()
|
|
{
|
|
// Le paramètre `evChargerId` était DÉCLARÉ au schéma, DOCUMENTÉ (« for all or a single EV
|
|
// charger ») et `Q_UNUSED` dans l'implémentation : accepté, sans effet, sans erreur. Un
|
|
// client qui s'y fiait lisait la configuration d'une AUTRE borne en croyant lire la sienne.
|
|
//
|
|
// Des trois issues possibles — filtrer, disparaître du schéma, rester ignoré — la dernière
|
|
// était la pire : un paramètre absent se voit, un paramètre refusé se voit, un paramètre
|
|
// ignoré ne se voit pas.
|
|
QVariant response;
|
|
response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", 25000}}));
|
|
verifyEnergyError(response);
|
|
removeDevices();
|
|
|
|
QUuid meterThingId = addMeter();
|
|
QVERIFY(!meterThingId.isNull());
|
|
QUuid borneA = addCharger("ABC", 16, 26657);
|
|
QUuid borneB = addCharger("A", 32, 26658);
|
|
QUuid voiture = addCar();
|
|
QVERIFY(!borneA.isNull() && !borneB.isNull() && !voiture.isNull());
|
|
|
|
for (const QUuid &b : {borneA, borneB}) {
|
|
QVariantMap info;
|
|
info.insert("evChargerId", b);
|
|
info.insert("assignedCarId", voiture);
|
|
info.insert("chargingMode", b == borneA ? "ChargingModeEco" : "ChargingModeNormal");
|
|
response = injectAndWait("NymeaEnergy.SetChargingInfo", QVariantMap({{"chargingInfo", info}}));
|
|
verifyEnergyError(response);
|
|
}
|
|
|
|
auto lire = [&](const QVariant &id) {
|
|
QVariantMap p;
|
|
if (id.isValid()) p.insert("evChargerId", id);
|
|
return injectAndWait("NymeaEnergy.GetChargingInfos", p)
|
|
.toMap().value("params").toMap().value("chargingInfos").toList();
|
|
};
|
|
|
|
// ---- Sans paramètre : les deux, comme avant. La correction n'enlève rien. ---------------
|
|
QCOMPARE(lire(QVariant()).count(), 2);
|
|
|
|
// ---- Avec paramètre : UNE seule, et c'est la bonne -------------------------------------
|
|
// Le test porte sur le CONTENU, pas sur le compte : un filtre qui rendrait toujours la
|
|
// première entrée passerait un test qui ne compte que les lignes.
|
|
const QVariantList uneA = lire(borneA);
|
|
QCOMPARE(uneA.count(), 1);
|
|
QCOMPARE(uneA.first().toMap().value("evChargerId").toUuid(), borneA);
|
|
QCOMPARE(uneA.first().toMap().value("chargingMode").toString(), QString("ChargingModeEco"));
|
|
|
|
const QVariantList uneB = lire(borneB);
|
|
QCOMPARE(uneB.count(), 1);
|
|
QCOMPARE(uneB.first().toMap().value("evChargerId").toUuid(), borneB);
|
|
QCOMPARE(uneB.first().toMap().value("chargingMode").toString(), QString("ChargingModeNormal"));
|
|
|
|
// ---- Identifiant inconnu : liste VIDE, jamais une erreur --------------------------------
|
|
// Non ambigu, et c'est ce qui autorise à ne pas lever d'erreur : toute borne configurée a
|
|
// une entrée, donc une liste vide ne peut vouloir dire qu'une chose — « pas cette borne ».
|
|
QCOMPARE(lire(QUuid::createUuid()).count(), 0);
|
|
}
|