1841 lines
102 KiB
C++
1841 lines
102 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();
|
|
m_batteryLevelConsideration = settings.value("batteryLevelConsideration", 0.9).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);
|
|
|
|
update(QDateTime::currentDateTime());
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
update(QDateTime::currentDateTime());
|
|
}
|
|
}
|
|
|
|
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) {
|
|
|
|
// FIXME: get the actual phase, not assume it is phase A in single phase charging
|
|
if (requiredThrottlePower.value("A") > 0) {
|
|
int throttleAmpere = qCeil(requiredThrottlePower.value("A") / 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) {
|
|
// FIXME: get the actual phase, not assume it is phase A in single phase charging
|
|
if (availablePhasePower.value("A") >= 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) {
|
|
// FIXME: get the actual phase, not assume it is phase A in single phase charging
|
|
if (availablePhasePower.value("A") >= 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;
|
|
}
|
|
|
|
QDateTime endDateTime = m_chargingInfos.value(evCharger->id()).nextEndTime(currentDateTime);
|
|
if (!endDateTime.isValid() && m_chargingInfos.value(evCharger->id()).chargingMode() != ChargingInfo::ChargingModeNormal) {
|
|
qCDebug(dcNymeaEnergy()).nospace() << "No valid target time set for " << evCharger->name();
|
|
if (m_chargingInfos.value(evCharger->id()).chargingMode() == ChargingInfo::ChargingModeEcoWithTargetTime) {
|
|
qCDebug(dcNymeaEnergy()) << "Resetting to ECO mode without time.";
|
|
}
|
|
m_chargingInfos[evCharger->id()].setChargingMode(ChargingInfo::ChargingModeEco);
|
|
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()).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) {
|
|
|
|
QDateTime endDateTime = info.nextEndTime(currentDateTime);
|
|
if (!endDateTime.isValid()) {
|
|
qCDebug(dcNymeaEnergy()).nospace() << "No valid target time set for " << evCharger->name();
|
|
if (info.chargingMode() == ChargingInfo::ChargingModeEcoWithTargetTime) {
|
|
qCDebug(dcNymeaEnergy()) << "Resetting to ECO mode without time.";
|
|
}
|
|
m_chargingInfos[evCharger->id()].setChargingMode(ChargingInfo::ChargingModeEco);
|
|
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) {
|
|
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();
|
|
if (info.chargingMode() == ChargingInfo::ChargingModeEcoWithTargetTime) {
|
|
qCDebug(dcNymeaEnergy()) << "Resetting to ECO mode without time.";
|
|
}
|
|
m_chargingInfos[evCharger->id()].setChargingMode(ChargingInfo::ChargingModeEco);
|
|
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);
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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. 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()));
|
|
update(QDateTime::currentDateTime());
|
|
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()));
|
|
update(QDateTime::currentDateTime());
|
|
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);
|
|
update(QDateTime::currentDateTime());
|
|
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;
|
|
}
|
|
|
|
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,")
|
|
<< " Carging 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());
|
|
}
|
|
|