Patrick Schurig abcf80a5d3 feat(R3): counts — chaque watt dans un compteur, et GetChargingInfos filtre
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
2026-08-30 10:14:19 +02:00

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);
}