Files
Eric F d398a6ced2 Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
2026-06-08 00:38:27 -04:00

1172 lines
56 KiB
C++

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