- 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
1172 lines
56 KiB
C++
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
|