Six appels à `update(QDateTime::currentDateTime())` manquaient la garde ENERGY_SIMULATION que leurs voisins ont (l.124, l.288) : setPhasePowerLimit, setAcquisitionTolerance, et trois dans onActionExecuted — qui partent sur toute action manuelle appliquée à une borne. Un cycle d'arbitrage complet partait donc à l'horloge MURALE au milieu d'une suite qui injecte un temps simulé. Les deux horloges se mélangeaient dans les verrous : l'armement paresseux (ECS-412) figeait m_lastSwitch sur l'heure qu'il était, et un scénario daté en dur se déroulait « avant » son propre armement — elapsed négatif, verrou jamais ouvert. Ce n'est pas un défaut de production : il n'y a là-bas qu'une seule horloge. C'est une faille de REPRODUCTIBILITÉ, et elle touchait tout test de simulation réglant une limite de phase, une tolérance d'acquisition, ou exécutant une action manuelle sur une borne. PREUVE DU CORRECTIF : la garde rendue, testL4ShedsAPacAndSaysItForcedTheLock repasse avec une DATE FIXE, trois fois sur trois — ce qu'il ne pouvait pas faire. L'ancrage sur l'horloge réelle, posé la veille faute d'avoir la cause, était un contournement ; il est retiré, et la date fixe revient parce qu'elle rend les journaux reproductibles. tools/check-simulation-clock.py, câblé en 4/4 de ci-quality.sh, et vérifié mordant : garde retirée, il nomme la ligne exacte. Il a trouvé trois des six déclencheurs dès son premier passage. Pourquoi une vérification STATIQUE et pas un test : la suite ne peut pas dire d'elle-même que son verdict dépend de l'heure, puisque c'est le verdict qui ment. Le défaut n'a été trouvé que parce qu'il échouait cinq fois sur cinq à code identique — une fenêtre de nuisance plus étroite ne basculerait qu'une fois sur vingt, passerait pour une fragilité de temporisation, et serait relancée jusqu'à ce qu'elle passe. C'est la famille de la semaine sous une forme de plus : après l'instrument dont le silence ressemble à un résultat, l'instrument dont le VERDICT ment selon l'heure. Suite complète : simulation 54/54, charging 48/48 identique à la référence ligne à ligne, loadmodel 21/21, spotmarket 32/32, doxygen 0 avertissement. Aucun test ne dépendait de ces cycles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
1992 lines
112 KiB
C++
1992 lines
112 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 "smartchargingmanager.h"
|
|
#include "evcharger.h"
|
|
#include "rootmeter.h"
|
|
#include "energysettings.h"
|
|
|
|
#include <qmath.h>
|
|
|
|
#include <QUrlQuery>
|
|
|
|
#include "loggingcategories.h"
|
|
NYMEA_LOGGING_CATEGORY(dcNymeaEnergy, "NymeaEnergy")
|
|
|
|
SmartChargingManager::SmartChargingManager(EnergyManager *energyManager, ThingManager *thingManager, SpotMarketManager *spotMarketManager, EnergyManagerConfiguration *configuration, QObject *parent):
|
|
QObject{parent},
|
|
m_energyManager{energyManager},
|
|
m_thingManager{thingManager},
|
|
m_spotMarketManager{spotMarketManager},
|
|
m_configuration{configuration}
|
|
{
|
|
EnergySettings settings;
|
|
m_phasePowerConsumptionLimit = settings.value("phasePowerConsumptionLimit", 0).toInt();
|
|
m_acquisitionTolerance = settings.value("acquisitionTolerance", 0.5).toDouble();
|
|
// 3g-1 — DÉFAUT ABAISSÉ de 0,9 à 0,2, et la raison n'est pas cosmétique.
|
|
//
|
|
// Ce réglage confondait une CIBLE et une RÉSERVE. Une cible, c'est le niveau qu'on aimerait
|
|
// atteindre ; une réserve, c'est le niveau sous lequel on ne descend pas. La règle qu'il
|
|
// pilote — « sous ce niveau, rien d'autre n'est servi » — a une sémantique de réserve, et
|
|
// elle était défaultée comme une cible.
|
|
//
|
|
// Tant que la règle ne touchait que le VE, 0,9 se défendait : « ne pas vider la batterie
|
|
// dans une voiture ». Depuis 3g-1 elle gouverne TOUTE l'installation, et 0,9 y devient un
|
|
// gel : sous 90 % de SOC, ni ECS, ni PAC, ni borne. Chez un client cela se manifeste comme
|
|
// un HEMS qui ne pilote plus rien, sans erreur et sans panne — le pire mode de défaillance
|
|
// qui soit, parce qu'il ne se signale pas.
|
|
//
|
|
// 0,2 protège ce qu'une réserve doit protéger — l'autonomie du soir — sans confisquer la
|
|
// journée. Un installateur qui veut « la batterie d'abord » monte la valeur délibérément,
|
|
// maintenant que l'écran l'expose.
|
|
m_batteryLevelConsideration = settings.value("batteryLevelConsideration", 0.2).toDouble();
|
|
|
|
settings.beginGroup("ChargingInfos");
|
|
foreach (const QString &evChargerIdString, settings.childGroups()) {
|
|
ThingId evChargerId = ThingId(evChargerIdString);
|
|
if (thingManager->findConfiguredThing(evChargerId)) {
|
|
settings.beginGroup(evChargerIdString);
|
|
ChargingInfo info(evChargerId);
|
|
info.setAssignedCarId(settings.value("assignedCarId").toUuid());
|
|
info.setChargingMode(static_cast<ChargingInfo::ChargingMode>(settings.value("chargingMode").toInt()));
|
|
if (!settings.contains("endDateTime")) {
|
|
// Pre 0.4 (nymea 1.5) had only endTime
|
|
// TODO: migrate endTime to endDateTime with repeat
|
|
} else {
|
|
info.setEndDateTime(settings.value("endDateTime").toDateTime());
|
|
}
|
|
QList<int> days;
|
|
foreach (const QVariant &day, settings.value("repeatDays").toList()) {
|
|
days.append(day.toInt());
|
|
}
|
|
info.setRepeatDays(days);
|
|
info.setTargetPercentage(settings.value("targetPercentage").toUInt());
|
|
info.setLocale(settings.value("locale").toLocale());
|
|
info.setSpotMarketChargingEnabled(settings.value("spotMarketChargingEnabled", false).toBool());
|
|
info.setDailySpotMarketPercentage(settings.value("dailySpotMarketPercentage", 0).toUInt());
|
|
m_chargingInfos.insert(evChargerId, info);
|
|
settings.endGroup();
|
|
} else {
|
|
qCWarning(dcNymeaEnergy()) << "EV charger with ID" << evChargerId << "not found in system. Not loading configuration.";
|
|
settings.remove(evChargerIdString);
|
|
}
|
|
}
|
|
settings.endGroup();
|
|
|
|
Things evChargers = m_thingManager->configuredThings().filterByInterface("evcharger");
|
|
qCDebug(dcNymeaEnergy) << "SmartChargingManager loading. EV chargers in nymea:" << evChargers.count();
|
|
|
|
foreach (Thing *thing, evChargers) {
|
|
EvCharger *evCharger = new EvCharger(m_thingManager, thing);
|
|
evCharger->setChargingEnabledLockDuration(m_configuration->chargingEnabledLockDuration());
|
|
evCharger->setChargingCurrentLockDuration(m_configuration->chargingCurrentLockDuration());
|
|
m_evChargers.insert(thing->id(), evCharger);
|
|
// Make sure we have a chargingInfo for all the evchargers
|
|
if (!m_chargingInfos.contains(thing->id())) {
|
|
setupEvCharger(thing);
|
|
}
|
|
setupPluggedInHandlers(thing);
|
|
}
|
|
|
|
connect(m_thingManager, &ThingManager::thingAdded, this, &SmartChargingManager::onThingAdded);
|
|
connect(m_thingManager, &ThingManager::thingRemoved, this, &SmartChargingManager::onThingRemoved);
|
|
connect(m_thingManager, &ThingManager::actionExecuted, this, &SmartChargingManager::onActionExecuted);
|
|
|
|
if (m_energyManager->rootMeter()) {
|
|
setupRootMeter(m_energyManager->rootMeter());
|
|
}
|
|
|
|
connect(m_energyManager, &EnergyManager::rootMeterChanged, this, [=](){
|
|
setupRootMeter(m_energyManager->rootMeter());
|
|
});
|
|
|
|
#ifndef ENERGY_SIMULATION
|
|
connect(m_energyManager->logs(), &EnergyLogs::powerBalanceEntryAdded, this, [this](EnergyLogs::SampleRate sampleRate, const PowerBalanceLogEntry &entry) {
|
|
QDateTime now = QDateTime::currentDateTime();
|
|
if (sampleRate == EnergyLogs::SampleRate1Min &&
|
|
entry.timestamp().date() == now.date() &&
|
|
entry.timestamp().time().hour() == now.time().hour() &&
|
|
entry.timestamp().time().minute() == now.time().minute() ) {
|
|
update(QDateTime::currentDateTime());
|
|
}
|
|
});
|
|
connect(m_energyManager->logs(), &EnergyLogs::thingPowerEntryAdded, this, [this](EnergyLogs::SampleRate sampleRate, const ThingPowerLogEntry &entry){
|
|
QDateTime now = QDateTime::currentDateTime();
|
|
if (sampleRate == EnergyLogs::SampleRate1Min &&
|
|
entry.timestamp().date() == now.date() &&
|
|
entry.timestamp().time().hour() == now.time().hour() &&
|
|
entry.timestamp().time().minute() == now.time().minute() ) {
|
|
updateManualSoCsWithMeter(sampleRate, entry);
|
|
}
|
|
});
|
|
connect(m_energyManager, &EnergyManager::powerBalanceChanged, this, [this]() {
|
|
verifyOverloadProtection(QDateTime::currentDateTime());
|
|
});
|
|
#endif
|
|
|
|
connect(m_spotMarketManager, &SpotMarketManager::enabledChanged, this, [this](bool enabled){
|
|
if (enabled)
|
|
return;
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Spotmarket has been disabled. Disable the spotmarket for all charging infos...";
|
|
foreach(const ChargingInfo &ci, m_chargingInfos.values()) {
|
|
ChargingInfo chargingInfo = ci;
|
|
chargingInfo.setSpotMarketChargingEnabled(false);
|
|
setChargingInfo(chargingInfo);
|
|
}
|
|
});
|
|
}
|
|
|
|
uint SmartChargingManager::phasePowerLimit() const
|
|
{
|
|
return m_phasePowerConsumptionLimit;
|
|
}
|
|
|
|
void SmartChargingManager::setPhasePowerLimit(uint phasePowerLimit)
|
|
{
|
|
if (m_phasePowerConsumptionLimit != phasePowerLimit) {
|
|
m_phasePowerConsumptionLimit = phasePowerLimit;
|
|
emit phasePowerLimitChanged(m_phasePowerConsumptionLimit);
|
|
|
|
EnergySettings settings;
|
|
settings.setValue("phasePowerConsumptionLimit", m_phasePowerConsumptionLimit);
|
|
|
|
#ifndef ENERGY_SIMULATION
|
|
// [ETM] DÉCLENCHEUR RÉEL — exclu en simulation, comme ses voisins (l.124, l.288).
|
|
//
|
|
// Cet appel manquait la garde, et c'est un défaut de REPRODUCTIBILITÉ, pas de
|
|
// production : il lance un cycle d'arbitrage complet à l'horloge MURALE au milieu
|
|
// d'une suite qui, elle, injecte un temps simulé. Les deux horloges se mélangent alors
|
|
// dans les verrous — l'armement paresseux (ECS-412) fige `m_lastSwitch` sur l'heure
|
|
// qu'il est, et un scénario daté en dur se déroule « avant » son propre armement,
|
|
// `elapsed` négatif. Le test passe ou échoue SELON L'HEURE DE LA JOURNÉE.
|
|
update(QDateTime::currentDateTime());
|
|
#endif
|
|
}
|
|
}
|
|
|
|
double SmartChargingManager::acquisitionTolerance() const
|
|
{
|
|
return m_acquisitionTolerance;
|
|
}
|
|
|
|
void SmartChargingManager::setAcquisitionTolerance(double acquisitionTolerance)
|
|
{
|
|
if (m_acquisitionTolerance != acquisitionTolerance) {
|
|
m_acquisitionTolerance = qMax(0.0, qMin(1.0, acquisitionTolerance));
|
|
emit acquisitionToleranceChanged(m_acquisitionTolerance);
|
|
|
|
EnergySettings settings;
|
|
settings.setValue("acquisitionTolerance", m_acquisitionTolerance);
|
|
|
|
#ifndef ENERGY_SIMULATION
|
|
// [ETM] DÉCLENCHEUR RÉEL — exclu en simulation, comme ses voisins (l.124, l.288).
|
|
//
|
|
// Cet appel manquait la garde, et c'est un défaut de REPRODUCTIBILITÉ, pas de
|
|
// production : il lance un cycle d'arbitrage complet à l'horloge MURALE au milieu
|
|
// d'une suite qui, elle, injecte un temps simulé. Les deux horloges se mélangent alors
|
|
// dans les verrous — l'armement paresseux (ECS-412) fige `m_lastSwitch` sur l'heure
|
|
// qu'il est, et un scénario daté en dur se déroule « avant » son propre armement,
|
|
// `elapsed` négatif. Le test passe ou échoue SELON L'HEURE DE LA JOURNÉE.
|
|
update(QDateTime::currentDateTime());
|
|
#endif
|
|
}
|
|
}
|
|
|
|
double SmartChargingManager::batteryLevelConsideration() const
|
|
{
|
|
return m_batteryLevelConsideration;
|
|
}
|
|
|
|
void SmartChargingManager::setBatteryLevelConsideration(double batteryLevelConsideration)
|
|
{
|
|
if (m_batteryLevelConsideration != batteryLevelConsideration) {
|
|
m_batteryLevelConsideration = qMax(0.0, qMin(1.0, batteryLevelConsideration));
|
|
emit batteryLevelConsiderationChanged(m_batteryLevelConsideration);
|
|
|
|
EnergySettings settings;
|
|
settings.setValue("batteryLevelConsideration", m_batteryLevelConsideration);
|
|
}
|
|
}
|
|
|
|
bool SmartChargingManager::lockOnUnplug() const
|
|
{
|
|
QSettings settings(NymeaSettings::settingsPath() + "/energy.conf", QSettings::IniFormat);
|
|
return settings.value("lockOnUnplug").toBool();
|
|
}
|
|
|
|
void SmartChargingManager::setLockOnUnplug(bool lockOnUnplug)
|
|
{
|
|
QSettings settings(NymeaSettings::settingsPath() + "/energy.conf", QSettings::IniFormat);
|
|
if (settings.value("lockOnUnplug").toBool() != lockOnUnplug) {
|
|
settings.setValue("lockOnUnplug", lockOnUnplug);
|
|
emit lockOnUnplugChanged(lockOnUnplug);
|
|
}
|
|
}
|
|
|
|
ChargingInfos SmartChargingManager::chargingInfos() const
|
|
{
|
|
return m_chargingInfos.values();
|
|
}
|
|
|
|
ChargingInfo SmartChargingManager::chargingInfo(const ThingId &evChargerId) const
|
|
{
|
|
return m_chargingInfos.value(evChargerId);
|
|
}
|
|
|
|
EnergyManager::EnergyError SmartChargingManager::setChargingInfo(const ChargingInfo &chargingInfo)
|
|
{
|
|
qCDebug(dcNymeaEnergy()) << "Setting charging info:" << chargingInfo;
|
|
Thing *evCharger = m_thingManager->findConfiguredThing(chargingInfo.evChargerId());
|
|
if (!evCharger || !evCharger->thingClass().interfaces().contains("evcharger")) {
|
|
qCWarning(dcNymeaEnergy()) << "No such EV charger:" << chargingInfo.evChargerId();
|
|
return EnergyManager::EnergyErrorInvalidParameter;
|
|
}
|
|
|
|
if (chargingInfo.targetPercentage() > 100) {
|
|
qCWarning(dcNymeaEnergy()) << "Charging info target percentage out of range:" << chargingInfo.targetPercentage();
|
|
return EnergyManager::EnergyErrorInvalidParameter;
|
|
}
|
|
|
|
foreach (int day, chargingInfo.repeatDays()) {
|
|
if (day < 1 || day > 7) {
|
|
qCWarning(dcNymeaEnergy()) << "Charging info repeat days invalid. All days must be within 1 (Mon) and 7 (Sun):" << chargingInfo.repeatDays();
|
|
return EnergyManager::EnergyErrorInvalidParameter;
|
|
}
|
|
}
|
|
|
|
if (chargingInfo.spotMarketChargingEnabled() && !m_spotMarketManager->enabled()) {
|
|
qCWarning(dcNymeaEnergy()) << "Charging info has spot market enabled, but there is no provider enabled in the system. Disabeling spotmarket for this charger.";
|
|
return EnergyManager::EnergyErrorInvalidParameter;
|
|
}
|
|
|
|
if (chargingInfo.dailySpotMarketPercentage() > 100) {
|
|
qCWarning(dcNymeaEnergy()) << "Charging info daily spot market percentage value out of range:" << chargingInfo.dailySpotMarketPercentage() << "is not in the range [0 - 100] %";
|
|
return EnergyManager::EnergyErrorInvalidParameter;
|
|
}
|
|
|
|
|
|
if (m_chargingInfos.value(chargingInfo.evChargerId()) != chargingInfo) {
|
|
// Make sure the assigned car exists
|
|
Thing *assignedCar = m_thingManager->findConfiguredThing(chargingInfo.assignedCarId());
|
|
if (!chargingInfo.assignedCarId().isNull() && !assignedCar) {
|
|
qCWarning(dcNymeaEnergy()) << "The given assigned car id cannot be found in the system.";
|
|
return EnergyManager::EnergyErrorInvalidParameter;
|
|
}
|
|
|
|
if (m_chargingInfos.value(evCharger->id()).chargingMode() != chargingInfo.chargingMode()) {
|
|
onChargingModeChanged(evCharger->id(), chargingInfo);
|
|
}
|
|
|
|
m_chargingInfos[chargingInfo.evChargerId()] = chargingInfo;
|
|
qCInfo(dcNymeaEnergy()) << "Charging info for" << evCharger->name() << "set to" << chargingInfo;
|
|
emit chargingInfoChanged(chargingInfo);
|
|
|
|
storeChargingInfo(chargingInfo);
|
|
|
|
#ifndef ENERGY_SIMULATION
|
|
update(QDateTime::currentDateTime());
|
|
#endif
|
|
}
|
|
|
|
return EnergyManager::EnergyErrorNoError;
|
|
}
|
|
|
|
ChargingSchedules SmartChargingManager::chargingSchedules() const
|
|
{
|
|
ChargingSchedules schedules;
|
|
foreach(const ChargingSchedules &cs, m_chargingSchedules.values()) {
|
|
schedules << cs;
|
|
}
|
|
return schedules;
|
|
}
|
|
|
|
SpotMarketManager *SmartChargingManager::spotMarketManager() const
|
|
{
|
|
return m_spotMarketManager;
|
|
}
|
|
|
|
#ifdef ENERGY_SIMULATION
|
|
void SmartChargingManager::simulationCallUpdate(const QDateTime &dateTime)
|
|
{
|
|
update(dateTime);
|
|
verifyOverloadProtection(dateTime);
|
|
}
|
|
|
|
void SmartChargingManager::simulationCallUpdateManualSoCsWithMeter(EnergyLogs::SampleRate sampleRate, const ThingPowerLogEntry &entry)
|
|
{
|
|
updateManualSoCsWithMeter(sampleRate, entry);
|
|
}
|
|
|
|
#endif
|
|
|
|
void SmartChargingManager::update(const QDateTime ¤tDateTime)
|
|
{
|
|
qCDebug(dcNymeaEnergy()) << "Updating smart charging";
|
|
updateManualSoCsWithoutMeter(currentDateTime);
|
|
prepareInformation(currentDateTime);
|
|
verifyOverloadProtection(currentDateTime);
|
|
verifyOverloadProtectionRecovery(currentDateTime);
|
|
planSpotMarketCharging(currentDateTime);
|
|
planSurplusCharging(currentDateTime);
|
|
adjustEvChargers(currentDateTime);
|
|
}
|
|
|
|
|
|
void SmartChargingManager::verifyOverloadProtection(const QDateTime ¤tDateTime)
|
|
{
|
|
if (!m_rootMeter) {
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: No root meter configured, the overload protection is not available.";
|
|
return;
|
|
}
|
|
|
|
// Verify if any of the phases is approaching the limit
|
|
if (m_phasePowerConsumptionLimit == 0) {
|
|
// Power limit not set up
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: No power limit configured, the overload protection is disabled.";
|
|
return;
|
|
}
|
|
|
|
QHash<QString, double> currentPhaseConsumption = {{"A", m_rootMeter->currentPowerPhaseA()},
|
|
{"B", m_rootMeter->currentPowerPhaseB()},
|
|
{"C", m_rootMeter->currentPowerPhaseC()}};
|
|
|
|
bool limitExceeded = false;
|
|
|
|
double consumptionLimitInWatt = 230 * m_phasePowerConsumptionLimit;
|
|
|
|
// Note: for now we assume the phases are always connected this way:
|
|
// EV phase A -> house phase A
|
|
// EV phase B -> house phase B
|
|
// EV phase C -> house phase C
|
|
|
|
// In case of single phase charging, we assume power on phase A
|
|
// EV phase A -> house phase A
|
|
|
|
// Once we have a mapping of consumer phases to house phases, we can be much more prcise here and also handle
|
|
// multiple chargers connected on different phases in single phase mode.
|
|
|
|
double maxRequiredThrottlePower = 0;
|
|
QHash<QString, double> requiredThrottlePower = {{"A", 0},
|
|
{"B", 0},
|
|
{"C", 0}};
|
|
|
|
foreach (const QString &phase, currentPhaseConsumption.keys()) {
|
|
if (currentPhaseConsumption.value(phase) > consumptionLimitInWatt) {
|
|
qCInfo(dcNymeaEnergy()) << "Overload protection: Phase" << phase << "exceeding limit:" << currentPhaseConsumption.value(phase) << "W (" << (currentPhaseConsumption.value(phase) / 230) << "A). Maximum allowance:" << consumptionLimitInWatt << "W (" << m_phasePowerConsumptionLimit << "A)";
|
|
limitExceeded = true;
|
|
double overshot = currentPhaseConsumption.value(phase) - consumptionLimitInWatt;
|
|
requiredThrottlePower[phase] = overshot;
|
|
if (overshot > maxRequiredThrottlePower) {
|
|
maxRequiredThrottlePower = overshot;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (limitExceeded) {
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Triggered, maximum power consumption per phase is:"
|
|
<< consumptionLimitInWatt << "[W], Overshot on phases:"
|
|
<< "A:" << requiredThrottlePower.value("A") << "[W],"
|
|
<< "B:" << requiredThrottlePower.value("B") << "[W],"
|
|
<< "C:" << requiredThrottlePower.value("C") << "[W]";
|
|
|
|
foreach (EvCharger *evCharger, m_evChargers) {
|
|
|
|
// Evaluate only charger which are actually charging and which are connected to the overloaded phase
|
|
if (!evCharger->chargingEnabled())
|
|
continue;
|
|
|
|
|
|
if (m_chargingInfos.value(evCharger->id()).chargingMode() == ChargingInfo::ChargingModeNormal && !m_overloadProtectionActive.value(evCharger, false)) {
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Activated for charger in normal mode" << evCharger->name();
|
|
m_overloadProtectionActive[evCharger] = true;
|
|
}
|
|
|
|
ChargingAction action;
|
|
|
|
if (evCharger->phaseCount() == 1) {
|
|
|
|
const QString phase = chargerPhaseKey(evCharger);
|
|
if (requiredThrottlePower.value(phase) > 0) {
|
|
int throttleAmpere = qCeil(requiredThrottlePower.value(phase) / 230);
|
|
int desiredFallbackAmpere = evCharger->maxChargingCurrent() - throttleAmpere;
|
|
|
|
if (desiredFallbackAmpere < static_cast<int>(m_processInfos[evCharger].minimalChargingCurrent)) {
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Need to switch of charging for" << evCharger->name();
|
|
action = ChargingAction(false, evCharger->maxChargingCurrent(), evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection, true);
|
|
} else {
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Adjusting charger from" << evCharger->maxChargingCurrent() << "[A] ->" << desiredFallbackAmpere << "[A]";
|
|
action = ChargingAction(true, desiredFallbackAmpere, evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection);
|
|
}
|
|
}
|
|
} else {
|
|
// Charger is on all phases
|
|
int throttleAmpere = qCeil(maxRequiredThrottlePower / 230);
|
|
int desiredFallbackAmpere = evCharger->maxChargingCurrent() - throttleAmpere;
|
|
|
|
if (desiredFallbackAmpere < static_cast<int>(m_processInfos[evCharger].minimalChargingCurrent)) {
|
|
|
|
// TODO: Check if switching to one phase would solve the overshot issue on the phases
|
|
// if (evCharger->canSetPhaseCount()) {
|
|
// }
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Need to switch of charging for" << evCharger->name();
|
|
action = ChargingAction(false, evCharger->maxChargingCurrent(), evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection, true);
|
|
} else {
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Adjusting charger from" << evCharger->maxChargingCurrent() << "[A] ->" << desiredFallbackAmpere << "[A]";
|
|
action = ChargingAction(true, desiredFallbackAmpere, evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection);
|
|
}
|
|
}
|
|
|
|
executeChargingAction(evCharger, action, currentDateTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SmartChargingManager::verifyOverloadProtectionRecovery(const QDateTime ¤tDateTime)
|
|
{
|
|
// Let's evaluate if we can move back any charger which is in normal mode,
|
|
// for the eco mode chargers the rest of the logic should do the trick...
|
|
|
|
foreach (EvCharger *evCharger, m_evChargers) {
|
|
if (m_chargingInfos.value(evCharger->id()).chargingMode() == ChargingInfo::ChargingModeNormal && m_overloadProtectionActive.value(evCharger, false)) {
|
|
// Check if we can move back to the user desired charging level
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Evaluating normal mode charger" << evCharger->name() << "which has been set originally to"
|
|
<< manualChargingEnabled(evCharger->id()) << manualMaxChargingCurrent(evCharger->id()) << "[A]";
|
|
|
|
|
|
QHash<QString, double> currentPhaseConsumption = {{"A", m_rootMeter->currentPowerPhaseA()},
|
|
{"B", m_rootMeter->currentPowerPhaseB()},
|
|
{"C", m_rootMeter->currentPowerPhaseC()}};
|
|
|
|
double consumptionLimitInWatt = 230 * m_phasePowerConsumptionLimit;
|
|
|
|
QHash<QString, double> availablePhasePower = {{"A", 0},
|
|
{"B", 0},
|
|
{"C", 0}};
|
|
|
|
// Calculate available power on each phase
|
|
foreach (const QString &phase, currentPhaseConsumption.keys()) {
|
|
double phasePower = currentPhaseConsumption.value(phase);
|
|
if (phasePower > consumptionLimitInWatt)
|
|
continue;
|
|
|
|
availablePhasePower[phase] = consumptionLimitInWatt - phasePower;
|
|
}
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Available phase power:"
|
|
<< "A:" << availablePhasePower.value("A") << "[W],"
|
|
<< "B:" << availablePhasePower.value("B") << "[W],"
|
|
<< "C:" << availablePhasePower.value("C") << "[W]";
|
|
|
|
// Check if the charger has been switched of or just throttled
|
|
if (evCharger->chargingEnabled()) {
|
|
// Charger has been throttled
|
|
double requiredRestorePhasePower = (manualMaxChargingCurrent(evCharger->id()) - evCharger->maxChargingCurrent()) * 230;
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: For restoring the original charging power at least" << requiredRestorePhasePower << "[W] per phase are required.";
|
|
|
|
bool restoreCharger = false;
|
|
|
|
if (evCharger->phaseCount() == 1) {
|
|
if (availablePhasePower.value(chargerPhaseKey(evCharger)) >= requiredRestorePhasePower) {
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Enought power available to restore the original configuration"
|
|
<< manualMaxChargingCurrent(evCharger->id()) << "[A]";
|
|
restoreCharger = true;
|
|
}
|
|
} else {
|
|
|
|
bool enoughtOnAllPhases = true;
|
|
foreach(const QString &phase, availablePhasePower.keys()) {
|
|
if (availablePhasePower.value(phase) < requiredRestorePhasePower) {
|
|
enoughtOnAllPhases = false;
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Not enought power available on phase" << phase
|
|
<< availablePhasePower.value(phase) << "[W]";
|
|
}
|
|
}
|
|
|
|
if (enoughtOnAllPhases) {
|
|
restoreCharger = true;
|
|
}
|
|
|
|
}
|
|
|
|
if (restoreCharger) {
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Restoring charger to" << manualMaxChargingCurrent(evCharger->id()) << "[A]";
|
|
ChargingAction action = ChargingAction(true, manualMaxChargingCurrent(evCharger->id()) , evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection);
|
|
executeChargingAction(evCharger, action, currentDateTime);
|
|
m_overloadProtectionActive[evCharger] = false;
|
|
}
|
|
|
|
} else {
|
|
// Charging has been disabled by overload protection, let's see if we can switch back on to the minimal
|
|
// charging current, once we switched it back on, the section above should handle the recovery to the original values.
|
|
|
|
double requiredRestorePhasePower = m_processInfos.value(evCharger).minimalChargingCurrent * 230;
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: At least" << requiredRestorePhasePower << "[W] per phase are needed for switching the charger back on.";
|
|
bool restoreChargerPower = false;
|
|
|
|
if (evCharger->phaseCount() == 1) {
|
|
if (availablePhasePower.value(chargerPhaseKey(evCharger)) >= requiredRestorePhasePower) {
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Enought power available to start charging using the minimal charging current of"
|
|
<< m_processInfos.value(evCharger).minimalChargingCurrent << "[A]";
|
|
restoreChargerPower = true;
|
|
}
|
|
} else {
|
|
|
|
bool enoughtOnAllPhases = true;
|
|
foreach(const QString &phase, availablePhasePower.keys()) {
|
|
if (availablePhasePower.value(phase) < requiredRestorePhasePower) {
|
|
enoughtOnAllPhases = false;
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Not enought power available on phase" << phase
|
|
<< availablePhasePower.value(phase) << "[W] for switching the charger back on.";
|
|
}
|
|
}
|
|
|
|
if (enoughtOnAllPhases) {
|
|
restoreChargerPower = true;
|
|
}
|
|
}
|
|
|
|
if (restoreChargerPower) {
|
|
qCDebug(dcNymeaEnergy()) << "Overload protection: Reenable charging at a minimum of" << m_processInfos.value(evCharger).minimalChargingCurrent << "[A]";
|
|
ChargingAction action = ChargingAction(true, m_processInfos.value(evCharger).minimalChargingCurrent, evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection);
|
|
executeChargingAction(evCharger, action, currentDateTime);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SmartChargingManager::prepareInformation(const QDateTime ¤tDateTime)
|
|
{
|
|
// We don't assume we have the entire phase power for charging...
|
|
double housePhaseLimitPower = m_phasePowerConsumptionLimit * 230;
|
|
|
|
qCDebug(dcNymeaEnergy()) << "---------------- Prepare information ---------------";
|
|
|
|
// 1. Filter out any EV chargers wich we can ignore entirely
|
|
EvChargers evChargers;
|
|
foreach (EvCharger *evCharger, m_evChargers) {
|
|
|
|
Thing *car = m_thingManager->findConfiguredThing(m_chargingInfos.value(evCharger->thing()->id()).assignedCarId());
|
|
if (!car) {
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name() << "has no car assigned. Ignoring...";
|
|
continue;
|
|
}
|
|
|
|
// [ETM] Le repli ne concerne QUE les modes qui EXIGENT une échéance. La condition
|
|
// portait sur « mode != Normal », donc elle emportait aussi les deux modes qui n'ont
|
|
// jamais eu d'échéance :
|
|
// - ChargingModeEcoWithMinCurrent était réécrit en ChargingModeEco au cycle suivant,
|
|
// PERSISTÉ et notifié — le mode « éco + courant minimum garanti » ne survivait
|
|
// donc jamais à un cycle, et la branche 4 d'adjustEvChargers() qui l'applique
|
|
// était morte. Réglage détruit en silence, du point de vue de l'app.
|
|
// - ChargingModeEco était « replié » sur lui-même à CHAQUE cycle : setChargingMode,
|
|
// storeChargingInfo (écriture disque) et chargingInfoChanged (notification) sans
|
|
// qu'aucune valeur ne change. Un *Changed qui ne change rien (invariant 11).
|
|
// Les deux autres sites du même repli (planSpotMarketCharging, planSurplusCharging)
|
|
// portent déjà cette garde ; c'est ce site-ci qui l'avait perdue en 7588473.
|
|
const ChargingInfo::ChargingMode currentMode = m_chargingInfos.value(evCharger->id()).chargingMode();
|
|
const bool exigeUneEcheance = currentMode == ChargingInfo::ChargingModeEcoWithTargetTime
|
|
|| currentMode == ChargingInfo::ChargingModeEcoMinWithTargetTime;
|
|
|
|
QDateTime endDateTime = m_chargingInfos.value(evCharger->id()).nextEndTime(currentDateTime);
|
|
if (!endDateTime.isValid() && exigeUneEcheance) {
|
|
qCDebug(dcNymeaEnergy()).nospace() << "No valid target time set for " << evCharger->name();
|
|
ChargingInfo::ChargingMode fallbackMode;
|
|
if (currentMode == ChargingInfo::ChargingModeEcoMinWithTargetTime) {
|
|
qCDebug(dcNymeaEnergy()) << "Resetting to ECO with min current mode.";
|
|
fallbackMode = ChargingInfo::ChargingModeEcoWithMinCurrent;
|
|
} else {
|
|
qCDebug(dcNymeaEnergy()) << "Resetting to ECO mode without time.";
|
|
fallbackMode = ChargingInfo::ChargingModeEco;
|
|
}
|
|
m_chargingInfos[evCharger->id()].setChargingMode(fallbackMode);
|
|
storeChargingInfo(m_chargingInfos.value(evCharger->id()));
|
|
emit chargingInfoChanged(m_chargingInfos.value(evCharger->id()));
|
|
}
|
|
|
|
if (!evCharger->available()) {
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name() << "is not available. Ignoring...";
|
|
continue;
|
|
}
|
|
|
|
evChargers.append(evCharger);
|
|
}
|
|
|
|
// 2. Prepare all information avaialble for each charger
|
|
foreach (EvCharger *evCharger, evChargers) {
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Prepare information for" << evCharger->name();
|
|
|
|
// Car information
|
|
Thing *assignedCar = m_thingManager->findConfiguredThing(m_chargingInfos.value(evCharger->thing()->id()).assignedCarId());
|
|
m_processInfos[evCharger].carPhaseCount = assignedCar->stateValue("phaseCount").toUInt();
|
|
m_processInfos[evCharger].carBatteryLevel = assignedCar->stateValue("batteryLevel").toInt();
|
|
m_processInfos[evCharger].carCapacityInWh = assignedCar->stateValue("capacity").toDouble() * 1000;
|
|
m_processInfos[evCharger].carCapacityInkWh = assignedCar->stateValue("capacity").toDouble();
|
|
m_processInfos[evCharger].carTotalEnergy = assignedCar->stateValue("capacity").toDouble();
|
|
m_processInfos[evCharger].carChargingEnergyLoss = assignedCar->setting("chargingEnergyLoss").toUInt();
|
|
m_processInfos[evCharger].carChargingEnergyLossFactor = 1.0 - (m_processInfos.value(evCharger).carChargingEnergyLoss / 100.0);
|
|
|
|
m_processInfos[evCharger].minimalChargingCurrent = qMax(evCharger->maxChargingCurrentMinValue(), assignedCar->stateValue("minChargingCurrent").toUInt());
|
|
|
|
qCDebug(dcNymeaEnergy()).nospace() << "Car: state of charge: " << m_processInfos.value(evCharger).carBatteryLevel << "%"
|
|
<< ", capacity: "<< m_processInfos.value(evCharger).carCapacityInWh << "Wh"
|
|
<< ", charging energy loss factor " << m_processInfos.value(evCharger).carChargingEnergyLossFactor
|
|
<< ", phases: " << m_processInfos.value(evCharger).carPhaseCount;
|
|
|
|
// Evaluate phase count switching
|
|
if (evCharger->canSetPhaseCount() && m_processInfos.value(evCharger).carPhaseCount > 1) {
|
|
if (evCharger->thing()->state("desiredPhaseCount").possibleValues().contains(1) && evCharger->thing()->state("desiredPhaseCount").possibleValues().contains(3)) {
|
|
// Both, car and EV charger have 3 phases available and we have a desired phase count, lets set the
|
|
qCDebug(dcNymeaEnergy()).nospace() << "Phase count switching is possible with this car/charger combination.";
|
|
m_processInfos[evCharger].canSwitchPhaseCount = true;
|
|
m_processInfos[evCharger].effectivePhaseCount = qMin(evCharger->phaseCount(), m_processInfos.value(evCharger).carPhaseCount);
|
|
m_processInfos[evCharger].minPossiblePhaseCount = 1;
|
|
m_processInfos[evCharger].maxPossiblePhaseCount = 3;
|
|
m_processInfos[evCharger].maxPossiblePhases = Electricity::PhaseAll;
|
|
} else {
|
|
// Verify if both, evCharger and car have at least 3 phases, otherwise we can skip any further checks regarding phase switching
|
|
m_processInfos[evCharger].canSwitchPhaseCount = false;
|
|
m_processInfos[evCharger].maxPossiblePhaseCount = qMin(evCharger->phaseCount(), m_processInfos.value(evCharger).carPhaseCount);
|
|
m_processInfos[evCharger].minPossiblePhaseCount = m_processInfos.value(evCharger).maxPossiblePhaseCount;
|
|
m_processInfos[evCharger].maxPossiblePhases = getAscendingPhasesForCount(m_processInfos.value(evCharger).maxPossiblePhaseCount);
|
|
m_processInfos[evCharger].effectivePhaseCount = m_processInfos.value(evCharger).maxPossiblePhaseCount;
|
|
qCDebug(dcNymeaEnergy()).nospace() << "Phase count switching is not possible with this car/charger combination. Using " << m_processInfos.value(evCharger).effectivePhaseCount << " phases.";
|
|
}
|
|
} else {
|
|
// Either the charger can not set phases or the car has only one phase
|
|
qCDebug(dcNymeaEnergy()).nospace() << "Phase count switching is not possible with this car/charger combination. Using only 1-phase charging.";
|
|
m_processInfos[evCharger].canSwitchPhaseCount = false;
|
|
m_processInfos[evCharger].maxPossiblePhaseCount = qMin(evCharger->phaseCount(), m_processInfos.value(evCharger).carPhaseCount);
|
|
m_processInfos[evCharger].minPossiblePhaseCount = m_processInfos.value(evCharger).maxPossiblePhaseCount;
|
|
m_processInfos[evCharger].maxPossiblePhases = getAscendingPhasesForCount(m_processInfos.value(evCharger).maxPossiblePhaseCount);
|
|
m_processInfos[evCharger].effectivePhaseCount = m_processInfos.value(evCharger).maxPossiblePhaseCount;
|
|
}
|
|
|
|
// If possible and the charger provides meter information, check which phases get used based on the power information,
|
|
// otherwise fall back to an ascending phase order
|
|
Electricity::Phases meteredPhases = evCharger->meteredPhases();
|
|
if (meteredPhases != Electricity::PhaseNone) {
|
|
m_processInfos[evCharger].effectivePhases = meteredPhases;
|
|
m_processInfos[evCharger].effectivePhaseCount = Electricity::getPhaseCount(meteredPhases);
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Effective phase count for charging including car phase limit (metered)"
|
|
<< m_processInfos.value(evCharger).effectivePhaseCount << "phases in use:"
|
|
<< Electricity::convertPhasesToString(m_processInfos.value(evCharger).effectivePhases );
|
|
} else {
|
|
// Fallback to ascending order
|
|
m_processInfos[evCharger].effectivePhases = getAscendingPhasesForCount(m_processInfos.value(evCharger).effectivePhaseCount);
|
|
qCDebug(dcNymeaEnergy()) << "Effective phase count for charging including car phase limit:" << m_processInfos.value(evCharger).effectivePhaseCount
|
|
<< Electricity::convertPhasesToString(m_processInfos.value(evCharger).effectivePhases);
|
|
}
|
|
|
|
m_processInfos[evCharger].chargerPhaseLimitPower = evCharger->maxChargingCurrentMaxValue() * m_processInfos.value(evCharger).voltage;
|
|
qCDebug(dcNymeaEnergy()) << "Household phase limit:" << housePhaseLimitPower << "[W] |" << m_phasePowerConsumptionLimit << "A - EV charger phase limit:"
|
|
<< m_processInfos.value(evCharger).chargerPhaseLimitPower << "[W] |" << evCharger->maxChargingCurrentMaxValue() << "A";
|
|
m_processInfos[evCharger].phaseLimitPower = qMin(housePhaseLimitPower, m_processInfos.value(evCharger).chargerPhaseLimitPower);
|
|
// Note: we have to consider the losses while charging according to the car. That means we need more time to charge the required energy
|
|
m_processInfos[evCharger].totalChargeHours = m_processInfos.value(evCharger).carCapacityInWh / (m_processInfos.value(evCharger).phaseLimitPower * m_processInfos.value(evCharger).carChargingEnergyLossFactor * m_processInfos.value(evCharger).maxPossiblePhaseCount);
|
|
qCDebug(dcNymeaEnergy()).nospace() << "This car needs " << QTime::fromMSecsSinceStartOfDay(m_processInfos.value(evCharger).totalChargeHours * 60 * 60000).toString("hh:mm:ss") << " to charge from 0 to 100% at a charging rate of "
|
|
<< m_processInfos.value(evCharger).phaseLimitPower << "W / phase (including " << m_processInfos.value(evCharger).carChargingEnergyLoss << "% loss) on phase(s) " << Electricity::convertPhasesToString(m_processInfos.value(evCharger).maxPossiblePhases);
|
|
m_processInfos[evCharger].remainingPercentageToCharge = qMax(0, static_cast<int>(m_chargingInfos.value(evCharger->id()).targetPercentage()) - m_processInfos.value(evCharger).carBatteryLevel);
|
|
|
|
// Calculate time
|
|
m_processInfos[evCharger].remainingHoursToCharge = m_processInfos.value(evCharger).totalChargeHours * m_processInfos.value(evCharger).remainingPercentageToCharge / 100.0;
|
|
m_processInfos[evCharger].projectedEndTime = currentDateTime.addSecs(m_processInfos.value(evCharger).remainingHoursToCharge * 60 * 60);
|
|
qCDebug(dcNymeaEnergy()) << "This car still need to charge" << QTime::fromMSecsSinceStartOfDay(m_processInfos.value(evCharger).remainingHoursToCharge * 60 * 60000).toString("hh:mm:ss")
|
|
<< "to charge" << m_processInfos.value(evCharger).remainingPercentageToCharge << "% using a power rate of" << m_processInfos.value(evCharger).phaseLimitPower
|
|
<< "W on phase(s)" << Electricity::convertPhasesToString(m_processInfos.value(evCharger).maxPossiblePhases);
|
|
qCDebug(dcNymeaEnergy()) << "Projected endtime" << m_processInfos.value(evCharger).projectedEndTime.toString("dd.MM.yyyy hh:mm:ss");
|
|
m_processInfos[evCharger].spotMarketPercentageRequiredToday = m_chargingInfos.value(evCharger->thing()->id()).dailySpotMarketPercentage();
|
|
m_processInfos[evCharger].chargeFixedAmountSpotmarketToday = (m_chargingInfos.value(evCharger->thing()->id()).spotMarketChargingEnabled() && m_chargingInfos.value(evCharger->thing()->id()).dailySpotMarketPercentage() > 0);
|
|
m_processInfos[evCharger].spotMaketEnergyRequiredToday = assignedCar->stateValue("capacity").toDouble() * m_chargingInfos.value(evCharger->thing()->id()).dailySpotMarketPercentage() / 100.0;
|
|
qCDebug(dcNymeaEnergy()).nospace() << "A total of " << m_processInfos.value(evCharger).spotMaketEnergyRequiredToday << " kWh (" << m_chargingInfos.value(evCharger->thing()->id()).dailySpotMarketPercentage() << "%) shall be charged from spotmarket today";
|
|
}
|
|
|
|
// Reset all actions
|
|
m_chargingActions.clear();
|
|
foreach(EvCharger *evCharger, evChargers) {
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging].setIssuer(ChargingAction::ChargingActionIssuerSurplusCharging);
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging].setChargingEnabled(false);
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging].setMaxChargingCurrent(m_processInfos.value(evCharger).minimalChargingCurrent);
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging].setDesiredPhaseCount(m_processInfos.value(evCharger).minPossiblePhaseCount);
|
|
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging].setIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging].setChargingEnabled(false);
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging].setMaxChargingCurrent(m_processInfos.value(evCharger).minimalChargingCurrent);
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging].setDesiredPhaseCount(m_processInfos.value(evCharger).minPossiblePhaseCount);
|
|
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement].setIssuer(ChargingAction::ChargingActionIssuerTimeRequirement);
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement].setChargingEnabled(false);
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement].setMaxChargingCurrent(m_processInfos.value(evCharger).minimalChargingCurrent);
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement].setDesiredPhaseCount(m_processInfos.value(evCharger).minPossiblePhaseCount);
|
|
}
|
|
}
|
|
|
|
void SmartChargingManager::planSpotMarketCharging(const QDateTime ¤tDateTime)
|
|
{
|
|
if (!m_spotMarketManager->enabled())
|
|
return;
|
|
|
|
bool schedulesChanged = false;
|
|
|
|
qCDebug(dcNymeaEnergy()) << "---------------- Plan spot market ---------------";
|
|
if (!m_spotMarketManager->available()) {
|
|
qCDebug(dcNymeaEnergy()) << "Spot maket manager enabled but not available. Skip spot market charging planing and all previouse planed schedules.";
|
|
|
|
// Clean up all schedules related to spotmarket
|
|
foreach (EvCharger *evCharger, m_chargingSchedules.keys()) {
|
|
schedulesChanged |= (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging) > 0);
|
|
m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
|
|
}
|
|
|
|
if (schedulesChanged)
|
|
emit chargingSchedulesChanged();
|
|
|
|
return;
|
|
}
|
|
|
|
// Get all charger with spotmarket enabled
|
|
qCDebug(dcNymeaEnergy()) << "Current time" << currentDateTime.toString("dd.MM.yyyy hh:mm:ss");
|
|
if (m_lastSpotMarketPlanning.isValid()) {
|
|
if (m_lastSpotMarketPlanning.date() != currentDateTime.date()) {
|
|
qCDebug(dcNymeaEnergy()) << "Day changed. Reset spotmarket data counters";
|
|
foreach (EvCharger *evCharger, m_evChargers) {
|
|
if (m_processInfos.contains(evCharger)) {
|
|
m_processInfos[evCharger].spotMarketEnergyChargedToday = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m_lastSpotMarketPlanning = currentDateTime;
|
|
|
|
// 1. Filter out any EV chargers wich have nothing to do with spot market charging
|
|
EvChargers evChargers;
|
|
foreach (EvCharger *evCharger, m_evChargers) {
|
|
Thing *car = m_thingManager->findConfiguredThing(m_chargingInfos.value(evCharger->thing()->id()).assignedCarId());
|
|
if (!car) {
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name() << "has no car assigned. Ignoring...";
|
|
schedulesChanged |= (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging) > 0);
|
|
m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
|
|
continue;
|
|
}
|
|
|
|
if (m_chargingInfos.value(evCharger->id()).chargingMode() == ChargingInfo::ChargingModeNormal) {
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name() << "is set to manual mode. Ignoring...";
|
|
schedulesChanged |= (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging) > 0);
|
|
m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
|
|
continue;
|
|
}
|
|
|
|
if (!m_chargingInfos.value(evCharger->id()).spotMarketChargingEnabled()) {
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name() << "spot market charging disabled. Ignoring...";
|
|
schedulesChanged |= (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging) > 0);
|
|
m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
|
|
continue;
|
|
}
|
|
|
|
if ((m_chargingInfos.value(evCharger->id()).chargingMode() == ChargingInfo::ChargingModeEco
|
|
|| m_chargingInfos.value(evCharger->id()).chargingMode() == ChargingInfo::ChargingModeEcoWithMinCurrent)
|
|
&& m_chargingInfos.value(evCharger->id()).dailySpotMarketPercentage() == 0) {
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name() << "has no target time set nor any daily percentage to charge with spot market. Ignoring...";
|
|
schedulesChanged |= (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging) > 0);
|
|
m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
|
|
continue;
|
|
}
|
|
|
|
// Note: we plan also for chargers which are not available or not plugged in currently,
|
|
// because we need to keep the future plan since they might get available and we need to consider that
|
|
|
|
evChargers.append(evCharger);
|
|
}
|
|
|
|
qCDebug(dcNymeaEnergy()) << evChargers.count() << "chargers are set to ECO mode and have spot market enabled";
|
|
|
|
// 2. Create the charging schedules for each individual charger
|
|
foreach (EvCharger *evCharger, evChargers) {
|
|
|
|
const ChargingInfo info = m_chargingInfos.value(evCharger->id());
|
|
|
|
if (info.chargingMode() == ChargingInfo::ChargingModeEcoWithTargetTime
|
|
|| info.chargingMode() == ChargingInfo::ChargingModeEcoMinWithTargetTime) {
|
|
|
|
QDateTime endDateTime = info.nextEndTime(currentDateTime);
|
|
if (!endDateTime.isValid()) {
|
|
qCDebug(dcNymeaEnergy()).nospace() << "No valid target time set for " << evCharger->name();
|
|
ChargingInfo::ChargingMode fallbackMode;
|
|
if (info.chargingMode() == ChargingInfo::ChargingModeEcoMinWithTargetTime) {
|
|
qCDebug(dcNymeaEnergy()) << "Resetting to ECO with min current mode.";
|
|
fallbackMode = ChargingInfo::ChargingModeEcoWithMinCurrent;
|
|
} else {
|
|
qCDebug(dcNymeaEnergy()) << "Resetting to ECO mode without time.";
|
|
fallbackMode = ChargingInfo::ChargingModeEco;
|
|
}
|
|
m_chargingInfos[evCharger->id()].setChargingMode(fallbackMode);
|
|
storeChargingInfo(m_chargingInfos.value(evCharger->id()));
|
|
emit chargingInfoChanged(m_chargingInfos.value(evCharger->id()));
|
|
continue;
|
|
}
|
|
|
|
QDateTime internalEndDateTime = endDateTime.addSecs(-10 * 60);
|
|
|
|
// Check how many days in the future this target time is.
|
|
|
|
// If we have schedules up to the target time, we plan them as good as possible.
|
|
if (m_spotMarketManager->currentProvider()->scoreEntries().availableUpTo(endDateTime)) {
|
|
|
|
// We have enougth data to plan as good as possible to the target time
|
|
ScoreEntries availableEntries;
|
|
for (int i = 0; i < m_spotMarketManager->currentProvider()->scoreEntries().count(); i++) {
|
|
const ScoreEntry score = m_spotMarketManager->currentProvider()->scoreEntries().at(i);
|
|
if (score.endDateTime() <= endDateTime) {
|
|
availableEntries.append(score);
|
|
}
|
|
}
|
|
|
|
// Let's weight the available entries
|
|
ScoreEntries weightedScoreEntries = SpotMarketManager::weightScoreEntries(availableEntries);
|
|
|
|
double minutesToCharge = m_processInfos[evCharger].remainingHoursToCharge * 60;
|
|
qCDebug(dcNymeaEnergy()) << "Having spot market available until target time. Distribute" << minutesToCharge << "minutes using all available spot market data.";
|
|
|
|
// We scheduled the amount to charge using the spot market, we can skip later the target time schedule.
|
|
m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = true;
|
|
|
|
bool lockCurrentSchedule = (info.chargingState() == ChargingInfo::ChargingStateSpotMarketCharging && evCharger->charging());
|
|
|
|
TimeFrames timeFrames = m_spotMarketManager->scheduleChargingTime(currentDateTime, weightedScoreEntries, minutesToCharge, m_configuration->minimumScheduleDuration(), lockCurrentSchedule);
|
|
ChargingSchedules schedules;
|
|
foreach (const TimeFrame &timeFrame, timeFrames) {
|
|
ChargingSchedule schedule(evCharger->id(), timeFrame);
|
|
schedule.setAction(ChargingAction(true, evCharger->maxChargingCurrentMaxValue(), m_processInfos.value(evCharger).maxPossiblePhaseCount, ChargingAction::ChargingActionIssuerSpotMarketCharging));
|
|
schedules.append(schedule);
|
|
}
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Distributed charging schedules" << schedules;
|
|
|
|
ChargingSchedules currentSchedules = m_chargingSchedules.value(evCharger).filterByIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
|
|
if (currentSchedules != schedules) {
|
|
schedulesChanged = true;
|
|
m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
|
|
m_chargingSchedules[evCharger].append(schedules);
|
|
}
|
|
|
|
// Done with this charger regarding spot market planing
|
|
continue;
|
|
|
|
} else {
|
|
|
|
// We don't have data up to the given end date time, le't split up the load into days
|
|
|
|
// Check how much we want to charge for today and how much we already loaded to meet the target in the future
|
|
|
|
double hoursUntilEndDateTime = currentDateTime.secsTo(endDateTime) / 3600.0;
|
|
qCDebug(dcNymeaEnergy()) << "Not enough data available to plan to the target time. Spot market data for"
|
|
<< m_spotMarketManager->currentProvider()->scoreEntries().count() << "of the required"
|
|
<< hoursUntilEndDateTime << "hours are available.";
|
|
|
|
if (hoursUntilEndDateTime < 36) {
|
|
|
|
// If the end date time is less than 36 h in the future and we have not enougth data, we probably get them soon...
|
|
// We plan proportial the amount of energy for that slot until we know it until the end.
|
|
|
|
//double ratio = 1 - ((hoursUntilEndDateTime - m_spotMarketManager->currentProvider()->scoreEntries().count()) / hoursUntilEndDateTime);
|
|
qCDebug(dcNymeaEnergy()) << "The target time is in" << hoursUntilEndDateTime << "hours. We probably get more information soon. Scheduling"
|
|
<< m_configuration->spotMarketChargePredictableEnergyPercentage() * 100 << "% of the energy within the next known" << m_spotMarketManager->currentProvider()->scoreEntries().count() << "hours.";
|
|
|
|
// Set the required energy for today
|
|
|
|
// If there is also a certain amount of enrgy to charge from spot market today, let's use the higher amount:
|
|
// either daily percentage or required energy to meet the target time
|
|
|
|
double remainingPercentagePartial = m_processInfos.value(evCharger).remainingPercentageToCharge / 100.0 * m_configuration->spotMarketChargePredictableEnergyPercentage();
|
|
double percentageToCharge = qMax(remainingPercentagePartial, m_chargingInfos.value(evCharger->id()).dailySpotMarketPercentage() / 100.0);
|
|
|
|
double energyRequiredToday = percentageToCharge * m_processInfos.value(evCharger).carCapacityInkWh;
|
|
m_processInfos[evCharger].spotMaketEnergyRequiredToday = energyRequiredToday;
|
|
m_processInfos[evCharger].chargeFixedAmountSpotmarketToday = (energyRequiredToday > 0);
|
|
m_processInfos[evCharger].spotMarketPercentageRequiredToday = energyRequiredToday * 100.0 / m_processInfos.value(evCharger).carCapacityInkWh;
|
|
if (m_processInfos.value(evCharger).chargeFixedAmountSpotmarketToday) {
|
|
qCDebug(dcNymeaEnergy()) << "A total of" << m_processInfos.value(evCharger).spotMaketEnergyRequiredToday
|
|
<< "kWh (" << m_processInfos.value(evCharger).spotMarketPercentageRequiredToday
|
|
<< "%) battery is required for today to meet the target time in the future.";
|
|
}
|
|
|
|
|
|
// Schedule the rest normal time requirement...
|
|
m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
|
|
|
|
} else {
|
|
// Calculate on day bases, 3 days int he future, lets charge every day 1/3
|
|
qCDebug(dcNymeaEnergy()) << "TODO: plan for more than 24h into the future";
|
|
}
|
|
|
|
// The logic for charging a certain amount of energy within one day will is the same as fo the daily percentage.
|
|
// The only difference is that this logic will say how much per day until we have enought schedules to reach the target time.
|
|
}
|
|
} else {
|
|
// We have not scheduled the amount to charge using the spot market,
|
|
// we must plan later the target time schedule.
|
|
m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
|
|
}
|
|
|
|
// Check if we have to charge a certain amount of energy today using spotmarket
|
|
|
|
if (m_processInfos.value(evCharger).chargeFixedAmountSpotmarketToday) {
|
|
|
|
// Either trough the dayly min percentage to charge or if our target time is far
|
|
// in the future and we charge some energy proportional to the distance
|
|
|
|
Thing *car = m_thingManager->findConfiguredThing(info.assignedCarId());
|
|
|
|
double totalEnergyCharged = car->property("totalEnergyCharged").toDouble();
|
|
if (!qFuzzyCompare(totalEnergyCharged, m_processInfos.value(evCharger).lastTotalEnergyCharged)) {
|
|
m_processInfos[evCharger].spotMarketEnergyChargedToday += totalEnergyCharged - m_processInfos.value(evCharger).lastTotalEnergyCharged;
|
|
m_processInfos[evCharger].lastTotalEnergyCharged = totalEnergyCharged;
|
|
|
|
// Check if we already reached our target for today, in which case we are done with this charger regarding spotmarket charging
|
|
if (m_processInfos.value(evCharger).spotMarketEnergyChargedToday >= m_processInfos.value(evCharger).spotMaketEnergyRequiredToday) {
|
|
// Remove any planed schedule, we are done here
|
|
m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// How many kWh we want to charge today (accoring to car percentage)
|
|
qCDebug(dcNymeaEnergy()) << "Charged" << m_processInfos.value(evCharger).spotMarketEnergyChargedToday << "kWh energy today so far. A total of"
|
|
<< m_processInfos.value(evCharger).spotMaketEnergyRequiredToday << "kWh spot market energy is required for today.";
|
|
|
|
if (m_processInfos.value(evCharger).spotMarketEnergyChargedToday > m_processInfos.value(evCharger).spotMaketEnergyRequiredToday) {
|
|
qCDebug(dcNymeaEnergy()) << "Already charged enougth energy for today.";
|
|
continue;
|
|
}
|
|
|
|
// We need to take the loss into account, we actually need more energy
|
|
double remainingEnergyToCharge = m_processInfos.value(evCharger).spotMaketEnergyRequiredToday - m_processInfos.value(evCharger).spotMarketEnergyChargedToday;
|
|
|
|
// Note: we already have the power loss in the total hours, so we should have that taken into account in the remaining time too
|
|
// x / remainingEnergyToCharge = totalHours / totalEnergy
|
|
double remeiningHours = m_processInfos.value(evCharger).totalChargeHours * remainingEnergyToCharge / m_processInfos.value(evCharger).carCapacityInkWh;
|
|
|
|
double percentageCharged = 0;
|
|
if (m_processInfos.value(evCharger).spotMaketEnergyRequiredToday != 0) {
|
|
// x : spotMarketEnergyChargedToday = 100 : totalEnergy
|
|
percentageCharged = m_processInfos.value(evCharger).spotMarketEnergyChargedToday * 100.0 / m_processInfos.value(evCharger).carTotalEnergy;
|
|
}
|
|
|
|
double remainingPercentage = m_processInfos.value(evCharger).spotMarketPercentageRequiredToday - percentageCharged;
|
|
double totalMinutesToday = m_processInfos.value(evCharger).totalChargeHours * 60.0 * m_processInfos.value(evCharger).spotMarketPercentageRequiredToday / 100.0;
|
|
double remainingChargeMinutesToday = remeiningHours * 60;
|
|
|
|
qCDebug(dcNymeaEnergy()).nospace() << "Remaining " << remainingPercentage << "% (" << remainingChargeMinutesToday << " min) to charge today from total spotmarket "
|
|
<< m_processInfos.value(evCharger).spotMarketPercentageRequiredToday
|
|
<< "% (" << totalMinutesToday << " min). Using "
|
|
<< m_processInfos.value(evCharger).chargerPhaseLimitPower << "W on "
|
|
<< m_processInfos.value(evCharger).maxPossiblePhaseCount << " phase(s) | "
|
|
<< Electricity::convertPhasesToString(m_processInfos.value(evCharger).maxPossiblePhases);
|
|
|
|
// Distribute the remaining time to the spot market data of this day. If we are currently charging from the spot market,
|
|
// lock the current schedule so it will be finished and not postboned creating a jitter
|
|
|
|
bool lockCurrentSchedule = (info.chargingState() == ChargingInfo::ChargingStateSpotMarketCharging && evCharger->charging());
|
|
|
|
TimeFrames timeFrames = m_spotMarketManager->scheduleCharingTimeForToday(currentDateTime, remainingChargeMinutesToday, m_configuration->minimumScheduleDuration(), lockCurrentSchedule);
|
|
ChargingSchedules schedules;
|
|
foreach (const TimeFrame &timeFrame, timeFrames) {
|
|
ChargingSchedule schedule(evCharger->id(), timeFrame);
|
|
schedule.setAction(ChargingAction(true, evCharger->maxChargingCurrentMaxValue(), m_processInfos.value(evCharger).maxPossiblePhaseCount, ChargingAction::ChargingActionIssuerSpotMarketCharging));
|
|
schedules.append(schedule);
|
|
}
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Distributed spot market" << schedules;
|
|
|
|
ChargingSchedules currentSchedules = m_chargingSchedules.value(evCharger).filterByIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
|
|
if (currentSchedules != schedules) {
|
|
schedulesChanged = true;
|
|
m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
|
|
m_chargingSchedules[evCharger].append(schedules);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (schedulesChanged) {
|
|
emit chargingSchedulesChanged();
|
|
}
|
|
|
|
// Derive any actions related to spot market charging for the current datatime
|
|
foreach (EvCharger *evCharger, m_evChargers) {
|
|
|
|
// If there are schedules for this EV charger (spotmarket or user created), charge now...
|
|
|
|
if (m_chargingSchedules.contains(evCharger)) {
|
|
ChargingSchedule currentSchedule = m_chargingSchedules.value(evCharger).getChargingSchedule(currentDateTime);
|
|
if (currentSchedule.isValid() && currentSchedule.action().chargingEnabled() && currentSchedule.action().issuer() == ChargingAction::ChargingActionIssuerSpotMarketCharging) {
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging] = currentSchedule.action();
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging].setIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
|
|
m_processInfos[evCharger].currentSchedule = currentSchedule;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SmartChargingManager::planSurplusCharging(const QDateTime ¤tDateTime)
|
|
{
|
|
if (!m_rootMeter) {
|
|
qCDebug(dcNymeaEnergy()) << "No root meter configured. Skipping surplus charging planing";
|
|
return;
|
|
}
|
|
|
|
qCDebug(dcNymeaEnergy()) << "---------------- Plan surplus charging ---------------";
|
|
qCDebug(dcNymeaEnergy()) << "Current time" << currentDateTime.toString("dd.MM.yyyy hh:mm:ss");
|
|
|
|
// 1. Find all the chargers that are set to eco mode, we'll leave them alone in normal mode
|
|
EvChargers evChargers;
|
|
foreach (EvCharger *evCharger, m_evChargers) {
|
|
|
|
Thing *assignedCar = m_thingManager->findConfiguredThing(m_chargingInfos.value(evCharger->id()).assignedCarId());
|
|
if (!assignedCar) {
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name() << "has no car assigned. Ignoring...";
|
|
if (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerTimeRequirement) > 0)
|
|
emit chargingSchedulesChanged();
|
|
|
|
continue;
|
|
}
|
|
|
|
const ChargingInfo info = chargingInfo(evCharger->id());
|
|
if (info.chargingMode() == ChargingInfo::ChargingModeNormal) {
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name() << "is set to manual mode. Ignoring...";
|
|
if (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerTimeRequirement) > 0)
|
|
emit chargingSchedulesChanged();
|
|
|
|
continue;
|
|
}
|
|
|
|
evChargers.append(evCharger);
|
|
}
|
|
qCDebug(dcNymeaEnergy()) << evChargers.count() << "EV chargers are set to ECO mode";
|
|
|
|
// 2. Find the chargers that need to charge full speed (e.g. because user wants it fully charged soon)
|
|
double addedPower = 0;
|
|
QMutableListIterator<EvCharger *> it(evChargers);
|
|
QHash<EvCharger*, Electricity::Phases> chargersNeedingFullSpeed;
|
|
while (it.hasNext()) {
|
|
EvCharger *evCharger = it.next();
|
|
ChargingProcessInfo processInfo = m_processInfos.value(evCharger);
|
|
|
|
qCDebug(dcNymeaEnergy()) << "**** Evaluating" << evCharger->name();
|
|
|
|
ChargingInfo info = m_chargingInfos.value(evCharger->id());
|
|
if (info.chargingMode() != ChargingInfo::ChargingModeEcoWithTargetTime
|
|
&& info.chargingMode() != ChargingInfo::ChargingModeEcoMinWithTargetTime) {
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name() << "does not use a target time.";
|
|
m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerTimeRequirement);
|
|
continue;
|
|
}
|
|
|
|
QDateTime endDateTime = info.nextEndTime(currentDateTime);
|
|
if (!endDateTime.isValid()) {
|
|
qCDebug(dcNymeaEnergy()).nospace() << "No valid target time set for " << evCharger->name();
|
|
ChargingInfo::ChargingMode fallbackMode;
|
|
if (info.chargingMode() == ChargingInfo::ChargingModeEcoMinWithTargetTime) {
|
|
qCDebug(dcNymeaEnergy()) << "Resetting to ECO with min current mode.";
|
|
fallbackMode = ChargingInfo::ChargingModeEcoWithMinCurrent;
|
|
} else {
|
|
qCDebug(dcNymeaEnergy()) << "Resetting to ECO mode without time.";
|
|
fallbackMode = ChargingInfo::ChargingModeEco;
|
|
}
|
|
m_chargingInfos[evCharger->id()].setChargingMode(fallbackMode);
|
|
storeChargingInfo(m_chargingInfos.value(evCharger->id()));
|
|
emit chargingInfoChanged(m_chargingInfos.value(evCharger->id()));
|
|
continue;
|
|
}
|
|
|
|
if (m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket) {
|
|
qCDebug(dcNymeaEnergy()) << "Have a target time available and spotmarket charging enabled. Skipping force check...";
|
|
m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerTimeRequirement);
|
|
continue;
|
|
}
|
|
|
|
QDateTime internalEndDateTime = endDateTime.addSecs(-10 * 60);
|
|
ChargingAction action(true, evCharger->maxChargingCurrentMaxValue(), m_processInfos.value(evCharger).maxPossiblePhaseCount, ChargingAction::ChargingActionIssuerTimeRequirement);
|
|
|
|
// We'll be trying to finish 10 min earlier (TODO: To be evaluated if really needed in practice)
|
|
if (processInfo.remainingPercentageToCharge > 0 && processInfo.projectedEndTime > internalEndDateTime) {
|
|
qCDebug(dcNymeaEnergy()) << "We need to charge" << evCharger->name() << "immediately to meet the target! Internal target time:"
|
|
<< internalEndDateTime.toString("dd.MM.yyyy hh:mm:ss") << "projected endtime:" << processInfo.projectedEndTime.toString("dd.MM.yyyy hh:mm:ss");
|
|
qCDebug(dcNymeaEnergy()) << "Charging schedule active for" << evCharger->name() << m_processInfos.value(evCharger).currentSchedule;
|
|
|
|
// Create the schedule for ui from now till the end
|
|
ChargingSchedule schedule(evCharger->id());
|
|
schedule.setEndDateTime(internalEndDateTime);
|
|
schedule.setStartDateTime(currentDateTime);
|
|
schedule.setAction(action);
|
|
|
|
m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerTimeRequirement);
|
|
m_chargingSchedules[evCharger].append(schedule);
|
|
|
|
// Force charging now
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement] = action;
|
|
|
|
// Wallbox has been handled. Remove from further logic
|
|
it.remove();
|
|
} else {
|
|
qCDebug(dcNymeaEnergy()) << "No need to force charging on" << evCharger->name() << ", there's still enough time for optimization. Internal target time:"
|
|
<< internalEndDateTime.toString("dd.MM.yyyy hh:mm:ss") << "projected endtime:" << processInfo.projectedEndTime.toString("dd.MM.yyyy hh:mm:ss");;
|
|
|
|
// Remove all time requiment based schedules, if needed, we re plan it here...
|
|
m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerTimeRequirement);
|
|
|
|
// Let's see how much we already scheduled so far using other planings like spotmarket, plan the rest as time requirement schedule
|
|
int alreadyScheduledMinutes = 0;
|
|
foreach (const ChargingSchedule &existingSchedule, m_chargingSchedules.value(evCharger)) {
|
|
alreadyScheduledMinutes += existingSchedule.durationMinutes();
|
|
}
|
|
|
|
int remainingMinutesTotal = qCeil(m_processInfos[evCharger].remainingHoursToCharge * 60);
|
|
int remainingMinutesToCharge = remainingMinutesTotal - alreadyScheduledMinutes;
|
|
if (alreadyScheduledMinutes > 0) {
|
|
qCDebug(dcNymeaEnergy()) << "Have already scheduled" << alreadyScheduledMinutes << "minutes from a total of"
|
|
<< remainingMinutesTotal << "minutes. Still need to schedule"
|
|
<< remainingMinutesToCharge << "minutes using time requirements.";
|
|
}
|
|
|
|
ChargingSchedule schedule(evCharger->id());
|
|
schedule.setEndDateTime(internalEndDateTime);
|
|
schedule.setStartDateTime(internalEndDateTime.addSecs(-remainingMinutesToCharge * 60));
|
|
schedule.setAction(action);
|
|
|
|
m_chargingSchedules[evCharger].append(schedule);
|
|
}
|
|
}
|
|
|
|
// 3. distribute remaining allowance between remaining chargers
|
|
|
|
// If negative, we've got some spare energy to use, if positive, we should lower charging rates or even stop
|
|
double currentLoad = m_rootMeter->currentPower();
|
|
|
|
// Adding some mechanism to prioritize house energy storage before the car, or steal
|
|
double fromBatteries = 0;
|
|
|
|
// Consider all batteries as one, to keep things as simple as possible
|
|
double totalBatteryLevel = 0;
|
|
double totalBatteryPower = 0;
|
|
int storageCount = 0;
|
|
|
|
int batteryLevelConsiderationPercentage = qRound(m_batteryLevelConsideration * 100);
|
|
|
|
foreach (Thing *storage, m_thingManager->configuredThings().filterByInterface("energystorage")) {
|
|
storageCount ++;
|
|
|
|
double batteryLevel = storage->stateValue("batteryLevel").toDouble();
|
|
double currentPower = storage->stateValue("currentPower").toDouble();
|
|
|
|
totalBatteryLevel += batteryLevel; // Sum up percentage and create average later using storageCount
|
|
totalBatteryPower += currentPower; // Create a total sum of all battery powers
|
|
}
|
|
|
|
totalBatteryLevel = qRound(totalBatteryLevel / storageCount);
|
|
|
|
if (storageCount > 0) {
|
|
qCDebug(dcNymeaEnergy()) << "Having" << storageCount << "batteries. Overall battery level is" << totalBatteryLevel << "% and overall battery power is" << totalBatteryPower << "W";
|
|
|
|
// If we are currently charging in eco mode and the battery level is under the consideration level,
|
|
// we should stop charging and prioritize the battery
|
|
|
|
if (totalBatteryLevel < batteryLevelConsiderationPercentage) {
|
|
// We are under the consideration level for the batteries, check if we have active chargers left, we should stop loading now
|
|
it.toFront();
|
|
while (it.hasNext()) {
|
|
EvCharger *evCharger = it.next();
|
|
const ChargingProcessInfo processInfo = m_processInfos.value(evCharger);
|
|
|
|
if (evCharger->charging()) {
|
|
// Let's pause the charging process, if there will be surplus available, it will be considerated in the next iterations...
|
|
qCDebug(dcNymeaEnergy()) << "The battery is under the consideration level. Pausing the charging on" << evCharger->name();
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging] = ChargingAction(false, evCharger->maxChargingCurrent(), evCharger->phaseCount(), ChargingAction::ChargingActionIssuerSurplusCharging);
|
|
// Remove this charger from further logic
|
|
it.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (totalBatteryPower < 0) {
|
|
// If the battery is discharging, we'll have to add that to the load in order to not end up draining the battery
|
|
fromBatteries += -totalBatteryPower;
|
|
} else if (totalBatteryPower > 0) {
|
|
// Battery is charging
|
|
if (totalBatteryLevel >= batteryLevelConsiderationPercentage) {
|
|
qCDebug(dcNymeaEnergy()) << "The battery has reached the consideration level" << batteryLevelConsiderationPercentage << "%. The battery power will used for surplus charging.";
|
|
fromBatteries += -totalBatteryPower;
|
|
} else {
|
|
// If the battery is charging, we'll add 500W to the load to leave that for the battery to "eat" up so the battery has priority
|
|
qCDebug(dcNymeaEnergy()) << "The battery has not reached yet the consideration level" << batteryLevelConsiderationPercentage << "%. Prioritize the energy storage and add 500W buffer).";
|
|
fromBatteries += 500;
|
|
}
|
|
}
|
|
} else {
|
|
qCDebug(dcNymeaEnergy()) << "There is no energy storage available to consider.";
|
|
}
|
|
|
|
// Add the evaluated battery load to the current root meter load in order to have the actual available power
|
|
currentLoad += fromBatteries;
|
|
|
|
// In the previous step we may have added some load which doesn't reflect on the root meter yet but will likely be in the next metering cycle
|
|
currentLoad += addedPower;
|
|
|
|
qCDebug(dcNymeaEnergy()).nospace() << "Current load on root meter: " << currentLoad << "W ( " << (currentLoad / 230) << "[A] ) Phases: A: " << m_rootMeter->currentPhaseA() << "[A], B: " << m_rootMeter->currentPhaseB() << "[A], C: " << m_rootMeter->currentPhaseC() << "[A]. Added in previous step: " << addedPower << "[W] (" << (addedPower / 230) << "[A]). From batteries: " << fromBatteries << "[W] (" << (fromBatteries / 230) << "[A])";
|
|
|
|
if (!evChargers.isEmpty()) {
|
|
qCDebug(dcNymeaEnergy()) << evChargers.count() << "chargers in ECO mode and can be optimized.";
|
|
|
|
it.toFront();
|
|
double allowanceInWatt = -currentLoad;
|
|
double allowanceInAmpere = allowanceInWatt / 230;
|
|
while (it.hasNext()) {
|
|
EvCharger *evCharger = it.next();
|
|
const ChargingProcessInfo processInfo = m_processInfos.value(evCharger);
|
|
|
|
// [ETM 3g-2] Le surplus de cette borne est décidé par le waterfall. Continuer ici
|
|
// remplirait m_chargingActions d'une consigne concurrente que plus personne
|
|
// n'exécute — et surtout le JOURNAL afficherait « ### Surplus would adjust
|
|
// evcharger … » juste sous la ligne [Arbitre] qui dit autre chose. Un journal qui
|
|
// se contredit envoie diagnostiquer la mauvaise moitié du moteur.
|
|
if (evSurplusPlannedByWaterfall(evCharger)) {
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name()
|
|
<< "surplus is planned by the ETM waterfall. Skipping proxy planning.";
|
|
continue;
|
|
}
|
|
|
|
qCDebug(dcNymeaEnergy()).nospace() << "Current charger setpoint: " << evCharger->maxChargingCurrent() << "A (" << (evCharger->maxChargingCurrent() * 230) << "W), Metered: " << (evCharger->currentPower() / 230) << "A (" << evCharger->currentPower() << "W)";
|
|
|
|
double newRawValue = (evCharger->currentPower() / 230) + allowanceInAmpere;
|
|
|
|
// Check if we need to switch phases, theoretically
|
|
uint desiredPhaseCount = processInfo.maxPossiblePhaseCount;
|
|
Electricity::Phases desiredPhases = getAscendingPhasesForCount(desiredPhaseCount);
|
|
|
|
if (m_processInfos.value(evCharger).canSwitchPhaseCount) {
|
|
uint possiblePhaseCount = getBestPhaseCount(evCharger, newRawValue);
|
|
desiredPhaseCount = qMin(processInfo.carPhaseCount, possiblePhaseCount);
|
|
desiredPhaseCount = qMin(desiredPhaseCount, processInfo.carPhaseCount);
|
|
if (desiredPhaseCount == 1) {
|
|
desiredPhases = Electricity::PhaseA;
|
|
} else {
|
|
desiredPhases = Electricity::PhaseAll;
|
|
}
|
|
}
|
|
newRawValue /= desiredPhaseCount;
|
|
|
|
double minimumRequiredAmps = processInfo.minimalChargingCurrent * m_acquisitionTolerance;
|
|
qCDebug(dcNymeaEnergy()).nospace() << "Charger " << evCharger->name() << " uses " << desiredPhaseCount << " "
|
|
<< (desiredPhaseCount == 1 ? "phase" : " phases")
|
|
<< ". Min required: " << minimumRequiredAmps
|
|
<< "A (" << (minimumRequiredAmps * 230) << "W) per phase with "
|
|
<< m_acquisitionTolerance << " aquisition tolerance.";
|
|
|
|
bool chargingEnabled = newRawValue >= minimumRequiredAmps;
|
|
|
|
// Adjust down to not overload any phases.
|
|
double allowanceOnRootMeterAmpere = m_rootMeter->calculateAllowanceAmpere(desiredPhases, m_phasePowerConsumptionLimit);
|
|
double newValue = qMin(newRawValue, allowanceOnRootMeterAmpere);
|
|
|
|
uint newValueInt = newValue >= 0 ? qRound(newValue) : 0;
|
|
|
|
qCDebug(dcNymeaEnergy()).nospace() << "### Surplus would adjust evcharger " << evCharger->name() << " to total " << newRawValue << "A (" << (newRawValue * 230) << "W) * " << desiredPhaseCount << " phases.";
|
|
qCDebug(dcNymeaEnergy()).nospace() << "# Charger limits: " << evCharger->maxChargingCurrentMinValue() << " - " << evCharger->maxChargingCurrentMaxValue() << " Root meter allowance: " << allowanceOnRootMeterAmpere << " A (" << (allowanceInAmpere * 230) << " W)";
|
|
qCDebug(dcNymeaEnergy()).nospace() << "# Theoretically: " << newValue << "A (" << (newValue * 230) << "W) * " << desiredPhaseCount << " Phases. " << (chargingEnabled ? "ON" : "OFF");
|
|
|
|
if (newValueInt <= evCharger->maxChargingCurrentMinValue())
|
|
newValueInt = evCharger->maxChargingCurrentMinValue();
|
|
|
|
if (newValueInt >= evCharger->maxChargingCurrentMaxValue())
|
|
newValueInt = evCharger->maxChargingCurrentMaxValue();
|
|
|
|
qCDebug(dcNymeaEnergy()).nospace() << "# Effective: " << newValueInt << "A (" << (newValueInt * 230) << "W) * " << desiredPhaseCount << " Phases. " << (chargingEnabled ? "ON" : "OFF");
|
|
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging] = ChargingAction(chargingEnabled, newValueInt, desiredPhaseCount, ChargingAction::ChargingActionIssuerSurplusCharging);
|
|
|
|
// If we're charging on solar power only, remove this charger from further logic
|
|
if (chargingEnabled) {
|
|
it.remove();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// [ETM 3g-2] Voir la documentation dans smartchargingmanager.h (inventaire [ETM], entrée 7).
|
|
void SmartChargingManager::setEvChargingState(const ThingId &evChargerId, ChargingInfo::ChargingState state)
|
|
{
|
|
if (!m_chargingInfos.contains(evChargerId))
|
|
return;
|
|
if (m_chargingInfos.value(evChargerId).chargingState() == state)
|
|
return;
|
|
m_chargingInfos[evChargerId].setChargingState(state);
|
|
emit chargingInfoChanged(m_chargingInfos.value(evChargerId));
|
|
}
|
|
|
|
void SmartChargingManager::adjustEvChargers(const QDateTime ¤tDateTime)
|
|
{
|
|
qCDebug(dcNymeaEnergy()) << "---------------- Adjusting chargers ---------------";
|
|
qCDebug(dcNymeaEnergy()) << "Current time" << currentDateTime.toString("dd.MM.yyyy hh:mm:ss");
|
|
|
|
|
|
foreach (EvCharger *evCharger, m_chargingActions.keys()) {
|
|
|
|
if (m_chargingInfos.value(evCharger->id()).chargingMode() == ChargingInfo::ChargingModeNormal) {
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name() << "is set to manual mode. Ignoring...";
|
|
continue;
|
|
}
|
|
|
|
// [ETM 3g-2] UNE CHARGE, UN COMMANDEUR. Cette borne a reçu son enveloppe du waterfall
|
|
// et l'adaptateur l'a déjà exécutée ; repasser ici dériverait une SECONDE consigne, de
|
|
// sa propre logique, et c'est elle qui atteindrait le matériel — la décision de
|
|
// l'arbitre resterait un motif publié que rien n'applique (relevé du 2026-08-26 §3.3).
|
|
if (evCommandedByWaterfall(evCharger)) {
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name()
|
|
<< "is commanded by the ETM waterfall this cycle. Not re-deciding here.";
|
|
continue;
|
|
}
|
|
|
|
ChargingProcessInfo processInfo = m_processInfos.value(evCharger);
|
|
|
|
// Final schedules
|
|
qCDebug(dcNymeaEnergy()) << "Final charging schedules" << evCharger->thing();
|
|
qCDebug(dcNymeaEnergy()) << m_chargingSchedules.value(evCharger);
|
|
emit chargingSchedulesChanged();
|
|
|
|
// We prioritize here what we should do...
|
|
|
|
// Note: here is where we need to perform the schedule taks depending on what each charger wants to do.
|
|
// For now: first come first serve
|
|
|
|
// 1. Time requirement
|
|
if (m_chargingActions.value(evCharger).value(ChargingAction::ChargingActionIssuerTimeRequirement).chargingEnabled()) {
|
|
|
|
// Note: this will only be set in surplus charging, so we don't need to check for an existing root meter, that already happened in the surplus planing phase
|
|
|
|
// Determine what the charger already uses
|
|
double currentPower = evCharger->currentPower() / processInfo.effectivePhaseCount;
|
|
qCDebug(dcNymeaEnergy()).nospace() << "Charger " << evCharger->name() << " is connected on phase(s): " << processInfo.effectivePhaseCount << " and uses " << currentPower << " W (" << (currentPower / 230) << "A) per phase";
|
|
|
|
double allowancePerPhase = m_rootMeter->calculateAllowanceAmpere(processInfo.effectivePhases, m_phasePowerConsumptionLimit);
|
|
qCDebug(dcNymeaEnergy()).nospace() << "Current load on root meter: " << m_rootMeter->currentPower() << "[W] ( " << (m_rootMeter->currentPower() / 230) << "[A] ) Phases: A: " << m_rootMeter->currentPhaseA() << "[A], B: " << m_rootMeter->currentPhaseB() << "[A], C: " << m_rootMeter->currentPhaseC() << "[A]";
|
|
|
|
// Reducing the max allowance by 1A (230W) so that we're not super close to the limit and are hitting the ovreload protection
|
|
// when the user turns on a midium sized consumer.
|
|
allowancePerPhase -= 1;
|
|
allowancePerPhase += currentPower / 230;
|
|
|
|
double maxChargingCurrent = qMin((double)evCharger->maxChargingCurrentMaxValue(), allowancePerPhase);
|
|
qCDebug(dcNymeaEnergy()) << evCharger->name() << "current upper limit:" << evCharger->maxChargingCurrentMaxValue() << "A - Per phase upper limit:" << allowancePerPhase << "A" << "Using:" << maxChargingCurrent << "A";
|
|
maxChargingCurrent = qMax((double)evCharger->maxChargingCurrentMinValue(), maxChargingCurrent);
|
|
maxChargingCurrent = qFloor(maxChargingCurrent);
|
|
|
|
m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement].setMaxChargingCurrent(maxChargingCurrent);
|
|
|
|
executeChargingAction(evCharger, m_chargingActions.value(evCharger).value(ChargingAction::ChargingActionIssuerTimeRequirement), currentDateTime);
|
|
|
|
if (m_chargingInfos.value(evCharger->thing()->id()).chargingState() != ChargingInfo::ChargingStateTimeRequirement) {
|
|
m_chargingInfos[evCharger->thing()->id()].setChargingState(ChargingInfo::ChargingStateTimeRequirement);
|
|
emit chargingInfoChanged(m_chargingInfos[evCharger->thing()->id()]);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// 2. Surplus charging
|
|
if (m_chargingActions.value(evCharger).value(ChargingAction::ChargingActionIssuerSurplusCharging).chargingEnabled()) {
|
|
|
|
executeChargingAction(evCharger, m_chargingActions.value(evCharger).value(ChargingAction::ChargingActionIssuerSurplusCharging), currentDateTime);
|
|
|
|
// // TODO: adjust re-check if we still have enougth energy for this
|
|
|
|
if (m_chargingInfos[evCharger->thing()->id()].chargingState() != ChargingInfo::ChargingStateSurplusCharging) {
|
|
m_chargingInfos[evCharger->thing()->id()].setChargingState(ChargingInfo::ChargingStateSurplusCharging);
|
|
emit chargingInfoChanged(m_chargingInfos[evCharger->thing()->id()]);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// 3. Spotmarket charging
|
|
if (m_chargingActions.value(evCharger).value(ChargingAction::ChargingActionIssuerSpotMarketCharging).chargingEnabled()) {
|
|
|
|
executeChargingAction(evCharger, m_chargingActions.value(evCharger).value(ChargingAction::ChargingActionIssuerSpotMarketCharging), currentDateTime);
|
|
|
|
// // TODO: adjust re-check if we still have enougth energy for this
|
|
|
|
if (m_chargingInfos[evCharger->thing()->id()].chargingState() != ChargingInfo::ChargingStateSpotMarketCharging) {
|
|
m_chargingInfos[evCharger->thing()->id()].setChargingState(ChargingInfo::ChargingStateSpotMarketCharging);
|
|
emit chargingInfoChanged(m_chargingInfos[evCharger->thing()->id()]);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// 4. Min current fallback (EcoWithMinCurrent / EcoMinWithTargetTime)
|
|
{
|
|
const ChargingInfo::ChargingMode mode = m_chargingInfos.value(evCharger->thing()->id()).chargingMode();
|
|
if (mode == ChargingInfo::ChargingModeEcoWithMinCurrent
|
|
|| mode == ChargingInfo::ChargingModeEcoMinWithTargetTime) {
|
|
uint minCurrent = qMax(EcoMinChargingCurrent, evCharger->maxChargingCurrentMinValue());
|
|
qCDebug(dcNymeaEnergy()).nospace() << "No surplus available for " << evCharger->name()
|
|
<< " — applying min current " << minCurrent << " A on " << processInfo.effectivePhaseCount << " phase(s).";
|
|
ChargingAction minAction(true, minCurrent, processInfo.effectivePhaseCount,
|
|
ChargingAction::ChargingActionIssuerSurplusCharging);
|
|
executeChargingAction(evCharger, minAction, currentDateTime);
|
|
if (m_chargingInfos[evCharger->thing()->id()].chargingState() != ChargingInfo::ChargingStateSurplusCharging) {
|
|
m_chargingInfos[evCharger->thing()->id()].setChargingState(ChargingInfo::ChargingStateSurplusCharging);
|
|
emit chargingInfoChanged(m_chargingInfos[evCharger->thing()->id()]);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// 5. Else ... idle, switch off
|
|
qCDebug(dcNymeaEnergy()).nospace() << "Setting " << evCharger->name() << " to power OFF. Idle state.";
|
|
evCharger->setChargingEnabled(false, currentDateTime);
|
|
|
|
if (m_chargingInfos[evCharger->thing()->id()].chargingState() != ChargingInfo::ChargingStateIdle) {
|
|
m_chargingInfos[evCharger->thing()->id()].setChargingState(ChargingInfo::ChargingStateIdle);
|
|
emit chargingInfoChanged(m_chargingInfos[evCharger->thing()->id()]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SmartChargingManager::updateManualSoCsWithMeter(EnergyLogs::SampleRate sampleRate, const ThingPowerLogEntry &entry)
|
|
{
|
|
Q_UNUSED(sampleRate)
|
|
|
|
EvCharger *charger = m_evChargers.value(entry.thingId());
|
|
if (!charger) {
|
|
return;
|
|
}
|
|
Thing *car = m_thingManager->findConfiguredThing(m_chargingInfos.value(entry.thingId()).assignedCarId());
|
|
if (!car || !car->thingClass().hasStateType("batteryLevel") || !car->thingClass().stateTypes().findByName("batteryLevel").writable()) {
|
|
return;
|
|
}
|
|
|
|
double chargingPower = entry.currentPower();
|
|
double addedkWh = chargingPower / 60000.0;
|
|
|
|
addedkWh *= m_processInfos.value(charger).carChargingEnergyLossFactor;
|
|
|
|
double capacity = car->stateValue("capacity").toDouble();
|
|
// x : 100 = addedkWh : capacity
|
|
double addedPercentage = addedkWh * 100 / capacity;
|
|
|
|
double lastPreciseSoC = car->property("preciseSoC").toDouble();
|
|
if (lastPreciseSoC == 0) {
|
|
lastPreciseSoC = car->stateValue("batteryLevel").toDouble();
|
|
}
|
|
double newPreciseSoC = qMin(100.0, lastPreciseSoC + addedPercentage);
|
|
double newEnergyCharged = car->property("totalEnergyCharged").toDouble() + addedkWh;
|
|
qCDebug(dcNymeaEnergy()) << "Updating manual SoC (metered) for" << car->name() << chargingPower << "W"
|
|
<< QTime::fromMSecsSinceStartOfDay(60000).toString() << addedPercentage << "% -> new soc" << newPreciseSoC
|
|
<< "% total energy:" << newEnergyCharged << "kWh";
|
|
car->setProperty("totalEnergyCharged", newEnergyCharged);
|
|
car->setProperty("preciseSoC", newPreciseSoC);
|
|
if (car->stateValue("batteryLevel").toInt() != qRound(newPreciseSoC)) {
|
|
ActionType batteryLevelActionType = car->thingClass().actionTypes().findByName("batteryLevel");
|
|
Action action(batteryLevelActionType.id(), car->id(), Action::TriggeredByRule);
|
|
action.setParams({Param(batteryLevelActionType.id(), qRound(newPreciseSoC))});
|
|
m_thingManager->executeAction(action);
|
|
}
|
|
}
|
|
|
|
void SmartChargingManager::updateManualSoCsWithoutMeter(const QDateTime ¤tDateTime)
|
|
{
|
|
foreach (EvCharger *charger, m_evChargers) {
|
|
// Chargers with metering capabilities will be updated with more precise values the energy logger
|
|
if (charger->hasPowerMeter()) {
|
|
continue;
|
|
}
|
|
ChargingInfo chargingInfo = m_chargingInfos.value(charger->id());
|
|
Thing *car = m_thingManager->findConfiguredThing(chargingInfo.assignedCarId());
|
|
if (!car || !car->thingClass().hasStateType("batteryLevel") || !car->thingClass().stateTypes().findByName("batteryLevel").writable()) {
|
|
continue;
|
|
}
|
|
|
|
double lastPreciseSoC = car->property("preciseSoC").toDouble();
|
|
QDateTime lastCalculation = car->property("lastSoCCalculation").toDateTime();
|
|
if (lastCalculation.isNull()) {
|
|
car->setProperty("preciseSoC", car->stateValue("batteryLevel"));
|
|
car->setProperty("lastSoCCalculation", currentDateTime);
|
|
continue;
|
|
}
|
|
|
|
if (!charger->charging()) {
|
|
car->setProperty("lastSoCCalculation", currentDateTime);
|
|
continue;
|
|
}
|
|
|
|
qulonglong duration = lastCalculation.msecsTo(currentDateTime);
|
|
double chargingPower = charger->currentPower();
|
|
double addedkWh = chargingPower * duration / 1000 / 60 / 60 / 1000;
|
|
|
|
// There are about 10% of losses during charging...
|
|
addedkWh *= m_processInfos.value(charger).carChargingEnergyLossFactor;
|
|
|
|
double capacity = car->stateValue("capacity").toDouble();
|
|
// x : 100 = addedkWh : capacity
|
|
double addedPercentage = addedkWh * 100 / capacity;
|
|
double newPreciseSoC = qMin(100.0, lastPreciseSoC + addedPercentage);
|
|
double newTotal = car->property("totalEnergyCharged").toDouble() + addedkWh;
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Updating manual SoC for" << car->name() << chargingPower << "W" << QTime::fromMSecsSinceStartOfDay(duration).toString() << addedPercentage << "% -> new soc" << newPreciseSoC << "%" << "Total:" << newTotal << "kWh";
|
|
car->setProperty("totalEnergyCharged", newTotal);
|
|
car->setProperty("preciseSoC", newPreciseSoC);
|
|
car->setProperty("lastSoCCalculation", currentDateTime);
|
|
if (car->stateValue("batteryLevel").toInt() != qRound(newPreciseSoC)) {
|
|
ActionType batteryLevelActionType = car->thingClass().actionTypes().findByName("batteryLevel");
|
|
Action action(batteryLevelActionType.id(), car->id(), Action::TriggeredByRule);
|
|
action.setParams({Param(batteryLevelActionType.id(), qRound(newPreciseSoC))});
|
|
m_thingManager->executeAction(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void SmartChargingManager::onThingAdded(Thing *thing)
|
|
{
|
|
if (thing->thingClass().interfaces().contains("evcharger")) {
|
|
EvCharger *evCharger = new EvCharger(m_thingManager, thing);
|
|
evCharger->setChargingEnabledLockDuration(m_configuration->chargingEnabledLockDuration());
|
|
evCharger->setChargingCurrentLockDuration(m_configuration->chargingCurrentLockDuration());
|
|
m_evChargers.insert(thing->id(), evCharger);
|
|
setupEvCharger(thing);
|
|
setupPluggedInHandlers(thing);
|
|
}
|
|
}
|
|
|
|
void SmartChargingManager::onThingRemoved(const ThingId &thingId)
|
|
{
|
|
if (m_chargingInfos.contains(thingId)) {
|
|
m_chargingInfos.remove(thingId);
|
|
emit chargingInfoRemoved(thingId);
|
|
|
|
m_evChargers.remove(thingId); // Deleted by parenting
|
|
}
|
|
}
|
|
|
|
void SmartChargingManager::onActionExecuted(const Action &action, Thing::ThingError status)
|
|
{
|
|
if (status != Thing::ThingErrorNoError)
|
|
return;
|
|
|
|
Thing *thing = m_thingManager->findConfiguredThing(action.thingId());
|
|
if (!thing) {
|
|
return;
|
|
}
|
|
|
|
if (action.triggeredBy() != Action::TriggeredByUser) {
|
|
return;
|
|
}
|
|
qCDebug(dcNymeaEnergy()) << "User action executed on EV charger:" << thing->thingClass().actionTypes().findById(action.actionTypeId()).name() << action.params();
|
|
|
|
if (thing->thingClass().interfaces().contains("evcharger")) {
|
|
// Storing the values (power and max charging current) the user sets for charging station manually,
|
|
// so we can reset the charging current to the user desired value if the user changes from eco mode to normal mode.
|
|
|
|
if (chargingInfo(thing->id()).chargingMode() == ChargingInfo::ChargingModeNormal) {
|
|
// Monitor the power, max charging current and desired phase count
|
|
StateType powerStateType = thing->thingClass().stateTypes().findByName("power");
|
|
ActionType powerActionType = thing->thingClass().actionTypes().findById(powerStateType.id());
|
|
|
|
StateType maxChargingStateType = thing->thingClass().stateTypes().findByName("maxChargingCurrent");
|
|
ActionType maxChargingActionType = thing->thingClass().actionTypes().findById(maxChargingStateType.id());
|
|
|
|
StateType desiredPhaseCountStateType = thing->thingClass().stateTypes().findByName("desiredPhaseCount");
|
|
ActionType desiredPhaseCountActionType = thing->thingClass().actionTypes().findById(desiredPhaseCountStateType.id());
|
|
|
|
if (action.actionTypeId() == powerActionType.id()) {
|
|
bool manualChargingEnabled = action.paramValue(powerActionType.paramTypes().findByName("power").id()).toBool();
|
|
qCDebug(dcNymeaEnergy()) << "Manual charging is now" << (manualChargingEnabled ? "enabled": "disabled") << "for" << thing->name();
|
|
storeManualChargingParameters(thing->id(), manualChargingEnabled, manualMaxChargingCurrent(thing->id()), manualDesiredPhaseCount(thing->id()));
|
|
#ifndef ENERGY_SIMULATION
|
|
// [ETM] DÉCLENCHEUR RÉEL — exclu en simulation (cf. check-simulation-clock.py).
|
|
update(QDateTime::currentDateTime());
|
|
#endif
|
|
return;
|
|
}
|
|
if (action.actionTypeId() == maxChargingActionType.id()) {
|
|
uint manualChargingCurrent = qRound(action.paramValue(maxChargingActionType.paramTypes().findByName("maxChargingCurrent").id()).toDouble());
|
|
qCDebug(dcNymeaEnergy()) << "Manual charging current set to" << manualChargingCurrent << "for" << thing->name();
|
|
storeManualChargingParameters(thing->id(), manualChargingEnabled(thing->id()), manualChargingCurrent, manualDesiredPhaseCount(thing->id()));
|
|
#ifndef ENERGY_SIMULATION
|
|
// [ETM] DÉCLENCHEUR RÉEL — exclu en simulation (cf. check-simulation-clock.py).
|
|
update(QDateTime::currentDateTime());
|
|
#endif
|
|
return;
|
|
}
|
|
if (desiredPhaseCountStateType.isValid() && action.actionTypeId() == desiredPhaseCountActionType.id()) {
|
|
uint phaseCount = action.paramValue(desiredPhaseCountActionType.paramTypes().findByName("desiredPhaseCount").id()).toUInt();
|
|
qCDebug(dcNymeaEnergy()) << "Manual phase count set to" << phaseCount << "for" << thing->name();
|
|
storeManualChargingParameters(thing->id(), manualChargingEnabled(thing->id()), manualMaxChargingCurrent(thing->id()), phaseCount);
|
|
#ifndef ENERGY_SIMULATION
|
|
// [ETM] DÉCLENCHEUR RÉEL — exclu en simulation (cf. check-simulation-clock.py).
|
|
update(QDateTime::currentDateTime());
|
|
#endif
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (thing->thingClass().interfaces().contains("electricvehicle")) {
|
|
// Storing the SoC the user sets, so we can reset to that value when it is unplugged (while it's plugged we'll increase the SoC if it's writable)
|
|
|
|
if (!thing->thingClass().hasStateType("batteryLevel")) {
|
|
return; // Shouldn't ever happen, but it's optional in the interface, so we'll have to verify
|
|
}
|
|
|
|
if (!thing->thingClass().stateTypes().findByName("batteryLevel").writable()) {
|
|
// We can't write the battery level which means it's delivered by an API to the car, nothing to do for us here then...
|
|
return;
|
|
}
|
|
|
|
EnergySettings settings;
|
|
settings.beginGroup("ManualSoCs");
|
|
settings.setValue(thing->id().toString(), thing->stateValue("batteryLevel").toInt());
|
|
settings.endGroup();
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Resetting custom SoC calculation";
|
|
thing->setProperty("preciseSoC", thing->stateValue("batteryLevel").toInt());
|
|
thing->setProperty("lastSoCCalculation", QVariant());
|
|
}
|
|
}
|
|
|
|
void SmartChargingManager::onChargingModeChanged(const ThingId &evChargerId, const ChargingInfo &chargingInfo)
|
|
{
|
|
EvCharger *evCharger = m_evChargers.value(evChargerId);
|
|
if (!evCharger) {
|
|
qCWarning(dcNymeaEnergy()) << "Charging mode changed but the associated thing does not exist. Ignoring the event.";
|
|
return;
|
|
}
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Charging mode changed for" << evCharger->name() << chargingInfo.chargingMode();
|
|
|
|
// Restore user desired ev charging settings if switching back to normal mode
|
|
if (chargingInfo.chargingMode() == ChargingInfo::ChargingModeNormal) {
|
|
bool enabled = manualChargingEnabled(evChargerId);
|
|
uint maxCurrent = manualMaxChargingCurrent(evChargerId);
|
|
uint desiredPhaseCount = manualDesiredPhaseCount(evChargerId);
|
|
qCDebug(dcNymeaEnergy()) << "EV charger" << evCharger->name() << "changed to normal mode. Restoring manual values of" << enabled << maxCurrent << "A" << desiredPhaseCount << "phases";
|
|
|
|
// In manual (aka fast) mode, we'll always select 3 phases for now
|
|
if (m_processInfos.value(evCharger).canSwitchPhaseCount) {
|
|
evCharger->setDesiredPhaseCount(desiredPhaseCount);
|
|
}
|
|
|
|
if (evCharger->chargingEnabled() != enabled) {
|
|
evCharger->setChargingEnabled(enabled, QDateTime::currentDateTime(), true);
|
|
}
|
|
|
|
if (evCharger->maxChargingCurrent() != maxCurrent) {
|
|
evCharger->setMaxChargingCurrent(maxCurrent, QDateTime::currentDateTime(), true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SmartChargingManager::setupRootMeter(Thing *thing)
|
|
{
|
|
if (m_rootMeter) {
|
|
m_rootMeter->deleteLater();
|
|
m_rootMeter = nullptr;
|
|
}
|
|
|
|
if (m_energyManager->rootMeter()) {
|
|
Q_ASSERT_X(thing->thingClass().interfaces().contains("energymeter"), "SmartChargingManager", "setupRootMeter called for a thing which isn't an energymeter.");
|
|
qCInfo(dcNymeaEnergy()) << "Setting root meter to" << thing->name();
|
|
m_rootMeter = new RootMeter(m_energyManager->rootMeter());
|
|
} else {
|
|
qCInfo(dcNymeaEnergy()) << "Root meter unset. Smart charging will cease to work until a new root meter is configured.";
|
|
}
|
|
}
|
|
|
|
|
|
void SmartChargingManager::setupEvCharger(Thing *thing)
|
|
{
|
|
Q_ASSERT_X(thing->thingClass().interfaces().contains("evcharger"), "SmartChargingManager", "setupEvCharger called for a thing which isn't an evcharger.");
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Setting up EV charger:" << thing->name();
|
|
ChargingInfo chargingInfo(thing->id());
|
|
m_chargingInfos.insert(thing->id(), chargingInfo);
|
|
emit chargingInfoAdded(chargingInfo);
|
|
}
|
|
|
|
void SmartChargingManager::setupPluggedInHandlers(const Thing *thing)
|
|
{
|
|
Q_ASSERT_X(thing->thingClass().interfaces().contains("evcharger"), "SmartChargingManager", "setupPluggedInHandlers called for a thing which isn't an evcharger.");
|
|
|
|
qCDebug(dcNymeaEnergy()) << "Setting up push notification";
|
|
|
|
connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId, const QVariant &value){
|
|
if (thing->thingClass().getStateType(stateTypeId).name() == "pluggedIn") {
|
|
if (value.toBool()) {
|
|
qCDebug(dcNymeaEnergy()) << "The car has been plugged in!";
|
|
|
|
ChargingInfo info = m_chargingInfos.value(thing->id());
|
|
|
|
if (info.chargingMode() == ChargingInfo::ChargingModeNormal) {
|
|
qCDebug(dcNymeaEnergy()) << "Charger is set to normal charging mode. No state of charge updating required.";
|
|
return;
|
|
}
|
|
|
|
Thing *car = m_thingManager->findConfiguredThing(info.assignedCarId());
|
|
if (!car) {
|
|
qCDebug(dcNymeaEnergy()) << "No car assigned to this EV charger.";
|
|
return;
|
|
}
|
|
|
|
if (!car->thingClass().actionTypes().findByName("batteryLevel").id().isNull()) {
|
|
qCDebug(dcNymeaEnergy()) << "This car requires manual State of charge input!";
|
|
ActionType batteryLevelActionType = car->thingClass().actionTypes().findByName("batteryLevel");
|
|
|
|
// Restore the last manually set SoC
|
|
EnergySettings settings;
|
|
settings.beginGroup("ManualSoCs");
|
|
int batteryLevel = settings.value(car->id().toString(), 20).toInt();
|
|
settings.endGroup();
|
|
Action batteryLevelAction(batteryLevelActionType.id(), car->id(), Action::TriggeredByRule);
|
|
batteryLevelAction.setParams({Param(batteryLevelActionType.id(), batteryLevel)});
|
|
m_thingManager->executeAction(batteryLevelAction);
|
|
|
|
// And fire the push notification so the user may adjust it
|
|
foreach (Thing *notificationThing, m_thingManager->configuredThings().filterByInterface("notifications")) {
|
|
ActionType actionType = notificationThing->thingClass().actionTypes().findByName("notify");
|
|
Action action(actionType.id(), notificationThing->id(), Action::TriggeredByRule);
|
|
QString title = QT_TR_NOOP("Car plugged in");
|
|
QString body = QT_TR_NOOP("Tap here to update the state of charge.");
|
|
ChargingInfo info = m_chargingInfos.value(thing->id());
|
|
|
|
QTranslator translator;
|
|
bool status = translator.load(info.locale(), "nymea-energy-plugin-nymea", "-", NymeaSettings::translationsPath(), ".qm");
|
|
if (!status) {
|
|
qCWarning(dcNymeaEnergy()) << "Error loading translations for notification from:" << NymeaSettings::translationsPath() + "/nymea-energy-plugin-nymea" + "-[" + info.locale().name() + "].qm";
|
|
}
|
|
QString translatedTitle = translator.translate("SmartChargingManager", title.toUtf8());
|
|
if (!translatedTitle.isEmpty()) {
|
|
title = translatedTitle;
|
|
}
|
|
QString translatedBody = translator.translate("SmartChargingManager", body.toUtf8());
|
|
if (!translatedBody.isEmpty()) {
|
|
body = translatedBody;
|
|
}
|
|
|
|
QUrlQuery data;
|
|
data.addQueryItem("open", "nymea.energy");
|
|
data.addQueryItem("thingId", thing->id().toString());
|
|
|
|
ParamList params = {
|
|
Param(actionType.paramTypes().findByName("title").id(), title),
|
|
Param(actionType.paramTypes().findByName("body").id(), body)
|
|
};
|
|
if (actionType.paramTypes().findByName("data").isValid()) {
|
|
params.append(Param(actionType.paramTypes().findByName("data").id(), data.toString()));
|
|
}
|
|
action.setParams(params);
|
|
m_thingManager->executeAction(action);
|
|
}
|
|
}
|
|
} else {
|
|
qCDebug(dcNymeaEnergy()) << "The car has been unplugged!";
|
|
if (lockOnUnplug()) {
|
|
qCDebug(dcNymeaEnergy()) << "Lock on unplug is set. Reverting to manual mode and disabling charging";
|
|
EvCharger *evCharger = m_evChargers.value(thing->id());
|
|
evCharger->setChargingEnabled(false, QDateTime::currentDateTime(), true);
|
|
m_chargingInfos[evCharger->id()].setChargingMode(ChargingInfo::ChargingModeNormal);
|
|
storeChargingInfo(m_chargingInfos.value(evCharger->id()));
|
|
emit chargingInfoChanged(m_chargingInfos.value(evCharger->id()));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void SmartChargingManager::storeChargingInfo(const ChargingInfo &chargingInfo)
|
|
{
|
|
EnergySettings settings;
|
|
settings.beginGroup("ChargingInfos");
|
|
settings.beginGroup(chargingInfo.evChargerId().toString());
|
|
settings.setValue("assignedCarId", chargingInfo.assignedCarId());
|
|
settings.setValue("chargingMode", chargingInfo.chargingMode());
|
|
settings.setValue("endDateTime", chargingInfo.endDateTime());
|
|
QVariantList days;
|
|
foreach (int day, chargingInfo.repeatDays()) {
|
|
days.append(day);
|
|
}
|
|
settings.setValue("repeatDays", days);
|
|
settings.setValue("targetPercentage", chargingInfo.targetPercentage());
|
|
settings.setValue("locale", chargingInfo.locale());
|
|
settings.setValue("spotMarketChargingEnabled", chargingInfo.spotMarketChargingEnabled());
|
|
settings.setValue("dailySpotMarketPercentage", chargingInfo.dailySpotMarketPercentage());
|
|
settings.endGroup();
|
|
settings.endGroup();
|
|
}
|
|
|
|
void SmartChargingManager::storeManualChargingParameters(const ThingId &evChargerId, bool enabled, int maxChargingCurrent, uint desiredPhaseCount)
|
|
{
|
|
EnergySettings settings;
|
|
settings.beginGroup("ChargingInfos");
|
|
settings.beginGroup(evChargerId.toString());
|
|
settings.setValue("manualChargingEnabled", enabled);
|
|
settings.setValue("manualMaxChargingCurrent", maxChargingCurrent);
|
|
settings.setValue("manualDesiredPhaseCount", desiredPhaseCount);
|
|
settings.endGroup();
|
|
settings.endGroup();
|
|
}
|
|
|
|
bool SmartChargingManager::manualChargingEnabled(const ThingId &evChargerId) const
|
|
{
|
|
EnergySettings settings;
|
|
settings.beginGroup("ChargingInfos");
|
|
settings.beginGroup(evChargerId.toString());
|
|
return settings.value("manualChargingEnabled", false).toBool();
|
|
}
|
|
|
|
uint SmartChargingManager::manualMaxChargingCurrent(const ThingId &evChargerId) const
|
|
{
|
|
EnergySettings settings;
|
|
settings.beginGroup("ChargingInfos");
|
|
settings.beginGroup(evChargerId.toString());
|
|
return settings.value("manualMaxChargingCurrent", 0).toUInt();
|
|
}
|
|
|
|
uint SmartChargingManager::manualDesiredPhaseCount(const ThingId &evChargerId) const
|
|
{
|
|
EnergySettings settings;
|
|
settings.beginGroup("ChargingInfos");
|
|
settings.beginGroup(evChargerId.toString());
|
|
return settings.value("manualDesiredPhaseCount", 3).toUInt();
|
|
}
|
|
|
|
Electricity::Phases SmartChargingManager::getAscendingPhasesForCount(uint phaseCount)
|
|
{
|
|
Electricity::Phases phases = Electricity::PhaseNone;
|
|
if (phaseCount == 3) {
|
|
phases = Electricity::PhaseAll;
|
|
} else if (phaseCount == 2) {
|
|
phases.setFlag(Electricity::PhaseA);
|
|
phases.setFlag(Electricity::PhaseB);
|
|
} else {
|
|
phases.setFlag(Electricity::PhaseA);
|
|
}
|
|
return phases;
|
|
}
|
|
|
|
QString SmartChargingManager::chargerPhaseKey(EvCharger *evCharger) const
|
|
{
|
|
// Use live metering first — reliable when charger is actively charging
|
|
Electricity::Phases metered = evCharger->meteredPhases();
|
|
if (metered.testFlag(Electricity::PhaseA)) return QStringLiteral("A");
|
|
if (metered.testFlag(Electricity::PhaseB)) return QStringLiteral("B");
|
|
if (metered.testFlag(Electricity::PhaseC)) return QStringLiteral("C");
|
|
|
|
// Charger may be off — fall back to last known phase stored by prepareInformation()
|
|
if (m_processInfos.contains(evCharger)) {
|
|
Electricity::Phases known = m_processInfos.value(evCharger).effectivePhases;
|
|
if (known.testFlag(Electricity::PhaseA)) return QStringLiteral("A");
|
|
if (known.testFlag(Electricity::PhaseB)) return QStringLiteral("B");
|
|
if (known.testFlag(Electricity::PhaseC)) return QStringLiteral("C");
|
|
}
|
|
|
|
// Phase unknown — conservative fallback (unchanged previous behavior)
|
|
qCWarning(dcNymeaEnergy()) << "Cannot determine connected phase for"
|
|
<< evCharger->name() << "— defaulting to phase A";
|
|
return QStringLiteral("A");
|
|
}
|
|
|
|
uint SmartChargingManager::getBestPhaseCount(EvCharger *evCharger, double surplusAmpere)
|
|
{
|
|
uint desiredPhaseCount = 1;
|
|
|
|
// Check if we already have enough surplus for minimal 3 phase charging without acquisition
|
|
// In that case we just switch to 3 phase charging and skip the rest of the calculations
|
|
if (surplusAmpere > (3.0 * m_processInfos.value(evCharger).minimalChargingCurrent)) {
|
|
qCDebug(dcNymeaEnergy()) << "There is enough solar power for 3-phase charging without an acquisition";
|
|
desiredPhaseCount = 3;
|
|
return desiredPhaseCount;
|
|
}
|
|
|
|
// Check if we can do the job with 1 phase without acquisition
|
|
if (surplusAmpere > m_processInfos.value(evCharger).minimalChargingCurrent &&
|
|
surplusAmpere < evCharger->maxChargingCurrentMaxValue()) {
|
|
qCDebug(dcNymeaEnergy()) << "There is enough solar power for 1-phase charging without an acquisition";
|
|
desiredPhaseCount = 1;
|
|
return desiredPhaseCount;
|
|
}
|
|
|
|
// Check if we can do the job with 1 phase + acquisition
|
|
if (surplusAmpere > m_processInfos.value(evCharger).minimalChargingCurrent * m_acquisitionTolerance &&
|
|
surplusAmpere < evCharger->maxChargingCurrentMaxValue()) {
|
|
qCDebug(dcNymeaEnergy()) << "There is enough solar power for 1-phase charging with an acquisition of" << m_processInfos.value(evCharger).minimalChargingCurrent - surplusAmpere << "[A]";
|
|
desiredPhaseCount = 1;
|
|
return desiredPhaseCount;
|
|
}
|
|
|
|
// Check if we are between the 3 phase min and 1 phase max charging gap, if so, stay on single phase
|
|
// in order to prevent a charging stop
|
|
// 1 * 16A * 230V = 3680W
|
|
// 3 * 6 * 230V = 4140W
|
|
if (surplusAmpere < m_processInfos.value(evCharger).minimalChargingCurrent * m_acquisitionTolerance * 3 &&
|
|
surplusAmpere > evCharger->maxChargingCurrentMaxValue() * m_acquisitionTolerance) {
|
|
qCDebug(dcNymeaEnergy()) << "We are right between the 3-phase min current and 1-phase max current interval. Falling back to single phase charging in order to prevent a charing stop.";
|
|
desiredPhaseCount = 1;
|
|
return desiredPhaseCount;
|
|
}
|
|
|
|
//We ruled out any situation where single phase charging would make sense, let's use 3 phases here
|
|
desiredPhaseCount = 3;
|
|
return desiredPhaseCount;
|
|
}
|
|
|
|
void SmartChargingManager::executeChargingAction(EvCharger *evCharger, const ChargingAction &chargingAction, const QDateTime ¤tDateTime)
|
|
{
|
|
qCDebug(dcNymeaEnergy()).nospace().noquote() << "Executing action " << evCharger->name()
|
|
<< " to power: " << (chargingAction.chargingEnabled() ? "ON," : "OFF,")
|
|
<< " Charging current: " << chargingAction.maxChargingCurrent() << "A, "
|
|
<< (m_processInfos.value(evCharger).canSwitchPhaseCount ? "Switch phases to: " + QString::number(chargingAction.desiredPhaseCount()) + ", " : "" )
|
|
<< "Issuer: " << chargingAction.issuerString() << ", forced: " << chargingAction.force();
|
|
|
|
if (m_processInfos.value(evCharger).canSwitchPhaseCount) {
|
|
evCharger->setDesiredPhaseCount(chargingAction.desiredPhaseCount());
|
|
m_processInfos[evCharger].effectivePhaseCount = chargingAction.desiredPhaseCount();
|
|
if (chargingAction.desiredPhaseCount() == 1) {
|
|
m_processInfos[evCharger].effectivePhases = Electricity::PhaseA;
|
|
} else {
|
|
m_processInfos[evCharger].effectivePhases = Electricity::PhaseAll;
|
|
}
|
|
}
|
|
|
|
// FIXME: execute sequential and check what to do if an action failes, retry?
|
|
|
|
evCharger->setMaxChargingCurrent(chargingAction.maxChargingCurrent(), currentDateTime, chargingAction.force());
|
|
evCharger->setChargingEnabled(chargingAction.chargingEnabled(), currentDateTime, chargingAction.force());
|
|
}
|
|
|