Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,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

View 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

View 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",
],
)

View 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

View 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

View 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

View 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

View 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"

View 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

View 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

View 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

View 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})

View 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