// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "OCPP201.hpp"

#include <fmt/core.h>
#include <fstream>

#include <websocketpp_utils/uri.hpp>

#include <conversions.hpp>
#include <device_model/composed_device_model_storage.hpp>
#include <error_handling.hpp>
#include <everest/conversions/ocpp/evse_security_ocpp.hpp>
#include <everest/conversions/ocpp/ocpp_conversions.hpp>
#include <everest/external_energy_limits/external_energy_limits.hpp>
#include <ocpp/v2/utils.hpp>

namespace {
void update_evcc_id_token(ocpp::v2::IdToken& id_token, const std::string& evcc_id,
                          const ocpp::OcppProtocolVersion ocpp_protocol_version) {
    if (ocpp_protocol_version != ocpp::OcppProtocolVersion::v21) {
        return;
    }
    auto info_vector =
        id_token.additionalInfo.has_value() ? id_token.additionalInfo.value() : std::vector<ocpp::v2::AdditionalInfo>{};
    if (!id_token.additionalInfo.has_value() or
        (id_token.additionalInfo.has_value() and
         std::find_if(id_token.additionalInfo->cbegin(), id_token.additionalInfo->cend(),
                      [evcc_id](const ocpp::v2::AdditionalInfo& info) {
                          return info.additionalIdToken.get() == evcc_id;
                      }) == id_token.additionalInfo->cend())) {
        ocpp::v2::AdditionalInfo info;
        info.additionalIdToken = evcc_id;
        info.type = "EVCCID";
        info_vector.push_back(info);
        id_token.additionalInfo = info_vector;
    }
}

std::string ocpp_protocol_version_to_string(const ocpp::OcppProtocolVersion ocpp_protocol_version) {
    switch (ocpp_protocol_version) {
    case ocpp::OcppProtocolVersion::v16:
        return "1.6";
    case ocpp::OcppProtocolVersion::v201:
        return "2.0.1";
    case ocpp::OcppProtocolVersion::v21:
        return "2.1";
    case ocpp::OcppProtocolVersion::Unknown:
        return "Unknown";
    }
    return "Unknown";
}
} // namespace

namespace module {

const std::string SQL_CORE_MIGRATIONS = "core_migrations";
const std::string CERTS_DIR = "certs";

// OCPP 2.0.1 specific configuration variable names
const std::string PNC_ENABLED_VAR_NAME = "PnCEnabled";
const std::string MASTER_PASS_GROUP_ID_VAR_NAME = "MasterPassGroupId";
const std::string EV_CONNECTION_TIMEOUT_VAR_NAME = "EVConnectionTimeOut";
const std::string CENTRAL_CONTRACT_VALIDATION_ALLOWED_VAR_NAME = "CentralContractValidationAllowed";
const std::string CONTRACT_CERTIFICATE_INSTALLATION_ENABLED_VAR_NAME = "ContractCertificateInstallationEnabled";
const std::string SETPOINT_PRIORITY_VAR_NAME = "SetpointPriority";
const std::string TX_START_POINT_VAR_NAME = "TxStartPoint";
const std::string TX_STOP_POINT_VAR_NAME = "TxStopPoint";
const std::string SETPOINT_SOURCE = "OCPP";

static constexpr int32_t LOWEST_SETPOINT_PRIORITY = 1000;
static constexpr int32_t HIGHEST_SETPOINT_PRIORITY = 0;

namespace fs = std::filesystem;

TxEvent get_tx_event(const ocpp::v2::ReasonEnum reason) {
    switch (reason) {
    case ocpp::v2::ReasonEnum::DeAuthorized:
    case ocpp::v2::ReasonEnum::Remote:
    case ocpp::v2::ReasonEnum::Local:
    case ocpp::v2::ReasonEnum::MasterPass:
    case ocpp::v2::ReasonEnum::StoppedByEV:
    case ocpp::v2::ReasonEnum::ReqEnergyTransferRejected:
        return TxEvent::DEAUTHORIZED;
    case ocpp::v2::ReasonEnum::EVDisconnected:
        return TxEvent::EV_DISCONNECTED;
    case ocpp::v2::ReasonEnum::ImmediateReset:
        return TxEvent::IMMEDIATE_RESET;
    // FIXME(kai): these reasons definitely do not all map to NONE
    case ocpp::v2::ReasonEnum::EmergencyStop:
    case ocpp::v2::ReasonEnum::EnergyLimitReached:
    case ocpp::v2::ReasonEnum::GroundFault:
    case ocpp::v2::ReasonEnum::LocalOutOfCredit:
    case ocpp::v2::ReasonEnum::Other:
    case ocpp::v2::ReasonEnum::OvercurrentFault:
    case ocpp::v2::ReasonEnum::PowerLoss:
    case ocpp::v2::ReasonEnum::PowerQuality:
    case ocpp::v2::ReasonEnum::Reboot:
    case ocpp::v2::ReasonEnum::SOCLimitReached:
    case ocpp::v2::ReasonEnum::TimeLimitReached:
    case ocpp::v2::ReasonEnum::Timeout:
        return TxEvent::NONE;
    }
    return TxEvent::NONE;
}

std::set<TxStartStopPoint> get_tx_start_stop_points(const std::string& tx_start_stop_point_csl) {
    std::set<TxStartStopPoint> tx_start_stop_points;
    std::vector<std::string> csv;
    std::string str;
    std::stringstream ss(tx_start_stop_point_csl);
    while (std::getline(ss, str, ',')) {
        csv.push_back(str);
    }

    for (const auto& tx_start_stop_point : csv) {
        if (tx_start_stop_point == "ParkingBayOccupancy") {
            tx_start_stop_points.insert(TxStartStopPoint::ParkingBayOccupancy);
        } else if (tx_start_stop_point == "EVConnected") {
            tx_start_stop_points.insert(TxStartStopPoint::EVConnected);
        } else if (tx_start_stop_point == "Authorized") {
            tx_start_stop_points.insert(TxStartStopPoint::Authorized);
        } else if (tx_start_stop_point == "PowerPathClosed") {
            tx_start_stop_points.insert(TxStartStopPoint::PowerPathClosed);
        } else if (tx_start_stop_point == "EnergyTransfer") {
            tx_start_stop_points.insert(TxStartStopPoint::EnergyTransfer);
        } else if (tx_start_stop_point == "DataSigned") {
            tx_start_stop_points.insert(TxStartStopPoint::DataSigned);
        } else {
            // default to PowerPathClosed for now
            tx_start_stop_points.insert(TxStartStopPoint::PowerPathClosed);
        }
    }
    return tx_start_stop_points;
}

ocpp::v2::TriggerReasonEnum stop_reason_to_trigger_reason_enum(const ocpp::v2::ReasonEnum& stop_reason) {
    switch (stop_reason) {
    case ocpp::v2::ReasonEnum::DeAuthorized:
        return ocpp::v2::TriggerReasonEnum::Deauthorized;
    case ocpp::v2::ReasonEnum::EmergencyStop:
        return ocpp::v2::TriggerReasonEnum::AbnormalCondition;
    case ocpp::v2::ReasonEnum::EnergyLimitReached:
        return ocpp::v2::TriggerReasonEnum::EnergyLimitReached;
    case ocpp::v2::ReasonEnum::EVDisconnected:
        return ocpp::v2::TriggerReasonEnum::EVCommunicationLost;
    case ocpp::v2::ReasonEnum::GroundFault:
        return ocpp::v2::TriggerReasonEnum::AbnormalCondition;
    case ocpp::v2::ReasonEnum::ImmediateReset:
        return ocpp::v2::TriggerReasonEnum::ResetCommand;
    case ocpp::v2::ReasonEnum::Local:
        return ocpp::v2::TriggerReasonEnum::StopAuthorized;
    case ocpp::v2::ReasonEnum::LocalOutOfCredit:
        return ocpp::v2::TriggerReasonEnum::ChargingStateChanged;
    case ocpp::v2::ReasonEnum::MasterPass:
        return ocpp::v2::TriggerReasonEnum::StopAuthorized;
    case ocpp::v2::ReasonEnum::PowerQuality:
        return ocpp::v2::TriggerReasonEnum::AbnormalCondition;
    case ocpp::v2::ReasonEnum::Reboot:
        return ocpp::v2::TriggerReasonEnum::ResetCommand;
    case ocpp::v2::ReasonEnum::Remote:
        return ocpp::v2::TriggerReasonEnum::RemoteStop;
    case ocpp::v2::ReasonEnum::SOCLimitReached:
        return ocpp::v2::TriggerReasonEnum::EnergyLimitReached;
    case ocpp::v2::ReasonEnum::StoppedByEV:
        return ocpp::v2::TriggerReasonEnum::StopAuthorized;
    case ocpp::v2::ReasonEnum::TimeLimitReached:
        return ocpp::v2::TriggerReasonEnum::TimeLimitReached;
    case ocpp::v2::ReasonEnum::Timeout:
        return ocpp::v2::TriggerReasonEnum::EVConnectTimeout;
    case ocpp::v2::ReasonEnum::Other:
    case ocpp::v2::ReasonEnum::ReqEnergyTransferRejected:
    case ocpp::v2::ReasonEnum::OvercurrentFault:
    case ocpp::v2::ReasonEnum::PowerLoss:
    default:
        return ocpp::v2::TriggerReasonEnum::AbnormalCondition;
    }
}

int32_t get_connector_id_from_error(const Everest::error::Error& error) {
    if (error.origin.mapping.has_value() and error.origin.mapping.value().connector.has_value()) {
        return error.origin.mapping.value().connector.value();
    }
    return 1;
}

void OCPP201::init_evse_maps() {
    {
        auto ready_handle = this->evse_ready_map.handle();
        for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) {
            (*ready_handle)[evse_id] = false;
        }
    }
    {
        auto soc_handle = this->evse_soc_map.handle();
        for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) {
            (*soc_handle)[evse_id] = std::nullopt;
        }
    }
    {
        auto evse_evcc_id_handle = this->evse_evcc_id.handle();
        for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) {
            (*evse_evcc_id_handle)[evse_id] = "";
        }
    }

    for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) {
        this->evse_hardware_capabilities_map[evse_id] = types::evse_board_support::HardwareCapabilities{};
        this->evse_supported_energy_transfer_modes[evse_id] = {};
        this->evse_service_renegotiation_supported[evse_id] = false;
    }
}

void OCPP201::init_module_configuration() {
    const auto ev_connection_timeout_request_value_response = this->charge_point->request_value<int32_t>(
        ocpp::v2::ControllerComponents::TxCtrlr, ocpp::v2::Variable{EV_CONNECTION_TIMEOUT_VAR_NAME},
        ocpp::v2::AttributeEnum::Actual);
    if (ev_connection_timeout_request_value_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
        ev_connection_timeout_request_value_response.value.has_value()) {
        this->r_auth->call_set_connection_timeout(ev_connection_timeout_request_value_response.value.value());
    }

    const auto master_pass_group_id_response = this->charge_point->request_value<std::string>(
        ocpp::v2::ControllerComponents::AuthCtrlr, ocpp::v2::Variable{MASTER_PASS_GROUP_ID_VAR_NAME},
        ocpp::v2::AttributeEnum::Actual);
    if (master_pass_group_id_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
        master_pass_group_id_response.value.has_value()) {
        this->r_auth->call_set_master_pass_group_id(master_pass_group_id_response.value.value());
    }

    types::evse_manager::PlugAndChargeConfiguration pnc_config;
    const auto iso15118_pnc_enabled_response = this->charge_point->request_value<bool>(
        ocpp::v2::ControllerComponents::ISO15118Ctrlr, ocpp::v2::Variable{PNC_ENABLED_VAR_NAME},
        ocpp::v2::AttributeEnum::Actual);
    if (iso15118_pnc_enabled_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
        iso15118_pnc_enabled_response.value.has_value()) {
        pnc_config.pnc_enabled = iso15118_pnc_enabled_response.value.value();
    }

    const auto central_contract_validation_allowed_response = this->charge_point->request_value<bool>(
        ocpp::v2::ControllerComponents::ISO15118Ctrlr, ocpp::v2::Variable{CENTRAL_CONTRACT_VALIDATION_ALLOWED_VAR_NAME},
        ocpp::v2::AttributeEnum::Actual);
    if (central_contract_validation_allowed_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
        central_contract_validation_allowed_response.value.has_value()) {
        pnc_config.central_contract_validation_allowed = central_contract_validation_allowed_response.value.value();
    }

    const auto contract_certificate_installation_enabled_response = this->charge_point->request_value<bool>(
        ocpp::v2::ControllerComponents::ISO15118Ctrlr,
        ocpp::v2::Variable{CONTRACT_CERTIFICATE_INSTALLATION_ENABLED_VAR_NAME}, ocpp::v2::AttributeEnum::Actual);
    if (contract_certificate_installation_enabled_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
        contract_certificate_installation_enabled_response.value.has_value()) {
        pnc_config.contract_certificate_installation_enabled =
            contract_certificate_installation_enabled_response.value.value();
    }

    for (const auto& evse_manager : this->r_evse_manager) {
        evse_manager->call_set_plug_and_charge_configuration(pnc_config);
    }
}

std::map<int32_t, int32_t> OCPP201::get_connector_structure() {
    std::map<int32_t, int32_t> evse_connector_structure;
    int evse_id = 1;
    for (const auto& evse : this->r_evse_manager) {
        auto _evse = evse->call_get_evse();
        int32_t num_connectors = _evse.connectors.size();

        if (_evse.id != evse_id) {
            throw std::runtime_error("Configured evse_id(s) must start with 1 counting upwards");
        }
        if (num_connectors > 0) {
            int connector_id = 1;
            for (const auto& connector : _evse.connectors) {
                if (connector.id != connector_id) {
                    throw std::runtime_error("Configured connector_id(s) must start with 1 counting upwards");
                }
                connector_id++;
            }
        } else {
            num_connectors = 1;
        }

        evse_connector_structure[evse_id] = num_connectors;
        evse_id++;
    }
    return evse_connector_structure;
}

types::powermeter::Powermeter get_meter_value(const types::evse_manager::SessionEvent& session_event) {
    const auto event_type = session_event.event;
    if (event_type == types::evse_manager::SessionEventEnum::SessionStarted) {
        if (!session_event.session_started.has_value()) {
            throw std::runtime_error("SessionEvent SessionStarted does not contain session_started context");
        }
        return session_event.session_started.value().meter_value;
    } else if (event_type == types::evse_manager::SessionEventEnum::SessionFinished) {
        if (!session_event.session_finished.has_value()) {
            throw std::runtime_error("SessionEvent SessionFinished does not contain session_finished context");
        }
        return session_event.session_finished.value().meter_value;
    } else if (event_type == types::evse_manager::SessionEventEnum::TransactionStarted) {
        if (!session_event.transaction_started.has_value()) {
            throw std::runtime_error("SessionEvent TransactionStarted does not contain transaction_started context");
        }
        return session_event.transaction_started.value().meter_value;
    } else if (event_type == types::evse_manager::SessionEventEnum::TransactionFinished) {
        if (!session_event.transaction_finished.has_value()) {
            throw std::runtime_error("SessionEvent TransactionFinished does not contain transaction_finished context");
        }
        return session_event.transaction_finished.value().meter_value;
    } else if (event_type == types::evse_manager::SessionEventEnum::ChargingStarted or
               event_type == types::evse_manager::SessionEventEnum::ChargingPausedEV or
               event_type == types::evse_manager::SessionEventEnum::ChargingPausedEVSE) {
        if (!session_event.charging_state_changed_event.has_value()) {
            throw std::runtime_error("SessionEvent does not contain charging_state_changed_event context");
        }
        return session_event.charging_state_changed_event.value().meter_value;
    } else if (event_type == types::evse_manager::SessionEventEnum::Authorized or
               event_type == types::evse_manager::SessionEventEnum::Deauthorized) {
        if (!session_event.authorization_event.has_value()) {
            throw std::runtime_error(
                "SessionEvent Authorized or Deauthorized does not contain authorization_event context");
        }
        return session_event.authorization_event.value().meter_value;
    } else {
        throw std::runtime_error("Could not retrieve meter value from SessionEvent");
    }
}

std::optional<types::units_signed::SignedMeterValue>
get_signed_meter_value(const types::evse_manager::SessionEvent& session_event) {
    const auto event_type = session_event.event;
    if (event_type == types::evse_manager::SessionEventEnum::SessionStarted) {
        if (!session_event.session_started.has_value()) {
            throw std::runtime_error("SessionEvent SessionStarted does not contain session_started context");
        }
        return session_event.session_started.value().signed_meter_value;
    } else if (event_type == types::evse_manager::SessionEventEnum::TransactionStarted) {
        if (!session_event.transaction_started.has_value()) {
            throw std::runtime_error("SessionEvent TransactionStarted does not contain transaction_started context");
        }
        return session_event.transaction_started.value().signed_meter_value;
    } else if (event_type == types::evse_manager::SessionEventEnum::TransactionFinished) {
        if (!session_event.transaction_finished.has_value()) {
            throw std::runtime_error("SessionEvent TransactionFinished does not contain transaction_finished context");
        }
        return session_event.transaction_finished.value().signed_meter_value;
    }
    return std::nullopt;
}

std::optional<ocpp::v2::IdToken> get_authorized_id_token(const types::evse_manager::SessionEvent& session_event) {
    const auto event_type = session_event.event;
    if (event_type == types::evse_manager::SessionEventEnum::SessionStarted) {
        if (!session_event.session_started.has_value()) {
            throw std::runtime_error("SessionEvent SessionStarted does not contain session_started context");
        }
        const auto session_started = session_event.session_started.value();
        if (session_started.id_tag.has_value()) {
            return conversions::to_ocpp_id_token(session_started.id_tag.value().id_token);
        }
    } else if (event_type == types::evse_manager::SessionEventEnum::TransactionStarted) {
        if (!session_event.transaction_started.has_value()) {
            throw std::runtime_error("SessionEvent TransactionStarted does not contain transaction_started context");
        }
        const auto transaction_started = session_event.transaction_started.value();
        return conversions::to_ocpp_id_token(transaction_started.id_tag.id_token);
    }
    return std::nullopt;
}

ocpp::v2::ChargingRateUnitEnum get_unit_or_default(const std::string& unit_string) {
    try {
        return ocpp::v2::conversions::string_to_charging_rate_unit_enum(unit_string);
    } catch (const std::out_of_range& e) {
        EVLOG_warning << "RequestCompositeScheduleUnit configured incorrectly with: " << unit_string
                      << ". Defaulting to using Amps.";
        return ocpp::v2::ChargingRateUnitEnum::A;
    }
}

void OCPP201::init() {
    invoke_init(*p_auth_provider);
    invoke_init(*p_auth_validator);

    source_ext_limit = info.id + "/OCPP_set_external_limits";

    // ensure all evse_energy_sink(s) that are connected have an evse id mapping
    for (const auto& evse_sink : this->r_evse_energy_sink) {
        if (not evse_sink->get_mapping().has_value()) {
            EVLOG_critical << "Please configure an evse mapping in your configuration file for the connected "
                              "r_evse_energy_sink with module_id: "
                           << evse_sink->module_id;
            throw std::runtime_error("At least one connected evse_energy_sink misses a mapping to an evse.");
        }
    }

    this->init_evse_maps();

    const auto error_handler = [this](const Everest::error::Error& error) {
        if (error.type == EVSE_MANAGER_INOPERATIVE_ERROR) {
            // handled by specific evse_manager error handler
            return;
        }
        if (this->started) {
            const auto event_data = get_event_data(error, false, this->event_id_counter++);
            this->charge_point->on_event({event_data});
        } else {
            std::scoped_lock lock(this->session_event_mutex);
            this->event_queue[get_component_from_error(error).evse.value_or(ocpp::v2::EVSE{0}).id].push(error);
        }
    };

    const auto error_cleared_handler = [this](const Everest::error::Error& error) {
        if (error.type == EVSE_MANAGER_INOPERATIVE_ERROR) {
            // handled by specific evse_manager error handler
            return;
        }
        if (this->started) {
            const auto event_data = get_event_data(error, true, this->event_id_counter++);
            this->charge_point->on_event({event_data});
        } else {
            std::scoped_lock lock(this->session_event_mutex);
            this->event_queue[get_component_from_error(error).evse.value_or(ocpp::v2::EVSE{0}).id].push(error);
        }
    };

    subscribe_global_all_errors(error_handler, error_cleared_handler);

    r_system->subscribe_firmware_update_status([this](const types::system::FirmwareUpdateStatus status) {
        if (this->started) {
            this->charge_point->on_firmware_update_status_notification(
                status.request_id, conversions::to_ocpp_firmware_status_enum(status.firmware_update_status));
        } else {
            std::scoped_lock lock(this->session_event_mutex);
            this->event_queue[0].push(status);
        }
    });

    r_system->subscribe_log_status([this](types::system::LogStatus status) {
        if (this->started) {
            this->charge_point->on_log_status_notification(
                conversions::to_ocpp_upload_logs_status_enum(status.log_status), status.request_id);
        } else {
            std::scoped_lock lock(this->session_event_mutex);
            this->event_queue[0].push(status);
        }
    });

    if (!this->r_reservation.empty() && this->r_reservation.at(0) != nullptr) {
        r_reservation.at(0)->subscribe_reservation_update(
            [this](const types::reservation::ReservationUpdateStatus status) {
                if (status.reservation_status == types::reservation::Reservation_status::Expired ||
                    status.reservation_status == types::reservation::Reservation_status::Removed) {
                    EVLOG_debug << "Received reservation status update for reservation " << status.reservation_id
                                << ": "
                                << (status.reservation_status == types::reservation::Reservation_status::Expired
                                        ? "Expired"
                                        : "Removed");
                    try {
                        this->charge_point->on_reservation_status(
                            status.reservation_id,
                            conversions::to_ocpp_reservation_update_status_enum(status.reservation_status));
                    } catch (const std::out_of_range& e) {
                    }
                }
            });
    }

    this->init_evse_subscriptions();
}

void OCPP201::ready() {
    invoke_ready(*p_auth_provider);
    invoke_ready(*p_auth_validator);

    this->ocpp_share_path = this->info.paths.share;

    const auto device_model_database_path = [&]() {
        const auto config_device_model_path = fs::path(this->config.DeviceModelDatabasePath);
        if (config_device_model_path.is_relative()) {
            return this->ocpp_share_path / config_device_model_path;
        } else {
            return config_device_model_path;
        }
    }();

    const auto everest_device_model_database_path = [&]() {
        const auto config_everest_device_model_path = fs::path(this->config.EverestDeviceModelDatabasePath);
        if (config_everest_device_model_path.is_relative()) {
            return this->ocpp_share_path / config_everest_device_model_path;
        } else {
            return config_everest_device_model_path;
        }
    }();

    const auto device_model_database_migration_path = [&]() {
        const auto config_device_model_database_migration_path =
            fs::path(this->config.DeviceModelDatabaseMigrationPath);
        if (config_device_model_database_migration_path.is_relative()) {
            return this->ocpp_share_path / config_device_model_database_migration_path;
        } else {
            return config_device_model_database_migration_path;
        }
    }();

    const auto device_model_config_path = [&]() {
        const auto config_device_model_config_path = fs::path(this->config.DeviceModelConfigPath);
        if (config_device_model_config_path.is_relative()) {
            return this->ocpp_share_path / config_device_model_config_path;
        } else {
            return config_device_model_config_path;
        }
    }();

    if (!fs::exists(this->config.MessageLogPath)) {
        try {
            fs::create_directory(this->config.MessageLogPath);
        } catch (const fs::filesystem_error& e) {
            EVLOG_AND_THROW(e);
        }
    }

    ocpp::v2::Callbacks callbacks;
    callbacks.is_reset_allowed_callback = [this](const std::optional<const int32_t> evse_id,
                                                 const ocpp::v2::ResetEnum&) {
        if (evse_id.has_value()) {
            return false; // Reset of EVSE is currently not supported
        }
        try {
            return this->r_system->call_is_reset_allowed(types::system::ResetType::NotSpecified);
        } catch (std::out_of_range& e) {
            EVLOG_warning << "Could not convert OCPP ResetEnum to EVerest ResetType while executing "
                             "is_reset_allowed_callback.";
            return false;
        }
    };
    callbacks.reset_callback = [this](const std::optional<const int32_t> evse_id, const ocpp::v2::ResetEnum& type) {
        if (evse_id.has_value()) {
            EVLOG_warning << "Reset of EVSE is currently not supported";
            return;
        }

        bool scheduled = type == ocpp::v2::ResetEnum::OnIdle;

        // small delay before stopping the charge point to make sure all responses are received
        std::this_thread::sleep_for(std::chrono::seconds(this->config.ResetStopDelay));
        try {
            this->r_system->call_reset(types::system::ResetType::NotSpecified, scheduled);
        } catch (std::out_of_range& e) {
            EVLOG_warning << "Could not convert OCPP ResetEnum to EVerest ResetType while executing reset_callack. No "
                             "reset will be executed.";
        }
    };

    callbacks.connector_effective_operative_status_changed_callback =
        [this](const int32_t evse_id, const int32_t connector_id, const ocpp::v2::OperationalStatusEnum new_status) {
            if (new_status == ocpp::v2::OperationalStatusEnum::Operative) {
                if (this->r_evse_manager.at(evse_id - 1)
                        ->call_enable_disable(connector_id, {types::evse_manager::Enable_source::CSMS,
                                                             types::evse_manager::Enable_state::Enable, 5000})) {
                    this->charge_point->on_enabled(evse_id, connector_id);
                }
            } else {
                if (this->r_evse_manager.at(evse_id - 1)
                        ->call_enable_disable(connector_id, {types::evse_manager::Enable_source::CSMS,
                                                             types::evse_manager::Enable_state::Disable, 5000})) {
                    this->charge_point->on_unavailable(evse_id, connector_id);
                }
            }
        };

    callbacks.remote_start_transaction_callback = [this](const ocpp::v2::RequestStartTransactionRequest& request,
                                                         const bool authorize_remote_start) {
        types::authorization::ProvidedIdToken provided_token;
        provided_token.id_token = conversions::to_everest_id_token(request.idToken);
        provided_token.authorization_type = types::authorization::AuthorizationType::OCPP;
        provided_token.prevalidated = !authorize_remote_start;
        provided_token.request_id = request.remoteStartId;

        if (request.groupIdToken.has_value()) {
            provided_token.parent_id_token = conversions::to_everest_id_token(request.groupIdToken.value());
        }

        if (request.evseId.has_value()) {
            provided_token.connectors = std::vector<int32_t>{request.evseId.value()};
        }
        this->p_auth_provider->publish_provided_token(provided_token);
        return ocpp::v2::RequestStartStopStatusEnum::Accepted;
    };

    callbacks.stop_transaction_callback = [this](const int32_t evse_id, const ocpp::v2::ReasonEnum& stop_reason) {
        if (evse_id <= 0 or evse_id > this->r_evse_manager.size()) {
            return ocpp::v2::RequestStartStopStatusEnum::Rejected;
        }

        types::evse_manager::StopTransactionRequest req;
        req.reason = conversions::to_everest_stop_transaction_reason(stop_reason);

        return this->r_evse_manager.at(evse_id - 1)->call_stop_transaction(req)
                   ? ocpp::v2::RequestStartStopStatusEnum::Accepted
                   : ocpp::v2::RequestStartStopStatusEnum::Rejected;
    };

    callbacks.pause_charging_callback = [this](const int32_t evse_id) {
        if (evse_id > 0 && evse_id <= this->r_evse_manager.size()) {
            this->r_evse_manager.at(evse_id - 1)->call_pause_charging();
        }
    };

    callbacks.unlock_connector_callback = [this](const int32_t evse_id, const int32_t connector_id) {
        // FIXME: This needs to properly handle different connectors
        ocpp::v2::UnlockConnectorResponse response;
        if (evse_id > 0 && evse_id <= this->r_evse_manager.size()) {
            if (this->r_evse_manager.at(evse_id - 1)->call_force_unlock(connector_id)) {
                response.status = ocpp::v2::UnlockStatusEnum::Unlocked;
            } else {
                response.status = ocpp::v2::UnlockStatusEnum::UnlockFailed;
            }
        } else {
            response.status = ocpp::v2::UnlockStatusEnum::UnknownConnector;
        }

        return response;
    };

    callbacks.get_log_request_callback = [this](const ocpp::v2::GetLogRequest& request) {
        auto req = conversions::to_everest_upload_logs_request(request);
        if (req.retries.has_value()) {
            req.retries = req.retries.value() + 1;
        }
        const auto response = this->r_system->call_upload_logs(req);
        return conversions::to_ocpp_get_log_response(response);
    };

    callbacks.is_reservation_for_token_callback = [this](const int32_t evse_id, const ocpp::CiString<255> idToken,
                                                         const std::optional<ocpp::CiString<255>> groupIdToken) {
        if (this->r_reservation.empty() || this->r_reservation.at(0) == nullptr) {
            return ocpp::ReservationCheckStatus::NotReserved;
        }

        types::reservation::ReservationCheck reservation_check_request;
        reservation_check_request.evse_id = evse_id;
        reservation_check_request.id_token = idToken.get();
        if (groupIdToken.has_value()) {
            reservation_check_request.group_id_token = groupIdToken.value().get();
        }

        const types::reservation::ReservationCheckStatus reservation_status =
            this->r_reservation.at(0)->call_exists_reservation(reservation_check_request);

        return ocpp_conversions::to_ocpp_reservation_check_status(reservation_status);
    };

    callbacks.update_firmware_request_callback = [this](const ocpp::v2::UpdateFirmwareRequest& request) {
        auto req = conversions::to_everest_firmware_update_request(request);
        if (req.retries.has_value()) {
            req.retries = req.retries.value() + 1;
        }
        const auto response = this->r_system->call_update_firmware(req);
        return conversions::to_ocpp_update_firmware_response(response);
    };

    callbacks.variable_changed_callback = [this](const ocpp::v2::SetVariableData& set_variable_data) {
        if (set_variable_data.component == ocpp::v2::ControllerComponents::TxCtrlr and
            set_variable_data.variable.name.get() == EV_CONNECTION_TIMEOUT_VAR_NAME) {
            try {
                auto ev_connection_timeout = std::stoi(set_variable_data.attributeValue.get());
                this->r_auth->call_set_connection_timeout(ev_connection_timeout);
            } catch (const std::exception& e) {
                EVLOG_error << "Could not parse EVConnectionTimeOut and did not set it in Auth module, error: "
                            << e.what();
                return;
            }
        } else if (set_variable_data.component == ocpp::v2::ControllerComponents::AuthCtrlr and
                   set_variable_data.variable.name.get() == MASTER_PASS_GROUP_ID_VAR_NAME) {
            this->r_auth->call_set_master_pass_group_id(set_variable_data.attributeValue.get());
        } else if (set_variable_data.component == ocpp::v2::ControllerComponents::TxCtrlr and
                   set_variable_data.variable.name.get() == TX_START_POINT_VAR_NAME) {
            const auto tx_start_points = get_tx_start_stop_points(set_variable_data.attributeValue.get());
            if (tx_start_points.empty()) {
                EVLOG_warning << "Could not set TxStartPoints";
                return;
            }
            this->transaction_handler->set_tx_start_points(tx_start_points);
        } else if (set_variable_data.component == ocpp::v2::ControllerComponents::TxCtrlr and
                   set_variable_data.variable.name.get() == TX_STOP_POINT_VAR_NAME) {
            const auto tx_stop_points = get_tx_start_stop_points(set_variable_data.attributeValue.get());
            if (tx_stop_points.empty()) {
                EVLOG_warning << "Could not set TxStartPoints";
                return;
            }
            this->transaction_handler->set_tx_stop_points(tx_stop_points);
        } else if (set_variable_data.component == ocpp::v2::ControllerComponents::ISO15118Ctrlr and
                   set_variable_data.variable.name.get() == PNC_ENABLED_VAR_NAME) {
            types::evse_manager::PlugAndChargeConfiguration pnc_config;
            pnc_config.pnc_enabled = ocpp::conversions::string_to_bool(set_variable_data.attributeValue.get());
            for (const auto& evse_manager : this->r_evse_manager) {
                evse_manager->call_set_plug_and_charge_configuration(pnc_config);
            }
        } else if (set_variable_data.component == ocpp::v2::ControllerComponents::ISO15118Ctrlr and
                   set_variable_data.variable.name.get() == CENTRAL_CONTRACT_VALIDATION_ALLOWED_VAR_NAME) {
            types::evse_manager::PlugAndChargeConfiguration pnc_config;
            pnc_config.central_contract_validation_allowed =
                ocpp::conversions::string_to_bool(set_variable_data.attributeValue.get());
            for (const auto& evse_manager : this->r_evse_manager) {
                evse_manager->call_set_plug_and_charge_configuration(pnc_config);
            }
        } else if (set_variable_data.component == ocpp::v2::ControllerComponents::ISO15118Ctrlr and
                   set_variable_data.variable.name.get() == CONTRACT_CERTIFICATE_INSTALLATION_ENABLED_VAR_NAME) {
            types::evse_manager::PlugAndChargeConfiguration pnc_config;
            pnc_config.contract_certificate_installation_enabled =
                ocpp::conversions::string_to_bool(set_variable_data.attributeValue.get());
            for (const auto& evse_manager : this->r_evse_manager) {
                evse_manager->call_set_plug_and_charge_configuration(pnc_config);
            }
        }
    };

    callbacks.validate_network_profile_callback =
        [this](const int32_t configuration_slot, const ocpp::v2::NetworkConnectionProfile& network_connection_profile) {
            auto ws_uri = ocpp::uri(network_connection_profile.ocppCsmsUrl.get());

            if (ws_uri.get_valid()) {
                return ocpp::v2::SetNetworkProfileStatusEnum::Accepted;
            } else {
                return ocpp::v2::SetNetworkProfileStatusEnum::Rejected;
            }
            // TODO(piet): Add further validation of the NetworkConnectionProfile
        };

    callbacks.configure_network_connection_profile_callback =
        [this](const int32_t configuration_slot, const ocpp::v2::NetworkConnectionProfile& network_connection_profile) {
            std::promise<ocpp::ConfigNetworkResult> promise;
            std::future<ocpp::ConfigNetworkResult> future = promise.get_future();
            ocpp::ConfigNetworkResult result;
            result.success = true;
            promise.set_value(result);
            return future;
        };

    callbacks.all_connectors_unavailable_callback = [this]() {
        EVLOG_info << "All connectors unavailable, proceed with firmware installation";
        this->r_system->call_allow_firmware_installation();
    };

    callbacks.transaction_event_callback = [this](const ocpp::v2::TransactionEventRequest& transaction_event) {
        const auto ocpp_transaction_event = conversions::to_everest_ocpp_transaction_event(transaction_event);
        this->p_ocpp_generic->publish_ocpp_transaction_event(ocpp_transaction_event);
    };

    callbacks.transaction_event_response_callback =
        [this](const ocpp::v2::TransactionEventRequest& transaction_event,
               const ocpp::v2::TransactionEventResponse& transaction_event_response) {
            auto ocpp_transaction_event = conversions::to_everest_ocpp_transaction_event(transaction_event);
            auto ocpp_transaction_event_response =
                conversions::to_everest_transaction_event_response(transaction_event_response);
            ocpp_transaction_event_response.original_transaction_event = ocpp_transaction_event;
            this->p_ocpp_generic->publish_ocpp_transaction_event_response(ocpp_transaction_event_response);
            if (transaction_event_response.idTokenInfo.has_value() and transaction_event.evse.has_value()) {
                types::authorization::ValidationResultUpdate result_update;
                result_update.validation_result =
                    conversions::to_everest_validation_result(transaction_event_response.idTokenInfo.value());
                result_update.connector_id = transaction_event.evse->id;
                p_auth_validator->publish_validate_result_update(result_update);
            }
        };

    callbacks.boot_notification_callback =
        [this](const ocpp::v2::BootNotificationResponse& boot_notification_response) {
            const auto everest_boot_notification_response =
                conversions::to_everest_boot_notification_response(boot_notification_response);
            this->p_ocpp_generic->publish_boot_notification_response(everest_boot_notification_response);
        };

    callbacks.set_display_message_callback =
        [this](const std::vector<ocpp::DisplayMessage>& messages) -> ocpp::v2::SetDisplayMessageResponse {
        ocpp::v2::SetDisplayMessageResponse response;
        if (this->r_display_message.empty()) {
            response.status = ocpp::v2::DisplayMessageStatusEnum::Rejected;
            return response;
        }

        std::vector<types::display_message::DisplayMessage> display_messages;
        for (const ocpp::DisplayMessage& message : messages) {
            const types::display_message::DisplayMessage m = ocpp_conversions::to_everest_display_message(message);
            display_messages.push_back(m);
        }

        const types::display_message::SetDisplayMessageResponse display_message_response =
            this->r_display_message.at(0)->call_set_display_message(display_messages);
        response = conversions::to_ocpp_set_display_message_response(display_message_response);

        return response;
    };

    callbacks.get_display_message_callback =
        [this](const ocpp::v2::GetDisplayMessagesRequest& request) -> std::vector<ocpp::DisplayMessage> {
        if (this->r_display_message.empty()) {
            return {};
        }
        types::display_message::GetDisplayMessageRequest get_request;

        types::display_message::GetDisplayMessageResponse response =
            this->r_display_message.at(0)->call_get_display_messages(
                conversions::to_everest_display_message_request(request));

        if (!response.messages.has_value() || response.messages.value().empty()) {
            return {};
        }

        std::vector<ocpp::DisplayMessage> ocpp_display_messages;
        for (const auto& message : response.messages.value()) {
            ocpp_display_messages.push_back(ocpp_conversions::to_ocpp_display_message(message));
        }

        return ocpp_display_messages;
    };

    callbacks.clear_display_message_callback =
        [this](const ocpp::v2::ClearDisplayMessageRequest& request) -> ocpp::v2::ClearDisplayMessageResponse {
        if (this->r_display_message.empty()) {
            ocpp::v2::ClearDisplayMessageResponse response;
            response.status = ocpp::v2::ClearMessageStatusEnum::Unknown;
            return response;
        }

        types::display_message::ClearDisplayMessageResponse response =
            this->r_display_message.at(0)->call_clear_display_message(
                conversions::to_everest_clear_display_message_request(request));
        return conversions::to_ocpp_clear_display_message_response(response);
    };

    if (this->p_session_cost != nullptr) {
        callbacks.set_running_cost_callback = [this](const ocpp::RunningCost& running_cost,
                                                     const uint32_t number_of_decimals,
                                                     const std::optional<std::string>& currency_code) {
            std::optional<types::money::CurrencyCode> currency;
            if (currency_code.has_value()) {
                try {
                    currency = types::money::string_to_currency_code(currency_code.value());
                } catch (const std::out_of_range& e) {
                    // If conversion fails, we just don't add the currency code. But we want to see it in the
                    // logging.
                    EVLOG_error << e.what();
                }
            }
            const types::session_cost::SessionCost cost =
                ocpp_conversions::create_session_cost(running_cost, number_of_decimals, currency);
            this->p_session_cost->publish_session_cost(cost);
        };

        callbacks.tariff_message_callback = [this](const ocpp::TariffMessage& message) {
            const types::session_cost::TariffMessage m = ocpp_conversions::to_everest_tariff_message(message);
            this->p_session_cost->publish_tariff_message(m);
        };

        callbacks.default_price_callback = [this](const std::vector<ocpp::DisplayMessageContent>& messages) {
            const types::session_cost::DefaultPrice p = ocpp_conversions::to_everest_default_price(messages);
            this->p_session_cost->publish_default_price(p);
        };
    }

    if (!this->r_data_transfer.empty()) {
        callbacks.data_transfer_callback = [this](const ocpp::v2::DataTransferRequest& request) {
            types::ocpp::DataTransferRequest data_transfer_request =
                conversions::to_everest_data_transfer_request(request);
            types::ocpp::DataTransferResponse data_transfer_response =
                this->r_data_transfer.at(0)->call_data_transfer(data_transfer_request);
            ocpp::v2::DataTransferResponse response =
                conversions::to_ocpp_data_transfer_response(data_transfer_response);
            return response;
        };
    }

    callbacks.connection_state_changed_callback =
        [this](const bool is_connected, const int /*configuration_slot*/,
               const ocpp::v2::NetworkConnectionProfile& /*network_connection_profile*/,
               const ocpp::OcppProtocolVersion protocol_version) {
            if (is_connected) {
                ocpp_protocol_version = protocol_version;
            } else {
                ocpp_protocol_version = ocpp::OcppProtocolVersion::Unknown;
            }
            this->p_ocpp_generic->publish_is_connected(is_connected);
        };

    callbacks.security_event_callback = [this](const ocpp::CiString<50>& event_type,
                                               const std::optional<ocpp::CiString<255>>& tech_info) {
        types::ocpp::SecurityEvent event;
        event.type = event_type.get();
        EVLOG_info << "Security Event in OCPP occurred: " << event.type;
        if (tech_info.has_value()) {
            event.info = tech_info.value().get();
        }
        this->p_ocpp_generic->publish_security_event(event);
    };

    // this callback publishes the schedules within EVerest and applies the schedules for the individual
    // r_evse_energy_sink
    const auto charging_schedules_callback = [this]() { charging_schedules_timer_callback(); };

    callbacks.set_charging_profiles_callback = charging_schedules_callback;

    callbacks.time_sync_callback = [this](const ocpp::DateTime& current_time) {
        this->r_system->call_set_system_time(current_time.to_rfc3339());
    };

    callbacks.reserve_now_callback =
        [this](const ocpp::v2::ReserveNowRequest& request) -> ocpp::v2::ReserveNowStatusEnum {
        ocpp::v2::ReserveNowResponse response;
        if (this->r_reservation.empty() || this->r_reservation.at(0) == nullptr) {
            EVLOG_info << "Reservation rejected because the interface r_reservation is a nullptr";
            return ocpp::v2::ReserveNowStatusEnum::Rejected;
        }

        types::reservation::Reservation reservation;
        reservation.reservation_id = request.id;
        reservation.expiry_time = request.expiryDateTime.to_rfc3339();
        reservation.id_token = request.idToken.idToken;
        reservation.evse_id = request.evseId;
        if (request.groupIdToken.has_value()) {
            reservation.parent_id_token = request.groupIdToken.value().idToken;
        }
        if (request.connectorType.has_value()) {
            reservation.connector_type =
                types::evse_manager::string_to_connector_type_enum(request.connectorType.value());
        }

        types::reservation::ReservationResult result = this->r_reservation.at(0)->call_reserve_now(reservation);
        return conversions::to_ocpp_reservation_status(result);
    };

    callbacks.cancel_reservation_callback = [this](const int32_t reservation_id) -> bool {
        EVLOG_debug << "Received cancel reservation request for reservation id " << reservation_id;
        if (this->r_reservation.empty() || this->r_reservation.at(0) == nullptr) {
            return false;
        }

        return this->r_reservation.at(0)->call_cancel_reservation(reservation_id);
    };

    callbacks.update_allowed_energy_transfer_modes_callback =
        [this](const std::vector<ocpp::v2::EnergyTransferModeEnum>& allowed_energy_transfer_modes,
               const ocpp::CiString<36>& transaction_id) -> bool {
        const int evse_id = transaction_handler->get_evse_id(transaction_id);
        if (evse_id == -1 || evse_id > this->r_evse_manager.size()) {
            return false;
        }
        auto& evse = this->r_evse_manager.at(evse_id - 1); // evse_id starts at 1 if valid

        if (evse != nullptr) {
            return evse->call_update_allowed_energy_transfer_modes(
                       conversions::to_everest_allowed_energy_transfer_modes(allowed_energy_transfer_modes)) ==
                   types::evse_manager::UpdateAllowedEnergyTransferModesResult::Accepted;
        }
        return false;
    };

    callbacks.ocpp_messages_callback = [this](const std::string& message, ocpp::MessageDirection direction) {
        switch (direction) {
        case ocpp::MessageDirection::CSMSToChargingStation: {
            types::ocpp::Message ocpp_message;
            ocpp_message.message = message;
            ocpp_message.version = ocpp_protocol_version_to_string(this->ocpp_protocol_version);
            ocpp_message.direction = types::ocpp::MessageDirection::CSMSToChargingStation;
            p_ocpp_generic->publish_ocpp_message(ocpp_message);
            break;
        }
        case ocpp::MessageDirection::ChargingStationToCSMS: {
            types::ocpp::Message ocpp_message;
            ocpp_message.message = message;
            ocpp_message.version = ocpp_protocol_version_to_string(this->ocpp_protocol_version);
            ocpp_message.direction = types::ocpp::MessageDirection::ChargingStationToCSMS;
            p_ocpp_generic->publish_ocpp_message(ocpp_message);
            break;
        }
        default:
            // unknown message direction (ignored)
            break;
        }
    };

    {
        auto ready_handle = this->evse_ready_map.handle();
        ready_handle.wait([this, &ready_handle]() {
            for (const auto& [evse, ready] : *ready_handle) {
                if (!ready) {
                    return false;
                }
            }
            EVLOG_info << "All EVSE ready. Starting OCPP2.X service";
            return true;
        });
    }

    const auto sql_init_path = this->ocpp_share_path / SQL_CORE_MIGRATIONS;

    std::map<int32_t, int32_t> evse_connector_structure = this->get_connector_structure();

    // initialize libocpp device model
    auto libocpp_device_model_storage = std::make_shared<ocpp::v2::DeviceModelStorageSqlite>(
        device_model_database_path, device_model_database_migration_path, device_model_config_path);

    // initialize everest device model
    this->everest_device_model_storage = std::make_shared<device_model::EverestDeviceModelStorage>(
        r_evse_manager, r_extensions_15118, this->evse_hardware_capabilities_map,
        this->evse_supported_energy_transfer_modes, this->evse_service_renegotiation_supported,
        everest_device_model_database_path, device_model_database_migration_path, get_config_service_client());

    // initialize composed device model, this will be provided to the ChargePoint constructor
    auto composed_device_model_storage = std::make_unique<module::device_model::ComposedDeviceModelStorage>();

    // register both device model storages
    composed_device_model_storage->register_device_model_storage("OCPP", std::move(libocpp_device_model_storage));
    composed_device_model_storage->register_device_model_storage("EVEREST", this->everest_device_model_storage);

    this->charge_point = std::make_unique<ocpp::v2::ChargePoint>(
        evse_connector_structure, std::move(composed_device_model_storage), this->ocpp_share_path.string(),
        this->config.CoreDatabasePath, sql_init_path.string(), this->config.MessageLogPath,
        std::make_shared<EvseSecurity>(*this->r_security), callbacks);

    // publish charging schedules at least once on startup
    charging_schedules_callback();
    charging_schedules_timer_start();

    this->init_module_configuration();

    if (this->config.EnableExternalWebsocketControl) {
        const std::string connect_topic = "everest_api/ocpp/cmd/connect";
        this->mqtt.subscribe(connect_topic,
                             [this](const std::string& data) { this->charge_point->connect_websocket(); });

        const std::string disconnect_topic = "everest_api/ocpp/cmd/disconnect";
        this->mqtt.subscribe(disconnect_topic,
                             [this](const std::string& data) { this->charge_point->disconnect_websocket(); });
    }

    std::set<TxStartStopPoint> tx_start_points;
    std::set<TxStartStopPoint> tx_stop_points;

    const auto tx_start_point_request_value_response = this->charge_point->request_value<std::string>(
        ocpp::v2::Component{"TxCtrlr"}, ocpp::v2::Variable{TX_START_POINT_VAR_NAME}, ocpp::v2::AttributeEnum::Actual);
    if (tx_start_point_request_value_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
        tx_start_point_request_value_response.value.has_value()) {
        auto tx_start_point_csl =
            tx_start_point_request_value_response.value.value(); // contains comma seperated list of TxStartPoints
        tx_start_points = get_tx_start_stop_points(tx_start_point_csl);
        EVLOG_info << "TxStartPoints from device model: " << tx_start_point_csl;
    }

    if (tx_start_points.empty()) {
        tx_start_points = {TxStartStopPoint::PowerPathClosed};
    }

    const auto tx_stop_point_request_value_response = this->charge_point->request_value<std::string>(
        ocpp::v2::Component{"TxCtrlr"}, ocpp::v2::Variable{TX_STOP_POINT_VAR_NAME}, ocpp::v2::AttributeEnum::Actual);
    if (tx_stop_point_request_value_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
        tx_stop_point_request_value_response.value.has_value()) {
        auto tx_stop_point_csl =
            tx_stop_point_request_value_response.value.value(); // contains comma seperated list of TxStartPoints
        tx_stop_points = get_tx_start_stop_points(tx_stop_point_csl);
        EVLOG_info << "TxStopPoints from device model: " << tx_stop_point_csl;
    }

    if (tx_stop_points.empty()) {
        tx_stop_points = {TxStartStopPoint::EVConnected, TxStartStopPoint::Authorized};
    }

    this->transaction_handler =
        std::make_unique<TransactionHandler>(this->r_evse_manager.size(), tx_start_points, tx_stop_points);

    const auto boot_reason = conversions::to_ocpp_boot_reason(this->r_system->call_get_boot_reason());
    this->charge_point->set_message_queue_resume_delay(std::chrono::seconds(this->config.MessageQueueResumeDelay));
    // we can now initialize the charge point's state machine. It reads the connector availability from the internal
    // database and potentially triggers enable/disable callbacks at the evse.
    this->charge_point->start(boot_reason, false);
    this->started = true;

    // Signal to EVSEs to start their internal state machines
    for (const auto& evse : this->r_evse_manager) {
        evse->call_external_ready_to_start_charging();
    }

    // wait for potential events from the evses in order to start OCPP with the correct initial state (e.g. EV might
    // be plugged in at startup)
    std::this_thread::sleep_for(std::chrono::milliseconds(this->config.DelayOcppStart));
    // start OCPP connection
    this->charge_point->connect_websocket();

    // process event queue
    for (auto& [evse_id, evse_event_queue] : this->event_queue) {
        while (!evse_event_queue.empty()) {
            auto queued_event = evse_event_queue.front();
            if (std::holds_alternative<types::evse_manager::SessionEvent>(queued_event)) {
                const auto session_event = std::get<types::evse_manager::SessionEvent>(queued_event);
                EVLOG_info << "Processing queued event for evse_id: " << evse_id << ", event: " << session_event.event;
                this->process_session_event(evse_id, session_event);
            } else if (std::holds_alternative<Everest::error::Error>(queued_event)) {
                const auto& error = std::get<Everest::error::Error>(queued_event);
                EVLOG_info << "Processing queued error event for evse_id: " << evse_id << ": " << error.type;
                bool is_active = error.state == Everest::error::State::Active;
                const auto event_data = get_event_data(error, !is_active, this->event_id_counter++);
                this->charge_point->on_event({event_data});

                // We do only report inoperative errors as faults
                if (error.type == EVSE_MANAGER_INOPERATIVE_ERROR) {
                    if (is_active) {
                        this->charge_point->on_faulted(evse_id, get_connector_id_from_error(error));
                    } else {
                        this->charge_point->on_fault_cleared(evse_id, get_connector_id_from_error(error));
                    }
                }
            } else if (std::holds_alternative<ocpp::v2::MeterValue>(queued_event)) {
                const auto meter_value = std::get<ocpp::v2::MeterValue>(queued_event);
                EVLOG_info << "Processing queued meter value for evse_id: " << evse_id;
                this->charge_point->on_meter_value(evse_id, meter_value);
            } else if (std::holds_alternative<types::system::FirmwareUpdateStatus>(queued_event)) {
                const auto fw_update_status = std::get<types::system::FirmwareUpdateStatus>(queued_event);
                EVLOG_info << "Processing queued firmware update status";
                this->charge_point->on_firmware_update_status_notification(
                    fw_update_status.request_id,
                    conversions::to_ocpp_firmware_status_enum(fw_update_status.firmware_update_status));
            } else if (std::holds_alternative<types::system::LogStatus>(queued_event)) {
                const auto log_status = std::get<types::system::LogStatus>(queued_event);
                EVLOG_info << "Processing queued log status";
                this->charge_point->on_log_status_notification(
                    conversions::to_ocpp_upload_logs_status_enum(log_status.log_status), log_status.request_id);
            } else {
                EVLOG_warning << "Unknown event type in queue for evse_id: " << evse_id;
            }
            evse_event_queue.pop();
        }
    }
}

void OCPP201::charging_schedules_timer_callback() {
    // this callback publishes the schedules within EVerest and applies the schedules for the individual
    // r_evse_energy_sink
    const auto composite_schedule_unit = get_unit_or_default(config.RequestCompositeScheduleUnit);
    const auto composite_schedules =
        charge_point->get_all_composite_schedules(config.RequestCompositeScheduleDurationS, composite_schedule_unit);
    publish_charging_schedules(composite_schedules);
    set_external_limits(composite_schedules);
}

void OCPP201::charging_schedules_timer_start() {
    if (config.CompositeScheduleIntervalS > 0) {
        const auto charging_schedules_callback = [this]() { charging_schedules_timer_callback(); };
        charging_schedules_timer.interval(charging_schedules_callback,
                                          std::chrono::seconds(config.CompositeScheduleIntervalS));
    }
}

void OCPP201::charging_schedules_timer_stop() {
    charging_schedules_timer.stop();
}

void OCPP201::init_evse_subscriptions() {
    int evse_id = 1;
    for (const auto& evse : this->r_evse_manager) {
        evse->subscribe_waiting_for_external_ready([this, evse_id](bool ready) {
            if (ready) {
                this->evse_ready_map.handle()->at(evse_id) = true;
                this->evse_ready_map.notify_one();
            }
        });

        evse->subscribe_ready([this, evse_id](bool ready) {
            if (ready) {
                EVLOG_info << "EVSE " << evse_id << " ready.";
                this->evse_ready_map.handle()->at(evse_id) = true;
                this->evse_ready_map.notify_one();
            }
        });

        evse->subscribe_hw_capabilities(
            [this, evse_id](const types::evse_board_support::HardwareCapabilities& hw_capabilities) {
                this->evse_hardware_capabilities_map[evse_id] = hw_capabilities;
            });

        evse->subscribe_supported_energy_transfer_modes(
            [this, evse_id](const std::vector<types::iso15118::EnergyTransferMode>& supported_energy_transfer_modes) {
                this->evse_supported_energy_transfer_modes[evse_id] = supported_energy_transfer_modes;
            });

        evse->subscribe_session_event([this, evse_id](types::evse_manager::SessionEvent session_event) {
            if (!this->started) {
                EVLOG_info << "OCPP not fully initialized, but received a session event on evse_id: " << evse_id
                           << " that will be queued up: " << session_event.event;
                std::scoped_lock lock(this->session_event_mutex);
                this->event_queue[evse_id].push(session_event);
                return;
            }
            this->process_session_event(evse_id, session_event);
        });

        evse->subscribe_powermeter([this, evse_id](const types::powermeter::Powermeter& power_meter) {
            ocpp::v2::MeterValue meter_value = conversions::to_ocpp_meter_value(
                power_meter, ocpp::v2::ReadingContextEnum::Sample_Periodic, power_meter.signed_meter_value);
            if (!this->started) {
                std::scoped_lock lock(this->session_event_mutex);
                this->event_queue[evse_id].push(meter_value);
                return;
            }
            auto evse_soc_map_handle = this->evse_soc_map.handle();
            if (evse_soc_map_handle->at(evse_id).has_value()) {
                auto sampled_soc_value = conversions::to_ocpp_sampled_value(
                    ocpp::v2::ReadingContextEnum::Sample_Periodic, ocpp::v2::MeasurandEnum::SoC, "Percent",
                    std::nullopt, ocpp::v2::LocationEnum::EV);
                sampled_soc_value.value = evse_soc_map_handle->at(evse_id).value();
                meter_value.sampledValue.push_back(sampled_soc_value);
            }
            this->charge_point->on_meter_value(evse_id, meter_value);
            const auto total_power_active_import = ocpp::v2::utils::get_total_power_active_import(meter_value);
            if (total_power_active_import.has_value()) {
                this->everest_device_model_storage->update_power(evse_id, total_power_active_import.value());
            }
        });

        evse->subscribe_ev_info([this, evse_id](const types::evse_manager::EVInfo& ev_info) {
            if (!this->started) {
                EVLOG_info << "EV Info received from evse_manager before DM was instantiated, ignoring...";
                EVLOG_info << "EV Info will be retrieved later";
                return;
            }
            if (ev_info.soc.has_value()) {
                this->evse_soc_map.handle()->at(evse_id) = ev_info.soc;
            }
            if (ev_info.evcc_id.has_value()) {
                this->evse_evcc_id.handle()->at(evse_id) = ev_info.evcc_id.value();
                this->everest_device_model_storage->update_connected_ev_vehicle_id(evse_id, ev_info.evcc_id.value());
            }
        });

        auto fault_handler = [this, evse_id](const Everest::error::Error& error) {
            if (this->started) {
                const auto event_data = get_event_data(error, false, this->event_id_counter++);
                this->charge_point->on_event({event_data});
                this->charge_point->on_faulted(evse_id, get_connector_id_from_error(error));
            } else {
                std::scoped_lock lock(this->session_event_mutex);
                this->event_queue[get_component_from_error(error).evse.value_or(ocpp::v2::EVSE{1}).id].push(error);
            }
        };

        auto fault_cleared_handler = [this, evse_id](const Everest::error::Error& error) {
            if (this->started) {
                const auto event_data = get_event_data(error, true, this->event_id_counter++);
                this->charge_point->on_event({event_data});
                this->charge_point->on_fault_cleared(evse_id, get_connector_id_from_error(error));
            } else {
                std::scoped_lock lock(this->session_event_mutex);
                this->event_queue[get_component_from_error(error).evse.value_or(ocpp::v2::EVSE{1}).id].push(error);
            }
        };

        // A permanent fault from the evse requirement indicates that the evse should move to faulted state
        evse->subscribe_error(EVSE_MANAGER_INOPERATIVE_ERROR, fault_handler, fault_cleared_handler);

        evse_id++;
    }

    int32_t extensions_id = 0;
    for (const auto& extension : this->r_extensions_15118) {
        extension->subscribe_iso15118_certificate_request(
            [this, extensions_id](const types::iso15118::RequestExiStreamSchema& certificate_request) {
                if (!this->started) {
                    EVLOG_info << "ISO15118 certificate_request received before OCPP was initialized, ignoring";
                    return;
                }
                auto ocpp_response = this->charge_point->on_get_15118_ev_certificate_request(
                    conversions::to_ocpp_get_15118_certificate_request(certificate_request));
                EVLOG_debug << "Received response from get_15118_ev_certificate_request: " << ocpp_response;
                // transform response, inject action, send to associated EvseManager
                types::iso15118::ResponseExiStreamStatus everest_response;
                everest_response.status = conversions::to_everest_iso15118_status(ocpp_response.status);
                everest_response.certificate_action = certificate_request.certificate_action;
                if (not ocpp_response.exiResponse.get().empty()) {
                    // since exi_response is an optional in the EVerest type we only set it when not empty
                    everest_response.exi_response = ocpp_response.exiResponse.get();
                }

                this->r_extensions_15118.at(extensions_id)->call_set_get_certificate_response(everest_response);
            });

        extension->subscribe_charging_needs([this,
                                             extensions_id](const types::iso15118::ChargingNeeds& charging_needs) {
            if (!this->started) {
                EVLOG_info << "ISO15118 charging_needs received before OCPP was initialized, ignoring";
                return;
            }
            const auto& mapping = this->r_extensions_15118.at(extensions_id)->get_mapping();
            if (mapping.has_value()) {
                try {
                    ocpp::v2::NotifyEVChargingNeedsRequest charge_needs;
                    charge_needs.chargingNeeds = conversions::to_ocpp_charging_needs(charging_needs);
                    charge_needs.evseId = mapping.value().evse;

                    this->charge_point->on_ev_charging_needs(charge_needs);
                } catch (const std::out_of_range& e) {
                    EVLOG_warning << "Could not convert iso15118 ChargingNeeds to OCPP NotifyEVChargingNeedsRequest: "
                                  << e.what();
                }
            } else {
                EVLOG_warning << "ISO15118 Extension interface mapping not set! Not sending 'ChargingNeeds'!";
            }
        });

        extension->subscribe_service_renegotiation_supported(
            [this, extensions_id](bool service_renegotiation_supported) {
                const auto& mapping = this->r_extensions_15118.at(extensions_id)->get_mapping();
                if (mapping.has_value()) {
                    this->evse_service_renegotiation_supported[mapping->evse] = service_renegotiation_supported;
                } else {
                    EVLOG_warning << "ISO15118 Extension interface mapping not set! Not retrieving 'Service "
                                     "Renegotiation Supported'!";
                }
            });

        extensions_id++;
    }
}

void OCPP201::process_session_event(const int32_t evse_id, const types::evse_manager::SessionEvent& session_event) {
    const auto connector_id = session_event.connector_id.value_or(1);
    std::lock_guard<std::mutex> lg(this->session_event_mutex);
    switch (session_event.event) {
    case types::evse_manager::SessionEventEnum::SessionStarted: {
        this->process_session_started(evse_id, connector_id, session_event);
        break;
    }
    case types::evse_manager::SessionEventEnum::SessionFinished: {
        this->process_session_finished(evse_id, connector_id, session_event);
        break;
    }
    case types::evse_manager::SessionEventEnum::TransactionStarted: {
        this->process_transaction_started(evse_id, connector_id, session_event);
        break;
    }
    case types::evse_manager::SessionEventEnum::TransactionFinished: {
        this->process_transaction_finished(evse_id, connector_id, session_event);
        break;
    }
    case types::evse_manager::SessionEventEnum::SessionResumed:
        this->process_session_resumed(evse_id, connector_id, session_event);
        break;
    case types::evse_manager::SessionEventEnum::ChargingStarted: {
        this->process_charging_started(evse_id, connector_id, session_event);
        break;
    }
    case types::evse_manager::SessionEventEnum::ChargingPausedEV: {
        this->process_charging_paused_ev(evse_id, connector_id, session_event);
        break;
    }
    case types::evse_manager::SessionEventEnum::ChargingPausedEVSE: {
        this->process_charging_paused_evse(evse_id, connector_id, session_event);
        break;
    }
    case types::evse_manager::SessionEventEnum::Disabled: {
        this->process_disabled(evse_id, connector_id, session_event);
        break;
    }
    case types::evse_manager::SessionEventEnum::Enabled: {
        this->process_enabled(evse_id, connector_id, session_event);
        break;
    }
    case types::evse_manager::SessionEventEnum::Authorized: {
        this->process_authorized(evse_id, connector_id, session_event);
        break;
    }
    case types::evse_manager::SessionEventEnum::Deauthorized: {
        this->process_deauthorized(evse_id, connector_id, session_event);
        break;
    }
    case types::evse_manager::SessionEventEnum::ReservationStart: {
        this->process_reserved(evse_id, connector_id);
        break;
    }
    case types::evse_manager::SessionEventEnum::ReservationEnd: {
        this->process_reservation_end(evse_id, connector_id);
        break;
    }
    // explicitly ignore the following session events for now
    // TODO(kai): implement
    case types::evse_manager::SessionEventEnum::AuthRequired:
    case types::evse_manager::SessionEventEnum::PrepareCharging:
    case types::evse_manager::SessionEventEnum::StoppingCharging:
    case types::evse_manager::SessionEventEnum::ChargingFinished:
    case types::evse_manager::SessionEventEnum::PluginTimeout:
    case types::evse_manager::SessionEventEnum::SwitchingPhases:
        break;
    }

    // process authorized event which will inititate a TransactionEvent(Updated) message in case the token has not
    // yet been authorized by the CSMS
    auto authorized_id_token = get_authorized_id_token(session_event);
    if (authorized_id_token.has_value()) {
        {
            auto evse_evcc_id_handle = this->evse_evcc_id.handle();
            if (!evse_evcc_id_handle->at(evse_id).empty()) {
                update_evcc_id_token(authorized_id_token.value(), evse_evcc_id_handle->at(evse_id),
                                     ocpp_protocol_version);
            }
        }
        this->charge_point->on_authorized(evse_id, connector_id, authorized_id_token.value());
    }
}

void OCPP201::process_tx_event_effect(const int32_t evse_id, const TxEventEffect tx_event_effect,
                                      const types::evse_manager::SessionEvent& session_event) {
    if (tx_event_effect == TxEventEffect::NONE) {
        return;
    }

    const auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
    if (transaction_data == nullptr) {
        throw std::runtime_error("Could not start transaction because no tranasaction data is present");
    }
    transaction_data->timestamp = ocpp_conversions::to_ocpp_datetime_or_now(session_event.timestamp);

    if (tx_event_effect == TxEventEffect::START_TRANSACTION) {
        transaction_data->started = true;
        transaction_data->meter_value = conversions::to_ocpp_meter_value(
            get_meter_value(session_event), ocpp::v2::ReadingContextEnum::Transaction_Begin,
            get_signed_meter_value(session_event));
        this->charge_point->on_transaction_started(
            evse_id, transaction_data->connector_id, transaction_data->session_id, transaction_data->timestamp,
            transaction_data->trigger_reason, transaction_data->meter_value, transaction_data->id_token,
            transaction_data->group_id_token, transaction_data->reservation_id, transaction_data->remote_start_id,
            transaction_data->charging_state);
    } else if (tx_event_effect == TxEventEffect::STOP_TRANSACTION) {
        transaction_data->meter_value = conversions::to_ocpp_meter_value(get_meter_value(session_event),
                                                                         ocpp::v2::ReadingContextEnum::Transaction_End,
                                                                         get_signed_meter_value(session_event));
        this->charge_point->on_transaction_finished(evse_id, transaction_data->timestamp, transaction_data->meter_value,
                                                    transaction_data->stop_reason, transaction_data->trigger_reason,
                                                    transaction_data->id_token, std::nullopt,
                                                    transaction_data->charging_state);
        this->transaction_handler->reset_transaction_data(evse_id);
    }
}

void OCPP201::process_session_started(const int32_t evse_id, const int32_t connector_id,
                                      const types::evse_manager::SessionEvent& session_event) {
    if (!session_event.session_started.has_value()) {
        throw std::runtime_error("SessionEvent SessionStarted does not contain session_started context");
    }

    const auto session_started = session_event.session_started.value();

    std::optional<ocpp::v2::IdToken> id_token = std::nullopt;
    std::optional<ocpp::v2::IdToken> group_id_token = std::nullopt;
    std::optional<int32_t> remote_start_id = std::nullopt;
    auto charging_state = ocpp::v2::ChargingStateEnum::Idle;
    auto trigger_reason = ocpp::v2::TriggerReasonEnum::Authorized;
    auto tx_event = TxEvent::AUTHORIZED;
    if (session_started.reason == types::evse_manager::StartSessionReason::EVConnected) {
        tx_event = TxEvent::EV_CONNECTED;
        trigger_reason = ocpp::v2::TriggerReasonEnum::CablePluggedIn;
        charging_state = ocpp::v2::ChargingStateEnum::EVConnected;
    } else if (!session_started.id_tag.has_value()) {
        EVLOG_warning << "Session started with reason Authorized, but no id_tag provided as part of the "
                         "session event";
    } else {
        id_token = conversions::to_ocpp_id_token(session_started.id_tag.value().id_token);
        auto evse_evcc_id_handle = this->evse_evcc_id.handle();
        if (!evse_evcc_id_handle->at(evse_id).empty()) {
            update_evcc_id_token(id_token.value(), evse_evcc_id_handle->at(evse_id), ocpp_protocol_version);
        }
        remote_start_id = session_started.id_tag.value().request_id;
        if (session_started.id_tag.value().parent_id_token.has_value()) {
            group_id_token = conversions::to_ocpp_id_token(session_started.id_tag.value().parent_id_token.value());
        }
        if (session_started.id_tag.value().authorization_type == types::authorization::AuthorizationType::OCPP) {
            trigger_reason = ocpp::v2::TriggerReasonEnum::RemoteStart;
        }
    }
    const auto timestamp = ocpp_conversions::to_ocpp_datetime_or_now(session_event.timestamp);
    const auto reservation_id = session_started.reservation_id;

    // this is always the first transaction related interaction, so we create TransactionData here
    auto transaction_data =
        std::make_shared<TransactionData>(connector_id, session_event.uuid, timestamp, trigger_reason, charging_state);
    transaction_data->id_token = id_token;
    transaction_data->group_id_token = group_id_token;
    transaction_data->remote_start_id = remote_start_id;
    transaction_data->reservation_id = reservation_id;
    this->transaction_handler->add_transaction_data(evse_id, transaction_data);

    const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, tx_event);
    this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
    if (session_started.reason == types::evse_manager::StartSessionReason::EVConnected) {
        this->charge_point->on_session_started(evse_id, connector_id);
    }
    if (tx_event == TxEvent::EV_CONNECTED) {
        this->everest_device_model_storage->update_connected_ev_available(evse_id, true);
    }
}

void OCPP201::process_session_finished(const int32_t evse_id, const int32_t connector_id,
                                       const types::evse_manager::SessionEvent& session_event) {
    this->evse_soc_map.handle()->at(evse_id).reset();
    auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
    if (transaction_data != nullptr) {
        transaction_data->charging_state = ocpp::v2::ChargingStateEnum::Idle;
        transaction_data->stop_reason = ocpp::v2::ReasonEnum::EVDisconnected;
        transaction_data->trigger_reason = ocpp::v2::TriggerReasonEnum::EVCommunicationLost;
    }
    const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::EV_DISCONNECTED);
    this->evse_evcc_id.handle()->at(evse_id) = "";
    this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
    this->charge_point->on_session_finished(evse_id, connector_id);
    this->everest_device_model_storage->update_connected_ev_available(evse_id, false);
}

void OCPP201::process_transaction_started(const int32_t evse_id, const int32_t connector_id,
                                          const types::evse_manager::SessionEvent& session_event) {
    if (!session_event.transaction_started.has_value()) {
        throw std::runtime_error("SessionEvent TransactionStarted does not contain session_started context");
    }

    auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
    if (transaction_data == nullptr) {
        EVLOG_warning << "Could not update transaction data because no transaction data is present. This might happen "
                         "in case a TxStopPoint is already active when a TransactionStarted event occurs (e.g. "
                         "TxStopPoint is EnergyTransfer or ParkingBayOccupied)";
        this->charge_point->on_session_started(evse_id, connector_id);
        auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::AUTHORIZED);
        this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
        tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::EV_CONNECTED);
        this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
        this->everest_device_model_storage->update_connected_ev_available(evse_id, true);
        return;
    }

    // at this point we dont know if the TransactionStarted event was triggered because of an Authorization or EV
    // Plug in event. We assume cable has been plugged in first and then authorized and update if other order was
    // applied
    auto tx_event = TxEvent::AUTHORIZED;
    auto trigger_reason = ocpp::v2::TriggerReasonEnum::Authorized;
    const auto transaction_started = session_event.transaction_started.value();
    if (transaction_started.reservation_id.has_value()) {
        transaction_data->reservation_id = transaction_started.reservation_id;
    }
    transaction_data->remote_start_id = transaction_started.id_tag.request_id;
    auto id_token = conversions::to_ocpp_id_token(transaction_started.id_tag.id_token);
    auto evse_evcc_id_handle = this->evse_evcc_id.handle();
    if (!evse_evcc_id_handle->at(evse_id).empty()) {
        update_evcc_id_token(id_token, evse_evcc_id_handle->at(evse_id), ocpp_protocol_version);
    }
    transaction_data->id_token = id_token;

    std::optional<ocpp::v2::IdToken> group_id_token = std::nullopt;
    if (transaction_started.id_tag.parent_id_token.has_value()) {
        transaction_data->group_id_token =
            conversions::to_ocpp_id_token(transaction_started.id_tag.parent_id_token.value());
    }

    // if session started reason was Authorized, Transaction is started because of EV plug in event
    if (transaction_data->trigger_reason == ocpp::v2::TriggerReasonEnum::Authorized or
        transaction_data->trigger_reason == ocpp::v2::TriggerReasonEnum::RemoteStart) {
        trigger_reason = ocpp::v2::TriggerReasonEnum::CablePluggedIn;
        transaction_data->charging_state = ocpp::v2::ChargingStateEnum::EVConnected;
        this->charge_point->on_session_started(evse_id, connector_id);
        tx_event = TxEvent::EV_CONNECTED;
    }

    if (transaction_started.id_tag.authorization_type == types::authorization::AuthorizationType::OCPP) {
        trigger_reason = ocpp::v2::TriggerReasonEnum::RemoteStart;
    }

    transaction_data->trigger_reason = trigger_reason;
    const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, tx_event);
    this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
    if (tx_event == TxEvent::EV_CONNECTED) {
        this->everest_device_model_storage->update_connected_ev_available(evse_id, true);
    }
}

void OCPP201::process_transaction_finished(const int32_t evse_id, const int32_t connector_id,
                                           const types::evse_manager::SessionEvent& session_event) {
    if (!session_event.transaction_finished.has_value()) {
        throw std::runtime_error("SessionEvent TransactionFinished does not contain transaction_finished context");
    }
    const auto transaction_finished = session_event.transaction_finished.value();
    auto tx_event = TxEvent::NONE;
    auto reason = ocpp::v2::ReasonEnum::Other;
    if (transaction_finished.reason.has_value()) {
        reason = conversions::to_ocpp_reason(transaction_finished.reason.value());
        tx_event = get_tx_event(reason);
    }
    auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
    if (transaction_data != nullptr) {
        std::optional<ocpp::v2::IdToken> id_token = std::nullopt;
        if (transaction_finished.id_tag.has_value()) {
            id_token = conversions::to_ocpp_id_token(transaction_finished.id_tag.value().id_token);
            auto evse_evcc_id_handle = this->evse_evcc_id.handle();
            if (!evse_evcc_id_handle->at(evse_id).empty()) {
                update_evcc_id_token(id_token.value(), evse_evcc_id_handle->at(evse_id), ocpp_protocol_version);
            }
        }

        // this is required to report the correct charging_state within a TransactionEvent(Ended) message
        auto charging_state = transaction_data->charging_state;
        if (reason == ocpp::v2::ReasonEnum::EVDisconnected) {
            charging_state = ocpp::v2::ChargingStateEnum::Idle;
        } else if (reason == ocpp::v2::ReasonEnum::ImmediateReset &&
                   charging_state != ocpp::v2::ChargingStateEnum::Idle) {
            charging_state = ocpp::v2::ChargingStateEnum::EVConnected;
        } else if (tx_event == TxEvent::DEAUTHORIZED) {
            charging_state = ocpp::v2::ChargingStateEnum::EVConnected;
        }
        transaction_data->trigger_reason = stop_reason_to_trigger_reason_enum(reason);
        transaction_data->stop_reason = reason;
        transaction_data->id_token = id_token;
        transaction_data->charging_state = charging_state;
    }

    // tx_event could be DEAUTHORIZED or EV_DISCONNECTED
    const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, tx_event);
    this->process_tx_event_effect(evse_id, tx_event_effect, session_event);

    if (tx_event == TxEvent::DEAUTHORIZED) {
        if (reason == ocpp::v2::ReasonEnum::Remote) {
            this->charge_point->on_charging_state_changed(evse_id, ocpp::v2::ChargingStateEnum::EVConnected,
                                                          ocpp::v2::TriggerReasonEnum::RemoteStop);
        } else {
            this->charge_point->on_charging_state_changed(evse_id, ocpp::v2::ChargingStateEnum::EVConnected,
                                                          ocpp::v2::TriggerReasonEnum::StopAuthorized);
        }
    } else {
        // TODO(piet): If StopTxOnEVSideDisconnect is false, authorization shall still be present. This cannot only
        // be handled within this module, but probably also within EvseManager and Auth

        // authorization is always withdrawn in case of TransactionFinished, so in case we haven't updated the
        // transaction handler yet, we have to do it
        // now
        const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::DEAUTHORIZED);
        this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
    }
}

void OCPP201::process_session_resumed(const int32_t evse_id, const int32_t connector_id,
                                      const types::evse_manager::SessionEvent& session_event) {
    // resume transaction with data we get from the session event
    // currently, the SessionResumed event only occurs after a power outage followed by
    // a TransactionFinished event. We have to add the transaction data again to be able to
    // properly process the transaction finished event.
    // Currently, sending a TransactionEvent(Ended) after a power loss is only supported if
    // the configuration variable InternalCtrlr::ResumeTransactionsOnBoot is set to true.
    // If this is not the case, libocpp will not be able to process a TransactionFinished event
    // after a power loss, because it does internally not restore transaction data on boot.
    const auto timestamp = ocpp_conversions::to_ocpp_datetime_or_now(session_event.timestamp);
    auto transaction_data =
        std::make_shared<TransactionData>(connector_id, session_event.uuid, timestamp,
                                          ocpp::v2::TriggerReasonEnum::TxResumed, ocpp::v2::ChargingStateEnum::Idle);
    transaction_data->started = true;
    this->transaction_handler->add_transaction_data(evse_id, transaction_data);
}

void OCPP201::process_charging_started(const int32_t evse_id, const int32_t connector_id,
                                       const types::evse_manager::SessionEvent& session_event) {
    auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
    if (transaction_data != nullptr) {
        transaction_data->trigger_reason = ocpp::v2::TriggerReasonEnum::ChargingStateChanged;
        transaction_data->charging_state = ocpp::v2::ChargingStateEnum::Charging;
    }
    const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::ENERGY_TRANSFER_STARTED);
    this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
    this->charge_point->on_charging_state_changed(evse_id, ocpp::v2::ChargingStateEnum::Charging);
}

void OCPP201::process_charging_paused_ev(const int32_t evse_id, const int32_t connector_id,
                                         const types::evse_manager::SessionEvent& session_event) {
    auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
    if (transaction_data != nullptr) {
        transaction_data->charging_state = ocpp::v2::ChargingStateEnum::SuspendedEV;
        transaction_data->trigger_reason = ocpp::v2::TriggerReasonEnum::ChargingStateChanged;
        transaction_data->stop_reason = ocpp::v2::ReasonEnum::StoppedByEV;
    }
    const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::ENERGY_TRANSFER_STOPPED);
    this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
    this->charge_point->on_charging_state_changed(evse_id, ocpp::v2::ChargingStateEnum::SuspendedEV);
}

void OCPP201::process_charging_paused_evse(const int32_t evse_id, const int32_t connector_id,
                                           const types::evse_manager::SessionEvent& session_event) {
    auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
    auto trigger_reason = ocpp::v2::TriggerReasonEnum::ChargingStateChanged;
    if (transaction_data != nullptr) {
        transaction_data->charging_state = ocpp::v2::ChargingStateEnum::SuspendedEVSE;
        if (transaction_data->stop_reason == ocpp::v2::ReasonEnum::Remote) {
            trigger_reason = ocpp::v2::TriggerReasonEnum::RemoteStop;
            transaction_data->trigger_reason = ocpp::v2::TriggerReasonEnum::ChargingStateChanged;
        }
    }
    const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::ENERGY_TRANSFER_STOPPED);
    this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
    this->charge_point->on_charging_state_changed(evse_id, ocpp::v2::ChargingStateEnum::SuspendedEVSE, trigger_reason);
}

void OCPP201::process_enabled(const int32_t evse_id, const int32_t connector_id,
                              const types::evse_manager::SessionEvent& session_event) {
    this->charge_point->on_enabled(evse_id, connector_id);
}

void OCPP201::process_disabled(const int32_t evse_id, const int32_t connector_id,
                               const types::evse_manager::SessionEvent& session_event) {
    this->charge_point->on_unavailable(evse_id, connector_id);
}

void OCPP201::process_authorized(const int32_t evse_id, const int32_t connector_id,
                                 const types::evse_manager::SessionEvent& session_event) {
    // currently handled as part of SessionStarted and TransactionStarted events
}

void OCPP201::process_deauthorized(const int32_t evse_id, const int32_t connector_id,
                                   const types::evse_manager::SessionEvent& session_event) {
    auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
    if (transaction_data != nullptr) {
        transaction_data->trigger_reason = ocpp::v2::TriggerReasonEnum::StopAuthorized;
    }
    const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::DEAUTHORIZED);
    this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
}

void OCPP201::process_reserved(const int32_t evse_id, const int32_t connector_id) {
    this->charge_point->on_reserved(evse_id, connector_id);
}

void OCPP201::process_reservation_end(const int32_t evse_id, const int32_t connector_id) {
    this->charge_point->on_reservation_cleared(evse_id, connector_id);
}

void OCPP201::publish_charging_schedules(const std::vector<ocpp::v2::EnhancedCompositeSchedule>& composite_schedules) {
    const auto everest_schedules = conversions::to_everest_charging_schedules(composite_schedules);
    this->p_ocpp_generic->publish_charging_schedules(everest_schedules);
}

void OCPP201::set_external_limits(const std::vector<ocpp::v2::EnhancedCompositeSchedule>& composite_schedules) {
    const auto start_time = ocpp::DateTime();

    auto to_timestamp = [&](int seconds_offset) {
        return ocpp::DateTime(start_time.to_time_point() + std::chrono::seconds(seconds_offset)).to_rfc3339();
    };

    int32_t setpoint_priority = LOWEST_SETPOINT_PRIORITY;
    const auto resp = this->charge_point->request_value<std::string>(ocpp::v2::ControllerComponents::SmartChargingCtrlr,
                                                                     ocpp::v2::Variable{SETPOINT_PRIORITY_VAR_NAME},
                                                                     ocpp::v2::AttributeEnum::Actual);

    if (resp.status == ocpp::v2::GetVariableStatusEnum::Accepted && resp.value.has_value()) {
        setpoint_priority = resp.value.value() == "CSMS" ? HIGHEST_SETPOINT_PRIORITY : LOWEST_SETPOINT_PRIORITY;
    }

    auto create_setpoint_entry =
        [&](const std::string& timestamp, const ocpp::v2::EnhancedChargingSchedulePeriod& period,
            const ocpp::v2::ChargingRateUnitEnum& unit) -> std::optional<types::energy::ScheduleSetpointEntry> {
        const bool has_basic_setpoint = period.setpoint.has_value();
        const bool has_freq_table = period.v2xFreqWattCurve.has_value() && !period.v2xFreqWattCurve->empty();

        if (!has_basic_setpoint && !has_freq_table) {
            return std::nullopt;
        }

        types::energy::ScheduleSetpointEntry entry;
        types::energy::SetpointType setpoint;
        setpoint.source = SETPOINT_SOURCE;
        setpoint.priority = setpoint_priority;
        entry.timestamp = timestamp;

        if (has_basic_setpoint) {
            if (unit == ocpp::v2::ChargingRateUnitEnum::A) {
                setpoint.ac_current_A = period.setpoint.value();
            } else {
                setpoint.total_power_W = period.setpoint.value();
            }
        }

        if (has_freq_table) {
            std::vector<types::energy::FrequencyWattPoint> frequency_table;
            for (const auto& point : period.v2xFreqWattCurve.value()) {
                types::energy::FrequencyWattPoint freq_point;
                freq_point.frequency_Hz = point.frequency;
                freq_point.total_power_W = point.power;
                frequency_table.push_back(freq_point);
            }
            setpoint.frequency_table = std::move(frequency_table);
        }

        entry.setpoint = std::move(setpoint);
        return entry;
    };

    auto create_limits_entry =
        [&](const std::string& timestamp, const ocpp::v2::EnhancedChargingSchedulePeriod& period,
            const ocpp::v2::ChargingRateUnitEnum& unit) -> std::optional<types::energy::ScheduleReqEntry> {
        if (!period.limit.has_value()) {
            return std::nullopt;
        }

        types::energy::ScheduleReqEntry entry;
        entry.timestamp = timestamp;

        types::energy::LimitsReq limits_req;
        if (unit == ocpp::v2::ChargingRateUnitEnum::A) {
            limits_req.ac_max_current_A = {period.limit.value(), source_ext_limit};
            if (period.numberPhases.has_value()) {
                limits_req.ac_max_phase_count = {period.numberPhases.value(), source_ext_limit};
            }
        } else {
            limits_req.total_power_W = {period.limit.value(), source_ext_limit};
        }

        entry.limits_to_leaves = limits_req;
        return entry;
    };

    for (const auto& composite_schedule : composite_schedules) {
        auto evse_id = composite_schedule.evseId;
        if (not external_energy_limits::is_evse_sink_configured(this->r_evse_energy_sink, evse_id)) {
            EVLOG_warning << "Can not apply external limits! No evse energy sink configured for evse_id: " << evse_id;
            continue;
        }

        types::energy::ExternalLimits limits;
        std::vector<types::energy::ScheduleReqEntry> schedule_import;
        std::vector<types::energy::ScheduleSetpointEntry> schedule_setpoints;

        const auto& unit = composite_schedule.chargingRateUnit;

        for (const auto& period : composite_schedule.chargingSchedulePeriod) {
            const auto timestamp = to_timestamp(period.startPeriod);

            if (auto setpoint_entry = create_setpoint_entry(timestamp, period, unit)) {
                schedule_setpoints.push_back(*setpoint_entry);
            }

            if (auto limits_entry = create_limits_entry(timestamp, period, unit)) {
                schedule_import.push_back(*limits_entry);
            }
        }

        limits.schedule_import = std::move(schedule_import);
        limits.schedule_setpoints = std::move(schedule_setpoints);

        auto& evse_sink = external_energy_limits::get_evse_sink_by_evse_id(this->r_evse_energy_sink, evse_id);
        evse_sink.call_set_external_limits(limits);
    }
}

} // namespace module
