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

#include <cmath>
#include <fstream>
#include <memory>
#include <optional>
#include <sstream>
#include <string>

#include "generated/types/ocpp.hpp"
#include "ocpp/common/types.hpp"
#include "ocpp/v16/charge_point_configuration.hpp"
#include "ocpp/v16/types.hpp"
#include <everest/conversions/ocpp/ocpp_conversions.hpp>
#include <fmt/core.h>

#include <conversions.hpp>
#include <error_mapping.hpp>
#include <everest/conversions/ocpp/evse_security_ocpp.hpp>
#include <everest/external_energy_limits/external_energy_limits.hpp>

namespace module {

// helper type for visitor
template <class... Ts> struct overloaded : Ts... {
    using Ts::operator()...;
};
template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

const std::string CERTS_SUB_DIR = "certs";
const std::string SQL_CORE_MIGRTATIONS = "core_migrations";
const std::string INOPERATIVE_ERROR_TYPE = "evse_manager/Inoperative";
const std::string SWITCHING_PHASES_REASON = "SwitchingPhases";
const ocpp::CiString<50> CONNECTION_TIMEOUT_CONFIG_KEY = "ConnectionTimeout";
const ocpp::CiString<50> ISO15118_PNC_ENABLED_CONFIG_KEY = "ISO15118PnCEnabled";
const ocpp::CiString<50> CENTRAL_CONTRACT_VALIDATION_ALLOWED_CONFIG_KEY = "CentralContractValidationAllowed";
const std::string OCPP_VERSION = "1.6";

namespace fs = std::filesystem;

/// \brief Converts the given \p error into the ErrorInfo that contains all
/// necessary data for a StatusNotification.req
static ocpp::v16::ErrorInfo get_error_info(const Everest::error::Error& error) {

    const auto error_type = error.type;
    const auto uuid = error.uuid.uuid;

    auto mrec_it = std::find_if(MREC_ERROR_MAP.begin(), MREC_ERROR_MAP.end(),
                                [&](const auto& entry) { return error_type.find(entry.first) != std::string::npos; });

    // is MREC error
    if (mrec_it != MREC_ERROR_MAP.end()) {
        // lambda to create MREC error info
        auto make_mrec_error_info = [&](ocpp::v16::ChargePointErrorCode code, const std::string& vendor_error_code) {
            return ocpp::v16::ErrorInfo{uuid, code, false, std::nullopt, CHARGE_X_MREC_VENDOR_ID, vendor_error_code};
        };
        return make_mrec_error_info(mrec_it->second.first, mrec_it->second.second);
    }

    auto ocpp_it = std::find_if(OCPP_ERROR_MAP.begin(), OCPP_ERROR_MAP.end(),
                                [&](const auto& entry) { return error_type.find(entry.first) != std::string::npos; });

    // is OCPP error
    if (ocpp_it != OCPP_ERROR_MAP.end()) {
        // lambda to create OCPP error info
        auto make_ocpp_error_info = [&](ocpp::v16::ChargePointErrorCode code) {
            return ocpp::v16::ErrorInfo{uuid, code, false, std::nullopt};
        };
        return make_ocpp_error_info(ocpp_it->second);
    }

    if (error_type == INOPERATIVE_ERROR_TYPE) {
        // clang-format off
        return ocpp::v16::ErrorInfo{uuid,
                                    ocpp::v16::ChargePointErrorCode::OtherError,
                                    true,
                                    "caused_by:" + error.message,
                                    error.vendor_id,
                                    error.description};
        // clang-format on
    }

    const auto get_simplified_error_type = [](const std::string& error_type) {
        // this function should return everything after the first '/'
        // delimiter - if there is no delimiter or the delimiter is at
        // the end, it should return the input itself
        static constexpr auto TYPE_INTERFACE_DELIMITER = '/';

        auto input = std::istringstream(error_type);
        std::string tmp;

        // move right after the first delimiter
        std::getline(input, tmp, TYPE_INTERFACE_DELIMITER);

        if (!input) {
            // no delimiter found or delimiter at the end
            return error_type;
        }

        // get the rest of the input
        std::getline(input, tmp);

        return tmp;
    };

    const auto is_fault = [](const Everest::error::Error&) {
        // NOTE (aw): this could be customized, depending on the error
        return false;
    };

    static constexpr auto TYPE_DELIMITER = '/';

    return {
        uuid,
        ocpp::v16::ChargePointErrorCode::OtherError,
        is_fault(error),
        error.origin.to_string(),                                                // info
        error.message,                                                           // vendor id
        get_simplified_error_type(error.type) + TYPE_DELIMITER + error.sub_type, // vendor error code
    };
}

void create_empty_user_config(const fs::path& user_config_path) {
    if (fs::exists(user_config_path.parent_path())) {
        std::ofstream fs(user_config_path.c_str());
        auto user_config = json::object();
        fs << user_config << std::endl;
        fs.close();
    } else {
        EVLOG_AND_THROW(
            std::runtime_error(fmt::format("Provided UserConfigPath is invalid: {}", user_config_path.string())));
    }
}

void OCPP::set_external_limits(const std::map<int32_t, ocpp::v16::EnhancedChargingSchedule>& charging_schedules) {
    const auto start_time = ocpp::DateTime();

    // iterate over all schedules reported by the libocpp to create ExternalLimits
    // for each connector
    for (auto const& [connector_id, schedule] : charging_schedules) {

        if (not external_energy_limits::is_evse_sink_configured(this->r_evse_energy_sink, connector_id)) {
            EVLOG_warning << "Can not apply external limits! No evse energy sink "
                             "configured for evse_id: "
                          << connector_id;
            continue;
        }

        types::energy::ExternalLimits limits;
        std::vector<types::energy::ScheduleReqEntry> schedule_import;
        for (const auto period : schedule.chargingSchedulePeriod) {
            types::energy::ScheduleReqEntry schedule_req_entry;
            types::energy::LimitsReq limits_req;
            const auto timestamp = start_time.to_time_point() + std::chrono::seconds(period.startPeriod);
            schedule_req_entry.timestamp = ocpp::DateTime(timestamp).to_rfc3339();
            if (period.numberPhases.has_value()) {
                limits_req.ac_max_phase_count = {period.numberPhases.value(), source_ext_limit};
            }
            if (schedule.chargingRateUnit == ocpp::v16::ChargingRateUnit::A) {
                limits_req.ac_max_current_A = {period.limit, source_ext_limit};
                if (schedule.minChargingRate.has_value()) {
                    limits_req.ac_min_current_A = {schedule.minChargingRate.value(), source_ext_limit};
                }
            } else {
                limits_req.total_power_W = {period.limit, source_ext_limit};
            }
            schedule_req_entry.limits_to_leaves = limits_req;
            schedule_import.push_back(schedule_req_entry);
        }
        limits.schedule_import = schedule_import;
        auto& evse_sink = external_energy_limits::get_evse_sink_by_evse_id(this->r_evse_energy_sink, connector_id);
        evse_sink.call_set_external_limits(limits);
    }
}

void OCPP::publish_charging_schedules(
    const std::map<int32_t, ocpp::v16::EnhancedChargingSchedule>& charging_schedules) {
    // publish the schedule over mqtt
    types::ocpp::ChargingSchedules schedules;
    for (const auto& charging_schedule : charging_schedules) {
        types::ocpp::ChargingSchedule sch = conversions::to_charging_schedule(charging_schedule.second);
        sch.evse = charging_schedule.first;
        schedules.schedules.emplace_back(std::move(sch));
    }
    this->p_ocpp_generic->publish_charging_schedules(schedules);
}

void OCPP::process_session_event(int32_t evse_id, const types::evse_manager::SessionEvent& session_event) {
    auto everest_connector_id = session_event.connector_id.value_or(1);
    auto ocpp_connector_id = this->evse_connector_map[evse_id][everest_connector_id];

    if (session_event.event == types::evse_manager::SessionEventEnum::Enabled) {
        this->charge_point->on_enabled(evse_id);
    } else if (session_event.event == types::evse_manager::SessionEventEnum::Disabled) {
        EVLOG_debug << "EVSE#" << evse_id << ": "
                    << "Received Disabled";
        this->charge_point->on_disabled(evse_id);
    } else if (session_event.event == types::evse_manager::SessionEventEnum::TransactionStarted) {
        EVLOG_info << "EVSE#" << evse_id << ": "
                   << "Received TransactionStarted";
        const auto transaction_started = session_event.transaction_started.value();

        const auto timestamp = ocpp_conversions::to_ocpp_datetime_or_now(session_event.timestamp);
        const auto energy_Wh_import = transaction_started.meter_value.energy_Wh_import.total;
        const auto session_id = session_event.uuid;
        const auto id_token = transaction_started.id_tag.id_token.value;
        const auto signed_meter_value = transaction_started.signed_meter_value;
        std::optional<int32_t> reservation_id_opt = std::nullopt;
        if (transaction_started.reservation_id) {
            reservation_id_opt.emplace(transaction_started.reservation_id.value());
        }
        std::optional<std::string> signed_meter_data;
        if (signed_meter_value.has_value()) {
            // there is no specified way of transmitting signing method, encoding
            // method and public key this has to be negotiated beforehand or done in a
            // custom data transfer
            signed_meter_data.emplace(signed_meter_value.value().signed_meter_data);
        }
        this->charge_point->on_transaction_started(ocpp_connector_id, session_event.uuid, id_token, energy_Wh_import,
                                                   reservation_id_opt, timestamp, signed_meter_data);
    } else if (session_event.event == types::evse_manager::SessionEventEnum::ChargingPausedEV) {
        EVLOG_debug << "Connector#" << ocpp_connector_id << ": "
                    << "Received ChargingPausedEV";
        this->charge_point->on_suspend_charging_ev(ocpp_connector_id);
    } else if (session_event.event == types::evse_manager::SessionEventEnum::ChargingPausedEVSE) {
        EVLOG_debug << "Connector#" << ocpp_connector_id << ": "
                    << "Received ChargingPausedEVSE";
        this->charge_point->on_suspend_charging_evse(ocpp_connector_id);
    } else if (session_event.event == types::evse_manager::SessionEventEnum::SwitchingPhases) {
        EVLOG_debug << "Connector#" << ocpp_connector_id << ": "
                    << "Received SwitchingPhases";
        this->charge_point->on_suspend_charging_evse(ocpp_connector_id, SWITCHING_PHASES_REASON);
    } else if (session_event.event == types::evse_manager::SessionEventEnum::ChargingStarted) {
        EVLOG_debug << "Connector#" << ocpp_connector_id << ": "
                    << "Received ChargingStarted";
        this->charge_point->on_resume_charging(ocpp_connector_id);
    } else if (session_event.event == types::evse_manager::SessionEventEnum::TransactionFinished) {
        EVLOG_debug << "Connector#" << ocpp_connector_id << ": "
                    << "Received TransactionFinished";

        const auto transaction_finished = session_event.transaction_finished.value();
        const auto timestamp = ocpp_conversions::to_ocpp_datetime_or_now(session_event.timestamp);
        const auto energy_Wh_import = transaction_finished.meter_value.energy_Wh_import.total;
        const auto signed_meter_value = transaction_finished.signed_meter_value;

        auto reason = ocpp::v16::Reason::Other;
        if (transaction_finished.reason.has_value()) {
            reason = conversions::to_ocpp_reason(transaction_finished.reason.value());
        }
        std::optional<ocpp::CiString<20>> id_tag_opt = std::nullopt;
        if (transaction_finished.id_tag.has_value()) {
            // we truncate potentially too large tokens
            id_tag_opt.emplace(
                ocpp::CiString<20>(transaction_finished.id_tag.value().id_token.value, ocpp::StringTooLarge::Truncate));
        }
        std::optional<std::string> signed_meter_data;
        if (signed_meter_value.has_value()) {
            // there is no specified way of transmitting signing method, encoding
            // method and public key this has to be negotiated beforehand or done in a
            // custom data transfer
            signed_meter_data.emplace(signed_meter_value.value().signed_meter_data);
        }
        this->charge_point->on_transaction_stopped(ocpp_connector_id, session_event.uuid, reason, timestamp,
                                                   energy_Wh_import, id_tag_opt, signed_meter_data);
        // always triggered by libocpp
    } else if (session_event.event == types::evse_manager::SessionEventEnum::SessionStarted) {
        EVLOG_info << "Connector#" << ocpp_connector_id << ": "
                   << "Received SessionStarted";
        // ev side disconnect
        auto session_started = session_event.session_started.value();
        this->charge_point->on_session_started(ocpp_connector_id, session_event.uuid,
                                               conversions::to_ocpp_session_started_reason(session_started.reason),
                                               session_started.logging_path);
    } else if (session_event.event == types::evse_manager::SessionEventEnum::SessionFinished) {
        EVLOG_debug << "Connector#" << ocpp_connector_id << ": "
                    << "Received SessionFinished";
        // ev side disconnect
        this->evse_soc_map.handle()->at(evse_id).reset();
        this->charge_point->on_session_stopped(ocpp_connector_id, session_event.uuid);
    } else if (session_event.event == types::evse_manager::SessionEventEnum::ReservationStart) {
        this->charge_point->on_reservation_start(ocpp_connector_id);
    } else if (session_event.event == types::evse_manager::SessionEventEnum::ReservationEnd) {
        this->charge_point->on_reservation_end(ocpp_connector_id);
    } else if (session_event.event == types::evse_manager::SessionEventEnum::PluginTimeout) {
        this->charge_point->on_plugin_timeout(ocpp_connector_id);
    }
}

void OCPP::init_evse_subscriptions() {
    int32_t evse_id = 1;
    for (auto& evse : this->r_evse_manager) {
        evse->subscribe_powermeter([this, evse_id](types::powermeter::Powermeter powermeter) {
            ocpp::Measurement measurement;
            measurement.power_meter = conversions::to_ocpp_power_meter(powermeter);
            auto evse_soc_map_handle = this->evse_soc_map.handle();
            if (evse_soc_map_handle->at(evse_id).has_value()) {
                // soc is present, so add this to the measurement
                measurement.soc_Percent = ocpp::StateOfCharge{evse_soc_map_handle->at(evse_id).value()};
            }
            if (powermeter.temperatures.has_value()) {
                measurement.temperature_C = conversions::to_ocpp_temperatures(powermeter.temperatures.value());
            }
            this->charge_point->on_meter_values(evse_id, measurement);
        });

        evse->subscribe_ev_info([this, evse_id](const types::evse_manager::EVInfo& ev_info) {
            if (ev_info.soc.has_value()) {
                this->evse_soc_map.handle()->at(evse_id) = ev_info.soc.value();
            }
        });

        evse->subscribe_enforced_limits([this, evse_id](types::energy::EnforcedLimits limits) {
            if (limits.limits_root_side.total_power_W.has_value()) {
                int32_t max_power = std::floor(limits.limits_root_side.total_power_W->value);
                this->charge_point->on_max_power_offered(evse_id, max_power);
            }
            if (limits.limits_root_side.ac_max_current_A.has_value()) {
                int32_t max_current = std::floor(limits.limits_root_side.ac_max_current_A->value);
                this->charge_point->on_max_current_offered(evse_id, max_current);
            }
        });

        evse->subscribe_session_event([this, evse_id](types::evse_manager::SessionEvent session_event) {
            std::lock_guard<std::mutex> lg(this->event_mutex);
            if (this->ocpp_stopped) {
                // dont call any on handler in case ocpp is stopped
                return;
            }

            if (session_event.event == types::evse_manager::SessionEventEnum::SessionResumed) {
                this->resuming_session_ids.insert(session_event.uuid);
                return;
            }

            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;
                this->event_queue.emplace(evse_id, session_event);
                return;
            }

            this->process_session_event(evse_id, session_event);
        });

        evse->subscribe_powermeter_public_key_ocmf([this, evse_id](std::string public_key_ocmf) {
            std::lock_guard<std::mutex> lg(this->event_mutex);
            if (!this->started) {
                this->event_queue.emplace(evse_id, PowermeterPublicKey{public_key_ocmf});
                return;
            }

            if (!this->charge_point->set_powermeter_public_key(evse_id, public_key_ocmf)) {
                EVLOG_error << "Failed to set powermeter public key for evse_id: " << evse_id;
            }
        });

        evse_id++;
    }

    int32_t extensions_id = 1;
    for (auto& extension : this->r_extensions_15118) {
        extension->subscribe_iso15118_certificate_request(
            [this, extensions_id](types::iso15118::RequestExiStreamSchema request) {
                this->charge_point->data_transfer_pnc_get_15118_ev_certificate(
                    extensions_id, request.exi_request, request.iso15118_schema_version,
                    conversions::to_ocpp_certificate_action_enum(request.certificate_action));
            });
        extensions_id++;
    }
}

void OCPP::init_evse_connector_map() {
    int32_t ocpp_connector_id = 1; // this represents the OCPP connector id
    int32_t evse_id = 1;           // this represents the evse id of EVerests evse manager
    for (const auto& evse : this->r_evse_manager) {
        const auto _evse = evse->call_get_evse();
        std::map<int32_t, int32_t> connector_map; // maps EVerest connector_id to OCPP connector_id

        if (_evse.id != evse_id) {
            throw std::runtime_error("Configured evse_id(s) must be starting with 1 counting upwards");
        }
        for (const auto& connector : _evse.connectors) {
            connector_map[connector.id] = ocpp_connector_id;
            this->connector_evse_index_map[ocpp_connector_id] =
                evse_id - 1; // - 1 to specify the index for r_evse_manager
            ocpp_connector_id++;
        }

        if (connector_map.size() == 0) {
            this->connector_evse_index_map[ocpp_connector_id] =
                evse_id - 1; // - 1 to specify the index for r_evse_manager
            connector_map[1] = ocpp_connector_id;
            ocpp_connector_id++;
        }

        this->evse_connector_map[_evse.id] = connector_map;
        evse_id++;
    }
}

void OCPP::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;
        }
    }
}

void OCPP::init_module_configuration() {
    std::vector<ocpp::CiString<50>> keys;
    ocpp::v16::GetConfigurationRequest req;
    keys.push_back(CONNECTION_TIMEOUT_CONFIG_KEY);
    keys.push_back(ISO15118_PNC_ENABLED_CONFIG_KEY);
    keys.push_back(CENTRAL_CONTRACT_VALIDATION_ALLOWED_CONFIG_KEY);
    req.key = keys;
    const auto res = this->charge_point->get_configuration_key(req);

    if (!res.configurationKey.has_value()) {
        return;
    }

    for (const auto& kv : res.configurationKey.value()) {
        this->handle_config_key(kv);
    }
}

void OCPP::handle_config_key(const ocpp::v16::KeyValue& kv) {
    if (!kv.value.has_value()) {
        return;
    }

    const auto set_pnc_config = [this](const types::evse_manager::PlugAndChargeConfiguration& pnc_config) {
        for (const auto& evse_manager : this->r_evse_manager) {
            evse_manager->call_set_plug_and_charge_configuration(pnc_config);
        }
    };

    if (kv.key == CONNECTION_TIMEOUT_CONFIG_KEY and kv.value.has_value()) {
        try {
            this->r_auth->call_set_connection_timeout(std::stoi(kv.value.value().get()));
        } catch (...) {
            EVLOG_warning << "Could not set ConnectionTimeout";
        }
    } else if (kv.key == ISO15118_PNC_ENABLED_CONFIG_KEY and kv.value.has_value()) {
        types::evse_manager::PlugAndChargeConfiguration pnc_config;
        pnc_config.pnc_enabled = ocpp::conversions::string_to_bool(kv.value.value());
        set_pnc_config(pnc_config);
    } else if (kv.key == CENTRAL_CONTRACT_VALIDATION_ALLOWED_CONFIG_KEY and kv.value.has_value()) {
        types::evse_manager::PlugAndChargeConfiguration pnc_config;
        pnc_config.central_contract_validation_allowed = ocpp::conversions::string_to_bool(kv.value.value());
        set_pnc_config(pnc_config);
    }
}

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

void OCPP::init() {
    invoke_init(*p_main);
    invoke_init(*p_ocpp_generic);
    invoke_init(*p_auth_validator);
    invoke_init(*p_auth_provider);
    invoke_init(*p_data_transfer);

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

    const auto error_handler = [this](const Everest::error::Error& error) {
        std::lock_guard<std::mutex> lg(this->event_mutex);
        const auto evse_id = error.origin.mapping.has_value() ? error.origin.mapping.value().evse : 0;
        if (this->started) {
            const auto error_info = get_error_info(error);
            this->charge_point->on_error(evse_id, error_info);
        } else {
            this->event_queue.emplace(evse_id, ErrorRaised{error});
        }
    };

    const auto error_cleared_handler = [this](const Everest::error::Error& error) {
        std::lock_guard<std::mutex> lg(this->event_mutex);
        const auto evse_id = error.origin.mapping.has_value() ? error.origin.mapping.value().evse : 0;
        if (this->started) {
            this->charge_point->on_error_cleared(evse_id, error.uuid.uuid);
        } else {
            this->event_queue.emplace(evse_id, ErrorCleared{error});
        }
    };

    subscribe_global_all_errors(error_handler, error_cleared_handler);

    this->init_evse_maps();

    for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) {
        this->r_evse_manager.at(evse_id - 1)->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();
            }
        });

        // also use the the ready signal, TODO(kai): maybe warn about it's usage
        // here`
        this->r_evse_manager.at(evse_id - 1)->subscribe_ready([this, evse_id](bool ready) {
            if (ready) {
                {
                    auto ready_handle = this->evse_ready_map.handle();
                    if (!ready_handle->at(evse_id)) {
                        EVLOG_error << "Received EVSE ready without receiving "
                                       "waiting_for_external_ready first, this is "
                                       "probably a bug in your evse_manager "
                                       "implementation / configuration. evse_id: "
                                    << evse_id;
                    }
                    ready_handle->at(evse_id) = true;
                }
                this->evse_ready_map.notify_one();
            }
        });
    }

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

    auto configured_config_path = fs::path(this->config.ChargePointConfigPath);

    // try to find the config file if it has been provided as a relative path
    if (!fs::exists(configured_config_path) && configured_config_path.is_relative()) {
        configured_config_path = this->ocpp_share_path / configured_config_path;
    }
    if (!fs::exists(configured_config_path)) {
        EVLOG_AND_THROW(Everest::EverestConfigError(
            fmt::format("OCPP config file is not available at given path: {} which was "
                        "resolved to: {}",
                        this->config.ChargePointConfigPath, configured_config_path.string())));
    }
    const auto config_path = configured_config_path;
    EVLOG_info << "OCPP config: " << config_path.string();

    auto configured_user_config_path = fs::path(this->config.UserConfigPath);
    // try to find the user config file if it has been provided as a relative path
    if (!fs::exists(configured_user_config_path) && configured_user_config_path.is_relative()) {
        configured_user_config_path = this->ocpp_share_path / configured_user_config_path;
    }
    const auto user_config_path = configured_user_config_path;
    EVLOG_info << "OCPP user config: " << user_config_path.string();

    std::ifstream ifs(config_path.c_str());
    std::string config_file((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
    auto json_config = json::parse(config_file);
    json_config.at("Core").at("NumberOfConnectors") = this->r_evse_manager.size();

    if (fs::exists(user_config_path)) {
        std::ifstream ifs(user_config_path.c_str());
        std::string user_config_file((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));

        try {
            const auto user_config = json::parse(user_config_file);
            EVLOG_info << "Augmenting chargepoint config with user_config entries";
            json_config.merge_patch(user_config);
        } catch (const json::parse_error& e) {
            EVLOG_error << "Error while parsing user config file.";
            EVLOG_AND_THROW(e);
        }
    } else {
        EVLOG_debug << "No user-config provided. Creating user config file";
        create_empty_user_config(user_config_path);
    }

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

    const auto charge_point_config_json = json_config.dump();
    charge_point_config = std::make_unique<ocpp::v16::ChargePointConfiguration>(charge_point_config_json,
                                                                                ocpp_share_path, user_config_path);
    std::shared_ptr<ocpp::EvseSecurity> security = std::make_shared<EvseSecurity>(*r_security);
    std::function<void(const std::string& message, ocpp::MessageDirection direction)> message_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_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_VERSION;
                ocpp_message.direction = types::ocpp::MessageDirection::ChargingStationToCSMS;
                p_ocpp_generic->publish_ocpp_message(ocpp_message);
                break;
            }
            default:
                // unknown message direction (ignored)
                break;
            }
        };

    charge_point = std::make_unique<ocpp::v16::ChargePoint>(*charge_point_config, ocpp_share_path, config.DatabasePath,
                                                            sql_init_path, config.MessageLogPath, security,
                                                            std::nullopt, message_callback);

    this->charge_point->set_message_queue_resume_delay(std::chrono::seconds(config.MessageQueueResumeDelay));

    this->init_evse_subscriptions(); // initialize EvseManager subscriptions as
                                     // early as possible

    this->r_system->subscribe_log_status([this](types::system::LogStatus log_status) {
        std::lock_guard<std::mutex> lg(this->event_mutex);
        if (this->started) {
            this->charge_point->on_log_status_notification(
                log_status.request_id, types::system::log_status_enum_to_string(log_status.log_status));
        } else {
            this->event_queue.emplace(0, log_status);
        }
    });

    this->r_system->subscribe_firmware_update_status(
        [this](types::system::FirmwareUpdateStatus firmware_update_status) {
            std::lock_guard<std::mutex> lg(this->event_mutex);
            if (this->started) {
                this->charge_point->on_firmware_update_status_notification(
                    firmware_update_status.request_id,
                    conversions::to_ocpp_firmware_status_notification(firmware_update_status.firmware_update_status));
            } else {
                this->event_queue.emplace(0, firmware_update_status);
            }
        });
}

void OCPP::ready() {
    invoke_ready(*p_main);
    invoke_ready(*p_ocpp_generic);
    invoke_ready(*p_auth_validator);
    invoke_ready(*p_auth_provider);
    invoke_ready(*p_data_transfer);

    this->init_evse_connector_map();

    this->charge_point->register_pause_charging_callback([this](int32_t connector) {
        if (this->connector_evse_index_map.count(connector)) {
            return this->r_evse_manager.at(this->connector_evse_index_map.at(connector))->call_pause_charging();
        }
        return false;
    });
    this->charge_point->register_resume_charging_callback([this](int32_t connector) {
        if (this->connector_evse_index_map.count(connector)) {
            return this->r_evse_manager.at(this->connector_evse_index_map.at(connector))->call_resume_charging();
        }
        return false;
    });
    this->charge_point->register_stop_transaction_callback([this](int32_t connector, ocpp::v16::Reason reason) {
        if (this->connector_evse_index_map.count(connector)) {
            types::evse_manager::StopTransactionRequest req;
            req.reason = conversions::to_everest_stop_transaction_reason(reason);
            return this->r_evse_manager.at(this->connector_evse_index_map.at(connector))->call_stop_transaction(req);
        }
        return false;
    });

    this->charge_point->register_unlock_connector_callback([this](int32_t connector) {
        if (this->connector_evse_index_map.count(connector)) {
            EVLOG_info << "Executing unlock connector callback";
            // UnlockStatus::Failed is currently not supported by EVerest
            return this->r_evse_manager.at(this->connector_evse_index_map.at(connector))->call_force_unlock(1)
                       ? ocpp::v16::UnlockStatus::Unlocked
                       : ocpp::v16::UnlockStatus::NotSupported;
        }
        return ocpp::v16::UnlockStatus::NotSupported;
    });

    // int32_t reservation_id, CiString<20> auth_token, DateTime expiry_time,
    // std::string parent_id
    this->charge_point->register_reserve_now_callback([this](int32_t reservation_id, int32_t connector,
                                                             ocpp::DateTime expiryDate, ocpp::CiString<20> idTag,
                                                             std::optional<ocpp::CiString<20>> parent_id) {
        types::reservation::Reservation reservation;
        reservation.id_token = idTag.get();
        reservation.reservation_id = reservation_id;
        reservation.expiry_time = expiryDate.to_rfc3339();

        if (parent_id) {
            reservation.parent_id_token.emplace(parent_id.value().get());
        }

        if (connector == 0) {
            reservation.evse_id = std::nullopt;
        } else {
            reservation.evse_id = connector;
        }

        auto response = this->r_reservation->call_reserve_now(reservation);
        return conversions::to_ocpp_reservation_status(response);
    });

    this->charge_point->register_upload_diagnostics_callback([this](const ocpp::v16::GetDiagnosticsRequest& msg) {
        types::system::UploadLogsRequest upload_logs_request;
        upload_logs_request.location = msg.location;

        if (msg.stopTime.has_value()) {
            upload_logs_request.latest_timestamp.emplace(msg.stopTime.value().to_rfc3339());
        }
        if (msg.startTime.has_value()) {
            upload_logs_request.oldest_timestamp.emplace(msg.startTime.value().to_rfc3339());
        }
        if (msg.retries.has_value()) {
            // As defined in OCPP the initial attempt does not count as a retry
            // hence the + 1
            upload_logs_request.retries.emplace(msg.retries.value() + 1);
        }
        if (msg.retryInterval.has_value()) {
            upload_logs_request.retry_interval_s.emplace(msg.retryInterval.value());
        }
        const auto upload_logs_response = this->r_system->call_upload_logs(upload_logs_request);
        ocpp::v16::GetLogResponse response;
        if (upload_logs_response.file_name.has_value()) {
            // we just truncate here since the upload operation could have already
            // been started by the system module and we cant do much about it, so
            // best we can do is truncate the filename and rather make sure in the
            // system module that shorter filenames are used
            response.filename.emplace(
                ocpp::CiString<255>(upload_logs_response.file_name.value(), ocpp::StringTooLarge::Truncate));
        }
        response.status = conversions::to_ocpp_log_status_enum_type(upload_logs_response.upload_logs_status);
        return response;
    });

    this->charge_point->register_upload_logs_callback([this](ocpp::v16::GetLogRequest msg) {
        types::system::UploadLogsRequest upload_logs_request;
        upload_logs_request.location = msg.log.remoteLocation;
        upload_logs_request.request_id = msg.requestId;
        upload_logs_request.type = ocpp::v16::conversions::log_enum_type_to_string(msg.logType);

        if (msg.log.latestTimestamp.has_value()) {
            upload_logs_request.latest_timestamp.emplace(msg.log.latestTimestamp.value().to_rfc3339());
        }
        if (msg.log.oldestTimestamp.has_value()) {
            upload_logs_request.oldest_timestamp.emplace(msg.log.oldestTimestamp.value().to_rfc3339());
        }
        if (msg.retries.has_value()) {
            // As defined in OCPP the initial attempt does not count as a retry
            // hence the + 1
            upload_logs_request.retries.emplace(msg.retries.value() + 1);
        }
        if (msg.retryInterval.has_value()) {
            upload_logs_request.retry_interval_s.emplace(msg.retryInterval.value());
        }

        const auto upload_logs_response = this->r_system->call_upload_logs(upload_logs_request);

        ocpp::v16::GetLogResponse response;
        if (upload_logs_response.file_name.has_value()) {
            // we just truncate here since the upload operation could have already
            // been started by the system module and we cant do much about it, so
            // best we can do is truncate the filename and rather make sure in the
            // system module that shorter filenames are used
            response.filename.emplace(
                ocpp::CiString<255>(upload_logs_response.file_name.value(), ocpp::StringTooLarge::Truncate));
        }
        response.status = conversions::to_ocpp_log_status_enum_type(upload_logs_response.upload_logs_status);
        return response;
    });
    this->charge_point->register_update_firmware_callback([this](const ocpp::v16::UpdateFirmwareRequest msg) {
        types::system::FirmwareUpdateRequest firmware_update_request;
        firmware_update_request.location = msg.location;
        firmware_update_request.request_id = -1;
        firmware_update_request.retrieve_timestamp.emplace(msg.retrieveDate.to_rfc3339());
        if (msg.retries.has_value()) {
            // As defined in OCPP the initial attempt does not count as a retry
            // hence the + 1
            firmware_update_request.retries.emplace(msg.retries.value() + 1);
        }
        if (msg.retryInterval.has_value()) {
            firmware_update_request.retry_interval_s.emplace(msg.retryInterval.value());
        }
        this->r_system->call_update_firmware(firmware_update_request);
    });

    this->charge_point->register_signed_update_firmware_callback([this](ocpp::v16::SignedUpdateFirmwareRequest msg) {
        types::system::FirmwareUpdateRequest firmware_update_request;
        firmware_update_request.request_id = msg.requestId;
        firmware_update_request.location = msg.firmware.location;
        firmware_update_request.signature.emplace(msg.firmware.signature.get());
        firmware_update_request.signing_certificate.emplace(msg.firmware.signingCertificate.get());
        firmware_update_request.retrieve_timestamp.emplace(msg.firmware.retrieveDateTime.to_rfc3339());

        if (msg.firmware.installDateTime.has_value()) {
            firmware_update_request.install_timestamp.emplace(msg.firmware.installDateTime.value());
        }
        if (msg.retries.has_value()) {
            // We add +1 since as defined in OCPP retries does not include the
            // initial attempt
            firmware_update_request.retries.emplace(msg.retries.value() + 1);
        }
        if (msg.retryInterval.has_value()) {
            firmware_update_request.retry_interval_s.emplace(msg.retryInterval.value());
        }

        const auto system_response = this->r_system->call_update_firmware(firmware_update_request);

        return conversions::to_ocpp_update_firmware_status_enum_type(system_response);
    });

    this->charge_point->register_all_connectors_unavailable_callback([this]() {
        EVLOG_info << "All connectors unavailable, proceed with firmware installation";
        this->r_system->call_allow_firmware_installation();
    });

    this->charge_point->register_provide_token_callback(
        [this](const std::string& id_token, std::vector<int32_t> referenced_connectors, bool prevalidated) {
            types::authorization::ProvidedIdToken provided_token;
            provided_token.id_token = {id_token, types::authorization::IdTokenType::Central};
            provided_token.authorization_type = types::authorization::AuthorizationType::OCPP;
            provided_token.connectors.emplace(referenced_connectors);
            provided_token.prevalidated.emplace(prevalidated);
            this->p_auth_provider->publish_provided_token(provided_token);
        });

    this->charge_point->register_disable_evse_callback([this](int32_t connector) {
        if (this->connector_evse_index_map.count(connector)) {
            return this->r_evse_manager.at(this->connector_evse_index_map.at(connector))
                ->call_enable_disable(
                    0, {types::evse_manager::Enable_source::CSMS, types::evse_manager::Enable_state::Disable, 5000});
        } else {
            return false;
        }
    });

    this->charge_point->register_set_system_time_callback(
        [this](const std::string& system_time) { this->r_system->call_set_system_time(system_time); });

    this->charge_point->register_enable_evse_callback([this](int32_t connector) {
        if (this->connector_evse_index_map.count(connector)) {
            return this->r_evse_manager.at(this->connector_evse_index_map.at(connector))
                ->call_enable_disable(
                    0, {types::evse_manager::Enable_source::CSMS, types::evse_manager::Enable_state::Enable, 5000});
        } else {
            return false;
        }
    });

    this->charge_point->register_cancel_reservation_callback(
        [this](int32_t reservation_id) { return this->r_reservation->call_cancel_reservation(reservation_id); });

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

    this->charge_point->register_is_token_reserved_for_connector_callback(
        [this](const int32_t connector, const std::string& id_token) -> ocpp::ReservationCheckStatus {
            types::reservation::ReservationCheck reservation_check_request;
            reservation_check_request.evse_id = connector;
            reservation_check_request.id_token = id_token;

            types::reservation::ReservationCheckStatus status =
                this->r_reservation->call_exists_reservation(reservation_check_request);

            return ocpp_conversions::to_ocpp_reservation_check_status(status);
        });

    const auto composite_schedule_unit = get_unit_or_default(this->config.RequestCompositeScheduleUnit);

    // publish charging schedules at least once on startup
    const auto charging_schedules = this->charge_point->get_all_enhanced_composite_charging_schedules(
        this->config.PublishChargingScheduleDurationS, composite_schedule_unit);
    this->set_external_limits(charging_schedules);
    this->publish_charging_schedules(charging_schedules);

    this->charging_schedules_timer = std::make_unique<Everest::SteadyTimer>([this, composite_schedule_unit]() {
        const auto charging_schedules = this->charge_point->get_all_enhanced_composite_charging_schedules(
            this->config.PublishChargingScheduleDurationS, composite_schedule_unit);
        this->set_external_limits(charging_schedules);
        this->publish_charging_schedules(charging_schedules);
    });
    if (this->config.PublishChargingScheduleIntervalS > 0) {
        this->charging_schedules_timer->interval(std::chrono::seconds(this->config.PublishChargingScheduleIntervalS));
    }

    this->charge_point->register_signal_set_charging_profiles_callback([this, composite_schedule_unit]() {
        // this is executed when CSMS sends new ChargingProfile that is accepted
        // by the ChargePoint
        EVLOG_info << "Received new Charging Schedules from CSMS";
        const auto charging_schedules = this->charge_point->get_all_enhanced_composite_charging_schedules(
            this->config.PublishChargingScheduleDurationS, composite_schedule_unit);
        this->set_external_limits(charging_schedules);
        this->publish_charging_schedules(charging_schedules);
    });

    this->charge_point->register_is_reset_allowed_callback([this](ocpp::v16::ResetType type) {
        const auto reset_type = conversions::to_everest_reset_type(type);
        return this->r_system->call_is_reset_allowed(reset_type);
    });

    this->charge_point->register_reset_callback([this](ocpp::v16::ResetType type) {
        // 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));
        // properly stop charge point before stopping all of the software
        this->charge_point->stop();
        // If it is a hard reset we can go ahead and forcibly reset directly
        const auto reset_type = conversions::to_everest_reset_type(type);
        this->r_system->call_reset(reset_type, false);
    });

    this->charge_point->register_connection_state_changed_callback(
        [this](bool is_connected) { this->p_ocpp_generic->publish_is_connected(is_connected); });

    this->charge_point->register_get_15118_ev_certificate_response_callback(
        [this](const int32_t connector_id, const ocpp::v2::Get15118EVCertificateResponse& certificate_response,
               const ocpp::v2::CertificateActionEnum& certificate_action) {
            types::iso15118::ResponseExiStreamStatus response;
            response.status = conversions::to_everest_iso15118_status(certificate_response.status);
            response.certificate_action = conversions::to_everest_certificate_action_enum(certificate_action);
            if (not certificate_response.exiResponse.get().empty()) {
                // since exi_response is an optional in the EVerest type we only set
                // it when not empty
                response.exi_response.emplace(certificate_response.exiResponse.get());
            }

            this->r_extensions_15118.at(connector_id - 1)->call_set_get_certificate_response(response);
        });

    this->charge_point->register_security_event_callback([this](const std::string& type, const std::string& tech_info) {
        EVLOG_info << "Security Event in OCPP occurred: " << type;
        types::ocpp::SecurityEvent event;
        event.type = type;
        event.info = tech_info;
        this->p_ocpp_generic->publish_security_event(event);
    });

    this->charge_point->register_transaction_started_callback(
        [this](const int32_t connector, const std::string& session_id) {
            types::ocpp::OcppTransactionEvent tevent;
            tevent.transaction_event = types::ocpp::TransactionEvent::Started;
            tevent.evse = {connector, 1};
            tevent.session_id = session_id;
            p_ocpp_generic->publish_ocpp_transaction_event(tevent);
        });

    this->charge_point->register_transaction_updated_callback(
        [this](const int32_t connector, const std::string& session_id, const int32_t transaction_id,
               const ocpp::v16::IdTagInfo& id_tag_info) {
            types::ocpp::OcppTransactionEvent tevent;
            tevent.transaction_event = types::ocpp::TransactionEvent::Updated;
            tevent.evse = {connector, 1};
            tevent.session_id = session_id;
            tevent.transaction_id = std::to_string(transaction_id);
            p_ocpp_generic->publish_ocpp_transaction_event(tevent);
            if (id_tag_info.parentIdTag.has_value()) {
                types::authorization::ValidationResultUpdate result_update;
                types::authorization::IdToken id_token;
                id_token.value = id_tag_info.parentIdTag.value();
                // Default to RFID auth type for parentIdTag since we have no
                // information about it in ocpp1.6
                id_token.type = types::authorization::IdTokenType::ISO14443;
                result_update.validation_result.parent_id_token = id_token;
                result_update.validation_result.authorization_status =
                    conversions::to_everest_authorization_status(id_tag_info.status);
                result_update.connector_id = connector;
                p_auth_validator->publish_validate_result_update(result_update);
            }
        });

    this->charge_point->register_transaction_stopped_callback(
        [this](const int32_t connector, const std::string& session_id, const int32_t transaction_id) {
            EVLOG_info << "Transaction stopped at connector: " << connector << ", session_id: " << session_id;
            types::ocpp::OcppTransactionEvent tevent;
            tevent.transaction_event = types::ocpp::TransactionEvent::Ended;
            tevent.evse = {connector, 1};
            tevent.session_id = session_id;
            tevent.transaction_id = std::to_string(transaction_id);
            p_ocpp_generic->publish_ocpp_transaction_event(tevent);
        });

    this->charge_point->register_boot_notification_response_callback(
        [this](const ocpp::v16::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);
        });

    this->charge_point->register_session_cost_callback(
        [this](const ocpp::RunningCost& session_cost,
               const uint32_t number_of_decimals) -> ocpp::v16::DataTransferResponse {
            const types::session_cost::SessionCost cost =
                ocpp_conversions::create_session_cost(session_cost, number_of_decimals, {});
            ocpp::v16::DataTransferResponse response;
            this->p_session_cost->publish_session_cost(cost);
            response.status = ocpp::v16::DataTransferStatus::Accepted;
            return response;
        });

    this->charge_point->register_tariff_message_callback(
        [this](const ocpp::TariffMessage& message) -> ocpp::v16::DataTransferResponse {
            const types::session_cost::TariffMessage m = ocpp_conversions::to_everest_tariff_message(message);
            this->p_session_cost->publish_tariff_message(m);
            ocpp::v16::DataTransferResponse response;
            response.status = ocpp::v16::DataTransferStatus::Accepted;
            return response;
        });

    if (this->p_session_cost != nullptr) {
        this->charge_point->register_default_price_callback([this](const ocpp::TariffMessage& message) {
            const types::session_cost::DefaultPrice p = ocpp_conversions::to_everest_default_price(message.message);
            this->p_session_cost->publish_default_price(p);
        });
    }

    this->charge_point->register_set_display_message_callback(
        [this](const std::vector<ocpp::DisplayMessage>& messages) -> ocpp::v16::DataTransferResponse {
            ocpp::v16::DataTransferResponse response;
            if (this->r_display_message.empty()) {
                EVLOG_warning << "No display message handler registered, dropping "
                                 "data transfer display message";
                response.status = ocpp::v16::DataTransferStatus::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_data_transfer_response(display_message_response);

            return response;
        });

    if (!this->r_data_transfer.empty()) {
        this->charge_point->register_data_transfer_callback([this](const ocpp::v16::DataTransferRequest& request) {
            types::ocpp::DataTransferRequest data_transfer_request;
            data_transfer_request.vendor_id = request.vendorId.get();
            if (request.messageId.has_value()) {
                data_transfer_request.message_id = request.messageId.value().get();
            }
            data_transfer_request.data = request.data;
            types::ocpp::DataTransferResponse data_transfer_response =
                this->r_data_transfer.at(0)->call_data_transfer(data_transfer_request);
            ocpp::v16::DataTransferResponse response;
            response.status = conversions::to_ocpp_data_transfer_status(data_transfer_response.status);
            response.data = data_transfer_response.data;
            return response;
        });
    }

    // We must wait for EVSEs to be marked as ready before initializing ocpp since
    // we will potentially update the operative status of the connectors
    {
        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 OCPP1.6 service";
            return true;
        });
    }

    this->charge_point->register_generic_configuration_key_changed_callback(
        [this](const ocpp::v16::KeyValue& key_value) { this->handle_config_key(key_value); });

    this->init_module_configuration();

    // if charger information interface is connected, override only these specific
    // properties which were loaded from configuration file(s)
    if (!this->r_charger_information.empty()) {
        auto ci = this->r_charger_information.at(0)->call_get_charger_information();

        this->charge_point->update_chargepoint_information(ci.vendor, ci.model, ci.chargepoint_serial,
                                                           ci.chargebox_serial, ci.firmware_version);
    }

    // we can now call init(), which initializes the charge points state machine.
    // It reads the connector availability from the internal database and
    // potentially triggers enable/disable callbacks at the evse.
    this->charge_point->init({}, this->resuming_session_ids);

    // this signals to the evses they can now start their internal state machines
    // signal to the EVSEs that OCPP is initialized
    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));
    const auto boot_reason = conversions::to_ocpp_boot_reason_enum(this->r_system->call_get_boot_reason());

    // we can now start the OCPP connection and process any queued events. We lock
    // the event mutex to avoid race conditions with error/event handlers that
    // might be called from other threads
    std::lock_guard<std::mutex> lg(this->event_mutex);
    this->charge_point->start({}, boot_reason, this->resuming_session_ids);
    EVLOG_info << "OCPP started";
    while (!this->event_queue.empty()) {
        auto queued_event = this->event_queue.front();
        this->event_queue.pop();
        std::visit(
            overloaded{
                [&](const types::evse_manager::SessionEvent& s) { process_session_event(queued_event.evse_id, s); },
                [&](const ErrorRaised& e) {
                    auto info = get_error_info(e);
                    charge_point->on_error(queued_event.evse_id, info);
                },
                [&](const ErrorCleared& e) { charge_point->on_error_cleared(queued_event.evse_id, e.uuid.uuid); },
                [&](const types::system::LogStatus& log) {
                    charge_point->on_log_status_notification(log.request_id,
                                                             types::system::log_status_enum_to_string(log.log_status));
                },
                [&](const types::system::FirmwareUpdateStatus& fw) {
                    charge_point->on_firmware_update_status_notification(
                        fw.request_id, conversions::to_ocpp_firmware_status_notification(fw.firmware_update_status));
                },
                [&](const PowermeterPublicKey public_key) {
                    this->charge_point->set_powermeter_public_key(queued_event.evse_id, public_key.value);
                }},
            queued_event.data);
    }
    this->started = true;
}

int32_t OCPP::get_ocpp_connector_id(int32_t evse_id, int32_t connector_id) {
    return this->evse_connector_map.at(evse_id).at(connector_id);
}

} // namespace module
