// 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 &currentDateTime)
{
    qCDebug(dcNymeaEnergy()) << "Updating smart charging";
    updateManualSoCsWithoutMeter(currentDateTime);
    prepareInformation(currentDateTime);
    verifyOverloadProtection(currentDateTime);
    verifyOverloadProtectionRecovery(currentDateTime);
    planSpotMarketCharging(currentDateTime);
    planSurplusCharging(currentDateTime);
    adjustEvChargers(currentDateTime);
}


void SmartChargingManager::verifyOverloadProtection(const QDateTime &currentDateTime)
{
    if (!m_rootMeter) {
        qCDebug(dcNymeaEnergy()) << "Overload protection: No root meter configured, the overload protection is not available.";
        return;
    }

    // Verify if any of the phases is approaching the limit
    if (m_phasePowerConsumptionLimit == 0) {
        // Power limit not set up
        qCDebug(dcNymeaEnergy()) << "Overload protection: No power limit configured, the overload protection is disabled.";
        return;
    }

    QHash<QString, double> currentPhaseConsumption = {{"A", m_rootMeter->currentPowerPhaseA()},
                                                      {"B", m_rootMeter->currentPowerPhaseB()},
                                                      {"C", m_rootMeter->currentPowerPhaseC()}};

    bool limitExceeded = false;

    double consumptionLimitInWatt = 230 * m_phasePowerConsumptionLimit;

    // Note: for now we assume the phases are always connected this way:
    // EV phase A -> house phase A
    // EV phase B -> house phase B
    // EV phase C -> house phase C

    // In case of single phase charging, we assume power on phase A
    // EV phase A -> house phase A

    // Once we have a mapping of consumer phases to house phases, we can be much more prcise here and also handle
    // multiple chargers connected on different phases in single phase mode.

    double maxRequiredThrottlePower = 0;
    QHash<QString, double> requiredThrottlePower = {{"A", 0},
                                                    {"B", 0},
                                                    {"C", 0}};

    foreach (const QString &phase, currentPhaseConsumption.keys()) {
        if (currentPhaseConsumption.value(phase) > consumptionLimitInWatt) {
            qCInfo(dcNymeaEnergy()) << "Overload protection: Phase" << phase << "exceeding limit:" << currentPhaseConsumption.value(phase) << "W (" << (currentPhaseConsumption.value(phase) / 230) << "A). Maximum allowance:" << consumptionLimitInWatt << "W (" << m_phasePowerConsumptionLimit << "A)";
            limitExceeded = true;
            double overshot = currentPhaseConsumption.value(phase) - consumptionLimitInWatt;
            requiredThrottlePower[phase] = overshot;
            if (overshot > maxRequiredThrottlePower) {
                maxRequiredThrottlePower = overshot;
            }
        }
    }

    if (limitExceeded) {
        qCDebug(dcNymeaEnergy()) << "Overload protection: Triggered, maximum power consumption per phase is:"
                                 << consumptionLimitInWatt << "[W], Overshot on phases:"
                                 << "A:" << requiredThrottlePower.value("A") << "[W],"
                                 << "B:" << requiredThrottlePower.value("B") << "[W],"
                                 << "C:" << requiredThrottlePower.value("C") << "[W]";

        foreach (EvCharger *evCharger, m_evChargers) {

            // Evaluate only charger which are actually charging and which are connected to the overloaded phase
            if (!evCharger->chargingEnabled())
                continue;


            if (m_chargingInfos.value(evCharger->id()).chargingMode() == ChargingInfo::ChargingModeNormal && !m_overloadProtectionActive.value(evCharger, false)) {
                qCDebug(dcNymeaEnergy()) << "Overload protection: Activated for charger in normal mode" << evCharger->name();
                m_overloadProtectionActive[evCharger] = true;
            }

            ChargingAction action;

            if (evCharger->phaseCount() == 1) {

                const QString phase = chargerPhaseKey(evCharger);
                if (requiredThrottlePower.value(phase) > 0) {
                    int throttleAmpere = qCeil(requiredThrottlePower.value(phase) / 230);
                    int desiredFallbackAmpere = evCharger->maxChargingCurrent() - throttleAmpere;

                    if (desiredFallbackAmpere < static_cast<int>(m_processInfos[evCharger].minimalChargingCurrent)) {
                        qCDebug(dcNymeaEnergy()) << "Overload protection: Need to switch of charging for" << evCharger->name();
                        action = ChargingAction(false, evCharger->maxChargingCurrent(), evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection, true);
                    } else {
                        qCDebug(dcNymeaEnergy()) << "Overload protection: Adjusting charger from" << evCharger->maxChargingCurrent() << "[A] ->" << desiredFallbackAmpere << "[A]";
                        action = ChargingAction(true, desiredFallbackAmpere, evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection);
                    }
                }
            } else {
                // Charger is on all phases
                int throttleAmpere = qCeil(maxRequiredThrottlePower / 230);
                int desiredFallbackAmpere = evCharger->maxChargingCurrent() - throttleAmpere;

                if (desiredFallbackAmpere < static_cast<int>(m_processInfos[evCharger].minimalChargingCurrent)) {

                    // TODO: Check if switching to one phase would solve the overshot issue on the phases
                    // if (evCharger->canSetPhaseCount()) {
                    // }

                    qCDebug(dcNymeaEnergy()) << "Overload protection: Need to switch of charging for" << evCharger->name();
                    action = ChargingAction(false, evCharger->maxChargingCurrent(), evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection, true);
                } else {
                    qCDebug(dcNymeaEnergy()) << "Overload protection: Adjusting charger from" << evCharger->maxChargingCurrent() << "[A] ->" << desiredFallbackAmpere << "[A]";
                    action = ChargingAction(true, desiredFallbackAmpere, evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection);
                }
            }

            executeChargingAction(evCharger, action, currentDateTime);
        }
    }
}

void SmartChargingManager::verifyOverloadProtectionRecovery(const QDateTime &currentDateTime)
{
    // Let's evaluate if we can move back any charger which is in normal mode,
    // for the eco mode chargers the rest of the logic should do the trick...

    foreach (EvCharger *evCharger, m_evChargers) {
        if (m_chargingInfos.value(evCharger->id()).chargingMode() == ChargingInfo::ChargingModeNormal && m_overloadProtectionActive.value(evCharger, false)) {
            // Check if we can move back to the user desired charging level
            qCDebug(dcNymeaEnergy()) << "Overload protection: Evaluating normal mode charger" << evCharger->name() << "which has been set originally to"
                                     << manualChargingEnabled(evCharger->id()) << manualMaxChargingCurrent(evCharger->id()) << "[A]";


            QHash<QString, double> currentPhaseConsumption = {{"A", m_rootMeter->currentPowerPhaseA()},
                                                              {"B", m_rootMeter->currentPowerPhaseB()},
                                                              {"C", m_rootMeter->currentPowerPhaseC()}};

            double consumptionLimitInWatt = 230 * m_phasePowerConsumptionLimit;

            QHash<QString, double> availablePhasePower = {{"A", 0},
                                                          {"B", 0},
                                                          {"C", 0}};

            // Calculate available power on each phase
            foreach (const QString &phase, currentPhaseConsumption.keys()) {
                double phasePower = currentPhaseConsumption.value(phase);
                if (phasePower > consumptionLimitInWatt)
                    continue;

                availablePhasePower[phase] = consumptionLimitInWatt - phasePower;
            }

            qCDebug(dcNymeaEnergy()) << "Overload protection: Available phase power:"
                                     << "A:" << availablePhasePower.value("A") << "[W],"
                                     << "B:" << availablePhasePower.value("B") << "[W],"
                                     << "C:" << availablePhasePower.value("C") << "[W]";

            // Check if the charger has been switched of or just throttled
            if (evCharger->chargingEnabled()) {
                // Charger has been throttled
                double requiredRestorePhasePower = (manualMaxChargingCurrent(evCharger->id()) - evCharger->maxChargingCurrent()) * 230;

                qCDebug(dcNymeaEnergy()) << "Overload protection: For restoring the original charging power at least" << requiredRestorePhasePower << "[W] per phase are required.";

                bool restoreCharger = false;

                if (evCharger->phaseCount() == 1) {
                    if (availablePhasePower.value(chargerPhaseKey(evCharger)) >= requiredRestorePhasePower) {
                        qCDebug(dcNymeaEnergy()) << "Overload protection: Enought power available to restore the original configuration"
                                                 << manualMaxChargingCurrent(evCharger->id()) << "[A]";
                        restoreCharger = true;
                    }
                } else {

                    bool enoughtOnAllPhases = true;
                    foreach(const QString &phase, availablePhasePower.keys()) {
                        if (availablePhasePower.value(phase) < requiredRestorePhasePower) {
                            enoughtOnAllPhases = false;
                            qCDebug(dcNymeaEnergy()) << "Overload protection: Not enought power available on phase" << phase
                                                     << availablePhasePower.value(phase) << "[W]";
                        }
                    }

                    if (enoughtOnAllPhases) {
                        restoreCharger = true;
                    }

                }

                if (restoreCharger) {
                    qCDebug(dcNymeaEnergy()) << "Overload protection: Restoring charger to" << manualMaxChargingCurrent(evCharger->id()) << "[A]";
                    ChargingAction action = ChargingAction(true, manualMaxChargingCurrent(evCharger->id()) , evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection);
                    executeChargingAction(evCharger, action, currentDateTime);
                    m_overloadProtectionActive[evCharger] = false;
                }

            } else {
                // Charging has been disabled by overload protection, let's see if we can switch back on to the minimal
                // charging current, once we switched it back on, the section above should handle the recovery to the original values.

                double requiredRestorePhasePower = m_processInfos.value(evCharger).minimalChargingCurrent * 230;

                qCDebug(dcNymeaEnergy()) << "Overload protection: At least" << requiredRestorePhasePower << "[W] per phase are needed for switching the charger back on.";
                bool restoreChargerPower = false;

                if (evCharger->phaseCount() == 1) {
                    if (availablePhasePower.value(chargerPhaseKey(evCharger)) >= requiredRestorePhasePower) {
                        qCDebug(dcNymeaEnergy()) << "Overload protection: Enought power available to start charging using the minimal charging current of"
                                                 << m_processInfos.value(evCharger).minimalChargingCurrent << "[A]";
                        restoreChargerPower = true;
                    }
                } else {

                    bool enoughtOnAllPhases = true;
                    foreach(const QString &phase, availablePhasePower.keys()) {
                        if (availablePhasePower.value(phase) < requiredRestorePhasePower) {
                            enoughtOnAllPhases = false;
                            qCDebug(dcNymeaEnergy()) << "Overload protection: Not enought power available on phase" << phase
                                                     << availablePhasePower.value(phase) << "[W] for switching the charger back on.";
                        }
                    }

                    if (enoughtOnAllPhases) {
                        restoreChargerPower = true;
                    }
                }

                if (restoreChargerPower) {
                    qCDebug(dcNymeaEnergy()) << "Overload protection: Reenable charging at a minimum of" << m_processInfos.value(evCharger).minimalChargingCurrent << "[A]";
                    ChargingAction action = ChargingAction(true, m_processInfos.value(evCharger).minimalChargingCurrent, evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection);
                    executeChargingAction(evCharger, action, currentDateTime);
                }
            }
        }
    }
}

void SmartChargingManager::prepareInformation(const QDateTime &currentDateTime)
{
    // 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();
            const ChargingInfo::ChargingMode currentMode = m_chargingInfos.value(evCharger->id()).chargingMode();
            ChargingInfo::ChargingMode fallbackMode;
            if (currentMode == ChargingInfo::ChargingModeEcoMinWithTargetTime) {
                qCDebug(dcNymeaEnergy()) << "Resetting to ECO with min current mode.";
                fallbackMode = ChargingInfo::ChargingModeEcoWithMinCurrent;
            } else {
                if (currentMode == ChargingInfo::ChargingModeEcoWithTargetTime)
                    qCDebug(dcNymeaEnergy()) << "Resetting to ECO mode without time.";
                fallbackMode = ChargingInfo::ChargingModeEco;
            }
            m_chargingInfos[evCharger->id()].setChargingMode(fallbackMode);
            storeChargingInfo(m_chargingInfos.value(evCharger->id()));
            emit chargingInfoChanged(m_chargingInfos.value(evCharger->id()));
        }

        if (!evCharger->available()) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "is not available. Ignoring...";
            continue;
        }

        evChargers.append(evCharger);
    }

    // 2. Prepare all information avaialble for each charger
    foreach (EvCharger *evCharger, evChargers) {

        qCDebug(dcNymeaEnergy()) << "Prepare information for" << evCharger->name();

        // Car information
        Thing *assignedCar = m_thingManager->findConfiguredThing(m_chargingInfos.value(evCharger->thing()->id()).assignedCarId());
        m_processInfos[evCharger].carPhaseCount = assignedCar->stateValue("phaseCount").toUInt();
        m_processInfos[evCharger].carBatteryLevel = assignedCar->stateValue("batteryLevel").toInt();
        m_processInfos[evCharger].carCapacityInWh = assignedCar->stateValue("capacity").toDouble() * 1000;
        m_processInfos[evCharger].carCapacityInkWh = assignedCar->stateValue("capacity").toDouble();
        m_processInfos[evCharger].carTotalEnergy = assignedCar->stateValue("capacity").toDouble();
        m_processInfos[evCharger].carChargingEnergyLoss = assignedCar->setting("chargingEnergyLoss").toUInt();
        m_processInfos[evCharger].carChargingEnergyLossFactor = 1.0 - (m_processInfos.value(evCharger).carChargingEnergyLoss / 100.0);

        m_processInfos[evCharger].minimalChargingCurrent = qMax(evCharger->maxChargingCurrentMinValue(), assignedCar->stateValue("minChargingCurrent").toUInt());

        qCDebug(dcNymeaEnergy()).nospace() << "Car: state of charge: " << m_processInfos.value(evCharger).carBatteryLevel << "%"
                                           << ", capacity: "<< m_processInfos.value(evCharger).carCapacityInWh << "Wh"
                                           << ", charging energy loss factor " << m_processInfos.value(evCharger).carChargingEnergyLossFactor
                                           << ", phases: " << m_processInfos.value(evCharger).carPhaseCount;

        // Evaluate phase count switching
        if (evCharger->canSetPhaseCount() && m_processInfos.value(evCharger).carPhaseCount > 1) {
            if (evCharger->thing()->state("desiredPhaseCount").possibleValues().contains(1) && evCharger->thing()->state("desiredPhaseCount").possibleValues().contains(3)) {
                // Both, car and EV charger have 3 phases available and we have a desired phase count, lets set the
                qCDebug(dcNymeaEnergy()).nospace() << "Phase count switching is possible with this car/charger combination.";
                m_processInfos[evCharger].canSwitchPhaseCount = true;
                m_processInfos[evCharger].effectivePhaseCount = qMin(evCharger->phaseCount(), m_processInfos.value(evCharger).carPhaseCount);
                m_processInfos[evCharger].minPossiblePhaseCount = 1;
                m_processInfos[evCharger].maxPossiblePhaseCount = 3;
                m_processInfos[evCharger].maxPossiblePhases = Electricity::PhaseAll;
            } else {
                // Verify if both, evCharger and car have at least 3 phases, otherwise we can skip any further checks regarding phase switching
                m_processInfos[evCharger].canSwitchPhaseCount = false;
                m_processInfos[evCharger].maxPossiblePhaseCount = qMin(evCharger->phaseCount(), m_processInfos.value(evCharger).carPhaseCount);
                m_processInfos[evCharger].minPossiblePhaseCount = m_processInfos.value(evCharger).maxPossiblePhaseCount;
                m_processInfos[evCharger].maxPossiblePhases = getAscendingPhasesForCount(m_processInfos.value(evCharger).maxPossiblePhaseCount);
                m_processInfos[evCharger].effectivePhaseCount = m_processInfos.value(evCharger).maxPossiblePhaseCount;
                qCDebug(dcNymeaEnergy()).nospace() << "Phase count switching is not possible with this car/charger combination. Using " << m_processInfos.value(evCharger).effectivePhaseCount << " phases.";
            }
        } else {
            // Either the charger can not set phases or the car has only one phase
            qCDebug(dcNymeaEnergy()).nospace() << "Phase count switching is not possible with this car/charger combination. Using only 1-phase charging.";
            m_processInfos[evCharger].canSwitchPhaseCount = false;
            m_processInfos[evCharger].maxPossiblePhaseCount = qMin(evCharger->phaseCount(), m_processInfos.value(evCharger).carPhaseCount);
            m_processInfos[evCharger].minPossiblePhaseCount = m_processInfos.value(evCharger).maxPossiblePhaseCount;
            m_processInfos[evCharger].maxPossiblePhases = getAscendingPhasesForCount(m_processInfos.value(evCharger).maxPossiblePhaseCount);
            m_processInfos[evCharger].effectivePhaseCount = m_processInfos.value(evCharger).maxPossiblePhaseCount;
        }

        // If possible and the charger provides meter information, check which phases get used based on the power information,
        // otherwise fall back to an ascending phase order
        Electricity::Phases meteredPhases = evCharger->meteredPhases();
        if (meteredPhases != Electricity::PhaseNone) {
            m_processInfos[evCharger].effectivePhases = meteredPhases;
            m_processInfos[evCharger].effectivePhaseCount = Electricity::getPhaseCount(meteredPhases);

            qCDebug(dcNymeaEnergy()) << "Effective phase count for charging including car phase limit (metered)"
                                     << m_processInfos.value(evCharger).effectivePhaseCount << "phases in use:"
                                     << Electricity::convertPhasesToString(m_processInfos.value(evCharger).effectivePhases );
        } else {
            // Fallback to ascending order
            m_processInfos[evCharger].effectivePhases = getAscendingPhasesForCount(m_processInfos.value(evCharger).effectivePhaseCount);
            qCDebug(dcNymeaEnergy()) << "Effective phase count for charging including car phase limit:" << m_processInfos.value(evCharger).effectivePhaseCount
                                     << Electricity::convertPhasesToString(m_processInfos.value(evCharger).effectivePhases);
        }

        m_processInfos[evCharger].chargerPhaseLimitPower = evCharger->maxChargingCurrentMaxValue() * m_processInfos.value(evCharger).voltage;
        qCDebug(dcNymeaEnergy()) << "Household phase limit:" << housePhaseLimitPower << "[W] |" << m_phasePowerConsumptionLimit <<  "A - EV charger phase limit:"
                                 << m_processInfos.value(evCharger).chargerPhaseLimitPower << "[W] |" << evCharger->maxChargingCurrentMaxValue() << "A";
        m_processInfos[evCharger].phaseLimitPower = qMin(housePhaseLimitPower, m_processInfos.value(evCharger).chargerPhaseLimitPower);
        // Note: we have to consider the losses while charging according to the car. That means we need more time to charge the required energy
        m_processInfos[evCharger].totalChargeHours = m_processInfos.value(evCharger).carCapacityInWh / (m_processInfos.value(evCharger).phaseLimitPower * m_processInfos.value(evCharger).carChargingEnergyLossFactor * m_processInfos.value(evCharger).maxPossiblePhaseCount);
        qCDebug(dcNymeaEnergy()).nospace() << "This car needs " << QTime::fromMSecsSinceStartOfDay(m_processInfos.value(evCharger).totalChargeHours * 60 * 60000).toString("hh:mm:ss") << " to charge from 0 to 100% at a charging rate of "
                                           << m_processInfos.value(evCharger).phaseLimitPower << "W / phase (including " << m_processInfos.value(evCharger).carChargingEnergyLoss << "% loss) on phase(s) " << Electricity::convertPhasesToString(m_processInfos.value(evCharger).maxPossiblePhases);
        m_processInfos[evCharger].remainingPercentageToCharge = qMax(0, static_cast<int>(m_chargingInfos.value(evCharger->id()).targetPercentage()) - m_processInfos.value(evCharger).carBatteryLevel);

        // Calculate time
        m_processInfos[evCharger].remainingHoursToCharge = m_processInfos.value(evCharger).totalChargeHours * m_processInfos.value(evCharger).remainingPercentageToCharge / 100.0;
        m_processInfos[evCharger].projectedEndTime = currentDateTime.addSecs(m_processInfos.value(evCharger).remainingHoursToCharge * 60 * 60);
        qCDebug(dcNymeaEnergy()) << "This car still need to charge" << QTime::fromMSecsSinceStartOfDay(m_processInfos.value(evCharger).remainingHoursToCharge * 60 * 60000).toString("hh:mm:ss")
                                 << "to charge" << m_processInfos.value(evCharger).remainingPercentageToCharge << "% using a power rate of" << m_processInfos.value(evCharger).phaseLimitPower
                                 << "W on phase(s)" << Electricity::convertPhasesToString(m_processInfos.value(evCharger).maxPossiblePhases);
        qCDebug(dcNymeaEnergy()) << "Projected endtime" << m_processInfos.value(evCharger).projectedEndTime.toString("dd.MM.yyyy hh:mm:ss");
        m_processInfos[evCharger].spotMarketPercentageRequiredToday = m_chargingInfos.value(evCharger->thing()->id()).dailySpotMarketPercentage();        
        m_processInfos[evCharger].chargeFixedAmountSpotmarketToday = (m_chargingInfos.value(evCharger->thing()->id()).spotMarketChargingEnabled() && m_chargingInfos.value(evCharger->thing()->id()).dailySpotMarketPercentage() > 0);
        m_processInfos[evCharger].spotMaketEnergyRequiredToday = assignedCar->stateValue("capacity").toDouble() * m_chargingInfos.value(evCharger->thing()->id()).dailySpotMarketPercentage() / 100.0;
        qCDebug(dcNymeaEnergy()).nospace() << "A total of " << m_processInfos.value(evCharger).spotMaketEnergyRequiredToday << " kWh (" << m_chargingInfos.value(evCharger->thing()->id()).dailySpotMarketPercentage() << "%) shall be charged from spotmarket today";
    }

    // Reset all actions
    m_chargingActions.clear();
    foreach(EvCharger *evCharger, evChargers) {
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging].setIssuer(ChargingAction::ChargingActionIssuerSurplusCharging);
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging].setChargingEnabled(false);
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging].setMaxChargingCurrent(m_processInfos.value(evCharger).minimalChargingCurrent);
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging].setDesiredPhaseCount(m_processInfos.value(evCharger).minPossiblePhaseCount);

        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging].setIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging].setChargingEnabled(false);
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging].setMaxChargingCurrent(m_processInfos.value(evCharger).minimalChargingCurrent);
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging].setDesiredPhaseCount(m_processInfos.value(evCharger).minPossiblePhaseCount);

        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement].setIssuer(ChargingAction::ChargingActionIssuerTimeRequirement);
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement].setChargingEnabled(false);
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement].setMaxChargingCurrent(m_processInfos.value(evCharger).minimalChargingCurrent);
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement].setDesiredPhaseCount(m_processInfos.value(evCharger).minPossiblePhaseCount);
    }
}

void SmartChargingManager::planSpotMarketCharging(const QDateTime &currentDateTime)
{
    if (!m_spotMarketManager->enabled())
        return;

    bool schedulesChanged = false;

    qCDebug(dcNymeaEnergy()) << "---------------- Plan spot market  ---------------";
    if (!m_spotMarketManager->available()) {
        qCDebug(dcNymeaEnergy()) << "Spot maket manager enabled but not available. Skip spot market charging planing and all previouse planed schedules.";

        // Clean up all schedules related to spotmarket
        foreach (EvCharger *evCharger, m_chargingSchedules.keys()) {
            schedulesChanged |= (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging) > 0);
            m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
        }

        if (schedulesChanged)
            emit chargingSchedulesChanged();

        return;
    }

    // Get all charger with spotmarket enabled
    qCDebug(dcNymeaEnergy()) << "Current time" << currentDateTime.toString("dd.MM.yyyy hh:mm:ss");
    if (m_lastSpotMarketPlanning.isValid()) {
        if (m_lastSpotMarketPlanning.date() != currentDateTime.date()) {
            qCDebug(dcNymeaEnergy()) << "Day changed. Reset spotmarket data counters";
            foreach (EvCharger *evCharger, m_evChargers) {
                if (m_processInfos.contains(evCharger)) {
                    m_processInfos[evCharger].spotMarketEnergyChargedToday = 0;
                }
            }
        }
    }

    m_lastSpotMarketPlanning = currentDateTime;

    // 1. Filter out any EV chargers wich have nothing to do with spot market charging
    EvChargers evChargers;
    foreach (EvCharger *evCharger, m_evChargers) {
        Thing *car = m_thingManager->findConfiguredThing(m_chargingInfos.value(evCharger->thing()->id()).assignedCarId());
        if (!car) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "has no car assigned. Ignoring...";
            schedulesChanged |= (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging) > 0);
            m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
            continue;
        }

        if (m_chargingInfos.value(evCharger->id()).chargingMode() == ChargingInfo::ChargingModeNormal) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "is set to manual mode. Ignoring...";
            schedulesChanged |= (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging) > 0);
            m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
            continue;
        }

        if (!m_chargingInfos.value(evCharger->id()).spotMarketChargingEnabled()) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "spot market charging disabled. Ignoring...";
            schedulesChanged |= (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging) > 0);
            m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
            continue;
        }

        if ((m_chargingInfos.value(evCharger->id()).chargingMode() == ChargingInfo::ChargingModeEco
             || m_chargingInfos.value(evCharger->id()).chargingMode() == ChargingInfo::ChargingModeEcoWithMinCurrent)
            && m_chargingInfos.value(evCharger->id()).dailySpotMarketPercentage() == 0) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "has no target time set nor any daily percentage to charge with spot market. Ignoring...";
            schedulesChanged |= (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging) > 0);
            m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
            continue;
        }

        // Note: we plan also for chargers which are not available or not plugged in currently,
        // because we need to keep the future plan since they might get available and we need to consider that

        evChargers.append(evCharger);
    }

    qCDebug(dcNymeaEnergy()) << evChargers.count() << "chargers are set to ECO mode and have spot market enabled";

    // 2. Create the charging schedules for each individual charger
    foreach (EvCharger *evCharger, evChargers) {

        const ChargingInfo info = m_chargingInfos.value(evCharger->id());

        if (info.chargingMode() == ChargingInfo::ChargingModeEcoWithTargetTime
            || info.chargingMode() == ChargingInfo::ChargingModeEcoMinWithTargetTime) {

            QDateTime endDateTime = info.nextEndTime(currentDateTime);
            if (!endDateTime.isValid()) {
                qCDebug(dcNymeaEnergy()).nospace() << "No valid target time set for " << evCharger->name();
                ChargingInfo::ChargingMode fallbackMode;
                if (info.chargingMode() == ChargingInfo::ChargingModeEcoMinWithTargetTime) {
                    qCDebug(dcNymeaEnergy()) << "Resetting to ECO with min current mode.";
                    fallbackMode = ChargingInfo::ChargingModeEcoWithMinCurrent;
                } else {
                    qCDebug(dcNymeaEnergy()) << "Resetting to ECO mode without time.";
                    fallbackMode = ChargingInfo::ChargingModeEco;
                }
                m_chargingInfos[evCharger->id()].setChargingMode(fallbackMode);
                storeChargingInfo(m_chargingInfos.value(evCharger->id()));
                emit chargingInfoChanged(m_chargingInfos.value(evCharger->id()));
                continue;
            }

            QDateTime internalEndDateTime = endDateTime.addSecs(-10 * 60);

            // Check how many days in the future this target time is.

            // If we have schedules up to the target time, we plan them as good as possible.
            if (m_spotMarketManager->currentProvider()->scoreEntries().availableUpTo(endDateTime)) {

                // We have enougth data to plan as good as possible to the target time
                ScoreEntries availableEntries;
                for (int i = 0; i < m_spotMarketManager->currentProvider()->scoreEntries().count(); i++) {
                    const ScoreEntry score = m_spotMarketManager->currentProvider()->scoreEntries().at(i);
                    if (score.endDateTime() <= endDateTime) {
                        availableEntries.append(score);
                    }
                }

                // Let's weight the available entries
                ScoreEntries weightedScoreEntries = SpotMarketManager::weightScoreEntries(availableEntries);

                double minutesToCharge = m_processInfos[evCharger].remainingHoursToCharge * 60;
                qCDebug(dcNymeaEnergy()) << "Having spot market available until target time. Distribute" << minutesToCharge << "minutes using all available spot market data.";

                // We scheduled the amount to charge using the spot market, we can skip later the target time schedule.
                m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = true;

                bool lockCurrentSchedule = (info.chargingState() == ChargingInfo::ChargingStateSpotMarketCharging && evCharger->charging());

                TimeFrames timeFrames = m_spotMarketManager->scheduleChargingTime(currentDateTime, weightedScoreEntries, minutesToCharge, m_configuration->minimumScheduleDuration(), lockCurrentSchedule);
                ChargingSchedules schedules;
                foreach (const TimeFrame &timeFrame, timeFrames) {
                    ChargingSchedule schedule(evCharger->id(), timeFrame);
                    schedule.setAction(ChargingAction(true, evCharger->maxChargingCurrentMaxValue(), m_processInfos.value(evCharger).maxPossiblePhaseCount, ChargingAction::ChargingActionIssuerSpotMarketCharging));
                    schedules.append(schedule);
                }

                qCDebug(dcNymeaEnergy()) << "Distributed charging schedules" << schedules;

                ChargingSchedules currentSchedules = m_chargingSchedules.value(evCharger).filterByIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
                if (currentSchedules != schedules) {
                    schedulesChanged = true;
                    m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
                    m_chargingSchedules[evCharger].append(schedules);
                }

                // Done with this charger regarding spot market planing
                continue;

            } else {

                // We don't have data up to the given end date time, le't split up the load into days

                // Check how much we want to charge for today and how much we already loaded to meet the target in the future

                double hoursUntilEndDateTime = currentDateTime.secsTo(endDateTime) / 3600.0;
                qCDebug(dcNymeaEnergy()) << "Not enough data available to plan to the target time. Spot market data for"
                                         << m_spotMarketManager->currentProvider()->scoreEntries().count() << "of the required"
                                         << hoursUntilEndDateTime << "hours are available.";

                if (hoursUntilEndDateTime < 36) {

                    // If the end date time is less than 36 h in the future and we have not enougth data, we probably get them soon...
                    // We plan proportial the amount of energy for that slot until we know it until the end.

                    //double ratio = 1 - ((hoursUntilEndDateTime - m_spotMarketManager->currentProvider()->scoreEntries().count()) / hoursUntilEndDateTime);
                    qCDebug(dcNymeaEnergy()) << "The target time is in" << hoursUntilEndDateTime << "hours. We probably get more information soon. Scheduling"
                                             << m_configuration->spotMarketChargePredictableEnergyPercentage() * 100 << "% of the energy within the next known" << m_spotMarketManager->currentProvider()->scoreEntries().count() << "hours.";

                    // Set the required energy for today

                    // If there is also a certain amount of enrgy to charge from spot market today, let's use the higher amount:
                    // either daily percentage or required energy to meet the target time

                    double remainingPercentagePartial = m_processInfos.value(evCharger).remainingPercentageToCharge / 100.0 * m_configuration->spotMarketChargePredictableEnergyPercentage();
                    double percentageToCharge = qMax(remainingPercentagePartial, m_chargingInfos.value(evCharger->id()).dailySpotMarketPercentage() / 100.0);

                    double energyRequiredToday = percentageToCharge * m_processInfos.value(evCharger).carCapacityInkWh;
                    m_processInfos[evCharger].spotMaketEnergyRequiredToday = energyRequiredToday;
                    m_processInfos[evCharger].chargeFixedAmountSpotmarketToday = (energyRequiredToday > 0);
                    m_processInfos[evCharger].spotMarketPercentageRequiredToday = energyRequiredToday * 100.0 / m_processInfos.value(evCharger).carCapacityInkWh;
                    if (m_processInfos.value(evCharger).chargeFixedAmountSpotmarketToday) {
                        qCDebug(dcNymeaEnergy()) << "A total of" << m_processInfos.value(evCharger).spotMaketEnergyRequiredToday
                                                 << "kWh (" << m_processInfos.value(evCharger).spotMarketPercentageRequiredToday
                                                 << "%) battery is required for today to meet the target time in the future.";
                    }


                    // Schedule the rest normal time requirement...
                    m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;

                } else {
                    // Calculate on day bases, 3 days int he future, lets charge every day 1/3
                    qCDebug(dcNymeaEnergy()) << "TODO: plan for more than 24h into the future";
                }

                // The logic for charging a certain amount of energy within one day will is the same as fo the daily percentage.
                // The only difference is that this logic will say how much per day until we have enought schedules to reach the target time.
            }
        } else {
            // We have not scheduled the amount to charge using the spot market,
            // we must plan later the target time schedule.
            m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
        }

        // Check if we have to charge a certain amount of energy today using spotmarket

        if (m_processInfos.value(evCharger).chargeFixedAmountSpotmarketToday) {

            // Either trough the dayly min percentage to charge or if our target time is far
            // in the future and we charge some energy proportional to the distance

            Thing *car = m_thingManager->findConfiguredThing(info.assignedCarId());

            double totalEnergyCharged = car->property("totalEnergyCharged").toDouble();
            if (!qFuzzyCompare(totalEnergyCharged, m_processInfos.value(evCharger).lastTotalEnergyCharged)) {
                m_processInfos[evCharger].spotMarketEnergyChargedToday += totalEnergyCharged - m_processInfos.value(evCharger).lastTotalEnergyCharged;
                m_processInfos[evCharger].lastTotalEnergyCharged = totalEnergyCharged;

                // Check if we already reached our target for today, in which case we are done with this charger regarding spotmarket charging
                if (m_processInfos.value(evCharger).spotMarketEnergyChargedToday >= m_processInfos.value(evCharger).spotMaketEnergyRequiredToday) {
                    // Remove any planed schedule, we are done here
                    m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
                    continue;
                }
            }

            // How many kWh we want to charge today (accoring to car percentage)
            qCDebug(dcNymeaEnergy()) << "Charged" << m_processInfos.value(evCharger).spotMarketEnergyChargedToday << "kWh energy today so far. A total of"
                                     << m_processInfos.value(evCharger).spotMaketEnergyRequiredToday << "kWh spot market energy is required for today.";

            if (m_processInfos.value(evCharger).spotMarketEnergyChargedToday > m_processInfos.value(evCharger).spotMaketEnergyRequiredToday) {
                qCDebug(dcNymeaEnergy()) << "Already charged enougth energy for today.";
                continue;
            }

            // We need to take the loss into account, we actually need more energy
            double remainingEnergyToCharge = m_processInfos.value(evCharger).spotMaketEnergyRequiredToday - m_processInfos.value(evCharger).spotMarketEnergyChargedToday;

            // Note: we already have the power loss in the total hours, so we should have that taken into account in the remaining time too
            // x / remainingEnergyToCharge = totalHours / totalEnergy
            double remeiningHours = m_processInfos.value(evCharger).totalChargeHours * remainingEnergyToCharge / m_processInfos.value(evCharger).carCapacityInkWh;

            double percentageCharged = 0;
            if (m_processInfos.value(evCharger).spotMaketEnergyRequiredToday != 0) {
                // x : spotMarketEnergyChargedToday = 100 : totalEnergy
                percentageCharged = m_processInfos.value(evCharger).spotMarketEnergyChargedToday * 100.0 / m_processInfos.value(evCharger).carTotalEnergy;
            }

            double remainingPercentage = m_processInfos.value(evCharger).spotMarketPercentageRequiredToday - percentageCharged;
            double totalMinutesToday = m_processInfos.value(evCharger).totalChargeHours * 60.0 * m_processInfos.value(evCharger).spotMarketPercentageRequiredToday / 100.0;
            double remainingChargeMinutesToday = remeiningHours * 60;

            qCDebug(dcNymeaEnergy()).nospace() << "Remaining " << remainingPercentage << "% (" << remainingChargeMinutesToday << " min) to charge today from total spotmarket "
                                               << m_processInfos.value(evCharger).spotMarketPercentageRequiredToday
                                               << "% ("  << totalMinutesToday << " min). Using "
                                               << m_processInfos.value(evCharger).chargerPhaseLimitPower << "W on "
                                               << m_processInfos.value(evCharger).maxPossiblePhaseCount << " phase(s) | "
                                               << Electricity::convertPhasesToString(m_processInfos.value(evCharger).maxPossiblePhases);

            // Distribute the remaining time to the spot market data of this day. If we are currently charging from the spot market,
            // lock the current schedule so it will be finished and not postboned creating a jitter

            bool lockCurrentSchedule = (info.chargingState() == ChargingInfo::ChargingStateSpotMarketCharging && evCharger->charging());

            TimeFrames timeFrames = m_spotMarketManager->scheduleCharingTimeForToday(currentDateTime, remainingChargeMinutesToday, m_configuration->minimumScheduleDuration(), lockCurrentSchedule);
            ChargingSchedules schedules;
            foreach (const TimeFrame &timeFrame, timeFrames) {
                ChargingSchedule schedule(evCharger->id(), timeFrame);
                schedule.setAction(ChargingAction(true, evCharger->maxChargingCurrentMaxValue(), m_processInfos.value(evCharger).maxPossiblePhaseCount, ChargingAction::ChargingActionIssuerSpotMarketCharging));
                schedules.append(schedule);
            }

            qCDebug(dcNymeaEnergy()) << "Distributed spot market" << schedules;

            ChargingSchedules currentSchedules = m_chargingSchedules.value(evCharger).filterByIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
            if (currentSchedules != schedules) {
                schedulesChanged = true;
                m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
                m_chargingSchedules[evCharger].append(schedules);
            }
        }
    }

    if (schedulesChanged) {
        emit chargingSchedulesChanged();
    }

    // Derive any actions related to spot market charging for the current datatime
    foreach (EvCharger *evCharger, m_evChargers) {

        // If there are schedules for this EV charger (spotmarket or user created), charge now...

        if (m_chargingSchedules.contains(evCharger)) {
            ChargingSchedule currentSchedule = m_chargingSchedules.value(evCharger).getChargingSchedule(currentDateTime);
            if (currentSchedule.isValid() && currentSchedule.action().chargingEnabled() && currentSchedule.action().issuer() == ChargingAction::ChargingActionIssuerSpotMarketCharging) {
                m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging] = currentSchedule.action();
                m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging].setIssuer(ChargingAction::ChargingActionIssuerSpotMarketCharging);
                m_processInfos[evCharger].currentSchedule = currentSchedule;
                continue;
            }
        }
    }
}

void SmartChargingManager::planSurplusCharging(const QDateTime &currentDateTime)
{
    if (!m_rootMeter) {
        qCDebug(dcNymeaEnergy()) << "No root meter configured. Skipping surplus charging planing";
        return;
    }

    qCDebug(dcNymeaEnergy()) << "---------------- Plan surplus charging  ---------------";
    qCDebug(dcNymeaEnergy()) << "Current time" << currentDateTime.toString("dd.MM.yyyy hh:mm:ss");

    // 1. Find all the chargers that are set to eco mode, we'll leave them alone in normal mode
    EvChargers evChargers;
    foreach (EvCharger *evCharger, m_evChargers) {

        Thing *assignedCar = m_thingManager->findConfiguredThing(m_chargingInfos.value(evCharger->id()).assignedCarId());
        if (!assignedCar) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "has no car assigned. Ignoring...";
            if (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerTimeRequirement) > 0)
                emit chargingSchedulesChanged();

            continue;
        }

        const ChargingInfo info = chargingInfo(evCharger->id());
        if (info.chargingMode() == ChargingInfo::ChargingModeNormal) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "is set to manual mode. Ignoring...";
            if (m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerTimeRequirement) > 0)
                emit chargingSchedulesChanged();

            continue;
        }

        evChargers.append(evCharger);
    }
    qCDebug(dcNymeaEnergy()) << evChargers.count() << "EV chargers are set to ECO mode";

    // 2. Find the chargers that need to charge full speed (e.g. because user wants it fully charged soon)
    double addedPower = 0;
    QMutableListIterator<EvCharger *> it(evChargers);
    QHash<EvCharger*, Electricity::Phases> chargersNeedingFullSpeed;
    while (it.hasNext()) {
        EvCharger *evCharger = it.next();
        ChargingProcessInfo processInfo = m_processInfos.value(evCharger);

        qCDebug(dcNymeaEnergy()) << "**** Evaluating" << evCharger->name();

        ChargingInfo info = m_chargingInfos.value(evCharger->id());
        if (info.chargingMode() != ChargingInfo::ChargingModeEcoWithTargetTime
            && info.chargingMode() != ChargingInfo::ChargingModeEcoMinWithTargetTime) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "does not use a target time.";
            m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerTimeRequirement);
            continue;
        }

        QDateTime endDateTime = info.nextEndTime(currentDateTime);
        if (!endDateTime.isValid()) {
            qCDebug(dcNymeaEnergy()).nospace() << "No valid target time set for " << evCharger->name();
            ChargingInfo::ChargingMode fallbackMode;
            if (info.chargingMode() == ChargingInfo::ChargingModeEcoMinWithTargetTime) {
                qCDebug(dcNymeaEnergy()) << "Resetting to ECO with min current mode.";
                fallbackMode = ChargingInfo::ChargingModeEcoWithMinCurrent;
            } else {
                qCDebug(dcNymeaEnergy()) << "Resetting to ECO mode without time.";
                fallbackMode = ChargingInfo::ChargingModeEco;
            }
            m_chargingInfos[evCharger->id()].setChargingMode(fallbackMode);
            storeChargingInfo(m_chargingInfos.value(evCharger->id()));
            emit chargingInfoChanged(m_chargingInfos.value(evCharger->id()));
            continue;
        }

        if (m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket) {
            qCDebug(dcNymeaEnergy()) << "Have a target time available and spotmarket charging enabled. Skipping force check...";
            m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerTimeRequirement);
            continue;
        }

        QDateTime internalEndDateTime = endDateTime.addSecs(-10 * 60);
        ChargingAction action(true, evCharger->maxChargingCurrentMaxValue(), m_processInfos.value(evCharger).maxPossiblePhaseCount, ChargingAction::ChargingActionIssuerTimeRequirement);

        // We'll be trying to finish 10 min earlier (TODO: To be evaluated if really needed in practice)
        if (processInfo.remainingPercentageToCharge > 0 && processInfo.projectedEndTime > internalEndDateTime) {
            qCDebug(dcNymeaEnergy()) << "We need to charge" << evCharger->name() << "immediately to meet the target! Internal target time:"
                                     << internalEndDateTime.toString("dd.MM.yyyy hh:mm:ss") << "projected endtime:" << processInfo.projectedEndTime.toString("dd.MM.yyyy hh:mm:ss");
            qCDebug(dcNymeaEnergy()) << "Charging schedule active for" << evCharger->name() << m_processInfos.value(evCharger).currentSchedule;

            // Create the schedule for ui from now till the end
            ChargingSchedule schedule(evCharger->id());
            schedule.setEndDateTime(internalEndDateTime);
            schedule.setStartDateTime(currentDateTime);
            schedule.setAction(action);

            m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerTimeRequirement);
            m_chargingSchedules[evCharger].append(schedule);

            // Force charging now
            m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement] = action;

            // Wallbox has been handled. Remove from further logic
            it.remove();
        } else {
            qCDebug(dcNymeaEnergy()) << "No need to force charging on" << evCharger->name() << ", there's still enough time for optimization. Internal target time:"
                                     << internalEndDateTime.toString("dd.MM.yyyy hh:mm:ss") << "projected endtime:" << processInfo.projectedEndTime.toString("dd.MM.yyyy hh:mm:ss");;

            // Remove all time requiment based schedules, if needed, we re plan it here...
            m_chargingSchedules[evCharger].removeAllIssuer(ChargingAction::ChargingActionIssuerTimeRequirement);

            // Let's see how much we already scheduled so far using other planings like spotmarket, plan the rest as time requirement schedule
            int alreadyScheduledMinutes = 0;
            foreach (const ChargingSchedule &existingSchedule, m_chargingSchedules.value(evCharger)) {
                alreadyScheduledMinutes += existingSchedule.durationMinutes();
            }

            int remainingMinutesTotal = qCeil(m_processInfos[evCharger].remainingHoursToCharge * 60);
            int remainingMinutesToCharge = remainingMinutesTotal - alreadyScheduledMinutes;
            if (alreadyScheduledMinutes > 0) {
                qCDebug(dcNymeaEnergy()) << "Have already scheduled" << alreadyScheduledMinutes << "minutes from a total of"
                                         << remainingMinutesTotal << "minutes. Still need to schedule"
                                         << remainingMinutesToCharge << "minutes using time requirements.";
            }

            ChargingSchedule schedule(evCharger->id());
            schedule.setEndDateTime(internalEndDateTime);
            schedule.setStartDateTime(internalEndDateTime.addSecs(-remainingMinutesToCharge * 60));
            schedule.setAction(action);

            m_chargingSchedules[evCharger].append(schedule);
        }
    }

    // 3. distribute remaining allowance between remaining chargers

    // If negative, we've got some spare energy to use, if positive, we should lower charging rates or even stop
    double currentLoad = m_rootMeter->currentPower();

    // Adding some mechanism to prioritize house energy storage before the car, or steal
    double fromBatteries = 0;

    // Consider all batteries as one, to keep things as simple as possible
    double totalBatteryLevel = 0;
    double totalBatteryPower = 0;
    int storageCount = 0;

    int batteryLevelConsiderationPercentage = qRound(m_batteryLevelConsideration * 100);

    foreach (Thing *storage, m_thingManager->configuredThings().filterByInterface("energystorage")) {
        storageCount ++;

        double batteryLevel = storage->stateValue("batteryLevel").toDouble();
        double currentPower = storage->stateValue("currentPower").toDouble();

        totalBatteryLevel += batteryLevel; // Sum up percentage and create average later using storageCount
        totalBatteryPower += currentPower; // Create a total sum of all battery powers
    }

    totalBatteryLevel = qRound(totalBatteryLevel / storageCount);

    if (storageCount > 0) {
        qCDebug(dcNymeaEnergy()) << "Having" << storageCount << "batteries. Overall battery level is" << totalBatteryLevel << "% and overall battery power is" << totalBatteryPower << "W";

        // If we are currently charging in eco mode and the battery level is under the consideration level,
        // we should stop charging and prioritize the battery

        if (totalBatteryLevel < batteryLevelConsiderationPercentage) {
            // We are under the consideration level for the batteries, check if we have active chargers left, we should stop loading now
            it.toFront();
            while (it.hasNext()) {
                EvCharger *evCharger = it.next();
                const ChargingProcessInfo processInfo = m_processInfos.value(evCharger);

                if (evCharger->charging()) {
                    // Let's pause the charging process, if there will be surplus available, it will be considerated in the next iterations...
                    qCDebug(dcNymeaEnergy()) << "The battery is under the consideration level. Pausing the charging on" << evCharger->name();
                    m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging] = ChargingAction(false, evCharger->maxChargingCurrent(), evCharger->phaseCount(), ChargingAction::ChargingActionIssuerSurplusCharging);
                    // Remove this charger from further logic
                    it.remove();
                }
            }
        }


        if (totalBatteryPower < 0) {
            // If the battery is discharging, we'll have to add that to the load in order to not end up draining the battery
            fromBatteries += -totalBatteryPower;
        } else if (totalBatteryPower > 0) {
            // Battery is charging
            if (totalBatteryLevel >= batteryLevelConsiderationPercentage) {
                qCDebug(dcNymeaEnergy()) << "The battery has reached the consideration level" << batteryLevelConsiderationPercentage << "%. The battery power will used for surplus charging.";
                fromBatteries += -totalBatteryPower;
            } else {
                // If the battery is charging, we'll add 500W to the load to leave that for the battery to "eat" up so the battery has priority
                qCDebug(dcNymeaEnergy()) << "The battery has not reached yet the consideration level" << batteryLevelConsiderationPercentage << "%. Prioritize the energy storage and add 500W buffer).";
                fromBatteries += 500;
            }
        }
    } else {
        qCDebug(dcNymeaEnergy()) << "There is no energy storage available to consider.";
    }

    // Add the evaluated battery load to the current root meter load in order to have the actual available power
    currentLoad += fromBatteries;

    // In the previous step we may have added some load which doesn't reflect on the root meter yet but will likely be in the next metering cycle
    currentLoad += addedPower;

    qCDebug(dcNymeaEnergy()).nospace() << "Current load on root meter: " << currentLoad << "W ( " << (currentLoad / 230) << "[A] ) Phases: A: " << m_rootMeter->currentPhaseA() << "[A], B: " << m_rootMeter->currentPhaseB() << "[A], C: " << m_rootMeter->currentPhaseC() << "[A]. Added in previous step: " << addedPower << "[W] (" << (addedPower / 230) << "[A]). From batteries: " << fromBatteries << "[W] (" << (fromBatteries / 230) << "[A])";

    if (!evChargers.isEmpty()) {
        qCDebug(dcNymeaEnergy()) << evChargers.count() << "chargers in ECO mode and can be optimized.";

        it.toFront();
        double allowanceInWatt = -currentLoad;
        double allowanceInAmpere = allowanceInWatt / 230;
        while (it.hasNext()) {
            EvCharger *evCharger = it.next();
            const ChargingProcessInfo processInfo = m_processInfos.value(evCharger);

            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 &currentDateTime)
{
    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. Min current fallback (EcoWithMinCurrent / EcoMinWithTargetTime)
        {
            const ChargingInfo::ChargingMode mode = m_chargingInfos.value(evCharger->thing()->id()).chargingMode();
            if (mode == ChargingInfo::ChargingModeEcoWithMinCurrent
                || mode == ChargingInfo::ChargingModeEcoMinWithTargetTime) {
                uint minCurrent = qMax(EcoMinChargingCurrent, evCharger->maxChargingCurrentMinValue());
                qCDebug(dcNymeaEnergy()).nospace() << "No surplus available for " << evCharger->name()
                    << " — applying min current " << minCurrent << " A on " << processInfo.effectivePhaseCount << " phase(s).";
                ChargingAction minAction(true, minCurrent, processInfo.effectivePhaseCount,
                                        ChargingAction::ChargingActionIssuerSurplusCharging);
                executeChargingAction(evCharger, minAction, currentDateTime);
                if (m_chargingInfos[evCharger->thing()->id()].chargingState() != ChargingInfo::ChargingStateSurplusCharging) {
                    m_chargingInfos[evCharger->thing()->id()].setChargingState(ChargingInfo::ChargingStateSurplusCharging);
                    emit chargingInfoChanged(m_chargingInfos[evCharger->thing()->id()]);
                }
                continue;
            }
        }

        // 5. Else ... idle, switch off
        qCDebug(dcNymeaEnergy()).nospace() << "Setting " << evCharger->name() << " to power OFF. Idle state.";
        evCharger->setChargingEnabled(false, currentDateTime);

        if (m_chargingInfos[evCharger->thing()->id()].chargingState() != ChargingInfo::ChargingStateIdle) {
            m_chargingInfos[evCharger->thing()->id()].setChargingState(ChargingInfo::ChargingStateIdle);
            emit chargingInfoChanged(m_chargingInfos[evCharger->thing()->id()]);
        }
    }
}

void SmartChargingManager::updateManualSoCsWithMeter(EnergyLogs::SampleRate sampleRate, const ThingPowerLogEntry &entry)
{
    Q_UNUSED(sampleRate)

    EvCharger *charger = m_evChargers.value(entry.thingId());
    if (!charger) {
        return;
    }
    Thing *car = m_thingManager->findConfiguredThing(m_chargingInfos.value(entry.thingId()).assignedCarId());
    if (!car || !car->thingClass().hasStateType("batteryLevel") || !car->thingClass().stateTypes().findByName("batteryLevel").writable()) {
        return;
    }

    double chargingPower = entry.currentPower();
    double addedkWh = chargingPower / 60000.0;

    addedkWh *= m_processInfos.value(charger).carChargingEnergyLossFactor;

    double capacity = car->stateValue("capacity").toDouble();
    // x : 100 = addedkWh : capacity
    double addedPercentage = addedkWh * 100 / capacity;

    double lastPreciseSoC = car->property("preciseSoC").toDouble();
    if (lastPreciseSoC == 0) {
        lastPreciseSoC = car->stateValue("batteryLevel").toDouble();
    }
    double newPreciseSoC = qMin(100.0, lastPreciseSoC + addedPercentage);
    double newEnergyCharged = car->property("totalEnergyCharged").toDouble() + addedkWh;
    qCDebug(dcNymeaEnergy()) << "Updating manual SoC (metered) for" << car->name() << chargingPower << "W"
                             << QTime::fromMSecsSinceStartOfDay(60000).toString() << addedPercentage << "% -> new soc" << newPreciseSoC
                             << "% total energy:" << newEnergyCharged << "kWh";
    car->setProperty("totalEnergyCharged", newEnergyCharged);
    car->setProperty("preciseSoC", newPreciseSoC);
    if (car->stateValue("batteryLevel").toInt() != qRound(newPreciseSoC)) {
        ActionType batteryLevelActionType = car->thingClass().actionTypes().findByName("batteryLevel");
        Action action(batteryLevelActionType.id(), car->id(), Action::TriggeredByRule);
        action.setParams({Param(batteryLevelActionType.id(), qRound(newPreciseSoC))});
        m_thingManager->executeAction(action);
    }
}

void SmartChargingManager::updateManualSoCsWithoutMeter(const QDateTime &currentDateTime)
{
    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;
}

QString SmartChargingManager::chargerPhaseKey(EvCharger *evCharger) const
{
    // Use live metering first — reliable when charger is actively charging
    Electricity::Phases metered = evCharger->meteredPhases();
    if (metered.testFlag(Electricity::PhaseA)) return QStringLiteral("A");
    if (metered.testFlag(Electricity::PhaseB)) return QStringLiteral("B");
    if (metered.testFlag(Electricity::PhaseC)) return QStringLiteral("C");

    // Charger may be off — fall back to last known phase stored by prepareInformation()
    if (m_processInfos.contains(evCharger)) {
        Electricity::Phases known = m_processInfos.value(evCharger).effectivePhases;
        if (known.testFlag(Electricity::PhaseA)) return QStringLiteral("A");
        if (known.testFlag(Electricity::PhaseB)) return QStringLiteral("B");
        if (known.testFlag(Electricity::PhaseC)) return QStringLiteral("C");
    }

    // Phase unknown — conservative fallback (unchanged previous behavior)
    qCWarning(dcNymeaEnergy()) << "Cannot determine connected phase for"
                               << evCharger->name() << "— defaulting to phase A";
    return QStringLiteral("A");
}

uint SmartChargingManager::getBestPhaseCount(EvCharger *evCharger, double surplusAmpere)
{
    uint desiredPhaseCount = 1;

    // Check if we already have enough surplus for minimal 3 phase charging without acquisition
    // In that case we just switch to 3 phase charging and skip the rest of the calculations
    if (surplusAmpere > (3.0 * m_processInfos.value(evCharger).minimalChargingCurrent)) {
        qCDebug(dcNymeaEnergy()) << "There is enough solar power for 3-phase charging without an acquisition";
        desiredPhaseCount = 3;
        return desiredPhaseCount;
    }

    // Check if we can do the job with 1 phase without acquisition
    if (surplusAmpere > m_processInfos.value(evCharger).minimalChargingCurrent &&
        surplusAmpere < evCharger->maxChargingCurrentMaxValue()) {
        qCDebug(dcNymeaEnergy()) << "There is enough solar power for 1-phase charging without an acquisition";
        desiredPhaseCount = 1;
        return desiredPhaseCount;
    }

    // Check if we can do the job with 1 phase + acquisition
    if (surplusAmpere > m_processInfos.value(evCharger).minimalChargingCurrent * m_acquisitionTolerance &&
        surplusAmpere < evCharger->maxChargingCurrentMaxValue()) {
        qCDebug(dcNymeaEnergy()) << "There is enough solar power for 1-phase charging with an acquisition of" << m_processInfos.value(evCharger).minimalChargingCurrent - surplusAmpere << "[A]";
        desiredPhaseCount = 1;
        return desiredPhaseCount;
    }

    // Check if we are between the 3 phase min and 1 phase max charging gap, if so, stay on single phase
    // in order to prevent a charging stop
    // 1 * 16A * 230V = 3680W
    // 3 * 6 * 230V = 4140W
    if (surplusAmpere < m_processInfos.value(evCharger).minimalChargingCurrent * m_acquisitionTolerance * 3 &&
        surplusAmpere > evCharger->maxChargingCurrentMaxValue() * m_acquisitionTolerance) {
        qCDebug(dcNymeaEnergy()) << "We are right between the 3-phase min current and 1-phase max current interval. Falling back to single phase charging in order to prevent a charing stop.";
        desiredPhaseCount = 1;
        return desiredPhaseCount;
    }

    //We ruled out any situation where single phase charging would make sense, let's use 3 phases here
    desiredPhaseCount = 3;
    return desiredPhaseCount;
}

void SmartChargingManager::executeChargingAction(EvCharger *evCharger, const ChargingAction &chargingAction, const QDateTime &currentDateTime)
{
    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());
}

