// 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 <integrations/thingutils.h>

#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}
{
    migrateSettings();

    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("ChargingConfigurations");
    foreach (const QString &evChargerIdString, settings.childGroups()) {
        ThingId evChargerId = ThingId(evChargerIdString);
        if (thingManager->findConfiguredThing(evChargerId)) {
            settings.beginGroup(evChargerIdString);

            ChargingConfiguration config(evChargerId);
            config.setAssignedCarId(settings.value("assignedCarId").toUuid());
            config.setChargingMode(static_cast<ChargingConfiguration::ChargingMode>(settings.value("chargingMode").toInt()));
            if (settings.contains("endDateTime"))
                config.setEndDateTime(settings.value("endDateTime").toDateTime());

            QList<int> days;
            foreach (const QVariant &day, settings.value("repeatDays").toList())
                days.append(day.toInt());

            config.setRepeatDays(days);
            config.setTargetPercentage(settings.value("targetPercentage").toUInt());
            config.setDailySpotMarketPercentage(settings.value("dailySpotMarketPercentage", 0).toUInt());
            config.setLockOnUnplug(settings.value("lockOnUnplug").toBool());
            config.setStopOnTargetPercentage(settings.value("stopOnTargetPercentage").toBool());

            config.setLocale(settings.value("locale").toLocale());
            config.setManualChargingEnabled(settings.value("manualChargingEnabled", false).toBool());
            config.setManualMaxChargingCurrent(settings.value("manualMaxChargingCurrent", 0).toUInt());
            config.setManualDesiredPhaseCount(settings.value("manualDesiredPhaseCount", 3).toUInt());

            m_chargingConfigurations.insert(evChargerId, config);
            settings.endGroup(); // evChargerIdString
        } else {
            qCWarning(dcNymeaEnergy()) << "EV charger with ID" << evChargerId << "not found in system. Not loading configuration.";
            settings.remove(evChargerIdString);
        }
    }
    settings.endGroup(); // ChargingConfigurations

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

        setupEvCharger(thing);
        setupPluggedInHandlers(thing);
    }

    foreach (Thing *thing, m_thingManager->configuredThings().filterByInterface("electricvehicle")) {
        setupCarBatteryHandlers(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
}

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

ChargingStates SmartChargingManager::chargingStates() const
{
    return m_chargingStates.values();
}

ChargingState SmartChargingManager::chargingState(const ThingId &evChargerId) const
{
    return m_chargingStates.value(evChargerId);
}

ChargingConfigurations SmartChargingManager::chargingConfigurations() const
{
    return m_chargingConfigurations.values();
}

ChargingConfiguration SmartChargingManager::chargingConfiguration(const ThingId &evChargerId) const
{
    return m_chargingConfigurations.value(evChargerId);
}

EnergyManager::EnergyError SmartChargingManager::setChargingConfiguration(const ChargingConfiguration &chargingConfiguration)
{
    qCDebug(dcNymeaEnergy()) << "Setting charging configuration:" << chargingConfiguration;
    Thing *evCharger = m_thingManager->findConfiguredThing(chargingConfiguration.evChargerId());
    if (!evCharger || !evCharger->thingClass().interfaces().contains("evcharger")) {
        qCWarning(dcNymeaEnergy()) << "No such EV charger:" << chargingConfiguration.evChargerId();
        return EnergyManager::EnergyErrorInvalidParameter;
    }

    if (chargingConfiguration.targetPercentage() > 100) {
        qCWarning(dcNymeaEnergy()) << "Charging configuration target percentage out of range:" << chargingConfiguration.targetPercentage();
        return EnergyManager::EnergyErrorInvalidParameter;
    }

    foreach (int day, chargingConfiguration.repeatDays()) {
        if (day < 1 || day > 7) {
            qCWarning(dcNymeaEnergy()) << "Charging configuration repeat days invalid. All days must be within 1 (Mon) and 7 (Sun):" << chargingConfiguration.repeatDays();
            return EnergyManager::EnergyErrorInvalidParameter;
        }
    }

    if (chargingConfiguration.dailySpotMarketPercentage() > 100) {
        qCWarning(dcNymeaEnergy()) << "Charging configuration daily spot market percentage value out of range:" << chargingConfiguration.dailySpotMarketPercentage() << "is not in the range [0 - 100] %";
        return EnergyManager::EnergyErrorInvalidParameter;
    }


    const ChargingConfiguration currentConfiguration = m_chargingConfigurations.value(chargingConfiguration.evChargerId());

    ChargingConfiguration effectiveConfiguration = chargingConfiguration;
    // Manual restore settings are internal-only and not part of public API payloads.
    // Keep the existing values when clients update regular charging configuration fields.
    effectiveConfiguration.setManualChargingEnabled(currentConfiguration.manualChargingEnabled());
    effectiveConfiguration.setManualMaxChargingCurrent(currentConfiguration.manualMaxChargingCurrent());
    effectiveConfiguration.setManualDesiredPhaseCount(currentConfiguration.manualDesiredPhaseCount());

    if (currentConfiguration != effectiveConfiguration) {
        // Make sure the assigned car exists
        Thing *assignedCar = m_thingManager->findConfiguredThing(effectiveConfiguration.assignedCarId());
        if (!effectiveConfiguration.assignedCarId().isNull() && !assignedCar) {
            qCWarning(dcNymeaEnergy()) << "The given assigned car id cannot be found in the system.";
            return EnergyManager::EnergyErrorInvalidParameter;
        }

        if (currentConfiguration.chargingMode() != effectiveConfiguration.chargingMode()) {
            onChargingModeChanged(evCharger->id(), effectiveConfiguration);
        }

        m_chargingConfigurations[effectiveConfiguration.evChargerId()] = effectiveConfiguration;
        qCInfo(dcNymeaEnergy()) << "Charging configuration for" << evCharger->name() << "set to" << effectiveConfiguration;
        emit chargingConfigurationChanged(effectiveConfiguration);

        storeChargingConfiguration(effectiveConfiguration);

#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_chargingStates.value(evCharger->id()).overloadProtectionActive()) {
                qCDebug(dcNymeaEnergy()) << "Overload protection: Activated for charger in normal mode" << evCharger->name();
                m_chargingStates[evCharger->id()].setOverloadProtectionActive(true);
                emit chargingStateChanged(m_chargingStates.value(evCharger->id()));
            }

            ChargingAction action;

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

                // FIXME: get the actual phase, not assume it is phase A in single phase charging
                if (requiredThrottlePower.value("A") > 0) {
                    int throttleAmpere = qCeil(requiredThrottlePower.value("A") / 230);
                    int desiredFallbackAmpere = evCharger->maxChargingCurrent() - throttleAmpere;

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

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

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

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

            executeChargingAction(evCharger, action, currentDateTime);
        }
    } else {
        // No limit exceeded, lets make sure eco mode chargers leave the overload protection active state
        foreach (const ChargingState &chargingState, m_chargingStates) {
            if (chargingState.overloadProtectionActive() && m_chargingConfigurations.value(chargingState.evChargerId()).chargingMode() != ChargingConfiguration::ChargingModeNormal) {
                m_chargingStates[chargingState.evChargerId()].setOverloadProtectionActive(false);
                emit chargingStateChanged(m_chargingStates.value(chargingState.evChargerId()));
            }
        }
    }
}

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...

    double consumptionLimitInWatt = 230 * m_phasePowerConsumptionLimit;

    foreach (EvCharger *evCharger, m_evChargers) {

        const ChargingConfiguration config = m_chargingConfigurations.value(evCharger->id());

        if (config.chargingMode() == ChargingConfiguration::ChargingModeNormal && m_chargingStates.value(evCharger->id()).overloadProtectionActive()) {
            // 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"
                                     << config.manualChargingEnabled() << config.manualMaxChargingCurrent() << "[A]";

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

            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 = (config.manualMaxChargingCurrent() - evCharger->maxChargingCurrent()) * 230;

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

                bool restoreCharger = false;

                if (evCharger->phaseCount() == 1) {
                    // FIXME: get the actual phase, not assume it is phase A in single phase charging
                    if (availablePhasePower.value("A") >= requiredRestorePhasePower) {
                        qCDebug(dcNymeaEnergy()) << "Overload protection: Enought power available to restore the original configuration"
                                                 << config.manualMaxChargingCurrent() << "[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) {
                    double maxChargingCurrent = ThingUtils::ensureValueClamping(QVariant(config.manualMaxChargingCurrent()),
                                                                                QMetaType::Double,
                                                                                m_processInfos.value(evCharger).minimalChargingCurrent,
                                                                                m_processInfos.value(evCharger).maximalChargingCurrent,
                                                                                evCharger->maxChargingCurrentStepSize()).toDouble();
                    qCDebug(dcNymeaEnergy()) << "Overload protection: Restoring charger to" << maxChargingCurrent << "[A]";
                    ChargingAction action = ChargingAction(true, maxChargingCurrent , evCharger->phaseCount(), ChargingAction::ChargingActionIssuerOverloadProtection);
                    executeChargingAction(evCharger, action, currentDateTime);
                    m_chargingStates[evCharger->id()].setOverloadProtectionActive(false);
                    emit chargingStateChanged(m_chargingStates.value(evCharger->id()));
                }

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

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

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

                if (evCharger->phaseCount() == 1) {
                    // FIXME: get the actual phase, not assume it is phase A in single phase charging
                    if (availablePhasePower.value("A") >= requiredRestorePhasePower) {
                        qCDebug(dcNymeaEnergy()) << "Overload protection: Enought power available to start charging using the minimal charging current of"
                                                 << m_processInfos.value(evCharger).minimalChargingCurrent << "[A]";
                        restoreChargerPower = true;
                    }
                } else {

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

                    if (enoughtOnAllPhases) {
                        restoreChargerPower = true;
                    }
                }

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

void SmartChargingManager::evaluateTargetPercentageReached(const ThingId &evChargerId, double batteryLevel, const QDateTime &currentDateTime)
{
    const ChargingConfiguration config = m_chargingConfigurations.value(evChargerId);
    if (!config.stopOnTargetPercentage() || config.chargingMode() != ChargingConfiguration::ChargingModeEcoWithTargetTime) {
        // Make sure we have the flag disabled
        if (m_chargingStates.value(evChargerId).targetPercentageReached()) {
            m_chargingStates[evChargerId].setTargetPercentageReached(false);
            emit chargingStateChanged(m_chargingStates.value(evChargerId));
            qCDebug(dcNymeaEnergy()) << "Disabled target percentage reached:" << m_chargingStates.value(evChargerId);
        }

        return;
    }

    // Feature enabled, check if we reached the target percentage.
    // If so, disable charging and
    EvCharger *evCharger = m_evChargers.value(evChargerId);
    if (batteryLevel >= config.targetPercentage()) {
        qCDebug(dcNymeaEnergy()) << "Target percentage" << config.targetPercentage() << "%" << "reached for" << evCharger->name();
        if (evCharger->charging()) {
            // Stop charging, we are done here
            ChargingAction action = ChargingAction(false, evCharger->maxChargingCurrent(), evCharger->phaseCount(), ChargingAction::ChargingActionIssuerTimeRequirement, true);
            executeChargingAction(evCharger, action, currentDateTime);
        }

        bool schedulesChanged = m_chargingSchedules.contains(evCharger) && !m_chargingSchedules.value(evCharger).isEmpty();
        if (schedulesChanged) {
            m_chargingSchedules[evCharger].clear();
        }
        m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
        m_processInfos[evCharger].currentSchedule.clear();
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging].setChargingEnabled(false);
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSpotMarketCharging].setChargingEnabled(false);
        m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement].setChargingEnabled(false);

        bool stateChanged = false;
        if (!m_chargingStates.value(evChargerId).targetPercentageReached()) {
            m_chargingStates[evChargerId].setTargetPercentageReached(true);
            stateChanged = true;
        }
        if (m_chargingStates.value(evCharger->id()).mode() != ChargingState::ModeIdle) {
            m_chargingStates[evCharger->id()].setMode(ChargingState::ModeIdle);
            stateChanged = true;
        }

        if (stateChanged) {
            emit chargingStateChanged(m_chargingStates.value(evCharger->id()));
            qCDebug(dcNymeaEnergy()) << m_chargingStates.value(evChargerId);
        }
        if (schedulesChanged)
            emit chargingSchedulesChanged();
        return;
    }

    qCDebug(dcNymeaEnergy()) << "Target percentage" << config.targetPercentage() << "%" << "not reached yet for" << evCharger->name();
    if (m_chargingStates.value(evChargerId).targetPercentageReached()) {
        m_chargingStates[evChargerId].setTargetPercentageReached(false);
        emit chargingStateChanged(m_chargingStates.value(evChargerId));
    }
}

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_chargingConfigurations.value(evCharger->thing()->id()).assignedCarId());
        if (!car) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "has no car assigned. Ignoring...";
            continue;
        }

        QDateTime endDateTime = m_chargingConfigurations.value(evCharger->id()).nextEndTime(currentDateTime);
        if (!endDateTime.isValid() && m_chargingConfigurations.value(evCharger->id()).chargingMode() != ChargingConfiguration::ChargingModeNormal) {
            qCDebug(dcNymeaEnergy()).nospace() << "No valid target time set for " << evCharger->name();
            if (m_chargingConfigurations.value(evCharger->id()).chargingMode() == ChargingConfiguration::ChargingModeEcoWithTargetTime) {
                qCDebug(dcNymeaEnergy()) << "Resetting to ECO mode without time.";
            }
            m_chargingConfigurations[evCharger->id()].setChargingMode(ChargingConfiguration::ChargingModeEco);
            storeChargingConfiguration(m_chargingConfigurations.value(evCharger->id()));
            emit chargingConfigurationChanged(m_chargingConfigurations.value(evCharger->id()));
        }

        // Note: even if one charger is not connected, we still want to do some planing for it,
        // this could have an impact on other sides, altough we should prevent action execution

        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_chargingConfigurations.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);

        uint carMaxChargingCurrent = 80;
        if (assignedCar->hasState("maxChargingCurrent"))
            carMaxChargingCurrent = assignedCar->stateValue("maxChargingCurrent").toUInt();

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

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

        m_processInfos[evCharger].singlePhaseCarLimitation = evCharger->canSetPhaseCount() && evCharger->thing()->state("desiredPhaseCount").possibleValues().contains(1)
                                                             && m_processInfos.value(evCharger).carPhaseCount == 1;

        // 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 = m_processInfos.value(evCharger).maximalChargingCurrent * 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] |" << m_processInfos.value(evCharger).maximalChargingCurrent << "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_chargingConfigurations.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_chargingConfigurations.value(evCharger->thing()->id()).dailySpotMarketPercentage();
        m_processInfos[evCharger].chargeFixedAmountSpotmarketToday = (m_chargingConfigurations.value(evCharger->thing()->id()).dailySpotMarketPercentage() > 0);
        m_processInfos[evCharger].spotMaketEnergyRequiredToday = assignedCar->stateValue("capacity").toDouble() * m_chargingConfigurations.value(evCharger->thing()->id()).dailySpotMarketPercentage() / 100.0;
        qCDebug(dcNymeaEnergy()).nospace() << "A total of " << m_processInfos.value(evCharger).spotMaketEnergyRequiredToday << " kWh (" << m_chargingConfigurations.value(evCharger->thing()->id()).dailySpotMarketPercentage() << "%) shall be charged from spotmarket today";
        qCDebug(dcNymeaEnergy()).nospace() << "The car is currently " << (evCharger->pluggedIn() ? "plugged in" : "unplugged");
    }

    // 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_chargingConfigurations.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_chargingStates.value(evCharger->id()).targetPercentageReached()) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "has reached the target percentage and is done. Ignoring...";
            schedulesChanged |= !m_chargingSchedules.value(evCharger).isEmpty();
            m_chargingSchedules[evCharger].clear();
            m_processInfos[evCharger].chargingUntilTargetTimeUsingSpotmarket = false;
            m_processInfos[evCharger].currentSchedule.clear();
            continue;
        }

        if (m_chargingConfigurations.value(evCharger->id()).chargingMode() == ChargingConfiguration::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_chargingConfigurations.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_chargingConfigurations.value(evCharger->id()).chargingMode() == ChargingConfiguration::ChargingModeEco && m_chargingConfigurations.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 ChargingConfiguration config = m_chargingConfigurations.value(evCharger->id());

        if (config.chargingMode() == ChargingConfiguration::ChargingModeEcoWithTargetTime) {

            QDateTime endDateTime = config.nextEndTime(currentDateTime);
            if (!endDateTime.isValid()) {
                qCDebug(dcNymeaEnergy()).nospace() << "No valid target time set for " << evCharger->name();
                if (config.chargingMode() == ChargingConfiguration::ChargingModeEcoWithTargetTime) {
                    qCDebug(dcNymeaEnergy()) << "Resetting to ECO mode without time.";
                }
                m_chargingConfigurations[evCharger->id()].setChargingMode(ChargingConfiguration::ChargingModeEco);
                storeChargingConfiguration(m_chargingConfigurations.value(evCharger->id()));
                emit chargingConfigurationChanged(m_chargingConfigurations.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 = (m_chargingStates.value(evCharger->id()).mode() == ChargingState::ModeSpotMarketCharging && 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, m_processInfos.value(evCharger).maximalChargingCurrent, 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_chargingConfigurations.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(config.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

            // FIXME
            bool lockCurrentSchedule = (m_chargingStates.value(evCharger->id()).mode() == ChargingState::ModeSpotMarketCharging && 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, m_processInfos.value(evCharger).maximalChargingCurrent, 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_chargingConfigurations.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;
        }

        if (m_chargingStates.value(evCharger->id()).targetPercentageReached()) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "has reached the target percentage and is done. Ignoring...";
            m_processInfos[evCharger].currentSchedule.clear();
            if (!m_chargingSchedules[evCharger].isEmpty()) {
                m_chargingSchedules[evCharger].clear();
                emit chargingSchedulesChanged();
            }
            continue;
        }

        const ChargingConfiguration config = chargingConfiguration(evCharger->id());
        if (config.chargingMode() == ChargingConfiguration::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();

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

        QDateTime endDateTime = config.nextEndTime(currentDateTime);
        if (!endDateTime.isValid()) {
            qCDebug(dcNymeaEnergy()).nospace() << "No valid target time set for " << evCharger->name();
            if (config.chargingMode() == ChargingConfiguration::ChargingModeEcoWithTargetTime) {
                qCDebug(dcNymeaEnergy()) << "Resetting to ECO mode without time.";
            }
            m_chargingConfigurations[evCharger->id()].setChargingMode(ChargingConfiguration::ChargingModeEco);
            storeChargingConfiguration(m_chargingConfigurations.value(evCharger->id()));
            emit chargingConfigurationChanged(m_chargingConfigurations.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, m_processInfos.value(evCharger).maximalChargingCurrent, 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");

            int targetTimeOvershot = internalEndDateTime.secsTo(processInfo.projectedEndTime);

            // Make sure we communicate the clients that the target is reachable
            if (m_chargingStates.value(evCharger->id()).targetTimeOvershot() != targetTimeOvershot) {
                m_chargingStates[evCharger->id()].setTargetTimeOvershot(targetTimeOvershot);
                emit chargingStateChanged(m_chargingStates.value(evCharger->id()));
            }

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

            // Make sure we communicate the clients that the target is reachable
            if (m_chargingStates.value(evCharger->id()).targetTimeOvershot() != 0) {
                m_chargingStates[evCharger->id()].setTargetTimeOvershot(0);
                emit chargingStateChanged(m_chargingStates.value(evCharger->id()));
            }

            // 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 the battery is below the consideration level, keep all ECO chargers idle.
        // Otherwise idle chargers could restart on the next surplus evaluation and flip-flop.

        if (totalBatteryLevel < batteryLevelConsiderationPercentage) {
            // We are under the consideration level for the batteries. Keep all chargers out of the
            // remaining surplus logic so the battery gets priority until the threshold is reached.
            it.toFront();
            while (it.hasNext()) {
                EvCharger *evCharger = it.next();
                qCDebug(dcNymeaEnergy()) << "The battery is under the consideration level. Preventing surplus charging on" << evCharger->name();
                m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging] = ChargingAction(false, evCharger->maxChargingCurrent(), evCharger->phaseCount(), ChargingAction::ChargingActionIssuerSurplusCharging);
                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);

            double clampedValue = ThingUtils::ensureValueClamping(QVariant(newValue), QMetaType::Double, m_processInfos.value(evCharger).minimalChargingCurrent, m_processInfos.value(evCharger).maximalChargingCurrent, evCharger->maxChargingCurrentStepSize()).toDouble();

            //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() << " - " << m_processInfos.value(evCharger).maximalChargingCurrent << " 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 >= m_processInfos.value(evCharger).maximalChargingCurrent)
            //     newValueInt = m_processInfos.value(evCharger).maximalChargingCurrent;

            qCDebug(dcNymeaEnergy()).nospace() << "# Effective: " << clampedValue << "A (" << (clampedValue * 230) << "W) * " << desiredPhaseCount << " Phases. " << (chargingEnabled ? "ON" : "OFF");

            m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerSurplusCharging] = ChargingAction(chargingEnabled, clampedValue, 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()) {

        qCDebug(dcNymeaEnergy()) << evCharger->name() << m_chargingStates.value(evCharger->id());

        if (m_chargingStates.value(evCharger->id()).targetPercentageReached()) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "has reached the target percentage and is done. Ignoring...";
            continue;
        }

        if (m_chargingConfigurations.value(evCharger->id()).chargingMode() == ChargingConfiguration::ChargingModeNormal) {
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "is set to manual mode. Ignoring...";
            continue;
        }

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

        ChargingProcessInfo processInfo = m_processInfos.value(evCharger);

        // Final schedules
        qCDebug(dcNymeaEnergy()) << "Final charging schedules" << evCharger->thing();
        qCDebug(dcNymeaEnergy()) << m_chargingSchedules.value(evCharger);

        // 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(static_cast<double>(m_processInfos.value(evCharger).maximalChargingCurrent), allowancePerPhase);
            qCDebug(dcNymeaEnergy()) << evCharger->name() << "current upper limit:" << m_processInfos.value(evCharger).maximalChargingCurrent << "A - Per phase upper limit:" << allowancePerPhase << "A" << "Using:" << maxChargingCurrent << "A";
            maxChargingCurrent = qMax(static_cast<double>(evCharger->maxChargingCurrentMinValue()), maxChargingCurrent);
            maxChargingCurrent = ThingUtils::ensureValueClamping(QVariant(maxChargingCurrent),
                                                                  QMetaType::Double,
                                                                  m_processInfos.value(evCharger).minimalChargingCurrent,
                                                                  m_processInfos.value(evCharger).maximalChargingCurrent,
                                                                  evCharger->maxChargingCurrentStepSize()).toDouble();

            m_chargingActions[evCharger][ChargingAction::ChargingActionIssuerTimeRequirement].setMaxChargingCurrent(maxChargingCurrent);

            executeChargingAction(evCharger, m_chargingActions.value(evCharger).value(ChargingAction::ChargingActionIssuerTimeRequirement), currentDateTime);

            if (m_chargingStates.value(evCharger->id()).mode() != ChargingState::ModeTimeRequirement) {
                m_chargingStates[evCharger->id()].setMode(ChargingState::ModeTimeRequirement);
                emit chargingStateChanged(m_chargingStates.value(evCharger->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_chargingStates.value(evCharger->id()).mode() != ChargingState::ModeSurplusCharging) {
                m_chargingStates[evCharger->id()].setMode(ChargingState::ModeSurplusCharging);
                emit chargingStateChanged(m_chargingStates.value(evCharger->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_chargingStates.value(evCharger->id()).mode() != ChargingState::ModeSpotMarketCharging) {
                m_chargingStates[evCharger->id()].setMode(ChargingState::ModeSpotMarketCharging);
                emit chargingStateChanged(m_chargingStates.value(evCharger->id()));
            }

            continue;
        }

        // 4. Else ... idle, switch off
        qCDebug(dcNymeaEnergy()).nospace() << "Setting " << evCharger->name() << " to power OFF. Idle state.";
        if (m_processInfos.value(evCharger).singlePhaseCarLimitation) {
            evCharger->setDesiredPhaseCount(1);
            m_processInfos[evCharger].effectivePhaseCount = 1;
            m_processInfos[evCharger].effectivePhases = Electricity::PhaseA;
        }
        evCharger->setChargingEnabled(false, currentDateTime);

        if (m_chargingStates.value(evCharger->id()).mode() != ChargingState::ModeIdle) {
            m_chargingStates[evCharger->id()].setMode(ChargingState::ModeIdle);
            emit chargingStateChanged(m_chargingStates.value(evCharger->id()));
        }
    }
}

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

    EvCharger *charger = m_evChargers.value(entry.thingId());
    if (!charger || !charger->available()) {
        return;
    }
    Thing *car = m_thingManager->findConfiguredThing(m_chargingConfigurations.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);
    }

    evaluateTargetPercentageReached(charger->id(), newPreciseSoC, entry.timestamp());
}

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() || !charger->available())
            continue;

        ChargingConfiguration chargingConfiguration = m_chargingConfigurations.value(charger->id());
        Thing *car = m_thingManager->findConfiguredThing(chargingConfiguration.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);
            // Even while idle we need to re-evaluate this flag, otherwise a previous
            // stop-on-target event can keep the charger permanently ignored.
            evaluateTargetPercentageReached(charger->id(), car->stateValue("batteryLevel").toDouble(), 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);

        int roundedBatteryLevel = qRound(newPreciseSoC);
        if (car->stateValue("batteryLevel").toInt() != roundedBatteryLevel) {
            ActionType batteryLevelActionType = car->thingClass().actionTypes().findByName("batteryLevel");
            Action action(batteryLevelActionType.id(), car->id(), Action::TriggeredByRule);
            action.setParams({Param(batteryLevelActionType.id(), roundedBatteryLevel)});
            m_thingManager->executeAction(action);
        }

        evaluateTargetPercentageReached(charger->id(), newPreciseSoC, currentDateTime);
    }
}

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

    if (thing->thingClass().interfaces().contains("electricvehicle")) {
        setupCarBatteryHandlers(thing);
    }
}

void SmartChargingManager::onThingRemoved(const ThingId &thingId)
{
    if (m_chargingConfigurations.contains(thingId)) {
        EvCharger *evCharger = m_evChargers.value(thingId);
        bool schedulesChanged = false;
        if (evCharger) {
            schedulesChanged = !m_chargingSchedules.take(evCharger).isEmpty();
            m_processInfos.remove(evCharger);
            m_chargingActions.remove(evCharger);
        }

        m_chargingConfigurations.remove(thingId);
        m_chargingStates.remove(thingId);

        emit chargingConfigurationRemoved(thingId);
        emit chargingStateRemoved(thingId);
        if (schedulesChanged)
            emit chargingSchedulesChanged();

        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.

        const ChargingConfiguration config = chargingConfiguration(thing->id());

        if (config.chargingMode() == ChargingConfiguration::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();

                // The power action only updates the enabled flag. Read the latest persisted
                // manual current/phase values to avoid overwriting them with stale snapshots.
                EnergySettings settings;
                settings.beginGroup("ChargingConfigurations");
                settings.beginGroup(thing->id().toString());
                double manualMaxChargingCurrent = settings.value("manualMaxChargingCurrent", config.manualMaxChargingCurrent()).toDouble();
                uint manualDesiredPhaseCount = settings.value("manualDesiredPhaseCount", config.manualDesiredPhaseCount()).toUInt();
                settings.endGroup();
                settings.endGroup();

                storeManualChargingParameters(thing->id(), manualChargingEnabled, manualMaxChargingCurrent, manualDesiredPhaseCount);
                update(QDateTime::currentDateTime());
                return;
            }
            if (action.actionTypeId() == maxChargingActionType.id()) {
                double manualChargingCurrent = action.paramValue(maxChargingActionType.paramTypes().findByName("maxChargingCurrent").id()).toDouble();
                qCDebug(dcNymeaEnergy()) << "Manual charging current set to" << manualChargingCurrent << "[A] for" << thing->name();
                storeManualChargingParameters(thing->id(), config.manualChargingEnabled(), manualChargingCurrent, config.manualDesiredPhaseCount());
                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(), config.manualChargingEnabled(), config.manualMaxChargingCurrent(), 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())
            return; // 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...


        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 ChargingConfiguration &chargingConfiguration)
{
    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() << chargingConfiguration.chargingMode();

    // Restore user desired ev charging settings if switching back to normal mode
    if (chargingConfiguration.chargingMode() == ChargingConfiguration::ChargingModeNormal) {
        bool enabled = chargingConfiguration.manualChargingEnabled();
        double maxCurrent = chargingConfiguration.manualMaxChargingCurrent();
        uint desiredPhaseCount = chargingConfiguration.manualDesiredPhaseCount();
        qCDebug(dcNymeaEnergy()) << "EV charger" << evCharger->name() << "changed to normal mode. Restoring manual values of" << enabled << maxCurrent << "A" << desiredPhaseCount << "phases";

        // Restore the user's saved phase count when the charger can write it, including
        // single-phase-car setups that were temporarily forced down while in smart modes.
        if (m_processInfos.value(evCharger).canSwitchPhaseCount || m_processInfos.value(evCharger).singlePhaseCarLimitation)
            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();

    if (!m_chargingConfigurations.contains(thing->id())) {
        ChargingConfiguration chargingConfiguration(thing->id());
        m_chargingConfigurations.insert(thing->id(), chargingConfiguration);
        emit chargingConfigurationAdded(chargingConfiguration);
    }

    if (!m_chargingStates.contains(thing->id())) {
        ChargingState chargingState(thing->id());
        chargingState.setPowerLockDuration(m_configuration->chargingEnabledLockDuration());
        m_chargingStates.insert(thing->id(), chargingState);
        emit chargingStateAdded(chargingState);
    }

    EvCharger *evCharger = m_evChargers.value(thing->id());

    connect(evCharger, &EvCharger::remainingChargingEnabledLockDurationChanged, this, [this, evCharger](int seconds){
        // Note: we don't want to emit the changed signal. This would cause a lot of traffic which is not required.
        // This makes sure if a client gets the current states, it will contain the current lock seconds, once the
        // lock gets inactive/active there will be be a dedicated changed signal telling all clients
        qCDebug(dcNymeaEnergy()) << "Lock seconds changed" << seconds;

        // We notify only if the counter gets resetted (jump back, not counting down)
        bool notify = false;
        if (seconds > m_chargingStates.value(evCharger->id()).powerLockSeconds() && m_chargingStates.value(evCharger->id()).powerLockSeconds() > 0)
            notify = true;

        m_chargingStates[evCharger->id()].setPowerLockSeconds(seconds);

        if (notify)
            emit chargingStateChanged(m_chargingStates.value(evCharger->id()));

    });

    connect(evCharger, &EvCharger::chargingEnabledLockedChanged, this, [this, evCharger](bool chargingEnabledLocked){
        Q_UNUSED(chargingEnabledLocked)
        int remainingLockDuration = evCharger->remainingChargingEnabledLockDuration();
        m_chargingStates[evCharger->id()].setPowerLockSeconds(remainingLockDuration);
        emit chargingStateChanged(m_chargingStates.value(evCharger->id()));
    });
}

void SmartChargingManager::setupCarBatteryHandlers(const Thing *thing)
{
    Q_ASSERT_X(thing->thingClass().interfaces().contains("electricvehicle"), "SmartChargingManager", "setupCarBatteryHandlers called for a thing which isn't an electricvehicle.");

    if (!thing->thingClass().hasStateType("batteryLevel")) {
        return;
    }

    StateType batteryLevelStateType = thing->thingClass().stateTypes().findByName("batteryLevel");
    if (!batteryLevelStateType.isValid()) {
        return;
    }

    connect(thing, &Thing::stateValueChanged, this, [this, thing, batteryLevelStateType](const StateTypeId &stateTypeId, const QVariant &value){
        if (stateTypeId != batteryLevelStateType.id()) {
            return;
        }

        const QDateTime currentDateTime = QDateTime::currentDateTime();
        const double batteryLevel = value.toDouble();

        foreach (const ChargingConfiguration &config, m_chargingConfigurations) {
            if (config.assignedCarId() != thing->id()) {
                continue;
            }

            evaluateTargetPercentageReached(config.evChargerId(), batteryLevel, currentDateTime);
        }
    });
}

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") {

            EvCharger *evCharger = m_evChargers.value(thing->id());
            // We reset in any case the locks, a plug/unplug action should always reset any locks on the car side.
            evCharger->resetLocks();

            if (value.toBool()) {
                qCDebug(dcNymeaEnergy()) << "The car has been plugged in!";


                if (m_chargingConfigurations.value(thing->id()).chargingMode() == ChargingConfiguration::ChargingModeNormal) {
                    qCDebug(dcNymeaEnergy()) << "Charger is set to normal charging mode. No state of charge updating required.";
                    return;
                }

                Thing *car = m_thingManager->findConfiguredThing(m_chargingConfigurations.value(thing->id()).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.");

                        QTranslator translator;
                        bool status = translator.load(m_chargingConfigurations.value(thing->id()).locale(), "nymea-energy-plugin-nymea", "-", NymeaSettings::translationsPath(), ".qm");
                        if (!status) {
                            qCWarning(dcNymeaEnergy()) << "Error loading translations for notification from:"
                                                       << NymeaSettings::translationsPath() + "/nymea-energy-plugin-nymea" + "-[" + m_chargingConfigurations.value(thing->id()).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 (m_chargingConfigurations.value(thing->id()).lockOnUnplug()) {
                    qCDebug(dcNymeaEnergy()) << "Lock on unplug is set. Reverting to manual mode and disabling charging";
                    evCharger->setChargingEnabled(false, QDateTime::currentDateTime(), true);
                    m_chargingConfigurations[evCharger->id()].setChargingMode(ChargingConfiguration::ChargingModeNormal);
                    storeChargingConfiguration(m_chargingConfigurations.value(evCharger->id()));
                    emit chargingConfigurationChanged(m_chargingConfigurations.value(evCharger->id()));
                }
            }
        }
    });
}

void SmartChargingManager::storeChargingConfiguration(const ChargingConfiguration &chargingConfiguration)
{
    EnergySettings settings;
    settings.beginGroup("ChargingConfigurations");
    settings.beginGroup(chargingConfiguration.evChargerId().toString());
    settings.setValue("assignedCarId", chargingConfiguration.assignedCarId());
    settings.setValue("chargingMode", chargingConfiguration.chargingMode());
    settings.setValue("endDateTime", chargingConfiguration.endDateTime());
    QVariantList days;
    foreach (int day, chargingConfiguration.repeatDays())
        days.append(day);

    settings.setValue("repeatDays", days);
    settings.setValue("targetPercentage", chargingConfiguration.targetPercentage());
    settings.setValue("dailySpotMarketPercentage", chargingConfiguration.dailySpotMarketPercentage());
    settings.setValue("lockOnUnplug", chargingConfiguration.lockOnUnplug());
    settings.setValue("stopOnTargetPercentage", chargingConfiguration.stopOnTargetPercentage());
    settings.setValue("locale", chargingConfiguration.locale());
    settings.setValue("manualChargingEnabled", chargingConfiguration.manualChargingEnabled());
    settings.setValue("manualMaxChargingCurrent", chargingConfiguration.manualMaxChargingCurrent());
    settings.setValue("manualDesiredPhaseCount", chargingConfiguration.manualDesiredPhaseCount());
    settings.endGroup();
    settings.endGroup();
}

void SmartChargingManager::storeManualChargingParameters(const ThingId &evChargerId, bool enabled, int maxChargingCurrent, uint desiredPhaseCount)
{
    if (m_chargingConfigurations.contains(evChargerId)) {
        m_chargingConfigurations[evChargerId].setManualChargingEnabled(enabled);
        m_chargingConfigurations[evChargerId].setManualMaxChargingCurrent(maxChargingCurrent);
        m_chargingConfigurations[evChargerId].setManualDesiredPhaseCount(desiredPhaseCount);
    }

    EnergySettings settings;
    settings.beginGroup("ChargingConfigurations");
    settings.beginGroup(evChargerId.toString());
    settings.setValue("manualChargingEnabled", enabled);
    settings.setValue("manualMaxChargingCurrent", maxChargingCurrent);
    settings.setValue("manualDesiredPhaseCount", desiredPhaseCount);
    settings.endGroup();
    settings.endGroup();
}

Electricity::Phases SmartChargingManager::getAscendingPhasesForCount(uint phaseCount)
{
    Electricity::Phases phases = Electricity::PhaseNone;
    if (phaseCount == 3) {
        phases = Electricity::PhaseAll;
    } else if (phaseCount == 2) {
        phases.setFlag(Electricity::PhaseA);
        phases.setFlag(Electricity::PhaseB);
    } else {
        phases.setFlag(Electricity::PhaseA);
    }
    return phases;
}

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

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

    // Check if we can do the job with 1 phase without acquisition
    if (surplusAmpere > m_processInfos.value(evCharger).minimalChargingCurrent &&
        surplusAmpere < m_processInfos.value(evCharger).maximalChargingCurrent) {
        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 < m_processInfos.value(evCharger).maximalChargingCurrent) {
        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 > m_processInfos.value(evCharger).maximalChargingCurrent * 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)
{
    // Note: some chargers derive the currnet charging power from current * voltage * phaseCount. If a car supports only 1 phase charging,
    // but the charger can switch phases, we should still switch to 1 phase for the car in order to represent the reality as best as possible.
    // Also warn that we are switching to single phase because the car can only operate using 1 phase.
    uint desiredPhaseCount = chargingAction.desiredPhaseCount();

    if (m_processInfos.value(evCharger).singlePhaseCarLimitation) {
        qCDebug(dcNymeaEnergy())
            << "Limitations: the charger supports phase switching and 3 phase charging, but the car is configured to support only one phase. Switching to single phase charging.";
        desiredPhaseCount = 1;
    }

    qCDebug(dcNymeaEnergy()).nospace().noquote() << "Executing action " << evCharger->name() << " to power: " << (chargingAction.chargingEnabled() ? "ON," : "OFF,")
                                                 << " Carging current: " << chargingAction.maxChargingCurrent() << "A, "
                                                 << (m_processInfos.value(evCharger).canSwitchPhaseCount || m_processInfos.value(evCharger).singlePhaseCarLimitation
                                                         ? "Switch phases to: " + QString::number(desiredPhaseCount) + ", "
                                                         : "")
                                                 << "Issuer: " << chargingAction.issuerString() << ", forced: " << chargingAction.force();

    // FIXME: execute sequential and check what to do if an action failes, retry?

    if (m_processInfos.value(evCharger).canSwitchPhaseCount || m_processInfos.value(evCharger).singlePhaseCarLimitation) {
        evCharger->setDesiredPhaseCount(desiredPhaseCount);
        m_processInfos[evCharger].effectivePhaseCount = desiredPhaseCount;
        if (desiredPhaseCount == 1) {
            m_processInfos[evCharger].effectivePhases = Electricity::PhaseA;
        } else {
            m_processInfos[evCharger].effectivePhases = Electricity::PhaseAll;
        }
    }

    evCharger->setMaxChargingCurrent(chargingAction.maxChargingCurrent(), currentDateTime, chargingAction.force());
    evCharger->setChargingEnabled(chargingAction.chargingEnabled(), currentDateTime, chargingAction.force());
}

void SmartChargingManager::migrateSettings()
{
    // Migrate from ChargingInfos -> ChargingConfiguration (API version 1.0)
    QSettings settings(NymeaSettings::settingsPath() +  "/energy.conf", QSettings::IniFormat);
    bool lockOnUnplug = false;

    if (settings.contains("lockOnUnplug")) {
        qCDebug(dcNymeaEnergy()) << "--> Migrate lock on unplug settings from global definition to charger based configurations...";
        lockOnUnplug = settings.value("lockOnUnplug").toBool();
        settings.remove("lockOnUnplug");
    }

    if (settings.childGroups().contains("ChargingInfos")) {

        qCDebug(dcNymeaEnergy()) << "--> Migrate ChargingInfos to ChargingConfigurations...";

        QList<ChargingConfiguration> configsToMigrate;

        // Note: since there is no straight forward method for renaming a group,
        // we have to migrate all keys manually

        settings.beginGroup("ChargingInfos");

        foreach (const QString &evChargerIdString, settings.childGroups()) {
            ThingId evChargerId = ThingId(evChargerIdString);
            if (!m_thingManager->findConfiguredThing(evChargerId)) {
                qCWarning(dcNymeaEnergy()) << "EV charger with ID" << evChargerId << "not found in system. Not loading configuration.";
                settings.remove(evChargerIdString);
                continue;
            }

            settings.beginGroup(evChargerIdString);

            ChargingConfiguration config(evChargerId);
            config.setAssignedCarId(settings.value("assignedCarId").toUuid());
            config.setChargingMode(static_cast<ChargingConfiguration::ChargingMode>(settings.value("chargingMode").toInt()));

            if (settings.contains("endDateTime"))
                config.setEndDateTime(settings.value("endDateTime").toDateTime());

            QList<int> days;
            foreach (const QVariant &day, settings.value("repeatDays").toList()) {
                days.append(day.toInt());
            }
            config.setRepeatDays(days);

            config.setTargetPercentage(settings.value("targetPercentage").toUInt());
            config.setLocale(settings.value("locale").toLocale());
            config.setDailySpotMarketPercentage(settings.value("dailySpotMarketPercentage", 0).toUInt());
            config.setManualChargingEnabled(settings.value("manualChargingEnabled", false).toBool());
            config.setManualMaxChargingCurrent(settings.value("manualMaxChargingCurrent", 0).toUInt());
            config.setManualDesiredPhaseCount(settings.value("manualDesiredPhaseCount", 3).toUInt());
            config.setLockOnUnplug(lockOnUnplug);

            configsToMigrate.append(config);

            settings.endGroup(); // evChargerId
        }

        settings.endGroup(); // ChargingInfos

        // Write this to the new section
        foreach (const ChargingConfiguration &config, configsToMigrate) {
            storeChargingConfiguration(config);
        }

        qCDebug(dcNymeaEnergy()) << "Migrated successfully" << configsToMigrate.count() << "configurations. Removing old ChargingInfos...";
        settings.remove("ChargingInfos");
    }
}
