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
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,76 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
target_compile_definitions(${MODULE_NAME} PRIVATE
API_VERSION=\"1.0.0\"
)
target_compile_features(${MODULE_NAME} PRIVATE cxx_std_17)
set_target_properties(${MODULE_NAME} PROPERTIES CXX_EXTENSIONS OFF)
if (DISABLE_EDM)
find_package(libwebsockets REQUIRED)
list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
find_package(json-rpc-cxx REQUIRED)
target_include_directories(${MODULE_NAME}
SYSTEM PRIVATE
${json-rpc-cxx_INCLUDE_DIRS}
)
else()
target_include_directories(${MODULE_NAME}
SYSTEM PRIVATE
$<TARGET_PROPERTY:json-rpc-cxx,INTERFACE_INCLUDE_DIRECTORIES>
)
endif()
target_link_libraries(${MODULE_NAME}
PRIVATE
date::date
date::date-tz
everest::external_energy_limits
everest::helpers
nlohmann_json::nlohmann_json
websockets_shared
)
target_sources(${MODULE_NAME}
PRIVATE
"data/DataStore.cpp"
"data/SessionInfo.cpp"
"helpers/Conversions.cpp"
"helpers/ErrorHandler.cpp"
"helpers/LimitDecimalPlaces.cpp"
"rpc/RpcHandler.cpp"
"rpc/methods/Api.cpp"
"rpc/methods/ChargePoint.cpp"
"rpc/methods/Evse.cpp"
"rpc/notifications/ChargePoint.cpp"
"rpc/notifications/Evse.cpp"
"server/TransportInterface.cpp"
"server/WebsocketServer.cpp"
"RpcApiRequestHandler.cpp"
)
target_compile_options(${MODULE_NAME}
PRIVATE
-Wimplicit-fallthrough
-Werror=switch-enum
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
if(EVEREST_CORE_BUILD_TESTING)
add_subdirectory(tests)
endif()
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,454 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "RpcApi.hpp"
#include "helpers/Conversions.hpp"
#include "helpers/ErrorHandler.hpp"
#include <cmath>
namespace module {
void RpcApi::init() {
for (const auto& evse_manager : r_evse_manager) {
// create one DataStore object per EVSE
auto& evse_data = this->data.evses.emplace_back(std::make_unique<data::DataStoreEvse>());
// subscribe to evse_manager interface variables
this->subscribe_evse_manager(evse_manager, *evse_data);
}
if (r_evse_energy_sink.empty()) {
EVLOG_warning << "No EVSE energy sinks configured. Configuration of EVSE external limits will not be possible.";
}
// Check how EVSEs are mapped
this->check_evse_mapping();
// Create the request handler
m_request_handler = std::make_unique<RpcApiRequestHandler>(data, r_evse_manager, r_evse_energy_sink);
std::vector<std::shared_ptr<server::TransportInterface>> transport_interfaces;
if (config.websocket_enabled) {
m_websocket_server = std::make_unique<server::WebSocketServer>(
config.websocket_tls_enabled, config.websocket_port, config.websocket_interface);
transport_interfaces.push_back(std::shared_ptr<server::TransportInterface>(std::move(m_websocket_server)));
}
if (transport_interfaces.empty()) {
throw std::runtime_error("No other transports currently available, please enable websocket transport");
}
m_rpc_handler = std::make_unique<rpc::RpcHandler>(std::move(transport_interfaces), data,
std::move(m_request_handler), config.max_decimal_places_other);
subscribe_global_errors();
}
void RpcApi::ready() {
// get charger information (cmd not available during init())
if (r_charger_information.size() > 0) {
types::json_rpc_api::ChargerInfoObj charger_info;
const auto info = r_charger_information[0]->call_get_charger_information();
// mandatory members
charger_info.vendor = info.vendor;
charger_info.model = info.model;
charger_info.serial = info.chargepoint_serial.value_or("unknown");
charger_info.firmware_version = info.firmware_version.value_or("unknown");
// optional members
if (info.friendly_name.has_value()) {
charger_info.friendly_name = info.friendly_name.value();
}
if (info.manufacturer.has_value()) {
charger_info.manufacturer = info.manufacturer.value();
}
if (info.manufacturer_url.has_value()) {
charger_info.manufacturer_url = info.manufacturer_url.value();
}
if (info.model_number.has_value()) {
charger_info.model_no = info.model_number.value();
}
if (info.model_revision.has_value()) {
charger_info.revision = info.model_revision.value();
}
if (info.board_revision.has_value()) {
charger_info.board_revision = info.board_revision.value();
}
this->data.chargerinfo.set_data(charger_info);
} else {
this->data.chargerinfo.set_unknown();
}
// Start server instances
m_rpc_handler->start_server();
}
void RpcApi::check_evse_session_event(data::DataStoreEvse& evse_data,
const types::evse_manager::SessionEvent& session_event) {
// store the session info in the data store
types::json_rpc_api::EVSEStateEnum evse_state =
types::json_rpc_api::evse_manager_session_event_to_evse_state(session_event);
evse_data.evsestatus.set_state(evse_state);
evse_data.sessioninfo.update_state(session_event);
if (evse_state == types::json_rpc_api::EVSEStateEnum::Charging) {
evse_data.evsestatus.set_charging_allowed(true);
}
if (session_event.source.has_value()) {
const auto source = session_event.source.value();
evse_data.sessioninfo.set_enable_disable_source(
types::evse_manager::enable_source_to_string(source.enable_source),
types::evse_manager::enable_state_to_string(source.enable_state), source.enable_priority);
if (source.enable_state == types::evse_manager::Enable_state::Disable) {
evse_data.evsestatus.set_available(false);
} else if (source.enable_state == types::evse_manager::Enable_state::Enable) {
evse_data.evsestatus.set_available(true);
}
}
if (session_event.event == types::evse_manager::SessionEventEnum::TransactionStarted) {
if (session_event.transaction_started.has_value()) {
const auto transaction_started = session_event.transaction_started.value();
const auto energy_Wh_import = transaction_started.meter_value.energy_Wh_import.total;
evse_data.sessioninfo.set_start_energy_import_wh(energy_Wh_import);
if (transaction_started.meter_value.energy_Wh_export.has_value()) {
const auto energy_Wh_export = transaction_started.meter_value.energy_Wh_export.value().total;
evse_data.sessioninfo.set_start_energy_export_wh(energy_Wh_export);
} else {
evse_data.sessioninfo.start_energy_export_wh_was_set = false;
}
}
} else if (session_event.event == types::evse_manager::SessionEventEnum::TransactionFinished) {
if (session_event.transaction_finished.has_value()) {
const auto transaction_finished = session_event.transaction_finished.value();
const auto energy_Wh_import = transaction_finished.meter_value.energy_Wh_import.total;
evse_data.sessioninfo.set_end_energy_import_wh(energy_Wh_import);
if (transaction_finished.meter_value.energy_Wh_export.has_value()) {
const auto energy_Wh_export = transaction_finished.meter_value.energy_Wh_export.value().total;
evse_data.sessioninfo.set_end_energy_export_wh(energy_Wh_export);
} else {
evse_data.sessioninfo.end_energy_export_wh_was_set = false;
}
}
evse_data.evsestatus.set_charged_energy_wh(evse_data.sessioninfo.get_charged_energy_wh());
evse_data.evsestatus.set_discharged_energy_wh(evse_data.sessioninfo.get_discharged_energy_wh());
evse_data.evsestatus.set_charging_duration_s(evse_data.sessioninfo.get_charging_duration_s().count());
}
}
void RpcApi::subscribe_evse_manager(const std::unique_ptr<evse_managerIntf>& evse_manager,
data::DataStoreEvse& evse_data) {
evse_manager->subscribe_powermeter([this, &evse_data](const types::powermeter::Powermeter& powermeter) {
this->meterdata_var_to_datastore(powermeter, evse_data.meterdata);
evse_data.sessioninfo.set_latest_energy_import_wh(powermeter.energy_Wh_import.total);
if (powermeter.energy_Wh_export.has_value()) {
evse_data.sessioninfo.set_latest_energy_export_wh(powermeter.energy_Wh_export.value().total);
}
if (powermeter.power_W.has_value()) {
evse_data.sessioninfo.set_latest_total_w(powermeter.power_W.value().total);
}
// update duration and energy values in the EVSE status store
evse_data.evsestatus.set_charging_duration_s(evse_data.sessioninfo.get_charging_duration_s().count());
evse_data.evsestatus.set_charged_energy_wh(evse_data.sessioninfo.get_charged_energy_wh());
evse_data.evsestatus.set_discharged_energy_wh(evse_data.sessioninfo.get_discharged_energy_wh());
});
evse_manager->subscribe_hw_capabilities(
[this, &evse_data](const types::evse_board_support::HardwareCapabilities& hwcaps) {
// there is only one connector supported currently
this->hwcaps_var_to_datastore(hwcaps, evse_data.hardwarecapabilities);
// also update evse_max_phase_count
evse_data.evsestatus.set_ac_charge_param_evse_max_phase_count(hwcaps.max_phase_count_import);
});
evse_manager->subscribe_evse_id([&evse_data](const std::string& evse_id) {
// set the EVSE id in the data store
evse_data.evseinfo.set_id(evse_id);
});
evse_manager->subscribe_session_event([this, &evse_data](types::evse_manager::SessionEvent session_event) {
this->check_evse_session_event(evse_data, session_event);
});
evse_manager->subscribe_selected_protocol([&evse_data](const std::string& selected_protocol) {
const auto var_selected_protocol =
types::json_rpc_api::evse_manager_protocol_to_charge_protocol(selected_protocol);
evse_data.evsestatus.set_charge_protocol(var_selected_protocol);
});
evse_manager->subscribe_enforced_limits([&evse_data](const types::energy::EnforcedLimits& enforced_limits) {
// set the external limits in the data store
if (evse_data.evseinfo.get_is_ac_transfer_mode()) {
const auto& max_current = enforced_limits.limits_root_side.ac_max_current_A;
if (max_current.has_value()) {
evse_data.evsestatus.set_ac_charge_param_evse_max_current(max_current.value().value);
}
RPCDataTypes::ACChargeStatusObj ac_charge_status;
const auto& max_phase_count = enforced_limits.limits_root_side.ac_max_phase_count;
ac_charge_status.evse_active_phase_count = max_phase_count.has_value() ? max_phase_count.value().value : 3;
evse_data.evsestatus.set_ac_charge_status(ac_charge_status);
} else {
evse_data.evsestatus.set_ac_charge_param(std::nullopt);
}
});
evse_manager->subscribe_supported_energy_transfer_modes(
[&evse_data](const std::vector<types::iso15118::EnergyTransferMode>& supported_energy_transfer_modes) {
// convert to rpc type
bool is_ac_transfer_mode = false;
const auto rpc_supported_energy_transfer_modes =
RPCDataTypes::iso15118_energy_transfer_modes_to_json_rpc_api(supported_energy_transfer_modes,
is_ac_transfer_mode);
evse_data.evseinfo.set_supported_energy_transfer_modes(rpc_supported_energy_transfer_modes);
evse_data.evseinfo.set_is_ac_transfer_mode(is_ac_transfer_mode);
});
evse_manager->subscribe_ev_info([&evse_data](types::evse_manager::EVInfo ev_info) {
RPCDataTypes::DisplayParametersObj display_parameters;
if (ev_info.soc.has_value()) {
// for some reason, soc in types::evse_manager::EVInfo is declared as float;
// all integers from 0 to 100 can be exactly represented in float, but let's
// (l)round them just in case:
display_parameters.present_soc = std::lround(ev_info.soc.value());
}
if (ev_info.battery_capacity.has_value()) {
display_parameters.battery_energy_capacity = ev_info.battery_capacity;
}
if (ev_info.estimated_time_full.has_value()) {
// calculate the distance of estimated_time_full to now, and if greater 0,
// report it
const auto time_until =
Everest::Date::from_rfc3339(ev_info.estimated_time_full.value()) - date::utc_clock::now();
const auto secs = std::chrono::duration_cast<std::chrono::seconds>(time_until).count();
if (secs > 0) {
display_parameters.remaining_time_to_maximum_soc = secs;
}
}
// pass an empty optional object if no member was set
std::optional<RPCDataTypes::DisplayParametersObj> result{};
if (display_parameters != RPCDataTypes::DisplayParametersObj()) {
result = display_parameters;
}
evse_data.evsestatus.set_display_parameters(result);
});
}
void RpcApi::subscribe_global_errors() {
// Subscribe to global error events
const auto error_handler = [this](const Everest::error::Error& error) {
const auto tmp_error = types::json_rpc_api::everest_error_to_rpc_error(error);
helpers::handle_error_raised(this->data, tmp_error);
};
const auto error_cleared_handler = [this](const Everest::error::Error& error) {
const auto tmp_error = types::json_rpc_api::everest_error_to_rpc_error(error);
helpers::handle_error_cleared(this->data, tmp_error);
};
subscribe_global_all_errors(error_handler, error_cleared_handler);
}
void RpcApi::meterdata_var_to_datastore(const types::powermeter::Powermeter& powermeter,
data::MeterDataStore& meter_data) {
types::json_rpc_api::MeterDataObj meter_data_new; // default initialized
if (const auto _data = meter_data.get_data(); _data.has_value()) {
// initialize with existing values
meter_data_new = _data.value();
}
// mandatory objects from the EVerest powermeter interface variable
// timestamp
meter_data_new.timestamp = powermeter.timestamp;
// energy_Wh_import
if (powermeter.energy_Wh_import.L1.has_value()) {
meter_data_new.energy_Wh_import.L1 = powermeter.energy_Wh_import.L1.value();
}
if (powermeter.energy_Wh_import.L2.has_value()) {
meter_data_new.energy_Wh_import.L2 = powermeter.energy_Wh_import.L2.value();
}
if (powermeter.energy_Wh_import.L3.has_value()) {
meter_data_new.energy_Wh_import.L3 = powermeter.energy_Wh_import.L3.value();
}
meter_data_new.energy_Wh_import.total = powermeter.energy_Wh_import.total;
// optional objects from the EVerest powermeter interface
if (powermeter.current_A.has_value()) {
meter_data_new.current_A.emplace();
const auto& inobj = powermeter.current_A.value();
if (inobj.L1.has_value()) {
meter_data_new.current_A.value().L1 = inobj.L1.value();
}
if (inobj.L2.has_value()) {
meter_data_new.current_A.value().L2 = inobj.L2.value();
}
if (inobj.L3.has_value()) {
meter_data_new.current_A.value().L3 = inobj.L3.value();
}
}
if (powermeter.energy_Wh_export.has_value()) {
// a shortcut reference to the input data sub-object
const auto& inobj = powermeter.energy_Wh_export.value();
// a shortcut reference to the output data sub-object optional
auto& export_opt = meter_data_new.energy_Wh_export;
// keep original (copied) optional value, or emplace empty if non exist
auto& newobj = export_opt.emplace(export_opt.value_or(types::json_rpc_api::Energy_Wh_export{}));
if (inobj.L1.has_value()) {
newobj.L1 = inobj.L1.value();
}
if (inobj.L2.has_value()) {
newobj.L2 = inobj.L2.value();
}
if (inobj.L3.has_value()) {
newobj.L3 = inobj.L3.value();
}
newobj.total = inobj.total;
}
if (powermeter.frequency_Hz.has_value()) {
// a shortcut reference to the input data sub-object
const auto& inobj = powermeter.frequency_Hz.value();
// a shortcut reference to the output data sub-object optional
auto& frequency_optional = meter_data_new.frequency_Hz;
// keep original (copied) optional value, or emplace empty if non exist
auto& newobj = frequency_optional.emplace(frequency_optional.value_or(types::json_rpc_api::Frequency_Hz{}));
newobj.L1 = inobj.L1;
if (inobj.L2.has_value()) {
newobj.L2 = inobj.L2.value();
}
if (inobj.L3.has_value()) {
newobj.L3 = inobj.L3.value();
}
}
if (powermeter.meter_id.has_value()) {
meter_data_new.meter_id = powermeter.meter_id.value();
}
// serial_number is not yet available
if (powermeter.phase_seq_error.has_value()) {
meter_data_new.phase_seq_error = powermeter.phase_seq_error.value();
}
if (powermeter.power_W.has_value()) {
// a shortcut reference to the input data sub-object
const auto& inobj = powermeter.power_W.value();
// a shortcut reference to the output data sub-object optional
auto& export_opt = meter_data_new.power_W;
// keep original (copied) optional value, or emplace empty if non exist
auto& newobj = export_opt.emplace(export_opt.value_or(types::json_rpc_api::Power_W{}));
if (inobj.L1.has_value()) {
newobj.L1 = inobj.L1.value();
}
if (inobj.L2.has_value()) {
newobj.L2 = inobj.L2.value();
}
if (inobj.L3.has_value()) {
newobj.L3 = inobj.L3.value();
}
newobj.total = inobj.total;
}
if (powermeter.voltage_V.has_value()) {
// a shortcut reference to the input data sub-object
const auto& inobj = powermeter.voltage_V.value();
// a shortcut reference to the output data sub-object optional
auto& export_opt = meter_data_new.voltage_V;
// keep original (copied) optional value, or emplace empty if non exist
auto& newobj = export_opt.emplace(export_opt.value_or(types::json_rpc_api::Voltage_V{}));
if (inobj.L1.has_value()) {
newobj.L1 = inobj.L1.value();
}
if (inobj.L2.has_value()) {
newobj.L2 = inobj.L2.value();
}
if (inobj.L3.has_value()) {
newobj.L3 = inobj.L3.value();
}
}
// submit changes
// Note: timestamp will skew this, as it will always change, and therefore always trigger a notification for the
// complete dataset
meter_data.set_data(meter_data_new);
}
void RpcApi::hwcaps_var_to_datastore(const types::evse_board_support::HardwareCapabilities& hwcaps,
data::HardwareCapabilitiesStore& hw_caps_data) {
types::json_rpc_api::HardwareCapabilitiesObj hw_caps_data_new; // default initialized
if (const auto _data = hw_caps_data.get_data(); _data.has_value()) {
// initialize with existing values
hw_caps_data_new = _data.value();
}
// mandatory objects from the EVerest hw_capabilites interface variable
hw_caps_data_new.max_current_A_export = hwcaps.max_current_A_import;
hw_caps_data_new.max_current_A_import = hwcaps.max_current_A_import;
hw_caps_data_new.max_phase_count_export = hwcaps.max_phase_count_export;
hw_caps_data_new.max_phase_count_import = hwcaps.max_phase_count_import;
hw_caps_data_new.min_current_A_export = hwcaps.min_current_A_export;
hw_caps_data_new.min_current_A_import = hwcaps.min_current_A_import;
hw_caps_data_new.min_phase_count_export = hwcaps.min_phase_count_export;
hw_caps_data_new.min_phase_count_import = hwcaps.min_phase_count_import;
hw_caps_data_new.phase_switch_during_charging = hwcaps.supports_changing_phases_during_charging;
// submit changes
hw_caps_data.set_data(hw_caps_data_new);
}
bool RpcApi::check_evse_mapping() {
// Iterate over the configured EVSE mapping and configure the data store accordingly
if (r_evse_manager.size() != this->data.evses.size()) {
throw std::runtime_error("The number of EVSE managers does not match the number of EVSE data stores.");
}
// As long as the EvseManager only supports one statically configured connector, we extract the
// connector id from the mapping. Only the connector type is retrieved from the EvseManager.
// Iterate over all over the mapping of the EVSE's and configure the data store accordingly
for (std::size_t idx = 0; idx < r_evse_manager.size(); idx++) {
const auto& evse_manager = r_evse_manager[idx];
const auto& evse_data = this->data.evses[idx];
// Initialize connector index for the case of no mapping information
types::json_rpc_api::ConnectorInfoObj connector;
connector.index = 1; // default connector id
connector.type = types::json_rpc_api::ConnectorTypeEnum::Unknown; // default type
evse_data->evseinfo.set_available_connector(connector);
evse_data->evsestatus.set_active_connector_index(connector.index); // TODO: support multiple connectors
// create one DataStore object per EVSE sink
if (const auto _mapping = evse_manager->get_mapping(); _mapping.has_value()) {
// Write EVSE index and connector index to the datastore
evse_data->evseinfo.set_index(_mapping.value().evse);
if (_mapping.value().connector.has_value()) {
// Initialize connector index
connector.index = _mapping.value().connector.value();
types::evse_manager::Evse evse = evse_manager->call_get_evse();
if (!evse.connectors.empty() && evse.connectors[0].type.has_value()) {
try {
connector.type = types::json_rpc_api::string_to_connector_type_enum(
types::evse_manager::connector_type_enum_to_string(
evse.connectors[0].type.value())); // use the first connector type
} catch (const std::out_of_range& e) {
EVLOG_debug << "Unknown connector type for connector index " << connector.index;
}
} else {
EVLOG_debug << "No connector type determined for connector index " << connector.index;
}
evse_data->evseinfo.set_available_connector(connector);
evse_data->evsestatus.set_active_connector_index(connector.index); // TODO: support multiple connectors
} else {
EVLOG_debug << "No connector index configured in the EVSE mapping, using default connector index "
<< connector.index;
}
} else {
// no mappings, setting limits et.al. will not work
// begin with index 1, as 0 is reserved for the complete charger
const auto evse_index = idx + 1;
EVLOG_warning << "No mapping found for EVSE manager with module_id \"" << evse_manager->module_id
<< "\". No control of limits is possible for this EVSE.";
EVLOG_warning << "Assigning index " << evse_index << " and connector index " << connector.index;
evse_data->evseinfo.set_index(evse_index);
}
}
return true;
}
} // namespace module

View File

@@ -0,0 +1,94 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef RPC_API_HPP
#define RPC_API_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for required interface implementations
#include <generated/interfaces/charger_information/Interface.hpp>
#include <generated/interfaces/evse_manager/Interface.hpp>
#include <generated/interfaces/external_energy_limits/Interface.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
#include "RpcApiRequestHandler.hpp"
#include "data/DataStore.hpp"
#include "rpc/RpcHandler.hpp"
#include "server/WebsocketServer.hpp"
#include <types/json_rpc_api/json_rpc_api.hpp>
#include <vector>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
bool websocket_enabled;
int websocket_port;
std::string websocket_interface;
bool websocket_tls_enabled;
bool authentication_required;
int max_decimal_places_other;
};
class RpcApi : public Everest::ModuleBase {
public:
RpcApi() = delete;
RpcApi(const ModuleInfo& info, std::vector<std::unique_ptr<evse_managerIntf>> r_evse_manager,
std::vector<std::unique_ptr<external_energy_limitsIntf>> r_evse_energy_sink,
std::vector<std::unique_ptr<charger_informationIntf>> r_charger_information, Conf& config) :
ModuleBase(info),
r_evse_manager(std::move(r_evse_manager)),
r_evse_energy_sink(std::move(r_evse_energy_sink)),
r_charger_information(std::move(r_charger_information)),
config(config){};
const std::vector<std::unique_ptr<evse_managerIntf>> r_evse_manager;
const std::vector<std::unique_ptr<external_energy_limitsIntf>> r_evse_energy_sink;
const std::vector<std::unique_ptr<charger_informationIntf>> r_charger_information;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
data::DataStoreCharger data;
std::unique_ptr<server::WebSocketServer> m_websocket_server;
std::unique_ptr<rpc::RpcHandler> m_rpc_handler;
std::unique_ptr<request_interface::RequestHandlerInterface> m_request_handler;
void check_evse_session_event(data::DataStoreEvse& evse_data,
const types::evse_manager::SessionEvent& session_event);
void subscribe_evse_manager(const std::unique_ptr<evse_managerIntf>& evse_manager, data::DataStoreEvse& evse_data);
void subscribe_global_errors();
void meterdata_var_to_datastore(const types::powermeter::Powermeter& powermeter, data::MeterDataStore& meter_data);
void hwcaps_var_to_datastore(const types::evse_board_support::HardwareCapabilities& hwcaps,
data::HardwareCapabilitiesStore& hw_caps_data);
bool check_evse_mapping();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // RPC_API_HPP

View File

@@ -0,0 +1,383 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include <cmath>
#include <everest/external_energy_limits/external_energy_limits.hpp>
#include <everest/logging.hpp>
#include <utils/date.hpp>
#include "RpcApiRequestHandler.hpp"
using namespace types::json_rpc_api;
static const std::string RPCAPI_MODULE_SOURCE = "RpcApi_module";
static const std::chrono::seconds CURRENT_LIMIT_APPLY_TIMEOUT{5};
static types::energy::ExternalLimits get_external_limits(int32_t phases) {
const auto timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
types::energy::ExternalLimits external_limits;
types::energy::ScheduleReqEntry target_entry;
target_entry.timestamp = timestamp;
types::energy::ScheduleReqEntry zero_entry;
zero_entry.timestamp = timestamp;
zero_entry.limits_to_leaves.total_power_W = {0, RPCAPI_MODULE_SOURCE};
target_entry.limits_to_leaves.ac_max_phase_count = {phases, RPCAPI_MODULE_SOURCE};
target_entry.limits_to_leaves.ac_min_phase_count = {phases, RPCAPI_MODULE_SOURCE};
external_limits.schedule_import = std::vector<types::energy::ScheduleReqEntry>(1, target_entry);
external_limits.schedule_export = std::vector<types::energy::ScheduleReqEntry>(1, zero_entry);
return external_limits;
}
// This function is used to get the external limits for AC charging based on the current or power value.
static types::energy::ExternalLimits get_external_limits(float phy_value, bool is_power = false) {
const auto timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
types::energy::ExternalLimits external_limits;
types::energy::ScheduleReqEntry target_entry;
target_entry.timestamp = timestamp;
types::energy::ScheduleReqEntry zero_entry;
zero_entry.timestamp = timestamp;
zero_entry.limits_to_leaves.total_power_W = {0, RPCAPI_MODULE_SOURCE};
if (is_power) {
target_entry.limits_to_leaves.total_power_W = {std::abs(phy_value), RPCAPI_MODULE_SOURCE};
} else {
target_entry.limits_to_leaves.ac_max_current_A = {std::abs(phy_value), RPCAPI_MODULE_SOURCE};
}
if (phy_value > 0) {
external_limits.schedule_import = std::vector<types::energy::ScheduleReqEntry>(1, target_entry);
external_limits.schedule_export = std::vector<types::energy::ScheduleReqEntry>(1, zero_entry);
} else {
external_limits.schedule_export = std::vector<types::energy::ScheduleReqEntry>(1, target_entry);
external_limits.schedule_import = std::vector<types::energy::ScheduleReqEntry>(1, zero_entry);
}
return external_limits;
}
static types::energy::ExternalLimits get_external_limits(float phy_value, bool is_power, int32_t phases) {
const auto timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
types::energy::ExternalLimits external_limits;
types::energy::ScheduleReqEntry target_entry;
target_entry.timestamp = timestamp;
types::energy::ScheduleReqEntry zero_entry;
zero_entry.timestamp = timestamp;
target_entry.limits_to_leaves.ac_max_phase_count = {phases, RPCAPI_MODULE_SOURCE};
target_entry.limits_to_leaves.ac_min_phase_count = {phases, RPCAPI_MODULE_SOURCE};
if (is_power) {
target_entry.limits_to_leaves.total_power_W = {std::abs(phy_value), RPCAPI_MODULE_SOURCE};
zero_entry.limits_to_leaves.total_power_W = {0, RPCAPI_MODULE_SOURCE};
} else {
target_entry.limits_to_leaves.ac_max_current_A = {std::abs(phy_value), RPCAPI_MODULE_SOURCE};
zero_entry.limits_to_leaves.ac_max_current_A = {0, RPCAPI_MODULE_SOURCE};
}
if (phy_value > 0) {
external_limits.schedule_import = std::vector<types::energy::ScheduleReqEntry>(1, target_entry);
external_limits.schedule_export = std::vector<types::energy::ScheduleReqEntry>(1, zero_entry);
} else {
external_limits.schedule_export = std::vector<types::energy::ScheduleReqEntry>(1, target_entry);
external_limits.schedule_import = std::vector<types::energy::ScheduleReqEntry>(1, zero_entry);
}
return external_limits;
}
RpcApiRequestHandler::RpcApiRequestHandler(
data::DataStoreCharger& dataobj, const std::vector<std::unique_ptr<evse_managerIntf>>& r_evse_managers,
const std::vector<std::unique_ptr<external_energy_limitsIntf>>& r_evse_energy_sink) :
data_store(dataobj), evse_managers(r_evse_managers), evse_energy_sink(r_evse_energy_sink) {
}
ErrorResObj RpcApiRequestHandler::set_charging_allowed(const int32_t evse_index, bool charging_allowed) {
ErrorResObj res{};
bool success{true};
// find the EVSE manager for the given index
const auto it = std::find_if(evse_managers.begin(), evse_managers.end(), [&evse_index](const auto& manager) {
const auto mapping = manager->get_mapping();
return (mapping.has_value() && (mapping.value().evse == evse_index));
});
if (it == evse_managers.end()) {
res.error = ResponseErrorEnum::ErrorInvalidEVSEIndex;
EVLOG_warning << "No EVSE manager found for index: " << evse_index;
return res;
}
const auto& evse_manager = *it;
auto evse_store = data_store.get_evse_store(evse_index);
// If the charging allowed state is already set to the desired value, we can return early.
if (evse_store->evsestatus.get_data()->charging_allowed == charging_allowed) {
EVLOG_debug << "Charging allowed state for EVSE index: " << evse_index
<< " is already set to: " << charging_allowed;
res.error = ResponseErrorEnum::NoError;
return res;
}
// Determine the current state of the EVSE. In case the EVSE is currently charging, we can use
// the resume-pause methods to control the charging process.
const auto evse_state = evse_store->evsestatus.get_state();
const bool is_charging = (evse_state == types::json_rpc_api::EVSEStateEnum::Charging);
const bool is_charging_paused = (evse_state == types::json_rpc_api::EVSEStateEnum::ChargingPausedEVSE ||
evse_state == types::json_rpc_api::EVSEStateEnum::ChargingPausedEV);
bool is_power_limit = !configured_limits.is_current_set;
if (charging_allowed) {
float phy_limit{0.0f};
// first we need to determine which limits to apply. If the limit (current or power) is already set, we will use
// that.
if (configured_limits.evse_limit.has_value()) {
// If current is set, use the configured current limit
phy_limit = configured_limits.evse_limit.value();
} else {
// If no limits are set, use the default values. TODO: It would be better to get the default values from the
// EVSE manager.
phy_limit = 999.9f; // Default maximum current
is_power_limit = false;
}
// If the phases are not set, we assume DC charging. This means there is no need to apply phase limits.
ErrorResObj result = check_active_phases_and_set_limits(evse_index, phy_limit, is_power_limit);
if (result.error != ResponseErrorEnum::NoError) {
EVLOG_warning << "Failed to set external limits for EVSE index: " << evse_index
<< " with error: " << result.error;
res.error = result.error;
return res;
}
if (is_charging_paused) {
if (!evse_manager->call_resume_charging()) {
success = false;
EVLOG_warning << "Failed to resume charging for EVSE index: " << evse_index;
}
}
} else {
if (is_charging) {
// If charging is not allowed, we need to pause the charging process
if (!evse_manager->call_pause_charging()) {
success = false;
EVLOG_warning << "Failed to pause charging for EVSE index: " << evse_index;
}
} else {
// Additionally, in case the EVSE is not charging, we set the limits to 0 to prevent any charging from
// starting
float max_power{0.0f};
ErrorResObj res_limit =
set_external_limit(evse_index, max_power,
std::function<types::energy::ExternalLimits(float)>([is_power_limit](float value) {
return get_external_limits(value, is_power_limit);
}));
if (res_limit.error != ResponseErrorEnum::NoError) {
EVLOG_warning << "Failed to set external limits for EVSE index: " << evse_index
<< " with error: " << res_limit.error;
return res;
}
}
}
// Update the EVSE status in the data store
// TODO: Add a var to the EVSEManager to track if charging is allowed
evse_store->evsestatus.set_charging_allowed(charging_allowed);
if (success) {
res.error = ResponseErrorEnum::NoError;
EVLOG_debug << "Charging " << (charging_allowed ? "allowed" : "not allowed")
<< " for EVSE index: " << evse_index;
} else {
res.error = ResponseErrorEnum::ErrorValuesNotApplied;
EVLOG_warning << "Failed to set charging " << (charging_allowed ? "allowed" : "not allowed")
<< " for EVSE index: " << evse_index;
}
return res;
}
ErrorResObj RpcApiRequestHandler::set_ac_charging(const int32_t evse_index, bool charging_allowed, bool max_current,
std::optional<int> phase_count) {
(void)evse_index;
(void)charging_allowed;
(void)max_current;
(void)phase_count;
ErrorResObj res{};
res.error = ResponseErrorEnum::ErrorValuesNotApplied;
// TODO: Currently not implemented.
return res;
}
// template method to set the external limits depending on the type of value (current, power, or phase count)
template <typename T>
ErrorResObj RpcApiRequestHandler::set_external_limit(const int32_t evse_index, T value,
std::function<types::energy::ExternalLimits(T)> make_limits) {
ErrorResObj res{};
res.error = ResponseErrorEnum::NoError;
bool is_sink_configured;
try {
is_sink_configured = external_energy_limits::is_evse_sink_configured(evse_energy_sink, evse_index);
} catch (const std::runtime_error&) {
is_sink_configured = false;
}
if (is_sink_configured) {
auto& energy_sink = external_energy_limits::get_evse_sink_by_evse_id(evse_energy_sink, evse_index);
try {
const auto external_limits = make_limits(value);
energy_sink.call_set_external_limits(external_limits);
} catch (const std::invalid_argument& e) {
EVLOG_warning << "Invalid limit: No conversion of given input could be performed: " << e.what();
res.error = ResponseErrorEnum::ErrorInvalidParameter;
}
} else {
res.error = ResponseErrorEnum::ErrorInvalidEVSEIndex;
EVLOG_warning << "No EVSE energy sink configured for evse_index: " << evse_index
<< ". This module does therefore not allow control of amps or power limits for this EVSE";
}
return res;
}
ErrorResObj RpcApiRequestHandler::set_ac_charging_current(const int32_t evse_index, float max_current) {
configured_limits.is_current_set = true;
configured_limits.evse_limit = max_current;
ErrorResObj res{};
const auto evse_store = data_store.get_evse_store(evse_index);
// Skipping applying limits if charging is not allowed.
// In this case, the zero limit is already applied to prevent charging. This value should not be overridden.
if (evse_store->evsestatus.get_data()->charging_allowed == false) {
res.error = ResponseErrorEnum::NoError;
return res;
}
res = check_active_phases_and_set_limits(evse_index, max_current, false);
if (res.error != ResponseErrorEnum::NoError) {
return res;
}
// Wait until the limits are applied or timeout occurs
if (evse_store->evsestatus.wait_until_current_limit_applied(max_current, CURRENT_LIMIT_APPLY_TIMEOUT)) {
res.error = ResponseErrorEnum::NoError;
} else {
res.error = ResponseErrorEnum::ErrorValuesNotApplied;
}
return res;
}
ErrorResObj RpcApiRequestHandler::set_ac_charging_phase_count(const int32_t evse_index, int phase_count) {
ErrorResObj res{};
if (configured_limits.evse_limit.has_value()) {
// In case a current or power limit is already set, we need to consider that when applying the phase count
bool is_power = !configured_limits.is_current_set;
res = set_external_limit(
evse_index, configured_limits.evse_limit.value(),
std::function<types::energy::ExternalLimits(float)>([is_power, phase_count](float phy_value) {
return get_external_limits(phy_value, is_power, phase_count);
}));
} else {
// If no current or power limit is set, we can just apply the phase count
res = set_external_limit(evse_index, phase_count,
std::function<types::energy::ExternalLimits(int)>(
[](int value) { return get_external_limits(static_cast<int32_t>(value)); }));
}
return res;
}
ErrorResObj RpcApiRequestHandler::set_dc_charging(const int32_t evse_index, bool charging_allowed, float max_power) {
(void)evse_index;
(void)charging_allowed;
(void)max_power;
ErrorResObj res{};
res.error = ResponseErrorEnum::ErrorValuesNotApplied;
// TODO: Currently not implemented.
return res;
}
ErrorResObj RpcApiRequestHandler::set_dc_charging_power(const int32_t evse_index, float max_power) {
configured_limits.is_current_set = false;
configured_limits.evse_limit = max_power;
ErrorResObj res =
set_external_limit(evse_index, max_power, std::function<types::energy::ExternalLimits(float)>([](float value) {
return get_external_limits(value, true);
}));
return res;
}
ErrorResObj RpcApiRequestHandler::enable_connector(const int32_t evse_index, int connector_id, bool enable,
int priority) {
ErrorResObj res{};
const auto it = std::find_if(evse_managers.begin(), evse_managers.end(), [&evse_index](const auto& manager) {
const auto mapping = manager->get_mapping();
return (mapping.has_value() && (mapping.value().evse == evse_index));
});
if (it == evse_managers.end()) {
res.error = ResponseErrorEnum::ErrorInvalidEVSEIndex;
EVLOG_warning << "No EVSE manager found for index: " << evse_index;
return res;
}
const auto& evse_manager = *it;
types::evse_manager::EnableDisableSource cmd_source;
cmd_source.enable_source = types::evse_manager::Enable_source::LocalAPI;
cmd_source.enable_state =
enable ? types::evse_manager::Enable_state::Enable : types::evse_manager::Enable_state::Disable;
cmd_source.enable_priority = priority;
const bool result_enabled = evse_manager->call_enable_disable(connector_id, cmd_source);
if (result_enabled == enable) {
res.error = ResponseErrorEnum::NoError;
EVLOG_debug << "Connector " << connector_id << " on EVSE index: " << evse_index << " has been "
<< (enable ? "enabled" : "disabled") << " with priority: " << priority;
} else {
res.error = ResponseErrorEnum::ErrorValuesNotApplied;
EVLOG_warning << "Failed to enable/disable connector " << connector_id << " on EVSE index: " << evse_index;
}
return res;
}
types::json_rpc_api::ErrorResObj RpcApiRequestHandler::check_active_phases_and_set_limits(const int32_t evse_index,
const float phy_value,
const bool is_power) {
ErrorResObj res{};
int phases{0};
const auto evse_store = data_store.get_evse_store(evse_index);
if (const auto _data = evse_store->evsestatus.get_data(); _data.has_value()) {
phases = _data->ac_charge_status.has_value() ? _data->ac_charge_status.value().evse_active_phase_count : 0;
}
if (phases == 0) {
res = set_external_limit(evse_index, phy_value,
std::function<types::energy::ExternalLimits(float)>(
[is_power](float phy_value) { return get_external_limits(phy_value, is_power); }));
} else {
res =
set_external_limit(evse_index, phy_value,
std::function<types::energy::ExternalLimits(float)>([is_power, phases](float phy_value) {
return get_external_limits(phy_value, is_power, phases);
}));
}
return res;
}

View File

@@ -0,0 +1,50 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef RPCAPIREQUESTHANDLER_HPP
#define RPCAPIREQUESTHANDLER_HPP
#include <generated/interfaces/error_history/Interface.hpp>
#include <generated/interfaces/evse_manager/Interface.hpp>
#include <generated/interfaces/external_energy_limits/Interface.hpp>
#include "data/DataStore.hpp"
#include "rpc/RequestHandlerInterface.hpp"
class RpcApiRequestHandler : public request_interface::RequestHandlerInterface {
public:
// delete default constructor
RpcApiRequestHandler() = delete;
RpcApiRequestHandler(data::DataStoreCharger& data_store,
const std::vector<std::unique_ptr<evse_managerIntf>>& r_evse_managers,
const std::vector<std::unique_ptr<external_energy_limitsIntf>>& r_evse_energy_sink);
types::json_rpc_api::ErrorResObj set_charging_allowed(const int32_t evse_index, bool charging_allowed) override;
types::json_rpc_api::ErrorResObj set_ac_charging(const int32_t evse_index, bool charging_allowed, bool max_current,
std::optional<int> phase_count) override;
types::json_rpc_api::ErrorResObj set_ac_charging_current(const int32_t evse_index, float max_current) override;
types::json_rpc_api::ErrorResObj set_ac_charging_phase_count(const int32_t evse_index, int phase_count) override;
types::json_rpc_api::ErrorResObj set_dc_charging(const int32_t evse_index, bool charging_allowed,
float max_power) override;
types::json_rpc_api::ErrorResObj set_dc_charging_power(const int32_t evse_index, float max_power) override;
types::json_rpc_api::ErrorResObj enable_connector(const int32_t evse_index, int connector_id, bool enable,
int priority) override;
private:
// Add any private member variables or methods here
data::DataStoreCharger& data_store;
types::json_rpc_api::ErrorResObj check_active_phases_and_set_limits(const int32_t evse_index, const float phy_value,
const bool is_power);
template <typename T>
types::json_rpc_api::ErrorResObj set_external_limit(int32_t evse_index, T value,
std::function<types::energy::ExternalLimits(T)> make_limits);
const std::vector<std::unique_ptr<evse_managerIntf>>& evse_managers;
const std::vector<std::unique_ptr<external_energy_limitsIntf>>& evse_energy_sink;
struct {
std::optional<float> evse_limit; ///< Maximum current or power limit for the EVSE
bool is_current_set = false; ///< Flag to indicate if current or power limit is set
} configured_limits;
};
#endif // RPCAPIREQUESTHANDLER_HPP

View File

@@ -0,0 +1,116 @@
#[=======================================================================[.rst:
Findjson-rpc-cxx
----------------
Finds the json-rpc-cxx library.
json-rpc-cxx is a header-only JSON-RPC 2.0 framework implemented in C++17
using nlohmann's json for modern C++.
Imported Targets
^^^^^^^^^^^^^^^^
This module provides the following imported targets, if found:
``json-rpc-cxx::json-rpc-cxx``
The json-rpc-cxx library
``json-rpc-cxx``
Alias for json-rpc-cxx::json-rpc-cxx
Result Variables
^^^^^^^^^^^^^^^^
This will define the following variables:
``json-rpc-cxx_FOUND``
True if the system has the json-rpc-cxx library.
``json-rpc-cxx_INCLUDE_DIRS``
Include directories needed to use json-rpc-cxx.
Cache Variables
^^^^^^^^^^^^^^^
The following cache variables may also be set:
``json-rpc-cxx_INCLUDE_DIR``
The directory containing ``jsonrpccxx/common.hpp``.
#]=======================================================================]
# Find nlohmann_json dependency first
find_package(nlohmann_json QUIET)
if(NOT nlohmann_json_FOUND)
# Try to find nlohmann/json with alternative names
find_package(nlohmann-json QUIET)
endif()
# Fallback for environments where nlohmann_json CMake package is not provided
find_path(json-rpc-cxx_NLOHMANN_JSON_INCLUDE_DIR
NAMES nlohmann/json.hpp
DOC "nlohmann_json include directory"
)
set(json-rpc-cxx_HAS_NLOHMANN FALSE)
if(TARGET nlohmann_json::nlohmann_json OR TARGET nlohmann_json)
set(json-rpc-cxx_HAS_NLOHMANN TRUE)
elseif(json-rpc-cxx_NLOHMANN_JSON_INCLUDE_DIR)
set(json-rpc-cxx_HAS_NLOHMANN TRUE)
endif()
# Look for the header file
find_path(json-rpc-cxx_INCLUDE_DIR
NAMES jsonrpccxx/common.hpp
HINTS
${json-rpc-cxx_ROOT}
${json-rpc-cxx_ROOT}/include
$ENV{json-rpc-cxx_ROOT}
$ENV{json-rpc-cxx_ROOT}/include
DOC "json-rpc-cxx include directory"
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(json-rpc-cxx
FOUND_VAR json-rpc-cxx_FOUND
REQUIRED_VARS
json-rpc-cxx_INCLUDE_DIR
json-rpc-cxx_HAS_NLOHMANN
)
if(json-rpc-cxx_FOUND)
set(json-rpc-cxx_INCLUDE_DIRS ${json-rpc-cxx_INCLUDE_DIR})
if(json-rpc-cxx_NLOHMANN_JSON_INCLUDE_DIR)
list(APPEND json-rpc-cxx_INCLUDE_DIRS ${json-rpc-cxx_NLOHMANN_JSON_INCLUDE_DIR})
endif()
# Create imported target
if(NOT TARGET json-rpc-cxx::json-rpc-cxx)
add_library(json-rpc-cxx::json-rpc-cxx INTERFACE IMPORTED)
set_target_properties(json-rpc-cxx::json-rpc-cxx PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${json-rpc-cxx_INCLUDE_DIRS}"
)
# Add C++17 requirement
set_target_properties(json-rpc-cxx::json-rpc-cxx PROPERTIES
INTERFACE_COMPILE_FEATURES cxx_std_17
)
# Link nlohmann_json dependency if found
if(TARGET nlohmann_json::nlohmann_json)
set_target_properties(json-rpc-cxx::json-rpc-cxx PROPERTIES
INTERFACE_LINK_LIBRARIES nlohmann_json::nlohmann_json
)
elseif(TARGET nlohmann_json)
set_target_properties(json-rpc-cxx::json-rpc-cxx PROPERTIES
INTERFACE_LINK_LIBRARIES nlohmann_json
)
endif()
# Create alias for simpler target name
add_library(json-rpc-cxx ALIAS json-rpc-cxx::json-rpc-cxx)
endif()
endif()
mark_as_advanced(
json-rpc-cxx_INCLUDE_DIR
json-rpc-cxx_NLOHMANN_JSON_INCLUDE_DIR
)

View File

@@ -0,0 +1,419 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "DataStore.hpp"
#include "GenericInfoStore.hpp"
#include "SessionInfo.hpp"
#include <everest/logging.hpp>
#include <sstream>
#include <string_view>
namespace data {
static bool almost_equal(float a, float b, float epsilon = std::numeric_limits<float>::epsilon() * 100) {
return std::fabs(a - b) <= epsilon * std::fmax(1.0f, std::fmax(std::fabs(a), std::fabs(b)));
}
namespace {
constexpr std::array<std::string_view, NUMBER_OF_EVSE_STATUS_FIELDS> evse_status_field_names{
"active_connector_index", "charging_allowed", "state",
"error_present", "charge_protocol", "charging_duration_s",
"charged_energy_wh", "discharged_energy_wh", "available"};
static_assert(evse_status_field_names.size() == NUMBER_OF_EVSE_STATUS_FIELDS,
"evse_status_field_names size should be in sync with EVSEStatusField enum definition");
} // namespace
// we currently don't get this info from the system yet, so allow setting to unknown
void ChargerInfoStore::set_unknown() {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->dataobj.vendor = "unknown";
this->dataobj.model = "unknown";
this->dataobj.serial = "unknown";
this->dataobj.firmware_version = "unknown";
// pretend we got something
this->data_is_valid = true;
}
void ChargerErrorsStore::add_error(const types::json_rpc_api::ErrorObj& error) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
// Check if the error already exists in the vector
for (const auto& existing_error : this->dataobj) {
if (existing_error.uuid == error.uuid) {
// Error already exists, no need to add it again
throw std::runtime_error("Error with UUID " + error.uuid + " already exists in the store.");
}
}
this->dataobj.push_back(error);
this->data_is_valid = true; // set the data as valid, since we have a valid error now
data_lock.unlock();
// Notify that data has changed
this->notify_data_changed();
}
void ChargerErrorsStore::clear_error(const types::json_rpc_api::ErrorObj& error) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
// Find and remove the error from the vector
for (auto it = this->dataobj.begin(); it != this->dataobj.end(); ++it) {
// String comparison for uuid
if (it->uuid == error.uuid) {
this->dataobj.erase(it);
data_lock.unlock();
// Notify that data has changed
this->notify_data_changed();
return; // Exit after removing the first matching error
}
}
}
void EVSEInfoStore::set_supported_energy_transfer_modes(
const std::vector<types::json_rpc_api::EnergyTransferModeEnum>& supported_energy_transfer_modes) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->dataobj.supported_energy_transfer_modes = supported_energy_transfer_modes;
}
void EVSEInfoStore::set_index(int32_t index) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->dataobj.index = index;
}
void EVSEInfoStore::set_id(const std::string& id) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->dataobj.id = id;
this->data_is_valid = true; // set the data as valid, since we have a valid id now
}
void EVSEInfoStore::set_available_connectors(const std::vector<RPCDataTypes::ConnectorInfoObj>& connectors) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->dataobj.available_connectors = connectors;
}
void EVSEInfoStore::set_available_connector(types::json_rpc_api::ConnectorInfoObj& available_connector) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
// Iterate through the vector and set the connector with the given index
for (auto& connector : this->dataobj.available_connectors) {
if (connector.index == available_connector.index) {
connector = available_connector; // Update the existing connector
return;
}
}
// If the connector with the given index is not found, add it to the vector
this->dataobj.available_connectors.push_back(available_connector);
}
bool EVSEInfoStore::get_is_ac_transfer_mode() const {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
return this->is_ac_transfer_mode;
}
int32_t EVSEInfoStore::get_index() {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
return this->dataobj.index;
}
void EVSEInfoStore::set_is_ac_transfer_mode(bool is_ac) {
this->is_ac_transfer_mode = is_ac;
}
std::vector<types::json_rpc_api::ConnectorInfoObj> EVSEInfoStore::get_available_connectors() const {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
return this->dataobj.available_connectors;
}
void EVSEStatusStore::update_data_is_valid() {
// shall be called only when a lock on this->data_mutex is held
if (this->data_is_valid) {
return; // No need to update if data is already valid
}
std::ostringstream missing_fields;
bool has_missing_fields{false};
for (size_t i = 0; i < this->field_status.size(); ++i) {
if (!this->field_status.test(i)) {
if (has_missing_fields) {
missing_fields << ',';
}
missing_fields << evse_status_field_names.at(i);
has_missing_fields = true;
}
}
this->data_is_valid = !has_missing_fields;
if (has_missing_fields) {
EVLOG_debug << "EVSEStatusStore: Missing fields [" << missing_fields.str() << "], data is invalid.";
} else {
EVLOG_debug << "EVSEStatusStore: All required fields are set, data is now valid.";
}
}
void EVSEStatusStore::set_field_status(EVSEStatusField field) {
this->field_status.set(to_underlying_value(field));
}
void EVSEStatusStore::set_ac_charge_param_evse_max_current(float current_limit) {
std::unique_lock<std::mutex> cv_lock(mtx_current_limit_applied);
// current_limit with 0 is not valid and means internally that no energy is
// available. The energy available state is already notified via the EVSE state, thus it
// is not necessary to forward current_limit=0 to the API clients
if (current_limit == 0.0f) {
return;
}
this->configured_current_limit = current_limit;
// Check if a new current limit is requested from the API
if (this->requested_current_limit != 0.0f) {
// Check if the requested limit is applied
this->cv_current_limit_applied.notify_all();
// We are skipping applying the new current limit, as long as a new limit is requested from the API and not yet
// applied
return;
}
// Apply the new current limit
EVLOG_debug << "Applying new current limit: " << this->configured_current_limit;
this->set_ac_charge_param_evse_current_limit_internal(this->configured_current_limit);
}
bool EVSEStatusStore::wait_until_current_limit_applied(float requested_limit, std::chrono::milliseconds timeout_ms) {
std::unique_lock<std::mutex> lock(mtx_current_limit_applied);
bool is_current_limit_applied{false};
this->requested_current_limit = requested_limit;
if (this->cv_current_limit_applied.wait_for(lock, timeout_ms, [this] {
return almost_equal(this->configured_current_limit, this->requested_current_limit);
})) {
this->requested_current_limit = 0.0f; // reset the request
is_current_limit_applied = true;
EVLOG_debug << "Current limit applied: " << this->configured_current_limit
<< " (requested: " << this->requested_current_limit << ")";
} else {
EVLOG_debug << "timed out waiting for current limit to be applied, configured: "
<< this->configured_current_limit << ", requested: " << this->requested_current_limit;
// If there is already a new limit configured, notify it now
this->requested_current_limit = 0.0f; // reset the request
set_ac_charge_param_evse_current_limit_internal(this->configured_current_limit);
}
return is_current_limit_applied;
}
void EVSEStatusStore::set_ac_charge_param_evse_max_phase_count(int32_t phase_count) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
auto& ac_charge_param = this->dataobj.ac_charge_param;
if (!ac_charge_param.has_value()) {
ac_charge_param.emplace();
}
auto& evse_phase_count = ac_charge_param.value().evse_max_phase_count;
if (evse_phase_count != phase_count) {
evse_phase_count = phase_count;
data_lock.unlock();
this->notify_data_changed();
}
}
void EVSEStatusStore::set_ac_charge_param_evse_current_limit_internal(float max_current) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
auto& ac_charge_param = this->dataobj.ac_charge_param;
if (!ac_charge_param.has_value()) {
ac_charge_param.emplace();
}
auto& evse_max_current = ac_charge_param.value().evse_max_current;
if (!almost_equal(evse_max_current, max_current)) {
evse_max_current = max_current;
data_lock.unlock();
this->notify_data_changed();
}
}
EVSEStatusStore::EVSEStatusStore() {
// Initialize data store with default values
this->set_charging_duration_s(0);
this->set_charged_energy_wh(0.0f);
this->set_discharged_energy_wh(0.0f);
this->set_error_present(false);
this->set_charging_allowed(true);
this->set_available(true);
}
// Example set method using the enum
void EVSEStatusStore::set_active_connector_index(int32_t active_connector_index) {
std::unique_lock<std::mutex> data_lock(this->data_mutex); // check if data has changed
this->set_field_status(EVSEStatusField::ActiveConnectorIndex);
this->update_data_is_valid();
// check if data has changed
if (this->dataobj.active_connector_index != active_connector_index) {
this->dataobj.active_connector_index = active_connector_index;
data_lock.unlock();
this->notify_data_changed();
}
}
// set the charging allowed flag
void EVSEStatusStore::set_charging_allowed(bool charging_allowed) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->set_field_status(EVSEStatusField::ChargingAllowed);
this->update_data_is_valid();
// check if data has changed
if (this->dataobj.charging_allowed != charging_allowed) {
this->dataobj.charging_allowed = charging_allowed;
data_lock.unlock();
this->notify_data_changed();
}
}
// set the EVSE state
void EVSEStatusStore::set_state(types::json_rpc_api::EVSEStateEnum state) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->set_field_status(EVSEStatusField::State);
this->update_data_is_valid();
// check if data has changed
if (this->dataobj.state != state) {
this->dataobj.state = state;
data_lock.unlock();
this->notify_data_changed();
}
}
// set EVSE errors
void EVSEStatusStore::set_error_present(const bool error_present) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->set_field_status(EVSEStatusField::ErrorPresent);
this->update_data_is_valid();
// check if data has changed
if (this->dataobj.error_present != error_present) {
this->dataobj.error_present = error_present;
data_lock.unlock();
this->notify_data_changed();
}
}
// set the charge protocol
void EVSEStatusStore::set_charge_protocol(types::json_rpc_api::ChargeProtocolEnum charge_protocol) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->set_field_status(EVSEStatusField::ChargeProtocol);
this->update_data_is_valid();
// check if data has changed
if (this->dataobj.charge_protocol != charge_protocol) {
this->dataobj.charge_protocol = charge_protocol;
data_lock.unlock();
this->notify_data_changed();
}
}
// set the charging duration in seconds
void EVSEStatusStore::set_charging_duration_s(int32_t charging_duration_s) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->set_field_status(EVSEStatusField::ChargingDurationS);
this->update_data_is_valid();
// check if data has changed
if (this->dataobj.charging_duration_s != charging_duration_s) {
this->dataobj.charging_duration_s = charging_duration_s;
data_lock.unlock();
this->notify_data_changed();
}
}
// set the charged energy in Wh
void EVSEStatusStore::set_charged_energy_wh(float charged_energy_wh) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->set_field_status(EVSEStatusField::ChargedEnergyWh);
this->update_data_is_valid();
// check if data has changed
if (this->dataobj.charged_energy_wh != charged_energy_wh) {
this->dataobj.charged_energy_wh = charged_energy_wh;
data_lock.unlock();
this->notify_data_changed();
}
}
// set the discharged energy in Wh
void EVSEStatusStore::set_discharged_energy_wh(float discharged_energy_wh) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->set_field_status(EVSEStatusField::DischargedEnergyWh);
this->update_data_is_valid();
// check if data has changed
if (this->dataobj.discharged_energy_wh != discharged_energy_wh) {
this->dataobj.discharged_energy_wh = discharged_energy_wh;
data_lock.unlock();
this->notify_data_changed();
}
}
// set the available flag
void EVSEStatusStore::set_available(bool available) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
this->set_field_status(EVSEStatusField::Available);
this->update_data_is_valid();
// check if data has changed
if (this->dataobj.available != available) {
this->dataobj.available = available;
data_lock.unlock();
this->notify_data_changed();
}
}
// set the AC charge parameters
void EVSEStatusStore::set_ac_charge_param(const std::optional<RPCDataTypes::ACChargeParametersObj>& ac_charge_param) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
// check if data has changed
if (this->dataobj.ac_charge_param != ac_charge_param) {
this->dataobj.ac_charge_param = ac_charge_param;
data_lock.unlock();
this->notify_data_changed();
}
}
// set the DC charge parameters
void EVSEStatusStore::set_dc_charge_param(const std::optional<RPCDataTypes::DCChargeParametersObj>& dc_charge_param) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
// check if data has changed
if (this->dataobj.dc_charge_param != dc_charge_param) {
this->dataobj.dc_charge_param = dc_charge_param;
data_lock.unlock();
this->notify_data_changed();
}
}
// set the AC charge status
void EVSEStatusStore::set_ac_charge_status(const std::optional<RPCDataTypes::ACChargeStatusObj>& ac_charge_status) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
// check if data has changed
if (this->dataobj.ac_charge_status != ac_charge_status) {
this->dataobj.ac_charge_status = ac_charge_status;
data_lock.unlock();
this->notify_data_changed();
}
}
// set the DC charge status
void EVSEStatusStore::set_dc_charge_status(const std::optional<RPCDataTypes::DCChargeStatusObj>& dc_charge_status) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
// check if data has changed
if (this->dataobj.dc_charge_status != dc_charge_status) {
this->dataobj.dc_charge_status = dc_charge_status;
data_lock.unlock();
this->notify_data_changed();
}
}
// set the display parameters
void EVSEStatusStore::set_display_parameters(
const std::optional<RPCDataTypes::DisplayParametersObj>& display_parameters) {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
// check if data has changed
if (this->dataobj.display_parameters != display_parameters) {
this->dataobj.display_parameters = display_parameters;
data_lock.unlock();
this->notify_data_changed();
}
}
types::json_rpc_api::EVSEStateEnum EVSEStatusStore::get_state() const {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
return this->dataobj.state;
}
std::optional<RPCDataTypes::ACChargeParametersObj> EVSEStatusStore::get_ac_charge_param() {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
return this->dataobj.ac_charge_param;
}
std::optional<RPCDataTypes::DCChargeParametersObj> EVSEStatusStore::get_dc_charge_param() {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
return this->dataobj.dc_charge_param;
}
} // namespace data

View File

@@ -0,0 +1,167 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef DATASTORE_HPP
#define DATASTORE_HPP
#include "GenericInfoStore.hpp"
#include "SessionInfo.hpp"
#include <atomic>
#include <bitset>
#include <chrono>
#include <condition_variable>
#include <everest/logging.hpp>
#include <type_traits>
namespace RPCDataTypes = types::json_rpc_api;
namespace data {
enum class EVSEStatusField {
ActiveConnectorIndex,
ChargingAllowed,
State,
ErrorPresent,
ChargeProtocol,
ChargingDurationS,
ChargedEnergyWh,
DischargedEnergyWh,
Available,
Count
};
template <typename T> constexpr auto to_underlying_value(T value) {
return static_cast<std::underlying_type_t<T>>(value);
}
static constexpr auto NUMBER_OF_EVSE_STATUS_FIELDS = to_underlying_value(EVSEStatusField::Count);
static_assert(NUMBER_OF_EVSE_STATUS_FIELDS == to_underlying_value(EVSEStatusField::Available) + 1,
"NUMBER_OF_EVSE_STATUS_FIELDS should be in sync with EVSEStatusField enum definition");
class ChargerInfoStore : public GenericInfoStore<RPCDataTypes::ChargerInfoObj> {
public:
// we currently don't get this info from the system yet, so allow setting to unknown
void set_unknown();
};
class ChargerErrorsStore : public GenericInfoStore<std::vector<types::json_rpc_api::ErrorObj>> {
public:
void add_error(const types::json_rpc_api::ErrorObj& error);
void clear_error(const types::json_rpc_api::ErrorObj& error);
};
class EVSEInfoStore : public GenericInfoStore<RPCDataTypes::EVSEInfoObj> {
public:
void set_supported_energy_transfer_modes(
const std::vector<types::json_rpc_api::EnergyTransferModeEnum>& supported_energy_transfer_modes);
void set_index(int32_t index);
void set_id(const std::string& id);
void set_available_connectors(const std::vector<RPCDataTypes::ConnectorInfoObj>& connectors);
void set_available_connector(types::json_rpc_api::ConnectorInfoObj& available_connector);
void set_is_ac_transfer_mode(bool is_ac);
bool get_is_ac_transfer_mode() const;
int32_t get_index();
std::vector<types::json_rpc_api::ConnectorInfoObj> get_available_connectors() const;
private:
std::atomic<bool> is_ac_transfer_mode;
};
class EVSEStatusStore : public GenericInfoStore<RPCDataTypes::EVSEStatusObj> {
private:
std::bitset<NUMBER_OF_EVSE_STATUS_FIELDS> field_status{0};
std::condition_variable cv_current_limit_applied;
std::mutex mtx_current_limit_applied;
float requested_current_limit{0.0f};
float configured_current_limit{0.0f};
void update_data_is_valid();
void set_field_status(EVSEStatusField field);
// Internal method to set the current limit in the AC charge parameters without checking for pending requests
void set_ac_charge_param_evse_current_limit_internal(float max_current);
public:
EVSEStatusStore();
// Set the active connector index
void set_active_connector_index(int32_t active_connector_index);
// set the charging allowed flag
void set_charging_allowed(bool charging_allowed);
// set the EVSE state
void set_state(types::json_rpc_api::EVSEStateEnum state);
// set EVSE errors
void set_error_present(const bool error_present);
// set the charge protocol
void set_charge_protocol(types::json_rpc_api::ChargeProtocolEnum charge_protocol);
// set the charging duration in seconds
void set_charging_duration_s(int32_t charging_duration_s);
// set the charged energy in Wh
void set_charged_energy_wh(float charged_energy_wh);
// set the discharged energy in Wh
void set_discharged_energy_wh(float discharged_energy_wh);
// set the available flag
void set_available(bool available);
// set the AC charge parameters
void set_ac_charge_param(const std::optional<RPCDataTypes::ACChargeParametersObj>& ac_charge_param);
// set the DC charge parameters
void set_dc_charge_param(const std::optional<RPCDataTypes::DCChargeParametersObj>& dc_charge_param);
// set the AC charge status
void set_ac_charge_status(const std::optional<RPCDataTypes::ACChargeStatusObj>& ac_charge_status);
// set the DC charge status
void set_dc_charge_status(const std::optional<RPCDataTypes::DCChargeStatusObj>& dc_charge_status);
// set the display parameters
void set_display_parameters(const std::optional<RPCDataTypes::DisplayParametersObj>& display_parameters);
// set the AC max phase count in the AC charge parameters
void set_ac_charge_param_evse_max_phase_count(int32_t phase_count);
// set the AC current limit and notify any waiting request threads
void set_ac_charge_param_evse_max_current(float current_limit);
// wait until the current limit is applied or timeout occurs
bool wait_until_current_limit_applied(float current_limit, std::chrono::milliseconds timeout_ms);
types::json_rpc_api::EVSEStateEnum get_state() const;
std::optional<RPCDataTypes::ACChargeParametersObj> get_ac_charge_param();
std::optional<RPCDataTypes::DCChargeParametersObj> get_dc_charge_param();
};
class HardwareCapabilitiesStore : public GenericInfoStore<RPCDataTypes::HardwareCapabilitiesObj> {};
class MeterDataStore : public GenericInfoStore<RPCDataTypes::MeterDataObj> {};
// This is the data store for a single EVSE. An EVSE can have multiple connectors.
struct DataStoreEvse {
EVSEInfoStore evseinfo;
EVSEStatusStore evsestatus;
MeterDataStore meterdata;
HardwareCapabilitiesStore hardwarecapabilities;
SessionInfoStore sessioninfo;
};
// This is the main data store for the charger. A charger can have multiple EVSEs, each with multiple connectors.
// For more information see 3-Tier model definition of OCPP 2.0.
struct DataStoreCharger {
ChargerInfoStore chargerinfo;
ChargerErrorsStore chargererrors;
std::string everest_version;
std::vector<std::unique_ptr<DataStoreEvse>> evses;
// get the EVSE data with a specific id
data::DataStoreEvse* get_evse_store(const int32_t evse_index) {
if (evses.empty()) {
EVLOG_error << "No EVSEs found in the data store.";
return nullptr;
}
for (const auto& evse : evses) {
const auto tmp_index = evse->evseinfo.get_index();
if (tmp_index == evse_index) {
return evse.get();
}
}
EVLOG_error << "EVSE index " << evse_index << " not found in data store.";
return nullptr;
}
};
} // namespace data
#endif // DATASTORE_HPP

View File

@@ -0,0 +1,82 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef GENERICINFOSTORE_HPP
#define GENERICINFOSTORE_HPP
#include <atomic>
#include <functional> // for std::function
#include <mutex>
#include <nlohmann/json.hpp>
#include <optional>
#include <types/json_rpc_api/json_rpc_api.hpp>
#include <vector>
// This contains types for all the data objects
namespace data {
template <typename T> class GenericInfoStore {
protected:
// the associated data store
T dataobj;
// protect the data object
// NB: mutable in order to be able to lock the mutex in const functions
mutable std::mutex data_mutex;
// function to call when changes occurred
std::function<void(const decltype(dataobj)&)> notification_callback;
// override this if structures need special (non-default) initialization
virtual void init_data(){};
// whether the non-optional values are valid, so that the RPC interface can generate an error
std::atomic<bool> data_is_valid{false};
public:
explicit GenericInfoStore() {
this->init_data();
};
// if the returned value has no value, the data is incomplete or not available
std::optional<T> get_data() const {
if (this->data_is_valid) {
std::unique_lock<std::mutex> data_lock{this->data_mutex};
return this->dataobj;
} else {
return std::nullopt;
}
}
// set the data object. This method may need to be overridden with custom copy functions if the data
// object is not a simple type e.g. pointers and has no copy/assignment operator
// Note: all setters, also in derived classes, must use the data mutex
// e.g. std::unique_lock<std::mutex> data_lock(this->data_mutex)
virtual void set_data(const T& in) {
// check for changes
std::unique_lock<std::mutex> data_lock(this->data_mutex);
if (in != this->dataobj) {
this->dataobj = in;
this->data_is_valid = true;
data_lock.unlock();
// call the notification callback if it is set
notify_data_changed();
}
}
// notify that data has changed
void notify_data_changed() {
std::unique_lock<std::mutex> data_lock(this->data_mutex);
if (this->notification_callback && this->data_is_valid) {
// create a copy of the data object
T data_copy = this->dataobj;
// unlock explicitly before entering callback
data_lock.unlock();
this->notification_callback(data_copy);
}
}
// register a callback which is triggered when any data in the associated data store changes
void register_notification_callback(const std::function<void(const T&)>& callback) {
this->notification_callback = callback;
}
};
} // namespace data
#endif // GENERICINFOSTORE_HPP

View File

@@ -0,0 +1,196 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "SessionInfo.hpp"
namespace data {
static void to_json(json& j, const SessionInfoStore::Error& e) {
j = json{{"type", e.type}, {"description", e.description}, {"severity", e.severity}};
}
SessionInfoStore::SessionInfoStore() :
start_energy_import_wh(0),
end_energy_import_wh(0),
start_energy_export_wh(0),
end_energy_export_wh(0),
latest_total_w(0),
state(State::Unknown) {
this->start_time_point = date::utc_clock::now();
this->end_time_point = this->start_time_point;
uk_random_delay_remaining.countdown_s = 0;
uk_random_delay_remaining.current_limit_after_delay_A = 0.;
uk_random_delay_remaining.current_limit_during_delay_A = 0;
}
bool SessionInfoStore::is_state_charging(const SessionInfoStore::State current_state) {
if (current_state == State::AuthRequired || current_state == State::Charging ||
current_state == State::ChargingPausedEV || current_state == State::ChargingPausedEVSE) {
return true;
}
return false;
}
void SessionInfoStore::reset() {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
this->state = State::Unknown;
this->start_energy_import_wh = 0;
this->end_energy_import_wh = 0;
this->start_energy_export_wh = 0;
this->end_energy_export_wh = 0;
this->start_time_point = date::utc_clock::now();
this->latest_total_w = 0;
this->permanent_fault = false;
}
void SessionInfoStore::update_state(const types::evse_manager::SessionEvent event) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
using Event = types::evse_manager::SessionEventEnum;
// using switch since some code analysis tools can detect missing cases
// (when new events are added)
switch (event.event) {
case Event::Enabled:
this->state = State::Unplugged;
break;
case Event::Disabled:
this->state = State::Disabled;
break;
case Event::AuthRequired:
this->state = State::AuthRequired;
break;
case Event::Authorized:
[[fallthrough]];
case Event::PrepareCharging:
[[fallthrough]];
case Event::SessionStarted:
[[fallthrough]];
case Event::SessionResumed:
[[fallthrough]];
case Event::TransactionStarted:
this->state = State::Preparing;
break;
case Event::ChargingStarted:
this->state = State::Charging;
break;
case Event::ChargingPausedEV:
this->state = State::ChargingPausedEV;
break;
case Event::ChargingPausedEVSE:
this->state = State::ChargingPausedEVSE;
break;
case Event::ChargingFinished:
this->state = State::Finished;
break;
case Event::StoppingCharging:
this->state = State::FinishedEV;
break;
case Event::TransactionFinished: {
if (event.transaction_finished->reason == types::evse_manager::StopTransactionReason::Local) {
this->state = State::FinishedEVSE;
} else {
this->state = State::Finished;
}
break;
}
case Event::PluginTimeout:
this->state = State::AuthTimeout;
break;
case Event::ReservationStart:
this->state = State::Reserved;
break;
case Event::ReservationEnd:
[[fallthrough]];
case Event::SessionFinished:
this->state = State::Unplugged;
break;
/// explicitly fall through all the SessionEventEnum values we are not handling
case Event::Deauthorized:
[[fallthrough]];
case Event::SwitchingPhases:
[[fallthrough]];
default:
break;
}
}
void SessionInfoStore::set_start_energy_import_wh(int32_t start_energy_import_wh) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
this->start_energy_import_wh = start_energy_import_wh;
this->end_energy_import_wh = start_energy_import_wh;
this->start_time_point = date::utc_clock::now();
this->end_time_point = this->start_time_point;
}
void SessionInfoStore::set_end_energy_import_wh(int32_t end_energy_import_wh) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
this->end_energy_import_wh = end_energy_import_wh;
this->end_time_point = date::utc_clock::now();
}
void SessionInfoStore::set_latest_energy_import_wh(int32_t latest_energy_wh) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
if (this->is_state_charging(this->state)) {
this->end_time_point = date::utc_clock::now();
this->end_energy_import_wh = latest_energy_wh;
}
}
void SessionInfoStore::set_start_energy_export_wh(int32_t start_energy_export_wh) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
this->start_energy_export_wh = start_energy_export_wh;
this->end_energy_export_wh = start_energy_export_wh;
this->start_energy_export_wh_was_set = true;
}
void SessionInfoStore::set_end_energy_export_wh(int32_t end_energy_export_wh) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
this->end_energy_export_wh = end_energy_export_wh;
this->end_energy_export_wh_was_set = true;
}
void SessionInfoStore::set_latest_energy_export_wh(int32_t latest_export_energy_wh) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
if (this->is_state_charging(this->state)) {
this->end_energy_export_wh = latest_export_energy_wh;
this->end_energy_export_wh_was_set = true;
}
}
void SessionInfoStore::set_latest_total_w(double latest_total_w) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
this->latest_total_w = latest_total_w;
}
void SessionInfoStore::set_uk_random_delay_remaining(const types::uk_random_delay::CountDown& cd) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
this->uk_random_delay_remaining = cd;
}
void SessionInfoStore::set_enable_disable_source(const std::string& active_source, const std::string& active_state,
const int active_priority) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
this->active_enable_disable_source = active_source;
this->active_enable_disable_state = active_state;
this->active_enable_disable_priority = active_priority;
}
float SessionInfoStore::get_charged_energy_wh() const {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
return this->end_energy_import_wh - this->start_energy_import_wh;
}
float SessionInfoStore::get_discharged_energy_wh() const {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
if (this->start_energy_export_wh_was_set && this->end_energy_export_wh_was_set) {
return this->end_energy_export_wh - this->start_energy_export_wh;
}
return 0.0f; // No discharged energy if export values are not set
}
std::chrono::seconds SessionInfoStore::get_charging_duration_s() const {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
return std::chrono::duration_cast<std::chrono::seconds>(this->end_time_point - this->start_time_point);
}
} // namespace data

View File

@@ -0,0 +1,97 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
// Description: This file defines the SessionInfoStore class, which is used to manage session information for EV
// charging sessions. It includes methods to update session state, set energy readings, and calculate charged and
// discharged energy. The code of SessionInfoStore class is mostly a copy of the EVerest API module.
#ifndef SESSIONINFO_HPP
#define SESSIONINFO_HPP
// headers for required interface implementations
#include <generated/types/evse_manager.hpp>
#include <generated/types/uk_random_delay.hpp>
// insert your custom include headers here
#include <date/date.h>
#include <date/tz.h>
#include <mutex>
namespace data {
class SessionInfoStore {
public:
SessionInfoStore();
struct Error {
std::string type;
std::string description;
std::string severity;
};
bool start_energy_export_wh_was_set{
false}; ///< Indicate if start export energy value (optional) has been received or not
bool end_energy_export_wh_was_set{
false}; ///< Indicate if end export energy value (optional) has been received or not
void reset();
void update_state(const types::evse_manager::SessionEvent event);
void set_start_energy_import_wh(int32_t start_energy_import_wh);
void set_end_energy_import_wh(int32_t end_energy_import_wh);
void set_latest_energy_import_wh(int32_t latest_energy_wh);
void set_start_energy_export_wh(int32_t start_energy_export_wh);
void set_end_energy_export_wh(int32_t end_energy_export_wh);
void set_latest_energy_export_wh(int32_t latest_export_energy_wh);
void set_latest_total_w(double latest_total_w);
void set_uk_random_delay_remaining(const types::uk_random_delay::CountDown& c);
void set_enable_disable_source(const std::string& active_source, const std::string& active_state,
const int active_priority);
void set_permanent_fault(bool f) {
permanent_fault = f;
}
float get_charged_energy_wh() const;
float get_discharged_energy_wh() const;
std::chrono::seconds get_charging_duration_s() const;
/// \brief Converts this struct into a serialized json object
operator std::string();
private:
mutable std::mutex session_info_mutex;
int32_t start_energy_import_wh; ///< Energy reading (import) at the beginning of this charging session in Wh
int32_t end_energy_import_wh; ///< Energy reading (import) at the end of this charging session in Wh
int32_t start_energy_export_wh; ///< Energy reading (export) at the beginning of this charging session in Wh
int32_t end_energy_export_wh; ///< Energy reading (export) at the end of this charging session in Wh
types::uk_random_delay::CountDown uk_random_delay_remaining; ///< Remaining time of a UK smart charging regs
///< delay. Set to 0 if no delay is active
std::chrono::time_point<date::utc_clock> start_time_point; ///< Start of the charging session
std::chrono::time_point<date::utc_clock> end_time_point; ///< End of the charging session
double latest_total_w; ///< Latest total power reading in W
enum class State {
Unknown,
Unplugged,
Disabled,
Preparing,
Reserved,
AuthRequired,
ChargingPausedEV,
ChargingPausedEVSE,
Charging,
AuthTimeout,
Finished,
FinishedEVSE,
FinishedEV
} state;
bool is_state_charging(const SessionInfoStore::State current_state);
std::string active_enable_disable_source{"Unspecified"};
std::string active_enable_disable_state{"Enabled"};
int active_enable_disable_priority{0};
bool permanent_fault{false};
};
} // namespace data
#endif // SESSIONINFO_HPP

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 226 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 309 KiB

View File

@@ -0,0 +1,896 @@
.. _everest_modules_handwritten_RpcApi:
.. *******************************************
.. RpcApi
.. *******************************************
Version Information
===================
Version history of the module:
.. list-table::
:widths: 15 85
:header-rows: 1
* - Version
- Description
* - 1.0.0
- Initial version of the RpcApi module
Introduction
------------
The RPC API provides a standardized interface for external applications to interact with an EVerest-based
ChargePoint. It is designed as a lightweight and transport-independent communication layer that enables
monitoring, control, and integration into higher-level energy management systems.
The API follows the **JSON-RPC 2.0** standard and uses **WebSocket** as the default transport channel,
offering a persistent and bidirectional connection between client and server. This ensures low latency,
efficient message exchange, and clear requestresponse semantics.
Typical use cases include:
* Integration of ChargePoints into energy or fleet management platforms
* Building mobile or web applications for end-users
* Monitoring charging sessions
* Controlling operational parameters such as charging power, current, or connector states
* Accessing structured error and status information for diagnostics
Key features:
* **Transport independence** designed to work over WebSocket today, extendable to other transports in the future
* **Request/response and notifications** methods for configuration and control, notifications for event-driven updates
* **Structured data model** standardized objects and enums for consistent integration
* **Scalable design** suitable for single-user tools as well as multi-user systems
In short, the RPC API serves as the **communication bridge** between the charging infrastructure and
external applications, providing a reliable, extensible, and system-friendly interface for monitoring and control.
General
-------
Feature Overview
~~~~~~~~~~~~~~~~
+---------------------------------------+-----------+
| Feature | Supported |
+=======================================+===========+
| WebSocket transport - no TLS | ✅ |
+---------------------------------------+-----------+
| WebSocket transport - with TLS | ❌ |
+---------------------------------------+-----------+
| Authentication / Permission handling | ❌ |
+---------------------------------------+-----------+
| Scope configuration | ❌ |
+---------------------------------------+-----------+
| Support for multiple EVSEs | ✅ |
+---------------------------------------+-----------+
| Support for multiple connectors | ❌ |
+---------------------------------------+-----------+
| EVSE information retrieval | ✅ |
+---------------------------------------+-----------+
| EVSE status retrieval | ✅ |
+---------------------------------------+-----------+
| Hardware capabilities retrieval | ✅ |
+---------------------------------------+-----------+
| Meter data retrieval | ✅ |
+---------------------------------------+-----------+
| Control of charging current (AC) | ✅ |
+---------------------------------------+-----------+
| Control of charging power (DC) | ✅ |
+---------------------------------------+-----------+
| Control of phase count (AC) | ✅ |
+---------------------------------------+-----------+
| Connector enable/disable | ✅ |
+---------------------------------------+-----------+
| Error monitoring (active errors) | ✅ |
+---------------------------------------+-----------+
| Notifications for status/capabilities | ✅ |
+---------------------------------------+-----------+
| DC charge parameters | ❌ |
+---------------------------------------+-----------+
| DC charge status | ❌ |
+---------------------------------------+-----------+
| Display parameters (ISO15118-20 data) | ❌ |
+---------------------------------------+-----------+
Authentication
~~~~~~~~~~~~~~
(Currently not supported)
The API should optionally support a client authentication mechanism. This can be used to introduce
permission management, which can be used to control which functions a client may access, and which
functions it may not.
If authentication is required, each call, except the initial messages to exchange e.g. the used API
version, is required to contain a valid authentication token. How this authentication token is created
is not part of this specification and must be specified during client and server development.
Tools
~~~~~
The *tools* subdirectory contains a Python-based JSON-RPC GUI client. This client allows testing of the
interface implementation and can serve as a reference example for developing your own client.
API Methods and Notifications
-----------------------------
General
~~~~~~~
The JSON API is generated from the json_rpc_api.yaml specification. This YAML file serves as the primary
reference for all available methods and notifications. It also defines which parameters are required
and which are optional.
Hierarchy of methods & notifications
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* **API** general methods, no effect on charge point
* **ChargePoint** affects the entire charging station
* **EVSE** relates to a specific EVSE (Electric Vehicle Supply Equipment)
Methods
~~~~~~~
This includes all calls that can be executed by the client. Please note that if authentication is active,
a token must be included in the request except for API.Hello. Otherwise, the request will be rejected.
The following chapter headings represent the method name of the JSON-RPC protocol. The response and
requests objects shown in the following chapters map the “params” value in the JSON-RPC object. If
"params" is marked as "{}" it means that no parameters are required and the "params" object can be omitted.
An example JSON-RPC request without "params" is shown in the :ref:`API.Hello request section <example-json-rpc-request-without-params>`
and an example JSON-RPC request with "params" is shown in the :ref:`EVSE.GetInfo request section <example-json-rpc-request-with-params>`.
.. note::
Configuring charging parameters (such as charging current limits) via the API interface cannot
always be guaranteed, since these values may also be influenced by other sources within EVerest.
For example, if EVerest has been configured with an upper limit of 12A, this value cannot be
exceeded via the API interface. In this specific case, if a client attempts to configure 16A
through the API, the applied value will still be limited to 12A. Therefore, it is important to
always observe the configured values returned via the EVSE.StatusChanged notification.
In future versions, unsuccessful configuration attempts may also result in an error response instead of
silently applying the nearest valid limit.
API.Hello
^^^^^^^^^
This method is used to perform an initial handshake with the server. It must be called by the client
within 5 seconds after establishing a connection; otherwise, the server will automatically close the
connection.
The response message contains basic information from the EVerest ChargePoint, such as the API version
in use, to enable further communication.
While the Hello call does not necessarily require a token, it may be called with one. If a token is
provided, the server verifies it, and the reply includes information about the tokens validity as well
as the associated user and permissions.
In case authentication is required, the optional parameter permission_scopes can be used to indicate
the permissions (e.g., read/write access) the client has when using the given token.
.. note::
The fields authenticated, permission_scopes and everest_version are currently not supported.
**Request:**
.. code-block:: json
{}
.. _example-json-rpc-request-without-params:
**Example JSON RPC Request:**
.. code-block:: json
{"jsonrpc": "2.0", "method": "API.Hello", "id": 1}
**Response:**
.. code-block:: json
{
"authentication_required": "bool",
"authenticated": "bool", // optional, always false for now
"permission_scopes": "PermissionScopes", // optional, not yet defined
"api_version": "string",
"everest_version": "string", // currently not supported
"charger_info": "$ChargerInfoObj"
}
ChargePoint.GetEVSEInfos
^^^^^^^^^^^^^^^^^^^^^^^^
This method is used to obtain general information about all configured EVSEs of the charge point.
**Request:**
.. code-block:: json
{}
**Response:**
Returns an array of type “EVSEInfoObj” of all configured EVSEs of the charge point.
.. code-block:: json
{
"infos": "[Array of $EVSEInfoObj]",
"error": "$ResponseErrorEnum"
}
ChargePoint.GetActiveErrors
^^^^^^^^^^^^^^^^^^^^^^^^^^^
This method returns a structured list of all currently active error conditions of the charger.
It is intended for diagnostic purposes and remote monitoring.
**Request:**
.. code-block:: json
{}
**Response:**
.. code-block:: json
{
"active_errors": "[Array of $ErrorObj]", // Empty array if no errors
"error": "$ResponseErrorEnum"
}
EVSE.GetInfo
^^^^^^^^^^^^
This method is used to obtain general information about an EVSE.
**Request:**
.. code-block:: json
{
"evse_index": "int"
}
.. _example-json-rpc-request-with-params:
**Example JSON RPC Request:**
.. code-block:: json
{"jsonrpc": "2.0", "method": "EVSE.GetInfo", "id": 1, "params": {"evse_index": 1}}
**Response:**
.. code-block:: json
{
"info": "$EVSEInfoObj",
"error": "$ResponseErrorEnum"
}
EVSE.GetStatus
^^^^^^^^^^^^^^
This method is used to obtain the current status of the EVSE.
**Request:**
.. code-block:: json
{
"evse_index": "int"
}
**Response:**
.. code-block:: json
{
"status": "$EVSEStatusObj",
"error": "$ResponseErrorEnum"
}
EVSE.GetHardwareCapabilities
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This method is used to obtain hardware capabilities of the EVSE. Please note that the hardware capabilities
can be updated via notification EVSE.HardwareCapabilitiesChanged by the EVSE.
**Request:**
.. code-block:: json
{
"evse_index": "int"
}
**Response:**
.. code-block:: json
{
"hardware_capabilities": "$HardwareCapabilitiesObj",
"error": "$ResponseErrorEnum"
}
EVSE.SetChargingAllowed
^^^^^^^^^^^^^^^^^^^^^^^
This method is used to explicitly allow charging on an EVSE or to remove the release.
Regardless of the authorisation status of the EV, this method can be used to delay a charging process
or to initiate a charging pause on EVSE's side.
**Request:**
.. code-block:: json
{
"evse_index": "int",
"charging_allowed": "bool"
}
**Response:**
.. code-block:: json
{
"error": "$ResponseErrorEnum"
}
EVSE.GetMeterData
^^^^^^^^^^^^^^^^^
**Request:**
.. code-block:: json
{
"evse_index": "int"
}
**Response:**
.. code-block:: json
{
"meter_data": "$MeterDataObj",
"error": "$ResponseErrorEnum"
}
EVSE.SetACChargingCurrent
^^^^^^^^^^^^^^^^^^^^^^^^^
This method is used to configure the AC charging current of an EVSE.
**Request:**
.. code-block:: json
{
"evse_index": "int",
"max_current": "float"
}
**Response:**
Returns an error parameter to show if the configuration of the charging current was successful.
.. code-block:: json
{
"error": "$ResponseErrorEnum"
}
EVSE.SetACChargingPhaseCount
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This method is used to configure the AC phase count of an EVSE.
**Request:**
.. code-block:: json
{
"evse_index": "int",
"phase_count": "int"
}
**Response:**
Returns an error parameter to show if the configuration of the charging current was successful.
.. code-block:: json
{
"error": "$ResponseErrorEnum"
}
EVSE.SetDCChargingPower
^^^^^^^^^^^^^^^^^^^^^^^
This method is used to configure the DC charging power an EVSE.
**Request:**
.. code-block:: json
{
"evse_index": "int",
"max_power": "float"
}
**Response:**
Returns an error parameter to show if the configuration was successful.
.. code-block:: json
{
"error": "$ResponseErrorEnum"
}
EVSE.EnableConnector
^^^^^^^^^^^^^^^^^^^^
Method to enable or disable a connector on the EVSE. connector_index is a positive integer identifying
the connector that should be enabled. If the connector_index is 0 the whole EVSE is enabled.
**Request:**
.. code-block:: json
{
"evse_index": "int",
"connector_index": "int",
"enable": "bool",
"priority": "int"
}
**Response:**
Returns an error parameter to show if the configuration of the charging current was successful.
.. code-block:: json
{
"error": "$ResponseErrorEnum"
}
Notifications
~~~~~~~~~~~~~
Notifications are signaled by the server as soon as a property within the parameters has changed.
ChargePoint.ActiveErrorsChanged
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: json
{
"active_errors": "[Array of $ErrorObj]"
}
EVSE.HardwareCapabilitiesChanged
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: json
{
"evse_index": "int",
"hardware_capabilities": "$HardwareCapabilitiesObj"
}
EVSE.StatusChanged
^^^^^^^^^^^^^^^^^^
.. code-block:: json
{
"evse_index": "int",
"evse_status": "$EVSEStatusObj"
}
EVSE.MeterDataChanged
^^^^^^^^^^^^^^^^^^^^^
.. code-block:: json
{
"evse_index": "int",
"meter_data": "$MeterDataObj"
}
Enums
-----
ResponseErrorEnum
~~~~~~~~~~~~~~~~~
Enumeration to differentiate between the various error cases that can occur after a method request.
::
"NoError",
"ErrorInvalidParameter",
"ErrorOutOfRange",
"ErrorValuesNotApplied",
"ErrorInvalidEVSEIndex",
"ErrorInvalidConnectorId",
"ErrorNoDataAvailable",
"ErrorUnknownError"
EnergyTransferModeEnum
~~~~~~~~~~~~~~~~~~~~~~
Enumeration to differentiate between the various energy transfer modes
::
"AC_single_phase_core",
"AC_two_phase",
"AC_three_phase_core",
"DC_core",
"DC_extended",
"DC_combo_core",
"DC_unique",
"DC",
"AC_BPT",
"AC_BPT_DER",
"AC_DER",
"DC_BPT",
"DC_ACDP",
"DC_ACDP_BPT",
"WPT"
ChargeProtocolEnum
~~~~~~~~~~~~~~~~~~
::
"Unknown",
"IEC61851",
"DIN70121",
"ISO15118",
"ISO15118_20"
EVSEStateEnum
~~~~~~~~~~~~~
::
"Unplugged",
"Disabled",
"Preparing",
"Reserved",
"AuthRequired",
"Charging",
"ChargingPausedEV",
"ChargingPausedEVSE",
"Finished",
"SwitchingPhases"
ConnectorTypeEnum
~~~~~~~~~~~~~~~~~
::
"cCCS1",
"cCCS2",
"cG105",
"cTesla",
"cType1",
"cType2",
"s309_1P_16A",
"s309_1P_32A",
"s309_3P_16A",
"s309_3P_32A",
"sBS1361",
"sCEE_7_7",
"sType2",
"sType3",
"Other1PhMax16A",
"Other1PhOver16A",
"Other3Ph",
"Pan",
"wInductive",
"wResonant",
"Undetermined",
"Unknown"
EVSEErrorEnum
~~~~~~~~~~~~~
EVSEErrorEnum can be used to show more details of an EVSE error for example in a service tool application
for the technician. The enum naming is identical to EVerest error handler semantic.
Example (excerpt)::
"NoError",
"power_supply_DC/HardwareFault",
"power_supply_DC/OverTemperature",
"power_supply_DC/UnderTemperature",
"power_supply_DC/UnderVoltageAC",
"power_supply_DC/OverVoltageAC",
"power_supply_DC/UnderVoltageDC",
"power_supply_DC/OverVoltageDC",
"power_supply_DC/OverCurrentAC",
"power_supply_DC/OverCurrentDC",
"power_supply_DC/VendorError",
"power_supply_DC/VendorWarning",
"evse_board_support/MREC2GroundFailure",
"evse_board_support/MREC3HighTemperature",
"evse_board_support/MREC4OverCurrentFailure",
"evse_board_support/MREC5OverVoltage",
"evse_board_support/MREC6UnderVoltage",
"evse_board_support/MREC8EmergencyStop",
"evse_board_support/MREC10InvalidVehicleMode",
"evse_board_support/MREC14PilotFault",
"evse_board_support/MREC15PowerLoss",
"evse_board_support/MREC17EVSEContactorFault",
"evse_board_support/MREC18CableOverTempDerate",
"evse_board_support/MREC19CableOverTempStop",
"evse_board_support/MREC20PartialInsertion",
"evse_board_support/MREC23ProximityFault",
"evse_board_support/MREC24ConnectorVoltageHigh",
"evse_board_support/MREC25BrokenLatch",
"evse_board_support/MREC26CutCable",
...
JSON Objects
------------
EVSEInfoObj
~~~~~~~~~~~
This object contains static information about a EVSE of a charge point. This parameter is derived from
the EvseManager identifier from the EVerest configuration. The "index" parameter is essential to perform
EVSE specific method calls. The “id” parameter is the EVSE ID. The EVSE ID is a globally unique identifier
defined in ISO 15118 to represent a specific EVSE. The “supported_energy_transfer_modes” must be used to
distinguish between DC and AC charging. Depending on this, the optional parameters of object “EVSEStatusObj”
are configured. In addition, it is possible to determine whether BPT is supported.
.. code-block:: json
{
"index": "int",
"id": "string",
"description": "string", // optional
"available_connectors": "[ConnectorInfoObj]",
"supported_energy_transfer_modes": "[EnergyTransferModeEnum]"
}
EVSEStatusObj
~~~~~~~~~~~~~
This object contains all information about the current status of a charge point EVSE. These parameters
change dynamically, depending on the current EVSE state, which is indicated by the “state” parameter.
The parameters “ac_charge_param" and “ac_charge_status" are only configured in a AC charging session
and parameters “dc_charge_param" and “dc_charge_status" are only configured in a DC charging session.
These parameters mainly contain parameters that are transmitted in an HLC session. The connector info
(e.g. to identify if it is a DC or AC charger) is part of object “EVSEInfoObj“. The “active_connector_index”
information can also be used by GUI applications to display the active connector correctly.
.. code-block:: json
{
"charged_energy_wh": "float",
"discharged_energy_wh": "float",
"charging_duration_s": "int",
"charging_allowed": "bool",
"available": "bool",
"active_connector_index": "int",
"error_present": "bool",
"charge_protocol": "$ChargeProtocolEnum",
"ac_charge_param": "$ACChargeParametersObj", // optional, only if AC supported
"dc_charge_param": "$DCChargeParametersObj", // optional, only if DC supported
"ac_charge_status": "$ACChargeStatusObj", // optional, only if AC supported
"dc_charge_status": "$DCChargeStatusObj", // optional, only if DC supported
"display_parameters": "$DisplayParametersObj",
"state": "$EVSEStateEnum"
}
ConnectorInfoObj
~~~~~~~~~~~~~~~~
This object contains static information about a connector of an EVSE. This parameter is derived from
the from the EVerest configuration. The "index" parameter is essential to perform connector specific
method calls. The “type” must be used to distinguish between DC and AC charging. Depending on this,
the optional parameters of object “EVSEStatusObj” are configured.
.. code-block:: json
{
"index": "int",
"type": "ConnectorTypeEnum",
"description": "string" // optional
}
HardwareCapabilitiesObj
~~~~~~~~~~~~~~~~~~~~~~~
This object contains all hardware related limits of a charge point EVSE.
.. code-block:: json
{
"max_current_A_export": "float",
"max_current_A_import": "float",
"max_phase_count_export": "int",
"max_phase_count_import": "int",
"min_current_A_export": "float",
"min_current_A_import": "float",
"min_phase_count_export": "int",
"min_phase_count_import": "int",
"phase_switch_during_charging": "bool"
}
MeterDataObj
~~~~~~~~~~~~
This object contains the following meter data of a charge point EVSE:
timestamp: Timestamp of measurement, represented as RFC3339 string
energy_Wh_import: Imported energy in Wh (from grid)
meter_id: A (user defined) meter if (e.g. id printed on the case)
serial_number: Serial number of the meter
phase_seq_error: AC only: true for 3 phase rotation error (ccw)
energy_Wh_export: Exported energy in Wh (to grid)
power_W: Instantaneous power in Watt. Negative values are exported, positive values imported Energy.
voltage_V: Voltage in Volts
current_A: Current in Ampere
frequency_Hz: Grid frequency in Hertz
.. code-block:: json
{
"current_A": {"L1": "float","L2": "float","L3": "float","N": "float"},
"energy_Wh_import": {"L1": "float","L2": "float","L3": "float","total": "float"},
"energy_Wh_export": {"L1": "float","L2": "float","L3": "float","total": "float"}, // optional
"frequency_Hz": {"L1": "float","L2": "float","L3": "float"}, // optional
"meter_id": "string",
"serial_number": "string", // optional
"phase_seq_error": "bool", // optional
"power_W": {"L1": "float","L2": "float","L3": "float","total": "float"}, // optional
"timestamp": "string",
"voltage_V": {"L1": "float","L2": "float","L3": "float"} // optional
}
ACChargeParametersObj
~~~~~~~~~~~~~~~~~~~~~
This object contains all AC related parameters of a charge point EVSE. Parameters like “evse_maximum_discharge_power”
are only transmitted if a BPT (bidirectional power transfer) session is active. Currently only “evse_max_current”
and “evse_max_phase_count“ are supported.
.. code-block:: json
{
"evse_nominal_voltage": "float",
"evse_max_current": "float",
"evse_max_phase_count": "int",
"evse_maximum_charge_power": "float",
"evse_minimum_charge_power": "float",
"evse_nominal_frequency": "float",
"evse_maximum_discharge_power": "float",
"evse_minimum_discharge_power": "float"
}
DCChargeParametersObj
~~~~~~~~~~~~~~~~~~~~~
Currently not supported.
This object contains all DC related parameters of a charge point EVSE. Parameters like “evse_maximum_discharge_power”
are only transmitted if a BPT (bidirectional power transfer) session is active.
.. code-block:: json
{
"evse_maximum_charge_current": "float",
"evse_maximum_charge_power": "float",
"evse_maximum_voltage": "float",
"evse_minimum_charge_current": "float",
"evse_minimum_charge_power": "float",
"evse_minimum_voltage": "float",
"evse_energy_to_be_delivered": "float",
"evse_maximum_discharge_current": "float",
"evse_maximum_discharge_power": "float",
"evse_minimum_discharge_current": "float",
"evse_minimum_discharge_power": "float"
}
ACChargeStatusObj
~~~~~~~~~~~~~~~~~
This object contains all DC related parameters of a charge point EVSE. Parameters like “evse_maximum_discharge_power”
are only transmitted if a BPT (bidirectional power transfer) session is active. Currently only “evse_max_current”
and “evse_max_phase_count“ are supported.
.. code-block:: json
{
"evse_active_phase_count": "int"
}
DCChargeStatusObj
~~~~~~~~~~~~~~~~~
Currently not supported.
This object contains all DC related parameters during charging of a charge point EVSE.
.. code-block:: json
{
"evse_present_current": "float",
"evse_present_voltage": "float",
"evse_power_limit_achieved": "bool",
"evse_current_limit_achieved": "bool",
"evse_voltage_limit_achieved": "bool"
}
DisplayParametersObj
~~~~~~~~~~~~~~~~~~~~
Currently not supported.
This object contains additional information which can be displayed in a GUI. These parameters are for
display purposes only and must not, under any circumstances, influence the EVSE behavior. Most of the
parameters are only transmitted in an ISO15118-20 charging session.
.. code-block:: json
{
"start_soc": "int",
"present_soc": "int",
"minimum_soc": "int",
"target_soc": "int",
"maximum_soc": "int",
"remaining_time_to_minimum_soc": "int",
"remaining_time_to_target_soc": "int",
"remaining_time_to_maximum_soc": "int",
"charging_complete": "bool",
"battery_energy_capacity": "float",
"inlet_hot": "bool"
}
ChargerInfoObj
~~~~~~~~~~~~~~
This object contains well-known general charger information, e.g. vendor and model name, firmware version etc.
.. code-block:: json
{
"vendor": "string",
"model": "string",
"serial": "string",
"friendly_name": "string",
"manufacturer": "string",
"manufacturer_url": "string",
"model_url": "string",
"model_no": "string",
"revision": "string",
"board_revision": "string",
"firmware_version": "string"
}
ErrorObj
~~~~~~~~
The ErrorObj structure represents a detailed description of an charger error. It includes the error
type, origin, severity, and timestamp, along with optional context like EVSE or connector index. Each
error is uniquely identified by a UUID and may include a vendor-specific ID and custom message.
.. code-block:: json
{
"type": "string",
"sub_type": "string",
"message": "string",
"description": "string",
"origin": {
"module_id": "string",
"implementation_id": "string",
"evse_index": "int", // optional
"connector_index": "int" // optional
},
"vendor_id": "string",
"severity": "SeverityEnum",
"timestamp": "string",
"uuid": "string"
}
What can a simple sequence look like?
-------------------------------------
The sequence diagram below is a simple sequence diagram based on the defined WebSocket methods and notifications.
The diagram is simplified for better visualization and therefore only shows the relevant parameters within
the objects.
Initialization
~~~~~~~~~~~~~~
The first diagram illustrates how a client establishes a connection to the server and how the server
initializes a single EVSE. After that, the client application is prepared to process incoming notifications
of the API, for example caused by plugging in an EV.
.. image:: img/initialization.drawio.svg
:alt: RPC Communication Flow
:align: center
:width: 80%
Session handling
~~~~~~~~~~~~~~~~
The second sequence diagram shows the notifications that are triggered as soon as an EV is plugged in
and recognized by the EVSE. It also shows how the client can actively adjust the charging current of a
running session.
.. image:: img/ac_session_handling.drawio.svg
:alt: RPC Communication Flow
:align: center
:width: 80%

View File

@@ -0,0 +1,208 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "Conversions.hpp"
namespace types {
namespace json_rpc_api {
EVSEStateEnum evse_manager_session_event_to_evse_state(types::evse_manager::SessionEvent state) {
using Event = types::evse_manager::SessionEventEnum;
switch (state.event) {
case Event::Enabled:
return EVSEStateEnum::Unplugged;
case Event::Disabled:
return EVSEStateEnum::Disabled;
case Event::AuthRequired:
return EVSEStateEnum::AuthRequired;
case Event::PrepareCharging:
[[fallthrough]];
case Event::SessionStarted:
[[fallthrough]];
case Event::SessionResumed:
[[fallthrough]];
case Event::TransactionStarted:
return EVSEStateEnum::Preparing;
case Event::ChargingStarted:
return EVSEStateEnum::Charging;
case Event::ChargingPausedEV:
return EVSEStateEnum::ChargingPausedEV;
case Event::ChargingPausedEVSE:
return EVSEStateEnum::ChargingPausedEVSE;
case Event::ChargingFinished:
return EVSEStateEnum::Finished;
case Event::StoppingCharging:
return EVSEStateEnum::FinishedEV;
case Event::TransactionFinished: {
if (state.transaction_finished.has_value() &&
state.transaction_finished->reason == types::evse_manager::StopTransactionReason::Local) {
return EVSEStateEnum::FinishedEVSE;
} else {
return EVSEStateEnum::Finished;
}
break;
}
case Event::PluginTimeout:
return EVSEStateEnum::AuthTimeout;
case Event::ReservationStart:
return EVSEStateEnum::Reserved;
case Event::ReservationEnd:
[[fallthrough]];
case Event::SessionFinished:
return EVSEStateEnum::Unplugged;
case Event::SwitchingPhases:
return EVSEStateEnum::SwitchingPhases;
case Event::Authorized:
[[fallthrough]];
case Event::Deauthorized:
[[fallthrough]];
default:
return EVSEStateEnum::Unknown;
}
}
ChargeProtocolEnum evse_manager_protocol_to_charge_protocol(const std::string& protocol) {
if (protocol == "IEC61851-1") {
return ChargeProtocolEnum::IEC61851;
} else if (protocol == "DIN70121") {
return ChargeProtocolEnum::DIN70121;
} else if (protocol.compare(0, 11, "ISO15118-20") == 0) {
return ChargeProtocolEnum::ISO15118_20;
}
// This check must be after the ISO15118-20 check
else if (protocol.compare(0, 10, "ISO15118-2") == 0) {
return ChargeProtocolEnum::ISO15118;
} else {
return ChargeProtocolEnum::Unknown;
}
}
ErrorObj everest_error_to_rpc_error(const Everest::error::Error& error_object) {
ErrorObj rpc_error;
rpc_error.type = error_object.type;
rpc_error.description = error_object.description;
rpc_error.message = error_object.message;
switch (error_object.severity) {
case Everest::error::Severity::High:
rpc_error.severity = Severity::High;
break;
case Everest::error::Severity::Medium:
rpc_error.severity = Severity::Medium;
break;
case Everest::error::Severity::Low:
rpc_error.severity = Severity::Low;
break;
default:
throw std::out_of_range("Provided severity " + std::to_string(static_cast<int>(error_object.severity)) +
" could not be converted to enum of type SeverityEnum");
}
rpc_error.origin.module_id = error_object.origin.module_id;
rpc_error.origin.implementation_id = error_object.origin.implementation_id;
rpc_error.origin.evse_index = 0;
rpc_error.origin.connector_index = 0;
if (error_object.origin.mapping.has_value()) {
rpc_error.origin.evse_index = error_object.origin.mapping.value().evse;
if (error_object.origin.mapping.value().connector.has_value()) {
rpc_error.origin.connector_index = error_object.origin.mapping.value().connector.value();
}
}
rpc_error.timestamp = Everest::Date::to_rfc3339(error_object.timestamp);
rpc_error.uuid = error_object.uuid.to_string();
return rpc_error;
}
std::vector<EnergyTransferModeEnum> iso15118_energy_transfer_modes_to_json_rpc_api(
const std::vector<types::iso15118::EnergyTransferMode>& supported_energy_transfer_modes,
bool& is_ac_transfer_mode) {
std::vector<EnergyTransferModeEnum> tmp{};
if (supported_energy_transfer_modes.empty()) {
// in case EvseManager lists no transfer modes at all
is_ac_transfer_mode = true;
return tmp;
}
is_ac_transfer_mode = false;
for (const auto& mode : supported_energy_transfer_modes) {
switch (mode) {
case types::iso15118::EnergyTransferMode::AC_single_phase_core:
tmp.push_back(EnergyTransferModeEnum::AC_single_phase_core);
is_ac_transfer_mode = true;
break;
case types::iso15118::EnergyTransferMode::AC_two_phase:
tmp.push_back(EnergyTransferModeEnum::AC_two_phase);
is_ac_transfer_mode = true;
break;
case types::iso15118::EnergyTransferMode::AC_three_phase_core:
tmp.push_back(EnergyTransferModeEnum::AC_three_phase_core);
is_ac_transfer_mode = true;
break;
case types::iso15118::EnergyTransferMode::DC_core:
tmp.push_back(EnergyTransferModeEnum::DC_core);
break;
case types::iso15118::EnergyTransferMode::DC_extended:
tmp.push_back(EnergyTransferModeEnum::DC_extended);
break;
case types::iso15118::EnergyTransferMode::DC_combo_core:
tmp.push_back(EnergyTransferModeEnum::DC_combo_core);
break;
case types::iso15118::EnergyTransferMode::DC_unique:
tmp.push_back(EnergyTransferModeEnum::DC_unique);
break;
case types::iso15118::EnergyTransferMode::DC:
tmp.push_back(EnergyTransferModeEnum::DC);
break;
case types::iso15118::EnergyTransferMode::AC_BPT:
tmp.push_back(EnergyTransferModeEnum::AC_BPT);
is_ac_transfer_mode = true;
break;
case types::iso15118::EnergyTransferMode::AC_BPT_DER:
tmp.push_back(EnergyTransferModeEnum::AC_BPT_DER);
is_ac_transfer_mode = true;
break;
case types::iso15118::EnergyTransferMode::AC_DER:
tmp.push_back(EnergyTransferModeEnum::AC_DER);
is_ac_transfer_mode = true;
break;
case types::iso15118::EnergyTransferMode::DC_BPT:
tmp.push_back(EnergyTransferModeEnum::DC_BPT);
break;
case types::iso15118::EnergyTransferMode::DC_ACDP:
tmp.push_back(EnergyTransferModeEnum::DC_ACDP);
break;
case types::iso15118::EnergyTransferMode::DC_ACDP_BPT:
tmp.push_back(EnergyTransferModeEnum::DC_ACDP_BPT);
break;
case types::iso15118::EnergyTransferMode::WPT:
tmp.push_back(EnergyTransferModeEnum::WPT);
is_ac_transfer_mode = true; // TBD
break;
case types::iso15118::EnergyTransferMode::MCS:
tmp.push_back(EnergyTransferModeEnum::MCS);
break;
case types::iso15118::EnergyTransferMode::MCS_BPT:
tmp.push_back(EnergyTransferModeEnum::MCS_BPT);
break;
default:
throw std::invalid_argument("Unsupported energy transfer mode");
}
}
return tmp;
}
void to_json(json& j, const EnergyTransferModeEnum& k) {
// the required parts of the type
j = energy_transfer_mode_enum_to_string(k);
}
} // namespace json_rpc_api
} // namespace types

View File

@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef CONVERSIONS_HPP
#define CONVERSIONS_HPP
#include <generated/types/evse_manager.hpp>
#include <types/json_rpc_api/json_rpc_api.hpp>
#include <utils/error.hpp>
namespace types {
namespace json_rpc_api {
EVSEStateEnum evse_manager_session_event_to_evse_state(types::evse_manager::SessionEvent state);
ChargeProtocolEnum evse_manager_protocol_to_charge_protocol(const std::string& protocol);
types::json_rpc_api::ErrorObj everest_error_to_rpc_error(const Everest::error::Error& error_object);
std::vector<types::json_rpc_api::EnergyTransferModeEnum> iso15118_energy_transfer_modes_to_json_rpc_api(
const std::vector<types::iso15118::EnergyTransferMode>& supported_energy_transfer_modes, bool& is_ac_transfer_mode);
/**
* @brief Serializes an EnergyTransferModeEnum object to a JSON representation.
*
* This function converts the given EnergyTransferModeEnum value into its corresponding
* JSON format and assigns it to the provided json object. This function is necessary
* for properly serializing the data for JSON-RPC API responses.
*
* @param j Reference to a json object where the serialized data will be stored.
* @param k The EnergyTransferModeEnum value to be serialized.
*/
void to_json(json& j, const types::json_rpc_api::EnergyTransferModeEnum& k);
} // namespace json_rpc_api
} // namespace types
#endif // CONVERSIONS_HPP

View File

@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "ErrorHandler.hpp"
#include <everest/logging.hpp>
namespace helpers {
void handle_error_raised(data::DataStoreCharger& data, const types::json_rpc_api::ErrorObj& error) {
try {
data.chargererrors.add_error(error);
} catch (const std::runtime_error& e) {
EVLOG_warning << "Error while adding error to the data store: " << e.what();
}
// only set error present for EVSE specific errors. Index 0 is reserved for the charger itself
if (!error.origin.evse_index.has_value() || error.origin.evse_index.value() == 0) {
return;
}
auto tmp_evse_store = data.get_evse_store(error.origin.evse_index.value());
if (tmp_evse_store != nullptr) {
EVLOG_debug << "Setting error present for EVSE index: " << error.origin.evse_index.value();
tmp_evse_store->evsestatus.set_error_present(true);
} else {
EVLOG_error << "Cannot set error present for EVSE index: " << error.origin.evse_index.value();
}
}
void handle_error_cleared(data::DataStoreCharger& data, const types::json_rpc_api::ErrorObj& error) {
try {
data.chargererrors.clear_error(error);
} catch (const std::runtime_error& e) {
EVLOG_warning << "Error while clearing error from the data store: " << e.what();
}
// only clear error present for EVSE specific errors. Index 0 is reserved for the charger itself
if (!error.origin.evse_index.has_value() || error.origin.evse_index.value() == 0) {
return;
}
auto tmp_charger_errors = data.chargererrors.get_data();
if (tmp_charger_errors.has_value()) {
for (const auto& charger_error : tmp_charger_errors.value()) {
if (charger_error.origin.evse_index.has_value() &&
charger_error.origin.evse_index.value() == error.origin.evse_index.value()) {
return;
}
}
}
auto tmp_evse_store = data.get_evse_store(error.origin.evse_index.value());
if (tmp_evse_store != nullptr) {
tmp_evse_store->evsestatus.set_error_present(false);
}
}
} // namespace helpers

View File

@@ -0,0 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef ERROR_HANDLER_HPP
#define ERROR_HANDLER_HPP
#include "../data/DataStore.hpp"
namespace helpers {
void handle_error_raised(data::DataStoreCharger& data, const types::json_rpc_api::ErrorObj& error);
void handle_error_cleared(data::DataStoreCharger& data, const types::json_rpc_api::ErrorObj& error);
} // namespace helpers
#endif // ERROR_HANDLER_HPP

View File

@@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "LimitDecimalPlaces.hpp"
#include <cmath>
#include <iomanip>
#include <sstream>
namespace helpers {
// This function formats and rounds a double value to a specified number of decimal places
double round_double(double value, double step, int precision) {
double rounded = std::round(value / step) * step;
std::ostringstream oss;
oss << std::fixed << std::setprecision(precision) << rounded;
return std::stod(oss.str());
}
// This function recursively rounds all float values in a JSON object or array
void round_floats_in_json(nlohmann::json& j, int precision) {
if (precision == 0) {
return; // No rounding needed if precision is 0
}
if (j.is_object() || j.is_array()) {
for (auto& el : j) {
round_floats_in_json(el, precision);
}
} else if (j.is_number_float()) {
const double step = std::pow(10.0, -precision);
const double value = j.get<double>();
j = round_double(value, step, precision);
}
}
} // namespace helpers

View File

@@ -0,0 +1,13 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef LIMIT_DECIMAL_PLACES_HPP
#define LIMIT_DECIMAL_PLACES_HPP
#include <nlohmann/json.hpp>
namespace helpers {
double round_double(double value, int precision = 3);
void round_floats_in_json(nlohmann::json& j, int precision = 3);
} // namespace helpers
#endif // LIMIT_DECIMAL_PLACES_HPP

View File

@@ -0,0 +1,50 @@
description: >-
The RpcApi module provides a JSON-RPC API for external applications. The main focus is to provide
data for displaying the charging parameters and setting charging parameters during charging.
config:
websocket_enabled:
description: Enable the websocket server.
type: boolean
default: true
websocket_port:
description: Port for the websocket server to listen on.
type: integer
default: 8080
websocket_interface:
description: >-
Network interface/device for the websocket server to listen on, e.g. 'lo' or 'eth1'.
Listen on all interfaces if "all".
type: string
minLength: 1
default: "lo"
websocket_tls_enabled:
description: Enable TLS for the websocket server. Currently not implemented.
type: boolean
default: false
authentication_required:
description: Require authentication for API requests. Currently not implemented.
type: boolean
default: false
max_decimal_places_other:
description: Maximum number of decimal places for all floating point values. Ignored if value is 0.
type: integer
default: 2
minimum: 0
requires:
evse_manager:
interface: evse_manager
min_connections: 1
max_connections: 128
evse_energy_sink:
interface: external_energy_limits
min_connections: 0
max_connections: 128
charger_information:
interface: charger_information
min_connections: 0
max_connections: 1
enable_global_errors: true
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- chargebyte GmbH

View File

@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef REQUEST_HANDLER_INTERFACE_HPP
#define REQUEST_HANDLER_INTERFACE_HPP
#include <optional>
#include <types/json_rpc_api/json_rpc_api.hpp>
namespace RPCDataTypes = types::json_rpc_api;
namespace request_interface {
// --- RequestHandlerInterface ---
// This interface is used to handle synchronous requests from the RPC API.
class RequestHandlerInterface {
public:
virtual ~RequestHandlerInterface() = default;
virtual RPCDataTypes::ErrorResObj set_charging_allowed(const int32_t evse_index, bool charging_allowed) = 0;
virtual RPCDataTypes::ErrorResObj set_ac_charging(const int32_t evse_index, bool charging_allowed, bool max_current,
std::optional<int> phase_count) = 0;
virtual RPCDataTypes::ErrorResObj set_ac_charging_current(const int32_t evse_index, float max_current) = 0;
virtual RPCDataTypes::ErrorResObj set_ac_charging_phase_count(const int32_t evse_index, int phase_count) = 0;
virtual RPCDataTypes::ErrorResObj set_dc_charging(const int32_t evse_index, bool charging_allowed,
float max_power) = 0;
virtual RPCDataTypes::ErrorResObj set_dc_charging_power(const int32_t evse_index, float max_power) = 0;
virtual RPCDataTypes::ErrorResObj enable_connector(const int32_t evse_index, int connector_id, bool enable,
int priority) = 0;
};
} // namespace request_interface
#endif // REQUEST_HANDLER_INTERFACE_HPP

View File

@@ -0,0 +1,362 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "RpcHandler.hpp"
#include <jsonrpccxx/client.hpp>
#include <jsonrpccxx/common.hpp>
#include <jsonrpccxx/server.hpp>
#include <jsonrpccxx/typemapper.hpp>
#include <everest/logging.hpp>
#include "../helpers/Conversions.hpp" // For to_json() for nlohmann::json
#include "../helpers/LimitDecimalPlaces.hpp"
namespace rpc {
template <typename T> struct is_optional : std::false_type {};
static const std::chrono::milliseconds REQ_COLLECTION_TIMEOUT(
10); // Timeout for collecting client requests. After this timeout, the requests will be processed.
static const std::chrono::milliseconds
REQ_PROCESSING_TIMEOUT(50); // Timeout for processing requests. After this timeout, the request will be processed.
// Helper functions
template <typename T> struct is_optional<std::optional<T>> : std::true_type {};
template <typename T> auto extract_param(const nlohmann::json& j) {
if constexpr (is_optional<T>::value) {
using InnerT = typename T::value_type;
if (j.is_null()) {
return std::optional<InnerT>{};
} else {
return std::optional<InnerT>{j.get<InnerT>()};
}
} else {
return j.get<T>();
}
}
// json-rpc-cpp does not support optional parameters in method signatures
// so we need to create our own get_handle function to handle methods with optional parameters correctly
template <typename...> using void_t = void;
template <typename Default, template <typename...> class Op, typename... Args> struct detector {
using value_t = std::false_type;
using type = Default;
};
template <template <typename...> class Op, typename... Args> struct detector<void_t<Op<Args...>>, Op, Args...> {
using value_t = std::true_type;
using type = Op<Args...>;
};
template <template <typename...> class Op, typename... Args>
using is_detected = typename detector<void, Op, Args...>::value_t;
template <typename T>
using is_to_json_serializable = decltype(to_json(std::declval<nlohmann::json&>(), std::declval<T>()));
template <typename T> constexpr bool is_to_json_serializable_v = is_detected<is_to_json_serializable, T>::value;
template <typename T, typename MethodT, typename... ParamTypes, std::size_t... I>
auto invoke_with_params_impl(T& instance, MethodT method, const json& params, std::index_sequence<I...>) {
return (instance.*method)((extract_param<std::remove_reference_t<ParamTypes>>(params.at(I)))...);
}
template <typename T, typename MethodT, typename... ParamTypes>
auto invoke_with_params(T& instance, MethodT method, const json& params) {
return invoke_with_params_impl<T, MethodT, ParamTypes...>(instance, method, params,
std::index_sequence_for<ParamTypes...>{});
}
template <typename T, typename ReturnType, typename... ParamTypes>
MethodHandle get_handle(ReturnType (T::*method)(ParamTypes...), T& instance, int precision = 3) {
return [&instance, method, precision](const json& params) -> json {
if (!params.is_array()) {
throw std::runtime_error("params must be array");
}
constexpr size_t expected = sizeof...(ParamTypes);
if (params.size() != expected) {
throw std::runtime_error("invalid number of parameters");
}
auto result = invoke_with_params<T, decltype(method), ParamTypes...>(instance, method, params);
if constexpr (std::is_same_v<ReturnType, void>) {
return json(); // no return value
} else if constexpr (std::is_same_v<ReturnType, nlohmann::json>) {
return result; // return json directly
} else if constexpr (is_to_json_serializable_v<ReturnType>) {
nlohmann::json j;
to_json(j, result);
helpers::round_floats_in_json(j, precision);
return j; // convert to json and round floats
} else {
return result; // fallback: no conversion to json possible, return as is
}
};
}
RpcHandler::RpcHandler(std::vector<std::shared_ptr<server::TransportInterface>> transport_interfaces,
DataStoreCharger& dataobj,
std::unique_ptr<request_interface::RequestHandlerInterface> request_handler, int precision) :
m_transport_interfaces(std::move(transport_interfaces)),
m_data_store(dataobj),
m_methods_api(dataobj),
m_methods_chargepoint(dataobj),
m_methods_evse(dataobj, std::move(request_handler)),
m_conn(m_transport_interfaces, m_api_hello_received, m_mtx),
m_precision(precision) {
init_rpc_api();
init_transport_interfaces();
m_notifications_evse = std::make_unique<notifications::Evse>(m_rpc_server, dataobj, m_precision);
m_notifications_chargepoint = std::make_unique<notifications::ChargePoint>(m_rpc_server, dataobj, m_precision);
}
void RpcHandler::init_rpc_api() {
// Initialize the RPC API here
m_methods_api.set_authentication_required(false);
m_methods_api.set_api_version(API_VERSION);
m_rpc_server = std::make_shared<JsonRpc2ServerWithClient>(m_conn);
m_rpc_server->Add(methods::METHOD_API_HELLO, get_handle(&methods::Api::hello, m_methods_api, m_precision), {});
m_rpc_server->Add(methods::METHOD_CHARGEPOINT_GET_EVSE_INFOS,
get_handle(&methods::ChargePoint::getEVSEInfos, m_methods_chargepoint, m_precision), {});
m_rpc_server->Add(methods::METHOD_CHARGEPOINT_GET_ACTIVE_ERRORS,
get_handle(&methods::ChargePoint::getActiveErrors, m_methods_chargepoint, m_precision), {});
m_rpc_server->Add(methods::METHOD_EVSE_GET_INFO, get_handle(&methods::Evse::get_info, m_methods_evse, m_precision),
{"evse_index"});
m_rpc_server->Add(methods::METHOD_EVSE_GET_STATUS,
get_handle(&methods::Evse::get_status, m_methods_evse, m_precision), {"evse_index"});
m_rpc_server->Add(methods::METHOD_EVSE_GET_HARDWARE_CAPABILITIES,
get_handle(&methods::Evse::get_hardware_capabilities, m_methods_evse, m_precision),
{"evse_index"});
m_rpc_server->Add(methods::METHOD_EVSE_SET_CHARGING_ALLOWED,
get_handle(&methods::Evse::set_charging_allowed, m_methods_evse, m_precision),
{"evse_index", "charging_allowed"});
m_rpc_server->Add(methods::METHOD_EVSE_GET_METER_DATA,
get_handle(&methods::Evse::get_meter_data, m_methods_evse, m_precision), {"evse_index"});
// TODO: m_rpc_server->Add(methods::METHOD_EVSE_SET_AC_CHARGING, (get_handle(&methods::Evse::set_ac_charging,
// m_methods_evse, m_precision)),
// {"evse_index", "charging_allowed", "max_current", "phase_count"});
m_rpc_server->Add(methods::METHOD_EVSE_SET_AC_CHARGING_CURRENT,
get_handle(&methods::Evse::set_ac_charging_current, m_methods_evse, m_precision),
{"evse_index", "max_current"});
m_rpc_server->Add(methods::METHOD_EVSE_SET_AC_CHARGING_PHASE_COUNT,
get_handle(&methods::Evse::set_ac_charging_phase_count, m_methods_evse, m_precision),
{"evse_index", "phase_count"});
// TODO: m_rpc_server->Add(methods::METHOD_EVSE_SET_DC_CHARGING, (get_handle(&methods::Evse::set_dc_charging,
// m_methods_evse, m_precision)),
// {"evse_index", "charging_allowed", "max_power"});
m_rpc_server->Add(methods::METHOD_EVSE_SET_DC_CHARGING_POWER,
get_handle(&methods::Evse::set_dc_charging_power, m_methods_evse, m_precision),
{"evse_index", "max_power"});
m_rpc_server->Add(methods::METHOD_EVSE_ENABLE_CONNECTOR,
get_handle(&methods::Evse::enable_connector, m_methods_evse, m_precision),
{"evse_index", "connector_index", "enable", "priority"});
}
void RpcHandler::init_transport_interfaces() {
for (const auto& transport_interface : m_transport_interfaces) {
if (!transport_interface) {
throw std::runtime_error("Transport interface is null");
}
m_last_req_notification = std::chrono::steady_clock::now();
transport_interface->on_client_connected =
[this, transport_interface](const server::TransportInterface::ClientId& client_id,
const server::TransportInterface::Address& address) {
this->client_connected(transport_interface, client_id, address);
};
transport_interface->on_client_disconnected =
[this, transport_interface](const server::TransportInterface::ClientId& client_id) {
this->client_disconnected(transport_interface, client_id);
};
transport_interface->on_data_available =
[this, transport_interface](const server::TransportInterface::ClientId& client_id,
const server::TransportInterface::Data& data) {
this->data_available(transport_interface, client_id, data);
};
}
}
void RpcHandler::client_connected(const std::shared_ptr<server::TransportInterface>& transport_interface,
const server::TransportInterface::ClientId& client_id,
[[maybe_unused]] const server::TransportInterface::Address& address) {
// In case of a new client, we expect that the client will send an API.Hello request within 5 seconds.
// The API.Hello request is a handshake message sent by the client to establish a connection and verify
// compatibility. If the API.Hello request is not received within the timeout period, the connection will be
// terminated.
// Launch a detached thread to wait for the client hello message
std::thread([this, client_id, transport_interface]() {
std::unique_lock<std::mutex> lock(m_mtx);
if (m_cv_api_hello.wait_for(lock, CLIENT_HELLO_TIMEOUT, [this, client_id] {
return m_api_hello_received.find(client_id) != m_api_hello_received.end();
}) == false) {
// Client did not send hello, close connection
if (transport_interface) {
transport_interface->kill_client_connection(client_id, "Disconnected due to timeout");
} else {
// Log the error instead of throwing an exception in a detached thread
// to avoid undefined behavior.
EVLOG_error << "Transport interface is null during client connection timeout handling";
}
}
}).detach();
}
void RpcHandler::client_disconnected(const std::shared_ptr<server::TransportInterface>& transport_interface,
const server::TransportInterface::ClientId& client_id) {
if (transport_interface) {
std::lock_guard<std::mutex> lock(m_mtx);
m_api_hello_received.erase(client_id);
} else {
// Log the error instead of throwing an exception in a detached thread
// to avoid undefined behavior.
EVLOG_error << "Transport interface is null during client disconnection handling";
}
// Clean up the client data
std::lock_guard<std::mutex> lock(m_mtx);
auto it = messages.find(client_id);
if (it != messages.end()) {
messages.erase(it);
}
}
void RpcHandler::data_available(const std::shared_ptr<server::TransportInterface>& transport_interface,
const server::TransportInterface::ClientId& client_id,
const server::TransportInterface::Data& data) {
// Handle request
using namespace std::chrono;
try {
auto now = steady_clock::now();
nlohmann::json request = nlohmann::json::parse(data);
if (request.is_null()) {
EVLOG_error << "Received null request from client " << client_id;
return;
}
// Store message in a map with client_id as key
std::lock_guard<std::mutex> lock(m_mtx);
messages[client_id].data.push_back(request);
messages[client_id].transport_interface = transport_interface;
EVLOG_debug << "Received message from client " << client_id << ": " << request.dump();
auto elapsed = duration_cast<milliseconds>(now - m_last_req_notification);
if (elapsed >= REQ_COLLECTION_TIMEOUT) {
m_last_req_notification = now; // restart timer
m_cv_data_available.notify_all();
}
} catch (const nlohmann::json::parse_error& e) {
EVLOG_error << "Failed to parse JSON request from client " << client_id << ": " << e.what();
} catch (const std::exception& e) {
EVLOG_error << "Exception occurred while handling data available: " << e.what();
}
}
void RpcHandler::process_client_requests() {
while (m_is_running) {
std::unique_lock<std::mutex> lock(m_mtx);
// Wait for data to be available or timeout
m_cv_data_available.wait_for(lock, REQ_PROCESSING_TIMEOUT, [this]() {
// Iterate over all clients and check if data is available
for (const auto& [client_id, client_req] : messages) {
if (!client_req.data.empty()) {
return true;
}
}
return false;
});
// Process requests for each client
bool all_requests_processed; // Flag to check if all requests are processed
do {
all_requests_processed = true;
for (auto& [client_id, client_req] : messages) {
if (client_req.data.empty()) {
continue; // Skip if no data available
}
// Process the data for this client
auto transport_interface = client_req.transport_interface;
if (!transport_interface) {
EVLOG_error << "Skip data. Transport interface is null for client " << client_id;
continue; // Skip if transport interface is null
}
// Get the first request from the client
nlohmann::json request = client_req.data.front();
client_req.data.pop_front(); // Remove the processed request
// Check if next request is available
if (client_req.data.empty()) {
all_requests_processed = false;
}
// Check if the request is an API.Hello request
if (is_api_hello_req(client_id, request)) {
// Notify condition variable to unblock the waiting thread
m_cv_api_hello.notify_all();
EVLOG_info << "API.Hello request received from client " << client_id;
} else {
// If it is not an API.Hello request, we need to check if the client has already sent an API.Hello
// request, if not, close the connection
if (m_api_hello_received.find(client_id) == m_api_hello_received.end()) {
EVLOG_debug << "Client " << client_id << " did not send API.Hello request. Closing connection.";
transport_interface->kill_client_connection(client_id,
"Disconnected due to missing API.Hello request");
continue; // Skip processing this request
}
}
// Process the request in a detached thread, because HandleRequest is blocking until the response is
// received
std::thread([this, transport_interface, client_id, request]() {
// Call the RPC server with the request
std::string res = m_rpc_server->HandleRequest(request.dump());
// Send the response back to the client
transport_interface->send_data(client_id, res);
EVLOG_debug << "Sent response to client " << client_id << ": " << res;
}).detach();
}
} while (!all_requests_processed && m_is_running);
}
}
void RpcHandler::start_server() {
m_is_running = true;
// Start all transport interfaces
for (const auto& transport_interface : m_transport_interfaces) {
if (!transport_interface->start_server()) {
throw std::runtime_error("Failed to start transport interface server");
}
}
// Start RPC receiver thread
m_rpc_recv_thread = std::thread([this]() { this->process_client_requests(); });
}
void RpcHandler::stop_server() {
m_is_running = false;
// Notify all threads to stop
m_cv_data_available.notify_all();
m_cv_api_hello.notify_all();
// Wait for the RPC receiver thread to finish
if (m_rpc_recv_thread.joinable()) {
m_rpc_recv_thread.join();
}
for (const auto& transport_interface : m_transport_interfaces) {
if (!transport_interface->stop_server()) {
throw std::runtime_error("Failed to stop transport interface server");
}
}
}
} // namespace rpc

View File

@@ -0,0 +1,150 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef RPCHANDLER_HPP
#define RPCHANDLER_HPP
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <functional>
#include <jsonrpccxx/client.hpp>
#include <jsonrpccxx/server.hpp>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
#include "../helpers/LimitDecimalPlaces.hpp"
#include "../server/TransportInterface.hpp"
#include "RequestHandlerInterface.hpp"
#include "methods/Api.hpp"
#include "methods/ChargePoint.hpp"
#include "methods/Evse.hpp"
#include "notifications/ChargePoint.hpp"
#include "notifications/Evse.hpp"
using namespace server;
using namespace jsonrpccxx;
namespace rpc {
static const std::chrono::seconds CLIENT_HELLO_TIMEOUT(5);
// struct to store json data, plus the transport interface
struct ClientReq {
std::shared_ptr<server::TransportInterface> transport_interface;
std::deque<nlohmann::json> data; // Queue of requests
};
class ClientConnector : public jsonrpccxx::IClientConnector {
public:
explicit ClientConnector(std::vector<std::shared_ptr<TransportInterface>>& interfaces,
std::unordered_map<TransportInterface::ClientId, bool>& api_hello_received,
std::mutex& api_hello_mutex) :
hello_received(api_hello_received), transport_interfaces(interfaces), hello_mutex(api_hello_mutex) {
}
std::string Send(const std::string& notification) override {
const std::vector<uint8_t> notif_char_array{notification.begin(), notification.end()};
std::vector<TransportInterface::ClientId> recipients;
{
std::lock_guard<std::mutex> lock(hello_mutex);
recipients.reserve(hello_received.size());
for (const auto& rec : hello_received) {
if (rec.second) {
recipients.push_back(rec.first);
}
}
}
for (const auto& interface : transport_interfaces) {
for (const auto& client_id : recipients) {
interface->send_data(client_id, notif_char_array);
}
}
return "";
}
private:
std::unordered_map<TransportInterface::ClientId, bool>& hello_received;
std::vector<std::shared_ptr<TransportInterface>>& transport_interfaces;
std::mutex& hello_mutex;
};
// Members
class JsonRpc2ServerWithClient : public JsonRpc2Server, public JsonRpcClient {
public:
JsonRpc2ServerWithClient() = delete;
explicit JsonRpc2ServerWithClient(ClientConnector& i) : JsonRpc2Server(), JsonRpcClient(i, version::v2){};
// helper to be able to put data object into caller
// which is something which json-rpc-cxx should be doing
template <typename T> void CallNotificationWithObject(const std::string& name, const T& in, int precision = 3) {
nlohmann::json j;
nlohmann::to_json(j, in);
helpers::round_floats_in_json(j, precision);
CallNotificationNamed(name, j);
}
};
class RpcHandler {
public:
// Constructor and Destructor
RpcHandler() = delete;
// RpcHandler just needs a transport interface array
RpcHandler(std::vector<std::shared_ptr<server::TransportInterface>> transport_interfaces, DataStoreCharger& dataobj,
std::unique_ptr<request_interface::RequestHandlerInterface> request_handler, int precision = 3);
~RpcHandler() = default;
// Methods
void start_server();
void stop_server();
private:
void init_rpc_api();
void init_transport_interfaces();
void client_connected(const std::shared_ptr<server::TransportInterface>& transport_interfaces,
const TransportInterface::ClientId& client_id, const TransportInterface::Address& address);
void client_disconnected(const std::shared_ptr<server::TransportInterface>& transport_interfaces,
const server::TransportInterface::ClientId& client_id);
void data_available(const std::shared_ptr<server::TransportInterface>& transport_interfaces,
const TransportInterface::ClientId& client_id, const TransportInterface::Data& data);
inline bool is_api_hello_req(const TransportInterface::ClientId& client_id, const nlohmann::json& request) {
// Check if the request is a hello request
if (request.contains("method") && request["method"] == methods::METHOD_API_HELLO) {
// If it's a API.Hello request, we set the api_hello_received flag to true
// and notify the condition variable to unblock the waiting thread
m_api_hello_received[client_id] = true;
return true;
}
return false;
}
void process_client_requests();
std::vector<std::shared_ptr<TransportInterface>> m_transport_interfaces;
DataStoreCharger& m_data_store;
std::mutex m_mtx;
std::condition_variable m_cv_api_hello;
std::condition_variable m_cv_data_available;
std::unordered_map<TransportInterface::ClientId, bool> m_api_hello_received;
std::shared_ptr<JsonRpc2ServerWithClient> m_rpc_server;
std::unordered_map<TransportInterface::ClientId, ClientReq> messages;
std::chrono::steady_clock::time_point m_last_req_notification; // Last tick time
std::thread m_rpc_recv_thread;
std::atomic<bool> m_is_running{false};
methods::Api m_methods_api;
methods::ChargePoint m_methods_chargepoint;
methods::Evse m_methods_evse;
ClientConnector m_conn;
std::unique_ptr<notifications::ChargePoint> m_notifications_chargepoint;
std::unique_ptr<notifications::Evse> m_notifications_evse;
int m_precision = 3;
};
} // namespace rpc
#endif // RPCHANDLER_HPP

View File

@@ -0,0 +1,55 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "Api.hpp"
using namespace data;
namespace methods {
RPCDataTypes::HelloResObj Api::hello() {
RPCDataTypes::HelloResObj res{};
// check if data is valid
const auto _chargerinfo = m_dataobj.chargerinfo.get_data();
if (not _chargerinfo.has_value()) {
throw std::runtime_error("Data is not valid");
}
res.charger_info = _chargerinfo.value();
res.authentication_required = is_authentication_required();
res.api_version = get_api_version();
res.everest_version = m_dataobj.everest_version;
if (m_authenticated.has_value()) {
res.authenticated = m_authenticated.value();
}
return res;
}
void Api::set_authentication_required(bool required) {
m_authentication_required = required;
}
bool Api::is_authentication_required() const {
return m_authentication_required;
}
void Api::set_api_version(const std::string& version) {
m_api_version = version;
}
const std::string& Api::get_api_version() const {
return m_api_version;
}
void Api::set_authenticated(bool authenticated) {
m_authenticated = authenticated;
}
bool Api::is_authenticated() const {
if (m_authenticated.has_value()) {
return m_authenticated.value();
}
return false;
}
} // namespace methods

View File

@@ -0,0 +1,48 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef METHODS_API_HPP
#define METHODS_API_HPP
#include <optional>
#include <string>
#include "../../data/DataStore.hpp"
using namespace data;
namespace RPCDataTypes = types::json_rpc_api;
namespace methods {
static const std::string METHOD_API_HELLO = "API.Hello";
/// This class includes all methods of the API namespace.
/// It contains the data object and the methods to access it.
class Api {
public:
// Constructor and Destructor
Api() = delete;
explicit Api(DataStoreCharger& dataobj) : m_dataobj(dataobj), m_authentication_required(false){};
~Api() = default;
// Methods
RPCDataTypes::HelloResObj hello();
void set_authentication_required(bool required);
bool is_authentication_required() const;
void set_api_version(const std::string& version);
const std::string& get_api_version() const;
void set_authenticated(bool authenticated);
bool is_authenticated() const;
private:
DataStoreCharger& m_dataobj;
bool m_authentication_required;
// optional
std::optional<bool> m_authenticated;
std::string m_api_version;
};
} // namespace methods
#endif // METHODS_API_HPP

View File

@@ -0,0 +1,38 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include <string>
#include "ChargePoint.hpp"
namespace methods {
RPCDataTypes::ChargePointGetEVSEInfosResObj ChargePoint::getEVSEInfos() {
RPCDataTypes::ChargePointGetEVSEInfosResObj res{};
// Iterate over all EVSEs and add the EVSEInfo objects to the response
for (const auto& evse : m_dataobj.evses) {
if (const auto _data = evse->evseinfo.get_data(); _data.has_value()) {
res.infos.push_back(_data.value());
}
}
// Error handling
if (res.infos.empty()) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable;
} else {
res.error = RPCDataTypes::ResponseErrorEnum::NoError;
}
return res;
}
RPCDataTypes::ChargePointGetActiveErrorsResObj ChargePoint::getActiveErrors() {
RPCDataTypes::ChargePointGetActiveErrorsResObj res{};
res.active_errors = m_dataobj.chargererrors.get_data().value_or(std::vector<RPCDataTypes::ErrorObj>{});
res.error = RPCDataTypes::ResponseErrorEnum::NoError;
return res;
}
} // namespace methods

View File

@@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef METHODS_CHARGEPOINT_HPP
#define METHODS_CHARGEPOINT_HPP
#include "../../data/DataStore.hpp"
using namespace data;
namespace RPCDataTypes = types::json_rpc_api;
namespace methods {
static const std::string METHOD_CHARGEPOINT_GET_EVSE_INFOS = "ChargePoint.GetEVSEInfos";
static const std::string METHOD_CHARGEPOINT_GET_ACTIVE_ERRORS = "ChargePoint.GetActiveErrors";
/// This class includes all methods of the ChargePoint namespace.
/// It contains the data object and the methods to access it.
class ChargePoint {
public:
// Constructor and Destructor
ChargePoint() = delete;
explicit ChargePoint(DataStoreCharger& dataobj) : m_dataobj(dataobj){};
~ChargePoint() = default;
// Methods
RPCDataTypes::ChargePointGetEVSEInfosResObj getEVSEInfos();
RPCDataTypes::ChargePointGetActiveErrorsResObj getActiveErrors();
private:
DataStoreCharger& m_dataobj;
};
} // namespace methods
#endif // METHODS_CHARGEPOINT_HPP

View File

@@ -0,0 +1,206 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "Evse.hpp"
namespace methods {
RPCDataTypes::EVSEGetInfoResObj Evse::get_info(const int32_t evse_index) {
RPCDataTypes::EVSEGetInfoResObj res{};
const auto* evse = m_dataobj.get_evse_store(evse_index);
if (!evse) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex;
return res;
}
const auto data = evse->evseinfo.get_data();
if (not data.has_value()) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable;
return res;
}
res.info = data.value();
res.error = RPCDataTypes::ResponseErrorEnum::NoError;
return res;
}
RPCDataTypes::EVSEGetStatusResObj Evse::get_status(const int32_t evse_index) {
RPCDataTypes::EVSEGetStatusResObj res{};
const auto* evse = m_dataobj.get_evse_store(evse_index);
if (!evse) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex;
return res;
}
const auto data = evse->evsestatus.get_data();
if (not data.has_value()) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable;
return res;
}
res.status = data.value();
res.error = RPCDataTypes::ResponseErrorEnum::NoError;
return res;
}
RPCDataTypes::EVSEGetHardwareCapabilitiesResObj Evse::get_hardware_capabilities(const int32_t evse_index) {
RPCDataTypes::EVSEGetHardwareCapabilitiesResObj res{};
const auto* evse = m_dataobj.get_evse_store(evse_index);
if (!evse) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex;
return res;
}
const auto data = evse->hardwarecapabilities.get_data();
if (not data.has_value()) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable;
return res;
}
res.hardware_capabilities = data.value();
return res;
}
RPCDataTypes::ErrorResObj Evse::set_charging_allowed(const int32_t evse_index, bool charging_allowed) {
RPCDataTypes::ErrorResObj res{};
const auto* evse = m_dataobj.get_evse_store(evse_index);
if (!evse) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex;
return res;
}
return m_request_handler_ptr->set_charging_allowed(evse_index, charging_allowed);
}
RPCDataTypes::EVSEGetMeterDataResObj Evse::get_meter_data(const int32_t evse_index) {
RPCDataTypes::EVSEGetMeterDataResObj res{};
const auto* evse = m_dataobj.get_evse_store(evse_index);
if (!evse) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex;
return res;
}
const auto data = evse->meterdata.get_data();
if (not data.has_value()) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable;
return res;
}
res.meter_data = data.value();
return res;
}
RPCDataTypes::ErrorResObj Evse::set_ac_charging(const int32_t evse_index, bool charging_allowed, float max_current,
std::optional<int> phase_count) {
RPCDataTypes::ErrorResObj res{};
const auto* evse = m_dataobj.get_evse_store(evse_index);
if (!evse) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex;
return res;
}
return m_request_handler_ptr->set_ac_charging(evse_index, charging_allowed, max_current, phase_count);
}
RPCDataTypes::ErrorResObj Evse::set_ac_charging_current(const int32_t evse_index, float max_current) {
RPCDataTypes::ErrorResObj res{};
const auto* evse = m_dataobj.get_evse_store(evse_index);
if (!evse) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex;
return res;
}
return m_request_handler_ptr->set_ac_charging_current(evse_index, max_current);
}
RPCDataTypes::ErrorResObj Evse::set_ac_charging_phase_count(const int32_t evse_index, int phase_count) {
RPCDataTypes::ErrorResObj res{};
const auto* evse = m_dataobj.get_evse_store(evse_index);
if (!evse) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex;
return res;
}
// Check if the requested phase count is equal to the current active phase count
// If so, we can return a success response without making any changes. Phase switching is not
// necessary in this case.
auto evse_status = evse->evsestatus.get_data();
if (evse_status.has_value() && evse_status.value().ac_charge_status.has_value()) {
if (evse_status.value().ac_charge_status.value().evse_active_phase_count == phase_count) {
res.error = RPCDataTypes::ResponseErrorEnum::NoError;
return res;
}
}
// If phase switching must be performed and the hardware capabilities do not allow it
// we return an error response.
auto hardwarecapabilities = evse->hardwarecapabilities.get_data();
if (hardwarecapabilities.has_value() && hardwarecapabilities.value().phase_switch_during_charging == false) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorOperationNotSupported;
return res;
}
// Check if the requested phase count is within the allowed range
if ((hardwarecapabilities.has_value() && phase_count < hardwarecapabilities.value().min_phase_count_export) ||
phase_count > hardwarecapabilities.value().max_phase_count_export) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorOutOfRange;
return res;
}
// Check if phases are 1 or 3, otherwise return an error
if (phase_count != 1 && phase_count != 3) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidParameter;
return res;
}
return m_request_handler_ptr->set_ac_charging_phase_count(evse_index, phase_count);
}
RPCDataTypes::ErrorResObj Evse::set_dc_charging(const int32_t evse_index, bool charging_allowed, float max_power) {
RPCDataTypes::ErrorResObj res{};
const auto* evse = m_dataobj.get_evse_store(evse_index);
if (!evse) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex;
return res;
}
return m_request_handler_ptr->set_dc_charging(evse_index, charging_allowed, max_power);
}
RPCDataTypes::ErrorResObj Evse::set_dc_charging_power(const int32_t evse_index, float max_power) {
RPCDataTypes::ErrorResObj res{};
const auto* evse = m_dataobj.get_evse_store(evse_index);
if (!evse) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex;
return res;
}
return m_request_handler_ptr->set_dc_charging_power(evse_index, max_power);
}
RPCDataTypes::ErrorResObj Evse::enable_connector(const int32_t evse_index, int connector_index, bool enable,
int priority) {
RPCDataTypes::ErrorResObj res{};
const auto* evse = m_dataobj.get_evse_store(evse_index);
if (!evse) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex;
return res;
}
// Iterate through the connectors to find the one with the given ID
const auto connectors = evse->evseinfo.get_available_connectors();
auto it = std::find_if(connectors.begin(), connectors.end(),
[connector_index](const auto& connector) { return connector.index == connector_index; });
// If not found, return an error
if (it == connectors.end()) {
res.error = RPCDataTypes::ResponseErrorEnum::ErrorInvalidConnectorIndex;
return res;
}
return m_request_handler_ptr->enable_connector(evse_index, connector_index, enable, priority);
}
} // namespace methods

View File

@@ -0,0 +1,65 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef METHODS_EVSE_HPP
#define METHODS_EVSE_HPP
#include <optional>
#include "../../data/DataStore.hpp"
#include "../../rpc/RequestHandlerInterface.hpp"
namespace RPCDataTypes = types::json_rpc_api;
namespace methods {
static const std::string METHOD_EVSE_GET_INFO = "EVSE.GetInfo";
static const std::string METHOD_EVSE_GET_STATUS = "EVSE.GetStatus";
static const std::string METHOD_EVSE_GET_HARDWARE_CAPABILITIES = "EVSE.GetHardwareCapabilities";
static const std::string METHOD_EVSE_SET_CHARGING_ALLOWED = "EVSE.SetChargingAllowed";
static const std::string METHOD_EVSE_GET_METER_DATA = "EVSE.GetMeterData";
static const std::string METHOD_EVSE_SET_AC_CHARGING = "EVSE.SetACCharging";
static const std::string METHOD_EVSE_SET_AC_CHARGING_CURRENT = "EVSE.SetACChargingCurrent";
static const std::string METHOD_EVSE_SET_AC_CHARGING_PHASE_COUNT = "EVSE.SetACChargingPhaseCount";
static const std::string METHOD_EVSE_SET_DC_CHARGING = "EVSE.SetDCCharging";
static const std::string METHOD_EVSE_SET_DC_CHARGING_POWER = "EVSE.SetDCChargingPower";
static const std::string METHOD_EVSE_ENABLE_CONNECTOR = "EVSE.EnableConnector";
/// This class includes all methods of the EVSE namespace.
/// It contains the data object and the methods to access it.
class Evse {
public:
// Constructor and Destructor
// Deleting the default constructor to ensure the class is always initialized with a DataStoreCharger object
Evse() = delete;
Evse(data::DataStoreCharger& dataobj, std::unique_ptr<request_interface::RequestHandlerInterface> req_handler) :
m_dataobj(dataobj), m_request_handler_ptr(std::move(req_handler)) {
}
~Evse() = default;
// Methods
RPCDataTypes::EVSEGetInfoResObj get_info(const int32_t evse_index);
RPCDataTypes::EVSEGetStatusResObj get_status(const int32_t evse_index);
RPCDataTypes::EVSEGetHardwareCapabilitiesResObj get_hardware_capabilities(const int32_t evse_index);
RPCDataTypes::ErrorResObj set_charging_allowed(const int32_t evse_index, bool charging_allowed);
RPCDataTypes::EVSEGetMeterDataResObj get_meter_data(const int32_t evse_index);
RPCDataTypes::ErrorResObj set_ac_charging(const int32_t evse_index, bool charging_allowed, float max_current,
std::optional<int> phase_count);
RPCDataTypes::ErrorResObj set_ac_charging_current(const int32_t evse_index, float max_current);
RPCDataTypes::ErrorResObj set_ac_charging_phase_count(const int32_t evse_index, int phase_count);
RPCDataTypes::ErrorResObj set_dc_charging(const int32_t evse_index, bool charging_allowed, float max_power);
RPCDataTypes::ErrorResObj set_dc_charging_power(const int32_t evse_index, float max_power);
RPCDataTypes::ErrorResObj enable_connector(const int32_t evse_index, int connector_id, bool enable, int priority);
private:
// Reference to the DataStoreCharger object that holds and manages EVSE-related data.
// This object is used to retrieve and update information about EVSEs, such as their status,
// hardware capabilities, and meter data, ensuring consistent access to the underlying data store.
data::DataStoreCharger& m_dataobj;
// Reference to the RequestHandlerInterface object for handling requests
std::unique_ptr<request_interface::RequestHandlerInterface> m_request_handler_ptr;
};
} // namespace methods
#endif // METHODS_EVSE_HPP

View File

@@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "ChargePoint.hpp"
#include "../RpcHandler.hpp"
namespace notifications {
static const std::string NOTIFICATION_CHARGEPOINT_ACTIVE_ERRORS_CHANGED = "ChargePoint.ActiveErrorsChanged";
ChargePoint::ChargePoint(std::shared_ptr<rpc::JsonRpc2ServerWithClient> rpc_server, data::DataStoreCharger& dataobj,
int precision) :
m_dataobj(dataobj), m_rpc_server(std::move(rpc_server)), m_precision(precision) {
// Register notification callbacks for the charger errors
m_dataobj.chargererrors.register_notification_callback(
[this](const std::vector<RPCDataTypes::ErrorObj>& active_errors) {
this->send_active_errors_changed(active_errors);
});
};
// Notifications
void ChargePoint::send_active_errors_changed(const std::vector<RPCDataTypes::ErrorObj>& active_errors) {
RPCDataTypes::ChargePointActiveErrorsChangedObj active_errors_changed;
active_errors_changed.active_errors = active_errors;
m_rpc_server->CallNotificationWithObject(NOTIFICATION_CHARGEPOINT_ACTIVE_ERRORS_CHANGED, active_errors_changed,
m_precision);
}
} // namespace notifications

View File

@@ -0,0 +1,40 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef NOTIFICATIONS_CHARGEPOINT_HPP
#define NOTIFICATIONS_CHARGEPOINT_HPP
#include "../../data/DataStore.hpp"
#include <jsonrpccxx/client.hpp>
namespace RPCDataTypes = types::json_rpc_api;
// forward declaration
namespace rpc {
class JsonRpc2ServerWithClient;
}
namespace notifications {
class ChargePoint {
public:
// Constructor and Destructor
// Deleting the default constructor to ensure the class is always initialized with a DataStoreCharger object
ChargePoint() = delete;
// This needs to take a copy of rpc_server for reference counting, not a reference to it
ChargePoint(std::shared_ptr<rpc::JsonRpc2ServerWithClient> rpc_server, data::DataStoreCharger& dataobj,
int precision = 3);
~ChargePoint() = default;
// Notifications
void send_active_errors_changed(const std::vector<RPCDataTypes::ErrorObj>& active_errors);
private:
// Reference to the DataStoreCharger object that holds EVSE data
data::DataStoreCharger& m_dataobj;
std::shared_ptr<rpc::JsonRpc2ServerWithClient> m_rpc_server;
int m_precision = 3;
};
} // namespace notifications
#endif // NOTIFICATIONS_CHARGEPOINT_HPP

View File

@@ -0,0 +1,48 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "Evse.hpp"
#include "../RpcHandler.hpp"
namespace notifications {
static const std::string NOTIFICATION_EVSE_HWCAPS_CHANGED = "EVSE.HardwareCapabilitiesChanged";
static const std::string NOTIFICATION_EVSE_STATUS_CHANGED = "EVSE.StatusChanged";
static const std::string NOTIFICATION_EVSE_METER_DATA_CHANGED = "EVSE.MeterDataChanged";
Evse::Evse(std::shared_ptr<rpc::JsonRpc2ServerWithClient> rpc_server, data::DataStoreCharger& dataobj, int precision) :
m_dataobj(dataobj), m_rpc_server(std::move(rpc_server)), m_precision(precision) {
for (const auto& evse : m_dataobj.evses) {
const int32_t index = evse->evseinfo.get_index();
evse->hardwarecapabilities.register_notification_callback(
[this, index](const RPCDataTypes::HardwareCapabilitiesObj& data) {
this->send_hardware_capabilities_changed(index, data);
});
evse->evsestatus.register_notification_callback(
[this, index](const RPCDataTypes::EVSEStatusObj& data) { this->send_status_changed(index, data); });
evse->meterdata.register_notification_callback(
[this, index](const RPCDataTypes::MeterDataObj& data) { this->send_meterdata_changed(index, data); });
}
};
// Notifications
void Evse::send_hardware_capabilities_changed(int32_t evse_index, const RPCDataTypes::HardwareCapabilitiesObj& hwcap) {
RPCDataTypes::EVSEHardwareCapabilitiesChangedObj hwcap_changed;
hwcap_changed.evse_index = evse_index;
hwcap_changed.hardware_capabilities = hwcap;
m_rpc_server->CallNotificationWithObject(NOTIFICATION_EVSE_HWCAPS_CHANGED, hwcap_changed, m_precision);
}
void Evse::send_status_changed(int32_t evse_index, const RPCDataTypes::EVSEStatusObj& status) {
RPCDataTypes::EVSEStatusChangedObj status_changed;
status_changed.evse_index = evse_index;
status_changed.evse_status = status;
m_rpc_server->CallNotificationWithObject(NOTIFICATION_EVSE_STATUS_CHANGED, status_changed, m_precision);
}
void Evse::send_meterdata_changed(int32_t evse_index, const RPCDataTypes::MeterDataObj& meter) {
RPCDataTypes::EVSEMeterDataChangedObj meter_changed;
meter_changed.evse_index = evse_index;
meter_changed.meter_data = meter;
m_rpc_server->CallNotificationWithObject(NOTIFICATION_EVSE_METER_DATA_CHANGED, meter_changed, m_precision);
}
} // namespace notifications

View File

@@ -0,0 +1,42 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef NOTIFICATIONS_EVSE_HPP
#define NOTIFICATIONS_EVSE_HPP
#include "../../data/DataStore.hpp"
#include <jsonrpccxx/client.hpp>
namespace RPCDataTypes = types::json_rpc_api;
// forward declaration
namespace rpc {
class JsonRpc2ServerWithClient;
}
namespace notifications {
class Evse {
public:
// Constructor and Destructor
// Deleting the default constructor to ensure the class is always initialized with a DataStoreCharger object
Evse() = delete;
// This needs to take a copy of rpc_server for reference counting, not a reference to it
Evse(std::shared_ptr<rpc::JsonRpc2ServerWithClient> rpc_server, data::DataStoreCharger& dataobj, int precision = 3);
~Evse() = default;
// Notifications
void send_hardware_capabilities_changed(int32_t evse_index, const RPCDataTypes::HardwareCapabilitiesObj& hwcap);
void send_status_changed(int32_t evse_index, const RPCDataTypes::EVSEStatusObj& status);
void send_meterdata_changed(int32_t evse_index, const RPCDataTypes::MeterDataObj& meter);
private:
// Reference to the DataStoreCharger object that holds EVSE data
data::DataStoreCharger& m_dataobj;
std::shared_ptr<rpc::JsonRpc2ServerWithClient> m_rpc_server;
int m_precision = 3;
};
} // namespace notifications
#endif // NOTIFICATIONS_EVSE_HPP

View File

@@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "TransportInterface.hpp"
namespace server {
const std::string& TransportInterface::server_name() const {
return m_server_name;
}
const std::string& TransportInterface::server_url() const {
return m_server_url;
}
void TransportInterface::set_server_url(const std::string& server_url) {
m_server_url = server_url;
}
} // namespace server

View File

@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef TRANSPORTINTERFACE_HPP
#define TRANSPORTINTERFACE_HPP
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
namespace server {
class TransportInterface {
public:
using ClientId = std::string;
using Address = std::string;
using Data = std::vector<uint8_t>;
explicit TransportInterface() = default;
virtual ~TransportInterface() = default;
const std::string& server_name() const;
virtual void send_data(const Data& data) = 0;
virtual void send_data(const ClientId& clientId, const Data& data) = 0;
inline void send_data(const ClientId& clientId, const std::string& data) {
send_data(clientId, std::vector<uint8_t>(data.begin(), data.end()));
};
virtual void kill_client_connection(const ClientId& clientId, const std::string& killReason) = 0;
virtual uint32_t connections_count() const = 0;
const std::string& server_url() const;
void set_server_url(const std::string& serverUrl);
virtual bool running() const = 0;
std::function<void(const ClientId&, const Address&)> on_client_connected;
std::function<void(const ClientId&)> on_client_disconnected;
std::function<void(const ClientId&, const Data&)> on_data_available;
protected:
std::string m_server_url;
std::string m_server_name;
public:
virtual bool start_server() = 0;
virtual bool stop_server() = 0;
};
} // namespace server
#endif // TRANSPORTINTERFACE_HPP

View File

@@ -0,0 +1,259 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "WebsocketServer.hpp"
#include <algorithm>
#include <everest/helpers/helpers.hpp>
#include <everest/logging.hpp>
#include <iostream>
#include <unordered_map>
namespace server {
static const int PER_SESSION_DATA_SIZE{4096};
static void log_callback(int level, const char* line) {
switch (level) {
case LLL_ERR:
EVLOG_error << line;
break;
case LLL_WARN:
EVLOG_warning << line;
break;
default:
case LLL_DEBUG:
EVLOG_debug << line;
break;
}
}
int WebSocketServer::callback_ws(struct lws* wsi, enum lws_callback_reasons reason, [[maybe_unused]] void* user,
void* in, size_t len) {
struct lws_context* context = lws_get_context(wsi);
WebSocketServer* server = static_cast<WebSocketServer*>(lws_context_user(context));
if (!server) {
throw std::runtime_error("Error: WebSocketServer instance not found!");
}
std::unique_lock<std::mutex> lock(server->m_clients_mutex); // To protect access to m_clients
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum"
switch (reason) {
case LWS_CALLBACK_ESTABLISHED: {
// Generate a random UUID for the client
std::string client_id = everest::helpers::get_uuid();
server->m_clients[client_id] = wsi;
char ip_address_buf[INET6_ADDRSTRLEN]{0};
if (lws_get_peer_simple(wsi, ip_address_buf, sizeof(ip_address_buf)) == NULL) {
ip_address_buf[0] = '\0'; // ensure empty string
EVLOG_warning << "Failed to get client IP address";
}
std::string ip_address(ip_address_buf, strnlen(ip_address_buf, sizeof(ip_address_buf)));
lock.unlock(); // Unlock before calling the callback
server->on_client_connected(client_id, ip_address); // Call the on_client_connected callback
lock.lock(); // Lock again after the callback
EVLOG_info << "Client " << client_id << " connected" << (ip_address.empty() ? "" : (" from " + ip_address));
break;
}
case LWS_CALLBACK_CLOSED: {
auto it = std::find_if(server->m_clients.begin(), server->m_clients.end(),
[wsi](const auto& client) { return client.second == wsi; });
if (it != server->m_clients.end()) {
const auto client_id = it->first;
lock.unlock(); // Unlock before calling the callback
EVLOG_info << "Client " << client_id << " disconnected";
server->on_client_disconnected(client_id); // Call the on_client_disconnected callback
lock.lock(); // Lock again after the callback
server->m_clients.erase(it);
}
break;
}
case LWS_CALLBACK_RECEIVE: {
auto it = std::find_if(server->m_clients.begin(), server->m_clients.end(),
[wsi](const auto& client) { return client.second == wsi; });
if (it != server->m_clients.end()) {
const auto client_id = it->first;
auto* data = static_cast<unsigned char*>(in);
std::vector<uint8_t> received_data(data, data + len);
lock.unlock(); // Unlock before calling the callback
server->on_data_available(client_id, received_data); // Call the on_data_available callback
lock.lock(); // Lock again after the callback
}
break;
}
default:
break;
}
#pragma GCC diagnostic pop
lock.unlock(); // Unlock the mutex after processing the callback
return 0;
}
WebSocketServer::WebSocketServer(bool ssl_enabled, int port, const std::string& iface) : m_ssl_enabled(ssl_enabled) {
// Constructor implementation
memset(&m_info, 0, sizeof(m_info));
lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO | LLL_DEBUG, log_callback);
m_info.port = port;
// interface: must not be empty (else nothing to do)
if (iface.empty()) {
throw std::runtime_error("WebSocketServer: parameter 'websocket_interface' must not be empty");
}
if (iface != "all") {
// create a local C-string
char* iface_ptr = new char[iface.size() + 1];
std::strcpy(iface_ptr, iface.c_str());
// store it persistently in a member, and free the local variable
m_iface = std::shared_ptr<char>(iface_ptr, [](char* ptr) {
delete[] ptr; // custom deleter to free the char array
});
m_info.iface = m_iface.get();
}
m_lws_protocols[0] = {"EVerestRpcApi", callback_ws, PER_SESSION_DATA_SIZE, 0, 0, NULL, 0};
m_lws_protocols[1] = LWS_PROTOCOL_LIST_TERM;
m_info.protocols = m_lws_protocols;
m_info.options = LWS_SERVER_OPTION_FAIL_UPON_UNABLE_TO_BIND;
m_info.options |= (m_ssl_enabled ? LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT : 0);
m_info.user = this; // To access WebSocketServer instance in callback
const std::string compiled_lws_version{LWS_LIBRARY_VERSION};
const char* linked_version_cstr = lws_get_library_version();
const std::string linked_lws_version = linked_version_cstr ? linked_version_cstr : "unknown";
EVLOG_info << "libwebsockets version (compiled/runtime): " << compiled_lws_version << " / " << linked_lws_version;
}
WebSocketServer::~WebSocketServer() {
stop_server();
}
bool WebSocketServer::running() const {
return m_running;
}
// send data to all connected clients
void WebSocketServer::send_data(const std::vector<uint8_t>& data) {
std::lock_guard<std::mutex> lock(m_clients_mutex);
for (const auto& client : m_clients) {
struct lws* wsi = client.second;
send_data(wsi, data);
}
}
// send data to client identified by ClientId
void WebSocketServer::send_data(const ClientId& client_id, const std::vector<uint8_t>& data) {
try {
std::lock_guard<std::mutex> lock(m_clients_mutex);
auto it = m_clients.find(client_id);
if (it == m_clients.end()) {
EVLOG_error << "Client " << client_id << " not found";
return;
}
struct lws* wsi = it->second;
send_data(wsi, data);
} catch (const std::exception& e) {
EVLOG_error << "Exception occurred while sending data to client " << client_id << ": " << e.what();
}
}
// send data to client identified by libwebsockets wsi
void WebSocketServer::send_data(struct lws* wsi, const std::vector<uint8_t>& data) {
try {
std::vector<unsigned char> buf(LWS_PRE + data.size());
memcpy(buf.data() + LWS_PRE, data.data(), data.size());
if (lws_write(wsi, buf.data() + LWS_PRE, data.size(), LWS_WRITE_BINARY) < 0) {
EVLOG_error << "Failed to send data to client";
}
} catch (const std::exception& e) {
// Note: the code in the try{} block probably cannot throw an exception
EVLOG_error << "Exception occurred while sending data to client: " << e.what();
}
}
void WebSocketServer::kill_client_connection(const ClientId& client_id, const std::string& kill_reason) {
std::lock_guard<std::mutex> lock(m_clients_mutex);
auto it = m_clients.find(client_id);
if (it != m_clients.end()) {
struct lws* wsi = it->second;
std::string close_reason = kill_reason.empty() ? "Connection closed by server" : kill_reason;
lws_close_reason(wsi, LWS_CLOSE_STATUS_PROTOCOL_ERR,
reinterpret_cast<unsigned char*>(const_cast<char*>(close_reason.data())), close_reason.size());
lws_set_timeout(wsi, PENDING_TIMEOUT_CLOSE_SEND, LWS_TO_KILL_ASYNC); // Set timeout to close the connection
lws_callback_on_writable(wsi); // Notify the event loop to close the connection
EVLOG_info << "Client " << client_id << " connection closed (reason: " << kill_reason << ")";
m_clients.erase(it); // Remove client from map
} else {
EVLOG_error << "Client ID " << client_id << " not found!";
}
}
uint WebSocketServer::connections_count() const {
std::lock_guard<std::mutex> lock(m_clients_mutex);
return m_clients.size();
}
bool WebSocketServer::start_server() {
if (m_running) {
EVLOG_warning << "WebSocket Server is already running";
return true;
}
m_context = lws_create_context(&m_info);
if (!m_context) {
EVLOG_error << "Failed to create WebSocket context";
return false;
}
EVLOG_info << "WebSocket Server running on port " << m_info.port
<< (m_info.iface ? " (interface \"" + std::string(m_info.iface) + "\" only)" : "")
<< (m_ssl_enabled ? " with" : " without") << " TLS";
m_server_thread = std::thread([this]() {
m_running = true;
while (m_running) {
lws_service(m_context, 1000);
}
});
while (!m_running) {
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Wait for server to start
}
return true; // Server started successfully
}
bool WebSocketServer::stop_server() {
if (!m_running) {
return true;
}
m_running = false;
lws_cancel_service(m_context); // To unblock the server thread loop immediately
if (m_server_thread.joinable()) {
m_server_thread.join(); // Wait for server thread to finish
}
if (m_context) {
lws_context_destroy(m_context);
m_context = nullptr;
}
EVLOG_info << "WebSocket Server stopped";
return true;
}
} // namespace server

View File

@@ -0,0 +1,54 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef WEBSOCKETSERVER_HPP
#define WEBSOCKETSERVER_HPP
#include <atomic>
#include <boost/asio.hpp>
#include <libwebsockets.h>
#include <memory>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
#include "TransportInterface.hpp"
namespace server {
class WebSocketServer : public TransportInterface {
public:
// Constructor and Destructor
explicit WebSocketServer(bool ssl_enabled, int port, const std::string& iface);
~WebSocketServer() override;
// Methods
bool running() const override;
void send_data(const std::vector<uint8_t>& data) override;
void send_data(const ClientId& client_id, const Data& data) override;
void send_data(struct lws* wsi, const std::vector<uint8_t>& data);
void kill_client_connection(const ClientId& client_id, const std::string& kill_reason) override;
uint connections_count() const override;
bool start_server() override;
bool stop_server() override;
private:
// Members
bool m_ssl_enabled;
std::shared_ptr<char> m_iface;
struct lws_context_creation_info m_info {};
struct lws_protocols m_lws_protocols[2];
std::atomic<bool> m_running{false};
struct lws_context* m_context = nullptr;
std::thread m_server_thread;
std::unordered_map<ClientId, struct lws*> m_clients; // Client-Mapping
mutable std::mutex m_clients_mutex;
// Methods
static int callback_ws(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len);
};
} // namespace server
#endif // WEBSOCKETSERVER_HPP

View File

@@ -0,0 +1,78 @@
set(TEST_TARGET_NAME ${PROJECT_NAME}_rpcapi_tests)
set(TEST_SOURCES
../data/DataStore.cpp
../data/SessionInfo.cpp
../helpers/Conversions.cpp
../helpers/ErrorHandler.cpp
../helpers/LimitDecimalPlaces.cpp
../rpc/RpcHandler.cpp
../rpc/methods/Api.cpp
../rpc/methods/ChargePoint.cpp
../rpc/methods/Evse.cpp
../rpc/notifications/Evse.cpp
../rpc/notifications/ChargePoint.cpp
../server/WebsocketServer.cpp
server/WebsocketServerTests.cpp
rpc/RpcHandlerTests.cpp
)
add_executable(${TEST_TARGET_NAME} ${TEST_SOURCES})
# The following is needed to import target compile definitions from the module
get_target_property(RPCAPI_COMPILE_DEFINITIONS ${MODULE_NAME} COMPILE_DEFINITIONS)
if (RPCAPI_COMPILE_DEFINITIONS)
target_compile_definitions(${TEST_TARGET_NAME} PRIVATE ${RPCAPI_COMPILE_DEFINITIONS})
endif()
set(INCLUDE_DIR
"../server"
"../rpc"
"../data"
".."
)
target_sources(${TEST_TARGET_NAME}
PRIVATE
"helpers/RequestHandlerDummy.cpp"
"helpers/WebSocketTestClient.cpp"
)
get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR)
target_include_directories(${TEST_TARGET_NAME}
PUBLIC
${INCLUDE_DIR}
${GENERATED_INCLUDE_DIR}
)
if (DISABLE_EDM)
list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/../cmake")
find_package(json-rpc-cxx REQUIRED)
target_include_directories(${TEST_TARGET_NAME}
PRIVATE
${json-rpc-cxx_INCLUDE_DIRS}
)
else()
message("RpcApi/tests: EDM is ensabled")
target_include_directories(${TEST_TARGET_NAME}
PRIVATE
$<TARGET_PROPERTY:json-rpc-cxx,INTERFACE_INCLUDE_DIRECTORIES>
)
endif()
target_link_libraries(${TEST_TARGET_NAME}
PRIVATE
GTest::gtest_main
date::date
date::date-tz
everest::framework
everest::log
everest::helpers
nlohmann_json::nlohmann_json
websockets_shared
)
add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME})
ev_register_test_target(${TEST_TARGET_NAME})

View File

@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef JSON_RPC_UTILS_HPP
#define JSON_RPC_UTILS_HPP
#include "../../helpers/LimitDecimalPlaces.hpp"
#include <nlohmann/json.hpp>
#include <string_view>
constexpr std::string_view JSON_RPC_SPEC_VERSION{"2.0"};
namespace json_rpc_utils {
inline nlohmann::json create_json_rpc_request(const std::string& method, const nlohmann::json& params, int id) {
nlohmann::json request;
request["jsonrpc"] = JSON_RPC_SPEC_VERSION;
request["method"] = method;
request["params"] = params;
request["id"] = id;
return request;
}
inline nlohmann::json create_json_rpc_response(const nlohmann::json& result, int id) {
auto tmp = result;
nlohmann::json response;
response["jsonrpc"] = JSON_RPC_SPEC_VERSION;
helpers::round_floats_in_json(tmp);
response["result"] = tmp;
response["id"] = id;
return response;
}
inline nlohmann::json create_json_rpc_error_response(int code, const std::string& message, int id) {
nlohmann::json error_response;
error_response["jsonrpc"] = JSON_RPC_SPEC_VERSION;
error_response["error"]["code"] = code;
error_response["error"]["message"] = message;
error_response["id"] = id;
return error_response;
}
// To check if single key-value pair is part of a JSON object. Key-value pair must be stored in a JSON object.
inline bool is_key_value_in_json_rpc_result(const nlohmann::json& json_obj, const nlohmann::json& json_key_value) {
if (not json_key_value.is_object()) {
throw std::invalid_argument("json_key_value must be a JSON object");
}
// Check if the JSON object contains the key-value pair in the result object of the JSON-RPC response
if (json_obj.contains("result") && json_obj["result"].is_object()) {
const auto& result_obj = json_obj["result"];
if (result_obj.contains(json_key_value.begin().key()) &&
result_obj[json_key_value.begin().key()] == json_key_value.begin().value()) {
return true;
}
}
return false;
}
} // namespace json_rpc_utils
#endif // JSON_RPC_UTILS_HPP

View File

@@ -0,0 +1,66 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "RequestHandlerDummy.hpp"
using namespace types::json_rpc_api;
RequestHandlerDummy::RequestHandlerDummy(data::DataStoreCharger& dataobj) : data_store(dataobj) {
}
types::json_rpc_api::ErrorResObj RequestHandlerDummy::set_charging_allowed(const int32_t evse_index,
bool charging_allowed) {
ErrorResObj res{};
auto evse_store = data_store.get_evse_store(evse_index);
evse_store->evsestatus.set_charging_allowed(charging_allowed);
return res;
}
types::json_rpc_api::ErrorResObj RequestHandlerDummy::set_ac_charging(const int32_t evse_index, bool charging_allowed,
bool max_current,
std::optional<int> phase_count) {
types::json_rpc_api::ErrorResObj res{types::json_rpc_api::ResponseErrorEnum::NoError};
return res;
}
types::json_rpc_api::ErrorResObj RequestHandlerDummy::set_ac_charging_current(const int32_t evse_index,
float max_current) {
ErrorResObj res{};
auto evse_store = data_store.get_evse_store(evse_index);
auto evse_state = evse_store->evsestatus.get_state();
// Skipping applying limits if charging is not allowed.
// In this case, the zero limit is already applied to prevent charging. This value should not be overridden.
if (evse_store->evsestatus.get_data()->charging_allowed == false) {
res.error = ResponseErrorEnum::NoError;
return res;
}
// Wait until the limits are applied or timeout occurs
if (evse_store->evsestatus.wait_until_current_limit_applied(max_current, std::chrono::milliseconds(100))) {
res.error = ResponseErrorEnum::NoError;
} else {
res.error = ResponseErrorEnum::ErrorValuesNotApplied;
}
return res;
}
types::json_rpc_api::ErrorResObj RequestHandlerDummy::set_ac_charging_phase_count(const int32_t evse_index,
int phase_count) {
types::json_rpc_api::ErrorResObj res{types::json_rpc_api::ResponseErrorEnum::NoError};
return res;
}
types::json_rpc_api::ErrorResObj RequestHandlerDummy::set_dc_charging(const int32_t evse_index, bool charging_allowed,
float max_power) {
types::json_rpc_api::ErrorResObj res{types::json_rpc_api::ResponseErrorEnum::NoError};
return res;
}
types::json_rpc_api::ErrorResObj RequestHandlerDummy::set_dc_charging_power(const int32_t evse_index, float max_power) {
types::json_rpc_api::ErrorResObj res{types::json_rpc_api::ResponseErrorEnum::NoError};
return res;
}
types::json_rpc_api::ErrorResObj RequestHandlerDummy::enable_connector(const int32_t evse_index, int connector_id,
bool enable, int priority) {
types::json_rpc_api::ErrorResObj res{types::json_rpc_api::ResponseErrorEnum::NoError};
return res;
}

View File

@@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef REQUESTHANDLERDUMMY_HPP
#define REQUESTHANDLERDUMMY_HPP
#include "../data/DataStore.hpp"
#include <../rpc/RequestHandlerInterface.hpp>
#include <types/json_rpc_api/json_rpc_api.hpp>
class RequestHandlerDummy : public request_interface::RequestHandlerInterface {
public:
RequestHandlerDummy() = delete;
explicit RequestHandlerDummy(data::DataStoreCharger& data_store);
~RequestHandlerDummy() override = default;
types::json_rpc_api::ErrorResObj set_charging_allowed(const int32_t evse_index, bool charging_allowed) override;
types::json_rpc_api::ErrorResObj set_ac_charging(const int32_t evse_index, bool charging_allowed, bool max_current,
std::optional<int> phase_count) override;
types::json_rpc_api::ErrorResObj set_ac_charging_current(const int32_t evse_index, float max_current) override;
types::json_rpc_api::ErrorResObj set_ac_charging_phase_count(const int32_t evse_index, int phase_count) override;
types::json_rpc_api::ErrorResObj set_dc_charging(const int32_t evse_index, bool charging_allowed,
float max_power) override;
types::json_rpc_api::ErrorResObj set_dc_charging_power(const int32_t evse_index, float max_power) override;
types::json_rpc_api::ErrorResObj enable_connector(const int32_t evse_index, int connector_id, bool enable,
int priority) override;
private:
data::DataStoreCharger& data_store;
};
#endif // REQUESTHANDLERDUMMY_HPP

View File

@@ -0,0 +1,180 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include "WebSocketTestClient.hpp"
#include <nlohmann/json.hpp>
#include "JsonRpcUtils.hpp"
using namespace json_rpc_utils;
WebSocketTestClient::WebSocketTestClient(const std::string& address, int port) :
m_address(address), m_port(port), m_context(nullptr), m_wsi(nullptr), m_connected(false) {
struct lws_protocols protocols[] = {{"EVerestRpcApi", callback, 0, 0, 0, NULL, 0}, LWS_PROTOCOL_LIST_TERM};
struct lws_context_creation_info info;
memset(&info, 0, sizeof(info));
info.port = CONTEXT_PORT_NO_LISTEN; /* client */
info.protocols = protocols;
info.gid = -1;
info.uid = -1;
info.user = this;
m_context = lws_create_context(&info);
if (!m_context) {
throw std::runtime_error("Failed to create WebSocket m_context");
}
m_ccinfo.context = m_context;
m_ccinfo.address = m_address.c_str();
m_ccinfo.port = m_port;
m_ccinfo.path = "/";
m_ccinfo.host = m_ccinfo.address;
m_ccinfo.origin = m_ccinfo.address;
m_ccinfo.protocol = "EVerestRpcApi";
}
WebSocketTestClient::~WebSocketTestClient() {
close();
}
int WebSocketTestClient::callback(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len) {
WebSocketTestClient* client = static_cast<WebSocketTestClient*>(lws_context_user(lws_get_context(wsi)));
if (client == nullptr) {
std::cerr << "Error: WebSocketTestClient instance not found!";
return -1;
}
switch (reason) {
case LWS_CALLBACK_CLIENT_ESTABLISHED:
client->m_connected = true;
client->m_cv.notify_all();
break;
case LWS_CALLBACK_CLIENT_RECEIVE: {
std::lock_guard<std::mutex> lock(client->m_cv_mutex);
try {
client->m_received_data.assign(static_cast<char*>(in), len);
client->m_cv.notify_all();
} catch (const std::exception& e) {
EVLOG_error << "Exception occurred while handling data available: " << e.what();
}
break;
}
case LWS_CALLBACK_CLIENT_CLOSED:
case LWS_CALLBACK_CLOSED_CLIENT_HTTP: {
client->m_connected = false;
EVLOG_info << "Client closed connection: " << (in ? static_cast<const char*>(in) : "(null)")
<< " reason: " << reason;
break;
}
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
EVLOG_error << "Client connection error: " << (in ? static_cast<const char*>(in) : "(null)");
break;
default:
break;
}
return 0;
}
bool WebSocketTestClient::connect() {
if (m_context == nullptr) {
EVLOG_error << "Error: WebSocket m_context not found!";
return false;
}
stop_lws_service_thread(); // Stop any existing service thread, otherwise the connect will fail
m_wsi = lws_client_connect_via_info(&m_ccinfo);
if (m_wsi == nullptr) {
EVLOG_error << "Error while connecting to WebSocket server";
} else {
EVLOG_info << "Connecting to WebSocket server...";
start_lws_service_thread();
}
return m_wsi != nullptr;
}
void WebSocketTestClient::start_lws_service_thread() {
if (m_lws_service_running) {
return;
}
m_lws_service_thread = std::thread([this]() {
m_lws_service_running = true;
while (m_lws_service_running) {
lws_service(m_context, 0);
}
});
// Wait for the service thread to start
while (!m_lws_service_running) {
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Wait for service thread to start
}
}
void WebSocketTestClient::stop_lws_service_thread() {
if (!m_lws_service_running) {
return;
}
m_lws_service_running = false;
lws_cancel_service(m_context);
if (m_lws_service_thread.joinable()) {
m_lws_service_thread.join();
}
}
bool WebSocketTestClient::is_connected() {
return m_connected;
}
void WebSocketTestClient::send(const std::string& message) {
if (!m_connected)
return;
try {
std::vector<unsigned char> buf(LWS_PRE + message.size());
memcpy(buf.data() + LWS_PRE, message.c_str(), message.size());
lws_write(m_wsi, buf.data() + LWS_PRE, message.size(), LWS_WRITE_TEXT);
} catch (const std::exception& e) {
EVLOG_error << "Error while sending message: " << e.what();
}
}
const std::string& WebSocketTestClient::receive() const {
return m_received_data;
}
void WebSocketTestClient::close() {
if (m_wsi) {
if (m_connected == true) {
lws_close_reason(m_wsi, LWS_CLOSE_STATUS_NORMAL, nullptr, 0);
}
if (m_context == nullptr) {
EVLOG_error << "Error: WebSocket m_context not found!";
return;
}
stop_lws_service_thread();
m_wsi = nullptr;
if (m_lws_service_thread.joinable()) {
m_lws_service_thread.join(); // Wait for client thread to finish
}
}
if (m_context) {
lws_context_destroy(m_context);
m_context = nullptr;
}
m_connected = false;
EVLOG_info << "WebSocket client closed";
}
void WebSocketTestClient::send_api_hello_req() {
nlohmann::json apiHelloReq = create_json_rpc_request("API.Hello", {}, 1);
send(apiHelloReq.dump());
}

View File

@@ -0,0 +1,94 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#ifndef WEBSOCKETTESTCLIENT_HPP
#define WEBSOCKETTESTCLIENT_HPP
#include <atomic>
#include <condition_variable>
#include <cstring>
#include <everest/logging.hpp>
#include <iostream>
#include <libwebsockets.h>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
class WebSocketTestClient {
public:
WebSocketTestClient(const std::string& address, int port);
~WebSocketTestClient();
bool connect();
bool is_connected();
void send(const std::string& message);
void send_api_hello_req();
const std::string& receive() const;
void close();
std::string get_received_data() {
std::string data;
{
std::lock_guard<std::mutex> lock(m_cv_mutex);
data = m_received_data;
m_received_data.clear(); // Clear the received data after getting it
}
return data;
}
std::string wait_for_data(std::chrono::milliseconds timeout, bool is_result = true) {
std::unique_lock<std::mutex> lock(m_cv_mutex);
bool received = m_cv.wait_for(lock, timeout, [this, is_result]() {
if (m_received_data.empty())
return false;
if (is_result) {
// Check string for "result" and "id" keys if is_result is true
bool has_result = m_received_data.find("\"result\"") != std::string::npos;
bool has_id = m_received_data.find("\"id\"") != std::string::npos;
return has_result && has_id;
}
return true; // For regular responses, we just check if we have data
});
if (!received) {
return ""; // Timeout
}
std::string data = m_received_data;
m_received_data.clear(); // Clear the received data after getting it
return data;
}
bool wait_for_response(std::chrono::milliseconds timeout) {
std::unique_lock<std::mutex> lock(m_cv_mutex);
return m_cv.wait_for(lock, timeout, [this] { return !m_received_data.empty(); });
}
bool wait_until_connected(std::chrono::milliseconds timeout) {
std::unique_lock<std::mutex> lock(m_cv_mutex);
return m_cv.wait_for(lock, timeout, [this] { return m_connected.load(); });
}
void start_lws_service_thread();
void stop_lws_service_thread();
private:
static int callback(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len);
std::string m_address;
int m_port;
struct lws_context* m_context;
struct lws_client_connect_info m_ccinfo {};
struct lws* m_wsi;
std::atomic<bool> m_connected{false};
std::atomic<bool> m_lws_service_running{false};
std::thread m_lws_service_thread;
std::string m_received_data;
public:
// Condition variable to wait for response
std::condition_variable m_cv;
std::mutex m_cv_mutex;
};
#endif // WEBSOCKETTESTCLIENT_HPP

View File

@@ -0,0 +1,877 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include <gtest/gtest.h>
#include <thread>
#include "../data/DataStore.hpp"
#include "../helpers/ErrorHandler.hpp"
#include "../helpers/JsonRpcUtils.hpp"
#include "../helpers/RequestHandlerDummy.hpp"
#include "../helpers/WebSocketTestClient.hpp"
#include "../rpc/RpcHandler.hpp"
#include "../server/WebsocketServer.hpp"
using namespace server;
using namespace rpc;
using namespace json_rpc_utils;
class RpcHandlerTest : public ::testing::Test {
protected:
int test_port = 8080;
void SetUp() override {
// Start the WebSocket server
m_websocket_server = std::make_unique<server::WebSocketServer>(false, test_port, "lo");
lws_set_log_level(LLL_ERR | LLL_WARN, NULL);
// Create RpcHandler instance. Move the transport interfaces and request handler to the RpcHandler
std::vector<std::shared_ptr<server::TransportInterface>> transport_interfaces;
request_handler = std::make_unique<RequestHandlerDummy>(data_store);
transport_interfaces.push_back(std::shared_ptr<server::TransportInterface>(std::move(m_websocket_server)));
m_rpc_handler =
std::make_unique<RpcHandler>(std::move(transport_interfaces), data_store, std::move(request_handler));
m_rpc_handler->start_server();
initialize_data_store();
}
void TearDown() override {
m_rpc_handler->stop_server();
}
void initialize_data_store() {
// Set up the data store with test data
RPCDataTypes::ChargerInfoObj charger_info;
charger_info.firmware_version = "1.0.0";
charger_info.model = "Test Charger";
charger_info.serial = "123456789";
charger_info.vendor = "Test Vendor";
data_store.chargerinfo.set_data(charger_info);
data_store.everest_version = "2025.1.0";
// Properly initialize EVSE objects
data_store.evses.emplace_back(std::make_unique<data::DataStoreEvse>());
}
void send_req_and_validate_res(WebSocketTestClient& client, const nlohmann::json& request,
const nlohmann::json& expected_response,
bool (*cmp_f)(const nlohmann::json&, const nlohmann::json&) = nullptr) {
// Send the request
client.send(request.dump());
// Wait for the response
std::string data = client.wait_for_data(std::chrono::seconds(1));
// Check if the response is not empty
ASSERT_FALSE(data.empty());
nlohmann::json response = nlohmann::json::parse(data);
// Check if the response is valid
if (cmp_f != nullptr) {
// Compare the response with the expected response using the provided comparison function
bool res = cmp_f(response, expected_response);
if (!res) {
// If the comparison fails, print the response and expected response for debugging
EVLOG_error << "Expected equality of these values: response: " << response.dump();
EVLOG_error << "Expected response: " << expected_response.dump();
}
ASSERT_TRUE(res);
} else {
// Compare the response with the expected response
ASSERT_EQ(response, expected_response);
}
}
std::unique_ptr<server::WebSocketServer> m_websocket_server;
std::unique_ptr<rpc::RpcHandler> m_rpc_handler;
// Condition variable to wait for response
std::condition_variable cv;
std::mutex cv_mutex;
// Data store object used to manage and access charger-related data, including EVSEs, connectors, and charger info.
data::DataStoreCharger data_store;
// Dummy request handler. Needed to create the responses of synchronous requests
std::unique_ptr<request_interface::RequestHandlerInterface> request_handler;
};
// Test: Connect to WebSocket server and check if API.Hello timeout occurs
TEST_F(RpcHandlerTest, ApiHelloTimeout) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Wait for the client hello timeout
EVLOG_info << "Waiting for client hello timeout...";
std::this_thread::sleep_for(std::chrono::seconds(CLIENT_HELLO_TIMEOUT) + std::chrono::milliseconds(100));
// Check if the client is still connected
ASSERT_FALSE(client.is_connected());
}
// Test: Connect to WebSocket server and send API.Hello request
TEST_F(RpcHandlerTest, ApiHelloReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up the expected response
RPCDataTypes::HelloResObj result;
result.authentication_required = false;
result.api_version = API_VERSION;
result.charger_info = data_store.chargerinfo.get_data().value();
result.everest_version = data_store.everest_version;
nlohmann::json expected_response = {{"jsonrpc", JSON_RPC_SPEC_VERSION}, {"result", result}, {"id", 1}};
// Send Api.Hello request
client.send_api_hello_req();
// Wait for the response
std::string data = client.wait_for_data(std::chrono::seconds(1));
// Check if the response is not empty
ASSERT_FALSE(data.empty());
// Check if the response is valid
nlohmann::json response = nlohmann::json::parse(data);
ASSERT_EQ(response, expected_response);
// Check if the client is still connected
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
}
// Test: Connect to WebSocket server and send EVSEInfo request
TEST_F(RpcHandlerTest, ChargePointGetEVSEInfosReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up the data store with test data
RPCDataTypes::ChargePointGetEVSEInfosResObj result; // Expected response
data_store.evses.emplace_back(std::make_unique<data::DataStoreEvse>());
data_store.evses.emplace_back(std::make_unique<data::DataStoreEvse>());
RPCDataTypes::EVSEInfoObj evse_info;
evse_info.index = 1;
evse_info.available_connectors.emplace_back();
evse_info.available_connectors[0].index = 1;
evse_info.available_connectors[0].type = types::json_rpc_api::ConnectorTypeEnum::cCCS2;
evse_info.available_connectors.emplace_back();
evse_info.available_connectors[1].index = 2;
evse_info.available_connectors[1].type = types::json_rpc_api::ConnectorTypeEnum::cCCS1;
evse_info.description = "Test EVSE 1";
result.error = RPCDataTypes::ResponseErrorEnum::NoError; ///< No error
// Set up request and expected response
nlohmann::json charge_point_get_evse_infos_req = create_json_rpc_request("ChargePoint.GetEVSEInfos", {}, 1);
nlohmann::json expected_error_no_data = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable)}};
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send ChargePoint.GetEVSEInfos request and validate response, no data available
send_req_and_validate_res(client, charge_point_get_evse_infos_req, expected_error_no_data,
is_key_value_in_json_rpc_result);
// Set up the data store with test data
data_store.evses[0]->evseinfo.set_data(evse_info);
result.infos.push_back(evse_info);
evse_info.index = 2;
evse_info.description = "Test EVSE 2";
data_store.evses[1]->evseinfo.set_data(evse_info);
result.infos.push_back(evse_info);
// Set up expected response
nlohmann::json expected_response = create_json_rpc_response(result, 1);
// Send ChargePoint.GetEVSEInfos request and validate response
send_req_and_validate_res(client, charge_point_get_evse_infos_req, expected_response);
}
// Test: Connect to WebSocket server and send ChargePoint.GetActiveErrors request
TEST_F(RpcHandlerTest, ChargePointGetActiveErrorsReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up the data store with test data
RPCDataTypes::EVSEInfoObj evse_info;
evse_info.index = 1; ///< Unique identifier
evse_info.available_connectors.emplace_back();
evse_info.available_connectors[0].index = 1;
evse_info.available_connectors[0].type = types::json_rpc_api::ConnectorTypeEnum::cCCS2;
evse_info.description = "Test EVSE 1";
data_store.evses[0]->evseinfo.set_data(evse_info);
// Add a second EVSE with a different index
data_store.evses.emplace_back(std::make_unique<data::DataStoreEvse>());
evse_info.index = 2; ///< Unique identifier
evse_info.description = "Test EVSE 2";
data_store.evses[1]->evseinfo.set_data(evse_info);
// Set up the EVSE status for both EVSEs
RPCDataTypes::EVSEStatusObj evse_status1, evse_status2;
evse_status1.error_present = false;
evse_status2.error_present = false;
data_store.evses[0]->evsestatus.set_data(evse_status1);
data_store.evses[1]->evsestatus.set_data(evse_status2);
types::json_rpc_api::ErrorObj error0, error1, error2;
error0.origin.evse_index = 1;
error0.origin.connector_index = 0;
error0.origin.module_id = "evse_1";
error0.origin.implementation_id = "board_support";
error0.message = "Test error message";
error0.description = "Test error description";
error0.uuid = "6db8758b-194d-48e1-99af-c8f0b1d2e3f3";
error0.severity = types::json_rpc_api::Severity::Low;
error0.timestamp = "2025-01-01T12:00:00Z";
error0.type = "TestErrorType";
error1.origin.evse_index = 2;
error1.origin.connector_index = 1;
error1.origin.module_id = "evse_2";
error1.origin.implementation_id = "board_support";
error1.message = "Test error message";
error1.description = "Test error description";
error1.uuid = "7db8758b-194d-48e1-99af-c8f0b1d2e3f4";
error1.severity = types::json_rpc_api::Severity::Medium;
error1.timestamp = "2025-01-01T12:00:00Z";
error1.type = "TestErrorType";
error2.origin.evse_index = 2;
error2.origin.connector_index = 1;
error2.origin.module_id = "evse_2";
error2.origin.implementation_id = "board_support";
error2.message = "Another test error message";
error2.description = "Another test error description";
error2.uuid = "8db8758b-194d-48e1-99af-c8f0b1d2e3f5";
error2.severity = types::json_rpc_api::Severity::High;
error2.timestamp = "2025-01-01T12:00:01Z";
error2.type = "AnotherTestErrorType";
// Set up the data store with test data
RPCDataTypes::ChargePointGetActiveErrorsResObj result; // Expected response
result.active_errors.push_back(error1);
result.active_errors.push_back(error2);
result.error = RPCDataTypes::ResponseErrorEnum::NoError; ///< No error
// Set up request and expected response
nlohmann::json charge_point_get_active_errors_req = create_json_rpc_request("ChargePoint.GetActiveErrors", {}, 1);
nlohmann::json expected_response = create_json_rpc_response(result, 1);
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Raise the error in the data store for the first EVSE
helpers::handle_error_raised(data_store, error0);
// Check if the error is set to present in the EVSE status
auto tmp_evse_store_1 = data_store.get_evse_store(1);
ASSERT_TRUE(tmp_evse_store_1 != nullptr);
ASSERT_TRUE(tmp_evse_store_1->evsestatus.get_data().value().error_present);
// Clear the error in the data store for the first EVSE
helpers::handle_error_cleared(data_store, error0);
// Check if the error is cleared in the EVSE status
ASSERT_FALSE(tmp_evse_store_1->evsestatus.get_data().value().error_present);
// Raise the second error in the data store for the second EVSE
helpers::handle_error_raised(data_store, error1);
helpers::handle_error_raised(data_store, error2);
// Check if error is set to present in the EVSE status
ASSERT_FALSE(tmp_evse_store_1->evsestatus.get_data().value().error_present);
auto tmp_evse_store_2 = data_store.get_evse_store(2);
ASSERT_TRUE(tmp_evse_store_2 != nullptr);
ASSERT_TRUE(tmp_evse_store_2->evsestatus.get_data().value().error_present);
// Send ChargePoint.GetActiveErrors request and validate response
send_req_and_validate_res(client, charge_point_get_active_errors_req, expected_response);
// Clear the errors for the second EVSE
helpers::handle_error_cleared(data_store, error1);
// Check if the error is still present in the EVSE status
ASSERT_TRUE(tmp_evse_store_2->evsestatus.get_data().value().error_present);
// Clear the second error
helpers::handle_error_cleared(data_store, error2);
// Check if error is cleared in the EVSE status
ASSERT_FALSE(tmp_evse_store_1->evsestatus.get_data().value().error_present);
ASSERT_FALSE(tmp_evse_store_2->evsestatus.get_data().value().error_present);
}
// Test: Connect to WebSocket server and send EVSE.Infos request with valid and invalid index
TEST_F(RpcHandlerTest, EvseGetEVSEInfosReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up requests
nlohmann::json evse_get_evse_infos_req_1 = create_json_rpc_request("EVSE.GetInfo", {{"evse_index", 1}}, 1);
nlohmann::json evse_get_evse_infos_req_2 = create_json_rpc_request("EVSE.GetInfo", {{"evse_index", 2}}, 1);
nlohmann::json evse_get_infos_req_invalid_index = create_json_rpc_request("EVSE.GetInfo", {{"evse_index", 99}}, 1);
// Set up the data store with test data
RPCDataTypes::EVSEInfoObj evse_info;
evse_info.index = 1; ///< Unique identifier
evse_info.available_connectors.emplace_back();
evse_info.available_connectors[0].index = 1;
evse_info.available_connectors[0].type = types::json_rpc_api::ConnectorTypeEnum::cCCS2;
evse_info.available_connectors.emplace_back();
evse_info.available_connectors[1].index = 2;
evse_info.available_connectors[1].type = types::json_rpc_api::ConnectorTypeEnum::cCCS1;
evse_info.description = "Test EVSE 1";
data_store.evses[0]->evseinfo.set_data(evse_info);
// Expected response 1
RPCDataTypes::EVSEGetInfoResObj result_1;
result_1.info = evse_info;
result_1.error = RPCDataTypes::ResponseErrorEnum::NoError; ///< No error
// Set up the second EVSE info
evse_info.index = 2;
evse_info.available_connectors[0].type = types::json_rpc_api::ConnectorTypeEnum::cType2;
evse_info.available_connectors[1].type = types::json_rpc_api::ConnectorTypeEnum::sType2;
data_store.evses.emplace_back(std::make_unique<data::DataStoreEvse>());
data_store.evses[1]->evseinfo.set_data(evse_info);
// Set up the expected responses
nlohmann::json expected_response_index_1 = create_json_rpc_response(result_1, 1);
// Expected response 2
RPCDataTypes::EVSEGetInfoResObj result_2;
result_2.info = evse_info;
result_2.error = RPCDataTypes::ResponseErrorEnum::NoError; ///< No error
nlohmann::json expected_response_index_2 = create_json_rpc_response(result_2, 1);
// Expected error object in case of invalid ID
nlohmann::json expected_error = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}};
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send EVSE.GetEVSEInfos request 1 and validate response
send_req_and_validate_res(client, evse_get_evse_infos_req_1, expected_response_index_1);
// Send EVSE.GetEVSEInfos request 2 and validate response
send_req_and_validate_res(client, evse_get_evse_infos_req_2, expected_response_index_2);
// Send EVSE.GetEVSEInfos request with invalid ID and validate response
send_req_and_validate_res(client, evse_get_infos_req_invalid_index, expected_error,
is_key_value_in_json_rpc_result);
}
// Test: Connect to WebSocket server and send Evse.GetStatusReq request with valid and invalid index
TEST_F(RpcHandlerTest, EvseGetStatusReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up the requests
nlohmann::json evse_get_status_req_valid_index = create_json_rpc_request("EVSE.GetStatus", {{"evse_index", 1}}, 1);
nlohmann::json evse_get_status_req_invalid_index =
create_json_rpc_request("EVSE.GetStatus", {{"evse_index", 99}}, 1);
// Set up the data store with test data
RPCDataTypes::EVSEInfoObj evse_info;
evse_info.index = 1; ///< Unique identifier
data_store.evses[0]->evseinfo.set_data(evse_info);
RPCDataTypes::EVSEStatusObj evse_status;
evse_status.charged_energy_wh = 123.45;
evse_status.discharged_energy_wh = 123.45;
evse_status.charging_duration_s = 600;
evse_status.charging_allowed = true;
evse_status.available = true;
evse_status.active_connector_index = 1;
evse_status.error_present = false;
evse_status.charge_protocol = types::json_rpc_api::ChargeProtocolEnum::ISO15118; ///< charge_protocol
evse_status.state = types::json_rpc_api::EVSEStateEnum::Charging;
evse_status.ac_charge_status.emplace().evse_active_phase_count = 3;
// Set up the expected responses
RPCDataTypes::EVSEGetStatusResObj res_valid_id;
res_valid_id.status = evse_status;
res_valid_id.error = RPCDataTypes::ResponseErrorEnum::NoError;
nlohmann::json expected_error_no_data = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable)}};
nlohmann::json res_obj_invalid_index = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}};
nlohmann::json expected_response = create_json_rpc_response(res_valid_id, 1);
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send EVSE.GetStatus request with valid ID, but no data available
send_req_and_validate_res(client, evse_get_status_req_valid_index, expected_error_no_data,
is_key_value_in_json_rpc_result);
// Set the EVSE status in the data store
data_store.evses[0]->evsestatus.set_data(evse_status);
// Send EVSE.GetStatus request with valid ID
send_req_and_validate_res(client, evse_get_status_req_valid_index, expected_response);
// Send EVSE.GetStatus request with invalid ID
send_req_and_validate_res(client, evse_get_status_req_invalid_index, res_obj_invalid_index,
is_key_value_in_json_rpc_result);
}
// Test: Connect to WebSocket server and send EVSE.GetHardwareCapabilities request with valid and invalid index
TEST_F(RpcHandlerTest, EvseGetHardwareCapabilitiesReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up requests
nlohmann::json evse_get_hardware_capabilities_req_valid_index =
create_json_rpc_request("EVSE.GetHardwareCapabilities", {{"evse_index", 1}}, 1);
nlohmann::json evse_get_hardware_capabilities_req_invalid_index =
create_json_rpc_request("EVSE.GetHardwareCapabilities", {{"evse_index", 99}}, 1);
// Set up the data store with test data
RPCDataTypes::EVSEInfoObj evse_info;
evse_info.index = 1;
data_store.evses[0]->evseinfo.set_data(evse_info);
RPCDataTypes::EVSEGetHardwareCapabilitiesResObj result;
result.error = RPCDataTypes::ResponseErrorEnum::NoError;
result.hardware_capabilities.max_current_A_export = 32.0;
result.hardware_capabilities.max_current_A_import = 16.0;
result.hardware_capabilities.max_phase_count_export = 3;
result.hardware_capabilities.max_phase_count_import = 3;
result.hardware_capabilities.min_current_A_export = 6.0;
result.hardware_capabilities.min_current_A_import = 6.0;
result.hardware_capabilities.min_phase_count_export = 1;
result.hardware_capabilities.min_phase_count_import = 1;
result.hardware_capabilities.phase_switch_during_charging = true;
// Set up the expected responses
nlohmann::json expected_response = create_json_rpc_response(result, 1);
nlohmann::json expected_error_no_data = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable)}};
nlohmann::json expected_error_invalid_index = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}};
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send EVSE.GetHardwareCapabilities request with valid ID, but no hardware capabilities available
send_req_and_validate_res(client, evse_get_hardware_capabilities_req_valid_index, expected_error_no_data,
is_key_value_in_json_rpc_result);
// Set the hardware capabilities in the data store
data_store.evses[0]->hardwarecapabilities.set_data(result.hardware_capabilities);
// Send EVSE.GetHardwareCapabilities request with valid ID
send_req_and_validate_res(client, evse_get_hardware_capabilities_req_valid_index, expected_response);
// Send EVSE.GetHardwareCapabilities request with invalid ID
send_req_and_validate_res(client, evse_get_hardware_capabilities_req_invalid_index, expected_error_invalid_index,
is_key_value_in_json_rpc_result);
}
// Test: Connect to WebSocket server and send EVSE.SetChargingAllowed request with valid and invalid index
TEST_F(RpcHandlerTest, EvseSetChargingAllowedReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up requests
nlohmann::json evse_set_charging_allowed_req_valid_index =
create_json_rpc_request("EVSE.SetChargingAllowed", {{"evse_index", 1}, {"charging_allowed", true}}, 1);
nlohmann::json evse_set_charging_allowed_req_valid_index_false =
create_json_rpc_request("EVSE.SetChargingAllowed", {{"evse_index", 1}, {"charging_allowed", false}}, 1);
nlohmann::json evse_set_charging_allowed_req_invalid_index =
create_json_rpc_request("EVSE.SetChargingAllowed", {{"evse_index", 99}, {"charging_allowed", true}}, 1);
// Set up the data store with test data
RPCDataTypes::EVSEInfoObj evse_info;
evse_info.index = 1;
data_store.evses[0]->evseinfo.set_data(evse_info);
RPCDataTypes::EVSEStatusObj evse_status;
evse_status.available = false;
data_store.evses[0]->evsestatus.set_data(evse_status);
// Set up the expected responses
types::json_rpc_api::ErrorResObj result{RPCDataTypes::ResponseErrorEnum::NoError};
nlohmann::json expected_response = create_json_rpc_response(result, 1);
nlohmann::json expected_error = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}};
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send EVSE.SetChargingAllowed request with valid ID
send_req_and_validate_res(client, evse_set_charging_allowed_req_valid_index, expected_response);
// Check if the EVSE status is updated
ASSERT_TRUE(data_store.evses[0]->evsestatus.get_data().has_value());
ASSERT_TRUE(data_store.evses[0]->evsestatus.get_data().value().charging_allowed);
// Send EVSE.SetChargingAllowed request with valid ID and false
send_req_and_validate_res(client, evse_set_charging_allowed_req_valid_index_false, expected_response);
// Check if the EVSE status is updated
ASSERT_TRUE(data_store.evses[0]->evsestatus.get_data().has_value());
ASSERT_FALSE(data_store.evses[0]->evsestatus.get_data().value().charging_allowed);
// Send EVSE.SetChargingAllowed request with invalid ID
send_req_and_validate_res(client, evse_set_charging_allowed_req_invalid_index, expected_error,
is_key_value_in_json_rpc_result);
}
// Test: Connect to WebSocket server and send EVSE.MeterData request with valid and invalid index
TEST_F(RpcHandlerTest, EvseMeterDataReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up requests
nlohmann::json evse_meter_data_req_valid_index =
create_json_rpc_request("EVSE.GetMeterData", {{"evse_index", 1}}, 1);
nlohmann::json evse_meter_data_req_invalid_index =
create_json_rpc_request("EVSE.GetMeterData", {{"evse_index", 99}}, 1);
// Set up the data store with test data
RPCDataTypes::EVSEInfoObj evse_info;
evse_info.index = 1;
data_store.evses[0]->evseinfo.set_data(evse_info);
// Configure meter data, but do not set it in the data store
RPCDataTypes::MeterDataObj meter_data{};
meter_data.energy_Wh_import.total = 123.45;
meter_data.timestamp = "2025-06-10T09:51:56Z";
// Set up the expected responses
types::json_rpc_api::EVSEGetMeterDataResObj result{{meter_data}, RPCDataTypes::ResponseErrorEnum::NoError};
nlohmann::json expected_response_no_error = create_json_rpc_response(result, 1);
nlohmann::json expected_error_no_data = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable)}};
nlohmann::json expected_error_invalid_index = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}};
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send EVSE.MeterData request with valid ID, but no meter data available
send_req_and_validate_res(client, evse_meter_data_req_valid_index, expected_error_no_data,
is_key_value_in_json_rpc_result);
// Set the meter data in the data store
data_store.evses[0]->meterdata.set_data(meter_data);
// Send EVSE.MeterData request with valid ID and meter data available
send_req_and_validate_res(client, evse_meter_data_req_valid_index, expected_response_no_error);
// Send EVSE.MeterData request with invalid ID
send_req_and_validate_res(client, evse_meter_data_req_invalid_index, expected_error_invalid_index,
is_key_value_in_json_rpc_result);
}
// Test: Connect to WebSocket server and send EVSE.SetACCharging request with valid and invalid index
TEST_F(RpcHandlerTest, EvseSetACChargingReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up requests
nlohmann::json evse_set_ac_charging_req_valid_index = create_json_rpc_request(
"EVSE.SetACCharging",
{{"evse_index", 1}, {"charging_allowed", true}, {"max_current", 12.3}, {"phase_count", 3}}, 1);
// As long as the method is not implemented, we expect an error response that the method is not implemented
nlohmann::json expected_res = create_json_rpc_error_response(-32601, "method not found: EVSE.SetACCharging", 1);
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send EVSE.SetACCharging request with valid ID
client.send(evse_set_ac_charging_req_valid_index.dump());
// Wait for the response
std::string received_data = client.wait_for_data(std::chrono::seconds(1), false);
// Check if the response is valid
nlohmann::json response = nlohmann::json::parse(received_data);
ASSERT_EQ(response, expected_res);
}
// Test: Connect to WebSocket server and send EVSE.SetACChargingCurrent request with valid and invalid index
TEST_F(RpcHandlerTest, EvseSetACChargingCurrentReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up requests
nlohmann::json evse_set_ac_charging_current_req_valid_index =
create_json_rpc_request("EVSE.SetACChargingCurrent", {{"evse_index", 1}, {"max_current", 12.3}}, 1);
nlohmann::json evse_set_ac_charging_current_req_invalid_index =
create_json_rpc_request("EVSE.SetACChargingCurrent", {{"evse_index", 99}, {"max_current", 12.3}}, 1);
nlohmann::json evse_set_ac_charging_current_req_invalid_max_current =
create_json_rpc_request("EVSE.SetACChargingCurrent", {{"evse_index", 1}, {"max_current", 15.0}}, 1);
// Set up the data store with test data
RPCDataTypes::EVSEInfoObj evse_info{};
evse_info.index = 1;
data_store.evses[0]->evseinfo.set_data(evse_info);
RPCDataTypes::EVSEStatusObj evse_status{};
evse_status.charging_allowed = true;
data_store.evses[0]->evsestatus.set_data(evse_status);
data_store.evses[0]->evsestatus.set_ac_charge_param_evse_max_current(12.3);
// Set up the expected responses
types::json_rpc_api::ErrorResObj result{RPCDataTypes::ResponseErrorEnum::NoError};
nlohmann::json expected_response = create_json_rpc_response(result, 1);
nlohmann::json expected_error_invalid_index = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}};
nlohmann::json expected_error_invalid_current = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorValuesNotApplied)}};
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send EVSE.SetACChargingCurrent request with valid ID
send_req_and_validate_res(client, evse_set_ac_charging_current_req_valid_index, expected_response);
// Send EVSE.SetACChargingCurrent request with invalid ID
send_req_and_validate_res(client, evse_set_ac_charging_current_req_invalid_index, expected_error_invalid_index,
is_key_value_in_json_rpc_result);
// Send EVSE.SetACChargingCurrent request with invalid AC charging current
send_req_and_validate_res(client, evse_set_ac_charging_current_req_invalid_max_current,
expected_error_invalid_current, is_key_value_in_json_rpc_result);
}
// Test: Connect to WebSocket server and send EVSE.SetACChargingPhaseCount request with valid and invalid index
TEST_F(RpcHandlerTest, EvseSetACChargingPhaseCountReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up requests
nlohmann::json evse_set_ac_charging_phase_count_req_valid_index =
create_json_rpc_request("EVSE.SetACChargingPhaseCount", {{"evse_index", 1}, {"phase_count", 3}}, 1);
nlohmann::json evse_set_ac_charging_phase_count_req_invalid_index =
create_json_rpc_request("EVSE.SetACChargingPhaseCount", {{"evse_index", 99}, {"phase_count", 3}}, 1);
// Set up the data store with test data
RPCDataTypes::EVSEInfoObj evse_info;
evse_info.index = 1;
data_store.evses[0]->evseinfo.set_data(evse_info);
RPCDataTypes::EVSEStatusObj evse_status;
evse_status.available = false;
evse_status.ac_charge_param.emplace();
evse_status.ac_charge_param->evse_max_current = 12.3;
data_store.evses[0]->evsestatus.set_data(evse_status);
RPCDataTypes::EVSEGetHardwareCapabilitiesResObj hw_cap;
hw_cap.error = RPCDataTypes::ResponseErrorEnum::NoError;
hw_cap.hardware_capabilities.max_current_A_export = 32.0;
hw_cap.hardware_capabilities.max_current_A_import = 16.0;
hw_cap.hardware_capabilities.max_phase_count_export = 3;
hw_cap.hardware_capabilities.max_phase_count_import = 3;
hw_cap.hardware_capabilities.min_current_A_export = 6.0;
hw_cap.hardware_capabilities.min_current_A_import = 6.0;
hw_cap.hardware_capabilities.min_phase_count_export = 1;
hw_cap.hardware_capabilities.min_phase_count_import = 1;
hw_cap.hardware_capabilities.phase_switch_during_charging = true;
data_store.evses[0]->hardwarecapabilities.set_data(hw_cap.hardware_capabilities);
// Set up the expected responses
types::json_rpc_api::ErrorResObj result{RPCDataTypes::ResponseErrorEnum::NoError};
nlohmann::json expected_response = create_json_rpc_response(result, 1);
nlohmann::json expected_error = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}};
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send EVSE.SetACChargingPhaseCount request with valid ID
send_req_and_validate_res(client, evse_set_ac_charging_phase_count_req_valid_index, expected_response);
// Send EVSE.SetACChargingPhaseCount request with invalid ID
send_req_and_validate_res(client, evse_set_ac_charging_phase_count_req_invalid_index, expected_error,
is_key_value_in_json_rpc_result);
}
// Test: Connect to WebSocket server and send EVSE.SetACChargingPhaseCount request with invalid phases and disabled
// phase switching
TEST_F(RpcHandlerTest, EvseSetACChargingPhaseCountReqBadCases) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up requests
nlohmann::json evse_set_ac_charging_phase_count_req_valid_phase_count =
create_json_rpc_request("EVSE.SetACChargingPhaseCount", {{"evse_index", 1}, {"phase_count", 1}}, 1);
nlohmann::json evse_set_ac_charging_phase_count_req_invalid_phase_count =
create_json_rpc_request("EVSE.SetACChargingPhaseCount", {{"evse_index", 1}, {"phase_count", 2}}, 1);
nlohmann::json evse_set_ac_charging_phase_count_req_out_of_range =
create_json_rpc_request("EVSE.SetACChargingPhaseCount", {{"evse_index", 1}, {"phase_count", 3}}, 1);
// Set up the data store with test data
RPCDataTypes::EVSEInfoObj evse_info;
evse_info.index = 1;
data_store.evses[0]->evseinfo.set_data(evse_info);
RPCDataTypes::EVSEStatusObj evse_status;
evse_status.available = false;
evse_status.ac_charge_param.emplace();
evse_status.ac_charge_param->evse_max_current = 12.3;
evse_status.ac_charge_status.emplace();
evse_status.ac_charge_status->evse_active_phase_count = 1;
data_store.evses[0]->evsestatus.set_data(evse_status);
RPCDataTypes::EVSEGetHardwareCapabilitiesResObj hw_cap;
hw_cap.error = RPCDataTypes::ResponseErrorEnum::NoError;
hw_cap.hardware_capabilities.max_current_A_export = 32.0;
hw_cap.hardware_capabilities.max_current_A_import = 16.0;
hw_cap.hardware_capabilities.max_phase_count_export = 1;
hw_cap.hardware_capabilities.max_phase_count_import = 1;
hw_cap.hardware_capabilities.min_current_A_export = 6.0;
hw_cap.hardware_capabilities.min_current_A_import = 6.0;
hw_cap.hardware_capabilities.min_phase_count_export = 1;
hw_cap.hardware_capabilities.min_phase_count_import = 1;
hw_cap.hardware_capabilities.phase_switch_during_charging = false;
data_store.evses[0]->hardwarecapabilities.set_data(hw_cap.hardware_capabilities);
// Set up the expected responses
types::json_rpc_api::ErrorResObj result{RPCDataTypes::ResponseErrorEnum::NoError};
nlohmann::json expected_no_error = create_json_rpc_response(result, 1);
nlohmann::json expected_invalid_param = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidParameter)}};
nlohmann::json expected_error_out_of_range = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorOutOfRange)}};
nlohmann::json expected_error_operation_not_supported = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorOperationNotSupported)}};
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send EVSE.SetACChargingPhaseCount request with phase count. This should not lead to an error, because
// an initialization of the phase count should be still possible.
send_req_and_validate_res(client, evse_set_ac_charging_phase_count_req_valid_phase_count, expected_no_error);
// Try to switch phase count although phase switching is not allowed (phase_switch_during_charging == false)
send_req_and_validate_res(client, evse_set_ac_charging_phase_count_req_invalid_phase_count,
expected_error_operation_not_supported, is_key_value_in_json_rpc_result);
// Send EVSE.SetACChargingPhaseCount request with phase count out of range
// Enable phase switching, because otherwise it returns an ErrorOperationNotSupported error
hw_cap.hardware_capabilities.phase_switch_during_charging = true;
data_store.evses[0]->hardwarecapabilities.set_data(hw_cap.hardware_capabilities);
send_req_and_validate_res(client, evse_set_ac_charging_phase_count_req_out_of_range, expected_error_out_of_range,
is_key_value_in_json_rpc_result);
// Invalid phase count error occurs when phase_count is configured to 2
hw_cap.hardware_capabilities.max_phase_count_export = 3;
data_store.evses[0]->hardwarecapabilities.set_data(hw_cap.hardware_capabilities);
send_req_and_validate_res(client, evse_set_ac_charging_phase_count_req_invalid_phase_count, expected_invalid_param,
is_key_value_in_json_rpc_result);
}
// Test: Connect to WebSocket server and send EVSE.SetDCCharging request with valid and invalid index
TEST_F(RpcHandlerTest, EvseSetDCChargingReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up requests
nlohmann::json evse_set_dc_charging_req_valid_index = create_json_rpc_request(
"EVSE.SetDCCharging", {{"evse_index", 1}, {"charging_allowed", true}, {"max_power", 12.3}}, 1);
// As long as the method is not implemented, we expect an error response that the method is not implemented
nlohmann::json expected_res = create_json_rpc_error_response(-32601, "method not found: EVSE.SetDCCharging", 1);
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send EVSE.SetDCCharging request with valid ID
client.send(evse_set_dc_charging_req_valid_index.dump());
// Wait for the response
std::string received_data = client.wait_for_data(std::chrono::seconds(1), false);
// Check if the response is valid
nlohmann::json response = nlohmann::json::parse(received_data);
ASSERT_EQ(response, expected_res);
}
// Test: Connect to WebSocket server and send EVSE.SetDCChargingPower request with valid and invalid index
TEST_F(RpcHandlerTest, EvseSetDCChargingPowerReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up requests
nlohmann::json evse_set_dc_charging_power_req_valid_index =
create_json_rpc_request("EVSE.SetDCChargingPower", {{"evse_index", 1}, {"max_power", 12.3}}, 1);
nlohmann::json evse_set_dc_charging_power_req_invalid_index =
create_json_rpc_request("EVSE.SetDCChargingPower", {{"evse_index", 99}, {"max_power", 12.3}}, 1);
// Set up the data store with test data
RPCDataTypes::EVSEInfoObj evse_info;
evse_info.index = 1;
data_store.evses[0]->evseinfo.set_data(evse_info);
RPCDataTypes::EVSEStatusObj evse_status;
evse_status.available = false;
evse_status.dc_charge_param.emplace();
data_store.evses[0]->evsestatus.set_data(evse_status);
// Set up the expected responses
types::json_rpc_api::ErrorResObj result{RPCDataTypes::ResponseErrorEnum::NoError};
nlohmann::json expected_response = create_json_rpc_response(result, 1);
nlohmann::json expected_error = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}};
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send EVSE.SetDCChargingPower request with valid ID
send_req_and_validate_res(client, evse_set_dc_charging_power_req_valid_index, expected_response);
// Send EVSE.SetDCChargingPower request with invalid ID
send_req_and_validate_res(client, evse_set_dc_charging_power_req_invalid_index, expected_error,
is_key_value_in_json_rpc_result);
}
// Test: Connect to WebSocket server and send EVSE.EnableConnector request with valid and invalid index
TEST_F(RpcHandlerTest, EvseEnableConnectorReq) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Set up requests
nlohmann::json evse_enable_connector_req_valid_index = create_json_rpc_request(
"EVSE.EnableConnector", {{"evse_index", 1}, {"enable", true}, {"priority", 1}, {"connector_index", 1}}, 1);
nlohmann::json evse_enable_connector_req_invalid_index = create_json_rpc_request(
"EVSE.EnableConnector", {{"evse_index", 99}, {"enable", true}, {"priority", 1}, {"connector_index", 1}}, 1);
nlohmann::json evse_enable_connector_req_invalid_connector_index = create_json_rpc_request(
"EVSE.EnableConnector", {{"evse_index", 1}, {"enable", true}, {"priority", 1}, {"connector_index", 99}}, 1);
// Set up the expected responses
types::json_rpc_api::ErrorResObj result{RPCDataTypes::ResponseErrorEnum::NoError};
nlohmann::json expected_response = create_json_rpc_response(result, 1);
nlohmann::json expected_error = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}};
nlohmann::json expected_error_invalid_connector_index = {
{"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidConnectorIndex)}};
// Set up the data store with test data
RPCDataTypes::EVSEInfoObj evse_info;
evse_info.index = 1;
evse_info.available_connectors.emplace_back();
evse_info.available_connectors[0].index = 1;
evse_info.available_connectors[0].type = types::json_rpc_api::ConnectorTypeEnum::cCCS2;
data_store.evses[0]->evseinfo.set_data(evse_info);
RPCDataTypes::EVSEStatusObj evse_status;
evse_status.available = false;
data_store.evses[0]->evsestatus.set_data(evse_status);
// Send Api.Hello request
client.send_api_hello_req();
client.wait_for_data(std::chrono::seconds(1));
// Send EVSE.EnableConnector request with valid ID
send_req_and_validate_res(client, evse_enable_connector_req_valid_index, expected_response);
// Send EVSE.EnableConnector request with invalid ID
send_req_and_validate_res(client, evse_enable_connector_req_invalid_index, expected_error,
is_key_value_in_json_rpc_result);
// Send EVSE.EnableConnector request with invalid connector ID
send_req_and_validate_res(client, evse_enable_connector_req_invalid_connector_index,
expected_error_invalid_connector_index, is_key_value_in_json_rpc_result);
}
// Test: Connect to WebSocket server and send invalid request
TEST_F(RpcHandlerTest, InvalidRequest) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
// Send Api.Hello request
client.send_api_hello_req();
// Wait for the response
client.wait_for_data(std::chrono::seconds(1));
// Send invalid request
nlohmann::json invalid_request = create_json_rpc_request("API.InvalidMethod", {}, 1);
// Expected response
nlohmann::json expected_response = create_json_rpc_error_response(-32601, "method not found: API.InvalidMethod", 1);
// Send invalid request
client.send(invalid_request.dump());
// Wait for the response
std::string received_data = client.wait_for_data(std::chrono::seconds(1), false);
// Check if the response is not empty
ASSERT_FALSE(received_data.empty());
// Check if the response is valid
nlohmann::json response = nlohmann::json::parse(received_data);
ASSERT_EQ(response, expected_response);
}

View File

@@ -0,0 +1,163 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright chargebyte GmbH and Contributors to EVerest
#include <everest/logging.hpp>
#include <gtest/gtest.h>
#include <thread>
#include "../helpers/WebSocketTestClient.hpp"
#include "../server/WebsocketServer.hpp"
using namespace server;
class WebSocketServerTest : public ::testing::Test {
protected:
std::unique_ptr<WebSocketServer> ws_server;
int test_port = 8080;
void SetUp() override {
ws_server = std::make_unique<WebSocketServer>(false, test_port, "lo");
lws_set_log_level(LLL_ERR | LLL_WARN, NULL);
ws_server->on_client_connected = [this](const TransportInterface::ClientId& client_id,
const server::TransportInterface::Address& address) {
// Handle client connected logic here
std::lock_guard<std::mutex> lock(cv_mutex);
try {
connected_clients.push_back(client_id);
} catch (const std::exception& e) {
EVLOG_error << "Exception occurred while handling client connected: " << e.what();
}
};
ws_server->on_client_disconnected = [this](const TransportInterface::ClientId& client_id) {
// Handle client disconnected logic here
std::lock_guard<std::mutex> lock(cv_mutex);
try {
connected_clients.erase(std::remove(connected_clients.begin(), connected_clients.end(), client_id),
connected_clients.end());
} catch (const std::exception& e) {
EVLOG_error << "Exception occurred while handling client disconnected: " << e.what();
}
};
ws_server->on_data_available = [this](const TransportInterface::ClientId& client_id,
const server::TransportInterface::Data& data) {
// Handle data available logic here
std::lock_guard<std::mutex> lock(cv_mutex);
try {
received_data[client_id] = std::string(data.begin(), data.end());
cv.notify_all();
} catch (const std::exception& e) {
EVLOG_error << "Exception occurred while handling data available: " << e.what();
}
};
ws_server->start_server();
}
std::vector<TransportInterface::ClientId>& get_connected_clients() {
std::lock_guard<std::mutex> lock(cv_mutex);
return connected_clients;
}
// Connected client id's
std::vector<TransportInterface::ClientId> connected_clients;
// Condition variable to wait requests
std::condition_variable cv;
std::mutex cv_mutex;
// Received data with client id
std::unordered_map<TransportInterface::ClientId, std::string> received_data;
void TearDown() override {
ws_server->stop_server();
}
};
// Test: Start and stop WebSocket server
TEST_F(WebSocketServerTest, WebSocketServerStarts) {
ASSERT_TRUE(ws_server->running());
TearDown();
ASSERT_FALSE(ws_server->running());
}
// Test: Connect WebSocket client to server
TEST_F(WebSocketServerTest, ClientCanConnect) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
}
// Test: Connect several WebSocket clients to server
TEST_F(WebSocketServerTest, MultipleClientsCanConnect) {
WebSocketTestClient client1("localhost", test_port);
WebSocketTestClient client2("localhost", test_port);
WebSocketTestClient client3("localhost", test_port);
ASSERT_TRUE(client1.connect());
ASSERT_TRUE(client2.connect());
ASSERT_TRUE(client3.connect());
std::this_thread::sleep_for(std::chrono::milliseconds(10));
ASSERT_TRUE(client1.is_connected());
ASSERT_TRUE(client2.is_connected());
ASSERT_TRUE(client3.is_connected());
ASSERT_TRUE(ws_server->connections_count() == 3);
}
// Test: Client can send data to server
TEST_F(WebSocketServerTest, ClientCanSendAndReceiveData) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
client.send("Hello World!");
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::unique_lock<std::mutex> lock(cv_mutex);
cv.wait_for(lock, std::chrono::seconds(1), [&] { return !received_data.empty(); });
lock.unlock();
ASSERT_EQ(ws_server->connections_count(), 1);
ASSERT_EQ(received_data[(get_connected_clients()[0])], "Hello World!");
}
// Test: Server can send data to client
TEST_F(WebSocketServerTest, ServerCanSendDataToClient) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
std::string message = "Hello from server!";
ws_server->send_data(get_connected_clients()[0], std::vector<uint8_t>(message.begin(), message.end()));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::string received_data = client.wait_for_data(std::chrono::seconds(1), false);
ASSERT_FALSE(received_data.empty());
ASSERT_EQ(received_data, "Hello from server!");
}
// Test: Server kills client connection
TEST_F(WebSocketServerTest, ServerCanKillClientConnection) {
WebSocketTestClient client("localhost", test_port);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100)));
client.send("Hello World!");
std::this_thread::sleep_for(std::chrono::milliseconds(10));
client.wait_for_data(std::chrono::seconds(1));
ASSERT_EQ(ws_server->connections_count(), 1);
ASSERT_EQ(received_data[get_connected_clients()[0]], "Hello World!");
ws_server->kill_client_connection(get_connected_clients()[0], "Test kill");
std::this_thread::sleep_for(std::chrono::milliseconds(100));
ASSERT_EQ(ws_server->connections_count(), 0);
ASSERT_FALSE(client.is_connected());
}

View File

@@ -0,0 +1,72 @@
# JSON-RPC WebSocket Client GUI
A simple Python-based GUI application to connect to the EVerest JSON-RPC API.
Designed for debugging, development, and manual interaction with JSON-RPC services.
---
## 🚀 Features
- Connect via IP and port to a JSON-RPC WebSocket server.
- Automatically sends `API.Hello` after connecting.
- Displays JSON-RPC **requests**, **responses**, and **notifications** in separate consoles.
- Dynamically filters notifications by method name.
- Send custom JSON-RPC method calls with parameters.
- Save, load, and delete custom method calls (persisted to disk).
- All settings are saved and reloaded across sessions.
- UNIX timestamp (with millisecond precision) shown for all received messages.
---
## 🖥️ Requirements
Make sure the following packages are installed (on Debian/Ubuntu-based systems):
```bash
sudo apt update
sudo apt install -y python3 python3-pip python3-tk
```
### 📦 Install dependencies
Create and activate a virtual environment:
```bash
python3 -m venv venv
source venv/bin/activate
```
Install required packages:
```bash
pip install -r requirements.txt
```
---
## ▶️ How to Run
```bash
python3 everest-json-rpc-websocket-client.py
```
---
## 💾 Persistent Files
- `settings.json`: Stores the last used IP and port.
- `saved_calls.json`: Stores saved method calls with their parameters.
---
## 📘 Compatibility Matrix
| GUI Version | JSON-RPC Server API Version | Notes |
|-------------|-----------------------------|--------------------------|
| `1.0.0` | `1.0.0` | Initial stable version |
---
## 📝 License
Apache-2.0

View File

@@ -0,0 +1,296 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright chargebyte GmbH and Contributors to EVerest
import asyncio
import json
import os
import threading
import time
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import websockets
SETTINGS_FILE = "settings.json"
CALLS_FILE = "saved_calls.json"
APP_VERSION = "1.0.0"
class JsonRpcWebSocketClient:
def __init__(self, root):
self.root = root
self.root.title(f"EVerest JSON-RPC WebSocket Client v{APP_VERSION}")
self.ws = None
self.connected = False
self.loop = None
self.connect_button_label = tk.StringVar(value="Connect")
self.notification_filters = set()
self.all_notifications = set()
self.settings = self.load_settings()
self.saved_calls = self.load_calls()
self.setup_ui()
def setup_ui(self):
self.root.columnconfigure(0, weight=1)
# Connection frame
conn_frame = ttk.LabelFrame(self.root, text="Connection")
conn_frame.grid(row=0, column=0, padx=10, pady=5, sticky="ew")
ttk.Label(conn_frame, text="IP:").grid(row=0, column=0, sticky="w")
self.ip_entry = ttk.Entry(conn_frame, width=15)
self.ip_entry.grid(row=0, column=1, padx=5)
self.ip_entry.insert(0, self.settings.get("ip", "127.0.0.1"))
ttk.Label(conn_frame, text="Port:").grid(row=0, column=2, sticky="w")
self.port_entry = ttk.Entry(conn_frame, width=7)
self.port_entry.grid(row=0, column=3, padx=5)
self.port_entry.insert(0, str(self.settings.get("port", 8080)))
self.connect_button = ttk.Button(conn_frame, textvariable=self.connect_button_label,
command=self.connect_or_disconnect)
self.connect_button.grid(row=0, column=4, padx=5)
# Request/Response console
rr_frame = ttk.LabelFrame(self.root, text="Requests / Responses")
rr_frame.grid(row=1, column=0, padx=10, pady=5, sticky="nsew")
rr_frame.columnconfigure(0, weight=1)
rr_frame.rowconfigure(0, weight=1)
self.rr_text = scrolledtext.ScrolledText(rr_frame, height=10)
self.rr_text.grid(row=0, column=0, sticky="nsew")
self.rr_text_menu = tk.Menu(root, tearoff=0)
self.rr_text_menu.add_command(label="Clear content", command=self.clear_rr_text)
self.rr_text.bind("<Button-3>", self.show_rr_text_menu)
# Notification frame with filters
notif_frame = ttk.LabelFrame(self.root, text="Notifications")
notif_frame.grid(row=2, column=0, padx=10, pady=5, sticky="nsew")
notif_frame.columnconfigure(0, weight=1)
notif_frame.rowconfigure(1, weight=1)
self.notification_autoscroll = tk.BooleanVar(value=True)
self.notification_autoscroll_checkbutton = ttk.Checkbutton(notif_frame, text='Autoscroll',
variable=self.notification_autoscroll)
self.notification_autoscroll_checkbutton.grid(row=0, column=0, sticky="w")
self.filter_frame = ttk.Frame(notif_frame)
self.filter_frame.grid(row=1, column=0, sticky="w")
self.notification_text = scrolledtext.ScrolledText(notif_frame, height=10)
self.notification_text.grid(row=2, column=0, sticky="nsew")
self.notification_text_menu = tk.Menu(root, tearoff=0)
self.notification_text_menu.add_command(label="Clear content", command=self.clear_notification_text)
self.notification_text_menu.add_separator()
self.notification_text_menu.add_checkbutton(label="Autoscroll", onvalue=True, offvalue=False,
variable=self.notification_autoscroll)
self.notification_text.bind("<Button-3>", self.show_notification_text_menu)
# Method call frame
call_frame = ttk.LabelFrame(self.root, text="Method Call")
call_frame.grid(row=3, column=0, padx=10, pady=5, sticky="ew")
ttk.Label(call_frame, text="Method:").grid(row=0, column=0, sticky="w")
self.method_entry = ttk.Entry(call_frame)
self.method_entry.grid(row=0, column=1, sticky="ew", padx=5)
ttk.Label(call_frame, text="Params (JSON):").grid(row=0, column=2, sticky="w")
self.params_entry = ttk.Entry(call_frame)
self.params_entry.grid(row=0, column=3, sticky="ew", padx=5)
self.call_button = ttk.Button(call_frame, text="Call", command=self.send_custom_call)
self.call_button.grid(row=0, column=4, padx=5)
self.save_button = ttk.Button(call_frame, text="Save", command=self.save_current_call)
self.save_button.grid(row=0, column=5, padx=5)
ttk.Label(call_frame, text="Saved Calls:").grid(row=1, column=0, sticky="w")
self.call_combobox = ttk.Combobox(call_frame, state="readonly")
self.call_combobox.grid(row=1, column=1, columnspan=2, sticky="ew", padx=5)
self.call_combobox.bind("<<ComboboxSelected>>", self.load_selected_call)
self.delete_button = ttk.Button(call_frame, text="Delete", command=self.delete_selected_call)
self.delete_button.grid(row=1, column=3, padx=5)
call_frame.columnconfigure(1, weight=1)
call_frame.columnconfigure(3, weight=1)
self.refresh_saved_calls()
# Logging functions
def log(self, message):
timestamp = int(time.time() * 1000)
formatted = f"[{timestamp}] {message}\n"
self.rr_text.insert(tk.END, formatted)
self.rr_text.see(tk.END)
def log_notification(self, message):
timestamp = int(time.time() * 1000)
msg_obj = json.loads(message)
method = msg_obj.get("method")
if method:
is_new_method = method not in self.all_notifications
self.all_notifications.add(method)
if is_new_method:
self.add_filter_checkbox(method)
if method not in self.notification_filters:
formatted = f"[{timestamp}] {message}\n"
self.notification_text.insert(tk.END, formatted)
if self.notification_autoscroll.get():
self.notification_text.see(tk.END)
# Add only new checkbox
def add_filter_checkbox(self, method):
var = tk.BooleanVar(value=(method not in self.notification_filters))
cb = ttk.Checkbutton(self.filter_frame, text=method, variable=var)
cb.var = var
cb.method = method
cb.config(command=self.update_filters)
cb.pack(side=tk.LEFT)
# UI utility functions
def clear_notification_text(self):
self.notification_text.delete('1.0', tk.END)
def clear_rr_text(self):
self.rr_text.delete('1.0', tk.END)
def show_notification_text_menu(self, event):
self.notification_text_menu.tk_popup(event.x_root, event.y_root)
def show_rr_text_menu(self, event):
self.rr_text_menu.tk_popup(event.x_root, event.y_root)
def update_filters(self):
self.notification_filters = set()
for cb in self.filter_frame.winfo_children():
if not cb.var.get():
self.notification_filters.add(cb.method)
# Settings & calls persistence
def load_settings(self):
if os.path.exists(SETTINGS_FILE):
with open(SETTINGS_FILE, "r") as f:
return json.load(f)
return {}
def save_settings(self):
self.settings["ip"] = self.ip_entry.get()
self.settings["port"] = int(self.port_entry.get())
with open(SETTINGS_FILE, "w") as f:
json.dump(self.settings, f)
def load_calls(self):
if os.path.exists(CALLS_FILE):
with open(CALLS_FILE, "r") as f:
return json.load(f)
return {}
def save_calls(self):
with open(CALLS_FILE, "w") as f:
json.dump(self.saved_calls, f)
def refresh_saved_calls(self):
self.call_combobox["values"] = list(self.saved_calls.keys())
def load_selected_call(self, event):
selected = self.call_combobox.get()
if selected in self.saved_calls:
call = self.saved_calls[selected]
self.method_entry.delete(0, tk.END)
self.method_entry.insert(0, call["method"])
self.params_entry.delete(0, tk.END)
self.params_entry.insert(0, json.dumps(call.get("params", "")))
def delete_selected_call(self):
selected = self.call_combobox.get()
if selected in self.saved_calls:
del self.saved_calls[selected]
self.save_calls()
self.refresh_saved_calls()
def save_current_call(self):
method = self.method_entry.get()
params = self.params_entry.get()
if not method:
return
try:
parsed_params = json.loads(params) if params else None
except json.JSONDecodeError:
messagebox.showerror("Invalid JSON", "Params field is not valid JSON")
return
self.saved_calls[method] = {"method": method, "params": parsed_params}
self.save_calls()
self.refresh_saved_calls()
# JSON-RPC call
def send_custom_call(self):
if not self.connected or not self.ws:
self.log("Not connected")
return
method = self.method_entry.get()
params = self.params_entry.get()
try:
msg = {"jsonrpc": "2.0", "method": method, "id": int(time.time()*1000)}
if params.strip():
parsed = json.loads(params)
msg["params"] = parsed
if self.loop:
asyncio.run_coroutine_threadsafe(self.ws.send(json.dumps(msg)), self.loop)
self.log(f"Sent: {json.dumps(msg)}")
except json.JSONDecodeError:
self.log("Invalid JSON in params")
# Connection management
def connect_or_disconnect(self):
if self.ws and self.connected:
self.connect_button.state(['disabled'])
if self.loop:
asyncio.run_coroutine_threadsafe(self.ws.close(), self.loop)
self.log("Disconnected")
self.connect_button.state(['!disabled'])
else:
self.connect_button.state(['disabled'])
self.save_settings()
ip = self.ip_entry.get()
port = self.port_entry.get()
uri = f"ws://{ip}:{port}"
self.loop = asyncio.new_event_loop()
threading.Thread(target=self.loop.run_until_complete, args=(self.connect_and_listen(uri),),
daemon=True).start()
self.connect_button.state(['!disabled'])
async def connect_and_listen(self, uri):
try:
async with websockets.connect(uri) as websocket:
self.ws = websocket
self.connected = True
self.connect_button_label.set("Disconnect")
hello = {"jsonrpc": "2.0", "method": "API.Hello", "id": 1}
await websocket.send(json.dumps(hello))
self.log(f"Sent: {json.dumps(hello)}")
async for message in websocket:
try:
msg = json.loads(message)
if "method" in msg and "id" not in msg:
self.log_notification(message)
else:
self.log(message)
except json.JSONDecodeError:
self.log("Invalid JSON received")
except Exception as e:
self.log(f"Connection failed: {str(e)}")
finally:
self.connected = False
self.ws = None
self.connect_button_label.set("Connect")
if __name__ == "__main__":
root = tk.Tk()
app = JsonRpcWebSocketClient(root)
root.mainloop()

View File

@@ -0,0 +1 @@
websockets~=15.0.1

View File

@@ -0,0 +1 @@
{"ChargePoint.GetEVSEInfos": {"method": "ChargePoint.GetEVSEInfos", "params": null}, "ChargePoint.GetActiveErrors": {"method": "ChargePoint.GetActiveErrors", "params": null}, "EVSE.GetHardwareCapabilities": {"method": "EVSE.GetHardwareCapabilities", "params": {"evse_index": 1}}, "EVSE.GetInfo": {"method": "EVSE.GetInfo", "params": {"evse_index": 1}}, "EVSE.GetStatus": {"method": "EVSE.GetStatus", "params": {"evse_index": 1}}, "EVSE.GetMeterData": {"method": "EVSE.GetMeterData", "params": {"evse_index": 1}}, "EVSE.SetChargingAllowed": {"method": "EVSE.SetChargingAllowed", "params": {"evse_index": 1, "charging_allowed": true}}, "EVSE.SetACChargingCurrent": {"method": "EVSE.SetACChargingCurrent", "params": {"evse_index": 1, "max_current": 7.42}}, "EVSE.SetACChargingPhaseCount": {"method": "EVSE.SetACChargingPhaseCount", "params": {"evse_index": 1, "phase_count": 3}}, "EVSE.SetDCChargingPower": {"method": "EVSE.SetDCChargingPower", "params": {"evse_index": 1, "max_power": 10000}}, "EVSE.EnableConnector": {"method": "EVSE.EnableConnector", "params": {"evse_index": 1, "connector_id": 1, "enable": true, "priority": 100}}}

View File

@@ -0,0 +1 @@
{"ip": "127.0.0.1", "port": 8080}

View File

@@ -0,0 +1,17 @@
# RpcApi types
To change the JSON-RPC types, edit `json_rpc_api.yaml`.
To generate and use an updated header in `modules/API/RpcApi/types/json_rpc_api/`:
1. Copy `json_rpc_api.yaml` into the top-level `types/` directory.
2. Create a fresh build directory (or reuse an existing one): `mkdir build-headers`.
3. (Re-)Initialize the build directory to pick up the types file: `cmake -B build-headers`.
4. Run the code generation target: `make -C build-headers generate_types_cpp_everest-core`.
5. The generated header appears at `build-headers/generated/include/generated/types/json_rpc_api.hpp`.
4. Move that header to `modules/API/RpcApi/types/json_rpc_api/`.
5. Remove the copied `json_rpc_api.yaml` (from step 1.) from the top-level `types/` directory.
6. Remove the build directory, if it wasn't reused: `rm -rf build-headers`.
7. If you reused an existing build directory, remove the build folder `build/generated` before your next compile.
8. Reformat the newly created header to conform to style guidelines: `clang-format -i modules/API/RpcApi/types/json_rpc_api/json_rpc_api.hpp`.
9. Commit both the updated YAML and the moved header to git.

View File

@@ -0,0 +1,821 @@
description: >-
JSON RPC API and its types. These types specify the exact definition of
the format of the JSON RPC API's Requests - including Notifications - and
Responses. They also specify which keys are optional, and the complete
range of enums.
This is version 1.0.0 of the JSON RPC API.
Changes to this definition need to be done with care, to take
compatibility into consideration. Bump the version as necessary.
types:
ResponseErrorEnum:
description: Enumeration to differentiate between the various error cases that can occur after a method request
type: string
enum:
- NoError
- ErrorInvalidParameter
- ErrorOutOfRange
- ErrorValuesNotApplied
- ErrorInvalidEVSEIndex
- ErrorInvalidConnectorIndex
- ErrorNoDataAvailable
- ErrorOperationNotSupported
- ErrorUnknownError
ChargeProtocolEnum:
description: Charge protocol
type: string
enum:
- Unknown
- IEC61851
- DIN70121
- ISO15118
- ISO15118_20
EVSEStateEnum:
description: EVSE state
type: string
enum:
- Unknown
- Unplugged
- Disabled
- Preparing
- Reserved
- AuthRequired
- ChargingPausedEV
- ChargingPausedEVSE
- Charging
- AuthTimeout
- Finished
- FinishedEVSE
- FinishedEV
- SwitchingPhases
ConnectorTypeEnum:
description: Enumerator of connector types
type: string
enum:
- cCCS1
- cCCS2
- cG105
- cTesla
- cType1
- cType2
- s309_1P_16A
- s309_1P_32A
- s309_3P_16A
- s309_3P_32A
- sBS1361
- sCEE_7_7
- sType2
- sType3
- Other1PhMax16A
- Other1PhOver16A
- Other3Ph
- Pan
- wInductive
- wResonant
- Undetermined
- Unknown
EnergyTransferModeEnum:
description: >-
Possible energy transfer modes. The modes AC_single_phase_core to DC_unique apply to DIN70121 and ISO15118-2.
The other modes DC to WPT apply to ISO15118-20.
type: string
enum:
- AC_single_phase_core
- AC_two_phase
- AC_three_phase_core
- DC_core
- DC_extended
- DC_combo_core
- DC_unique
- DC
- AC_BPT
- AC_BPT_DER
- AC_DER
- DC_BPT
- DC_ACDP
- DC_ACDP_BPT
- WPT
- MCS
- MCS_BPT
Severity:
description: Severity of an error
type: string
enum:
- High
- Medium
- Low
ImplementationIdentifier:
description: Identifier of an implementation
type: object
required:
- module_id
- implementation_id
properties:
module_id:
type: string
minLength: 2
implementation_id:
type: string
evse_index:
type: integer
connector_index:
type: integer
# data type sub-objects
ChargerInfoObj:
description: General well-known information about the charger
type: object
additionalProperties: false
required:
- vendor
- model
- serial
- firmware_version
properties:
vendor:
description: EVSE vendor
type: string
model:
description: EVSE model
type: string
serial:
description: EVSE serial number
type: string
friendly_name:
description: EVSE friendly name
type: string
manufacturer:
description: EVSE manufacturer
type: string
manufacturer_url:
description: Manufacturer's URL
type: string
model_url:
description: EVSE model's URL
type: string
model_no:
description: EVSE model number
type: string
revision:
description: EVSE model revision
type: string
board_revision:
description: EVSE board revision
type: string
firmware_version:
description: EVSE firmware version
type: string
ErrorObj:
description: Represents an error
type: object
required:
- type
- description
- message
- origin
- timestamp
- uuid
- severity
- state
properties:
type:
type: string
minLength: 2
sub_type:
type: string
description:
type: string
minLength: 2
message:
type: string
minLength: 2
severity:
$ref: /json_rpc_api#/Severity
origin:
$ref: /json_rpc_api#/ImplementationIdentifier
timestamp:
type: string
format: date-time
uuid:
type: string
minLength: 2
additionalProperties: false
ConnectorInfoObj:
description: Static information about a connector of an EVSE
type: object
additionalProperties: false
required:
- index
- type
properties:
index:
description: Unique identifier
type: integer
type:
description: Connector type
type: object
$ref: /json_rpc_api#/ConnectorTypeEnum
description:
description: Description
type: string
ACChargeParametersObj:
description: AC related parameters of a charge point EVSE
type: object
additionalProperties: false
required:
- evse_max_current
- evse_max_phase_count
- evse_maximum_charge_power
- evse_minimum_charge_power
- evse_nominal_frequency
properties:
evse_nominal_voltage:
description: evse_nominal_voltage
type: number
evse_max_current:
description: evse_max_current
type: number
evse_max_phase_count:
description: evse_max_phase_count
type: integer
evse_maximum_charge_power:
description: evse_maximum_charge_power
type: number
evse_minimum_charge_power:
description: evse_minimum_charge_power
type: number
evse_nominal_frequency:
description: evse_nominal_frequency
type: number
evse_maximum_discharge_power:
description: evse_maximum_discharge_power
type: number
evse_minimum_discharge_power:
description: evse_minimum_discharge_power
type: number
DCChargeParametersObj:
description: DC related parameters of a charge point EVSE
type: object
additionalProperties: false
required:
- evse_maximum_charge_current
- evse_maximum_charge_power
- evse_maximum_voltage
- evse_minimum_charge_current
- evse_minimum_charge_power
- evse_minimum_voltage
properties:
evse_maximum_charge_current:
description: evse_maximum_charge_current
type: number
evse_maximum_charge_power:
description: evse_maximum_charge_power
type: number
evse_maximum_voltage:
description: evse_maximum_voltage
type: number
evse_minimum_charge_current:
description: evse_minimum_charge_current
type: number
evse_minimum_charge_power:
description: evse_minimum_charge_power
type: number
evse_minimum_voltage:
description: evse_minimum_voltage
type: number
evse_energy_to_be_delivered:
description: evse_energy_to_be_delivered
type: number
evse_maximum_discharge_current:
description: evse_maximum_discharge_current
type: number
evse_maximum_discharge_power:
description: evse_maximum_discharge_power
type: number
evse_minimum_discharge_current:
description: evse_minimum_discharge_current
type: number
evse_minimum_discharge_power:
description: evse_minimum_discharge_power
type: number
ACChargeStatusObj:
description: AC related parameters during charging of a charge point EVSE
type: object
additionalProperties: false
required:
- evse_active_phase_count
properties:
evse_active_phase_count:
description: evse_active_phase_count
type: integer
DCChargeStatusObj:
description: DC related parameters during charging of a charge point EVSE
type: object
additionalProperties: false
required:
- evse_present_current
- evse_present_voltage
- evse_power_limit_achieved
- evse_current_limit_achieved
- evse_voltage_limit_achieved
properties:
evse_present_current:
description: evse_present_current
type: number
evse_present_voltage:
description: evse_present_voltage
type: number
evse_power_limit_achieved:
description: evse_power_limit_achieved
type: boolean
evse_current_limit_achieved:
description: evse_current_limit_achieved
type: boolean
evse_voltage_limit_achieved:
description: evse_voltage_limit_achieved
type: boolean
DisplayParametersObj:
description: Additional information which can be displayed in a GUI
type: object
additionalProperties: false
properties:
start_soc:
description: start_soc
type: integer
present_soc:
description: present_soc
type: integer
minimum_soc:
description: minimum_soc
type: integer
target_soc:
description: target_soc
type: integer
maximum_soc:
description: maximum_soc
type: integer
remaining_time_to_minimum_soc:
description: remaining_time_to_minimum_soc
type: integer
remaining_time_to_target_soc:
description: remaining_time_to_target_soc
type: integer
remaining_time_to_maximum_soc:
description: remaining_time_to_maximum_soc
type: integer
charging_complete:
description: charging_complete
type: boolean
battery_energy_capacity:
description: battery_energy_capacity
type: number
inlet_hot:
description: inlet_hot
type: boolean
HardwareCapabilitiesObj:
description: Hardware capabilities
type: object
additionalProperties: false
required:
- max_current_A_export
- max_current_A_import
- max_phase_count_export
- max_phase_count_import
- min_current_A_export
- min_current_A_import
- min_phase_count_export
- min_phase_count_import
- phase_switch_during_charging
properties:
max_current_A_export:
type: number
max_current_A_import:
type: number
max_phase_count_export:
type: integer
max_phase_count_import:
type: integer
min_current_A_export:
type: number
min_current_A_import:
type: number
min_phase_count_export:
type: integer
min_phase_count_import:
type: integer
phase_switch_during_charging:
type: boolean
EVSEInfoObj:
description: Information about the EVSE
type: object
required:
- index
- id
- available_connectors
- supported_energy_transfer_modes
properties:
index:
description: Unique index of the EVSE, used for identifying it
type: integer
id:
description: Unique identifier string, as used in V2G communication
type: string
description:
description: Description
type: string
available_connectors:
description: Available connectors
type: array
items:
description: Connector information object
type: object
$ref: /json_rpc_api#/ConnectorInfoObj
supported_energy_transfer_modes:
description: Supported energy transfer modes of the EVSE
type: array
items:
$ref: /json_rpc_api#/EnergyTransferModeEnum
EVSEStatusObj:
description: All information about the current status of a charge point EVSE
type: object
additionalProperties: false
required:
- charged_energy_wh
- discharged_energy_wh
- charging_duration_s
- charging_allowed
- available
- active_connector_index
- error_present
- charge_protocol
- state
properties:
charged_energy_wh:
description: charged_energy_wh
type: number
discharged_energy_wh:
description: discharged_energy_wh
type: number
charging_duration_s:
description: charging_duration_s
type: integer
charging_allowed:
description: charging_allowed
type: boolean
available:
description: available
type: boolean
active_connector_index:
description: active_connector_index
type: integer
error_present:
description: error_present
type: boolean
charge_protocol:
description: charge_protocol
type: object
$ref: /json_rpc_api#/ChargeProtocolEnum
ac_charge_param:
description: ac_charge_param
type: object
$ref: /json_rpc_api#/ACChargeParametersObj
dc_charge_param:
description: dc_charge_param
type: object
$ref: /json_rpc_api#/DCChargeParametersObj
ac_charge_status:
description: ac_charge_status
type: object
$ref: /json_rpc_api#/ACChargeStatusObj
dc_charge_status:
description: dc_charge_status
type: object
$ref: /json_rpc_api#/DCChargeStatusObj
display_parameters:
description: display_parameters
type: object
$ref: /json_rpc_api#/DisplayParametersObj
state:
description: state
type: object
$ref: /json_rpc_api#/EVSEStateEnum
MeterDataObj:
description: Meter data of a charge point EVSE
type: object
additionalProperties: false
required:
- energy_Wh_import
- timestamp
properties:
current_A:
description: Current in Ampere
type: object
additionalProperties: false
properties:
L1:
description: AC L1 value only
type: number
L2:
description: AC L2 value only
type: number
L3:
description: AC L3 value only
type: number
N:
description: AC Neutral value only
type: number
energy_Wh_import:
description: Imported energy in Wh (from grid)
type: object
additionalProperties: false
required:
- total
properties:
L1:
description: AC L1 value only
type: number
L2:
description: AC L2 value only
type: number
L3:
description: AC L3 value only
type: number
total:
description: DC / AC Sum value (which is relevant for billing)
type: number
energy_Wh_export:
description: Exported energy in Wh (to grid)
type: object
additionalProperties: false
required:
- total
properties:
L1:
description: AC L1 value only
type: number
L2:
description: AC L2 value only
type: number
L3:
description: AC L3 value only
type: number
total:
description: DC / AC Sum value (which is relevant for billing)
type: number
frequency_Hz:
description: Grid frequency in Hertz
type: object
additionalProperties: false
required:
- L1
properties:
L1:
description: AC L1 value
type: number
L2:
description: AC L2 value
type: number
L3:
description: AC L3 value
type: number
meter_id:
type: string
serial_number:
type: string
phase_seq_error:
type: boolean
power_W:
description:
Instantaneous power in Watt. Negative values are exported, positive
values imported energy.
type: object
additionalProperties: false
required:
- total
properties:
L1:
description: AC L1 value only
type: number
L2:
description: AC L2 value only
type: number
L3:
description: AC L3 value only
type: number
total:
description: DC / AC Sum value
type: number
timestamp:
description: Timestamp of the meter values, as RFC3339 string
type: string
voltage_V:
description: Voltage in Volts
type: object
additionalProperties: false
properties:
L1:
description: AC L1 value only
type: number
L2:
description: AC L2 value only
type: number
L3:
description: AC L3 value only
type: number
# response types
HelloResObj:
description: Response to API.Hello
type: object
additionalProperties: false
required:
- authentication_required
- api_version
- everest_version
- charger_info
properties:
authentication_required:
description: Whether authentication is required
type: boolean
authenticated:
description: Whether the client is properly authenticated
type: boolean
# permission_scopes:
# type: object
api_version:
description: Version of the JSON RPC API
type: string
everest_version:
description: The version of the running EVerest instance
type: string
charger_info:
description: Charger information
type: object
$ref: /json_rpc_api#/ChargerInfoObj
ChargePointGetEVSEInfosResObj:
description: Response to ChargePoint.GetEVSEInfos
type: object
additionalProperties: false
required:
- infos
- error
properties:
infos:
description: Array of EVSE infos
type: array
items:
description: EVSE infos
type: object
$ref: /json_rpc_api#/EVSEInfoObj
error:
description: Response error
type: object
$ref: /json_rpc_api#/ResponseErrorEnum
ChargePointGetActiveErrorsResObj:
description: Response to ChargePoint.GetActiveErrors
type: object
additionalProperties: false
required:
- active_errors
- error
properties:
active_errors:
description: Array of active charge point errors
type: array
items:
description: EVSE error
type: object
$ref: /json_rpc_api#/ErrorObj
error:
description: Response error
type: object
$ref: /json_rpc_api#/ResponseErrorEnum
EVSEGetInfoResObj:
description: Response to EVSE.GetInfo
type: object
additionalProperties: false
required:
- info
- error
properties:
info:
type: object
$ref: /json_rpc_api#/EVSEInfoObj
error:
description: Response error
type: object
$ref: /json_rpc_api#/ResponseErrorEnum
EVSEGetStatusResObj:
description: Response to EVSE.GetStatus
type: object
additionalProperties: false
required:
- status
- error
properties:
status:
type: object
$ref: /json_rpc_api#/EVSEStatusObj
error:
description: Response error
type: object
$ref: /json_rpc_api#/ResponseErrorEnum
EVSEGetHardwareCapabilitiesResObj:
description: Response to EVSE.GetHardwareCapabilities
type: object
additionalProperties: false
required:
- hardware_capabilities
- error
properties:
hardware_capabilities:
type: object
$ref: /json_rpc_api#/HardwareCapabilitiesObj
error:
description: Response error
type: object
$ref: /json_rpc_api#/ResponseErrorEnum
EVSEGetMeterDataResObj:
description: Response to EVSE.GetMeterData
type: object
additionalProperties: false
required:
- meter_data
- error
properties:
meter_data:
type: object
$ref: /json_rpc_api#/MeterDataObj
error:
description: Response error
type: object
$ref: /json_rpc_api#/ResponseErrorEnum
ErrorResObj:
description: Response which only contains an error type
type: object
additionalProperties: false
required:
- error
properties:
error:
description: Response error
type: object
$ref: /json_rpc_api#/ResponseErrorEnum
# notifications
ChargePointActiveErrorsChangedObj:
description: Notification ChargePoint.ActiveErrorsChanged
type: object
additionalProperties: false
required:
- active_errors
properties:
active_errors:
description: Array of active charge point errors
type: array
items:
description: EVSE error
type: object
$ref: /json_rpc_api#/ErrorObj
EVSEHardwareCapabilitiesChangedObj:
description: Notification EVSE.HardwareCapabilitiesChanged
type: object
additionalProperties: false
required:
- evse_index
- hardware_capabilities
properties:
evse_index:
description: Index of the EVSE
type: integer
hardware_capabilities:
type: object
$ref: /json_rpc_api#/HardwareCapabilitiesObj
EVSEStatusChangedObj:
description: Notification EVSE.StatusChanged
type: object
additionalProperties: false
required:
- evse_index
- evse_status
properties:
evse_index:
description: Index of the EVSE
type: integer
evse_status:
type: object
$ref: /json_rpc_api#/EVSEStatusObj
EVSEMeterDataChangedObj:
description: Notification EVSE.MeterDataChanged
type: object
additionalProperties: false
required:
- evse_index
- meter_data
properties:
evse_index:
description: Index of the EVSE
type: integer
meter_data:
type: object
$ref: /json_rpc_api#/MeterDataObj

File diff suppressed because it is too large Load Diff