- 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
327 lines
15 KiB
C++
327 lines
15 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
|
|
|
#include "BUPowermeter.hpp"
|
|
#include "ftxui/component/component.hpp"
|
|
#include "ftxui/component/screen_interactive.hpp"
|
|
#include "ftxui/dom/table.hpp"
|
|
#include <ranges>
|
|
|
|
using namespace ftxui;
|
|
|
|
namespace module {
|
|
|
|
void optional_add(std::vector<std::vector<std::string>>& table, std::string text, std::string s_value) {
|
|
table.push_back({text, s_value});
|
|
}
|
|
|
|
void optional_add(std::vector<std::vector<std::string>>& table, std::string text, std::optional<float> o_value) {
|
|
if (o_value.has_value()) {
|
|
table.push_back({text, std::to_string(o_value.value())});
|
|
}
|
|
}
|
|
|
|
void optional_add(std::vector<std::vector<std::string>>& table, std::string text, std::optional<std::string> o_value) {
|
|
if (o_value.has_value()) {
|
|
table.push_back({text, o_value.value()});
|
|
}
|
|
}
|
|
|
|
void BUPowermeter::init() {
|
|
}
|
|
|
|
// Helper function to wrap long text into multiple lines
|
|
std::vector<std::string> WrapText(const std::string& text, size_t max_width) {
|
|
std::vector<std::string> lines;
|
|
size_t start = 0;
|
|
|
|
if (max_width < text.size()) {
|
|
while (start < text.size()) {
|
|
size_t end = std::min(start + max_width, text.size());
|
|
size_t last_space = text.rfind(' ', end);
|
|
|
|
// If there's no space, split at max_width
|
|
if (last_space == std::string::npos || last_space < start) {
|
|
lines.push_back(text.substr(start, max_width));
|
|
start += max_width;
|
|
} else {
|
|
lines.push_back(text.substr(start, last_space - start));
|
|
start = last_space + 1; // Skip the space
|
|
}
|
|
}
|
|
} else {
|
|
lines.push_back(text);
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
// Helper function to transform text strings into FTXUI Elements
|
|
std::vector<Element> WrapTextToElements(const std::vector<std::string>& strings) {
|
|
std::vector<Element> elements;
|
|
elements.reserve(strings.size()); // Preallocate memory
|
|
|
|
// Use std::transform to convert strings to FTXUI elements
|
|
std::transform(strings.begin(), strings.end(), std::back_inserter(elements),
|
|
[](const std::string& line) { return text(line); } // Convert each string to FTXUI text
|
|
);
|
|
|
|
return elements;
|
|
}
|
|
|
|
// Helper function to format and render the table content
|
|
std::vector<std::vector<Element>> FormatTableContent(const std::vector<std::vector<std::string>>& raw_table_content,
|
|
size_t max_width) {
|
|
std::vector<std::vector<Element>> table_elements;
|
|
|
|
for (const auto& row : raw_table_content) {
|
|
std::vector<Element> formatted_row;
|
|
|
|
for (const auto& cell : row) {
|
|
// Wrap long strings into multiple lines
|
|
std::vector<std::string> wrapped_lines = WrapText(cell, max_width);
|
|
|
|
// Convert wrapped lines into FTXUI text elements
|
|
auto cell_element = vbox(WrapTextToElements(wrapped_lines));
|
|
|
|
formatted_row.push_back(cell_element);
|
|
}
|
|
|
|
table_elements.push_back(std::move(formatted_row));
|
|
}
|
|
|
|
return table_elements;
|
|
}
|
|
|
|
void BUPowermeter::ready() {
|
|
auto screen = ScreenInteractive::Fullscreen();
|
|
|
|
r_powermeter->subscribe_public_key_ocmf([this, &screen](std::string pk) {
|
|
public_key = pk;
|
|
last_event_received = event_received;
|
|
event_received = std::chrono::steady_clock::now();
|
|
screen.Post(Event::Custom);
|
|
});
|
|
|
|
r_powermeter->subscribe_powermeter([this, &screen](types::powermeter::Powermeter pm) {
|
|
powermeter = pm;
|
|
last_event_received = event_received;
|
|
event_received = std::chrono::steady_clock::now();
|
|
screen.Post(Event::Custom);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Commands that can be sent over Powermeter interface
|
|
// ---------------------------------------------------------------------------
|
|
Component start_transaction_button = Button(
|
|
"Start transaction",
|
|
[&] {
|
|
types::powermeter::OCMFUserIdentificationStatus
|
|
identification_status; ///< OCMF Identification Status (IS): General status for user assignment
|
|
std::vector<types::powermeter::OCMFIdentificationFlags>
|
|
identification_flags; ///< OCMF Identification Flags (IF): Detailed statements about the user
|
|
///< assignment, represented by one or more identifiers
|
|
types::powermeter::OCMFIdentificationType
|
|
identification_type; ///< OCMF Identification Type (IT): Type of identification data
|
|
std::optional<types::powermeter::OCMFIdentificationLevel>
|
|
identification_level; ///< OCMF Identification Level (IL): Encoded overall status of the user assignment
|
|
std::optional<std::string>
|
|
identification_data; ///< OCMF Identification Data (ID): The actual identification data e.g. a hex-coded
|
|
///< UID according to ISO 14443.
|
|
std::optional<std::string> tariff_text; ///< Tariff text
|
|
|
|
last_command = "Start transaction";
|
|
last_command_duration = "";
|
|
screen.Post(Event::Custom);
|
|
types::powermeter::TransactionReq tr;
|
|
tr.evse_id = config.evse_id;
|
|
tr.transaction_id = config.transaction_id;
|
|
tr.identification_status = types::powermeter::OCMFUserIdentificationStatus::ASSIGNED;
|
|
tr.identification_type = types::powermeter::OCMFIdentificationType::ISO14443;
|
|
tr.identification_data = config.identification_data;
|
|
tr.tariff_text = config.tariff_text;
|
|
|
|
auto now_b = std::chrono::steady_clock::now();
|
|
tr_start = r_powermeter->call_start_transaction(tr);
|
|
auto now_e = std::chrono::steady_clock::now();
|
|
last_command_duration =
|
|
fmt::format("{} ms", std::chrono::duration_cast<std::chrono::milliseconds>(now_e - now_b).count());
|
|
},
|
|
ButtonOption::Animated(Color::Blue, Color::White, Color::RedLight, Color::White));
|
|
|
|
Component stop_transaction_button = Button(
|
|
"Stop transaction",
|
|
[&] {
|
|
last_command = "Stop transaction";
|
|
std::string transaction_id = config.transaction_id;
|
|
auto now_b = std::chrono::steady_clock::now();
|
|
tr_stop = r_powermeter->call_stop_transaction(transaction_id);
|
|
auto now_e = std::chrono::steady_clock::now();
|
|
last_command_duration =
|
|
fmt::format("{} ms", std::chrono::duration_cast<std::chrono::milliseconds>(now_e - now_b).count());
|
|
},
|
|
ButtonOption::Animated(Color::Blue, Color::White, Color::RedLight, Color::White));
|
|
|
|
Component stop_transaction0_button = Button(
|
|
"Stop transaction empty",
|
|
[&] {
|
|
last_command = "Stop transaction";
|
|
std::string transaction_id = "";
|
|
auto now_b = std::chrono::steady_clock::now();
|
|
tr_stop = r_powermeter->call_stop_transaction(transaction_id);
|
|
auto now_e = std::chrono::steady_clock::now();
|
|
last_command_duration =
|
|
fmt::format("{} ms", std::chrono::duration_cast<std::chrono::milliseconds>(now_e - now_b).count());
|
|
},
|
|
ButtonOption::Animated(Color::Blue, Color::White, Color::RedLight, Color::White));
|
|
|
|
Component stop_transaction1_button = Button(
|
|
"Stop transaction dummy",
|
|
[&] {
|
|
last_command = "Stop transaction";
|
|
std::string transaction_id = "DUMMY";
|
|
auto now_b = std::chrono::steady_clock::now();
|
|
tr_stop = r_powermeter->call_stop_transaction(transaction_id);
|
|
auto now_e = std::chrono::steady_clock::now();
|
|
last_command_duration =
|
|
fmt::format("{} ms", std::chrono::duration_cast<std::chrono::milliseconds>(now_e - now_b).count());
|
|
},
|
|
ButtonOption::Animated(Color::Blue, Color::White, Color::RedLight, Color::White));
|
|
|
|
auto cmds_component = Container::Horizontal({
|
|
start_transaction_button,
|
|
stop_transaction_button,
|
|
stop_transaction0_button,
|
|
stop_transaction1_button,
|
|
});
|
|
|
|
auto cmds_renderer = Renderer(cmds_component, [&] {
|
|
auto cmds_win = window(text("Commands"), cmds_component->Render());
|
|
return vbox({
|
|
hbox({cmds_win}) | center,
|
|
separator(),
|
|
}) |
|
|
flex_grow;
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Vars
|
|
// ---------------------------------------------------------------------------
|
|
auto vars_renderer = Renderer([&] {
|
|
std::vector<std::vector<std::string>> table_content;
|
|
auto last_event_duration = fmt::format(
|
|
"{} ms",
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(event_received - last_event_received).count());
|
|
types::units::Energy ed = {};
|
|
types::units::Power pd = {};
|
|
types::units::Voltage vd = {};
|
|
types::units_signed::SignedMeterValue svd = {};
|
|
types::units::Current cd = {};
|
|
types::units::Frequency fd = {};
|
|
|
|
optional_add(table_content, "Transaction start response: status",
|
|
types::powermeter::transaction_request_status_to_string(tr_start.status));
|
|
optional_add(table_content, "Transaction start response: error", tr_start.error);
|
|
optional_add(table_content, "Transaction start response: transaction_min_stop_time",
|
|
tr_start.transaction_min_stop_time);
|
|
optional_add(table_content, "Transaction start response: transaction_max_stop_time",
|
|
tr_start.transaction_max_stop_time);
|
|
optional_add(table_content, "Transaction stop response: status",
|
|
types::powermeter::transaction_request_status_to_string(tr_stop.status));
|
|
optional_add(table_content, "Transaction stop response: error", tr_stop.error);
|
|
optional_add(table_content, "Transaction stop response: signed_meter_value",
|
|
tr_stop.signed_meter_value.value_or(svd).signed_meter_data);
|
|
if (powermeter.signed_meter_value.has_value()) {
|
|
optional_add(table_content, "Powermeter: signed meter value",
|
|
powermeter.signed_meter_value.value().signed_meter_data);
|
|
optional_add(table_content, "Powermeter: signing method",
|
|
powermeter.signed_meter_value.value().signing_method);
|
|
optional_add(table_content, "Powermeter: encoding method",
|
|
powermeter.signed_meter_value.value().encoding_method);
|
|
optional_add(table_content, "Powermeter: public key", powermeter.signed_meter_value.value().public_key);
|
|
}
|
|
optional_add(table_content, "Powermeter: time stamp", powermeter.timestamp);
|
|
if (powermeter.temperatures.has_value()) {
|
|
int i = 1;
|
|
for (const auto& sensor : *powermeter.temperatures) {
|
|
std::string label = "Temperature sensor" + std::to_string(i++);
|
|
optional_add(table_content, label, sensor.temperature);
|
|
}
|
|
}
|
|
|
|
optional_add(table_content, "Powermeter: imported energy in Wh (from grid), total",
|
|
std::to_string(powermeter.energy_Wh_import.total));
|
|
optional_add(table_content, "Powermeter: imported energy in Wh (from grid): L1",
|
|
powermeter.energy_Wh_import.L1);
|
|
optional_add(table_content, "Powermeter: imported energy in Wh (from grid): L2",
|
|
powermeter.energy_Wh_import.L2);
|
|
optional_add(table_content, "Powermeter: imported energy in Wh (from grid): L3",
|
|
powermeter.energy_Wh_import.L3);
|
|
|
|
optional_add(table_content, "Powermeter: exported energy in Wh (to grid), total",
|
|
std::to_string(powermeter.energy_Wh_export.value_or(ed).total));
|
|
optional_add(table_content, "Powermeter: exported energy in Wh (to grid): L1",
|
|
powermeter.energy_Wh_export.value_or(ed).L1);
|
|
optional_add(table_content, "Powermeter: exported energy in Wh (to grid): L2",
|
|
powermeter.energy_Wh_export.value_or(ed).L2);
|
|
optional_add(table_content, "Powermeter: exported energy in Wh (to grid): L3",
|
|
powermeter.energy_Wh_export.value_or(ed).L3);
|
|
|
|
optional_add(table_content, "Powermeter: user defined meter ID", powermeter.meter_id);
|
|
optional_add(table_content, "Powermeter: 3 phase rotation error (ccw)", powermeter.phase_seq_error);
|
|
|
|
optional_add(table_content, "Powermeter: voltage in V, DC", powermeter.voltage_V.value_or(vd).DC);
|
|
optional_add(table_content, "Powermeter: voltage in V: L1", powermeter.voltage_V.value_or(vd).L1);
|
|
optional_add(table_content, "Powermeter: voltage in V: L2", powermeter.voltage_V.value_or(vd).L2);
|
|
optional_add(table_content, "Powermeter: voltage in V: L3", powermeter.voltage_V.value_or(vd).L3);
|
|
optional_add(table_content, "Powermeter: current in A, DC", powermeter.current_A.value_or(cd).DC);
|
|
optional_add(table_content, "Powermeter: current in A: L1", powermeter.current_A.value_or(cd).L1);
|
|
optional_add(table_content, "Powermeter: current in A: L2", powermeter.current_A.value_or(cd).L2);
|
|
optional_add(table_content, "Powermeter: current in A: L3", powermeter.current_A.value_or(cd).L3);
|
|
optional_add(table_content, "Powermeter: frequency in Hz: L1", powermeter.frequency_Hz.value_or(fd).L1);
|
|
optional_add(table_content, "Powermeter: frequency in Hz: L2", powermeter.frequency_Hz.value_or(fd).L2);
|
|
optional_add(table_content, "Powermeter: frequency in Hz: L3", powermeter.frequency_Hz.value_or(fd).L3);
|
|
int i = 1;
|
|
for (const auto& sensor : *powermeter.temperatures) {
|
|
std::string label = "Temperature sensor" + std::to_string(i++);
|
|
optional_add(table_content, label, sensor.temperature);
|
|
}
|
|
optional_add(table_content, "Public key", public_key);
|
|
|
|
size_t max_width = 120;
|
|
auto formatted_content = FormatTableContent(table_content, max_width);
|
|
auto vars = Table(std::move(formatted_content));
|
|
|
|
vars.SelectColumn(0).Border(LIGHT);
|
|
for (int i = 0; i < table_content.size(); i++) {
|
|
vars.SelectRow(i).Border(LIGHT);
|
|
}
|
|
|
|
return vbox({
|
|
hbox({
|
|
window(text("Information"),
|
|
vbox({text("Last command: " + last_command),
|
|
text("Last command duration: " + last_command_duration),
|
|
text("Time between events: " + last_event_duration), vars.Render()})),
|
|
}) | center,
|
|
}) |
|
|
flex_grow;
|
|
});
|
|
|
|
auto main_container = Container::Vertical({
|
|
cmds_renderer,
|
|
vars_renderer,
|
|
});
|
|
|
|
auto main_renderer = Renderer(main_container, [&] {
|
|
return vbox({
|
|
text("Powermeter Component") | bold | hcenter,
|
|
main_container->Render(),
|
|
});
|
|
});
|
|
|
|
screen.Loop(main_renderer);
|
|
}
|
|
|
|
} // namespace module
|