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:
76
tools/EVerest-main/modules/API/RpcApi/CMakeLists.txt
Normal file
76
tools/EVerest-main/modules/API/RpcApi/CMakeLists.txt
Normal 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
|
||||
454
tools/EVerest-main/modules/API/RpcApi/RpcApi.cpp
Normal file
454
tools/EVerest-main/modules/API/RpcApi/RpcApi.cpp
Normal 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
|
||||
94
tools/EVerest-main/modules/API/RpcApi/RpcApi.hpp
Normal file
94
tools/EVerest-main/modules/API/RpcApi/RpcApi.hpp
Normal 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
|
||||
383
tools/EVerest-main/modules/API/RpcApi/RpcApiRequestHandler.cpp
Normal file
383
tools/EVerest-main/modules/API/RpcApi/RpcApiRequestHandler.cpp
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
419
tools/EVerest-main/modules/API/RpcApi/data/DataStore.cpp
Normal file
419
tools/EVerest-main/modules/API/RpcApi/data/DataStore.cpp
Normal 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
|
||||
167
tools/EVerest-main/modules/API/RpcApi/data/DataStore.hpp
Normal file
167
tools/EVerest-main/modules/API/RpcApi/data/DataStore.hpp
Normal 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
|
||||
@@ -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
|
||||
196
tools/EVerest-main/modules/API/RpcApi/data/SessionInfo.cpp
Normal file
196
tools/EVerest-main/modules/API/RpcApi/data/SessionInfo.cpp
Normal 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
|
||||
97
tools/EVerest-main/modules/API/RpcApi/data/SessionInfo.hpp
Normal file
97
tools/EVerest-main/modules/API/RpcApi/data/SessionInfo.hpp
Normal 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 |
896
tools/EVerest-main/modules/API/RpcApi/docs/index.rst
Normal file
896
tools/EVerest-main/modules/API/RpcApi/docs/index.rst
Normal 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 request–response 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 token’s 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 EVSE’s of the charge point.
|
||||
|
||||
**Request:**
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{}
|
||||
|
||||
**Response:**
|
||||
Returns an array of type “EVSEInfoObj” of all configured EVSE’s 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%
|
||||
208
tools/EVerest-main/modules/API/RpcApi/helpers/Conversions.cpp
Normal file
208
tools/EVerest-main/modules/API/RpcApi/helpers/Conversions.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
50
tools/EVerest-main/modules/API/RpcApi/manifest.yaml
Normal file
50
tools/EVerest-main/modules/API/RpcApi/manifest.yaml
Normal 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
|
||||
@@ -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
|
||||
362
tools/EVerest-main/modules/API/RpcApi/rpc/RpcHandler.cpp
Normal file
362
tools/EVerest-main/modules/API/RpcApi/rpc/RpcHandler.cpp
Normal 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
|
||||
150
tools/EVerest-main/modules/API/RpcApi/rpc/RpcHandler.hpp
Normal file
150
tools/EVerest-main/modules/API/RpcApi/rpc/RpcHandler.hpp
Normal 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
|
||||
55
tools/EVerest-main/modules/API/RpcApi/rpc/methods/Api.cpp
Normal file
55
tools/EVerest-main/modules/API/RpcApi/rpc/methods/Api.cpp
Normal 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
|
||||
48
tools/EVerest-main/modules/API/RpcApi/rpc/methods/Api.hpp
Normal file
48
tools/EVerest-main/modules/API/RpcApi/rpc/methods/Api.hpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
206
tools/EVerest-main/modules/API/RpcApi/rpc/methods/Evse.cpp
Normal file
206
tools/EVerest-main/modules/API/RpcApi/rpc/methods/Evse.cpp
Normal 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
|
||||
65
tools/EVerest-main/modules/API/RpcApi/rpc/methods/Evse.hpp
Normal file
65
tools/EVerest-main/modules/API/RpcApi/rpc/methods/Evse.hpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
259
tools/EVerest-main/modules/API/RpcApi/server/WebsocketServer.cpp
Normal file
259
tools/EVerest-main/modules/API/RpcApi/server/WebsocketServer.cpp
Normal 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
|
||||
@@ -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
|
||||
78
tools/EVerest-main/modules/API/RpcApi/tests/CMakeLists.txt
Normal file
78
tools/EVerest-main/modules/API/RpcApi/tests/CMakeLists.txt
Normal 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})
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
websockets~=15.0.1
|
||||
@@ -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}}}
|
||||
@@ -0,0 +1 @@
|
||||
{"ip": "127.0.0.1", "port": 8080}
|
||||
17
tools/EVerest-main/modules/API/RpcApi/types/README.md
Normal file
17
tools/EVerest-main/modules/API/RpcApi/types/README.md
Normal 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.
|
||||
821
tools/EVerest-main/modules/API/RpcApi/types/json_rpc_api.yaml
Normal file
821
tools/EVerest-main/modules/API/RpcApi/types/json_rpc_api.yaml
Normal 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
Reference in New Issue
Block a user