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:
763
tools/EVerest-main/modules/API/API/API.cpp
Normal file
763
tools/EVerest-main/modules/API/API/API.cpp
Normal file
@@ -0,0 +1,763 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#include "API.hpp"
|
||||
#include <everest/external_energy_limits/external_energy_limits.hpp>
|
||||
#include <utils/date.hpp>
|
||||
#include <utils/yaml_loader.hpp>
|
||||
|
||||
namespace module {
|
||||
|
||||
static const auto NOTIFICATION_PERIOD = std::chrono::seconds(1);
|
||||
static const std::string API_MODULE_SOURCE = "API_module";
|
||||
|
||||
SessionInfo::SessionInfo() :
|
||||
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 SessionInfo::is_state_charging(const SessionInfo::State current_state) {
|
||||
return current_state == State::AuthRequired || current_state == State::Charging ||
|
||||
current_state == State::ChargingPausedEV || current_state == State::ChargingPausedEVSE;
|
||||
}
|
||||
|
||||
void SessionInfo::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;
|
||||
}
|
||||
|
||||
types::energy::ExternalLimits get_external_limits(const std::string& data, bool is_watts) {
|
||||
const auto limit = std::stof(data);
|
||||
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;
|
||||
|
||||
if (is_watts) {
|
||||
target_entry.limits_to_leaves.total_power_W = {std::fabs(limit), API_MODULE_SOURCE};
|
||||
zero_entry.limits_to_leaves.total_power_W = {0, API_MODULE_SOURCE};
|
||||
} else {
|
||||
target_entry.limits_to_leaves.ac_max_current_A = {std::fabs(limit), API_MODULE_SOURCE};
|
||||
zero_entry.limits_to_leaves.ac_max_current_A = {0, API_MODULE_SOURCE};
|
||||
}
|
||||
|
||||
if (limit > 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_import = std::vector<types::energy::ScheduleReqEntry>(1, zero_entry);
|
||||
external_limits.schedule_export = std::vector<types::energy::ScheduleReqEntry>(1, target_entry);
|
||||
}
|
||||
return external_limits;
|
||||
}
|
||||
|
||||
types::energy::ExternalLimits get_external_limits(int32_t phases, float amps) {
|
||||
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.ac_max_current_A = {0, API_MODULE_SOURCE};
|
||||
|
||||
// check if phases are 1 or 3, otherwise throw an exception
|
||||
const auto is_valid = (phases == 1 || phases == 3);
|
||||
if (is_valid) {
|
||||
target_entry.limits_to_leaves.ac_max_phase_count = {phases, API_MODULE_SOURCE};
|
||||
target_entry.limits_to_leaves.ac_min_phase_count = {phases, API_MODULE_SOURCE};
|
||||
target_entry.limits_to_leaves.ac_max_current_A = {std::fabs(amps), API_MODULE_SOURCE};
|
||||
} else {
|
||||
std::string error_msg = "Invalid phase count " + std::to_string(phases);
|
||||
throw std::out_of_range(error_msg);
|
||||
}
|
||||
|
||||
if (amps > 0) {
|
||||
external_limits.schedule_import = std::vector<types::energy::ScheduleReqEntry>(1, target_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 void remove_error_from_list(std::vector<module::SessionInfo::Error>& list, const std::string& error_type) {
|
||||
list.erase(std::remove_if(list.begin(), list.end(),
|
||||
[error_type](const module::SessionInfo::Error& err) { return err.type == error_type; }),
|
||||
list.end());
|
||||
}
|
||||
|
||||
void SessionInfo::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::PrepareCharging:
|
||||
case Event::SessionStarted:
|
||||
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:
|
||||
case Event::SessionFinished:
|
||||
this->state = State::Unplugged;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string SessionInfo::state_to_string(SessionInfo::State s) {
|
||||
switch (s) {
|
||||
case SessionInfo::State::Unknown:
|
||||
return "Unknown";
|
||||
case SessionInfo::State::Unplugged:
|
||||
return "Unplugged";
|
||||
case SessionInfo::State::Disabled:
|
||||
return "Disabled";
|
||||
case SessionInfo::State::Preparing:
|
||||
return "Preparing";
|
||||
case SessionInfo::State::Reserved:
|
||||
return "Reserved";
|
||||
case SessionInfo::State::AuthRequired:
|
||||
return "AuthRequired";
|
||||
case SessionInfo::State::ChargingPausedEV:
|
||||
return "ChargingPausedEV";
|
||||
case SessionInfo::State::ChargingPausedEVSE:
|
||||
return "ChargingPausedEVSE";
|
||||
case SessionInfo::State::Charging:
|
||||
return "Charging";
|
||||
case SessionInfo::State::Finished:
|
||||
return "Finished";
|
||||
case SessionInfo::State::FinishedEVSE:
|
||||
return "FinishedEVSE";
|
||||
case SessionInfo::State::FinishedEV:
|
||||
return "FinishedEV";
|
||||
case SessionInfo::State::AuthTimeout:
|
||||
return "AuthTimeout";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
void SessionInfo::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 SessionInfo::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 SessionInfo::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 SessionInfo::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 SessionInfo::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 SessionInfo::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 SessionInfo::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 SessionInfo::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 SessionInfo::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;
|
||||
}
|
||||
|
||||
static void to_json(json& j, const SessionInfo::Error& e) {
|
||||
j = json{{"type", e.type}, {"description", e.description}, {"severity", e.severity}};
|
||||
}
|
||||
|
||||
SessionInfo::operator std::string() {
|
||||
std::lock_guard<std::mutex> lock(this->session_info_mutex);
|
||||
|
||||
auto charged_energy_wh = this->end_energy_import_wh - this->start_energy_import_wh;
|
||||
int32_t discharged_energy_wh{0};
|
||||
if (this->start_energy_export_wh_was_set && this->end_energy_export_wh_was_set) {
|
||||
discharged_energy_wh = this->end_energy_export_wh - this->start_energy_export_wh;
|
||||
}
|
||||
auto now = date::utc_clock::now();
|
||||
|
||||
auto charging_duration_s =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(this->end_time_point - this->start_time_point);
|
||||
|
||||
json session_info = json::object({
|
||||
{"state", state_to_string(this->state)},
|
||||
{"permanent_fault", this->permanent_fault},
|
||||
{"charged_energy_wh", charged_energy_wh},
|
||||
{"discharged_energy_wh", discharged_energy_wh},
|
||||
{"latest_total_w", this->latest_total_w},
|
||||
{"charging_duration_s", charging_duration_s.count()},
|
||||
{"datetime", Everest::Date::to_rfc3339(now)},
|
||||
|
||||
});
|
||||
|
||||
json active_disable_enable = json::object({{"source", this->active_enable_disable_source},
|
||||
{"state", this->active_enable_disable_state},
|
||||
{"priority", this->active_enable_disable_priority}});
|
||||
session_info["active_enable_disable_source"] = active_disable_enable;
|
||||
|
||||
if (uk_random_delay_remaining.countdown_s > 0) {
|
||||
json random_delay =
|
||||
json::object({{"remaining_s", uk_random_delay_remaining.countdown_s},
|
||||
{"current_limit_after_delay_A", uk_random_delay_remaining.current_limit_after_delay_A},
|
||||
{"current_limit_during_delay_A", uk_random_delay_remaining.current_limit_during_delay_A},
|
||||
{"start_time", uk_random_delay_remaining.start_time.value_or("")}});
|
||||
session_info["uk_random_delay"] = random_delay;
|
||||
}
|
||||
|
||||
return session_info.dump();
|
||||
}
|
||||
|
||||
void API::init() {
|
||||
// ensure all evse_energy_sink(s) that are connected have an evse id mapping
|
||||
for (const auto& evse_sink : this->r_evse_energy_sink) {
|
||||
if (not evse_sink->get_mapping().has_value()) {
|
||||
EVLOG_critical << "Please configure an evse mapping in your configuration file for the connected "
|
||||
"r_evse_energy_sink with module_id: "
|
||||
<< evse_sink->module_id;
|
||||
throw std::runtime_error("At least one connected evse_energy_sink misses a mapping to an evse.");
|
||||
}
|
||||
}
|
||||
|
||||
this->limit_decimal_places = std::make_unique<LimitDecimalPlaces>(this->config);
|
||||
std::vector<std::string> connectors;
|
||||
std::string var_connectors = this->api_base + "connectors";
|
||||
|
||||
evse_manager_check.set_total(r_evse_manager.size());
|
||||
|
||||
for (const auto& evse : this->r_evse_manager) {
|
||||
|
||||
auto& session_info = this->info.emplace_back(std::make_unique<SessionInfo>());
|
||||
auto& hw_caps = this->hw_capabilities_str.emplace_back("");
|
||||
std::string evse_base = this->api_base + evse->module_id;
|
||||
connectors.push_back(evse->module_id);
|
||||
|
||||
evse->subscribe_ready([this, &evse](bool ready) {
|
||||
if (ready) {
|
||||
this->evse_manager_check.notify_ready(evse->module_id);
|
||||
}
|
||||
});
|
||||
|
||||
// API variables
|
||||
std::string var_base = evse_base + "/var/";
|
||||
|
||||
std::string var_hw_caps = var_base + "hardware_capabilities";
|
||||
evse->subscribe_hw_capabilities(
|
||||
[this, var_hw_caps, &hw_caps](types::evse_board_support::HardwareCapabilities hw_capabilities) {
|
||||
hw_caps = this->limit_decimal_places->limit(hw_capabilities);
|
||||
this->mqtt.publish(var_hw_caps, hw_caps);
|
||||
});
|
||||
|
||||
std::string var_powermeter = var_base + "powermeter";
|
||||
evse->subscribe_powermeter([this, var_powermeter, &session_info](types::powermeter::Powermeter powermeter) {
|
||||
this->mqtt.publish(var_powermeter, this->limit_decimal_places->limit(powermeter));
|
||||
session_info->set_latest_energy_import_wh(powermeter.energy_Wh_import.total);
|
||||
if (powermeter.energy_Wh_export.has_value()) {
|
||||
session_info->set_latest_energy_export_wh(powermeter.energy_Wh_export.value().total);
|
||||
}
|
||||
if (powermeter.power_W.has_value()) {
|
||||
session_info->set_latest_total_w(powermeter.power_W.value().total);
|
||||
}
|
||||
});
|
||||
|
||||
std::string var_limits = var_base + "limits";
|
||||
evse->subscribe_limits([this, var_limits](types::evse_manager::Limits limits) {
|
||||
this->mqtt.publish(var_limits, this->limit_decimal_places->limit(limits));
|
||||
});
|
||||
|
||||
std::string var_telemetry = var_base + "telemetry";
|
||||
evse->subscribe_telemetry([this, var_telemetry](types::evse_board_support::Telemetry telemetry) {
|
||||
this->mqtt.publish(var_telemetry, this->limit_decimal_places->limit(telemetry));
|
||||
});
|
||||
|
||||
std::string var_ev_info = var_base + "ev_info";
|
||||
evse->subscribe_ev_info([this, var_ev_info](types::evse_manager::EVInfo ev_info) {
|
||||
json ev_info_json = ev_info;
|
||||
this->mqtt.publish(var_ev_info, ev_info_json.dump());
|
||||
});
|
||||
|
||||
std::string var_selected_protocol = var_base + "selected_protocol";
|
||||
evse->subscribe_selected_protocol([this, var_selected_protocol](const std::string& selected_protocol) {
|
||||
this->selected_protocol = selected_protocol;
|
||||
});
|
||||
|
||||
evse->subscribe_error(
|
||||
"evse_manager/Inoperative",
|
||||
[this, &session_info](const Everest::error::Error&) { session_info->set_permanent_fault(true); },
|
||||
[this, &session_info](const Everest::error::Error&) { session_info->set_permanent_fault(false); });
|
||||
|
||||
std::string var_datetime = var_base + "datetime";
|
||||
std::string var_session_info = var_base + "session_info";
|
||||
std::string var_logging_path = var_base + "logging_path";
|
||||
this->api_threads.push_back(std::thread(
|
||||
[this, var_datetime, var_session_info, var_hw_caps, var_selected_protocol, &session_info, &hw_caps]() {
|
||||
auto next_tick = std::chrono::steady_clock::now();
|
||||
while (this->running) {
|
||||
std::string datetime_str = Everest::Date::to_rfc3339(date::utc_clock::now());
|
||||
this->mqtt.publish(var_datetime, datetime_str);
|
||||
this->mqtt.publish(var_session_info, *session_info);
|
||||
this->mqtt.publish(var_hw_caps, hw_caps);
|
||||
this->mqtt.publish(var_selected_protocol, this->selected_protocol);
|
||||
|
||||
next_tick += NOTIFICATION_PERIOD;
|
||||
std::this_thread::sleep_until(next_tick);
|
||||
}
|
||||
}));
|
||||
|
||||
evse->subscribe_session_event(
|
||||
[this, var_session_info, var_logging_path, &session_info](types::evse_manager::SessionEvent session_event) {
|
||||
session_info->update_state(session_event);
|
||||
|
||||
if (session_event.source.has_value()) {
|
||||
const auto source = session_event.source.value();
|
||||
session_info->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 (session_event.event == types::evse_manager::SessionEventEnum::SessionStarted) {
|
||||
if (session_event.session_started.has_value()) {
|
||||
auto session_started = session_event.session_started.value();
|
||||
if (session_started.logging_path.has_value()) {
|
||||
this->mqtt.publish(var_logging_path, session_started.logging_path.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (session_event.event == types::evse_manager::SessionEventEnum::TransactionStarted) {
|
||||
if (session_event.transaction_started.has_value()) {
|
||||
auto transaction_started = session_event.transaction_started.value();
|
||||
auto energy_Wh_import = transaction_started.meter_value.energy_Wh_import.total;
|
||||
session_info->set_start_energy_import_wh(energy_Wh_import);
|
||||
|
||||
if (transaction_started.meter_value.energy_Wh_export.has_value()) {
|
||||
auto energy_Wh_export = transaction_started.meter_value.energy_Wh_export.value().total;
|
||||
session_info->set_start_energy_export_wh(energy_Wh_export);
|
||||
} else {
|
||||
session_info->start_energy_export_wh_was_set = false;
|
||||
}
|
||||
}
|
||||
} else if (session_event.event == types::evse_manager::SessionEventEnum::TransactionFinished) {
|
||||
if (session_event.transaction_finished.has_value()) {
|
||||
auto transaction_finished = session_event.transaction_finished.value();
|
||||
auto energy_Wh_import = transaction_finished.meter_value.energy_Wh_import.total;
|
||||
session_info->set_end_energy_import_wh(energy_Wh_import);
|
||||
|
||||
if (transaction_finished.meter_value.energy_Wh_export.has_value()) {
|
||||
auto energy_Wh_export = transaction_finished.meter_value.energy_Wh_export.value().total;
|
||||
session_info->set_end_energy_export_wh(energy_Wh_export);
|
||||
} else {
|
||||
session_info->end_energy_export_wh_was_set = false;
|
||||
}
|
||||
}
|
||||
|
||||
this->mqtt.publish(var_session_info, *session_info);
|
||||
}
|
||||
});
|
||||
|
||||
// API commands
|
||||
std::string cmd_base = evse_base + "/cmd/";
|
||||
|
||||
std::string cmd_enable_disable = cmd_base + "enable_disable";
|
||||
this->mqtt.subscribe(cmd_enable_disable, [this, &evse](const std::string& data) {
|
||||
auto connector_id = 0;
|
||||
types::evse_manager::EnableDisableSource enable_source{types::evse_manager::Enable_source::LocalAPI,
|
||||
types::evse_manager::Enable_state::Enable, 100};
|
||||
|
||||
if (!data.empty()) {
|
||||
try {
|
||||
auto arg = json::parse(data);
|
||||
if (arg.contains("connector_id")) {
|
||||
connector_id = arg.at("connector_id");
|
||||
}
|
||||
if (arg.contains("source")) {
|
||||
enable_source.enable_source = types::evse_manager::string_to_enable_source(arg.at("source"));
|
||||
}
|
||||
if (arg.contains("state")) {
|
||||
enable_source.enable_state = types::evse_manager::string_to_enable_state(arg.at("state"));
|
||||
}
|
||||
if (arg.contains("priority")) {
|
||||
enable_source.enable_priority = arg.at("priority");
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "enable: Cannot parse argument, command ignored: " << e.what();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
EVLOG_error << "enable: No argument specified, ignoring command";
|
||||
return;
|
||||
}
|
||||
this->evse_manager_check.wait_ready();
|
||||
evse->call_enable_disable(connector_id, enable_source);
|
||||
});
|
||||
|
||||
std::string cmd_disable = cmd_base + "disable";
|
||||
this->mqtt.subscribe(cmd_disable, [this, &evse](const std::string& data) {
|
||||
auto connector_id = 0;
|
||||
types::evse_manager::EnableDisableSource enable_source{types::evse_manager::Enable_source::LocalAPI,
|
||||
types::evse_manager::Enable_state::Disable, 100};
|
||||
|
||||
if (!data.empty()) {
|
||||
try {
|
||||
connector_id = std::stoi(data);
|
||||
EVLOG_warning << "disable: Argument is an integer, using deprecated compatibility mode";
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "disable: Cannot parse argument, ignoring command";
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
EVLOG_error << "disable: No argument specified, ignoring command";
|
||||
return;
|
||||
}
|
||||
this->evse_manager_check.wait_ready();
|
||||
evse->call_enable_disable(connector_id, enable_source);
|
||||
});
|
||||
|
||||
std::string cmd_enable = cmd_base + "enable";
|
||||
this->mqtt.subscribe(cmd_enable, [this, &evse](const std::string& data) {
|
||||
auto connector_id = 0;
|
||||
types::evse_manager::EnableDisableSource enable_source{types::evse_manager::Enable_source::LocalAPI,
|
||||
types::evse_manager::Enable_state::Enable, 100};
|
||||
|
||||
if (!data.empty()) {
|
||||
try {
|
||||
connector_id = std::stoi(data);
|
||||
EVLOG_warning << "disable: Argument is an integer, using deprecated compatibility mode";
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "disable: Cannot parse argument, ignoring command";
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
EVLOG_error << "disable: No argument specified, ignoring command";
|
||||
return;
|
||||
}
|
||||
this->evse_manager_check.wait_ready();
|
||||
evse->call_enable_disable(connector_id, enable_source);
|
||||
});
|
||||
|
||||
std::string cmd_pause_charging = cmd_base + "pause_charging";
|
||||
this->mqtt.subscribe(cmd_pause_charging, [this, &evse](const std::string&) {
|
||||
this->evse_manager_check.wait_ready();
|
||||
evse->call_pause_charging(); //
|
||||
});
|
||||
|
||||
std::string cmd_resume_charging = cmd_base + "resume_charging";
|
||||
this->mqtt.subscribe(cmd_resume_charging, [this, &evse](const std::string&) {
|
||||
this->evse_manager_check.wait_ready();
|
||||
evse->call_resume_charging(); //
|
||||
});
|
||||
|
||||
std::string cmd_stop_charging = cmd_base + "stop_charging";
|
||||
this->mqtt.subscribe(cmd_stop_charging, [this, &evse](const std::string&) {
|
||||
this->evse_manager_check.wait_ready();
|
||||
|
||||
types::evse_manager::StopTransactionRequest request;
|
||||
request.reason = types::evse_manager::StopTransactionReason::Local;
|
||||
|
||||
evse->call_stop_transaction(request);
|
||||
});
|
||||
|
||||
std::string cmd_force_unlock = cmd_base + "force_unlock";
|
||||
this->mqtt.subscribe(cmd_force_unlock, [this, &evse](const std::string& data) {
|
||||
int connector_id = 1;
|
||||
if (!data.empty()) {
|
||||
try {
|
||||
connector_id = std::stoi(data);
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "Could not parse connector id for force unlock, using " << connector_id
|
||||
<< ", error: " << e.what();
|
||||
}
|
||||
}
|
||||
// match processing in ChargePointImpl::handleUnlockConnectorRequest
|
||||
// so that OCPP UnlockConnector and everest_api/evse_manager/cmd/force_unlock
|
||||
// perform the same action
|
||||
types::evse_manager::StopTransactionRequest req;
|
||||
req.reason = types::evse_manager::StopTransactionReason::UnlockCommand;
|
||||
this->evse_manager_check.wait_ready();
|
||||
evse->call_stop_transaction(req);
|
||||
evse->call_force_unlock(connector_id);
|
||||
});
|
||||
|
||||
// Check if a uk_random_delay is connected that matches this evse_manager
|
||||
for (const auto& random_delay : this->r_random_delay) {
|
||||
if (random_delay->module_id == evse->module_id) {
|
||||
|
||||
random_delay->subscribe_countdown([&session_info](const types::uk_random_delay::CountDown& s) {
|
||||
session_info->set_uk_random_delay_remaining(s);
|
||||
});
|
||||
|
||||
std::string cmd_uk_random_delay = cmd_base + "uk_random_delay";
|
||||
this->mqtt.subscribe(cmd_uk_random_delay, [&random_delay](const std::string& data) {
|
||||
if (data == "enable") {
|
||||
random_delay->call_enable();
|
||||
} else if (data == "disable") {
|
||||
random_delay->call_disable();
|
||||
} else if (data == "cancel") {
|
||||
random_delay->call_cancel();
|
||||
}
|
||||
});
|
||||
|
||||
std::string uk_random_delay_set_max_duration_s = cmd_base + "uk_random_delay_set_max_duration_s";
|
||||
this->mqtt.subscribe(uk_random_delay_set_max_duration_s, [&random_delay](const std::string& data) {
|
||||
int seconds = 600;
|
||||
try {
|
||||
seconds = std::stoi(data);
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "Could not parse connector duration value for "
|
||||
"uk_random_delay_set_max_duration_s, using default value of "
|
||||
<< seconds << " seconds, error: " << e.what();
|
||||
}
|
||||
random_delay->call_set_duration_s(seconds);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string var_ocpp_connection_status = this->api_base + "ocpp/var/connection_status";
|
||||
std::string var_ocpp_schedule = this->api_base + "ocpp/var/charging_schedules";
|
||||
|
||||
if (this->r_ocpp.size() == 1) {
|
||||
this->r_ocpp.at(0)->subscribe_is_connected([this](bool is_connected) {
|
||||
std::scoped_lock lock(ocpp_data_mutex);
|
||||
if (is_connected) {
|
||||
this->ocpp_connection_status = "connected";
|
||||
} else {
|
||||
this->ocpp_connection_status = "disconnected";
|
||||
}
|
||||
});
|
||||
|
||||
this->r_ocpp.at(0)->subscribe_charging_schedules([this, &var_ocpp_schedule](json schedule) {
|
||||
std::scoped_lock lock(ocpp_data_mutex);
|
||||
this->ocpp_charging_schedule = std::move(schedule);
|
||||
this->ocpp_charging_schedule_updated = true;
|
||||
});
|
||||
}
|
||||
|
||||
std::string var_info = this->api_base + "info/var/info";
|
||||
|
||||
if (this->config.charger_information_file != "") {
|
||||
if (not r_charger_information.empty()) {
|
||||
EVLOG_warning << "The configured charger information file (" << this->config.charger_information_file
|
||||
<< ") is ignored in favor of the charger information interface connection.";
|
||||
} else {
|
||||
auto charger_information_path = std::filesystem::path(this->config.charger_information_file);
|
||||
try {
|
||||
this->charger_information = Everest::load_yaml(charger_information_path);
|
||||
|
||||
} catch (const std::exception& err) {
|
||||
EVLOG_error << "Error parsing charger information file at " << this->config.charger_information_file
|
||||
<< ": " << err.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->api_threads.emplace_back(
|
||||
[this, var_connectors, connectors, var_info, var_ocpp_connection_status, var_ocpp_schedule]() {
|
||||
auto next_tick = std::chrono::steady_clock::now();
|
||||
while (this->running) {
|
||||
json connectors_array = connectors;
|
||||
this->mqtt.publish(var_connectors, connectors_array.dump());
|
||||
if (not this->charger_information.is_null()) {
|
||||
this->mqtt.publish(var_info, this->charger_information.dump());
|
||||
}
|
||||
{
|
||||
std::scoped_lock lock(ocpp_data_mutex);
|
||||
this->mqtt.publish(var_ocpp_connection_status, this->ocpp_connection_status);
|
||||
if (this->ocpp_charging_schedule_updated) {
|
||||
this->ocpp_charging_schedule_updated = false;
|
||||
this->mqtt.publish(var_ocpp_schedule, ocpp_charging_schedule.dump());
|
||||
}
|
||||
}
|
||||
|
||||
next_tick += NOTIFICATION_PERIOD;
|
||||
std::this_thread::sleep_until(next_tick);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void API::ready() {
|
||||
if (not r_charger_information.empty()) {
|
||||
this->charger_information = r_charger_information.at(0)->call_get_charger_information();
|
||||
}
|
||||
|
||||
this->evse_manager_check.wait_ready();
|
||||
// The following API commands require the EVSE managers to be ready
|
||||
|
||||
for (const auto& evse : this->r_evse_manager) {
|
||||
std::string evse_base = this->api_base + evse->module_id;
|
||||
std::string cmd_base = evse_base + "/cmd/";
|
||||
auto evse_id = evse->call_get_evse().id;
|
||||
|
||||
if (external_energy_limits::is_evse_sink_configured(this->r_evse_energy_sink, evse_id)) {
|
||||
auto& evse_energy_sink =
|
||||
external_energy_limits::get_evse_sink_by_evse_id(this->r_evse_energy_sink, evse_id);
|
||||
|
||||
std::string cmd_set_limit = cmd_base + "set_limit_amps";
|
||||
this->mqtt.subscribe(cmd_set_limit, [&evse_energy_sink = evse_energy_sink](const std::string& data) {
|
||||
try {
|
||||
const auto external_limits = get_external_limits(data, false);
|
||||
evse_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.";
|
||||
}
|
||||
});
|
||||
|
||||
std::string cmd_set_limit_watts = cmd_base + "set_limit_watts";
|
||||
this->mqtt.subscribe(cmd_set_limit_watts, [&evse_energy_sink = evse_energy_sink](const std::string& data) {
|
||||
try {
|
||||
const auto external_limits = get_external_limits(data, true);
|
||||
evse_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.";
|
||||
}
|
||||
});
|
||||
|
||||
std::string cmd_set_limit_phases = cmd_base + "set_limit_amps_phases";
|
||||
this->mqtt.subscribe(cmd_set_limit_phases, [&evse_energy_sink = evse_energy_sink](const std::string& data) {
|
||||
int32_t phases{};
|
||||
float amps{};
|
||||
try {
|
||||
auto arg = json::parse(data);
|
||||
if (arg.contains("amps") && arg.contains("phases")) {
|
||||
amps = arg.at("amps");
|
||||
phases = arg.at("phases");
|
||||
} else {
|
||||
EVLOG_error << "Invalid limit: Missing amps or phases.";
|
||||
return;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "set_limit_amps_phases: Cannot parse argument, command ignored: " << e.what();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const auto external_limits = get_external_limits(phases, amps);
|
||||
evse_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.";
|
||||
} catch (const std::out_of_range& e) {
|
||||
EVLOG_warning << "Invalid limit: Out of range "
|
||||
<< ", error: " << e.what();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
EVLOG_warning << "No evse energy sink configured for evse_id: " << evse_id
|
||||
<< ". API module does therefore not allow control of amps or power limits for this EVSE";
|
||||
}
|
||||
}
|
||||
|
||||
std::string var_active_errors = this->api_base + "errors/var/active_errors";
|
||||
this->api_threads.emplace_back([this, var_active_errors]() {
|
||||
auto next_tick = std::chrono::steady_clock::now();
|
||||
while (this->running) {
|
||||
if (not r_error_history.empty()) {
|
||||
// request active errors
|
||||
types::error_history::FilterArguments filter;
|
||||
filter.state_filter = types::error_history::State::Active;
|
||||
auto active_errors = r_error_history.at(0)->call_get_errors(filter);
|
||||
json errors_json = json(active_errors);
|
||||
|
||||
// publish
|
||||
this->mqtt.publish(var_active_errors, errors_json.dump());
|
||||
}
|
||||
next_tick += NOTIFICATION_PERIOD;
|
||||
std::this_thread::sleep_until(next_tick);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
225
tools/EVerest-main/modules/API/API/API.hpp
Normal file
225
tools/EVerest-main/modules/API/API/API.hpp
Normal file
@@ -0,0 +1,225 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef API_HPP
|
||||
#define 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/error_history/Interface.hpp>
|
||||
#include <generated/interfaces/evse_manager/Interface.hpp>
|
||||
#include <generated/interfaces/external_energy_limits/Interface.hpp>
|
||||
#include <generated/interfaces/ocpp/Interface.hpp>
|
||||
#include <generated/interfaces/uk_random_delay/Interface.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
// insert your custom include headers here
|
||||
#include <condition_variable>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
|
||||
#include <date/date.h>
|
||||
#include <date/tz.h>
|
||||
|
||||
#include "StartupMonitor.hpp"
|
||||
#include "limit_decimal_places.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
class LimitDecimalPlaces;
|
||||
|
||||
class SessionInfo {
|
||||
public:
|
||||
SessionInfo();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// \brief Converts this struct into a serialized json object
|
||||
operator std::string();
|
||||
|
||||
private:
|
||||
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;
|
||||
|
||||
static bool is_state_charging(SessionInfo::State current_state);
|
||||
|
||||
std::string state_to_string(State s);
|
||||
|
||||
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 module
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {
|
||||
std::string charger_information_file;
|
||||
int powermeter_energy_import_decimal_places;
|
||||
int powermeter_energy_export_decimal_places;
|
||||
int powermeter_power_decimal_places;
|
||||
int powermeter_voltage_decimal_places;
|
||||
int powermeter_VAR_decimal_places;
|
||||
int powermeter_current_decimal_places;
|
||||
int powermeter_frequency_decimal_places;
|
||||
int hw_caps_max_current_export_decimal_places;
|
||||
int hw_caps_max_current_import_decimal_places;
|
||||
int hw_caps_min_current_export_decimal_places;
|
||||
int hw_caps_min_current_import_decimal_places;
|
||||
int hw_caps_max_plug_temperature_C_decimal_places;
|
||||
int limits_max_current_decimal_places;
|
||||
int telemetry_evse_temperature_C_decimal_places;
|
||||
int telemetry_fan_rpm_decimal_places;
|
||||
int telemetry_supply_voltage_12V_decimal_places;
|
||||
int telemetry_supply_voltage_minus_12V_decimal_places;
|
||||
int telemetry_plug_temperature_C_decimal_places;
|
||||
double powermeter_energy_import_round_to;
|
||||
double powermeter_energy_export_round_to;
|
||||
double powermeter_power_round_to;
|
||||
double powermeter_voltage_round_to;
|
||||
double powermeter_VAR_round_to;
|
||||
double powermeter_current_round_to;
|
||||
double powermeter_frequency_round_to;
|
||||
double hw_caps_max_current_export_round_to;
|
||||
double hw_caps_max_current_import_round_to;
|
||||
double hw_caps_min_current_export_round_to;
|
||||
double hw_caps_min_current_import_round_to;
|
||||
double hw_caps_max_plug_temperature_C_round_to;
|
||||
double limits_max_current_round_to;
|
||||
double telemetry_evse_temperature_C_round_to;
|
||||
double telemetry_fan_rpm_round_to;
|
||||
double telemetry_supply_voltage_12V_round_to;
|
||||
double telemetry_supply_voltage_minus_12V_round_to;
|
||||
double telemetry_plug_temperature_C_round_to;
|
||||
};
|
||||
|
||||
class API : public Everest::ModuleBase {
|
||||
public:
|
||||
API() = delete;
|
||||
API(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider,
|
||||
std::vector<std::unique_ptr<charger_informationIntf>> r_charger_information,
|
||||
std::vector<std::unique_ptr<evse_managerIntf>> r_evse_manager, std::vector<std::unique_ptr<ocppIntf>> r_ocpp,
|
||||
std::vector<std::unique_ptr<uk_random_delayIntf>> r_random_delay,
|
||||
std::vector<std::unique_ptr<error_historyIntf>> r_error_history,
|
||||
std::vector<std::unique_ptr<external_energy_limitsIntf>> r_evse_energy_sink, Conf& config) :
|
||||
ModuleBase(info),
|
||||
mqtt(mqtt_provider),
|
||||
r_charger_information(std::move(r_charger_information)),
|
||||
r_evse_manager(std::move(r_evse_manager)),
|
||||
r_ocpp(std::move(r_ocpp)),
|
||||
r_random_delay(std::move(r_random_delay)),
|
||||
r_error_history(std::move(r_error_history)),
|
||||
r_evse_energy_sink(std::move(r_evse_energy_sink)),
|
||||
config(config){};
|
||||
|
||||
Everest::MqttProvider& mqtt;
|
||||
const std::vector<std::unique_ptr<charger_informationIntf>> r_charger_information;
|
||||
const std::vector<std::unique_ptr<evse_managerIntf>> r_evse_manager;
|
||||
const std::vector<std::unique_ptr<ocppIntf>> r_ocpp;
|
||||
const std::vector<std::unique_ptr<uk_random_delayIntf>> r_random_delay;
|
||||
const std::vector<std::unique_ptr<error_historyIntf>> r_error_history;
|
||||
const std::vector<std::unique_ptr<external_energy_limitsIntf>> r_evse_energy_sink;
|
||||
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
|
||||
std::vector<std::thread> api_threads;
|
||||
bool running = true;
|
||||
|
||||
StartupMonitor evse_manager_check;
|
||||
|
||||
std::list<std::unique_ptr<SessionInfo>> info;
|
||||
std::list<std::string> hw_capabilities_str;
|
||||
std::string selected_protocol;
|
||||
json charger_information;
|
||||
std::unique_ptr<LimitDecimalPlaces> limit_decimal_places;
|
||||
|
||||
std::mutex ocpp_data_mutex;
|
||||
json ocpp_charging_schedule;
|
||||
bool ocpp_charging_schedule_updated = false;
|
||||
std::string ocpp_connection_status = "unknown";
|
||||
|
||||
const std::string api_base = "everest_api/";
|
||||
// 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 // API_HPP
|
||||
32
tools/EVerest-main/modules/API/API/BUILD.bazel
Normal file
32
tools/EVerest-main/modules/API/API/BUILD.bazel
Normal file
@@ -0,0 +1,32 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_test")
|
||||
load("//modules:module.bzl", "cc_everest_module")
|
||||
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_TEST_INCOMPATIBLE")
|
||||
|
||||
cc_everest_module(
|
||||
name = "API",
|
||||
srcs = glob(
|
||||
[
|
||||
"*.cpp",
|
||||
"*.hpp",
|
||||
],
|
||||
),
|
||||
deps = [
|
||||
"@everest-core//lib:external_energy_limits",
|
||||
"@rapidyaml",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "API_test",
|
||||
target_compatible_with = CROSS_TEST_INCOMPATIBLE,
|
||||
srcs = [
|
||||
"StartupMonitor.cpp",
|
||||
"StartupMonitor.hpp",
|
||||
"tests/StartupMonitor_test.cpp",
|
||||
],
|
||||
includes = ["."],
|
||||
deps = [
|
||||
"//lib/everest/log:liblog",
|
||||
"@googletest//:gtest_main",
|
||||
],
|
||||
)
|
||||
30
tools/EVerest-main/modules/API/API/CMakeLists.txt
Normal file
30
tools/EVerest-main/modules/API/API/CMakeLists.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
#
|
||||
# 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_link_libraries(${MODULE_NAME}
|
||||
PRIVATE
|
||||
ryml::ryml
|
||||
everest::external_energy_limits
|
||||
everest::yaml
|
||||
)
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"limit_decimal_places.cpp"
|
||||
"StartupMonitor.cpp"
|
||||
)
|
||||
# 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
|
||||
287
tools/EVerest-main/modules/API/API/README.md
Normal file
287
tools/EVerest-main/modules/API/API/README.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# API module documentation
|
||||
This module is responsible for providing a simple MQTT based API to EVerest internals
|
||||
|
||||
## Periodically published variables for each connected EvseManager
|
||||
This module periodically publishes the following variables for each connected EvseManager.
|
||||
|
||||
### everest_api/connectors
|
||||
This variable is published every second and contains an array of the connectors for which the api is available:
|
||||
```
|
||||
["evse_manager"]
|
||||
```
|
||||
|
||||
The following documentation assumes that the only connector available is called "evse_manager".
|
||||
|
||||
### everest_api/evse_manager/var/datetime
|
||||
This variable is published every second and contains a string representation of the current UTC datetime in RFC3339 format:
|
||||
```
|
||||
2022-10-11T16:18:57.746Z
|
||||
```
|
||||
|
||||
### everest_api/evse_manager/var/hardware_capabilities
|
||||
This variable is published every second and contains the hardware capabilities in the following format:
|
||||
```json
|
||||
{
|
||||
"max_current_A_export":16.0,
|
||||
"max_current_A_import":32.0,
|
||||
"max_phase_count_export":3,
|
||||
"max_phase_count_import":3,
|
||||
"min_current_A_export":0.0,
|
||||
"min_current_A_import":6.0,
|
||||
"min_phase_count_export":1,
|
||||
"min_phase_count_import":1,
|
||||
"supports_changing_phases_during_charging":true
|
||||
}
|
||||
```
|
||||
|
||||
### everest_api/evse_manager/var/session_info
|
||||
This variable is published every second and contains a json object with information relating to the current charging session in the following format:
|
||||
```json
|
||||
{
|
||||
"charged_energy_wh": 0,
|
||||
"charging_duration_s": 84,
|
||||
"datetime": "2022-10-11T16:48:35.747Z",
|
||||
"discharged_energy_wh": 0,
|
||||
"latest_total_w": 0.0,
|
||||
"permanent_fault": false,
|
||||
"state": "Unplugged",
|
||||
"active_enable_disable_source": {
|
||||
"source": "Unspecified",
|
||||
"state": "Enable",
|
||||
"priority": 5000
|
||||
},
|
||||
"uk_random_delay": {
|
||||
"remaining_s": 34,
|
||||
"current_limit_after_delay_A": 16.0,
|
||||
"current_limit_during_delay_A": 0.0,
|
||||
"start_time": "2024-02-28T14:11:11.129Z"
|
||||
},
|
||||
"last_enable_disable_source": "Unspecified"
|
||||
}
|
||||
```
|
||||
|
||||
- **charged_energy_wh** contains the charged energy in Wh
|
||||
- **charging_duration_s** contains the duration of the current charging session in seconds
|
||||
- **datetime** contains a string representation of the current UTC datetime in RFC3339 format
|
||||
- **discharged_energy_wh** contains the energy fed into the power grid by the EV in Wh
|
||||
- **latest_total_w** contains the latest total power reading over all phases in Watt
|
||||
- **uk_random_delay_remaining_s** Remaining time of a currently active random delay according to UK smart charging regulations. Not set if no delay is active.
|
||||
- **state** contains the current state of the charging session, from a list of the following possible states:
|
||||
- Unplugged
|
||||
- Disabled
|
||||
- Preparing
|
||||
- Reserved
|
||||
- AuthRequired
|
||||
- Charging
|
||||
- ChargingPausedEV
|
||||
- ChargingPausedEVSE
|
||||
- Finished
|
||||
- FinishedEV
|
||||
- FinishedEVSE
|
||||
- AuthTimeout
|
||||
|
||||
### everest_api/evse_manager/var/limits
|
||||
This variable is published every second and contains a json object with information
|
||||
relating to the current limits of this EVSE.
|
||||
```json
|
||||
{
|
||||
"max_current": 16.0,
|
||||
"nr_of_phases_available": 1,
|
||||
"uuid": "evse_manager"
|
||||
}
|
||||
```
|
||||
|
||||
### everest_api/evse_manager/var/telemetry
|
||||
This variable is published every second and contains telemetry of the EVSE.
|
||||
```json
|
||||
{
|
||||
"fan_rpm": 0.0,
|
||||
"rcd_current": 0.0991784930229187,
|
||||
"relais_on": false,
|
||||
"supply_voltage_12V": 11.950915336608887,
|
||||
"supply_voltage_minus_12V": -11.94166374206543,
|
||||
"temperature": 30.729248046875
|
||||
}
|
||||
```
|
||||
|
||||
### everest_api/evse_manager/var/powermeter
|
||||
This variable is published every second and contains powermeter information
|
||||
of the EVSE.
|
||||
```json
|
||||
{
|
||||
"current_A": {
|
||||
"L1": 16.113445281982422,
|
||||
"L2": 16.113445281982422,
|
||||
"L3": 16.113445281982422,
|
||||
"N": 0.20141807198524475
|
||||
},
|
||||
"energy_Wh_import": {
|
||||
"L1": 1537.3179931640625,
|
||||
"L2": 1537.3179931640625,
|
||||
"L3": 1537.3179931640625,
|
||||
"total": 4611.9541015625
|
||||
},
|
||||
"frequency_Hz": {
|
||||
"L1": 50.03734588623047,
|
||||
"L2": 50.03734588623047,
|
||||
"L3": 50.03734588623047
|
||||
},
|
||||
"meter_id": "YETI_POWERMETER",
|
||||
"phase_seq_error": false,
|
||||
"power_W": {
|
||||
"L1": 3602.54833984375,
|
||||
"L2": 3602.54833984375,
|
||||
"L3": 3602.54833984375,
|
||||
"total": 10807.64453125
|
||||
},
|
||||
"timestamp": 1665509120.0,
|
||||
"voltage_V": {
|
||||
"L1": 223.5740509033203,
|
||||
"L2": 223.5740509033203,
|
||||
"L3": 223.5740509033203
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Periodically published variables for OCPP
|
||||
|
||||
### everest_api/ocpp/var/connection_status
|
||||
This variable is published every second and contains the connection
|
||||
status of the OCPP module.
|
||||
If the OCPP module has not yet published its "is_connected" status or
|
||||
no OCPP module is configured "unknown" is published. Otherwise "connected"
|
||||
or "disconnected" are published.
|
||||
|
||||
|
||||
## Commands and variables published in response
|
||||
### everest_api/evse_manager/cmd/enable_disable
|
||||
Command to enable or disable a connector on the EVSE. The payload should be
|
||||
the following json:
|
||||
|
||||
```json
|
||||
{
|
||||
"connector_id": 0,
|
||||
"source": "LocalAPI",
|
||||
"state": "Enable",
|
||||
"priority": 42
|
||||
}
|
||||
```
|
||||
connector_id is a positive integer identifying the connector that should be
|
||||
enabled. If the connector_id is 0 the whole EVSE is enabled.
|
||||
|
||||
The source is an enum of the following source types :
|
||||
|
||||
- Unspecified
|
||||
- LocalAPI
|
||||
- LocalKeyLock
|
||||
- ServiceTechnician
|
||||
- RemoteKeyLock
|
||||
- MobileApp
|
||||
- FirmwareUpdate
|
||||
- CSMS
|
||||
|
||||
The state can be either "enable", "disable", or "unassigned".
|
||||
|
||||
"enable" and "disable" enforce the state to be enable/disable, while unassigned means
|
||||
that the source does not care about the state and other sources may decide.
|
||||
|
||||
Each call to this command will update an internal table that looks like this:
|
||||
|
||||
| Source | State | Priority |
|
||||
| ------------ | ---------- | -------- |
|
||||
| Unspecified | unassigned | 10000 |
|
||||
| LocalAPI | disable | 42 |
|
||||
| LocalKeyLock | enable | 0 |
|
||||
|
||||
Evaluation will be done based on priorities. 0 is the highest priority,
|
||||
10000 the lowest, so in this example the connector will be enabled regardless
|
||||
of what other sources say.
|
||||
Imagine LocalKeyLock sends a "unassigned, prio 0", the table will then look like this:
|
||||
|
||||
| Source | State | Priority |
|
||||
| ------------ | ---------- | -------- |
|
||||
| Unspecified | unassigned | 10000 |
|
||||
| LocalAPI | disable | 42 |
|
||||
| LocalKeyLock | unassigned | 0 |
|
||||
|
||||
So now the connector will be disabled, because the second highest priority (42) sets it to disabled.
|
||||
|
||||
If all sources are unassigned, the connector is enabled.
|
||||
If two sources have the same priority, "disabled" has priority over "enabled".
|
||||
|
||||
### everest_api/evse_manager/cmd/enable
|
||||
Legacy command to enable a connector on the EVSE kept for compatibility reasons.
|
||||
They payload should be a positive integer identifying the connector that should be enabled.
|
||||
If the payload is 0 the whole EVSE is enabled.
|
||||
It will actually call the following command on everest_api/evse_manager/cmd/enable_enable:
|
||||
```json
|
||||
{
|
||||
"connector_id": 1,
|
||||
"source": "LocalAPI",
|
||||
"state": "Enable",
|
||||
"priority": 100
|
||||
}
|
||||
```
|
||||
|
||||
### everest_api/evse_manager/cmd/disable
|
||||
Legacy command to enable a connector on the EVSE kept for compatibility reasons.
|
||||
Command to disable a connector on the EVSE. They payload should be a positive integer
|
||||
identifying the connector that should be disabled. If the payload is 0 the whole EVSE is disabled.
|
||||
It will actually call the following command on everest_api/evse_manager/cmd/enable_disable:
|
||||
```json
|
||||
{
|
||||
"connector_id": 1,
|
||||
"source": "LocalAPI",
|
||||
"state": "Disable",
|
||||
"priority": 100
|
||||
}
|
||||
```
|
||||
|
||||
### everest_api/evse_manager/cmd/pause_charging
|
||||
If any arbitrary payload is published to this topic charging will be paused by the EVSE.
|
||||
|
||||
### everest_api/evse_manager/cmd/resume_charging
|
||||
If any arbitrary payload is published to this topic charging will be resumed by the EVSE.
|
||||
|
||||
### everest_api/evse_manager/cmd/stop_charging
|
||||
If any arbitrary payload is published to this topic charging will be stopped by the EVSE.
|
||||
|
||||
### everest_api/evse_manager/cmd/set_limit_amps
|
||||
Command to set an amps limit for this EVSE that will be considered within the EnergyManager. This does not automatically imply that this limit will be set by the EVSE because the energymanagement might consider limitations from other sources, too. The payload can be a positive or negative number.
|
||||
|
||||
📌 **Note:** You have to configure one evse_energy_sink connection per EVSE within the configuration file in order to use this topic!
|
||||
|
||||
### everest_api/evse_manager/cmd/set_limit_watts
|
||||
Command to set a watt limit for this EVSE that will be considered within the EnergyManager. This does not automatically imply that this limit will be set by the EVSE because the energymanagement might consider limitations from other sources, too. The payload can be a positive or negative number.
|
||||
|
||||
📌 **Note:** You have to configure one evse_energy_sink connection per EVSE within the configuration file in order to use this topic!
|
||||
|
||||
### everest_api/evse_manager/cmd/set_limit_amps_phases
|
||||
Command to set a current (amps) and a phase limit for this EVSE, which will be considered by the energy
|
||||
management. The payload should be in the following json format:
|
||||
```json
|
||||
{
|
||||
"amps": 8.0,
|
||||
"phases": 3
|
||||
}
|
||||
```
|
||||
Setting these limits does not automatically imply that they will be set by the EVSE because the
|
||||
energy management might consider limitations from other sources, too. The "amps" value can be a
|
||||
positive or negative number. The "phases" value must be either 1 or 3.
|
||||
Please consider that switching between AC single-phase (1ph) and three-phase (3ph) charging does only
|
||||
work if 1ph/3ph switching is activated in the EVerest configuration. For more information please look
|
||||
in the EVerest documentation.
|
||||
|
||||
📌 **Note:** You have to configure one evse_energy_sink connection per EVSE within the configuration file in order to use this topic!
|
||||
|
||||
### everest_api/evse_manager/cmd/force_unlock
|
||||
Command to force unlock a connector on the EVSE. The payload should be a positive integer identifying the connector that should be unlocked. If the payload is empty or cannot be converted to an integer connector 1 is assumed.
|
||||
|
||||
### everest_api/evse_manager/cmd/uk_random_delay
|
||||
Command to control the UK Smart Charging random delay feature. The payload can be the following enum: "enable" and "disable" to enable/disable the feature entirely or "cancel" to cancel an ongoing delay.
|
||||
|
||||
### everest_api/evse_manager/cmd/uk_random_delay_set_max_duration_s
|
||||
Command to set the UK Smart Charging random delay maximum duration. Payload is an integer in seconds.
|
||||
|
||||
### everest_api/errors/var/active_errors
|
||||
Publishes an array of all active errors of the charging station
|
||||
76
tools/EVerest-main/modules/API/API/StartupMonitor.cpp
Normal file
76
tools/EVerest-main/modules/API/API/StartupMonitor.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "StartupMonitor.hpp"
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace module {
|
||||
|
||||
bool StartupMonitor::check_ready() {
|
||||
bool result{false};
|
||||
if (ready_set) {
|
||||
result = ready_set->size() >= n_managers;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool StartupMonitor::set_total(std::uint8_t total) {
|
||||
bool result{true};
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (!ready_set) {
|
||||
n_managers = total;
|
||||
if (total == 0) {
|
||||
managers_ready = true;
|
||||
} else {
|
||||
managers_ready = false;
|
||||
ready_set = std::make_unique<ready_t>();
|
||||
}
|
||||
} else {
|
||||
// already set
|
||||
EVLOG_error << "Invalid attempt to set number of EVSE managers";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
if (total == 0) {
|
||||
cv.notify_all();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void StartupMonitor::wait_ready() {
|
||||
std::unique_lock lock(mutex);
|
||||
cv.wait(lock, [this] { return this->managers_ready; });
|
||||
}
|
||||
|
||||
bool StartupMonitor::notify_ready(const std::string& evse_manager_id) {
|
||||
bool result{true};
|
||||
bool notify{false};
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (ready_set) {
|
||||
ready_set->insert(evse_manager_id);
|
||||
notify = StartupMonitor::check_ready();
|
||||
if (notify) {
|
||||
managers_ready = true;
|
||||
n_managers = 0;
|
||||
ready_set->clear(); // reclaim memory
|
||||
}
|
||||
} else {
|
||||
result = false;
|
||||
if (managers_ready) {
|
||||
EVLOG_warning << "EVSE manager ready after complete";
|
||||
} else {
|
||||
EVLOG_error << "EVSE manager ready before total number set";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (notify) {
|
||||
cv.notify_all();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
68
tools/EVerest-main/modules/API/API/StartupMonitor.hpp
Normal file
68
tools/EVerest-main/modules/API/API/StartupMonitor.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef STARTUPMONITOR_HPP
|
||||
#define STARTUPMONITOR_HPP
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
namespace module {
|
||||
|
||||
/**
|
||||
* \brief collect ready responses from all EVSE managers
|
||||
*
|
||||
* Provides a mechanism for API code to wait for all EVSE managers to be ready.
|
||||
* Every EVSE manager is expected to set a `ready` variable to true. This class
|
||||
* collects the IDs of EVSE managers to check that the expected number are
|
||||
* ready before allowing API calls to proceed.
|
||||
*
|
||||
* \note an EVSE manager is not expected to set `ready` more than once, however
|
||||
* this class manages this so that the `ready` is only counted once.
|
||||
*/
|
||||
class StartupMonitor {
|
||||
private:
|
||||
using ready_t = std::set<std::string>;
|
||||
|
||||
std::condition_variable cv;
|
||||
std::mutex mutex;
|
||||
|
||||
protected:
|
||||
std::unique_ptr<ready_t> ready_set; //!< set of received ready responses
|
||||
std::uint16_t n_managers{0}; //!< total number of EVSE managers
|
||||
bool managers_ready{false}; //!< all EVSE managers are ready
|
||||
|
||||
/**
|
||||
* \brief check whether all ready responses have been received
|
||||
* \returns true when the ready set contains at least n_managers responses
|
||||
*/
|
||||
bool check_ready();
|
||||
|
||||
public:
|
||||
/**
|
||||
* \brief set the total number of EVSE managers
|
||||
* \param[in] total the number of EVSE managers
|
||||
* \returns false if the total has already been set
|
||||
*/
|
||||
bool set_total(std::uint8_t total);
|
||||
|
||||
/**
|
||||
* \brief wait for all EVSE managers to be ready
|
||||
*/
|
||||
void wait_ready();
|
||||
|
||||
/**
|
||||
* \brief notify that a specific EVSE manager is ready
|
||||
* \param[in] evse_manager_id the ID of the EVSE manager
|
||||
* \returns false if the total has not been set
|
||||
* \note notify_ready() may be called multiple times with the same evse_manager_id
|
||||
*/
|
||||
bool notify_ready(const std::string& evse_manager_id);
|
||||
};
|
||||
|
||||
} // namespace module
|
||||
|
||||
#endif // STARTUPMONITOR_HPP
|
||||
5
tools/EVerest-main/modules/API/API/charger_info.yaml
Normal file
5
tools/EVerest-main/modules/API/API/charger_info.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
model_name: "BelayBox"
|
||||
pcb_serial_number: "0123"
|
||||
charger_serial_number: "0123"
|
||||
firmware_version: "v0.1.2"
|
||||
hardware_version: "v0.1.2"
|
||||
313
tools/EVerest-main/modules/API/API/limit_decimal_places.cpp
Normal file
313
tools/EVerest-main/modules/API/API/limit_decimal_places.cpp
Normal file
@@ -0,0 +1,313 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include "limit_decimal_places.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <c4/format.hpp>
|
||||
#include <ryml.hpp>
|
||||
#include <ryml_std.hpp>
|
||||
|
||||
namespace module {
|
||||
|
||||
std::string LimitDecimalPlaces::limit(const types::powermeter::Powermeter& powermeter) {
|
||||
ryml::Tree tree;
|
||||
ryml::NodeRef root = tree.rootref();
|
||||
root |= ryml::MAP;
|
||||
|
||||
// add informative power meter entries
|
||||
root["timestamp"] << powermeter.timestamp;
|
||||
|
||||
if (powermeter.meter_id.has_value()) {
|
||||
root["meter_id"] << powermeter.meter_id.value();
|
||||
}
|
||||
|
||||
if (powermeter.phase_seq_error.has_value()) {
|
||||
root["phase_seq_error"] << ryml::fmt::boolalpha(powermeter.phase_seq_error.value());
|
||||
}
|
||||
|
||||
// limit decimal places
|
||||
|
||||
// energy_Wh_import always exists
|
||||
root["energy_Wh_import"] |= ryml::MAP;
|
||||
root["energy_Wh_import"]["total"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(powermeter.energy_Wh_import.total, this->config.powermeter_energy_import_round_to),
|
||||
this->config.powermeter_energy_import_decimal_places);
|
||||
if (powermeter.energy_Wh_import.L1.has_value()) {
|
||||
root["energy_Wh_import"]["L1"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(powermeter.energy_Wh_import.L1.value(),
|
||||
this->config.powermeter_energy_import_round_to),
|
||||
this->config.powermeter_energy_import_decimal_places);
|
||||
}
|
||||
if (powermeter.energy_Wh_import.L2.has_value()) {
|
||||
root["energy_Wh_import"]["L2"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(powermeter.energy_Wh_import.L2.value(),
|
||||
this->config.powermeter_energy_import_round_to),
|
||||
this->config.powermeter_energy_import_decimal_places);
|
||||
}
|
||||
if (powermeter.energy_Wh_import.L3.has_value()) {
|
||||
root["energy_Wh_import"]["L3"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(powermeter.energy_Wh_import.L3.value(),
|
||||
this->config.powermeter_energy_import_round_to),
|
||||
this->config.powermeter_energy_import_decimal_places);
|
||||
}
|
||||
|
||||
// everything else in the power meter is optional
|
||||
if (powermeter.energy_Wh_export.has_value()) {
|
||||
auto& energy_Wh_export = powermeter.energy_Wh_export.value();
|
||||
root["energy_Wh_export"] |= ryml::MAP;
|
||||
root["energy_Wh_export"]["total"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(energy_Wh_export.total, this->config.powermeter_energy_export_round_to),
|
||||
this->config.powermeter_energy_export_decimal_places);
|
||||
if (energy_Wh_export.L1.has_value()) {
|
||||
root["energy_Wh_export"]["L1"]
|
||||
<< ryml::fmt::real(this->round_to_nearest_step(energy_Wh_export.L1.value(),
|
||||
this->config.powermeter_energy_export_round_to),
|
||||
this->config.powermeter_energy_export_decimal_places);
|
||||
}
|
||||
if (energy_Wh_export.L2.has_value()) {
|
||||
root["energy_Wh_export"]["L2"]
|
||||
<< ryml::fmt::real(this->round_to_nearest_step(energy_Wh_export.L2.value(),
|
||||
this->config.powermeter_energy_export_round_to),
|
||||
this->config.powermeter_energy_export_decimal_places);
|
||||
}
|
||||
if (energy_Wh_export.L3.has_value()) {
|
||||
root["energy_Wh_export"]["L3"]
|
||||
<< ryml::fmt::real(this->round_to_nearest_step(energy_Wh_export.L3.value(),
|
||||
this->config.powermeter_energy_export_round_to),
|
||||
this->config.powermeter_energy_export_decimal_places);
|
||||
}
|
||||
}
|
||||
|
||||
if (powermeter.power_W.has_value()) {
|
||||
auto& power_W = powermeter.power_W.value();
|
||||
root["power_W"] |= ryml::MAP;
|
||||
root["power_W"]["total"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(power_W.total, this->config.powermeter_power_round_to),
|
||||
this->config.powermeter_power_decimal_places);
|
||||
if (power_W.L1.has_value()) {
|
||||
root["power_W"]["L1"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(power_W.L1.value(), this->config.powermeter_power_round_to),
|
||||
this->config.powermeter_power_decimal_places);
|
||||
}
|
||||
if (power_W.L2.has_value()) {
|
||||
root["power_W"]["L2"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(power_W.L2.value(), this->config.powermeter_power_round_to),
|
||||
this->config.powermeter_power_decimal_places);
|
||||
}
|
||||
if (power_W.L3.has_value()) {
|
||||
root["power_W"]["L3"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(power_W.L3.value(), this->config.powermeter_power_round_to),
|
||||
this->config.powermeter_power_decimal_places);
|
||||
}
|
||||
}
|
||||
|
||||
if (powermeter.voltage_V.has_value()) {
|
||||
auto& voltage_V = powermeter.voltage_V.value();
|
||||
root["voltage_V"] |= ryml::MAP;
|
||||
if (voltage_V.DC.has_value()) {
|
||||
root["voltage_V"]["DC"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(voltage_V.DC.value(), this->config.powermeter_voltage_round_to),
|
||||
this->config.powermeter_voltage_decimal_places);
|
||||
}
|
||||
if (voltage_V.L1.has_value()) {
|
||||
root["voltage_V"]["L1"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(voltage_V.L1.value(), this->config.powermeter_voltage_round_to),
|
||||
this->config.powermeter_voltage_decimal_places);
|
||||
}
|
||||
if (voltage_V.L2.has_value()) {
|
||||
root["voltage_V"]["L2"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(voltage_V.L2.value(), this->config.powermeter_voltage_round_to),
|
||||
this->config.powermeter_voltage_decimal_places);
|
||||
}
|
||||
if (voltage_V.L3.has_value()) {
|
||||
root["voltage_V"]["L3"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(voltage_V.L3.value(), this->config.powermeter_voltage_round_to),
|
||||
this->config.powermeter_voltage_decimal_places);
|
||||
}
|
||||
}
|
||||
|
||||
if (powermeter.VAR.has_value()) {
|
||||
auto& VAR = powermeter.VAR.value();
|
||||
root["VAR"] |= ryml::MAP;
|
||||
root["VAR"]["total"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(VAR.total, this->config.powermeter_VAR_round_to),
|
||||
this->config.powermeter_VAR_decimal_places);
|
||||
if (VAR.L1.has_value()) {
|
||||
root["VAR"]["L1"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(VAR.L1.value(), this->config.powermeter_VAR_round_to),
|
||||
this->config.powermeter_VAR_decimal_places);
|
||||
}
|
||||
if (VAR.L2.has_value()) {
|
||||
root["VAR"]["L2"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(VAR.L2.value(), this->config.powermeter_VAR_round_to),
|
||||
this->config.powermeter_VAR_decimal_places);
|
||||
}
|
||||
if (VAR.L3.has_value()) {
|
||||
root["VAR"]["L3"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(VAR.L3.value(), this->config.powermeter_VAR_round_to),
|
||||
this->config.powermeter_VAR_decimal_places);
|
||||
}
|
||||
}
|
||||
|
||||
if (powermeter.current_A.has_value()) {
|
||||
auto& current_A = powermeter.current_A.value();
|
||||
root["current_A"] |= ryml::MAP;
|
||||
if (current_A.DC.has_value()) {
|
||||
root["current_A"]["DC"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(current_A.DC.value(), this->config.powermeter_current_round_to),
|
||||
this->config.powermeter_current_decimal_places);
|
||||
}
|
||||
if (current_A.L1.has_value()) {
|
||||
root["current_A"]["L1"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(current_A.L1.value(), this->config.powermeter_current_round_to),
|
||||
this->config.powermeter_current_decimal_places);
|
||||
}
|
||||
if (current_A.L2.has_value()) {
|
||||
root["current_A"]["L2"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(current_A.L2.value(), this->config.powermeter_current_round_to),
|
||||
this->config.powermeter_current_decimal_places);
|
||||
}
|
||||
if (current_A.L3.has_value()) {
|
||||
root["current_A"]["L3"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(current_A.L3.value(), this->config.powermeter_current_round_to),
|
||||
this->config.powermeter_current_decimal_places);
|
||||
}
|
||||
|
||||
if (current_A.N.has_value()) {
|
||||
root["current_A"]["N"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(current_A.N.value(), this->config.powermeter_current_round_to),
|
||||
this->config.powermeter_current_decimal_places);
|
||||
}
|
||||
}
|
||||
|
||||
if (powermeter.frequency_Hz.has_value()) {
|
||||
auto& frequency_Hz = powermeter.frequency_Hz.value();
|
||||
root["frequency_Hz"] |= ryml::MAP;
|
||||
root["frequency_Hz"]["L1"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(frequency_Hz.L1, this->config.powermeter_frequency_round_to),
|
||||
this->config.powermeter_frequency_decimal_places);
|
||||
|
||||
if (frequency_Hz.L2.has_value()) {
|
||||
root["frequency_Hz"]["L2"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(frequency_Hz.L2.value(), this->config.powermeter_frequency_round_to),
|
||||
this->config.powermeter_frequency_decimal_places);
|
||||
}
|
||||
if (frequency_Hz.L3.has_value()) {
|
||||
root["frequency_Hz"]["L3"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(frequency_Hz.L3.value(), this->config.powermeter_frequency_round_to),
|
||||
this->config.powermeter_frequency_decimal_places);
|
||||
}
|
||||
}
|
||||
|
||||
std::stringstream power_meter_stream;
|
||||
power_meter_stream << ryml::as_json(tree);
|
||||
return power_meter_stream.str();
|
||||
}
|
||||
|
||||
std::string LimitDecimalPlaces::limit(const types::evse_board_support::HardwareCapabilities& hw_capabilities) {
|
||||
ryml::Tree tree;
|
||||
ryml::NodeRef root = tree.rootref();
|
||||
root |= ryml::MAP;
|
||||
|
||||
// add informative hardware capabilities entries
|
||||
root["max_phase_count_import"] << hw_capabilities.max_phase_count_import;
|
||||
root["min_phase_count_import"] << hw_capabilities.min_phase_count_import;
|
||||
root["max_phase_count_export"] << hw_capabilities.max_phase_count_export;
|
||||
root["min_phase_count_export"] << hw_capabilities.min_phase_count_export;
|
||||
root["supports_changing_phases_during_charging"]
|
||||
<< ryml::fmt::boolalpha(hw_capabilities.supports_changing_phases_during_charging);
|
||||
|
||||
// limit decimal places
|
||||
|
||||
root["max_current_A_import"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(hw_capabilities.max_current_A_import,
|
||||
this->config.hw_caps_max_current_import_round_to),
|
||||
this->config.hw_caps_max_current_import_decimal_places);
|
||||
root["min_current_A_import"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(hw_capabilities.min_current_A_import,
|
||||
this->config.hw_caps_min_current_import_round_to),
|
||||
this->config.hw_caps_max_current_import_decimal_places);
|
||||
root["max_current_A_export"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(hw_capabilities.max_current_A_export,
|
||||
this->config.hw_caps_max_current_export_round_to),
|
||||
this->config.hw_caps_max_current_import_decimal_places);
|
||||
root["min_current_A_export"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(hw_capabilities.min_current_A_export,
|
||||
this->config.hw_caps_min_current_export_round_to),
|
||||
this->config.hw_caps_min_current_export_decimal_places);
|
||||
if (hw_capabilities.max_plug_temperature_C.has_value()) {
|
||||
root["max_plug_temperature_C"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(hw_capabilities.max_plug_temperature_C.value(),
|
||||
this->config.hw_caps_max_plug_temperature_C_round_to),
|
||||
this->config.hw_caps_max_plug_temperature_C_decimal_places);
|
||||
}
|
||||
|
||||
root["connector_type"] << types::evse_board_support::connector_type_to_string(hw_capabilities.connector_type);
|
||||
|
||||
std::stringstream hardware_capabilities_stream;
|
||||
hardware_capabilities_stream << ryml::as_json(tree);
|
||||
return hardware_capabilities_stream.str();
|
||||
}
|
||||
|
||||
std::string LimitDecimalPlaces::limit(const types::evse_manager::Limits& limits) {
|
||||
ryml::Tree tree;
|
||||
ryml::NodeRef root = tree.rootref();
|
||||
root |= ryml::MAP;
|
||||
|
||||
// add informative limits entries
|
||||
if (limits.uuid.has_value()) {
|
||||
root["uuid"] << limits.uuid.value();
|
||||
}
|
||||
root["nr_of_phases_available"] << limits.nr_of_phases_available;
|
||||
|
||||
// limit decimal places
|
||||
root["max_current"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(limits.max_current, this->config.limits_max_current_round_to),
|
||||
this->config.limits_max_current_decimal_places);
|
||||
|
||||
std::stringstream limits_stream;
|
||||
limits_stream << ryml::as_json(tree);
|
||||
return limits_stream.str();
|
||||
}
|
||||
|
||||
std::string LimitDecimalPlaces::limit(const types::evse_board_support::Telemetry& telemetry) {
|
||||
ryml::Tree tree;
|
||||
ryml::NodeRef root = tree.rootref();
|
||||
root |= ryml::MAP;
|
||||
|
||||
root["phase_seq_error"] << ryml::fmt::boolalpha(telemetry.relais_on);
|
||||
|
||||
// limit decimal places
|
||||
root["temperature"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(telemetry.evse_temperature_C, this->config.telemetry_evse_temperature_C_round_to),
|
||||
this->config.telemetry_evse_temperature_C_decimal_places);
|
||||
root["fan_rpm"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(telemetry.fan_rpm, this->config.telemetry_fan_rpm_round_to),
|
||||
this->config.telemetry_fan_rpm_decimal_places);
|
||||
root["supply_voltage_12V"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(telemetry.supply_voltage_12V, this->config.telemetry_supply_voltage_12V_round_to),
|
||||
this->config.telemetry_supply_voltage_12V_decimal_places);
|
||||
root["supply_voltage_minus_12V"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(telemetry.supply_voltage_minus_12V,
|
||||
this->config.telemetry_supply_voltage_minus_12V_round_to),
|
||||
this->config.telemetry_supply_voltage_minus_12V_decimal_places);
|
||||
if (telemetry.plug_temperature_C.has_value()) {
|
||||
root["plug_temperature_C"] << ryml::fmt::real(
|
||||
this->round_to_nearest_step(telemetry.plug_temperature_C.value(),
|
||||
this->config.telemetry_plug_temperature_C_round_to),
|
||||
this->config.telemetry_plug_temperature_C_decimal_places);
|
||||
}
|
||||
std::stringstream telemetry_stream;
|
||||
telemetry_stream << ryml::as_json(tree);
|
||||
return telemetry_stream.str();
|
||||
}
|
||||
|
||||
double LimitDecimalPlaces::round_to_nearest_step(double value, double step) {
|
||||
if (step <= 0) {
|
||||
return value;
|
||||
}
|
||||
return std::round(value / step) * step;
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
29
tools/EVerest-main/modules/API/API/limit_decimal_places.hpp
Normal file
29
tools/EVerest-main/modules/API/API/limit_decimal_places.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef LIMIT_DECIMAL_PLACES_HPP
|
||||
#define LIMIT_DECIMAL_PLACES_HPP
|
||||
|
||||
#include <generated/interfaces/evse_manager/Interface.hpp>
|
||||
|
||||
#include "API.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf;
|
||||
|
||||
class LimitDecimalPlaces {
|
||||
public:
|
||||
LimitDecimalPlaces(const Conf& config) : config(config){};
|
||||
std::string limit(const types::powermeter::Powermeter& powermeter);
|
||||
std::string limit(const types::evse_board_support::HardwareCapabilities& hw_capabilities);
|
||||
std::string limit(const types::evse_manager::Limits& limits);
|
||||
std::string limit(const types::evse_board_support::Telemetry& telemetry);
|
||||
double round_to_nearest_step(double value, double step);
|
||||
|
||||
private:
|
||||
const Conf& config;
|
||||
};
|
||||
|
||||
} // namespace module
|
||||
|
||||
#endif // LIMIT_DECIMAL_PLACES_HPP
|
||||
203
tools/EVerest-main/modules/API/API/manifest.yaml
Normal file
203
tools/EVerest-main/modules/API/API/manifest.yaml
Normal file
@@ -0,0 +1,203 @@
|
||||
description: >-
|
||||
The EVerest API module, exposing some internal functionality on an external
|
||||
MQTT connection.
|
||||
config:
|
||||
charger_information_file:
|
||||
description: >-
|
||||
Path to a file containing information about the charger like its serial number.
|
||||
The content of this file is not used, when the optional dependency `charger_information`
|
||||
is used.
|
||||
type: string
|
||||
default: ""
|
||||
powermeter_energy_import_decimal_places:
|
||||
description: Maximum number of decimal places for import energy in the power meter
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
powermeter_energy_export_decimal_places:
|
||||
description: Maximum number of decimal places for export energy in the power meter
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
powermeter_power_decimal_places:
|
||||
description: Maximum number of decimal places for power in the power meter
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
powermeter_voltage_decimal_places:
|
||||
description: Maximum number of decimal places for voltage in the power meter
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
powermeter_VAR_decimal_places:
|
||||
description: Maximum number of decimal places for VAR in the power meter
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
powermeter_current_decimal_places:
|
||||
description: Maximum number of decimal places for current in the power meter
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
powermeter_frequency_decimal_places:
|
||||
description: Maximum number of decimal places for frequency in the power meter
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
hw_caps_max_current_export_decimal_places:
|
||||
description: Maximum number of decimal places for maximum export current in the hardware capabilities
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
hw_caps_max_current_import_decimal_places:
|
||||
description: Maximum number of decimal places for maximum import current in the hardware capabilities
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
hw_caps_min_current_export_decimal_places:
|
||||
description: Maximum number of decimal places for minimum export current in the hardware capabilities
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
hw_caps_min_current_import_decimal_places:
|
||||
description: Maximum number of decimal places for minimum import current in the hardware capabilities
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
hw_caps_max_plug_temperature_C_decimal_places:
|
||||
description: Maximum number of decimal places for max_plug_temperature_C in the hardware capabilities
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
limits_max_current_decimal_places:
|
||||
description: Maximum number of decimal places for maximum current in the limits
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
telemetry_evse_temperature_C_decimal_places:
|
||||
description: Maximum number of decimal places for evse_temperature_C in telemetry
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
telemetry_fan_rpm_decimal_places:
|
||||
description: Maximum number of decimal places for fan RPM in telemetry
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
telemetry_supply_voltage_12V_decimal_places:
|
||||
description: Maximum number of decimal places for supply voltage 12V in telemetry
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
telemetry_supply_voltage_minus_12V_decimal_places:
|
||||
description: Maximum number of decimal places for supply voltage -12V in telemetry
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
telemetry_plug_temperature_C_decimal_places:
|
||||
description: Maximum number of decimal places for RCD current in telemetry
|
||||
type: integer
|
||||
default: 2
|
||||
minimum: 0
|
||||
powermeter_energy_import_round_to:
|
||||
description: Round import energy to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
powermeter_energy_export_round_to:
|
||||
description: Round export energy to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
powermeter_power_round_to:
|
||||
description: Round power to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
powermeter_voltage_round_to:
|
||||
description: Round voltage to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
powermeter_VAR_round_to:
|
||||
description: Round VAR to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
powermeter_current_round_to:
|
||||
description: Round current to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
powermeter_frequency_round_to:
|
||||
description: Round frequency to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
hw_caps_max_current_export_round_to:
|
||||
description: Round maximum export current in hardware limits to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
hw_caps_max_current_import_round_to:
|
||||
description: Round maximum import current in hardware limits to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
hw_caps_min_current_export_round_to:
|
||||
description: Round minimum export current in hardware limits to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
hw_caps_min_current_import_round_to:
|
||||
description: Round minimum import current in hardware limits to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
hw_caps_max_plug_temperature_C_round_to:
|
||||
description: Round max_plug_temperature_C in hardware limits to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
limits_max_current_round_to:
|
||||
description: Round maximum current in limits to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
telemetry_evse_temperature_C_round_to:
|
||||
description: Round evse_temperature_C in telemetry to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
telemetry_fan_rpm_round_to:
|
||||
description: Round fan RPM in telemetry to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
telemetry_supply_voltage_12V_round_to:
|
||||
description: Round supply voltage 12V in telemetry to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
telemetry_supply_voltage_minus_12V_round_to:
|
||||
description: Round supply voltage -12V in telemetry to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
telemetry_plug_temperature_C_round_to:
|
||||
description: Round plug_temperature_C in telemetry to the nearest step. Ignored if value is 0
|
||||
type: number
|
||||
default: 0
|
||||
requires:
|
||||
charger_information:
|
||||
interface: charger_information
|
||||
min_connections: 0
|
||||
max_connections: 1
|
||||
evse_manager:
|
||||
interface: evse_manager
|
||||
min_connections: 1
|
||||
max_connections: 128
|
||||
ocpp:
|
||||
interface: ocpp
|
||||
min_connections: 0
|
||||
max_connections: 1
|
||||
random_delay:
|
||||
interface: uk_random_delay
|
||||
min_connections: 0
|
||||
max_connections: 128
|
||||
error_history:
|
||||
interface: error_history
|
||||
min_connections: 0
|
||||
max_connections: 1
|
||||
evse_energy_sink:
|
||||
interface: external_energy_limits
|
||||
min_connections: 0
|
||||
max_connections: 128
|
||||
enable_external_mqtt: true
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Kai-Uwe Hermann
|
||||
23
tools/EVerest-main/modules/API/API/tests/CMakeLists.txt
Normal file
23
tools/EVerest-main/modules/API/API/tests/CMakeLists.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
set(TEST_TARGET_NAME ${PROJECT_NAME}_API_tests)
|
||||
|
||||
set(TESTS_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/tests/include")
|
||||
|
||||
add_executable(${TEST_TARGET_NAME})
|
||||
|
||||
add_dependencies(${TEST_TARGET_NAME} ${MODULE_NAME})
|
||||
|
||||
target_include_directories(${TEST_TARGET_NAME} PRIVATE
|
||||
. .. ${TESTS_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_sources(${TEST_TARGET_NAME} PRIVATE
|
||||
StartupMonitor_test.cpp
|
||||
../StartupMonitor.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(${TEST_TARGET_NAME} PRIVATE
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME})
|
||||
ev_register_test_target(${TEST_TARGET_NAME})
|
||||
124
tools/EVerest-main/modules/API/API/tests/StartupMonitor_test.cpp
Normal file
124
tools/EVerest-main/modules/API/API/tests/StartupMonitor_test.cpp
Normal file
@@ -0,0 +1,124 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "StartupMonitor.hpp"
|
||||
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
using namespace module;
|
||||
|
||||
struct StartupMonitorTest : public StartupMonitor {
|
||||
[[nodiscard]] constexpr bool startup_complete() const {
|
||||
return managers_ready;
|
||||
}
|
||||
[[nodiscard]] constexpr std::uint8_t total() const {
|
||||
return n_managers;
|
||||
}
|
||||
[[nodiscard]] inline std::uint8_t startup_count() const {
|
||||
return (ready_set) ? ready_set->size() : 0;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(StartupMonitor, init) {
|
||||
StartupMonitorTest startup;
|
||||
EXPECT_FALSE(startup.startup_complete());
|
||||
EXPECT_EQ(startup.startup_count(), 0);
|
||||
EXPECT_EQ(startup.total(), 0);
|
||||
|
||||
bool woken{false};
|
||||
std::thread thread([&startup, &woken]() {
|
||||
startup.wait_ready();
|
||||
woken = true;
|
||||
});
|
||||
|
||||
EXPECT_FALSE(woken);
|
||||
|
||||
EXPECT_TRUE(startup.set_total(1));
|
||||
EXPECT_EQ(startup.total(), 1);
|
||||
EXPECT_FALSE(woken);
|
||||
EXPECT_TRUE(startup.notify_ready("manager1"));
|
||||
// EXPECT_EQ(startup.startup_count(), 1); will be 0 because startup is complete
|
||||
thread.join();
|
||||
EXPECT_TRUE(woken);
|
||||
EXPECT_TRUE(startup.startup_complete());
|
||||
EXPECT_EQ(startup.total(), 0);
|
||||
}
|
||||
|
||||
TEST(StartupMonitor, zero) {
|
||||
StartupMonitorTest startup;
|
||||
EXPECT_FALSE(startup.startup_complete());
|
||||
EXPECT_EQ(startup.startup_count(), 0);
|
||||
EXPECT_EQ(startup.total(), 0);
|
||||
|
||||
bool woken{false};
|
||||
std::thread thread([&startup, &woken]() {
|
||||
startup.wait_ready();
|
||||
woken = true;
|
||||
});
|
||||
|
||||
EXPECT_FALSE(woken);
|
||||
|
||||
EXPECT_TRUE(startup.set_total(0));
|
||||
EXPECT_EQ(startup.total(), 0);
|
||||
EXPECT_EQ(startup.startup_count(), 0);
|
||||
thread.join();
|
||||
EXPECT_TRUE(woken);
|
||||
EXPECT_TRUE(startup.startup_complete());
|
||||
EXPECT_EQ(startup.total(), 0);
|
||||
}
|
||||
|
||||
TEST(StartupMonitor, invalidSequence) {
|
||||
StartupMonitorTest startup;
|
||||
EXPECT_FALSE(startup.startup_complete());
|
||||
EXPECT_FALSE(startup.notify_ready("manager1")); // total not set yet
|
||||
EXPECT_TRUE(startup.set_total(1));
|
||||
EXPECT_EQ(startup.startup_count(), 0);
|
||||
EXPECT_EQ(startup.total(), 1);
|
||||
|
||||
bool woken{false};
|
||||
std::thread thread([&startup, &woken]() {
|
||||
startup.wait_ready();
|
||||
woken = true;
|
||||
});
|
||||
|
||||
EXPECT_FALSE(startup.set_total(2)); // total already set
|
||||
EXPECT_EQ(startup.total(), 1); // didn't change
|
||||
EXPECT_TRUE(startup.notify_ready("manager2"));
|
||||
// EXPECT_EQ(startup.startup_count(), 1); will be 0 because startup is complete
|
||||
thread.join();
|
||||
EXPECT_TRUE(woken);
|
||||
EXPECT_TRUE(startup.startup_complete());
|
||||
EXPECT_EQ(startup.total(), 0);
|
||||
}
|
||||
|
||||
TEST(StartupMonitor, duplicateReady) {
|
||||
StartupMonitorTest startup;
|
||||
EXPECT_FALSE(startup.startup_complete());
|
||||
EXPECT_TRUE(startup.set_total(2));
|
||||
EXPECT_EQ(startup.startup_count(), 0);
|
||||
EXPECT_EQ(startup.total(), 2);
|
||||
|
||||
bool woken{false};
|
||||
std::thread thread([&startup, &woken]() {
|
||||
startup.wait_ready();
|
||||
woken = true;
|
||||
});
|
||||
|
||||
EXPECT_TRUE(startup.notify_ready("manager1"));
|
||||
EXPECT_EQ(startup.startup_count(), 1);
|
||||
EXPECT_TRUE(startup.notify_ready("manager1")); // duplicate
|
||||
EXPECT_EQ(startup.startup_count(), 1);
|
||||
EXPECT_FALSE(startup.startup_complete());
|
||||
EXPECT_TRUE(startup.notify_ready("manager2"));
|
||||
// EXPECT_EQ(startup.startup_count(), 2); will be 0 because startup is complete
|
||||
EXPECT_TRUE(startup.startup_complete());
|
||||
|
||||
thread.join();
|
||||
EXPECT_TRUE(woken);
|
||||
EXPECT_TRUE(startup.startup_complete());
|
||||
EXPECT_EQ(startup.total(), 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user