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:
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "AST_DC650.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
void AST_DC650::init() {
|
||||
invoke_init(*p_main);
|
||||
}
|
||||
|
||||
void AST_DC650::ready() {
|
||||
invoke_ready(*p_main);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef AST_DC650_HPP
|
||||
#define AST_DC650_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
// insert your custom include headers here
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class AST_DC650 : public Everest::ModuleBase {
|
||||
public:
|
||||
AST_DC650() = delete;
|
||||
AST_DC650(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, std::unique_ptr<powermeterImplBase> p_main,
|
||||
Conf& config) :
|
||||
ModuleBase(info), mqtt(mqtt_provider), p_main(std::move(p_main)), config(config){};
|
||||
|
||||
Everest::MqttProvider& mqtt;
|
||||
const std::unique_ptr<powermeterImplBase> p_main;
|
||||
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
|
||||
// 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 // AST_DC650_HPP
|
||||
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# 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_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
slip_protocol.cpp
|
||||
serial_device.cpp
|
||||
ast_app_layer.cpp
|
||||
diagnostics.cpp
|
||||
)
|
||||
|
||||
target_compile_features(${MODULE_NAME} PUBLIC cxx_std_17)
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"main/powermeterImpl.cpp"
|
||||
)
|
||||
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
# insert other things like install cmds etc here
|
||||
target_link_libraries(${MODULE_NAME} PRIVATE
|
||||
everest::framework
|
||||
everest::crc
|
||||
)
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef TIMEOUT_HPP
|
||||
#define TIMEOUT_HPP
|
||||
|
||||
#include <chrono>
|
||||
using namespace std::chrono;
|
||||
|
||||
/*
|
||||
Simple helper class for a timeout
|
||||
*/
|
||||
|
||||
class Timeout {
|
||||
public:
|
||||
explicit Timeout(milliseconds _t) : t(_t), start(steady_clock::now()) {
|
||||
}
|
||||
|
||||
bool reached() {
|
||||
return (steady_clock::now() - start) > t;
|
||||
}
|
||||
|
||||
private:
|
||||
milliseconds t;
|
||||
time_point<steady_clock> start;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,375 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "ast_app_layer.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <endian.h>
|
||||
#include <errno.h>
|
||||
#include <everest/logging.hpp>
|
||||
#include <fcntl.h>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/select.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
namespace ast_app_layer {
|
||||
|
||||
uint32_t timepoint_to_uint32(date::utc_clock::time_point timepoint) {
|
||||
return static_cast<uint32_t>(
|
||||
std::chrono::duration_cast<std::chrono::seconds>(timepoint.time_since_epoch()).count());
|
||||
}
|
||||
|
||||
void insert_u16_as_u8s(std::vector<std::uint8_t>& vec, std::uint16_t u16) {
|
||||
std::uint8_t upper = static_cast<std::uint8_t>((u16 >> 8) & 0x00FF);
|
||||
std::uint8_t lower = static_cast<std::uint8_t>(u16 & 0x00FF);
|
||||
vec.push_back(lower);
|
||||
vec.push_back(upper);
|
||||
}
|
||||
|
||||
void insert_u32_as_u8s(std::vector<std::uint8_t>& vec, std::uint32_t u32) {
|
||||
vec.push_back(static_cast<std::uint8_t>(u32 & 0x000000FF));
|
||||
vec.push_back(static_cast<std::uint8_t>((u32 >> 8) & 0x000000FF));
|
||||
vec.push_back(static_cast<std::uint8_t>((u32 >> 16) & 0x000000FF));
|
||||
vec.push_back(static_cast<std::uint8_t>((u32 >> 24) & 0x000000FF));
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> AstAppLayer::create_command(ast_app_layer::Command cmd) {
|
||||
std::vector<std::uint8_t> command_data{};
|
||||
|
||||
insert_u16_as_u8s(command_data, static_cast<std::uint16_t>(cmd.type));
|
||||
insert_u16_as_u8s(command_data, static_cast<std::uint16_t>(cmd.length));
|
||||
command_data.push_back((uint8_t)ast_app_layer::CommandStatus::OK);
|
||||
|
||||
for (uint16_t i = 0; i < cmd.data.size(); i++) {
|
||||
command_data.push_back(cmd.data[i]);
|
||||
}
|
||||
return command_data;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> AstAppLayer::create_simple_command(ast_app_layer::CommandType cmd_type) {
|
||||
ast_app_layer::Command cmd{};
|
||||
|
||||
cmd.type = cmd_type;
|
||||
cmd.length = 0x0005;
|
||||
cmd.status = ast_app_layer::CommandStatus::OK;
|
||||
|
||||
return create_command(cmd);
|
||||
}
|
||||
|
||||
int8_t AstAppLayer::get_utc_offset_in_quarter_hours(
|
||||
const std::chrono::time_point<std::chrono::system_clock>& timepoint_system_clock) {
|
||||
std::stringstream offset;
|
||||
std::int8_t offset_quarterhours = 0;
|
||||
|
||||
auto tm = std::chrono::system_clock::to_time_t(timepoint_system_clock);
|
||||
offset << std::put_time(std::localtime(&tm), "%z");
|
||||
int offset_int = std::stoi(offset.str());
|
||||
|
||||
int offset_h = offset_int / 100;
|
||||
int offset_remaining = offset_int % 100; // in case of timezones that are not full-hour offsets of UTC
|
||||
if (offset_remaining != 0) {
|
||||
std::int8_t offset_remaining_extra_hour = offset_remaining / 60;
|
||||
if (offset_remaining_extra_hour != 0) {
|
||||
offset_quarterhours += offset_remaining_extra_hour * 4; // can be positive or negative
|
||||
offset_remaining -= offset_remaining_extra_hour * 4;
|
||||
}
|
||||
std::int8_t offset_remaining_quarterhours = offset_remaining / 15;
|
||||
offset_quarterhours += offset_remaining_quarterhours;
|
||||
}
|
||||
offset_quarterhours += offset_h * 4;
|
||||
|
||||
return offset_quarterhours;
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_start_transaction(ast_app_layer::UserIdStatus user_id_status,
|
||||
ast_app_layer::UserIdType user_id_type,
|
||||
const std::string& user_id_data,
|
||||
std::int8_t gmt_offset_quarter_hours,
|
||||
std::vector<std::uint8_t>& command_data) {
|
||||
ast_app_layer::Command cmd{};
|
||||
|
||||
cmd.type = ast_app_layer::CommandType::START_TRANSACTION;
|
||||
cmd.length = 0x0034;
|
||||
cmd.status = ast_app_layer::CommandStatus::OK;
|
||||
|
||||
insert_u32_as_u8s(cmd.data, (timepoint_to_uint32(date::utc_clock::now())));
|
||||
cmd.data.push_back(
|
||||
static_cast<std::uint8_t>(gmt_offset_quarter_hours)); // GMT offset in quarters of an hour, e.g. 0x08 = +2h
|
||||
cmd.data.push_back(static_cast<std::uint8_t>(user_id_status));
|
||||
cmd.data.push_back(static_cast<std::uint8_t>(user_id_type));
|
||||
|
||||
std::uint8_t byte_count = 0;
|
||||
for (std::uint8_t databyte : user_id_data) { // push up to 40 characters of user id name into command
|
||||
cmd.data.push_back(databyte);
|
||||
byte_count++;
|
||||
if (byte_count >= 40)
|
||||
break;
|
||||
}
|
||||
while (byte_count < 40) { // fill remaining user id name characters with zeros
|
||||
cmd.data.push_back(0x00);
|
||||
byte_count++;
|
||||
}
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_stop_transaction(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::STOP_TRANSACTION);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_time(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::TIME);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_set_time(date::utc_clock::time_point timepoint,
|
||||
std::int8_t gmt_offset_quarters_of_an_hour,
|
||||
std::vector<std::uint8_t>& command_data) {
|
||||
ast_app_layer::Command cmd{};
|
||||
|
||||
cmd.type = ast_app_layer::CommandType::TIME;
|
||||
cmd.length = 0x000A;
|
||||
cmd.status = ast_app_layer::CommandStatus::OK;
|
||||
|
||||
insert_u32_as_u8s(cmd.data, timepoint_to_uint32(timepoint));
|
||||
cmd.data.push_back(gmt_offset_quarters_of_an_hour);
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_voltage(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_VOLTAGE_L1);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_current(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_CURRENT_L1);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_import_power(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_IMPORT_DEV_POWER);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_export_power(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_EXPORT_DEV_POWER);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_total_dev_import_energy(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_IMPORT_DEV_ENERGY);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_total_dev_export_energy(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_EXPORT_DEV_ENERGY);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_total_power(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_DEV_POWER);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_total_start_import_energy(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_START_IMPORT_DEV_ENERGY);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_total_stop_import_energy(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_STOP_IMPORT_DEV_ENERGY);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_total_start_export_energy(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_START_EXPORT_DEV_ENERGY);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_total_stop_export_energy(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_STOP_EXPORT_DEV_ENERGY);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_total_transaction_duration(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_TRANSACT_TOTAL_DURATION);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_pubkey_str16(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_PUBKEY_STR16);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_pubkey_asn1(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_PUBKEY_ASN1);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_meter_pubkey(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::REQUEST_METER_PUBKEY);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_ocmf_stats(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::OCMF_STATS);
|
||||
}
|
||||
|
||||
/* OCMF ID: 1..235000
|
||||
OCMF data from specified transaction will be at minimum import energy of transaction
|
||||
*/
|
||||
void AstAppLayer::create_command_get_transaction_ocmf(uint32_t ocmf_id, std::vector<std::uint8_t>& command_data) {
|
||||
ast_app_layer::Command cmd{};
|
||||
|
||||
cmd.type = ast_app_layer::CommandType::GET_OCMF;
|
||||
cmd.length = 0x0009;
|
||||
cmd.status = ast_app_layer::CommandStatus::OK;
|
||||
|
||||
insert_u32_as_u8s(cmd.data, ocmf_id);
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_last_transaction_ocmf(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_LAST_OCMF);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_ocmf_info(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::OCMF_INFO);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_ocmf_config(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::OCMF_CONFIG);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_charge_point_id(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::CHARGE_POINT_ID);
|
||||
}
|
||||
|
||||
/* only works in "assembly mode" */
|
||||
void AstAppLayer::create_command_set_charge_point_id(ast_app_layer::UserIdType id_type, std::string id_data,
|
||||
std::vector<std::uint8_t>& command_data) {
|
||||
ast_app_layer::Command cmd{};
|
||||
|
||||
cmd.type = ast_app_layer::CommandType::CHARGE_POINT_ID;
|
||||
cmd.length = 0x0013;
|
||||
cmd.status = ast_app_layer::CommandStatus::OK;
|
||||
|
||||
cmd.data.push_back((uint8_t)id_type);
|
||||
|
||||
std::uint8_t byte_count = 0;
|
||||
for (std::uint8_t databyte : id_data) { // push up to 13 characters of id data into command
|
||||
cmd.data.push_back(databyte);
|
||||
byte_count++;
|
||||
if (byte_count >= 13)
|
||||
break;
|
||||
}
|
||||
while (byte_count < 13) { // fill remaining user id name characters with zeros
|
||||
cmd.data.push_back(0x00);
|
||||
byte_count++;
|
||||
}
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_errors(ast_app_layer::ErrorCategory category, ast_app_layer::ErrorSource src,
|
||||
std::vector<std::uint8_t>& command_data) {
|
||||
ast_app_layer::Command cmd{};
|
||||
|
||||
cmd.type = ast_app_layer::CommandType::GET_ERRORS;
|
||||
cmd.length = 0x0007;
|
||||
cmd.status = ast_app_layer::CommandStatus::OK;
|
||||
|
||||
cmd.data.push_back((uint8_t)category);
|
||||
cmd.data.push_back((uint8_t)src);
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_log_stats(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_LOG_STATS);
|
||||
}
|
||||
|
||||
/* log entry ids: 1..2500 */
|
||||
void AstAppLayer::create_command_get_log_entry(uint32_t log_entry_id, std::vector<std::uint8_t>& command_data) {
|
||||
ast_app_layer::Command cmd{};
|
||||
|
||||
cmd.type = ast_app_layer::CommandType::GET_LOG_ENTRY;
|
||||
cmd.length = 0x0009;
|
||||
cmd.status = ast_app_layer::CommandStatus::OK;
|
||||
|
||||
insert_u32_as_u8s(cmd.data, log_entry_id);
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_last_log_entry(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::GET_LAST_LOG_ENTRY);
|
||||
}
|
||||
|
||||
/* log entry ids: 1..2500
|
||||
thus: if 20 log entries and log_entry_id == 2, then log entry 18 will be returned
|
||||
*/
|
||||
void AstAppLayer::create_command_get_log_entry_reverse(uint32_t log_entry_id, std::vector<std::uint8_t>& command_data) {
|
||||
ast_app_layer::Command cmd{};
|
||||
|
||||
cmd.type = ast_app_layer::CommandType::GET_LOG_ENTRY_REVERSE;
|
||||
cmd.length = 0x0009;
|
||||
cmd.status = ast_app_layer::CommandStatus::OK;
|
||||
|
||||
insert_u32_as_u8s(cmd.data, log_entry_id);
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_application_board_mode(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::AB_MODE_SET);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_set_application_board_mode(ast_app_layer::ApplicationBoardMode mode,
|
||||
std::vector<std::uint8_t>& command_data) {
|
||||
ast_app_layer::Command cmd{};
|
||||
|
||||
cmd.type = ast_app_layer::CommandType::AB_MODE_SET;
|
||||
cmd.length = 0x0006;
|
||||
cmd.status = ast_app_layer::CommandStatus::OK;
|
||||
|
||||
cmd.data.push_back((uint8_t)mode);
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
// diagnostics
|
||||
|
||||
void AstAppLayer::create_command_get_hardware_version(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::AB_HW_VERSION);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_application_board_server_id(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::AB_SERVER_ID);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_application_board_serial_number(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::AB_SERIAL_NR);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_application_board_software_version(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::AB_SW_VERSION);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_application_board_fw_checksum(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::AB_FW_CHECKSUM);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_application_board_fw_hash(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::AB_FW_HASH);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_application_board_status(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::AB_STATUS);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_metering_board_software_version(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::MB_SW_VERSION);
|
||||
}
|
||||
|
||||
void AstAppLayer::create_command_get_metering_board_fw_checksum(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::MB_FW_CHECKSUM);
|
||||
}
|
||||
|
||||
/* doubles as OCMF "meter model name" */
|
||||
void AstAppLayer::create_command_get_device_type(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(ast_app_layer::CommandType::AB_DEVICE_TYPE);
|
||||
}
|
||||
|
||||
} // namespace ast_app_layer
|
||||
@@ -0,0 +1,361 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/*
|
||||
This is an implementation for the AST powermeter application layer
|
||||
*/
|
||||
#ifndef AST_APP_LAYER
|
||||
#define AST_APP_LAYER
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <everest/logging.hpp>
|
||||
#include <generated/types/powermeter.hpp>
|
||||
|
||||
namespace ast_app_layer {
|
||||
|
||||
enum class CommandType : std::uint16_t {
|
||||
RESET_DC_METER = 0x4101, // reset the DC meter
|
||||
AB_MODE_SET = 0x4102, // get or set the mode of application board
|
||||
AB_SERVER_ID = 0x4110, // get or set the server ID of application board
|
||||
AB_SERIAL_NR = 0x4111, // get or set the serial number
|
||||
AB_HW_VERSION = 0x4112, // get or set the hardware version
|
||||
AB_DEVICE_TYPE = 0x4113, // get or set the device type
|
||||
AB_SW_VERSION = 0x4114, // get the software version of application board
|
||||
AB_FW_CHECKSUM = 0x4115, // get the checksum (crc16) of application board
|
||||
MB_SW_VERSION = 0x4116, // get the software version of metering board
|
||||
MB_FW_CHECKSUM = 0x4117, // get the crc16 from software integrity check of metering board
|
||||
AB_FW_HASH = 0x4119, // get the firmware hash(crc16) of application board
|
||||
MEASUREMENT_MODE = 0x4120, // get the measurement mode
|
||||
GET_NORMAL_VOLTAGE = 0x4121, // get the normal voltage
|
||||
GET_NORMAL_CURRENT = 0x4122, // get the normal current
|
||||
GET_MAX_CURRENT = 0x4123, // get the maximum current
|
||||
LINE_LOSS_IMPEDANCE = 0x4130, // get or set the line loss impedance
|
||||
LINE_LOSS_MEAS_MODE = 0x4131, // get the line loss energy measurement mode
|
||||
TIME = 0x4135, // get or set the device time
|
||||
TEMPERATURE = 0x4136, // get the temperature
|
||||
AB_STATUS = 0x4137, // get the statusword of application board
|
||||
METER_BUS_ADDR = 0x4212, // get or set the meter bus address
|
||||
START_TRANSACTION = 0x1501, // start transaction
|
||||
STOP_TRANSACTION = 0x1502, // stop transaction
|
||||
OCMF_STATS = 0x1510, // get the statistics of OCMF
|
||||
GET_OCMF = 0x1520, // get the OCMF
|
||||
GET_LAST_OCMF = 0x1521, // get last OCMF
|
||||
GET_OCMF_REVERSE = 0x1522, // get the OCMF reverse
|
||||
GET_PUBKEY_ASN1 = 0x1530, // get public key in ASN1 format
|
||||
CHARGE_POINT_ID = 0x1531, // get or set the charge point identification
|
||||
GET_PUBKEY_BIN = 0x1532, // get public key as binary string
|
||||
GET_TRANSACT_IMPORT_LINE_LOSS_ENERGY = 0x1540, // get total transaction import line loss energy
|
||||
GET_TRANSACT_EXPORT_LINE_LOSS_ENERGY = 0x1541, // get total transaction export line loss energy
|
||||
GET_TRANSACT_TOTAL_IMPORT_DEV_ENERGY = 0x1550, // get total transaction import device energy
|
||||
GET_TRANSACT_TOTAL_EXPORT_DEV_ENERGY = 0x1551, // get total transaction export device energy
|
||||
GET_TRANSACT_TOTAL_IMPORT_MAINS_ENERGY = 0x1560, // get total transaction import mains energy
|
||||
GET_TRANSACT_TOTAL_EXPORT_MAINS_ENERGY = 0x1561, // get total transaction export mains energy
|
||||
GET_TRANSACT_TOTAL_DURATION = 0x156A, // get total transaction duration.
|
||||
GET_TOTAL_START_IMPORT_LINE_LOSS_ENERGY = 0x1570, // get total start import line loss energy
|
||||
GET_TOTAL_START_EXPORT_LINE_LOSS_ENERGY = 0x1571, // get total start export line loss energy
|
||||
GET_TOTAL_START_IMPORT_DEV_ENERGY = 0x1580, // get total start import device energy
|
||||
GET_TOTAL_START_EXPORT_DEV_ENERGY = 0x1581, // get total start export device energy
|
||||
GET_TOTAL_START_IMPORT_MAINS_ENERGY = 0x1590, // get total start import mains energy
|
||||
GET_TOTAL_START_EXPORT_MAINS_ENERGY = 0x1591, // get total start export mains energy
|
||||
GET_TOTAL_STOP_IMPORT_LINE_LOSS_ENERGY = 0x15A0, // get total stop import line loss energy
|
||||
GET_TOTAL_STOP_EXPORT_LINE_LOSS_ENERGY = 0x15A1, // get total stop export line loss energy
|
||||
GET_TOTAL_STOP_IMPORT_DEV_ENERGY = 0x15B0, // get total stop import device energy
|
||||
GET_TOTAL_STOP_EXPORT_DEV_ENERGY = 0x15B1, // get total stop export device energy
|
||||
GET_TOTAL_STOP_IMPORT_MAINS_ENERGY = 0x15C0, // get total stop import mains energy
|
||||
GET_TOTAL_STOP_EXPORT_MAINS_ENERGY = 0x15C1, // get total stop export mains energy
|
||||
GET_LOG_STATS = 0x1710, // get the statistic of log
|
||||
GET_LOG_ENTRY = 0x1720, // get the log entry
|
||||
GET_LAST_LOG_ENTRY = 0x1721, // get last log entry
|
||||
GET_LOG_ENTRY_REVERSE = 0x1722, // get the log entry reverse
|
||||
REGISTER_DISPLAY_PUBKEY = 0x1801, // register display public key
|
||||
REQUEST_CHALLENGE = 0x1810, // request challenge
|
||||
SET_SIGNED_CHALLENGE = 0x1811, // set signed challenge
|
||||
REQUEST_METER_PUBKEY = 0x1830, // request meter public key
|
||||
GET_TOTAL_IMPORT_MAINS_ENERGY = 0x0110, // get total import mains energy
|
||||
GET_TOTAL_EXPORT_MAINS_ENERGY = 0x0120, // get total export mains energy
|
||||
GET_CURRENT_L1 = 0x0131, // get current (phase L1)
|
||||
GET_VOLTAGE_L1 = 0x0132, // get voltage (phase L1)
|
||||
GET_TOTAL_IMPORT_MAINS_POWER = 0x0133, // total import mains power
|
||||
GET_TOTAL_EXPORT_MAINS_POWER = 0x0134, // total export mains power
|
||||
GET_DEV_VOLTAGE_L1 = 0x0137, // get device voltage (phase L1)
|
||||
GET_IMPORT_DEV_POWER = 0x0138, // get import device power
|
||||
GET_EXPORT_DEV_POWER = 0x0139, // get export device power
|
||||
GET_IMPORT_LINE_LOSS_POWER = 0x013A, // get import line loss power
|
||||
GET_EXPORT_LINE_LOSS_POWER = 0x013B, // get export line loss power
|
||||
GET_TOTAL_DEV_POWER = 0x013C, // get total device power
|
||||
GET_TOTAL_IMPORT_LINE_LOSS_ENERGY = 0x0160, // get the total import line loss energy
|
||||
GET_TOTAL_EXPORT_LINE_LOSS_ENERGY = 0x0163, // get the total export line loss energy
|
||||
GET_TOTAL_IMPORT_DEV_ENERGY = 0x0170, // get the total import device energy
|
||||
GET_TOTAL_EXPORT_DEV_ENERGY = 0x0173, // get the total export device energy
|
||||
GET_SECOND_INDEX = 0x0180, // get the second index
|
||||
GET_PUBKEY_STR16 = 0x0211, // get the public key in string format (base16)
|
||||
GET_PUBKEY_STR32 = 0x0212, // get the public key in string format (base32)
|
||||
GET_PUBKEY_CSTR16 = 0x0213, // get the public key compressed in string format (base16)
|
||||
GET_PUBKEY_CSTR32 = 0x0214, // get the public key compressed in string format (base32)
|
||||
REPEAT_DATA = 0xA000, // repeat data
|
||||
OCMF_INFO = 0xA001, // get or set OCMF info
|
||||
OCMF_CONFIG = 0xA002, // get or set the table of OCMF configuration field
|
||||
GET_ERRORS = 0xA004, // get errors
|
||||
AB_DMC = 0xA006, // get or set the DMC application board
|
||||
AB_PROD_DATE = 0xA00B, // get or set the production date of application board
|
||||
SET_REQUEST_CHALLENGE = 0xA00C // enable/disable the request challenge
|
||||
};
|
||||
|
||||
enum class CommandStatus : std::uint8_t {
|
||||
OK = 0, // no error
|
||||
GENERAL_ERROR = 1, // internal process has an error
|
||||
OUT_OF_RANGE = 2, // data is out of range
|
||||
SECURITY_ACCES_DENIED = 3, // the current mode of the slave cannot start this command
|
||||
REJECTED = 4, // error of communication between application and metering board
|
||||
LOGICAL_ADDRESS_NOT_FOUND = 5, // command not implemented
|
||||
FORMAL_INVALID = 6, // length in bytes is incorrect; e.g. received more data than expected
|
||||
NOT_AVAILABLE = 7, // data not available
|
||||
IS_BUSY = 8, // slave cannot process command; already processing long operation
|
||||
PUBKEY_MISSING = 9 // public key is not stored in internal memory
|
||||
};
|
||||
|
||||
enum class CommandResult : std::uint8_t {
|
||||
OK = 0,
|
||||
GENERAL_ERROR = 1, // read ERROR register (read with GET_ERRORS[0xA004]) for more information
|
||||
OUT_OF_RANGE = 2, //
|
||||
SECURITY_ACCESS_DENIED = 3, // application board is not in production mode
|
||||
REJECTED = 4, // metering board is not responding
|
||||
FORMAT_INVALID = 6, // payload is not empty
|
||||
NOT_AVAILABLE = 7, // data could not be read
|
||||
BUSY = 8, // transaction ongoing, metering board unavailable
|
||||
PUBLIC_KEY_MISSING = 9, //
|
||||
PROTOCOL_ERROR = 250, // error on reception at host: protocol error (SLIP protocol)
|
||||
TIMEOUT = 251, // no response during at least 1100 ms
|
||||
COMMUNICATION_FAILED = 254, // error on communication between PM and host device
|
||||
PENDING = 255 // special state for transaction commands
|
||||
};
|
||||
|
||||
inline std::string command_result_to_string(CommandResult res) {
|
||||
switch (res) {
|
||||
case CommandResult::OK:
|
||||
return "OK";
|
||||
case CommandResult::GENERAL_ERROR:
|
||||
return "General Error";
|
||||
case CommandResult::OUT_OF_RANGE:
|
||||
return "Out of Range";
|
||||
case CommandResult::SECURITY_ACCESS_DENIED:
|
||||
return "Security Access Denied";
|
||||
case CommandResult::REJECTED:
|
||||
return "Rejected";
|
||||
case CommandResult::FORMAT_INVALID:
|
||||
return "Format Invalid";
|
||||
case CommandResult::NOT_AVAILABLE:
|
||||
return "Not Available";
|
||||
case CommandResult::BUSY:
|
||||
return "Busy";
|
||||
case CommandResult::PUBLIC_KEY_MISSING:
|
||||
return "Public Key Missing";
|
||||
case CommandResult::PROTOCOL_ERROR:
|
||||
return "Protocol Error";
|
||||
case CommandResult::TIMEOUT:
|
||||
return "Timeout";
|
||||
case CommandResult::COMMUNICATION_FAILED:
|
||||
return "Communication Failed";
|
||||
case CommandResult::PENDING:
|
||||
return "Pending";
|
||||
}
|
||||
throw std::out_of_range("No known string conversion for provided enum of type CommandResult");
|
||||
}
|
||||
|
||||
enum class UserIdStatus : std::uint8_t {
|
||||
USER_NOT_ASSIGNED = 0x00,
|
||||
USER_ASSIGNED = 0x01
|
||||
};
|
||||
|
||||
enum class UserIdType : std::uint8_t {
|
||||
NONE = 0, // not available
|
||||
DENIED = 1, // not retrievable (e.g. two-factor-auth)
|
||||
UNDEFINED = 2, // type unknown / other
|
||||
ISO14443 = 10, // UID of RFID card according to ISO14443 (4 or 7 bytes HEX)
|
||||
ISO15693 = 11, // UID of RFID card according to ISO15693 (8 bytes HEX)
|
||||
EMAID = 20, // Electro-Mobility-Account-ID according to ISO/IEC15118 (14 or 15 bytes string)
|
||||
EVCCID = 21, // ID of an EV according to ISO/IEC15118 (max 6 bytes)
|
||||
EVCOID = 30, // EV-Contract-ID according to DIN91286
|
||||
ISO7812 = 40, // Identification-Card-Format according to ISO/IEC7812 (credit-/banking-cards, etc.)
|
||||
CAR_TXN_NR = 50, // Card-Transaction-Number (CardTxNbr) for credit- or banking-cards
|
||||
CENTRAL = 60, // centrally generated ID (no fixed format, e.g. UUID); OCPP 2.0
|
||||
CENTRAL_1 = 61, // centrally generated ID (no fixed format, e.g. start-via-SMS); (up to) OCPP 1.6
|
||||
CENTRAL_2 = 62, // centrally generated ID (no fixed format, e.g. start-by-operator); (up to) OCPP 1.6
|
||||
LOCAL = 70, // locally generated ID (no fixed format, e.g. UUID); OCPP 2.0
|
||||
LOCAL_1 = 71, // locally generated ID (no fixed format, e.g. generated by chargepoint); (up to) OCPP 1.6
|
||||
LOCAL_2 = 72, // locally generated ID (no fixed format, other); (up to) OCPP 1.6
|
||||
PHONE_NUMBER = 80, // international phone number (leading '+' with country code)
|
||||
KEY_CODE = 90 // private user key (no fixed format)
|
||||
};
|
||||
|
||||
enum class ErrorCategory : std::uint8_t {
|
||||
LAST = 0,
|
||||
LAST_CRITICAL = 1,
|
||||
FIRST = 2,
|
||||
FIRST_CRITICAL = 3
|
||||
};
|
||||
|
||||
enum class ErrorSource : std::uint8_t {
|
||||
SYSTEM = 0,
|
||||
COMMUNICATION = 1
|
||||
};
|
||||
|
||||
enum class ApplicationBoardMode : std::uint8_t {
|
||||
APPLICATION = 0,
|
||||
ASSEMBLY = 1,
|
||||
// PRODUCTION = 2
|
||||
};
|
||||
|
||||
enum class LogType : std::uint8_t {
|
||||
NONE = 0,
|
||||
LINE_LOSS_MEASUREMENT_MODE = 1,
|
||||
IMPEDANCE_CHANGED = 2,
|
||||
OPERATION_MODE_CHANGED = 3,
|
||||
ASSEMBLY_CONFIG_CHANGED = 4,
|
||||
FATAL_ERROR_EVENT = 5,
|
||||
TIME_DELTA_TOO_BIG_EVENT = 6,
|
||||
CHARGE_POINT_ID_CHANGED = 7,
|
||||
EXTERNAL_DISPLAY_PAIRED = 8,
|
||||
EXTERNAL_DISPLAY_FAILURE = 9,
|
||||
CHARGE_DATA_OUT_OF_MEMORY = 10,
|
||||
LOG_DATA_OUT_OF_MEMORY = 11,
|
||||
FW_VERSION_CHANGED = 12,
|
||||
PULSE_LED_SOURCE_CHANGED = 13
|
||||
};
|
||||
|
||||
inline std::string log_type_to_string(LogType log) {
|
||||
switch (log) {
|
||||
case LogType::NONE:
|
||||
return "None";
|
||||
case LogType::LINE_LOSS_MEASUREMENT_MODE:
|
||||
return "Line loss measurement mode";
|
||||
case LogType::IMPEDANCE_CHANGED:
|
||||
return "Impedance changed";
|
||||
case LogType::OPERATION_MODE_CHANGED:
|
||||
return "Operation mode changed";
|
||||
case LogType::ASSEMBLY_CONFIG_CHANGED:
|
||||
return "Assembly config changed";
|
||||
case LogType::FATAL_ERROR_EVENT:
|
||||
return "Fatal error event";
|
||||
case LogType::TIME_DELTA_TOO_BIG_EVENT:
|
||||
return "Time delta too big event";
|
||||
case LogType::CHARGE_POINT_ID_CHANGED:
|
||||
return "Charge point id changed";
|
||||
case LogType::EXTERNAL_DISPLAY_PAIRED:
|
||||
return "External display paired";
|
||||
case LogType::EXTERNAL_DISPLAY_FAILURE:
|
||||
return "External display failure";
|
||||
case LogType::CHARGE_DATA_OUT_OF_MEMORY:
|
||||
return "Charge data out of memory";
|
||||
case LogType::LOG_DATA_OUT_OF_MEMORY:
|
||||
return "Log data out of memory";
|
||||
case LogType::FW_VERSION_CHANGED:
|
||||
return "Fw version changed";
|
||||
case LogType::PULSE_LED_SOURCE_CHANGED:
|
||||
return "Pulse LED source changed";
|
||||
}
|
||||
throw std::out_of_range("No known string conversion for provided enum of type LogType");
|
||||
}
|
||||
|
||||
class LogEntry {
|
||||
public:
|
||||
ast_app_layer::LogType type;
|
||||
std::uint32_t second_index{};
|
||||
std::uint32_t utc_time{};
|
||||
std::uint8_t utc_offset{};
|
||||
std::vector<uint8_t> old_value; // max. 10 elements
|
||||
std::vector<uint8_t> new_value; // max. 10 elements
|
||||
std::vector<uint8_t> server_id; // 10 elements
|
||||
std::vector<uint8_t> signature; // 64 elements
|
||||
};
|
||||
|
||||
class Command {
|
||||
public:
|
||||
ast_app_layer::CommandType type;
|
||||
std::uint16_t length;
|
||||
ast_app_layer::CommandStatus status;
|
||||
std::vector<std::uint8_t> data;
|
||||
};
|
||||
|
||||
static constexpr std::uint16_t PM_AST_MAX_RX_LENGTH = 1000;
|
||||
static constexpr std::uint16_t PM_AST_SERIAL_RX_INITIAL_TIMEOUT_MS = 1100;
|
||||
static constexpr std::uint16_t PM_AST_SERIAL_RX_WITHIN_MESSAGE_TIMEOUT_MS = 100;
|
||||
|
||||
class AstAppLayer {
|
||||
|
||||
public:
|
||||
void create_command_start_transaction(ast_app_layer::UserIdStatus user_id_status,
|
||||
ast_app_layer::UserIdType user_id_type, const std::string& user_id_data,
|
||||
std::int8_t gmt_offset_quarter_hours,
|
||||
std::vector<std::uint8_t>& command_data);
|
||||
void create_command_stop_transaction(std::vector<uint8_t>& command_data);
|
||||
|
||||
void create_command_get_time(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_set_time(date::utc_clock::time_point timepoint, int8_t gmt_offset_quarters_of_an_hour,
|
||||
std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_voltage(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_current(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_import_power(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_export_power(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_total_power(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_total_start_import_energy(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_total_stop_import_energy(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_total_start_export_energy(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_total_stop_export_energy(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_total_dev_import_energy(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_total_dev_export_energy(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_total_transaction_duration(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_pubkey_str16(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_pubkey_asn1(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_meter_pubkey(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_ocmf_stats(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_transaction_ocmf(std::uint32_t ocmf_id, std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_last_transaction_ocmf(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_ocmf_info(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_ocmf_config(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_charge_point_id(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_set_charge_point_id(ast_app_layer::UserIdType id_type, std::string id_data,
|
||||
std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_errors(ast_app_layer::ErrorCategory category, ast_app_layer::ErrorSource src,
|
||||
std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_log_stats(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_log_entry(uint32_t log_entry_id, std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_last_log_entry(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_log_entry_reverse(uint32_t log_entry_id, std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_application_board_mode(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_set_application_board_mode(ast_app_layer::ApplicationBoardMode mode,
|
||||
std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_hardware_version(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_application_board_server_id(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_application_board_serial_number(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_application_board_software_version(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_application_board_fw_checksum(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_application_board_fw_hash(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_application_board_status(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_metering_board_software_version(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_metering_board_fw_checksum(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_device_type(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
int8_t
|
||||
get_utc_offset_in_quarter_hours(const std::chrono::time_point<std::chrono::system_clock>& timepoint_system_clock);
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> create_command(ast_app_layer::Command cmd);
|
||||
std::vector<uint8_t> create_simple_command(ast_app_layer::CommandType cmd_type);
|
||||
};
|
||||
|
||||
} // namespace ast_app_layer
|
||||
#endif // AST_APP_LAYER
|
||||
@@ -0,0 +1,225 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <diagnostics.hpp>
|
||||
|
||||
namespace module {
|
||||
|
||||
void to_json(json& j, const DeviceData& k) {
|
||||
j["UTC"] = module::conversions::u32_epoch_to_rfc3339(k.utc_time_s);
|
||||
j["GMT_offset_quarterhours"] = k.gmt_offset_quarterhours;
|
||||
j["total_start_import_energy_Wh"] = k.total_start_import_energy_Wh;
|
||||
j["total_stop_import_energy_Wh"] = k.total_stop_import_energy_Wh;
|
||||
j["total_start_export_energy_Wh"] = k.total_start_export_energy_Wh;
|
||||
j["total_stop_export_energy_Wh"] = k.total_stop_export_energy_Wh;
|
||||
j["total_transaction_duration_s"] = k.total_transaction_duration_s;
|
||||
j["OCMF_stats"] = json();
|
||||
j["OCMF_stats"]["number_transactions"] = k.ocmf_stats.number_transactions;
|
||||
j["OCMF_stats"]["timestamp_first_transaction"] = k.ocmf_stats.timestamp_first_transaction;
|
||||
j["OCMF_stats"]["timestamp_last_transaction"] = k.ocmf_stats.timestamp_last_transaction;
|
||||
j["OCMF_stats"]["max_number_of_transactions"] = k.ocmf_stats.max_number_of_transactions;
|
||||
j["last_ocmf_transaction"] = k.last_ocmf_transaction;
|
||||
j["requested_ocmf"] = k.requested_ocmf;
|
||||
j["OCMF_info"] = json();
|
||||
j["OCMF_info"]["gateway_id"] = k.ocmf_info.gateway_id;
|
||||
j["OCMF_info"]["manufacturer"] = k.ocmf_info.manufacturer;
|
||||
j["OCMF_info"]["model"] = k.ocmf_info.model;
|
||||
j["total_dev_import_energy_Wh"] = k.total_dev_import_energy_Wh;
|
||||
j["total_dev_export_energy_Wh"] = k.total_dev_export_energy_Wh;
|
||||
j["status"] = module::conversions::to_bin_string(k.ab_status);
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const DeviceData& k) {
|
||||
os << json(k).dump(4);
|
||||
return os;
|
||||
}
|
||||
|
||||
void to_json(json& j, const DeviceDiagnostics& k) {
|
||||
j["charge_point_id"] = k.charge_point_id;
|
||||
j["charge_point_id_type"] = k.charge_point_id_type;
|
||||
j["log_stats"] = json();
|
||||
j["log_stats"]["number_log_entries"] = k.log_stats.number_log_entries;
|
||||
j["log_stats"]["timestamp_first_log"] = k.log_stats.timestamp_first_log;
|
||||
j["log_stats"]["timestamp_last_log"] = k.log_stats.timestamp_last_log;
|
||||
j["log_stats"]["max_number_of_logs"] = k.log_stats.max_number_of_logs;
|
||||
j["app_board"] = json();
|
||||
j["app_board"]["type"] = k.app_board.type;
|
||||
j["app_board"]["HW_ver"] = k.app_board.hw_ver;
|
||||
j["app_board"]["server_id"] = k.app_board.server_id;
|
||||
j["app_board"]["mode"] = k.app_board.mode;
|
||||
j["app_board"]["serial_nr"] = k.app_board.serial_number;
|
||||
j["app_board"]["SW_ver"] = k.app_board.sw_ver;
|
||||
j["app_board"]["FW_CRC"] = module::conversions::hexdump(k.app_board.fw_crc);
|
||||
j["app_board"]["FW_hash"] = module::conversions::hexdump(k.app_board.fw_hash);
|
||||
j["m_board"] = json();
|
||||
j["m_board"]["HW_ver"] = k.m_board.hw_ver;
|
||||
j["m_board"]["SW_ver"] = k.m_board.sw_ver;
|
||||
j["m_board"]["FW_CRC"] = module::conversions::hexdump(k.m_board.fw_crc);
|
||||
j["pubkey"] = json();
|
||||
j["pubkey"]["asn1"] = json();
|
||||
j["pubkey"]["str16"] = json();
|
||||
j["pubkey"]["default"] = json();
|
||||
j["pubkey"]["asn1"]["key"] = k.pubkey_asn1;
|
||||
j["pubkey"]["str16"]["key"] = k.pubkey_str16;
|
||||
j["pubkey"]["str16"]["format"] = k.pubkey_str16_format;
|
||||
j["pubkey"]["default"]["key"] = k.pubkey;
|
||||
j["pubkey"]["default"]["format"] = k.pubkey_format;
|
||||
j["ocmf_config_table"] = json::array();
|
||||
if (k.ocmf_config_table.size() > 0) {
|
||||
for (uint8_t n = 0; n < k.ocmf_config_table.size(); n++) {
|
||||
j["ocmf_config_table"][n] = module::conversions::hexdump((uint8_t)k.ocmf_config_table.at(n));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const DeviceDiagnostics& k) {
|
||||
os << json(k).dump(4);
|
||||
return os;
|
||||
}
|
||||
|
||||
void to_json(json& j, const Logging& k) {
|
||||
j["log"] = json();
|
||||
j["log"]["last"] = json();
|
||||
j["log"]["last"]["type"] = "" + std::to_string((int)k.last_log.type) + ": " + log_type_to_string(k.last_log.type);
|
||||
j["log"]["last"]["second_index"] = k.last_log.second_index;
|
||||
j["log"]["last"]["utc_time"] = module::conversions::u32_epoch_to_rfc3339(k.last_log.utc_time);
|
||||
j["log"]["last"]["utc_offset_quarterhours"] = k.last_log.utc_offset;
|
||||
j["log"]["last"]["old_value"] = module::conversions::hexdump(k.last_log.old_value);
|
||||
j["log"]["last"]["new_value"] = module::conversions::hexdump(k.last_log.new_value);
|
||||
j["log"]["last"]["server_id"] = module::conversions::hexdump(k.last_log.server_id);
|
||||
j["log"]["last"]["signature"] = module::conversions::hexdump(k.last_log.signature);
|
||||
|
||||
j["errors"] = json();
|
||||
j["errors"]["system"] = json();
|
||||
j["errors"]["system"]["last"] = json::array();
|
||||
for (uint8_t n = 0; n < 5; n++) {
|
||||
j["errors"]["system"]["last"][n]["id"] = k.source[(uint8_t)ast_app_layer::ErrorSource::SYSTEM]
|
||||
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST]
|
||||
.error[n]
|
||||
.id;
|
||||
j["errors"]["system"]["last"][n]["priority"] = k.source[(uint8_t)ast_app_layer::ErrorSource::SYSTEM]
|
||||
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST]
|
||||
.error[n]
|
||||
.priority;
|
||||
j["errors"]["system"]["last"][n]["counter"] = k.source[(uint8_t)ast_app_layer::ErrorSource::SYSTEM]
|
||||
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST]
|
||||
.error[n]
|
||||
.counter;
|
||||
}
|
||||
j["errors"]["system"]["last_critical"] = json::array();
|
||||
for (uint8_t n = 0; n < 5; n++) {
|
||||
j["errors"]["system"]["last_critical"][n]["id"] =
|
||||
k.source[(uint8_t)ast_app_layer::ErrorSource::SYSTEM]
|
||||
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST_CRITICAL]
|
||||
.error[n]
|
||||
.id;
|
||||
j["errors"]["system"]["last_critical"][n]["priority"] =
|
||||
k.source[(uint8_t)ast_app_layer::ErrorSource::SYSTEM]
|
||||
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST_CRITICAL]
|
||||
.error[n]
|
||||
.priority;
|
||||
j["errors"]["system"]["last_critical"][n]["counter"] =
|
||||
k.source[(uint8_t)ast_app_layer::ErrorSource::SYSTEM]
|
||||
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST_CRITICAL]
|
||||
.error[n]
|
||||
.counter;
|
||||
}
|
||||
j["errors"]["communication"] = json();
|
||||
j["errors"]["communication"]["last"] = json::array();
|
||||
for (uint8_t n = 0; n < 5; n++) {
|
||||
j["errors"]["communication"]["last"][n]["id"] = k.source[(uint8_t)ast_app_layer::ErrorSource::COMMUNICATION]
|
||||
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST]
|
||||
.error[n]
|
||||
.id;
|
||||
j["errors"]["communication"]["last"][n]["priority"] =
|
||||
k.source[(uint8_t)ast_app_layer::ErrorSource::COMMUNICATION]
|
||||
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST]
|
||||
.error[n]
|
||||
.priority;
|
||||
j["errors"]["communication"]["last"][n]["counter"] =
|
||||
k.source[(uint8_t)ast_app_layer::ErrorSource::COMMUNICATION]
|
||||
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST]
|
||||
.error[n]
|
||||
.counter;
|
||||
}
|
||||
j["errors"]["communication"]["last_critical"] = json::array();
|
||||
for (uint8_t n = 0; n < 5; n++) {
|
||||
j["errors"]["communication"]["last_critical"][n]["id"] =
|
||||
k.source[(uint8_t)ast_app_layer::ErrorSource::COMMUNICATION]
|
||||
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST_CRITICAL]
|
||||
.error[n]
|
||||
.id;
|
||||
j["errors"]["communication"]["last_critical"][n]["priority"] =
|
||||
k.source[(uint8_t)ast_app_layer::ErrorSource::COMMUNICATION]
|
||||
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST_CRITICAL]
|
||||
.error[n]
|
||||
.priority;
|
||||
j["errors"]["communication"]["last_critical"][n]["counter"] =
|
||||
k.source[(uint8_t)ast_app_layer::ErrorSource::COMMUNICATION]
|
||||
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST_CRITICAL]
|
||||
.error[n]
|
||||
.counter;
|
||||
}
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const Logging& k) {
|
||||
os << json(k).dump(4);
|
||||
return os;
|
||||
}
|
||||
|
||||
namespace conversions {
|
||||
|
||||
std::string hexdump(const std::vector<std::uint8_t>& msg) {
|
||||
std::stringstream ss;
|
||||
for (auto index : msg) {
|
||||
ss << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << (int)index << " ";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string hexdump(const std::vector<std::uint8_t>& msg, std::uint8_t start, std::uint8_t number_of_chars) {
|
||||
if ((start + number_of_chars) > msg.size())
|
||||
return std::string{};
|
||||
std::stringstream ss;
|
||||
for (std::uint8_t n = start; n < (start + number_of_chars); n++) {
|
||||
ss << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << (int)msg.at(n);
|
||||
if (n < (start + number_of_chars - 1))
|
||||
ss << " ";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string hexdump(std::uint8_t msg) {
|
||||
std::stringstream ss;
|
||||
ss << "0x" << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << (int)msg;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string hexdump(std::uint16_t msg) {
|
||||
std::stringstream ss;
|
||||
ss << "0x" << std::hex << std::uppercase << std::setw(4) << std::setfill('0') << (uint16_t)msg;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string get_string(const std::vector<std::uint8_t>& vec) {
|
||||
std::string str;
|
||||
for (std::uint16_t n = 0; n < vec.size(); n++) {
|
||||
if ((vec[n] < ' ') || (vec[n] > '~')) {
|
||||
str += " ";
|
||||
} else {
|
||||
str += vec[n];
|
||||
}
|
||||
}
|
||||
return std::move(str);
|
||||
}
|
||||
|
||||
std::string u32_epoch_to_rfc3339(std::uint32_t epoch_time) {
|
||||
auto tt = static_cast<time_t>(epoch_time);
|
||||
std::tm tm = *std::gmtime(&tt);
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S.000Z");
|
||||
return std::move(ss.str());
|
||||
}
|
||||
|
||||
} // namespace conversions
|
||||
} // namespace module
|
||||
@@ -0,0 +1,143 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef DIAGNOSTICS_HPP
|
||||
#define DIAGNOSTICS_HPP
|
||||
|
||||
#include "ast_app_layer.hpp"
|
||||
#include <bitset>
|
||||
#include <cstdint>
|
||||
#include <date/date.h>
|
||||
#include <date/tz.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace module {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct OcmfStats {
|
||||
std::uint32_t number_transactions{};
|
||||
std::uint32_t timestamp_first_transaction{};
|
||||
std::uint32_t timestamp_last_transaction{};
|
||||
std::uint32_t max_number_of_transactions{};
|
||||
};
|
||||
|
||||
struct OcmfInfo {
|
||||
std::string gateway_id;
|
||||
std::string manufacturer;
|
||||
std::string model;
|
||||
};
|
||||
|
||||
struct DeviceData {
|
||||
std::uint32_t utc_time_s{};
|
||||
std::uint8_t gmt_offset_quarterhours{};
|
||||
std::uint64_t total_start_import_energy_Wh{}; // meter value needs to be divided by 10 for Wh
|
||||
std::uint64_t total_stop_import_energy_Wh{}; // meter value needs to be divided by 10 for Wh
|
||||
std::uint64_t total_start_export_energy_Wh{}; // meter value needs to be divided by 10 for Wh
|
||||
std::uint64_t total_stop_export_energy_Wh{}; // meter value needs to be divided by 10 for Wh
|
||||
std::uint32_t total_transaction_duration_s{}; // must be less than 27 days in total
|
||||
OcmfStats ocmf_stats;
|
||||
std::string last_ocmf_transaction;
|
||||
std::string requested_ocmf;
|
||||
OcmfInfo ocmf_info;
|
||||
std::uint64_t total_dev_import_energy_Wh{}; // meter value needs to be divided by 10 for Wh
|
||||
std::uint64_t total_dev_export_energy_Wh{}; // meter value needs to be divided by 10 for Wh
|
||||
std::uint64_t ab_status{};
|
||||
};
|
||||
|
||||
void to_json(json& j, const DeviceData& k);
|
||||
std::ostream& operator<<(std::ostream& os, const DeviceData& k);
|
||||
|
||||
struct LogStats {
|
||||
std::uint32_t number_log_entries{};
|
||||
std::uint32_t timestamp_first_log{};
|
||||
std::uint32_t timestamp_last_log{};
|
||||
std::uint32_t max_number_of_logs{}; // ???
|
||||
};
|
||||
|
||||
struct ApplicationBoardInfo {
|
||||
std::string type;
|
||||
std::string hw_ver;
|
||||
std::string server_id;
|
||||
std::uint8_t mode{};
|
||||
std::uint32_t serial_number{};
|
||||
std::string sw_ver;
|
||||
std::uint16_t fw_crc{};
|
||||
std::uint16_t fw_hash{};
|
||||
};
|
||||
|
||||
struct MeteringBoardInfo {
|
||||
std::string hw_ver;
|
||||
std::string sw_ver;
|
||||
std::uint16_t fw_crc{};
|
||||
};
|
||||
|
||||
struct DeviceDiagnostics {
|
||||
std::string charge_point_id;
|
||||
std::uint8_t charge_point_id_type{0};
|
||||
LogStats log_stats;
|
||||
ApplicationBoardInfo app_board;
|
||||
MeteringBoardInfo m_board;
|
||||
std::string pubkey_asn1;
|
||||
std::string pubkey_str16;
|
||||
std::string pubkey;
|
||||
std::uint8_t pubkey_str16_format{}; // 0x04 for uncompressed string
|
||||
std::uint8_t pubkey_format{}; // 0x04 for uncompressed string
|
||||
std::vector<std::uint8_t> ocmf_config_table;
|
||||
};
|
||||
|
||||
void to_json(json& j, const DeviceDiagnostics& k);
|
||||
std::ostream& operator<<(std::ostream& os, const DeviceDiagnostics& k);
|
||||
|
||||
// TODO(LAD): add error data
|
||||
|
||||
struct ErrorData {
|
||||
std::uint32_t id{0};
|
||||
std::uint16_t priority{0};
|
||||
std::uint32_t counter{0};
|
||||
};
|
||||
|
||||
struct FiveErrors {
|
||||
ErrorData error[5];
|
||||
};
|
||||
|
||||
struct ErrorSet {
|
||||
FiveErrors category[4];
|
||||
};
|
||||
|
||||
struct Logging {
|
||||
ast_app_layer::LogEntry last_log;
|
||||
ErrorSet source[2];
|
||||
};
|
||||
|
||||
void to_json(json& j, const Logging& k);
|
||||
std::ostream& operator<<(std::ostream& os, const Logging& k);
|
||||
|
||||
namespace conversions {
|
||||
|
||||
template <typename T> static std::string to_bin_string(const T& num) {
|
||||
std::stringstream ss{};
|
||||
for (uint8_t n = 0; n < sizeof(T); n++) {
|
||||
ss << std::bitset<8>(num >> ((sizeof(T) - n - 1) * 8));
|
||||
if (n % 2) {
|
||||
if (n != sizeof(T) - 1) {
|
||||
ss << " - ";
|
||||
}
|
||||
} else {
|
||||
ss << " ";
|
||||
}
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string hexdump(const std::vector<std::uint8_t>& msg);
|
||||
std::string hexdump(const std::vector<std::uint8_t>& msg, std::uint8_t start, std::uint8_t number_of_chars);
|
||||
std::string hexdump(std::uint8_t msg);
|
||||
std::string hexdump(std::uint16_t msg);
|
||||
std::string get_string(const std::vector<std::uint8_t>& vec);
|
||||
std::string u32_epoch_to_rfc3339(std::uint32_t epoch_time);
|
||||
|
||||
} // namespace conversions
|
||||
} // namespace module
|
||||
|
||||
#endif // DIAGNOSTICS_HPP
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef MAIN_POWERMETER_IMPL_HPP
|
||||
#define MAIN_POWERMETER_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
#include "../AST_DC650.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
#include "ast_app_layer.hpp"
|
||||
#include "diagnostics.hpp"
|
||||
#include "serial_device.hpp"
|
||||
#include "slip_protocol.hpp"
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
struct Conf {
|
||||
int powermeter_device_id;
|
||||
std::string serial_port;
|
||||
int baudrate;
|
||||
int parity;
|
||||
int rs485_direction_gpio;
|
||||
bool ignore_echo;
|
||||
int gmt_offset_quarter_hours;
|
||||
bool publish_device_data;
|
||||
bool publish_device_diagnostics;
|
||||
};
|
||||
|
||||
class powermeterImpl : public powermeterImplBase {
|
||||
public:
|
||||
powermeterImpl() = delete;
|
||||
powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<AST_DC650>& mod, Conf& config) :
|
||||
powermeterImplBase(ev, "main"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual types::powermeter::TransactionStartResponse
|
||||
handle_start_transaction(types::powermeter::TransactionReq& value) override;
|
||||
virtual types::powermeter::TransactionStopResponse handle_stop_transaction(std::string& transaction_id) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<AST_DC650>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
// insert your private definitions here
|
||||
|
||||
enum class MessageStatus : std::uint8_t {
|
||||
NONE = 0,
|
||||
SENT = 1,
|
||||
RECEIVED = 2
|
||||
};
|
||||
|
||||
MessageStatus start_transaction_msg_status{MessageStatus::NONE};
|
||||
ast_app_layer::CommandResult start_transact_result{};
|
||||
MessageStatus stop_transaction_msg_status{MessageStatus::NONE};
|
||||
ast_app_layer::CommandResult stop_transact_result{};
|
||||
MessageStatus get_transaction_values_msg_status{MessageStatus::NONE};
|
||||
|
||||
serial_device::SerialDevice serial_device{};
|
||||
slip_protocol::SlipProtocol slip{};
|
||||
ast_app_layer::AstAppLayer app_layer{};
|
||||
|
||||
types::powermeter::Powermeter pm_last_values;
|
||||
|
||||
DeviceData device_data_obj{};
|
||||
DeviceDiagnostics device_diagnostics_obj{};
|
||||
Logging logging_obj{};
|
||||
ast_app_layer::ErrorCategory category_requested{};
|
||||
ast_app_layer::ErrorSource source_requested{};
|
||||
std::uint8_t error_diagnostics_target{0};
|
||||
std::string last_ocmf_str;
|
||||
|
||||
void init_default_values();
|
||||
void read_powermeter_values();
|
||||
void set_device_time();
|
||||
void set_device_charge_point_id(ast_app_layer::UserIdType id_type, std::string charge_point_id);
|
||||
void read_device_data();
|
||||
void read_diagnostics_data();
|
||||
void publish_device_data_topic();
|
||||
void publish_device_diagnostics_topic();
|
||||
void publish_logging_topic();
|
||||
void get_device_public_key();
|
||||
void readRegisters();
|
||||
ast_app_layer::CommandResult process_response(const std::vector<std::uint8_t>& register_message);
|
||||
void request_device_type();
|
||||
void request_error_diagnostics(std::uint8_t addr);
|
||||
void error_diagnostics(std::uint8_t addr);
|
||||
ast_app_layer::CommandResult receive_response();
|
||||
std::string get_meter_ocmf();
|
||||
|
||||
static constexpr auto TIMEOUT_2s{std::chrono::seconds(2)};
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
|
||||
#endif // MAIN_POWERMETER_IMPL_HPP
|
||||
@@ -0,0 +1,60 @@
|
||||
description: Module that collects power and energy measurements from an AST powermeter
|
||||
provides:
|
||||
main:
|
||||
description: Implementation of the driver functionality
|
||||
interface: powermeter
|
||||
config:
|
||||
powermeter_device_id:
|
||||
description: The powermeter's address on the serial bus
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 255
|
||||
default: 1
|
||||
serial_port:
|
||||
description: Serial port the hardware is connected to
|
||||
type: string
|
||||
default: /dev/ttyUSB0
|
||||
baudrate:
|
||||
description: Baudrate
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 230400
|
||||
default: 9600
|
||||
parity:
|
||||
description: 'Parity bit: 0: None, 1: Odd, 2: Even'
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 2
|
||||
default: 0
|
||||
rs485_direction_gpio:
|
||||
description: GPIO to use for direction switching. Set to -1 to disable.
|
||||
type: integer
|
||||
default: -1
|
||||
ignore_echo:
|
||||
description: On some hardware every message that is sent is read back, this setting filters the sent message in the reply.
|
||||
type: boolean
|
||||
default: false
|
||||
gmt_offset_quarter_hours:
|
||||
description: GMT offset in quarters of an hour, e.g. 8 = +2h
|
||||
type: integer
|
||||
minimum: -47
|
||||
maximum: 47
|
||||
default: 0
|
||||
publish_device_data:
|
||||
description: Regularly publish device data not covered by the "powermeter" interface (for debugging purposes).
|
||||
type: boolean
|
||||
default: false
|
||||
publish_device_diagnostics:
|
||||
description: Regularly publish device diagnostics (e.g. log messages, errors, SW/HW revisions; for debugging purposes).
|
||||
type: boolean
|
||||
default: false
|
||||
enable_external_mqtt: true
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Lars Dieckmann
|
||||
- Andreas Heinrich
|
||||
- Cornelius Claussen
|
||||
- Florin Mihut
|
||||
- Jan Christoph Habig
|
||||
- Kai-Uwe Hermann
|
||||
@@ -0,0 +1,190 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "serial_device.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <endian.h>
|
||||
#include <everest/logging.hpp>
|
||||
#include <fcntl.h>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/select.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
namespace {
|
||||
std::string hexdump(std::uint8_t* msg, int msg_len) {
|
||||
std::stringstream ss;
|
||||
for (int i = 0; i < msg_len; i++) {
|
||||
ss << std::hex << static_cast<int>(msg[i]) << " ";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string hexdump(std::vector<std::uint8_t> msg) {
|
||||
std::stringstream ss;
|
||||
|
||||
for (auto index : msg) {
|
||||
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(index) << " ";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace serial_device {
|
||||
|
||||
SerialDevice::~SerialDevice() {
|
||||
if (fd != 0) {
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
bool SerialDevice::open_device(const std::string& device, int _baud, bool _ignore_echo) {
|
||||
|
||||
ignore_echo = _ignore_echo;
|
||||
|
||||
fd = open(device.c_str(), O_RDWR | O_NOCTTY | O_SYNC);
|
||||
if (fd < 0) {
|
||||
EVLOG_error << fmt::format("Serial: error {} opening {}: {}", errno, device, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int baud;
|
||||
switch (_baud) {
|
||||
case 9600:
|
||||
baud = B9600;
|
||||
break;
|
||||
case 19200:
|
||||
baud = B19200;
|
||||
break;
|
||||
case 38400:
|
||||
baud = B38400;
|
||||
break;
|
||||
case 57600:
|
||||
baud = B57600;
|
||||
break;
|
||||
case 115200:
|
||||
baud = B115200;
|
||||
break;
|
||||
case 230400:
|
||||
baud = B230400;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
struct termios tty;
|
||||
if (tcgetattr(fd, &tty) != 0) {
|
||||
printf("Serial: error %d from tcgetattr\n", errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
cfsetospeed(&tty, baud);
|
||||
cfsetispeed(&tty, baud);
|
||||
|
||||
tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8; // 8-bit chars
|
||||
// disable IGNBRK for mismatched speed tests; otherwise receive break
|
||||
// as \000 chars
|
||||
tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXOFF | IXANY);
|
||||
tty.c_lflag = 0; // no signaling chars, no echo,
|
||||
// no canonical processing
|
||||
tty.c_oflag = 0; // no remapping, no delays
|
||||
tty.c_cc[VMIN] = 1; // read blocks
|
||||
tty.c_cc[VTIME] = 1; // 0.1 seconds inter character read timeout after first byte was received
|
||||
|
||||
tty.c_cflag |= (CLOCAL | CREAD); // ignore modem controls,
|
||||
// enable reading
|
||||
tty.c_cflag &= ~(PARENB | PARODD); // shut off parity
|
||||
tty.c_cflag &= ~CSTOPB; // 1 Stop bit
|
||||
tty.c_cflag &= ~CRTSCTS;
|
||||
|
||||
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
|
||||
EVLOG_error << "Serial: error from tcsetattr: " << errno;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
This function receives a byte array.
|
||||
*/
|
||||
int SerialDevice::rx(std::vector<std::uint8_t>& rxbuf, std::optional<int> initial_timeout_ms,
|
||||
std::optional<int> in_msg_timeout_ms) {
|
||||
std::scoped_lock lock(serial_mutex);
|
||||
int _initial_timeout = SERIAL_RX_INITIAL_TIMEOUT_MS;
|
||||
if (initial_timeout_ms.has_value()) {
|
||||
_initial_timeout = initial_timeout_ms.value();
|
||||
}
|
||||
int _in_msg_timeout = SERIAL_RX_WITHIN_MESSAGE_TIMEOUT_MS;
|
||||
if (in_msg_timeout_ms.has_value()) {
|
||||
_in_msg_timeout = in_msg_timeout_ms.value();
|
||||
}
|
||||
struct timeval timeout;
|
||||
timeout.tv_sec = 0;
|
||||
timeout.tv_usec = _initial_timeout * 1000; // intial timeout until device responds
|
||||
fd_set set;
|
||||
FD_ZERO(&set);
|
||||
FD_SET(fd, &set);
|
||||
|
||||
int bytes_read_total = 0;
|
||||
while (true) {
|
||||
int rv = select(fd + 1, &set, NULL, NULL, &timeout);
|
||||
timeout.tv_usec = _in_msg_timeout * 1000; // reduce timeout after first chunk,
|
||||
// no uneccesary waiting at the end of the message
|
||||
if (rv == -1) { // error in select function call
|
||||
perror("rx: select:");
|
||||
break;
|
||||
} else if (rv == 0) { // no more bytes to read within timeout, so transfer is complete
|
||||
EVLOG_debug << "No more bytes to read within timeout. (rv == 0)";
|
||||
break;
|
||||
} else { // received more bytes, add them to buffer
|
||||
// do we have space in the rx buffer left?
|
||||
if (bytes_read_total >= rxbuf.capacity()) {
|
||||
// no buffer space left, but more to read.
|
||||
EVLOG_info
|
||||
<< R"(No buffer space left, but more to read. (Did you mean to set "ignore_echo" to "false"?))";
|
||||
break;
|
||||
}
|
||||
|
||||
rxbuf.resize(rxbuf.capacity());
|
||||
int bytes_read = read(fd, static_cast<std::uint8_t*>(&rxbuf[0] + bytes_read_total),
|
||||
static_cast<size_t>(rxbuf.capacity() - bytes_read_total));
|
||||
|
||||
if (bytes_read > 0) {
|
||||
bytes_read_total += bytes_read;
|
||||
rxbuf.resize(bytes_read_total);
|
||||
} else if (bytes_read < 0) {
|
||||
EVLOG_error << "Error reading from device: " << strerror(errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bytes_read_total;
|
||||
}
|
||||
|
||||
/*
|
||||
This function transmits a byte vector.
|
||||
*/
|
||||
void SerialDevice::tx(const std::vector<std::uint8_t>& request) {
|
||||
{
|
||||
std::scoped_lock lock(serial_mutex);
|
||||
// clear input and output buffer
|
||||
tcflush(fd, TCIOFLUSH);
|
||||
|
||||
// write to serial port
|
||||
write(fd, request.data(), request.size());
|
||||
tcdrain(fd);
|
||||
}
|
||||
if (ignore_echo) {
|
||||
// read back echo of what we sent and ignore it
|
||||
std::vector<std::uint8_t> req_buf{};
|
||||
req_buf.reserve(request.size() + 1);
|
||||
rx(req_buf, std::nullopt, std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace serial_device
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/*
|
||||
This is an implementation for custom serial communications
|
||||
*/
|
||||
#ifndef SERIAL_DEVICE
|
||||
#define SERIAL_DEVICE
|
||||
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace serial_device {
|
||||
|
||||
constexpr int SERIAL_RX_INITIAL_TIMEOUT_MS = 500;
|
||||
constexpr int SERIAL_RX_WITHIN_MESSAGE_TIMEOUT_MS = 100;
|
||||
|
||||
class SerialDevice {
|
||||
|
||||
public:
|
||||
SerialDevice() = default;
|
||||
SerialDevice(const SerialDevice&) = delete;
|
||||
SerialDevice(SerialDevice&&) = delete;
|
||||
SerialDevice& operator=(const SerialDevice&) = delete;
|
||||
SerialDevice& operator=(SerialDevice&&) = delete;
|
||||
~SerialDevice();
|
||||
|
||||
bool open_device(const std::string& device, int baud, bool ignore_echo);
|
||||
void tx(const std::vector<std::uint8_t>& request);
|
||||
int rx(std::vector<std::uint8_t>& rxbuf, std::optional<int> initial_timeout_ms,
|
||||
std::optional<int> in_msg_timeout_ms);
|
||||
|
||||
private:
|
||||
// Serial interface
|
||||
int fd{0};
|
||||
bool ignore_echo{false};
|
||||
|
||||
std::mutex serial_mutex;
|
||||
};
|
||||
|
||||
} // namespace serial_device
|
||||
#endif // SERIAL_DEVICE
|
||||
@@ -0,0 +1,218 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "slip_protocol.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <endian.h>
|
||||
#include <everest/crc/crc.hpp>
|
||||
#include <everest/logging.hpp>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/select.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
namespace {
|
||||
inline void remove_start_and_stop_frame(std::vector<std::uint8_t>& vec) {
|
||||
vec.erase(vec.begin());
|
||||
vec.pop_back();
|
||||
}
|
||||
|
||||
// TODO: this is a duplicate of the implementation in DZG_GSH01, refactor into common library
|
||||
inline bool is_message_crc_correct(std::vector<std::uint8_t>& vec) {
|
||||
if (vec.size() < 2) {
|
||||
return false;
|
||||
}
|
||||
const auto crc_check = static_cast<std::uint16_t>((vec.at(vec.size() - 1) << 8) | vec.at(vec.size() - 2));
|
||||
// remove CRC tail from vector
|
||||
for (std::uint8_t i = 0; i < 2; i++) {
|
||||
vec.pop_back();
|
||||
}
|
||||
const std::uint16_t crc_calc = calculate_xModem_crc16(vec);
|
||||
return crc_check == crc_calc;
|
||||
}
|
||||
|
||||
inline void restore_special_characters(std::vector<std::uint8_t>& vec) {
|
||||
for (std::uint16_t j = 0; j < (vec.size() - 1);
|
||||
j++) { // can only go to vec.size() - 1 because two bytes will be checked
|
||||
if ((vec.at(j) == 0xDB) && (vec.at(j + 1) == 0xDC)) {
|
||||
vec.at(j) = 0xC0;
|
||||
vec.erase(vec.begin() + j + 1);
|
||||
} else if ((vec.at(j) == 0xDB) && (vec.at(j + 1) == 0xDD)) {
|
||||
vec.at(j) = 0xDB;
|
||||
vec.erase(vec.begin() + j + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace slip_protocol {
|
||||
|
||||
std::vector<std::uint8_t> SlipProtocol::package_single(std::uint8_t address, const std::vector<std::uint8_t>& payload) {
|
||||
std::vector<std::uint8_t> vec{};
|
||||
|
||||
// address
|
||||
vec.push_back(address);
|
||||
|
||||
// payload
|
||||
for (auto payload_byte : payload) {
|
||||
if (payload_byte == 0xC0) { // check for and replace special char 0xC0
|
||||
vec.push_back(0xDB);
|
||||
vec.push_back(0xDC);
|
||||
} else if (payload_byte == 0xDB) { // check for and replace special char 0xDB
|
||||
vec.push_back(0xDB);
|
||||
vec.push_back(0xDD);
|
||||
} else { // otherwise just use normal input
|
||||
vec.push_back(payload_byte);
|
||||
}
|
||||
}
|
||||
|
||||
// CRC16
|
||||
std::uint16_t crc = calculate_xModem_crc16(vec);
|
||||
vec.push_back(static_cast<std::uint8_t>(crc & 0x00FF)); // LSB CRC16
|
||||
vec.push_back(static_cast<std::uint8_t>((crc >> 8) & 0x00FF)); // MSB CRC16
|
||||
|
||||
// add start frame to front (can be done only after CRC has been calculated, as start frame is not part of CRC)
|
||||
vec.insert(vec.begin(), SLIP_START_END_FRAME);
|
||||
|
||||
// end frame
|
||||
vec.push_back(SLIP_START_END_FRAME);
|
||||
|
||||
return vec;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> SlipProtocol::package_multi(const std::uint8_t address,
|
||||
const std::vector<std::vector<std::uint8_t>>& multi_payload) {
|
||||
std::vector<std::uint8_t> payload{};
|
||||
|
||||
// concatenate requests
|
||||
for (auto multi_payload_part : multi_payload) {
|
||||
payload.insert(payload.end(), multi_payload_part.begin(), multi_payload_part.end());
|
||||
}
|
||||
|
||||
// ...and return as one long request
|
||||
return package_single(address, payload);
|
||||
}
|
||||
|
||||
SlipReturnStatus SlipProtocol::unpack(std::vector<std::uint8_t>& message, std::uint8_t listen_to_address) {
|
||||
SlipReturnStatus retval = SlipReturnStatus::SLIP_ERROR_UNINITIALIZED;
|
||||
std::uint8_t message_start_end_frame_counter = 0;
|
||||
|
||||
if (message.size() < 1) {
|
||||
return SlipReturnStatus::SLIP_ERROR_SIZE_ERROR;
|
||||
}
|
||||
|
||||
// check if first element is SLIP_START_END_FRAME and if not, drop first item(s) until start frame is first
|
||||
std::uint8_t i = 0;
|
||||
while (i < message.size()) {
|
||||
if (message.at(i) == SLIP_START_END_FRAME)
|
||||
break;
|
||||
i++;
|
||||
}
|
||||
if (i > 0) {
|
||||
std::uint8_t j = 0;
|
||||
while (message.size() > 0) {
|
||||
if (j == i)
|
||||
break;
|
||||
message.erase(message.begin());
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
// count number of SLIP_START_END_FRAMEs to check for multiple (or broken) messages
|
||||
for (std::uint16_t n = 0; n < message.size(); n++) {
|
||||
if (message.at(n) == SLIP_START_END_FRAME) {
|
||||
message_start_end_frame_counter++;
|
||||
}
|
||||
}
|
||||
|
||||
if (message_start_end_frame_counter != 2) { // unexpected/broken message -OR- multiple messages received
|
||||
|
||||
// split message vector into sub-vectors by delimiter (SLIP_START_END_FRAME)
|
||||
std::vector<std::vector<std::uint8_t>> sub_messages{};
|
||||
std::vector<std::uint8_t> current_sub_message{};
|
||||
for (std::uint16_t n = 0; n < message.size(); n++) {
|
||||
if (message.at(n) == SLIP_START_END_FRAME) {
|
||||
if (current_sub_message.size() > 0) {
|
||||
if (listen_to_address == 0xFF) {
|
||||
// listen_to_address is broadcast address: listen to all messages
|
||||
sub_messages.push_back(current_sub_message);
|
||||
} else {
|
||||
// dedicated client address selected, only listen to this address
|
||||
if (current_sub_message.at(0) == listen_to_address) {
|
||||
sub_messages.push_back(current_sub_message);
|
||||
}
|
||||
}
|
||||
current_sub_message.clear();
|
||||
} else {
|
||||
// intentionally do nothing on empty sub_message
|
||||
}
|
||||
} else {
|
||||
current_sub_message.push_back(message.at(n));
|
||||
}
|
||||
}
|
||||
|
||||
// from here on, we have all message parts as elements in sub_messages
|
||||
for (auto sub_message : sub_messages) {
|
||||
restore_special_characters(sub_message);
|
||||
// check all sub-messages' CRC and only process on match
|
||||
if (is_message_crc_correct(sub_message)) {
|
||||
// on correct CRC
|
||||
message_queue.push_back(sub_message);
|
||||
message_counter++;
|
||||
if (retval == SlipReturnStatus::SLIP_ERROR_UNINITIALIZED) { // only set SLIP_OK if no other error
|
||||
retval = SlipReturnStatus::SLIP_OK;
|
||||
}
|
||||
} else {
|
||||
retval = SlipReturnStatus::SLIP_ERROR_MALFORMED;
|
||||
EVLOG_error << "Malformed message received!";
|
||||
}
|
||||
}
|
||||
} else { // single message received
|
||||
|
||||
if (message.size() > 3) {
|
||||
// only process messages for correct client or if listen_to_address is broadcast address
|
||||
if ((message.at(1) == listen_to_address) || (0xFF == listen_to_address)) {
|
||||
// check if last element is SLIP_START_END_FRAME and if not, reduce message size
|
||||
while (message.at(message.size() - 1) != SLIP_START_END_FRAME) {
|
||||
if (message.size() <= 3)
|
||||
break;
|
||||
message.pop_back();
|
||||
}
|
||||
|
||||
remove_start_and_stop_frame(message);
|
||||
restore_special_characters(message);
|
||||
if (is_message_crc_correct(message)) {
|
||||
// message intact, check for special characters and restore to original contents
|
||||
message_queue.push_back(message);
|
||||
message_counter++;
|
||||
retval = SlipReturnStatus::SLIP_OK;
|
||||
} else {
|
||||
retval = SlipReturnStatus::SLIP_ERROR_CRC_MISMATCH;
|
||||
EVLOG_error << "CRC mismatch!";
|
||||
}
|
||||
} else {
|
||||
retval = SlipReturnStatus::SLIP_OK;
|
||||
}
|
||||
} else {
|
||||
retval = SlipReturnStatus::SLIP_ERROR_SIZE_ERROR;
|
||||
EVLOG_error << "Message broken: too short!";
|
||||
}
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> SlipProtocol::retrieve_single_message() {
|
||||
if (message_queue.size() > 0) {
|
||||
message_counter--;
|
||||
auto ret_vec = message_queue.back();
|
||||
message_queue.pop_back();
|
||||
return ret_vec;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace slip_protocol
|
||||
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/*
|
||||
This is an implementation for the SLIP serial protocol
|
||||
*/
|
||||
#ifndef SLIP_PROTOCOL
|
||||
#define SLIP_PROTOCOL
|
||||
|
||||
#include <cstdint>
|
||||
#include <everest/crc/crc.hpp>
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
namespace slip_protocol {
|
||||
|
||||
constexpr int SLIP_START_END_FRAME = 0xC0;
|
||||
constexpr int SLIP_BROADCAST_ADDR = 0xFF;
|
||||
|
||||
constexpr std::uint16_t SLIP_SIZE_ON_ERROR = 1;
|
||||
|
||||
constexpr std::int8_t SLIP_ERROR_SIZE_ERROR = -1;
|
||||
constexpr std::int8_t SLIP_ERROR_MALFORMED = -2;
|
||||
constexpr std::int8_t SLIP_ERROR_CRC_MISMATCH = -3;
|
||||
|
||||
enum class SlipReturnStatus : std::int8_t {
|
||||
SLIP_ERROR_CRC_MISMATCH = -3,
|
||||
SLIP_ERROR_MALFORMED = -2,
|
||||
SLIP_ERROR_SIZE_ERROR = -1,
|
||||
SLIP_OK = 0,
|
||||
SLIP_ERROR_UNINITIALIZED = 1
|
||||
};
|
||||
|
||||
class SlipProtocol {
|
||||
|
||||
public:
|
||||
std::vector<std::uint8_t> package_single(std::uint8_t address, const std::vector<std::uint8_t>& payload);
|
||||
std::vector<std::uint8_t> package_multi(std::uint8_t address,
|
||||
const std::vector<std::vector<std::uint8_t>>& multi_payload);
|
||||
|
||||
SlipReturnStatus unpack(std::vector<std::uint8_t>& message, std::uint8_t listen_to_address);
|
||||
auto get_message_counter() const {
|
||||
return message_counter;
|
||||
}
|
||||
std::vector<std::uint8_t> retrieve_single_message();
|
||||
|
||||
private:
|
||||
std::vector<std::vector<std::uint8_t>> message_queue;
|
||||
std::uint8_t message_counter{0};
|
||||
};
|
||||
|
||||
} // namespace slip_protocol
|
||||
#endif // SLIP_PROTOCOL
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "Acrel_DJSF1352_RN.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
void Acrel_DJSF1352_RN::init() {
|
||||
invoke_init(*p_main);
|
||||
}
|
||||
|
||||
void Acrel_DJSF1352_RN::ready() {
|
||||
invoke_ready(*p_main);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef ACREL_DJSF1352_RN_HPP
|
||||
#define ACREL_DJSF1352_RN_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
// headers for required interface implementations
|
||||
#include <generated/interfaces/serial_communication_hub/Interface.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
// insert your custom include headers here
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class Acrel_DJSF1352_RN : public Everest::ModuleBase {
|
||||
public:
|
||||
Acrel_DJSF1352_RN() = delete;
|
||||
Acrel_DJSF1352_RN(const ModuleInfo& info, std::unique_ptr<powermeterImplBase> p_main,
|
||||
std::unique_ptr<serial_communication_hubIntf> r_serial_comm_hub, Conf& config) :
|
||||
ModuleBase(info), p_main(std::move(p_main)), r_serial_comm_hub(std::move(r_serial_comm_hub)), config(config){};
|
||||
|
||||
const std::unique_ptr<powermeterImplBase> p_main;
|
||||
const std::unique_ptr<serial_communication_hubIntf> r_serial_comm_hub;
|
||||
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
|
||||
// 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 // ACREL_DJSF1352_RN_HPP
|
||||
@@ -0,0 +1,21 @@
|
||||
#
|
||||
# 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
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"main/powermeterImpl.cpp"
|
||||
)
|
||||
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
# insert other things like install cmds etc here
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
@@ -0,0 +1,116 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "powermeterImpl.hpp"
|
||||
#include <fmt/core.h>
|
||||
#include <thread>
|
||||
#include <utils/date.hpp>
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
void powermeterImpl::init() {
|
||||
this->init_default_values();
|
||||
}
|
||||
|
||||
void powermeterImpl::ready() {
|
||||
std::thread t([this] {
|
||||
while (true) {
|
||||
read_powermeter_values();
|
||||
sleep(1);
|
||||
}
|
||||
});
|
||||
t.detach();
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStartResponse
|
||||
powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& value) {
|
||||
types::powermeter::TransactionStartResponse r;
|
||||
r.status = types::powermeter::TransactionRequestStatus::NOT_SUPPORTED;
|
||||
return r;
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transaction(std::string& transaction_id) {
|
||||
types::powermeter::TransactionStopResponse r;
|
||||
r.status = types::powermeter::TransactionRequestStatus::NOT_SUPPORTED;
|
||||
return r;
|
||||
}
|
||||
|
||||
void powermeterImpl::init_default_values() {
|
||||
this->pm_last_values.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
|
||||
this->pm_last_values.meter_id = std::string(this->mod->info.id);
|
||||
|
||||
this->pm_last_values.energy_Wh_import.total = 0.0f;
|
||||
|
||||
types::units::Power pwr;
|
||||
pwr.total = 0.0f;
|
||||
this->pm_last_values.power_W = pwr;
|
||||
|
||||
types::units::Voltage volt;
|
||||
volt.DC = 0.0f;
|
||||
this->pm_last_values.voltage_V = volt;
|
||||
|
||||
types::units::Current amp;
|
||||
amp.DC = 0.0f;
|
||||
this->pm_last_values.current_A = amp;
|
||||
}
|
||||
|
||||
void powermeterImpl::read_powermeter_values() {
|
||||
// read power data
|
||||
auto power_data_response =
|
||||
mod->r_serial_comm_hub->call_modbus_read_holding_registers(config.powermeter_device_id, 0x0000, 20);
|
||||
process_power_data_message(power_data_response);
|
||||
|
||||
this->pm_last_values.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
|
||||
|
||||
this->publish_powermeter(this->pm_last_values);
|
||||
}
|
||||
|
||||
void powermeterImpl::process_power_data_message(const types::serial_comm_hub_requests::Result message) {
|
||||
if (message.status_code == types::serial_comm_hub_requests::StatusCodeEnum::Success) {
|
||||
types::units::Voltage volt;
|
||||
volt.DC = message.value.value()[DC_VOLTAGE] * pow(10.0, (message.value.value()[DC_VOLT_DECIMAL_POINT] - 3));
|
||||
this->pm_last_values.voltage_V = volt;
|
||||
|
||||
types::units::Current amp;
|
||||
amp.DC = (int16_t)(message.value.value()[DC_CURRENT]) *
|
||||
pow(10.0, (message.value.value()[DC_CURR_DECIMAL_POINT] - 3));
|
||||
this->pm_last_values.current_A = amp;
|
||||
|
||||
types::units::Power power;
|
||||
power.total =
|
||||
(int16_t)(message.value.value()[POWER]) * pow(10.0, (message.value.value()[POWER_DECIMAL_POINT] - 3));
|
||||
this->pm_last_values.power_W = power;
|
||||
|
||||
types::units::Energy energy_in;
|
||||
energy_in.total = float(uint32_t(message.value.value()[TOTAL_POS_ACT_ENERGY_HIGH] << 16) |
|
||||
uint32_t(message.value.value()[TOTAL_POS_ACT_ENERGY_LOW]));
|
||||
this->pm_last_values.energy_Wh_import = energy_in;
|
||||
|
||||
types::units::Energy energy_out;
|
||||
energy_out.total = float(uint32_t(message.value.value()[TOTAL_REV_ACT_ENERGY_HIGH] << 16) |
|
||||
uint32_t(message.value.value()[TOTAL_REV_ACT_ENERGY_LOW]));
|
||||
this->pm_last_values.energy_Wh_export = energy_out;
|
||||
} else {
|
||||
// error: message sending failed
|
||||
output_error_with_content(message);
|
||||
}
|
||||
}
|
||||
|
||||
void powermeterImpl::output_error_with_content(const types::serial_comm_hub_requests::Result& response) {
|
||||
std::stringstream ss;
|
||||
|
||||
if (response.value.has_value()) {
|
||||
for (size_t i = 0; i < response.value.value().size(); i++) {
|
||||
if (i != 0)
|
||||
ss << ", ";
|
||||
ss << "0x" << std::setfill('0') << std::setw(2) << std::hex << int(response.value.value()[i]);
|
||||
}
|
||||
}
|
||||
|
||||
EVLOG_debug << "received error response: " << status_code_enum_to_string(response.status_code) << " (" << ss.str()
|
||||
<< ")\n";
|
||||
}
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
@@ -0,0 +1,103 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef MAIN_POWERMETER_IMPL_HPP
|
||||
#define MAIN_POWERMETER_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
#include "../Acrel_DJSF1352_RN.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
struct Conf {
|
||||
int powermeter_device_id;
|
||||
};
|
||||
|
||||
class powermeterImpl : public powermeterImplBase {
|
||||
public:
|
||||
powermeterImpl() = delete;
|
||||
powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<Acrel_DJSF1352_RN>& mod, Conf& config) :
|
||||
powermeterImplBase(ev, "main"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual types::powermeter::TransactionStartResponse
|
||||
handle_start_transaction(types::powermeter::TransactionReq& value) override;
|
||||
virtual types::powermeter::TransactionStopResponse handle_stop_transaction(std::string& transaction_id) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<Acrel_DJSF1352_RN>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
|
||||
enum PmRegisters {
|
||||
DC_VOLTAGE,
|
||||
DC_VOLT_DECIMAL_POINT,
|
||||
DC_CURRENT,
|
||||
DC_CURR_DECIMAL_POINT,
|
||||
BROKEN_WIRE_DETECT,
|
||||
INTERNAL_TEMP,
|
||||
RESERVED_1,
|
||||
RESERVED_2,
|
||||
POWER,
|
||||
POWER_DECIMAL_POINT,
|
||||
RESERVED_3,
|
||||
RESERVED_4,
|
||||
TOTAL_POS_ACT_ENERGY_HIGH,
|
||||
TOTAL_POS_ACT_ENERGY_LOW,
|
||||
TOTAL_REV_ACT_ENERGY_HIGH,
|
||||
TOTAL_REV_ACT_ENERGY_LOW,
|
||||
VOLTAGE_TRANSFORM_RATIO,
|
||||
PRIMARY_RATED_CURRENT,
|
||||
SWITCH_IO_STATUS,
|
||||
ALARM_STATUS
|
||||
};
|
||||
|
||||
enum CurrRateRegisters {
|
||||
SHARP,
|
||||
PEAK,
|
||||
SHOULDER,
|
||||
OFF_PEAK
|
||||
};
|
||||
|
||||
types::powermeter::Powermeter pm_last_values;
|
||||
|
||||
std::thread output_thread;
|
||||
|
||||
void init_default_values();
|
||||
void read_powermeter_values();
|
||||
void process_power_data_message(const types::serial_comm_hub_requests::Result message);
|
||||
void output_error_with_content(const types::serial_comm_hub_requests::Result& response);
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
|
||||
#endif // MAIN_POWERMETER_IMPL_HPP
|
||||
@@ -0,0 +1,24 @@
|
||||
description: Driver module for powermeters of manufacturer Acrel. Supports DJSF1352.
|
||||
provides:
|
||||
main:
|
||||
description: Provides an Acrel DC powermeter
|
||||
interface: powermeter
|
||||
config:
|
||||
powermeter_device_id:
|
||||
description: The powermeter's address on the serial bus
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 255
|
||||
default: 1
|
||||
requires:
|
||||
serial_comm_hub:
|
||||
interface: serial_communication_hub
|
||||
min_connections: 1
|
||||
max_connections: 1
|
||||
enable_external_mqtt: false
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Lars Dieckmann
|
||||
- Jan Christoph Habig
|
||||
- Florin Mihut
|
||||
@@ -0,0 +1,11 @@
|
||||
ev_add_module(Acrel_DJSF1352_RN)
|
||||
ev_add_module(AST_DC650)
|
||||
ev_add_module(DZG_GSH01)
|
||||
ev_add_module(GenericPowermeter)
|
||||
ev_add_module(IsabellenhuetteIemDcr)
|
||||
ev_add_module(LemDCBM400600)
|
||||
ev_add_module(CarloGavazzi_EM580)
|
||||
|
||||
if(${EVEREST_ENABLE_RS_SUPPORT})
|
||||
ev_add_module(RsIskraMeter)
|
||||
endif()
|
||||
@@ -0,0 +1,29 @@
|
||||
#
|
||||
# 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
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"main/powermeterImpl.cpp"
|
||||
)
|
||||
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
# insert other things like install cmds etc here
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"main/transport.cpp"
|
||||
)
|
||||
|
||||
if(EVEREST_CORE_BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "CarloGavazzi_EM580.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
void CarloGavazzi_EM580::init() {
|
||||
invoke_init(*p_main);
|
||||
}
|
||||
|
||||
void CarloGavazzi_EM580::ready() {
|
||||
invoke_ready(*p_main);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef CARLO_GAVAZZI_EM580_HPP
|
||||
#define CARLO_GAVAZZI_EM580_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
// headers for required interface implementations
|
||||
#include <generated/interfaces/serial_communication_hub/Interface.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
// insert your custom include headers here
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class CarloGavazzi_EM580 : public Everest::ModuleBase {
|
||||
public:
|
||||
CarloGavazzi_EM580() = delete;
|
||||
CarloGavazzi_EM580(const ModuleInfo& info, std::unique_ptr<powermeterImplBase> p_main,
|
||||
std::unique_ptr<serial_communication_hubIntf> r_modbus, Conf& config) :
|
||||
ModuleBase(info), p_main(std::move(p_main)), r_modbus(std::move(r_modbus)), config(config){};
|
||||
|
||||
const std::unique_ptr<powermeterImplBase> p_main;
|
||||
const std::unique_ptr<serial_communication_hubIntf> r_modbus;
|
||||
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
|
||||
// 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 // CARLO_GAVAZZI_EM580_HPP
|
||||
@@ -0,0 +1,243 @@
|
||||
# Carlo Gavazzi EM580 Power Meter Driver
|
||||
|
||||
Driver module for the **Carlo Gavazzi EM580** power meter using Modbus via EVerest's `serial_communication_hub` interface.
|
||||
It implements the standardized EVerest `powermeter` interface and supports **OCMF/Eichrecht** transaction flows.
|
||||
|
||||
## Overview
|
||||
|
||||
This is an **EVerest Hardware Driver** module that:
|
||||
|
||||
- **Implements**: `powermeter` interface
|
||||
- **Communicates**: Modbus RTU (through `SerialCommHub`)
|
||||
- **Provides**: Live meter values, OCMF transaction start/stop handling, public key publishing
|
||||
|
||||
## Features
|
||||
|
||||
- **Live measurements**: Publishes `powermeter` readings periodically (`live_measurement_interval_ms`)
|
||||
- **OCMF transactions**:
|
||||
- `start_transaction`: writes OCMF identification fields + tariff text (TT) + start command
|
||||
- `stop_transaction`: ends transaction, waits for READY, reads OCMF file, confirms file read
|
||||
- **Modbus protocol compliance**: transport splits writes into chunks (max 123 registers per request)
|
||||
- **Resilience / retries**:
|
||||
- Separate initial connection retry settings vs. normal operation retry settings
|
||||
- Communication-fault raise/clear hooks
|
||||
- **Device state monitoring**: periodic read of device state bitfield (`device_state_read_interval_ms`)
|
||||
- **Signature key readout**: reads signature type and public keys; publishes public key (hex) via `public_key_ocmf`
|
||||
|
||||
## Hardware requirements & compatibility
|
||||
|
||||
### Supported devices
|
||||
|
||||
- **Carlo Gavazzi EM580** with **Modbus RTU** enabled/available.
|
||||
- **OCMF/Eichrecht flow**: requires a meter variant/firmware that supports the OCMF register set used by this driver.
|
||||
|
||||
If you are unsure which EM580 variant you have, check the device documentation/ordering code and confirm:
|
||||
- Modbus RTU via RS-485 is supported and enabled
|
||||
- The meter is configured for a known Modbus **unit id** (device address)
|
||||
|
||||
- **Carlo Gavazzi EM300** with **Modbus RTU** enabled/available.
|
||||
These models do not support OCMF/Eichrecht and can only be used as usual power meter.
|
||||
All transaction related configuration etc. does not apply for such devices.
|
||||
|
||||
|
||||
### Bus / physical layer
|
||||
|
||||
- **RS-485 (2-wire, half duplex)**: correct A/B wiring is essential.
|
||||
- **Termination**: enable 120Ω termination at the ends of the RS-485 bus (and only at the ends).
|
||||
- **Biasing**: ensure the bus has proper bias resistors (often provided by the adapter/master or by dedicated biasing).
|
||||
|
||||
### Host requirements
|
||||
|
||||
- A Linux host running EVerest with access to a serial device (e.g. `/dev/ttyUSB0`).
|
||||
- A **USB-to-RS485** adapter (or equivalent RS-485 interface) supported by the OS.
|
||||
- Permissions to access the serial device node (group membership / udev rules).
|
||||
|
||||
## Configuration
|
||||
|
||||
### Required connections
|
||||
|
||||
The module requires a `serial_communication_hub` implementation (typically `SerialCommHub`) via its `modbus` requirement.
|
||||
`SerialCommHub` encapsulates the serial port settings (port, baudrate, parity, timeouts). The EM580 module only needs the
|
||||
hub connection plus its Modbus unit id (`powermeter_device_id`).
|
||||
|
||||
### Example configuration (bringup)
|
||||
|
||||
See `config/bringup/config-bringup-CGEM580.yaml`:
|
||||
|
||||
```yaml
|
||||
active_modules:
|
||||
cgem580:
|
||||
module: CarloGavazzi_EM580
|
||||
config_implementation:
|
||||
main:
|
||||
powermeter_device_id: 1
|
||||
communication_retry_count: 3
|
||||
communication_retry_delay_ms: 500
|
||||
communication_error_pause_delay_s: 10
|
||||
initial_connection_retry_count: 10
|
||||
initial_connection_retry_delay_ms: 2000
|
||||
timezone_offset_minutes: 60
|
||||
live_measurement_interval_ms: 1000
|
||||
device_state_read_interval_ms: 10000
|
||||
connections:
|
||||
modbus:
|
||||
- module_id: comm_hub
|
||||
implementation_id: main
|
||||
```
|
||||
|
||||
### Multiple EM580 devices on one RS-485 bus
|
||||
|
||||
You can run multiple EM580 devices on the same RS-485 line by:
|
||||
- Creating multiple `CarloGavazzi_EM580` module instances
|
||||
- Pointing them all to the same `SerialCommHub`
|
||||
- Giving each instance a unique `powermeter_device_id` (Modbus unit id)
|
||||
|
||||
### Configuration parameters
|
||||
|
||||
All parameters are defined in `modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/manifest.yaml`:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `powermeter_device_id` | integer | `1` | Modbus device ID on the bus |
|
||||
| `communication_retry_count` | integer | `3` | Retries for regular Modbus operations |
|
||||
| `communication_retry_delay_ms` | integer | `500` | Delay between regular retries |
|
||||
| `communication_error_pause_delay_s` | integer | `10` | Pause after a communication failure in the live measurement thread before retrying (also applies to initial communication) |
|
||||
| `initial_connection_retry_count` | integer | `10` (0 = infinite) | Retries during initial device setup/signature config reads |
|
||||
| `initial_connection_retry_delay_ms` | integer | `2000` | Delay between initialization retries |
|
||||
| `timezone_offset_minutes` | integer | `0` | Timezone offset from UTC (minutes) |
|
||||
| `live_measurement_interval_ms` | integer | `1000` | Interval for reading/publishing live measurements |
|
||||
| `device_state_read_interval_ms` | integer | `10000` | Interval for reading device-state bitfield (VendorError reporting) |
|
||||
| `public_key_format` | enum | `binary` | The key format to use for the public key.
|
||||
|
||||
### Parameter tuning notes
|
||||
|
||||
- **`initial_connection_*`**:
|
||||
- Used during module startup for device setup / signature config reads.
|
||||
- `initial_connection_retry_count: 0` means **retry forever**.
|
||||
- `initial_connection_retry_delay_ms` has a **minimum of 100ms** (see `manifest.yaml`).
|
||||
- **`communication_*`**:
|
||||
- Used for regular Modbus operations during runtime.
|
||||
- `communication_retry_delay_ms` has a **minimum of 10ms** (see `manifest.yaml`).
|
||||
- **`communication_error_pause_delay_s`**:
|
||||
- After a communication exception in the live thread, the module waits this long before retrying.
|
||||
- If the line is physically broken (wrong wiring / adapter unplugged), increasing this value reduces log spam.
|
||||
- **`live_measurement_interval_ms` / `device_state_read_interval_ms`**:
|
||||
- Keep live measurements reasonable for your bus speed and number of devices.
|
||||
- For multi-drop RS-485, consider increasing intervals if you see bus contention/timeouts.
|
||||
|
||||
## Interfaces
|
||||
|
||||
### Provides
|
||||
|
||||
- `main`: `powermeter`
|
||||
|
||||
### Requires
|
||||
|
||||
- `modbus`: `serial_communication_hub`
|
||||
|
||||
## Transaction flow (OCMF)
|
||||
|
||||
### `start_transaction`
|
||||
|
||||
High-level flow:
|
||||
|
||||
1. Read OCMF state register and ensure it is `NOT_READY` before starting.
|
||||
2. Write OCMF transaction registers:
|
||||
- Identification status/level/flags/type
|
||||
- Identification data (ID)
|
||||
- Charging point identifier type + value (EVSE ID)
|
||||
- Tariff text (TT) as `tariff_text + "<=>" + transaction_id`
|
||||
- Written as **0-terminated** and only the **used** portion (no full padding).
|
||||
3. Write session modality (charging vehicle).
|
||||
4. Write the start command (`'B'`).
|
||||
|
||||
### `stop_transaction`
|
||||
|
||||
High-level flow:
|
||||
|
||||
1. Write end command (`'E'`) if stopping the currently tracked transaction.
|
||||
2. Wait for OCMF state `READY`.
|
||||
3. Read OCMF file (size + content).
|
||||
4. Confirm file read by writing `NOT_READY` to OCMF state.
|
||||
5. Return OCMF report in `signed_meter_value` (with `public_key` attached).
|
||||
|
||||
## Signature validation (recommended)
|
||||
|
||||
For troubleshooting and integration testing, this module ships a small signature validation tool under:
|
||||
`modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/`.
|
||||
|
||||
It can validate EM580 OCMF signatures (`ECDSA-brainpoolP384r1-SHA256`) against the public key read from the meter.
|
||||
See `ocmf_validation/README.md` for usage details.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No communication / timeouts
|
||||
|
||||
Common causes and checks:
|
||||
|
||||
- **Wrong unit id**:
|
||||
- Verify `powermeter_device_id` matches the meter’s Modbus address.
|
||||
- If you have multiple meters, ensure each has a unique id (1..247 are typical).
|
||||
- **Wrong serial settings**:
|
||||
- Ensure `SerialCommHub` settings match the meter configuration (baudrate, parity).
|
||||
- If you are unsure, start with conservative settings and increase once stable.
|
||||
- **RS-485 wiring / termination**:
|
||||
- Swap A/B if you see only timeouts.
|
||||
- Ensure termination is correct (only at bus ends).
|
||||
- Keep cables short / twisted pair; avoid star topologies where possible.
|
||||
- **Adapter / permissions**:
|
||||
- Confirm the serial device path exists and is stable (`/dev/ttyUSB0` can change between boots).
|
||||
- Ensure the EVerest process has permissions to open the device node.
|
||||
|
||||
### Repeated `CommunicationFault` raises
|
||||
|
||||
The module raises a communication fault when Modbus operations fail and clears it once communication is restored.
|
||||
If you see frequent toggling:
|
||||
|
||||
- Reduce bus load (increase `live_measurement_interval_ms`, especially with multiple meters)
|
||||
- Increase `communication_retry_count` modestly (and keep `communication_retry_delay_ms` ≥ 10ms)
|
||||
- Consider increasing `communication_error_pause_delay_s` to reduce retry storms on hard faults
|
||||
|
||||
### OCMF transaction does not complete / stuck waiting for READY
|
||||
|
||||
- Check that the meter is in the expected OCMF state and is not holding a previous transaction open.
|
||||
- Ensure the transaction ids passed to `start_transaction` / `stop_transaction` match your intended flow.
|
||||
- Inspect logs around OCMF state transitions and file readout to see which step fails.
|
||||
|
||||
### Tariff text (TT) is truncated
|
||||
|
||||
The EM580 TT field is limited to `CHAR[252]`. The driver logs a warning and truncates overlong strings.
|
||||
Shorten `tariff_text` and/or the appended data (e.g. transaction id formatting).
|
||||
|
||||
### Signature verification fails
|
||||
|
||||
Use the validation tool in `ocmf_validation/` to validate the produced OCMF string against the published public key.
|
||||
If it fails:
|
||||
- Ensure public key and OCMF data come from the **same device** and **same transaction**
|
||||
- Ensure the OCMF data is not modified (OCMF signatures are over compact JSON)
|
||||
|
||||
## Notes / Limitations
|
||||
|
||||
- **Write-multiple-registers limit**: the Modbus transport enforces the protocol limit by chunking into max 123 registers.
|
||||
- **Tariff text length**: TT is a `CHAR[252]` field (126 words). The driver logs a warning and truncates if needed.
|
||||
|
||||
## References
|
||||
|
||||
- Module docs: `modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/docs/index.rst`
|
||||
- OCMF spec: see [SAFE-eV OCMF specification](https://github.com/SAFE-eV/OCMF-Open-Charge-Metering-Format)
|
||||
|
||||
## Unit tests
|
||||
|
||||
Unit tests live under `modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/tests/` and include:
|
||||
|
||||
- Helper-level tests (`helper.hpp`)
|
||||
- `powermeterImpl` behavior tests using a fake Modbus transport and small test hooks
|
||||
|
||||
Build/run example (target name may vary by build system settings):
|
||||
|
||||
```bash
|
||||
ninja -C build everest-core_carlo_gavazzi_em580_helper_tests
|
||||
./build/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/tests/everest-core_carlo_gavazzi_em580_helper_tests
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
.. _everest_modules_handwritten_CarloGavazzi_EM580:
|
||||
|
||||
.. *******************
|
||||
.. Carlo Gavazzi EM580
|
||||
.. *******************
|
||||
|
||||
Module implementing the Carlo Gavazzi EM580 power meter driver adapter via Modbus RTU (through SerialCommHub).
|
||||
This module also supports models without OCMF/Eichrecht support (e.g. EM300 series).
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
The module consists of a single ``main`` implementation that serves the ``powermeter`` interface. Modbus access is done
|
||||
via the required ``serial_communication_hub`` interface.
|
||||
|
||||
Features
|
||||
========
|
||||
|
||||
- Live meter reads and ``powermeter`` publishing (interval configurable)
|
||||
- Resilient Modbus transport with retries and protocol-compliant write chunking
|
||||
|
||||
If supported by meter:
|
||||
- OCMF/Eichrecht transaction start/stop logic
|
||||
- Public key reading and publishing (hex)
|
||||
|
||||
Module Configuration
|
||||
====================
|
||||
|
||||
The module configuration parameters are defined in ``manifest.yaml``. A complete example configuration can be found at
|
||||
``config/bringup/config-bringup-CGEM580.yaml``.
|
||||
|
||||
Transaction flow (OCMF)
|
||||
=======================
|
||||
|
||||
Start transaction
|
||||
-----------------
|
||||
|
||||
At transaction start the module:
|
||||
|
||||
1. Ensures OCMF state is ``NOT_READY``
|
||||
2. Writes OCMF identification data, EVSE ID and tariff text (TT) (0-terminated, used bytes only)
|
||||
3. Writes session modality
|
||||
4. Sends the start command (``'B'``)
|
||||
|
||||
Stop transaction
|
||||
----------------
|
||||
|
||||
At transaction stop the module:
|
||||
|
||||
1. Sends the end command (``'E'``) for the tracked transaction
|
||||
2. Waits for OCMF state ``READY``
|
||||
3. Reads the OCMF file (size + content)
|
||||
4. Confirms the file read by setting state back to ``NOT_READY``
|
||||
|
||||
Notes / Limitations
|
||||
===================
|
||||
|
||||
- Modbus ``Write Multiple Registers`` requests are chunked to max 123 registers per request.
|
||||
- TT is a ``CHAR[252]`` field (126 words); overlong strings are warned and truncated.
|
||||
|
||||
Device identification code (register ``300012`` / ``000Bh``)
|
||||
----------------------------------------------------------
|
||||
At startup the driver reads the Carlo Gavazzi **Controls identification code** from Modbus register ``300012``
|
||||
(``000Bh``) to decide whether OCMF transactions are exposed.
|
||||
|
||||
The following identification codes are **explicitly supported** as **EM300/ET300 series** (live metering only;
|
||||
``start_transaction`` / ``stop_transaction`` return ``NOT_SUPPORTED``): **331**, **332**, **335**, **336**, **340**,
|
||||
**341**, **345**, **346**, **355**.
|
||||
|
||||
Any **other** identification code is treated as an OCMF-capable device (e.g. EM580 class): the full transaction flow
|
||||
applies. If a new meter without OCMF uses a code not listed above, the driver should be updated to recognise it;
|
||||
otherwise it may incorrectly attempt the OCMF path.
|
||||
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef CARLO_GAVAZZI_EM580_HELPER_HPP
|
||||
#define CARLO_GAVAZZI_EM580_HELPER_HPP
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
#include "transport.hpp"
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
namespace em580 {
|
||||
namespace registers {
|
||||
|
||||
constexpr std::int32_t MODBUS_BASE_ADDRESS = 300001;
|
||||
|
||||
// the following identification register is only accessible/visible when a direct single access is used
|
||||
constexpr std::int32_t MODBUS_IDENTIFICATION_CODE_ADDRESS = 300012; // 000Bh: Carlo Gavazzi Controls identification code
|
||||
|
||||
constexpr std::int32_t MODBUS_SIGNATURE_TYPE_ADDRESS = 309472; // 24FFh: Signature type (UINT16)
|
||||
constexpr std::int32_t MODBUS_PUBLIC_KEY_ADDRESS = 309473; // 2500h: Public key (UINT16[130])
|
||||
// DER formatted public key (Table 4.20/4.21), mandatory to read whole block
|
||||
// from 2600h.
|
||||
constexpr std::int32_t MODBUS_PUBLIC_KEY_DER_ADDRESS = 309729; // 2600h: Public key DER (read-only)
|
||||
constexpr std::uint16_t MODBUS_PUBLIC_KEY_DER_WORD_COUNT_256 = 46; // 2600h..262Dh (92 bytes, DER length 0x5A + 2)
|
||||
constexpr std::uint16_t MODBUS_PUBLIC_KEY_DER_WORD_COUNT_384 = 62; // 2600h..263Dh (124 bytes, DER length 0x7A + 2)
|
||||
|
||||
constexpr std::int32_t MODBUS_SIGNED_MAP_ADDRESS = 302049;
|
||||
constexpr std::uint16_t MODBUS_SIGNED_MAP_WORD_COUNT_256 = 93; // 61 words signed Data + 32 words signature
|
||||
constexpr std::uint16_t MODBUS_SIGNED_MAP_WORD_COUNT_384 = 109; // 61 words signed Data + 48 words signature
|
||||
|
||||
constexpr std::int32_t MODBUS_REAL_TIME_VALUES_ADDRESS = 300001;
|
||||
// We only need instantaneous values up to 300052 (frequency) for the live polling loop.
|
||||
// Energy totals are read from 301281+ (INT64, Wh) and signed values from 302049+.
|
||||
constexpr std::uint16_t MODBUS_REAL_TIME_VALUES_COUNT = 52; // Registers 300001-300052 (52 words)
|
||||
|
||||
constexpr std::int32_t MODBUS_REAL_TIME_ENERGY_ADDRESS_EM300_SERIES = 301025;
|
||||
constexpr std::uint16_t MODBUS_REAL_TIME_ENERGY_COUNT_EM300_SERIES = 12; // Table 2.5-1: 301025-301036 (12 words)
|
||||
|
||||
constexpr std::int32_t MODBUS_REAL_TIME_ENERGY_ADDRESS = 301281;
|
||||
constexpr std::uint16_t MODBUS_REAL_TIME_ENERGY_COUNT = 32; // Registers 301281-301312 (32 words)
|
||||
|
||||
constexpr std::int32_t MODBUS_TEMPERATURE_ADDRESS = 300776; // Internal Temperature
|
||||
|
||||
constexpr std::int32_t MODBUS_FIRMWARE_MEASURE_MODULE_ADDRESS = 300771; // Measure module firmware version/revision
|
||||
constexpr std::int32_t MODBUS_FIRMWARE_COMMUNICATION_MODULE_ADDRESS =
|
||||
300772; // Communication module firmware version/revision
|
||||
constexpr std::int32_t MODBUS_SERIAL_NUMBER_START_ADDRESS = 320481; // Serial number (7 registers: 320481-320487)
|
||||
constexpr std::uint16_t MODBUS_SERIAL_NUMBER_REGISTER_COUNT = 7; // 7 UINT16 registers = 14 bytes
|
||||
constexpr std::int32_t MODBUS_PRODUCTION_YEAR_ADDRESS = 320488; // Production year (1 UINT16 register)
|
||||
// same register for older series, but with following note in datasheet:
|
||||
// This register is available only in EM330 and EM340 manufactured from
|
||||
// October 1st 2018 (from serial number YR2018 274xxxS and following)
|
||||
constexpr std::int32_t MODBUS_PRODUCTION_YEAR_ADDRESS_EM300_SERIES = 320497;
|
||||
|
||||
// Device state register (Table 4.30, Section 4.3.6)
|
||||
constexpr std::int32_t MODBUS_DEVICE_STATE_ADDRESS = 320499; // 5012h: Device state (UINT16 bitfield)
|
||||
|
||||
// Time synchronization registers
|
||||
constexpr std::int32_t MODBUS_UTC_TIMESTAMP_ADDRESS = 328723; // UTC Timestamp for synchronization (INT64, 4 words)
|
||||
constexpr std::int32_t MODBUS_TIMEZONE_OFFSET_ADDRESS = 328722; // Local time delta in minutes (INT16, 1 word)
|
||||
|
||||
// OCMF Transaction registers (Table 4.34)
|
||||
constexpr std::int32_t MODBUS_OCMF_IDENTIFICATION_STATUS_ADDRESS = 328673; // 7000h: OCMF Ident. Status (UINT16)
|
||||
constexpr std::int32_t MODBUS_OCMF_IDENTIFICATION_LEVEL_ADDRESS = 328674; // 7001h: OCMF Ident. Level (UINT16)
|
||||
constexpr std::int32_t MODBUS_OCMF_IDENTIFICATION_FLAGS_START_ADDRESS =
|
||||
328675; // 7002h: OCMF Ident. Flags 1-4 (4 UINT16)
|
||||
constexpr std::uint16_t MODBUS_OCMF_IDENTIFICATION_FLAGS_COUNT = 4; // 4 flags
|
||||
constexpr std::int32_t MODBUS_OCMF_IDENTIFICATION_TYPE_ADDRESS = 328679; // 7006h: OCMF Ident. Type (UINT16)
|
||||
constexpr std::int32_t MODBUS_OCMF_IDENTIFICATION_DATA_START_ADDRESS =
|
||||
328680; // 7007h: OCMF Ident. Data (CHAR[40] = 20 words)
|
||||
constexpr std::uint16_t MODBUS_OCMF_IDENTIFICATION_DATA_WORD_COUNT = 20; // 40 bytes = 20 words
|
||||
constexpr std::int32_t MODBUS_OCMF_CHARGING_POINT_ID_TYPE_ADDRESS =
|
||||
328700; // 701Bh: OCMF Charging point identifier type (UINT16)
|
||||
constexpr std::int32_t MODBUS_OCMF_CHARGING_POINT_ID_START_ADDRESS = 328701; // 701Ch: OCMF CPI (CHAR[40] = 20 words)
|
||||
constexpr std::uint16_t MODBUS_OCMF_CHARGING_POINT_ID_WORD_COUNT = 20; // 40 bytes = 20 words
|
||||
constexpr std::int32_t MODBUS_OCMF_SESSION_MODALITY_ADDRESS = 328727; // 7036h: OCMF Session Modality (UINT16)
|
||||
constexpr std::uint16_t MODBUS_OCMF_SESSION_MODALITY_CHARGING_VEHICLE = 0; // Charging vehicle
|
||||
constexpr std::uint16_t MODBUS_OCMF_SESSION_MODALITY_VEHICLE_TO_GRID = 1; // Vehicle to grid
|
||||
constexpr std::uint16_t MODBUS_OCMF_SESSION_MODALITY_BIDIRECTIONAL = 2; // Bidirectional
|
||||
|
||||
// Tariff text register (Table 4.32)
|
||||
// 326881 (6900h): Tariff text (CHAR[252] = 126 words)
|
||||
constexpr std::int32_t MODBUS_OCMF_TARIFF_TEXT_ADDRESS = 326881; // 6900h: Tariff text (CHAR[252] = 126 words)
|
||||
constexpr std::uint16_t MODBUS_OCMF_TARIFF_TEXT_WORD_COUNT = 126; // 252 bytes = 126 words (CHAR[252])
|
||||
constexpr std::int32_t MODBUS_OCMF_TRANSACTION_ID_GENERATION_ADDRESS =
|
||||
328417; // 6F00h: OCMF Transaction ID Generation (UINT16)
|
||||
|
||||
// Tariff update register (Table 4.33)
|
||||
constexpr std::int32_t MODBUS_OCMF_TARIFF_UPDATE_ADDRESS = 327085; // 69CCh: Tariff update (UINT16)
|
||||
|
||||
// OCMF Command register (Table 4.35)
|
||||
// The register is UINT16 containing the ASCII code (e.g. 'B', 'E', 'A').
|
||||
constexpr std::int32_t MODBUS_OCMF_COMMAND_ADDRESS = 328737; // 7040h: OCMF Command Data (UINT16)
|
||||
constexpr std::uint16_t MODBUS_OCMF_COMMAND_START = 0x42; // Start transaction ('B')
|
||||
constexpr std::uint16_t MODBUS_OCMF_COMMAND_END = 0x45; // End transaction ('E')
|
||||
constexpr std::uint16_t MODBUS_OCMF_COMMAND_ABORT = 0x41; // Abort transaction ('A')
|
||||
|
||||
// OCMF State / status registers (Table 4.39 and related)
|
||||
constexpr std::int32_t MODBUS_OCMF_STATE_ADDRESS = 328929; // 7100h: OCMF State (UINT16)
|
||||
constexpr std::int32_t MODBUS_OCMF_TRANSACTION_ID_ADDRESS = 328931; // 7102h: OCMF Transaction ID (UINT32)
|
||||
constexpr std::uint16_t MODBUS_OCMF_STATE_NOT_READY = 0; // Not ready
|
||||
constexpr std::uint16_t MODBUS_OCMF_STATE_RUNNING = 1; // Running
|
||||
constexpr std::uint16_t MODBUS_OCMF_STATE_READY = 2; // Ready
|
||||
constexpr std::uint16_t MODBUS_OCMF_STATE_CORRUPTED = 3; // Corrupted
|
||||
constexpr std::int32_t MODBUS_OCMF_STATE_SIZE_ADDRESS = 328930; // 7101h: OCMF Size (UINT16)
|
||||
constexpr std::int32_t MODBUS_OCMF_STATE_FILE_ADDRESS = 328945; // 7110h: OCMF File (max theoretically 2031 words)
|
||||
constexpr std::uint16_t MODBUS_OCMF_STATE_FILE_WORD_COUNT = 2031; // 2031 words = 4062 bytes
|
||||
constexpr std::int32_t MODBUS_OCMF_CHARGING_STATUS_ADDRESS = 328742; // 7045h: Charging status (UINT16)
|
||||
constexpr std::int32_t MODBUS_OCMF_LAST_TRANSACTION_ID_ADDRESS = 328762; // 7059h: Last transaction id (CHAR[])
|
||||
constexpr std::uint16_t MODBUS_OCMF_LAST_TRANSACTION_ID_WORD_COUNT = 7; // 14 bytes = 7 words
|
||||
constexpr std::int32_t MODBUS_OCMF_TIME_SYNC_STATUS_ADDRESS = 328769; // 7060h: Time synchronization status (UINT16)
|
||||
|
||||
} // namespace registers
|
||||
} // namespace em580
|
||||
|
||||
namespace modbus_utils {
|
||||
|
||||
inline void check_bounds_or_throw(const transport::DataVector& data, transport::DataVector::size_type offset,
|
||||
transport::DataVector::size_type needed_bytes, const char* what) {
|
||||
if (offset > data.size() || needed_bytes > (data.size() - offset)) {
|
||||
throw std::out_of_range(std::string(what) + ": offset/length out of range (offset=" + std::to_string(offset) +
|
||||
", needed=" + std::to_string(needed_bytes) + ", size=" + std::to_string(data.size()) +
|
||||
")");
|
||||
}
|
||||
}
|
||||
|
||||
// Strong type wrappers to prevent parameter swapping
|
||||
struct ByteOffset {
|
||||
explicit ByteOffset(transport::DataVector::size_type val) : value(val) {
|
||||
}
|
||||
explicit operator transport::DataVector::size_type() const {
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
transport::DataVector::size_type value;
|
||||
};
|
||||
|
||||
struct ByteLength {
|
||||
explicit ByteLength(transport::DataVector::size_type val) : value(val) {
|
||||
}
|
||||
explicit operator transport::DataVector::size_type() const {
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
transport::DataVector::size_type value;
|
||||
};
|
||||
|
||||
inline std::uint32_t to_uint32(const transport::DataVector& data, ByteOffset offset) {
|
||||
const auto off = static_cast<transport::DataVector::size_type>(offset);
|
||||
check_bounds_or_throw(data, off, 4, "to_uint32");
|
||||
return static_cast<std::uint32_t>(data[off] << 24 | data[off + 1] << 16 | data[off + 2] << 8 | data[off + 3]);
|
||||
}
|
||||
|
||||
inline std::int32_t to_int32(const transport::DataVector& data, ByteOffset offset) {
|
||||
const auto off = static_cast<transport::DataVector::size_type>(offset);
|
||||
check_bounds_or_throw(data, off, 4, "to_int32");
|
||||
return static_cast<std::int32_t>(data[off + 2] << 24 | data[off + 3] << 16 | data[off] << 8 | data[off + 1]);
|
||||
}
|
||||
|
||||
inline std::int64_t to_int64(const transport::DataVector& data, ByteOffset offset) {
|
||||
const auto off = static_cast<transport::DataVector::size_type>(offset);
|
||||
check_bounds_or_throw(data, off, 8, "to_int64");
|
||||
// EM580 Modbus spec:
|
||||
// - Byte order inside a word is MSB -> LSB.
|
||||
// - Word order for INT64/UINT64 is LSW -> MSW.
|
||||
const std::uint64_t w0 = (static_cast<std::uint64_t>(data[off]) << 8) | static_cast<std::uint64_t>(data[off + 1]);
|
||||
const std::uint64_t w1 =
|
||||
(static_cast<std::uint64_t>(data[off + 2]) << 8) | static_cast<std::uint64_t>(data[off + 3]);
|
||||
const std::uint64_t w2 =
|
||||
(static_cast<std::uint64_t>(data[off + 4]) << 8) | static_cast<std::uint64_t>(data[off + 5]);
|
||||
const std::uint64_t w3 =
|
||||
(static_cast<std::uint64_t>(data[off + 6]) << 8) | static_cast<std::uint64_t>(data[off + 7]);
|
||||
const std::uint64_t u = (w0) | (w1 << 16) | (w2 << 32) | (w3 << 48);
|
||||
return static_cast<std::int64_t>(u);
|
||||
}
|
||||
|
||||
inline std::uint16_t to_uint16(const transport::DataVector& data, ByteOffset offset) {
|
||||
const auto off = static_cast<transport::DataVector::size_type>(offset);
|
||||
check_bounds_or_throw(data, off, 2, "to_uint16");
|
||||
return static_cast<std::uint16_t>(data[off] << 8 | data[off + 1]);
|
||||
}
|
||||
|
||||
inline std::int16_t to_int16(const transport::DataVector& data, ByteOffset offset) {
|
||||
std::uint16_t raw = to_uint16(data, offset);
|
||||
return static_cast<std::int16_t>(raw);
|
||||
}
|
||||
|
||||
inline std::string to_hex_string(const transport::DataVector& data, ByteOffset offset, ByteLength length) {
|
||||
const auto off = static_cast<transport::DataVector::size_type>(offset);
|
||||
const auto len = static_cast<transport::DataVector::size_type>(length);
|
||||
check_bounds_or_throw(data, off, len, "to_hex_string");
|
||||
std::stringstream ss;
|
||||
for (std::size_t index = 0; index < len; ++index) {
|
||||
ss << std::uppercase << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(data[off + index]);
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
inline std::size_t max_payload_bytes_for_words(std::size_t max_words) {
|
||||
const std::size_t capacity_bytes = max_words * 2;
|
||||
return capacity_bytes > 0 ? capacity_bytes - 1 : 0; // reserve NUL
|
||||
}
|
||||
|
||||
inline void log_truncation_warning_if_needed(const char* field_name, const std::string& value, std::size_t max_words) {
|
||||
const std::size_t capacity_bytes = max_words * 2;
|
||||
const std::size_t max_payload_bytes = max_payload_bytes_for_words(max_words);
|
||||
if (value.size() > max_payload_bytes) {
|
||||
EVLOG_warning << field_name << " too long (" << value.size() << " bytes). Max is " << max_payload_bytes
|
||||
<< " bytes (" << max_words << " words / " << capacity_bytes << " bytes incl. NUL). "
|
||||
<< "It will be truncated.";
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a string to a big-endian Modbus CHAR array (vector of UINT16 words)
|
||||
/// that is **0-terminated** and contains only the **used** part (i.e. no full
|
||||
/// fixed-length padding).
|
||||
///
|
||||
/// - Max capacity is `max_words * 2` bytes.
|
||||
/// - Ensures a terminating `\\0` byte is present within the returned data.
|
||||
/// - If `str` is too long, it is truncated to fit `max_words * 2 - 1` bytes (+
|
||||
/// 1 byte terminator).
|
||||
inline std::vector<std::uint16_t> string_to_modbus_char_array(const std::string& str, std::size_t max_words) {
|
||||
const std::size_t max_bytes = max_words * 2;
|
||||
if (max_bytes == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::size_t used_len = std::min(str.size(), max_bytes - 1); // leave space for terminator
|
||||
const std::size_t bytes_to_write = used_len + 1; // include terminator byte
|
||||
const std::size_t words_to_write = (bytes_to_write + 1) / 2; // ceil(bytes/2)
|
||||
|
||||
std::vector<std::uint16_t> data(words_to_write, 0);
|
||||
for (std::size_t i = 0; i < used_len; ++i) {
|
||||
const std::size_t word_idx = i / 2;
|
||||
if ((i % 2) == 0) {
|
||||
data[word_idx] = static_cast<std::uint8_t>(str[i]) << 8;
|
||||
} else {
|
||||
data[word_idx] |= static_cast<std::uint8_t>(str[i]);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
} // namespace modbus_utils
|
||||
|
||||
namespace ocmf {
|
||||
|
||||
/// Confirm OCMF file read by writing NOT_READY (0) into the OCMF state
|
||||
/// register.
|
||||
inline void confirm_file_read(transport::AbstractModbusTransport& modbus_transport) {
|
||||
std::vector<std::uint16_t> ocmf_confirmation_data = {em580::registers::MODBUS_OCMF_STATE_NOT_READY};
|
||||
modbus_transport.write_multiple_registers(em580::registers::MODBUS_OCMF_STATE_ADDRESS, ocmf_confirmation_data);
|
||||
}
|
||||
|
||||
/// Wait until OCMF state becomes READY (2).
|
||||
/// @return true if READY, false on CORRUPTED or timeout.
|
||||
inline bool wait_for_ready(transport::AbstractModbusTransport& modbus_transport,
|
||||
std::chrono::milliseconds poll_interval = std::chrono::milliseconds{100},
|
||||
int max_retries = 10) {
|
||||
std::uint16_t state = em580::registers::MODBUS_OCMF_STATE_NOT_READY;
|
||||
transport::DataVector state_data;
|
||||
int retries = 0;
|
||||
|
||||
while (state != em580::registers::MODBUS_OCMF_STATE_READY) {
|
||||
state_data = modbus_transport.fetch(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1);
|
||||
state = modbus_utils::to_uint16(state_data, modbus_utils::ByteOffset{0});
|
||||
|
||||
if (state == em580::registers::MODBUS_OCMF_STATE_CORRUPTED) {
|
||||
return false;
|
||||
}
|
||||
if (state != em580::registers::MODBUS_OCMF_STATE_READY) {
|
||||
EVLOG_info << "OCMF state: " << state;
|
||||
std::this_thread::sleep_for(poll_interval);
|
||||
retries++;
|
||||
if (retries > max_retries) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool is_uuid36(const std::string& s) {
|
||||
if (s.size() != 36) {
|
||||
return false;
|
||||
}
|
||||
for (std::size_t i = 0; i < s.size(); ++i) {
|
||||
const char c = s[i];
|
||||
if (i == 8 || i == 13 || i == 18 || i == 23) {
|
||||
if (c != '-') {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!std::isxdigit(static_cast<unsigned char>(c))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::optional<std::string> extract_transaction_id_from_ocmf_record(const std::string& ocmf_record) {
|
||||
const std::string key = "\"TT\"";
|
||||
std::size_t key_pos = ocmf_record.find(key);
|
||||
if (key_pos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::size_t colon_pos = ocmf_record.find(':', key_pos + key.size());
|
||||
if (colon_pos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::size_t value_start = ocmf_record.find('"', colon_pos + 1);
|
||||
if (value_start == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
++value_start;
|
||||
|
||||
std::string tt_value;
|
||||
tt_value.reserve(128);
|
||||
bool escaped = false;
|
||||
for (std::size_t i = value_start; i < ocmf_record.size(); ++i) {
|
||||
const char c = ocmf_record[i];
|
||||
if (escaped) {
|
||||
tt_value.push_back(c);
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') {
|
||||
break;
|
||||
}
|
||||
tt_value.push_back(c);
|
||||
}
|
||||
|
||||
const std::string marker = "<=>";
|
||||
const std::size_t marker_pos = tt_value.rfind(marker);
|
||||
if (marker_pos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string tail = tt_value.substr(marker_pos + marker.size());
|
||||
while (!tail.empty() && std::isspace(static_cast<unsigned char>(tail.front()))) {
|
||||
tail.erase(tail.begin());
|
||||
}
|
||||
while (!tail.empty() && std::isspace(static_cast<unsigned char>(tail.back()))) {
|
||||
tail.pop_back();
|
||||
}
|
||||
|
||||
std::optional<std::string> last_uuid;
|
||||
if (tail.size() >= 36) {
|
||||
for (std::size_t i = 0; i + 36 <= tail.size(); ++i) {
|
||||
const std::string candidate = tail.substr(i, 36);
|
||||
if (is_uuid36(candidate)) {
|
||||
last_uuid = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return last_uuid;
|
||||
}
|
||||
|
||||
/// Extract transaction id (UUID) from a tariff text string.
|
||||
///
|
||||
/// Driver convention: tariff text is written as "<user text><=><transaction_id>".
|
||||
/// Returns the last UUID found after the "<=>" marker.
|
||||
inline std::optional<std::string> extract_transaction_id_from_tariff_text(const std::string& tariff_text,
|
||||
std::string_view marker) {
|
||||
const std::size_t marker_pos = tariff_text.rfind(marker);
|
||||
if (marker_pos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string tail = tariff_text.substr(marker_pos + marker.size());
|
||||
while (!tail.empty() && std::isspace(static_cast<unsigned char>(tail.front()))) {
|
||||
tail.erase(tail.begin());
|
||||
}
|
||||
while (!tail.empty() && std::isspace(static_cast<unsigned char>(tail.back()))) {
|
||||
tail.pop_back();
|
||||
}
|
||||
|
||||
// The transaction id is appended at the end, so search from the back.
|
||||
if (tail.size() < 36) {
|
||||
return std::nullopt;
|
||||
}
|
||||
for (std::size_t i = tail.size() - 36 + 1; i-- > 0;) {
|
||||
const std::string candidate = tail.substr(i, 36);
|
||||
if (is_uuid36(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
inline std::uint16_t flag_to_value(types::powermeter::OCMFIdentificationFlags flag) {
|
||||
switch (flag) {
|
||||
case types::powermeter::OCMFIdentificationFlags::RFID_NONE:
|
||||
return 0;
|
||||
case types::powermeter::OCMFIdentificationFlags::RFID_PLAIN:
|
||||
return 1;
|
||||
case types::powermeter::OCMFIdentificationFlags::RFID_RELATED:
|
||||
return 2;
|
||||
case types::powermeter::OCMFIdentificationFlags::RFID_PSK:
|
||||
return 3;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_NONE:
|
||||
return 0;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_RS:
|
||||
return 1;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_AUTH:
|
||||
return 2;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_RS_TLS:
|
||||
return 3;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_AUTH_TLS:
|
||||
return 4;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_CACHE:
|
||||
return 5;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_WHITELIST:
|
||||
return 6;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_CERTIFIED:
|
||||
return 7;
|
||||
case types::powermeter::OCMFIdentificationFlags::ISO15118_NONE:
|
||||
return 0;
|
||||
case types::powermeter::OCMFIdentificationFlags::ISO15118_PNC:
|
||||
return 1;
|
||||
case types::powermeter::OCMFIdentificationFlags::PLMN_NONE:
|
||||
return 0;
|
||||
case types::powermeter::OCMFIdentificationFlags::PLMN_RING:
|
||||
return 1;
|
||||
case types::powermeter::OCMFIdentificationFlags::PLMN_SMS:
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline std::uint16_t level_to_value(types::powermeter::OCMFIdentificationLevel level) {
|
||||
switch (level) {
|
||||
case types::powermeter::OCMFIdentificationLevel::NONE:
|
||||
return 0;
|
||||
case types::powermeter::OCMFIdentificationLevel::HEARSAY:
|
||||
return 1;
|
||||
case types::powermeter::OCMFIdentificationLevel::TRUSTED:
|
||||
return 2;
|
||||
case types::powermeter::OCMFIdentificationLevel::VERIFIED:
|
||||
return 3;
|
||||
case types::powermeter::OCMFIdentificationLevel::CERTIFIED:
|
||||
return 4;
|
||||
case types::powermeter::OCMFIdentificationLevel::SECURE:
|
||||
return 5;
|
||||
case types::powermeter::OCMFIdentificationLevel::MISMATCH:
|
||||
return 6;
|
||||
case types::powermeter::OCMFIdentificationLevel::INVALID:
|
||||
return 7;
|
||||
case types::powermeter::OCMFIdentificationLevel::OUTDATED:
|
||||
return 8;
|
||||
case types::powermeter::OCMFIdentificationLevel::UNKNOWN:
|
||||
return 9;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline std::uint16_t type_to_value(types::powermeter::OCMFIdentificationType type) {
|
||||
switch (type) {
|
||||
case types::powermeter::OCMFIdentificationType::NONE:
|
||||
return 0;
|
||||
case types::powermeter::OCMFIdentificationType::DENIED:
|
||||
return 1;
|
||||
case types::powermeter::OCMFIdentificationType::UNDEFINED:
|
||||
return 2;
|
||||
case types::powermeter::OCMFIdentificationType::ISO14443:
|
||||
return 10;
|
||||
case types::powermeter::OCMFIdentificationType::ISO15693:
|
||||
return 11;
|
||||
case types::powermeter::OCMFIdentificationType::EMAID:
|
||||
return 20;
|
||||
case types::powermeter::OCMFIdentificationType::EVCCID:
|
||||
return 21;
|
||||
case types::powermeter::OCMFIdentificationType::EVCOID:
|
||||
return 30;
|
||||
case types::powermeter::OCMFIdentificationType::ISO7812:
|
||||
return 40;
|
||||
case types::powermeter::OCMFIdentificationType::CARD_TXN_NR:
|
||||
return 50;
|
||||
case types::powermeter::OCMFIdentificationType::CENTRAL:
|
||||
return 60;
|
||||
case types::powermeter::OCMFIdentificationType::CENTRAL_1:
|
||||
return 61;
|
||||
case types::powermeter::OCMFIdentificationType::CENTRAL_2:
|
||||
return 62;
|
||||
case types::powermeter::OCMFIdentificationType::LOCAL:
|
||||
return 70;
|
||||
case types::powermeter::OCMFIdentificationType::LOCAL_1:
|
||||
return 71;
|
||||
case types::powermeter::OCMFIdentificationType::LOCAL_2:
|
||||
return 72;
|
||||
case types::powermeter::OCMFIdentificationType::PHONE_NUMBER:
|
||||
return 80;
|
||||
case types::powermeter::OCMFIdentificationType::KEY_CODE:
|
||||
return 90;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace ocmf
|
||||
|
||||
namespace device_state_utils {
|
||||
|
||||
inline std::vector<std::string> decode_device_state_errors(std::uint16_t device_state) {
|
||||
struct BitError {
|
||||
const char* message;
|
||||
std::uint16_t bit;
|
||||
};
|
||||
|
||||
static constexpr std::array<BitError, 12> errors = {{
|
||||
{"V1N over maximum range", 0U},
|
||||
{"V2N over maximum range", 1U},
|
||||
{"V3N over maximum range", 2U},
|
||||
{"V12 over maximum range", 3U},
|
||||
{"V23 over maximum range", 4U},
|
||||
{"V31 over maximum range", 5U},
|
||||
{"I1 over maximum range", 6U},
|
||||
{"I2 over maximum range", 7U},
|
||||
{"I3 over maximum range", 8U},
|
||||
{"Frequency outside validity range", 9U},
|
||||
{"EVCS module internal fault", 12U},
|
||||
{"Measure module internal fault", 13U},
|
||||
}};
|
||||
|
||||
std::vector<std::string> out;
|
||||
for (const auto& err : errors) {
|
||||
if ((device_state & static_cast<std::uint16_t>(1U << err.bit)) != 0U) {
|
||||
out.emplace_back(err.message);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace device_state_utils
|
||||
|
||||
#endif // CARLO_GAVAZZI_EM580_HELPER_HPP
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef MAIN_POWERMETER_IMPL_HPP
|
||||
#define MAIN_POWERMETER_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
#include "../CarloGavazzi_EM580.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "transport.hpp"
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
struct Conf {
|
||||
int powermeter_device_id;
|
||||
int communication_retry_count;
|
||||
int communication_retry_delay_ms;
|
||||
int communication_error_pause_delay_s;
|
||||
int initial_connection_retry_count;
|
||||
int initial_connection_retry_delay_ms;
|
||||
int timezone_offset_minutes;
|
||||
int live_measurement_interval_ms;
|
||||
int device_state_read_interval_ms;
|
||||
std::string public_key_format;
|
||||
};
|
||||
|
||||
class powermeterImpl : public powermeterImplBase {
|
||||
public:
|
||||
powermeterImpl() = delete;
|
||||
powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<CarloGavazzi_EM580>& mod, Conf& config) :
|
||||
powermeterImplBase(ev, "main"), mod(mod), config(config){};
|
||||
|
||||
// Marker used to append the transaction id to the tariff text (TT field).
|
||||
// Format: "<tariff_text><=><transaction_id>"
|
||||
static constexpr std::string_view TARIFF_TEXT_TRANSACTION_ID_MARKER = "<=>";
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
~powermeterImpl() override;
|
||||
// Test-only access helpers (used by unit tests to avoid spinning up the full
|
||||
// EVerest runtime). These are intentionally narrow: inject transport + tweak
|
||||
// minimal internal state + invoke handlers.
|
||||
struct TestAccess {
|
||||
static void set_modbus_transport(powermeterImpl& self,
|
||||
std::unique_ptr<transport::AbstractModbusTransport> transport) {
|
||||
self.p_modbus_transport = std::move(transport);
|
||||
}
|
||||
|
||||
static void set_pending_closed_transaction(powermeterImpl& self, bool pending) {
|
||||
self.m_pending_closed_transaction = pending;
|
||||
}
|
||||
|
||||
static void set_transaction_id(powermeterImpl& self, std::string transaction_id) {
|
||||
self.m_transaction_id = std::move(transaction_id);
|
||||
self.m_transaction_active.store(true);
|
||||
}
|
||||
|
||||
static void set_public_key_hex(powermeterImpl& self, std::string public_key_hex) {
|
||||
self.m_public_key_hex = std::move(public_key_hex);
|
||||
}
|
||||
|
||||
static void set_signed_map_word_count(powermeterImpl& self, std::uint16_t signed_map_word_count) {
|
||||
self.m_signed_map_word_count = signed_map_word_count;
|
||||
}
|
||||
|
||||
static types::powermeter::TransactionStartResponse start_transaction(powermeterImpl& self,
|
||||
types::powermeter::TransactionReq& req) {
|
||||
return self.handle_start_transaction(req);
|
||||
}
|
||||
|
||||
static types::powermeter::TransactionStopResponse stop_transaction(powermeterImpl& self,
|
||||
std::string& transaction_id) {
|
||||
return self.handle_stop_transaction(transaction_id);
|
||||
}
|
||||
};
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual types::powermeter::TransactionStartResponse
|
||||
handle_start_transaction(types::powermeter::TransactionReq& value) override;
|
||||
virtual types::powermeter::TransactionStopResponse handle_stop_transaction(std::string& transaction_id) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<CarloGavazzi_EM580>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
std::unique_ptr<transport::AbstractModbusTransport> p_modbus_transport;
|
||||
|
||||
std::optional<types::units_signed::SignedMeterValue> m_start_signed_meter_value;
|
||||
|
||||
std::uint16_t m_public_key_length_in_bits;
|
||||
std::string m_public_key_hex;
|
||||
std::string m_transaction_id;
|
||||
std::string m_measure_module_firmware_version;
|
||||
std::string m_communication_module_firmware_version;
|
||||
std::string m_serial_number;
|
||||
std::string m_signature_method_string;
|
||||
std::uint16_t m_signed_map_word_count{0};
|
||||
|
||||
std::atomic_bool m_transaction_active{false};
|
||||
std::atomic_bool m_pending_time_sync{false};
|
||||
bool m_pending_closed_transaction{false};
|
||||
|
||||
// Background threads (started in ready(), joined on destruction)
|
||||
std::atomic_bool stop_requested_{false};
|
||||
std::mutex stop_mutex_;
|
||||
std::condition_variable stop_cv_;
|
||||
std::thread live_measure_thread_;
|
||||
std::thread time_sync_thread_;
|
||||
|
||||
// flag whether transactions are supported
|
||||
bool m_transaction_support{true};
|
||||
|
||||
void configure_device();
|
||||
void read_signature_config();
|
||||
types::units_signed::SignedMeterValue read_signed_meter_value();
|
||||
void read_powermeter_values();
|
||||
void dump_device_state(void);
|
||||
void read_identification();
|
||||
void read_firmware_versions();
|
||||
void read_serial_number();
|
||||
void read_transaction_state_and_id();
|
||||
std::string read_ocmf_file();
|
||||
void synchronize_time();
|
||||
void set_timezone(int offset_minutes);
|
||||
void time_sync_thread();
|
||||
[[nodiscard]] bool is_transaction_active() const;
|
||||
void clear_transaction_states();
|
||||
void write_transaction_registers(const types::powermeter::TransactionReq& transaction_req);
|
||||
void read_device_state();
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
|
||||
#endif // MAIN_POWERMETER_IMPL_HPP
|
||||
@@ -0,0 +1,114 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "transport.hpp"
|
||||
#include <string>
|
||||
|
||||
// Modbus protocol limits:
|
||||
// - Read Input Registers (0x04): typically max 125 registers per request
|
||||
// - Write Multiple Registers (0x10): max 123 registers per request (PDU size
|
||||
// limit)
|
||||
constexpr std::uint16_t MAX_READ_REGISTERS_PER_MESSAGE = 125;
|
||||
constexpr std::uint16_t MAX_WRITE_REGISTERS_PER_MESSAGE = 123;
|
||||
|
||||
namespace transport {
|
||||
|
||||
transport::DataVector SerialCommHubTransport::fetch(std::int32_t address, std::uint16_t register_count) {
|
||||
return retry_with_config([this, address, register_count]() {
|
||||
transport::DataVector response;
|
||||
response.reserve(static_cast<std::size_t>(register_count) * 2U); // this is a uint8_t vector
|
||||
|
||||
std::uint16_t remaining_register_to_read{register_count};
|
||||
std::int32_t read_address{address - m_base_address};
|
||||
|
||||
while (remaining_register_to_read > 0) {
|
||||
const std::uint16_t register_to_read = remaining_register_to_read > MAX_READ_REGISTERS_PER_MESSAGE
|
||||
? MAX_READ_REGISTERS_PER_MESSAGE
|
||||
: remaining_register_to_read;
|
||||
|
||||
types::serial_comm_hub_requests::Result serial_com_hub_result =
|
||||
m_serial_hub.call_modbus_read_input_registers(static_cast<int>(m_device_id),
|
||||
static_cast<int>(read_address), register_to_read);
|
||||
|
||||
// Check for communication errors
|
||||
if (serial_com_hub_result.status_code == types::serial_comm_hub_requests::StatusCodeEnum::Timeout) {
|
||||
throw transport::ModbusTimeoutException("Modbus read timeout: Packet receive timeout");
|
||||
} else if (serial_com_hub_result.status_code != types::serial_comm_hub_requests::StatusCodeEnum::Success) {
|
||||
std::string error_msg =
|
||||
"Modbus read failed with status: " +
|
||||
types::serial_comm_hub_requests::status_code_enum_to_string(serial_com_hub_result.status_code);
|
||||
throw std::runtime_error(error_msg);
|
||||
}
|
||||
|
||||
if (not serial_com_hub_result.value.has_value())
|
||||
throw std::runtime_error("no result from serial com hub!");
|
||||
|
||||
// make sure that returned vector is a int32 vector
|
||||
static_assert(
|
||||
std::is_same_v<std::int32_t,
|
||||
decltype(types::serial_comm_hub_requests::Result::value)::value_type::value_type>);
|
||||
|
||||
union {
|
||||
std::int32_t val_32;
|
||||
struct {
|
||||
std::uint8_t v3;
|
||||
std::uint8_t v2;
|
||||
std::uint8_t v1;
|
||||
std::uint8_t v0;
|
||||
} val_8;
|
||||
} swapit;
|
||||
|
||||
static_assert(sizeof(swapit.val_32) == sizeof(swapit.val_8));
|
||||
|
||||
transport::DataVector tmp{};
|
||||
|
||||
for (auto item : serial_com_hub_result.value.value()) {
|
||||
swapit.val_32 = item;
|
||||
tmp.push_back(swapit.val_8.v2);
|
||||
tmp.push_back(swapit.val_8.v3);
|
||||
}
|
||||
|
||||
response.insert(response.end(), tmp.begin(), tmp.end());
|
||||
|
||||
read_address += register_to_read;
|
||||
remaining_register_to_read -= register_to_read;
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
void SerialCommHubTransport::write_multiple_registers(std::int32_t address, const std::vector<std::uint16_t>& data) {
|
||||
retry_with_config_void([this, address, &data]() {
|
||||
std::int32_t write_address = address - m_base_address;
|
||||
std::size_t offset = 0;
|
||||
while (offset < data.size()) {
|
||||
const std::size_t remaining = data.size() - offset;
|
||||
const std::size_t chunk_size = remaining > static_cast<std::size_t>(MAX_WRITE_REGISTERS_PER_MESSAGE)
|
||||
? static_cast<std::size_t>(MAX_WRITE_REGISTERS_PER_MESSAGE)
|
||||
: remaining;
|
||||
|
||||
types::serial_comm_hub_requests::VectorUint16 data_raw;
|
||||
data_raw.data.reserve(chunk_size);
|
||||
for (std::size_t i = 0; i < chunk_size; ++i) {
|
||||
data_raw.data.push_back(data[offset + i]);
|
||||
}
|
||||
|
||||
types::serial_comm_hub_requests::StatusCodeEnum status = m_serial_hub.call_modbus_write_multiple_registers(
|
||||
static_cast<int>(m_device_id), static_cast<int>(write_address + static_cast<std::int32_t>(offset)),
|
||||
data_raw);
|
||||
|
||||
if (status == types::serial_comm_hub_requests::StatusCodeEnum::Timeout) {
|
||||
throw transport::ModbusTimeoutException("Modbus write timeout: Packet receive timeout");
|
||||
} else if (status != types::serial_comm_hub_requests::StatusCodeEnum::Success) {
|
||||
std::string error_msg = "Failed to write Modbus registers: " +
|
||||
types::serial_comm_hub_requests::status_code_enum_to_string(status);
|
||||
throw std::runtime_error(error_msg);
|
||||
}
|
||||
|
||||
offset += chunk_size;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace transport
|
||||
@@ -0,0 +1,249 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef POWERMETER_TRANSPORT_HPP
|
||||
#define POWERMETER_TRANSPORT_HPP
|
||||
|
||||
/**
|
||||
* Baseclass for transport classes.
|
||||
*
|
||||
* Transports are:
|
||||
* - direct connection via modbus
|
||||
* - connection via SerialComHub
|
||||
*/
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <everest/logging.hpp>
|
||||
#include <functional>
|
||||
#include <generated/interfaces/serial_communication_hub/Interface.hpp>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace transport {
|
||||
|
||||
using DataVector = std::vector<std::uint8_t>;
|
||||
|
||||
// Custom exception to distinguish timeout errors from other Modbus errors
|
||||
class ModbusTimeoutException : public std::runtime_error {
|
||||
public:
|
||||
explicit ModbusTimeoutException(const std::string& message) : std::runtime_error(message) {
|
||||
}
|
||||
};
|
||||
|
||||
// Error handler callback type: void(error_message)
|
||||
using ErrorHandler = std::function<void(const std::string&)>;
|
||||
// Clear error callback type: void()
|
||||
using ClearErrorHandler = std::function<void()>;
|
||||
|
||||
class AbstractModbusTransport {
|
||||
|
||||
public:
|
||||
AbstractModbusTransport() = default;
|
||||
virtual ~AbstractModbusTransport() = default;
|
||||
|
||||
AbstractModbusTransport(const AbstractModbusTransport&) = delete;
|
||||
AbstractModbusTransport& operator=(const AbstractModbusTransport&) = delete;
|
||||
AbstractModbusTransport(AbstractModbusTransport&&) = delete;
|
||||
AbstractModbusTransport& operator=(AbstractModbusTransport&&) = delete;
|
||||
|
||||
virtual transport::DataVector fetch(std::int32_t address, std::uint16_t register_count) = 0;
|
||||
virtual void write_multiple_registers(std::int32_t address, const std::vector<std::uint16_t>& data) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* data transport via SerialComHub
|
||||
*/
|
||||
|
||||
class SerialCommHubTransport : public AbstractModbusTransport {
|
||||
|
||||
private:
|
||||
serial_communication_hubIntf& m_serial_hub;
|
||||
std::int32_t m_device_id;
|
||||
std::int32_t m_base_address;
|
||||
|
||||
// Retry configuration
|
||||
std::int32_t m_initial_retry_count;
|
||||
std::int32_t m_initial_retry_delay_ms;
|
||||
std::int32_t m_normal_retry_count;
|
||||
std::int32_t m_normal_retry_delay_ms;
|
||||
|
||||
// State tracking
|
||||
std::atomic_bool m_initial_connection_mode{true};
|
||||
|
||||
// Error handling callbacks (optional)
|
||||
ErrorHandler m_error_handler;
|
||||
ClearErrorHandler m_clear_error_handler;
|
||||
|
||||
// Internal retry helper for functions that return a value
|
||||
template <typename Func> auto retry_with_config(Func&& func) -> decltype(std::forward<Func>(func)()) {
|
||||
const bool is_initial = m_initial_connection_mode.load();
|
||||
const int max_retries = is_initial ? m_initial_retry_count : m_normal_retry_count;
|
||||
const int delay_ms = is_initial ? m_initial_retry_delay_ms : m_normal_retry_delay_ms;
|
||||
const bool infinite_retries = is_initial && (m_initial_retry_count == 0);
|
||||
|
||||
// For initial connection, 0 means infinite retries
|
||||
int attempt = 1;
|
||||
while (infinite_retries || attempt <= max_retries) {
|
||||
try {
|
||||
auto result = std::forward<Func>(func)();
|
||||
// First successful call - switch to normal mode
|
||||
bool was_initial = m_initial_connection_mode.exchange(false);
|
||||
// Clear CommunicationFault error if communication is restored
|
||||
// Only clear if we're not in initial connection mode (i.e., we've had
|
||||
// at least one successful operation)
|
||||
if (m_clear_error_handler && !was_initial) {
|
||||
m_clear_error_handler();
|
||||
}
|
||||
return result;
|
||||
} catch (const ModbusTimeoutException& e) {
|
||||
// Timeout errors should raise CommunicationFault
|
||||
const bool should_retry = infinite_retries ? true : attempt < max_retries;
|
||||
if (should_retry) {
|
||||
if (infinite_retries) {
|
||||
EVLOG_warning << "Modbus operation failed (attempt " << attempt
|
||||
<< ", infinite retries): " << e.what() << ". Retrying in " << delay_ms << "ms...";
|
||||
} else {
|
||||
EVLOG_warning << "Modbus operation failed (attempt " << attempt << "/" << max_retries
|
||||
<< "): " << e.what() << ". Retrying in " << delay_ms << "ms...";
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
|
||||
} else {
|
||||
EVLOG_error << "Modbus operation failed after " << attempt << " attempts: " << e.what();
|
||||
// Raise CommunicationFault error for timeout errors
|
||||
if (m_error_handler) {
|
||||
m_error_handler("Modbus communication error: " + std::string(e.what()));
|
||||
}
|
||||
rethrow_exception(std::current_exception());
|
||||
}
|
||||
attempt++;
|
||||
} catch (const std::exception& e) {
|
||||
// Other errors (non-timeout) should not raise CommunicationFault
|
||||
const bool should_retry = infinite_retries ? true : attempt < max_retries;
|
||||
if (should_retry) {
|
||||
if (infinite_retries) {
|
||||
EVLOG_warning << "Modbus operation failed (attempt " << attempt
|
||||
<< ", infinite retries): " << e.what() << ". Retrying in " << delay_ms << "ms...";
|
||||
} else {
|
||||
EVLOG_warning << "Modbus operation failed (attempt " << attempt << "/" << max_retries
|
||||
<< "): " << e.what() << ". Retrying in " << delay_ms << "ms...";
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
|
||||
} else {
|
||||
EVLOG_error << "Modbus operation failed after " << attempt << " attempts: " << e.what();
|
||||
// Don't raise CommunicationFault for non-timeout errors
|
||||
rethrow_exception(std::current_exception());
|
||||
}
|
||||
attempt++;
|
||||
}
|
||||
}
|
||||
// This should never be reached, but needed to satisfy compiler
|
||||
throw std::runtime_error("Retry loop exited unexpectedly");
|
||||
}
|
||||
|
||||
// Internal retry helper for void functions
|
||||
template <typename Func> void retry_with_config_void(Func&& func) {
|
||||
const bool is_initial = m_initial_connection_mode.load();
|
||||
const int max_retries = is_initial ? m_initial_retry_count : m_normal_retry_count;
|
||||
const int delay_ms = is_initial ? m_initial_retry_delay_ms : m_normal_retry_delay_ms;
|
||||
const bool infinite_retries = is_initial && (m_initial_retry_count == 0);
|
||||
|
||||
// For initial connection, 0 means infinite retries
|
||||
int attempt = 1;
|
||||
while (infinite_retries || attempt <= max_retries) {
|
||||
try {
|
||||
std::forward<Func>(func)();
|
||||
// First successful call - switch to normal mode
|
||||
bool was_initial = m_initial_connection_mode.exchange(false);
|
||||
// Clear CommunicationFault error if communication is restored
|
||||
// Only clear if we're not in initial connection mode (i.e., we've had
|
||||
// at least one successful operation)
|
||||
if (m_clear_error_handler && !was_initial) {
|
||||
m_clear_error_handler();
|
||||
}
|
||||
return;
|
||||
} catch (const ModbusTimeoutException& e) {
|
||||
// Timeout errors should raise CommunicationFault
|
||||
const bool should_retry = infinite_retries ? true : attempt < max_retries;
|
||||
if (should_retry) {
|
||||
if (infinite_retries) {
|
||||
EVLOG_warning << "Modbus operation failed (attempt " << attempt
|
||||
<< ", infinite retries): " << e.what() << ". Retrying in " << delay_ms << "ms...";
|
||||
} else {
|
||||
EVLOG_warning << "Modbus operation failed (attempt " << attempt << "/" << max_retries
|
||||
<< "): " << e.what() << ". Retrying in " << delay_ms << "ms...";
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
|
||||
} else {
|
||||
EVLOG_error << "Modbus operation failed after " << attempt << " attempts: " << e.what();
|
||||
// Raise CommunicationFault error for timeout errors
|
||||
if (m_error_handler) {
|
||||
m_error_handler("Modbus communication error: " + std::string(e.what()));
|
||||
}
|
||||
rethrow_exception(std::current_exception());
|
||||
}
|
||||
attempt++;
|
||||
} catch (const std::exception& e) {
|
||||
// Other errors (non-timeout) should not raise CommunicationFault
|
||||
const bool should_retry = infinite_retries ? true : attempt < max_retries;
|
||||
if (should_retry) {
|
||||
if (infinite_retries) {
|
||||
EVLOG_warning << "Modbus operation failed (attempt " << attempt
|
||||
<< ", infinite retries): " << e.what() << ". Retrying in " << delay_ms << "ms...";
|
||||
} else {
|
||||
EVLOG_warning << "Modbus operation failed (attempt " << attempt << "/" << max_retries
|
||||
<< "): " << e.what() << ". Retrying in " << delay_ms << "ms...";
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
|
||||
} else {
|
||||
EVLOG_error << "Modbus operation failed after " << attempt << " attempts: " << e.what();
|
||||
// Don't raise CommunicationFault for non-timeout errors
|
||||
rethrow_exception(std::current_exception());
|
||||
}
|
||||
attempt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
struct RetryConfig {
|
||||
std::int32_t initial_retry_count;
|
||||
std::int32_t initial_retry_delay_ms;
|
||||
std::int32_t normal_retry_count;
|
||||
std::int32_t normal_retry_delay_ms;
|
||||
};
|
||||
|
||||
struct TransportConfig {
|
||||
std::int32_t device_id;
|
||||
std::int32_t base_address;
|
||||
RetryConfig retry;
|
||||
};
|
||||
|
||||
SerialCommHubTransport(serial_communication_hubIntf& serial_hub, TransportConfig config) :
|
||||
SerialCommHubTransport(serial_hub, config, nullptr, nullptr) {
|
||||
}
|
||||
|
||||
SerialCommHubTransport(serial_communication_hubIntf& serial_hub, TransportConfig config, ErrorHandler error_handler,
|
||||
ClearErrorHandler clear_error_handler) :
|
||||
m_serial_hub(serial_hub),
|
||||
m_device_id(config.device_id),
|
||||
m_base_address(config.base_address),
|
||||
m_initial_retry_count(config.retry.initial_retry_count),
|
||||
m_initial_retry_delay_ms(config.retry.initial_retry_delay_ms),
|
||||
m_normal_retry_count(config.retry.normal_retry_count),
|
||||
m_normal_retry_delay_ms(config.retry.normal_retry_delay_ms),
|
||||
m_error_handler(std::move(error_handler)),
|
||||
m_clear_error_handler(std::move(clear_error_handler)) {
|
||||
}
|
||||
|
||||
transport::DataVector fetch(std::int32_t address, std::uint16_t register_count) override;
|
||||
void write_multiple_registers(std::int32_t address, const std::vector<std::uint16_t>& data) override;
|
||||
};
|
||||
|
||||
} // namespace transport
|
||||
|
||||
#endif // POWERMETER_TRANSPORT_HPP
|
||||
@@ -0,0 +1,76 @@
|
||||
description: Carlo Gavazzi EM580 powermeter
|
||||
provides:
|
||||
main:
|
||||
description: Implementation of the driver functionality
|
||||
interface: powermeter
|
||||
config:
|
||||
powermeter_device_id:
|
||||
description: The powermeter's address on the serial bus
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 255
|
||||
default: 1
|
||||
communication_retry_count:
|
||||
description: Number of retries for communication operations before giving up.
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 3
|
||||
communication_retry_delay_ms:
|
||||
description: Delay in milliseconds between retry attempts.
|
||||
type: integer
|
||||
minimum: 10
|
||||
maximum: 10000
|
||||
default: 500
|
||||
communication_error_pause_delay_s:
|
||||
description: Delay in seconds before retrying communication in the live measurement thread after a failure. Default 10 seconds. Applies to initial communication too.
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 600
|
||||
default: 10
|
||||
initial_connection_retry_count:
|
||||
description: Number of retries for initial connection/signature config read during module initialization. 0 means infinite retries.
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
default: 10
|
||||
initial_connection_retry_delay_ms:
|
||||
description: Delay in milliseconds between retry attempts during initialization.
|
||||
type: integer
|
||||
minimum: 100
|
||||
maximum: 60000
|
||||
default: 2000
|
||||
timezone_offset_minutes:
|
||||
description: Timezone offset from UTC in minutes (e.g., 60 for UTC+1, -300 for UTC-5). Range -1440 to +1440 minutes. Default is 0 (UTC).
|
||||
type: integer
|
||||
minimum: -1440
|
||||
maximum: 1440
|
||||
default: 0
|
||||
live_measurement_interval_ms:
|
||||
description: Interval in milliseconds between live powermeter reads and publishes. Default 1000 ms (once per second). Allowed range 500-60000 ms (twice per second to once per minute).
|
||||
type: integer
|
||||
minimum: 500
|
||||
maximum: 60000
|
||||
default: 1000
|
||||
device_state_read_interval_ms:
|
||||
description: Interval in milliseconds between reading the device state bitfield (used for VendorError reporting). Default 10000 ms (once per 10 seconds). Allowed range 500-60000 ms.
|
||||
type: integer
|
||||
minimum: 500
|
||||
maximum: 60000
|
||||
default: 10000
|
||||
public_key_format:
|
||||
description: The key format to use for the public key.
|
||||
type: string
|
||||
enum:
|
||||
- binary
|
||||
- der
|
||||
default: binary
|
||||
requires:
|
||||
modbus:
|
||||
interface: serial_communication_hub
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- florin.mihut@pionix.com
|
||||
enable_external_mqtt: false
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# OCMF Signature Validation
|
||||
|
||||
This directory contains tools for validating OCMF (Open Charge Metering Format) signatures from the Carlo Gavazzi EM580 powermeter.
|
||||
|
||||
## Overview
|
||||
|
||||
The EM580 device signs OCMF transaction data using **ECDSA-brainpoolP384r1-SHA256**. This validation tool verifies the authenticity of OCMF data by checking the digital signature against the device's public key.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
Install the required Python library:
|
||||
|
||||
```bash
|
||||
pip install cryptography
|
||||
```
|
||||
|
||||
Or if using Nix:
|
||||
|
||||
```bash
|
||||
nix-shell -p "python3.withPackages (ps: with ps; [ cryptography ])"
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- **`validate_ocmf_signature.py`** - Main validation script
|
||||
- **`test_validation.sh`** - Convenience script for quick testing
|
||||
- **`README.md`** - This file
|
||||
|
||||
## Usage
|
||||
|
||||
### Method 1: Using the convenience script
|
||||
|
||||
Edit `test_validation.sh` to set your public key and OCMF data, then run:
|
||||
|
||||
```bash
|
||||
./test_validation.sh
|
||||
```
|
||||
|
||||
### Method 2: Using the validation script directly
|
||||
|
||||
#### Validate OCMF pipe-separated string format
|
||||
|
||||
The EM580 device outputs OCMF data in the format: `OCMF|<data_json>|<signature_json>`
|
||||
|
||||
```bash
|
||||
python3 validate_ocmf_signature.py \
|
||||
--public-key "04521C09090AB6A2826A613D36483A71F789F6C0D900F9A9106415EA8BE3F6AFEB5926B39E264CB3727647DA49B153370221F18048B343AC0318203F7043F840CD8BB5C9C6734C0DB46B19711AD94A0DB8F1FA854E2D60D25B33D7DDE145F61E6C" \
|
||||
--ocmf-string 'OCMF|{"FV":"1.2",...}|{"SD":"signature_hex","SA":"ECDSA-brainpoolP384r1-SHA256"}'
|
||||
```
|
||||
|
||||
Or read from a file:
|
||||
|
||||
```bash
|
||||
python3 validate_ocmf_signature.py \
|
||||
--public-key "04<194_hex_chars>" \
|
||||
--ocmf-string "$(cat ocmf_data.txt)"
|
||||
```
|
||||
|
||||
#### Validate with separate components
|
||||
|
||||
```bash
|
||||
python3 validate_ocmf_signature.py \
|
||||
--public-key "04<194_hex_chars>" \
|
||||
--text "data-to-be-signed" \
|
||||
--signature "<signature_hex>"
|
||||
```
|
||||
|
||||
#### Validate from file
|
||||
|
||||
```bash
|
||||
python3 validate_ocmf_signature.py \
|
||||
--public-key "04<194_hex_chars>" \
|
||||
--file data.json \
|
||||
--signature "<signature_hex>"
|
||||
```
|
||||
|
||||
## Public Key Format
|
||||
|
||||
The public key must be in **uncompressed format**:
|
||||
- Starts with `0x04`
|
||||
- Followed by X coordinate (48 bytes = 96 hex chars)
|
||||
- Followed by Y coordinate (48 bytes = 96 hex chars)
|
||||
- **Total: 97 bytes = 194 hex characters** for P384
|
||||
|
||||
The public key can be read from the EM580 device at Modbus register **309473** (address 2500h). For a 384-bit key, read 49 words (98 bytes), but the last byte is unused, so use only the first 97 bytes.
|
||||
|
||||
## Signature Format
|
||||
|
||||
The signature can be in two formats:
|
||||
1. **DER format** (ASN.1 encoded) - most common, typically 102-110 bytes
|
||||
2. **Raw format**: r || s (each 48 bytes for P384, total 96 bytes = 192 hex chars)
|
||||
|
||||
The script automatically detects the format.
|
||||
|
||||
## OCMF Data Format
|
||||
|
||||
The EM580 device outputs OCMF data in a pipe-separated format:
|
||||
|
||||
```
|
||||
OCMF|<data_json>|<signature_json>
|
||||
```
|
||||
|
||||
Where:
|
||||
- `<data_json>` - JSON object containing all meter data (FV, GI, GS, RD, etc.)
|
||||
- `<signature_json>` - JSON object with:
|
||||
- `SD`: The signature in hex format
|
||||
- `SA`: The signature algorithm (e.g., "ECDSA-brainpoolP384r1-SHA256")
|
||||
|
||||
## JSON Normalization
|
||||
|
||||
**Important**: OCMF requires signatures to be computed over **compact JSON** (no spaces). The validation script automatically normalizes JSON to compact format before verification.
|
||||
|
||||
Example:
|
||||
- Original: `{"LI": 99,"LR": 0}`
|
||||
- Compact: `{"LI":99,"LR":0}`
|
||||
|
||||
The script handles this normalization automatically.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
Loading public key...
|
||||
✓ Public key loaded (brainpoolP384r1)
|
||||
|
||||
✓ Parsed OCMF string format
|
||||
Data length: 828 characters
|
||||
Signature length: 204 hex characters
|
||||
|
||||
⚠ JSON normalization: Original had 828 chars, compact has 825 chars
|
||||
Using compact JSON format for signature verification (OCMF requirement)
|
||||
Original hash: acafca116bd433ed0a8ad1200de600adf977d9bdef966bdecb3ec1c3cda2fdcc
|
||||
Compact hash: fa3020425aaf1d03f8e2bce13f76e60cb098b3bff1664d1d45503b0d9c6b351b
|
||||
|
||||
Verifying signature...
|
||||
Algorithm: ECDSA-brainpoolP384r1-SHA256
|
||||
Message length: 825 characters (825 bytes)
|
||||
Message hash (SHA256): fa3020425aaf1d03f8e2bce13f76e60cb098b3bff1664d1d45503b0d9c6b351b
|
||||
Message preview (first 100 chars): {"FV":"1.2","GI":"Carlo Gavazzi Controls-EM580DINAV23XS3DET","GS":"KZ1660104001D"...
|
||||
|
||||
✓ SIGNATURE VALID - The message is authentic!
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Signature verification fails
|
||||
|
||||
If signature verification fails, check:
|
||||
|
||||
1. **Public key**: Ensure it matches the device's current public key (read from register 309473)
|
||||
2. **Signature**: Ensure it's from the same transaction as the data
|
||||
3. **Data format**: The script automatically normalizes JSON, but verify the data hasn't been modified
|
||||
4. **Key/Signature pair**: The public key and signature must be from the same device and transaction
|
||||
|
||||
### Common errors
|
||||
|
||||
- **"Expected 97 bytes for uncompressed P384 public key"**: The public key format is incorrect. Ensure it's 194 hex characters (97 bytes) starting with `04`.
|
||||
- **"Invalid hex string"**: Check that the public key and signature contain only valid hexadecimal characters (0-9, A-F).
|
||||
- **"Signature format not recognized"**: The signature should be either DER format (starts with 0x30) or raw format (96 bytes).
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Algorithm
|
||||
- **Curve**: brainpoolP384r1 (Brainpool P-384)
|
||||
- **Hash**: SHA-256
|
||||
- **Signature**: ECDSA
|
||||
|
||||
### Data-to-be-signed
|
||||
The device signs the **compact JSON representation** of the OCMF data (the `<data_json>` part, without the "OCMF|" prefix or signature JSON).
|
||||
|
||||
### Byte Order
|
||||
- Public key: Uncompressed format (0x04 || X || Y), big-endian
|
||||
- Signature: DER format (ASN.1) or raw (r || s), big-endian
|
||||
|
||||
## References
|
||||
|
||||
- [OCMF Specification](https://github.com/SAFE-eV/OCMF-Open-Charge-Metering-Format)
|
||||
- EM580 Modbus Communication Protocol document (Table 4.19, 4.21)
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Quick test script for OCMF validation
|
||||
#
|
||||
# Usage:
|
||||
# 1. Edit this script to set your PUBLIC_KEY and OCMF_DATA_FILE
|
||||
# 2. Run: ./test_validation.sh
|
||||
#
|
||||
# Or set environment variables:
|
||||
# PUBLIC_KEY="04..." OCMF_DATA_FILE="path/to/ocmf.txt" ./test_validation.sh
|
||||
|
||||
# Default values - edit these or set as environment variables
|
||||
PUBLIC_KEY="${PUBLIC_KEY:-04521C09090AB6A2826A613D36483A71F789F6C0D900F9A9106415EA8BE3F6AFEB5926B39E264CB3727647DA49B153370221F18048B343AC0318203F7043F840CD8BB5C9C6734C0DB46B19711AD94A0DB8F1FA854E2D60D25B33D7DDE145F61E6C}"
|
||||
OCMF_DATA_FILE="${OCMF_DATA_FILE:-./text.txt}"
|
||||
|
||||
# Check if OCMF data file exists
|
||||
if [ ! -f "$OCMF_DATA_FILE" ]; then
|
||||
echo "Error: OCMF data file not found: $OCMF_DATA_FILE"
|
||||
echo "Please set OCMF_DATA_FILE environment variable or edit this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OCMF_DATA=$(cat "$OCMF_DATA_FILE")
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
python3 "$SCRIPT_DIR/validate_ocmf_signature.py" \
|
||||
--public-key "$PUBLIC_KEY" \
|
||||
--ocmf-string "$OCMF_DATA"
|
||||
@@ -0,0 +1 @@
|
||||
OCMF|{"FV":"1.2","GI":"Carlo Gavazzi Controls-EM580DINAV23XS3DET","GS":"KZ1660104001D","GV":"M_1.6.3-C_1.6.3","PG":"T23","MV":"Carlo Gavazzi Controls","MM":"EM580DINAV23XS3DET","MS":"KZ1660104001D","MF":"M_1.6.3-C_1.6.3","IS":true,"IL":"NONE","IF":[],"IT":"ISO14443","ID":"A1z */-+.()[]{}$%^&*_+-=[];',","TT":"This-is-just-a-long-string-to-test-the-tariff-text-functionality.No-spaces-are-allowed.The-kWh-price-is-0.30-EUR/kWh-just-joking-it-is-2.30-EUR/kWh<=>12345678-1234-5678-1234-567812345678","CT":"EVSEID","CI":"DE*ENBW*BER001*EVSE01","LC":{"LN":"CABLE_LOSS","LI": 99,"LR": 0,"LU": "mOhm"},"RD":[{"TM":"2025-12-17T12:09:16,000+0100 S","TX":"B","RV":1.637,"RI":"1-b:1.8.0","RU":"kWh","RT":"AC","RM":"","ST":"G"},{"TM":"2025-12-17T12:30:30,000+0100 S","TX":"E","RV":1.643,"RI":"1-b:1.8.0","RU":"kWh","RT":"AC","RM":"","ST":"G"}]}|{"SD":"306402306ECEF6E68BF22926278DF470DEA50E12DACA2DCBC54F6EED7B73276EC22795F9D48795608D03EE4639EE11EC7013BC980230633380379E601677F1C1DC0958FE421722ABA8361E30019B34463B9A038229E5063EB54DBDBC9EA63E3F069384FDB72C","SA":"ECDSA-brainpoolP384r1-SHA256"}
|
||||
@@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OCMF Signature Validation Script
|
||||
|
||||
Validates ECDSA signatures using brainpoolP384r1 curve and SHA256 hash algorithm.
|
||||
This script can be used to verify the authenticity of OCMF (Open Charge Metering Format) data.
|
||||
|
||||
Usage:
|
||||
python3 validate_ocmf_signature.py --public-key <hex_key> --text <message> --signature <hex_signature>
|
||||
python3 validate_ocmf_signature.py --public-key <hex_key> --file <json_file> --signature <hex_signature>
|
||||
python3 validate_ocmf_signature.py --public-key <hex_key> --ocmf-json <ocmf_json_string>
|
||||
|
||||
The signature should be in DER format (hex encoded).
|
||||
The public key should be in uncompressed format (hex encoded, 97 bytes = 194 hex chars for P384).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from hashlib import sha256
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature, decode_dss_signature
|
||||
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
|
||||
except ImportError:
|
||||
print("Error: cryptography library is required. Install it with: pip install cryptography")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def parse_ocmf_json(ocmf_json_str):
|
||||
"""
|
||||
Parse OCMF JSON string and extract the data-to-be-signed and signature.
|
||||
|
||||
OCMF format structure:
|
||||
{
|
||||
"SD": "data-to-be-signed",
|
||||
"SA": "signature-algorithm",
|
||||
"SI": "signature"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
ocmf_data = json.loads(ocmf_json_str)
|
||||
if "SD" not in ocmf_data or "SI" not in ocmf_data:
|
||||
raise ValueError("OCMF JSON must contain 'SD' (data) and 'SI' (signature) fields")
|
||||
return ocmf_data["SD"], ocmf_data["SI"]
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON format: {e}")
|
||||
|
||||
|
||||
def parse_ocmf_string(ocmf_str):
|
||||
"""
|
||||
Parse OCMF pipe-separated string format: OCMF|<data_json>|<signature_json>
|
||||
|
||||
The signature_json should contain "SD" field with the signature hex.
|
||||
"""
|
||||
parts = ocmf_str.split("|", 2)
|
||||
if len(parts) != 3 or parts[0] != "OCMF":
|
||||
raise ValueError("Invalid OCMF string format. Expected: OCMF|<data_json>|<signature_json>")
|
||||
|
||||
data_json = parts[1]
|
||||
signature_json_str = parts[2]
|
||||
|
||||
# Parse signature JSON to get SD field
|
||||
try:
|
||||
signature_json = json.loads(signature_json_str)
|
||||
signature_hex = signature_json.get("SD", "")
|
||||
if not signature_hex:
|
||||
raise ValueError("Signature JSON must contain 'SD' field")
|
||||
return data_json, signature_hex
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid signature JSON format: {e}")
|
||||
|
||||
|
||||
def hex_to_bytes(hex_str):
|
||||
"""Convert hex string to bytes, handling both with and without 0x prefix."""
|
||||
hex_str = hex_str.strip()
|
||||
if hex_str.startswith("0x") or hex_str.startswith("0X"):
|
||||
hex_str = hex_str[2:]
|
||||
# Remove any whitespace or separators
|
||||
hex_str = hex_str.replace(" ", "").replace(":", "").replace("-", "")
|
||||
try:
|
||||
return bytes.fromhex(hex_str)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid hex string: {e}")
|
||||
|
||||
|
||||
def load_public_key_from_hex(public_key_hex):
|
||||
"""
|
||||
Load ECDSA public key from hex string.
|
||||
|
||||
For brainpoolP384r1:
|
||||
- Uncompressed format: 0x04 || X || Y (97 bytes = 194 hex chars)
|
||||
- X and Y are each 48 bytes (96 hex chars)
|
||||
"""
|
||||
public_key_bytes = hex_to_bytes(public_key_hex)
|
||||
|
||||
# Accept DER-encoded SubjectPublicKeyInfo (starts with 0x30) as well.
|
||||
# This is the format typically returned by devices in a DER key block.
|
||||
if len(public_key_bytes) >= 2 and public_key_bytes[0] == 0x30:
|
||||
try:
|
||||
key = serialization.load_der_public_key(public_key_bytes, backend=default_backend())
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to load DER public key: {e}")
|
||||
if not isinstance(key, ec.EllipticCurvePublicKey):
|
||||
raise ValueError("DER public key is not an EC public key")
|
||||
if not isinstance(key.curve, ec.BrainpoolP384R1):
|
||||
raise ValueError(f"DER public key curve mismatch: expected brainpoolP384r1, got {type(key.curve).__name__}")
|
||||
return key
|
||||
|
||||
# For P384, uncompressed key should be 97 bytes (0x04 + 48 bytes X + 48 bytes Y)
|
||||
if len(public_key_bytes) != 97:
|
||||
raise ValueError(f"Expected 97 bytes for uncompressed P384 public key, got {len(public_key_bytes)} bytes")
|
||||
|
||||
if public_key_bytes[0] != 0x04:
|
||||
raise ValueError("Uncompressed public key must start with 0x04")
|
||||
|
||||
# Extract X and Y coordinates (each 48 bytes)
|
||||
x = public_key_bytes[1:49]
|
||||
y = public_key_bytes[49:97]
|
||||
|
||||
# Create public key using brainpoolP384r1 curve
|
||||
public_numbers = ec.EllipticCurvePublicNumbers(
|
||||
int.from_bytes(x, byteorder='big'),
|
||||
int.from_bytes(y, byteorder='big'),
|
||||
ec.BrainpoolP384R1()
|
||||
)
|
||||
|
||||
return public_numbers.public_key(default_backend())
|
||||
|
||||
|
||||
def decode_signature(signature_hex):
|
||||
"""
|
||||
Decode signature from hex string.
|
||||
|
||||
The signature can be in two formats:
|
||||
1. DER encoded (ASN.1 format) - standard for ECDSA
|
||||
2. Raw format: r || s (each 48 bytes for P384)
|
||||
"""
|
||||
signature_bytes = hex_to_bytes(signature_hex)
|
||||
|
||||
# Try DER format first (most common)
|
||||
try:
|
||||
# For P384, DER signature is typically around 104-110 bytes
|
||||
# Try to decode as DER
|
||||
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
|
||||
r, s = decode_dss_signature(signature_bytes)
|
||||
return r, s
|
||||
except Exception:
|
||||
# If DER fails, try raw format: r || s (each 48 bytes = 96 bytes total for P384)
|
||||
if len(signature_bytes) == 96:
|
||||
r = int.from_bytes(signature_bytes[:48], byteorder='big')
|
||||
s = int.from_bytes(signature_bytes[48:], byteorder='big')
|
||||
return r, s
|
||||
else:
|
||||
raise ValueError(f"Signature format not recognized. Expected DER or 96-byte raw format, got {len(signature_bytes)} bytes")
|
||||
|
||||
|
||||
def verify_signature(public_key, message, signature_hex):
|
||||
"""
|
||||
Verify ECDSA signature using brainpoolP384r1 and SHA256.
|
||||
|
||||
Args:
|
||||
public_key: ECDSA public key object
|
||||
message: The message/text to verify (string or bytes)
|
||||
signature_hex: Signature in hex format (DER or raw)
|
||||
|
||||
Returns:
|
||||
bool: True if signature is valid, False otherwise
|
||||
"""
|
||||
# Convert message to bytes if it's a string
|
||||
if isinstance(message, str):
|
||||
message_bytes = message.encode('utf-8')
|
||||
else:
|
||||
message_bytes = message
|
||||
|
||||
# Decode signature
|
||||
r, s = decode_signature(signature_hex)
|
||||
|
||||
# Verify signature
|
||||
try:
|
||||
public_key.verify(
|
||||
encode_dss_signature(r, s),
|
||||
message_bytes,
|
||||
ec.ECDSA(hashes.SHA256())
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Signature verification failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def verify_signature_prehashed(public_key, message, signature_hex):
|
||||
"""
|
||||
Verify signature where the device signs SHA256(message) directly (pre-hashed ECDSA).
|
||||
"""
|
||||
# Convert message to bytes if it's a string
|
||||
if isinstance(message, str):
|
||||
message_bytes = message.encode('utf-8')
|
||||
else:
|
||||
message_bytes = message
|
||||
|
||||
digest = sha256(message_bytes).digest()
|
||||
r, s = decode_signature(signature_hex)
|
||||
|
||||
try:
|
||||
public_key.verify(
|
||||
encode_dss_signature(r, s),
|
||||
digest,
|
||||
ec.ECDSA(Prehashed(hashes.SHA256()))
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Prehashed signature verification failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate ECDSA-brainpoolP384r1-SHA256 signatures for OCMF data",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Validate with separate components
|
||||
python3 validate_ocmf_signature.py \\
|
||||
--public-key "04<194_hex_chars>" \\
|
||||
--text "data-to-be-signed" \\
|
||||
--signature "<signature_hex>"
|
||||
|
||||
# Validate from OCMF pipe-separated string
|
||||
python3 validate_ocmf_signature.py \\
|
||||
--public-key "04<194_hex_chars>" \\
|
||||
--ocmf-string 'OCMF|{"data":"..."}|{"SD":"signature","SA":"ECDSA-brainpoolP384r1-SHA256"}'
|
||||
|
||||
# Validate from OCMF JSON string
|
||||
python3 validate_ocmf_signature.py \\
|
||||
--public-key "04<194_hex_chars>" \\
|
||||
--ocmf-json '{"SD":"data","SA":"ECDSA-brainpoolP384r1-SHA256","SI":"signature"}'
|
||||
|
||||
# Validate from file
|
||||
python3 validate_ocmf_signature.py \\
|
||||
--public-key "04<194_hex_chars>" \\
|
||||
--file ocmf_data.json \\
|
||||
--signature "<signature_hex>"
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--public-key',
|
||||
required=True,
|
||||
help='Public key in hex format (uncompressed, 194 hex chars for P384)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--text',
|
||||
help='The text/message to verify (data-to-be-signed)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--signature',
|
||||
help='Signature in hex format (DER or raw r||s format)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--file',
|
||||
help='Read text from file (UTF-8)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--ocmf-json',
|
||||
help='OCMF JSON string containing SD (data) and SI (signature) fields'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--ocmf-string',
|
||||
help='OCMF pipe-separated string format: OCMF|<data_json>|<signature_json>'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--dump-candidates',
|
||||
action='store_true',
|
||||
help='Dump the exact message candidates that are attempted for verification (also written to /tmp).'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load public key
|
||||
try:
|
||||
print("Loading public key...")
|
||||
public_key = load_public_key_from_hex(args.public_key)
|
||||
print(f"✓ Public key loaded (brainpoolP384r1)")
|
||||
except Exception as e:
|
||||
print(f"✗ Error loading public key: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Determine message and signature
|
||||
message = None
|
||||
signature = None
|
||||
|
||||
if args.ocmf_string:
|
||||
# Parse OCMF pipe-separated string
|
||||
try:
|
||||
message, signature = parse_ocmf_string(args.ocmf_string)
|
||||
print(f"✓ Parsed OCMF string format")
|
||||
print(f" Data length: {len(message)} characters")
|
||||
print(f" Signature length: {len(signature)} hex characters")
|
||||
except Exception as e:
|
||||
print(f"✗ Error parsing OCMF string: {e}")
|
||||
sys.exit(1)
|
||||
elif args.ocmf_json:
|
||||
# Parse OCMF JSON
|
||||
try:
|
||||
message, signature = parse_ocmf_json(args.ocmf_json)
|
||||
print(f"✓ Parsed OCMF JSON")
|
||||
print(f" Data length: {len(message)} characters")
|
||||
print(f" Signature length: {len(signature)} hex characters")
|
||||
except Exception as e:
|
||||
print(f"✗ Error parsing OCMF JSON: {e}")
|
||||
sys.exit(1)
|
||||
elif args.file:
|
||||
# Read from file
|
||||
if not args.signature:
|
||||
print("✗ Error: --signature is required when using --file")
|
||||
sys.exit(1)
|
||||
try:
|
||||
with open(args.file, 'r', encoding='utf-8') as f:
|
||||
message = f.read()
|
||||
signature = args.signature
|
||||
print(f"✓ Read message from file: {args.file}")
|
||||
print(f" Message length: {len(message)} characters")
|
||||
except Exception as e:
|
||||
print(f"✗ Error reading file: {e}")
|
||||
sys.exit(1)
|
||||
elif args.text and args.signature:
|
||||
# Direct text and signature
|
||||
message = args.text
|
||||
signature = args.signature
|
||||
print(f"✓ Using provided text and signature")
|
||||
print(f" Message length: {len(message)} characters")
|
||||
else:
|
||||
print("✗ Error: Must provide either --ocmf-string, --ocmf-json, or (--text and --signature), or (--file and --signature)")
|
||||
sys.exit(1)
|
||||
|
||||
# Normalize JSON for OCMF (compact format, no spaces)
|
||||
# NOTE: Do NOT normalize / compact JSON. We verify exactly the extracted JSON bytes.
|
||||
# If the device signs compact JSON, the device output must already be compact.
|
||||
|
||||
# Verify signature
|
||||
print("\nVerifying signature...")
|
||||
print(f" Algorithm: ECDSA-brainpoolP384r1-SHA256")
|
||||
|
||||
# Double-check what we're about to hash
|
||||
message_bytes = message.encode('utf-8') if isinstance(message, str) else message
|
||||
final_hash = sha256(message_bytes).hexdigest()
|
||||
print(f" Message length: {len(message)} characters ({len(message_bytes)} bytes)")
|
||||
print(f" Message hash (SHA256): {final_hash}")
|
||||
print(f" Message preview (first 100 chars): {message[:100]}...")
|
||||
|
||||
try:
|
||||
# Verify exactly what we received as <data_json> from the OCMF pipe string.
|
||||
candidates = [("json_exact", message)]
|
||||
if isinstance(message, str):
|
||||
candidates.extend(
|
||||
[
|
||||
("ocmf_prefix_json_exact", "OCMF|" + message),
|
||||
("json_exact_nullterm", message + "\x00"),
|
||||
("ocmf_prefix_json_exact_nullterm", "OCMF|" + message + "\x00"),
|
||||
]
|
||||
)
|
||||
|
||||
def dump_candidate(label, candidate_value):
|
||||
if not args.dump_candidates:
|
||||
return
|
||||
if isinstance(candidate_value, str):
|
||||
b = candidate_value.encode("utf-8")
|
||||
else:
|
||||
b = candidate_value
|
||||
print(f"\n--- candidate:{label} ---")
|
||||
print(f"len(chars)={len(candidate_value) if isinstance(candidate_value, str) else 'n/a'} len(bytes)={len(b)}")
|
||||
print("sha256(bytes)=", sha256(b).hexdigest())
|
||||
# Show a safe representation so NUL and other non-printables are visible.
|
||||
preview = candidate_value if isinstance(candidate_value, str) else b.decode("utf-8", errors="replace")
|
||||
print("repr=", repr(preview))
|
||||
out_path = f"/tmp/ocmf_message_candidate_{label}.txt"
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(b)
|
||||
print("written=", out_path)
|
||||
|
||||
for name, candidate in candidates:
|
||||
dump_candidate(name, candidate)
|
||||
print(f"\nAttempt: {name} (standard ECDSA over message bytes)")
|
||||
if verify_signature(public_key, candidate, signature):
|
||||
print("\n✓ SIGNATURE VALID - The message is authentic!")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"Attempt: {name} (prehashed ECDSA over SHA256(message))")
|
||||
if verify_signature_prehashed(public_key, candidate, signature):
|
||||
print("\n✓ SIGNATURE VALID - The message is authentic!")
|
||||
sys.exit(0)
|
||||
|
||||
# Some devices sign the ASCII hex digest rather than the raw message (rare, but cheap to test).
|
||||
if isinstance(candidate, str):
|
||||
digest_hex = sha256(candidate.encode("utf-8")).hexdigest()
|
||||
dump_candidate(f"{name}_sha256hex", digest_hex)
|
||||
print(f"Attempt: {name} (standard ECDSA over sha256(message).hexdigest() bytes)")
|
||||
if verify_signature(public_key, digest_hex, signature):
|
||||
print("\n✓ SIGNATURE VALID - The message is authentic!")
|
||||
sys.exit(0)
|
||||
print(f"Attempt: {name} (prehashed ECDSA over SHA256(sha256(message).hexdigest()))")
|
||||
if verify_signature_prehashed(public_key, digest_hex, signature):
|
||||
print("\n✓ SIGNATURE VALID - The message is authentic!")
|
||||
sys.exit(0)
|
||||
|
||||
print("\n✗ SIGNATURE INVALID - none of the tried variants matched.")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n✗ Error during verification: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
set(TEST_TARGET_NAME ${PROJECT_NAME}_carlo_gavazzi_em580_helper_tests)
|
||||
|
||||
set(MODULE_DIR "${PROJECT_SOURCE_DIR}/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580")
|
||||
|
||||
add_executable(${TEST_TARGET_NAME}
|
||||
test_em580_helper.cpp
|
||||
test_em580_powermeter_impl.cpp
|
||||
${MODULE_DIR}/main/powermeterImpl.cpp
|
||||
${MODULE_DIR}/main/transport.cpp
|
||||
)
|
||||
|
||||
add_dependencies(${TEST_TARGET_NAME} ${MODULE_NAME})
|
||||
|
||||
set(INCLUDE_DIR
|
||||
"main"
|
||||
"tests"
|
||||
"${MODULE_DIR}/main"
|
||||
"${MODULE_DIR}/tests"
|
||||
)
|
||||
|
||||
get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR)
|
||||
|
||||
target_include_directories(${TEST_TARGET_NAME} PRIVATE
|
||||
tests
|
||||
${INCLUDE_DIR}
|
||||
${GENERATED_INCLUDE_DIR}
|
||||
${CMAKE_BINARY_DIR}/generated/modules/CarloGavazzi_EM580
|
||||
)
|
||||
|
||||
target_link_libraries(${TEST_TARGET_NAME} PRIVATE
|
||||
GTest::gtest_main
|
||||
everest::framework
|
||||
)
|
||||
|
||||
add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME})
|
||||
ev_register_test_target(${TEST_TARGET_NAME})
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
EVEREST_DIR="$(cd -- "${SCRIPT_DIR}/../../../../.." && pwd)"
|
||||
DIST_DIR="${EVEREST_DIR}/build/dist"
|
||||
DIST_ETC_DIR="${DIST_DIR}/etc/everest"
|
||||
MANAGER_BIN="${DIST_DIR}/bin/manager"
|
||||
BUPOWERMETER_BIN="${DIST_DIR}/libexec/everest/modules/BUPowermeter/BUPowermeter"
|
||||
|
||||
if [[ ! -x "${MANAGER_BIN}" ]]; then
|
||||
echo "ERROR: manager binary not found/executable at: ${MANAGER_BIN}" >&2
|
||||
echo "Did you build EVerest and generate the dist/ folder?" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -x "${BUPOWERMETER_BIN}" ]]; then
|
||||
echo "ERROR: BUPowermeter binary not found/executable at: ${BUPOWERMETER_BIN}" >&2
|
||||
echo "Did you build EVerest and generate the dist/ folder?" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "${DIST_ETC_DIR}" ]]; then
|
||||
echo "ERROR: dist etc dir not found at: ${DIST_ETC_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
other_pids=()
|
||||
|
||||
cleanup() {
|
||||
for pid in "${other_pids[@]:-}"; do
|
||||
kill "${pid}" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
read -r -p "Start CGEM580 bringup with 1, 6, 7, 12 or 13 devices? [1/6/7/12/13]: " device_count
|
||||
|
||||
case "${device_count}" in
|
||||
1)
|
||||
config_file="config-bringup-CGEM580.yaml"
|
||||
;;
|
||||
6)
|
||||
config_file="config-bringup-CGEM580-6x.yaml"
|
||||
;;
|
||||
7)
|
||||
config_file="config-bringup-CGEM580-7x.yaml"
|
||||
;;
|
||||
12)
|
||||
config_file="config-bringup-CGEM580-12x.yaml"
|
||||
;;
|
||||
13)
|
||||
config_file="config-bringup-CGEM580-13x.yaml"
|
||||
;;
|
||||
*)
|
||||
echo "Invalid choice: '${device_count}'. Please enter 1, 6, 7, 12 or 13."
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! -f "${DIST_ETC_DIR}/${config_file}" ]]; then
|
||||
echo "ERROR: config file not found at: ${DIST_ETC_DIR}/${config_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start manager first, then powermeters. When the manager window closes, all others will be closed as well.
|
||||
# Important: run with CWD in ${DIST_ETC_DIR} (matches previous scripts and avoids any CWD-sensitive behavior).
|
||||
xterm -bg black -fg white -geometry 400x150 -e bash -lc "cd \"${DIST_ETC_DIR}\" && \"${MANAGER_BIN}\" --prefix \"${DIST_DIR}\" --conf \"${config_file}\"" &
|
||||
manager_pid=$!
|
||||
|
||||
for i in $(seq 1 "${device_count}"); do
|
||||
xterm -bg black -fg white -geometry 200x55 -e bash -lc "cd \"${DIST_ETC_DIR}\" && sleep 1 && \"${BUPOWERMETER_BIN}\" --module cli_${i}" &
|
||||
other_pids+=($!)
|
||||
done
|
||||
|
||||
wait "${manager_pid}"
|
||||
@@ -0,0 +1,249 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "helper.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <tuple>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kFetchCallAlignmentBytes = 8;
|
||||
constexpr std::size_t kWriteCallAlignmentBytes = 32;
|
||||
|
||||
struct alignas(kFetchCallAlignmentBytes) FetchCall {
|
||||
std::int32_t address;
|
||||
std::uint16_t register_count;
|
||||
};
|
||||
|
||||
struct alignas(kWriteCallAlignmentBytes) WriteCall {
|
||||
std::int32_t address;
|
||||
std::vector<std::uint16_t> data;
|
||||
};
|
||||
|
||||
class FakeModbusTransport : public transport::AbstractModbusTransport {
|
||||
public:
|
||||
// Script one fetch response for (address, register_count).
|
||||
void push_fetch_response(std::int32_t address, std::uint16_t register_count, transport::DataVector response) {
|
||||
scripted_fetch_[Key{address, register_count}].push_back(std::move(response));
|
||||
}
|
||||
|
||||
const std::vector<FetchCall>& fetch_calls() const {
|
||||
return fetch_calls_;
|
||||
}
|
||||
const std::vector<WriteCall>& write_calls() const {
|
||||
return write_calls_;
|
||||
}
|
||||
|
||||
transport::DataVector fetch(std::int32_t address, std::uint16_t register_count) override {
|
||||
fetch_calls_.push_back(FetchCall{address, register_count});
|
||||
|
||||
const Key key{address, register_count};
|
||||
auto iter = scripted_fetch_.find(key);
|
||||
if (iter == scripted_fetch_.end() || iter->second.empty()) {
|
||||
throw std::runtime_error("FakeModbusTransport: no scripted fetch response for address/count");
|
||||
}
|
||||
transport::DataVector out = std::move(iter->second.front());
|
||||
iter->second.pop_front();
|
||||
return out;
|
||||
}
|
||||
|
||||
void write_multiple_registers(std::int32_t address, const std::vector<std::uint16_t>& data) override {
|
||||
write_calls_.push_back(WriteCall{address, data});
|
||||
}
|
||||
|
||||
private:
|
||||
using Key = std::tuple<std::int32_t, std::uint16_t>;
|
||||
std::map<Key, std::deque<transport::DataVector>> scripted_fetch_;
|
||||
std::vector<FetchCall> fetch_calls_;
|
||||
std::vector<WriteCall> write_calls_;
|
||||
};
|
||||
|
||||
transport::DataVector u16_be(std::uint16_t value) {
|
||||
constexpr std::uint32_t kByteBits = 8U;
|
||||
constexpr std::uint32_t kByteMask = 0xFFU;
|
||||
const auto high = static_cast<std::uint8_t>((static_cast<std::uint32_t>(value) >> kByteBits) & kByteMask);
|
||||
const auto low = static_cast<std::uint8_t>(static_cast<std::uint32_t>(value) & kByteMask);
|
||||
return transport::DataVector{high, low};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(EM580Helper, ExtractTransactionIdFromTTHappyPath) {
|
||||
const std::string uuid = "12345678-1234-5678-1234-567812345678";
|
||||
const std::string ocmf = R"(OCMF|{"TT":"price-2.30-EUR/kWh<=>)" + uuid + R"(","RD":[]}|
|
||||
{"SA":"ECDSA-brainpoolP384r1-SHA256","SD":"00"})";
|
||||
|
||||
const auto tid = ocmf::extract_transaction_id_from_ocmf_record(ocmf);
|
||||
ASSERT_TRUE(tid.has_value());
|
||||
EXPECT_EQ(*tid, uuid);
|
||||
}
|
||||
|
||||
TEST(EM580Helper, ExtractTransactionIdFromTTMissingMarker) {
|
||||
const std::string ocmf = R"(OCMF|{"TT":"no-marker-here","RD":[]}|
|
||||
{"SA":"ECDSA-brainpoolP384r1-SHA256","SD":"00"})";
|
||||
|
||||
const auto tid = ocmf::extract_transaction_id_from_ocmf_record(ocmf);
|
||||
EXPECT_FALSE(tid.has_value());
|
||||
}
|
||||
|
||||
TEST(EM580Helper, MaxPayloadBytesForWords) {
|
||||
EXPECT_EQ(modbus_utils::max_payload_bytes_for_words(0), 0);
|
||||
EXPECT_EQ(modbus_utils::max_payload_bytes_for_words(1),
|
||||
1); // 2 bytes total, reserve NUL => 1
|
||||
EXPECT_EQ(modbus_utils::max_payload_bytes_for_words(126),
|
||||
251); // 252 bytes total, reserve NUL => 251
|
||||
}
|
||||
|
||||
TEST(EM580Helper, StringToModbusCharArrayZeroTerminatedAndUsedOnly) {
|
||||
const auto words = modbus_utils::string_to_modbus_char_array("AB", 126);
|
||||
ASSERT_EQ(words.size(), 2U); // 'A''B''\0' => 3 bytes => 2 words
|
||||
EXPECT_EQ(words[0], 0x4142);
|
||||
EXPECT_EQ(words[1], 0x0000);
|
||||
}
|
||||
|
||||
TEST(EM580Helper, StringToModbusCharArrayTruncatesToFitWithNul) {
|
||||
// 1 word => 2 bytes total => only 1 byte payload + NUL
|
||||
const auto words = modbus_utils::string_to_modbus_char_array("HELLO", 1);
|
||||
ASSERT_EQ(words.size(), 1U);
|
||||
EXPECT_EQ(words[0], 0x4800); // 'H' + '\0'
|
||||
}
|
||||
|
||||
TEST(EM580Helper, OcmfConfirmFileReadWritesNotReadyToStateRegister) {
|
||||
FakeModbusTransport transport;
|
||||
ocmf::confirm_file_read(transport);
|
||||
|
||||
ASSERT_EQ(transport.write_calls().size(), 1U);
|
||||
EXPECT_EQ(transport.write_calls()[0].address, em580::registers::MODBUS_OCMF_STATE_ADDRESS);
|
||||
ASSERT_EQ(transport.write_calls()[0].data.size(), 1U);
|
||||
EXPECT_EQ(transport.write_calls()[0].data[0], em580::registers::MODBUS_OCMF_STATE_NOT_READY);
|
||||
}
|
||||
|
||||
TEST(EM580Helper, OcmfWaitForReadyReachesReady) {
|
||||
FakeModbusTransport transport;
|
||||
transport.push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
|
||||
u16_be(em580::registers::MODBUS_OCMF_STATE_NOT_READY));
|
||||
transport.push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
|
||||
u16_be(em580::registers::MODBUS_OCMF_STATE_RUNNING));
|
||||
transport.push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
|
||||
u16_be(em580::registers::MODBUS_OCMF_STATE_READY));
|
||||
|
||||
const bool success = ocmf::wait_for_ready(transport, std::chrono::milliseconds{0}, 10);
|
||||
EXPECT_TRUE(success);
|
||||
EXPECT_EQ(transport.fetch_calls().size(), 3U);
|
||||
}
|
||||
|
||||
TEST(EM580Helper, OcmfWaitForReadyFailsOnCorrupted) {
|
||||
FakeModbusTransport transport;
|
||||
transport.push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
|
||||
u16_be(em580::registers::MODBUS_OCMF_STATE_CORRUPTED));
|
||||
|
||||
const bool success = ocmf::wait_for_ready(transport, std::chrono::milliseconds{0}, 10);
|
||||
EXPECT_FALSE(success);
|
||||
EXPECT_EQ(transport.fetch_calls().size(), 1U);
|
||||
}
|
||||
|
||||
TEST(EM580Helper, IsUuid36ValidAndInvalid) {
|
||||
EXPECT_TRUE(ocmf::is_uuid36("12345678-1234-5678-1234-567812345678"));
|
||||
EXPECT_TRUE(ocmf::is_uuid36("ABCDEFAB-CDEF-ABCD-EFAB-CDEFABCDEFAB"));
|
||||
|
||||
EXPECT_FALSE(ocmf::is_uuid36("12345678-1234-5678-1234-56781234567")); // too short
|
||||
EXPECT_FALSE(ocmf::is_uuid36("123456781234-5678-1234-567812345678")); // missing '-'
|
||||
EXPECT_FALSE(ocmf::is_uuid36("12345678-1234-5678-1234-56781234567Z")); // non-hex
|
||||
}
|
||||
|
||||
TEST(EM580Helper, ExtractTransactionIdFromRecordMissingTTField) {
|
||||
const std::string ocmf = R"(OCMF|{"FV":"1.2","RD":[]}|
|
||||
{"SA":"ECDSA-brainpoolP384r1-SHA256","SD":"00"})";
|
||||
|
||||
const auto tid = ocmf::extract_transaction_id_from_ocmf_record(ocmf);
|
||||
EXPECT_FALSE(tid.has_value());
|
||||
}
|
||||
|
||||
TEST(EM580Helper, ExtractTransactionIdFromRecordInvalidUuidAfterMarker) {
|
||||
const std::string ocmf = R"(OCMF|{"TT":"foo<=>not-a-uuid","RD":[]}|
|
||||
{"SA":"ECDSA-brainpoolP384r1-SHA256","SD":"00"})";
|
||||
|
||||
const auto tid = ocmf::extract_transaction_id_from_ocmf_record(ocmf);
|
||||
EXPECT_FALSE(tid.has_value());
|
||||
}
|
||||
|
||||
TEST(EM580Helper, DecodeDeviceStateErrorsReturnsMatchingMessages) {
|
||||
// Bits 0 and 13 correspond to V1N over-range and Measure module internal
|
||||
// fault.
|
||||
const auto state = static_cast<std::uint16_t>((1U << 0U) | (1U << 13U));
|
||||
const auto errors = device_state_utils::decode_device_state_errors(state);
|
||||
|
||||
ASSERT_EQ(errors.size(), 2U);
|
||||
EXPECT_EQ(errors[0], "V1N over maximum range");
|
||||
EXPECT_EQ(errors[1], "Measure module internal fault");
|
||||
}
|
||||
|
||||
TEST(EM580Helper, ModbusToUint16AndToUint32ByteOrder) {
|
||||
const transport::DataVector data = {
|
||||
0x12, 0x34, // u16 @0 => 0x1234
|
||||
0xAA, 0xBB, // u16 @2 => 0xAABB
|
||||
0xDE, 0xAD, 0xBE, 0xEF // u32 @4 => 0xDEADBEEF
|
||||
};
|
||||
|
||||
EXPECT_EQ(modbus_utils::to_uint16(data, modbus_utils::ByteOffset{0}), 0x1234);
|
||||
EXPECT_EQ(modbus_utils::to_uint16(data, modbus_utils::ByteOffset{2}), 0xAABB);
|
||||
EXPECT_EQ(modbus_utils::to_uint32(data, modbus_utils::ByteOffset{4}), 0xDEADBEEF);
|
||||
}
|
||||
|
||||
TEST(EM580Helper, StringToModbusCharArrayEmptyStringIsJustTerminator) {
|
||||
const auto words = modbus_utils::string_to_modbus_char_array("", 126);
|
||||
ASSERT_EQ(words.size(), 1U);
|
||||
EXPECT_EQ(words[0], 0x0000);
|
||||
}
|
||||
|
||||
TEST(EM580Helper, StringToModbusCharArrayPacksOddLengthAndAddsNul) {
|
||||
// "ABC\0" => 4 bytes => 2 words: 0x4142 0x4300
|
||||
const auto words = modbus_utils::string_to_modbus_char_array("ABC", 126);
|
||||
ASSERT_EQ(words.size(), 2U);
|
||||
EXPECT_EQ(words[0], 0x4142);
|
||||
EXPECT_EQ(words[1], 0x4300);
|
||||
}
|
||||
|
||||
TEST(EM580Helper, StringToModbusCharArrayTruncatesAndStillTerminates) {
|
||||
// max_words=2 => 4 bytes total => 3 payload bytes + NUL
|
||||
const auto words = modbus_utils::string_to_modbus_char_array("HELLO", 2);
|
||||
ASSERT_EQ(words.size(), 2U);
|
||||
EXPECT_EQ(words[0], 0x4845); // 'H''E'
|
||||
EXPECT_EQ(words[1], 0x4C00); // 'L''\0' (truncated)
|
||||
}
|
||||
|
||||
TEST(EM580Helper, OcmfWaitForReadyTimesOutAfterMaxRetries) {
|
||||
FakeModbusTransport transport;
|
||||
static constexpr int kNonReadyReads = 11;
|
||||
// kNonReadyReads non-ready reads => retries becomes kNonReadyReads and
|
||||
// (retries > max_retries(10)) => false
|
||||
#pragma unroll
|
||||
for (int i = 0; i < kNonReadyReads; ++i) {
|
||||
transport.push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
|
||||
u16_be(em580::registers::MODBUS_OCMF_STATE_NOT_READY));
|
||||
}
|
||||
const bool success = ocmf::wait_for_ready(transport, std::chrono::milliseconds{0}, 10);
|
||||
EXPECT_FALSE(success);
|
||||
EXPECT_EQ(transport.fetch_calls().size(), static_cast<std::size_t>(kNonReadyReads));
|
||||
}
|
||||
|
||||
TEST(EM580Helper, DecodeDeviceStateErrorsEmptyIfNoBitsSet) {
|
||||
const auto errors = device_state_utils::decode_device_state_errors(0U);
|
||||
EXPECT_TRUE(errors.empty());
|
||||
}
|
||||
|
||||
TEST(EM580Helper, ModbusToHexStringUppercaseNoSeparators) {
|
||||
const transport::DataVector data = {0x00, 0x2a, 0xAB, 0xCD, 0xEF};
|
||||
EXPECT_EQ(modbus_utils::to_hex_string(data, modbus_utils::ByteOffset{0}, modbus_utils::ByteLength{data.size()}),
|
||||
"002AABCDEF");
|
||||
}
|
||||
|
||||
TEST(EM580Helper, ModbusToInt16Sign) {
|
||||
const transport::DataVector data = {0xFF, 0xFE}; // 0xFFFE => -2
|
||||
EXPECT_EQ(modbus_utils::to_int16(data, modbus_utils::ByteOffset{0}), -2);
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "helper.hpp"
|
||||
#include "powermeterImpl.hpp"
|
||||
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kFetchCallAlignmentBytes = 8;
|
||||
constexpr std::size_t kWriteCallAlignmentBytes = 32;
|
||||
constexpr std::size_t kBytesPerRegister = 2;
|
||||
|
||||
struct alignas(kFetchCallAlignmentBytes) FetchCall {
|
||||
std::int32_t address;
|
||||
std::uint16_t register_count;
|
||||
};
|
||||
|
||||
struct alignas(kWriteCallAlignmentBytes) WriteCall {
|
||||
std::int32_t address;
|
||||
std::vector<std::uint16_t> data;
|
||||
};
|
||||
|
||||
class FakeModbusTransport : public transport::AbstractModbusTransport {
|
||||
public:
|
||||
void push_fetch_response(std::int32_t address, std::uint16_t register_count, transport::DataVector response) {
|
||||
scripted_fetch_[Key{address, register_count}].push_back(std::move(response));
|
||||
}
|
||||
|
||||
const std::vector<FetchCall>& fetch_calls() const {
|
||||
return fetch_calls_;
|
||||
}
|
||||
|
||||
const std::vector<WriteCall>& write_calls() const {
|
||||
return write_calls_;
|
||||
}
|
||||
|
||||
transport::DataVector fetch(std::int32_t address, std::uint16_t register_count) override {
|
||||
fetch_calls_.push_back(FetchCall{address, register_count});
|
||||
|
||||
const Key key{address, register_count};
|
||||
auto iter = scripted_fetch_.find(key);
|
||||
if (iter == scripted_fetch_.end() || iter->second.empty()) {
|
||||
throw std::runtime_error("FakeModbusTransport: no scripted fetch response for address/count");
|
||||
}
|
||||
transport::DataVector out = std::move(iter->second.front());
|
||||
iter->second.pop_front();
|
||||
return out;
|
||||
}
|
||||
|
||||
void write_multiple_registers(std::int32_t address, const std::vector<std::uint16_t>& data) override {
|
||||
write_calls_.push_back(WriteCall{address, data});
|
||||
}
|
||||
|
||||
private:
|
||||
using Key = std::tuple<std::int32_t, std::uint16_t>;
|
||||
std::map<Key, std::deque<transport::DataVector>> scripted_fetch_;
|
||||
std::vector<FetchCall> fetch_calls_;
|
||||
std::vector<WriteCall> write_calls_;
|
||||
};
|
||||
|
||||
transport::DataVector u16_be(std::uint16_t value) {
|
||||
constexpr std::uint32_t kByteBits = 8U;
|
||||
constexpr std::uint32_t kByteMask = 0xFFU;
|
||||
const auto high = static_cast<std::uint8_t>((static_cast<std::uint32_t>(value) >> kByteBits) & kByteMask);
|
||||
const auto low = static_cast<std::uint8_t>(static_cast<std::uint32_t>(value) & kByteMask);
|
||||
return transport::DataVector{high, low};
|
||||
}
|
||||
|
||||
transport::DataVector bytes(std::string_view str) {
|
||||
return transport::DataVector{str.begin(), str.end()};
|
||||
}
|
||||
|
||||
transport::DataVector zero_bytes_for_words(std::uint16_t words) {
|
||||
return transport::DataVector(words * kBytesPerRegister, 0U);
|
||||
}
|
||||
|
||||
module::main::Conf make_test_conf() {
|
||||
constexpr int kDefaultIntervalMs = 1000;
|
||||
module::main::Conf conf{};
|
||||
conf.powermeter_device_id = 1;
|
||||
conf.communication_retry_count = 0;
|
||||
conf.communication_retry_delay_ms = 0;
|
||||
conf.initial_connection_retry_count = 0;
|
||||
conf.initial_connection_retry_delay_ms = 0;
|
||||
conf.timezone_offset_minutes = 0;
|
||||
conf.live_measurement_interval_ms = kDefaultIntervalMs;
|
||||
conf.device_state_read_interval_ms = kDefaultIntervalMs;
|
||||
conf.communication_error_pause_delay_s = 0;
|
||||
return conf;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(EM580PowermeterImpl, StartTransactionHappyPathCountsWrites) {
|
||||
static Everest::PtrContainer<module::CarloGavazzi_EM580> dummy_mod;
|
||||
auto conf = make_test_conf();
|
||||
module::main::powermeterImpl impl(nullptr, dummy_mod, conf);
|
||||
|
||||
auto transport = std::make_unique<FakeModbusTransport>();
|
||||
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
|
||||
u16_be(em580::registers::MODBUS_OCMF_STATE_NOT_READY));
|
||||
transport->push_fetch_response(em580::registers::MODBUS_SIGNED_MAP_ADDRESS,
|
||||
em580::registers::MODBUS_SIGNED_MAP_WORD_COUNT_256,
|
||||
zero_bytes_for_words(em580::registers::MODBUS_SIGNED_MAP_WORD_COUNT_256));
|
||||
|
||||
auto* transport_ptr = transport.get();
|
||||
module::main::powermeterImpl::TestAccess::set_modbus_transport(impl, std::move(transport));
|
||||
module::main::powermeterImpl::TestAccess::set_signed_map_word_count(
|
||||
impl, em580::registers::MODBUS_SIGNED_MAP_WORD_COUNT_256);
|
||||
|
||||
types::powermeter::TransactionReq req{};
|
||||
req.evse_id = "DE*TEST*EVSE01";
|
||||
req.transaction_id = "12345678-1234-5678-1234-567812345678";
|
||||
req.identification_status = types::powermeter::OCMFUserIdentificationStatus::ASSIGNED;
|
||||
req.identification_flags = {};
|
||||
req.identification_type = types::powermeter::OCMFIdentificationType::ISO14443;
|
||||
req.identification_level = types::powermeter::OCMFIdentificationLevel::NONE;
|
||||
req.identification_data.emplace("ABC");
|
||||
req.tariff_text.emplace("TARIFF");
|
||||
|
||||
const auto resp = module::main::powermeterImpl::TestAccess::start_transaction(impl, req);
|
||||
EXPECT_EQ(resp.status, types::powermeter::TransactionRequestStatus::OK);
|
||||
|
||||
// Happy-path expectation: 8 writes from write_transaction_registers + 2
|
||||
// additional writes (session modality + 'B')
|
||||
EXPECT_EQ(transport_ptr->write_calls().size(), 10U);
|
||||
}
|
||||
|
||||
TEST(EM580PowermeterImpl, StopTransactionPendingClosedTransactionMismatchReturnsError) {
|
||||
static Everest::PtrContainer<module::CarloGavazzi_EM580> dummy_mod;
|
||||
auto conf = make_test_conf();
|
||||
module::main::powermeterImpl impl(nullptr, dummy_mod, conf);
|
||||
|
||||
auto transport = std::make_unique<FakeModbusTransport>();
|
||||
// read_ocmf_file(): size then file bytes
|
||||
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_SIZE_ADDRESS, 1, u16_be(1));
|
||||
|
||||
const std::string other_uuid = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
|
||||
const std::string ocmf_file = R"(OCMF|{"TT":"x<=>)" + other_uuid + R"("}|
|
||||
{"SA":"ECDSA-brainpoolP384r1-SHA256","SD":"00"})";
|
||||
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_FILE_ADDRESS, 1, bytes(ocmf_file));
|
||||
|
||||
auto* transport_ptr = transport.get();
|
||||
module::main::powermeterImpl::TestAccess::set_modbus_transport(impl, std::move(transport));
|
||||
module::main::powermeterImpl::TestAccess::set_pending_closed_transaction(impl, true);
|
||||
|
||||
std::string requested_uuid = "12345678-1234-5678-1234-567812345678";
|
||||
const auto resp = module::main::powermeterImpl::TestAccess::stop_transaction(impl, requested_uuid);
|
||||
EXPECT_EQ(resp.status, types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR);
|
||||
ASSERT_TRUE(resp.error.has_value());
|
||||
EXPECT_EQ(*resp.error, "Transaction id mismatch");
|
||||
EXPECT_EQ(transport_ptr->write_calls().size(), 0U);
|
||||
}
|
||||
|
||||
TEST(EM580PowermeterImpl, StopTransactionEmptyIdWithoutPendingClosedTransactionCleansUpAndReturnsOk) {
|
||||
static Everest::PtrContainer<module::CarloGavazzi_EM580> dummy_mod;
|
||||
auto conf = make_test_conf();
|
||||
module::main::powermeterImpl impl(nullptr, dummy_mod, conf);
|
||||
|
||||
auto transport = std::make_unique<FakeModbusTransport>();
|
||||
// clear_transaction_states() fetches OCMF state once; use NOT_READY to avoid
|
||||
// extra cleanup behavior.
|
||||
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
|
||||
u16_be(em580::registers::MODBUS_OCMF_STATE_NOT_READY));
|
||||
|
||||
auto* transport_ptr = transport.get();
|
||||
module::main::powermeterImpl::TestAccess::set_modbus_transport(impl, std::move(transport));
|
||||
module::main::powermeterImpl::TestAccess::set_transaction_id(impl, "12345678-1234-5678-1234-567812345678");
|
||||
module::main::powermeterImpl::TestAccess::set_pending_closed_transaction(impl, false);
|
||||
|
||||
std::string empty_id;
|
||||
const auto resp = module::main::powermeterImpl::TestAccess::stop_transaction(impl, empty_id);
|
||||
EXPECT_EQ(resp.status, types::powermeter::TransactionRequestStatus::OK);
|
||||
|
||||
// Expect one write ('E' end command) when no pending closed transaction
|
||||
// exists.
|
||||
EXPECT_EQ(transport_ptr->write_calls().size(), 1U);
|
||||
}
|
||||
|
||||
TEST(EM580PowermeterImpl, StartTransactionSpuriousReadyStateDoesCleanupAndNoTransactionWrites) {
|
||||
static Everest::PtrContainer<module::CarloGavazzi_EM580> dummy_mod;
|
||||
auto conf = make_test_conf();
|
||||
module::main::powermeterImpl impl(nullptr, dummy_mod, conf);
|
||||
|
||||
auto transport = std::make_unique<FakeModbusTransport>();
|
||||
// handle_start_transaction() first checks OCMF state.
|
||||
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
|
||||
u16_be(em580::registers::MODBUS_OCMF_STATE_READY));
|
||||
// clear_transaction_states(): reads state again, sees READY, reads file
|
||||
// (size+file) and confirms NOT_READY.
|
||||
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
|
||||
u16_be(em580::registers::MODBUS_OCMF_STATE_READY));
|
||||
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_SIZE_ADDRESS, 1, u16_be(1));
|
||||
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_FILE_ADDRESS, 1,
|
||||
bytes("OCMF|{\"TT\":\"x<=>12345678-1234-5678-1234-567812345678\"}|{}"));
|
||||
|
||||
auto* transport_ptr = transport.get();
|
||||
module::main::powermeterImpl::TestAccess::set_modbus_transport(impl, std::move(transport));
|
||||
|
||||
types::powermeter::TransactionReq req{};
|
||||
req.evse_id = "DE*TEST*EVSE01";
|
||||
req.transaction_id = "12345678-1234-5678-1234-567812345678";
|
||||
req.identification_status = types::powermeter::OCMFUserIdentificationStatus::ASSIGNED;
|
||||
req.identification_flags = {};
|
||||
req.identification_type = types::powermeter::OCMFIdentificationType::ISO14443;
|
||||
req.identification_level = types::powermeter::OCMFIdentificationLevel::NONE;
|
||||
req.identification_data.emplace("ABC");
|
||||
req.tariff_text.emplace("TARIFF");
|
||||
|
||||
const auto resp = module::main::powermeterImpl::TestAccess::start_transaction(impl, req);
|
||||
EXPECT_EQ(resp.status, types::powermeter::TransactionRequestStatus::OK);
|
||||
|
||||
// Only the cleanup confirm write should happen here (no transaction register
|
||||
// writes).
|
||||
EXPECT_EQ(transport_ptr->write_calls().size(), 1U);
|
||||
EXPECT_EQ(transport_ptr->write_calls()[0].address, em580::registers::MODBUS_OCMF_STATE_ADDRESS);
|
||||
}
|
||||
|
||||
TEST(EM580PowermeterImpl, StopTransactionUnknownIdReturnsErrorAndNoWrites) {
|
||||
static Everest::PtrContainer<module::CarloGavazzi_EM580> dummy_mod;
|
||||
auto conf = make_test_conf();
|
||||
module::main::powermeterImpl impl(nullptr, dummy_mod, conf);
|
||||
|
||||
auto transport = std::make_unique<FakeModbusTransport>();
|
||||
auto* transport_ptr = transport.get();
|
||||
module::main::powermeterImpl::TestAccess::set_modbus_transport(impl, std::move(transport));
|
||||
module::main::powermeterImpl::TestAccess::set_pending_closed_transaction(impl, false);
|
||||
module::main::powermeterImpl::TestAccess::set_transaction_id(impl, "11111111-1111-1111-1111-111111111111");
|
||||
|
||||
std::string requested_uuid = "22222222-2222-2222-2222-222222222222";
|
||||
const auto resp = module::main::powermeterImpl::TestAccess::stop_transaction(impl, requested_uuid);
|
||||
EXPECT_EQ(resp.status, types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR);
|
||||
ASSERT_TRUE(resp.error.has_value());
|
||||
EXPECT_EQ(*resp.error, "No open transaction or unknown transaction id");
|
||||
EXPECT_TRUE(transport_ptr->write_calls().empty());
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# 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_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
slip_protocol.cpp
|
||||
serial_device.cpp
|
||||
app_layer.cpp
|
||||
diagnostics.cpp
|
||||
)
|
||||
|
||||
target_compile_features(${MODULE_NAME} PUBLIC cxx_std_17)
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"main/powermeterImpl.cpp"
|
||||
)
|
||||
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
# insert other things like install cmds etc here
|
||||
target_link_libraries(${MODULE_NAME} PRIVATE
|
||||
everest::crc
|
||||
everest::framework
|
||||
)
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "DZG_GSH01.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
void DZG_GSH01::init() {
|
||||
invoke_init(*p_main);
|
||||
}
|
||||
|
||||
void DZG_GSH01::ready() {
|
||||
invoke_ready(*p_main);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef DZG_GSH01_HPP
|
||||
#define DZG_GSH01_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
// insert your custom include headers here
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class DZG_GSH01 : public Everest::ModuleBase {
|
||||
public:
|
||||
DZG_GSH01() = delete;
|
||||
DZG_GSH01(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, std::unique_ptr<powermeterImplBase> p_main,
|
||||
Conf& config) :
|
||||
ModuleBase(info), mqtt(mqtt_provider), p_main(std::move(p_main)), config(config){};
|
||||
|
||||
Everest::MqttProvider& mqtt;
|
||||
const std::unique_ptr<powermeterImplBase> p_main;
|
||||
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
|
||||
// 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 // DZG_GSH01_HPP
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef TIMEOUT_HPP
|
||||
#define TIMEOUT_HPP
|
||||
|
||||
#include <chrono>
|
||||
using namespace std::chrono;
|
||||
|
||||
/*
|
||||
Simple helper class for a timeout
|
||||
*/
|
||||
|
||||
class Timeout {
|
||||
public:
|
||||
explicit Timeout(milliseconds _t) : t(_t), start(steady_clock::now()) {
|
||||
}
|
||||
|
||||
bool reached() {
|
||||
if ((steady_clock::now() - start) > t)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
milliseconds t;
|
||||
time_point<steady_clock> start;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,393 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "app_layer.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <endian.h>
|
||||
#include <everest/logging.hpp>
|
||||
#include <fcntl.h>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/select.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
namespace app_layer {
|
||||
|
||||
// Status Word Bit Meaning - initialization in cpp-file necessary (static)
|
||||
std::map<std::uint64_t, std::string> StatusWord::bit_meaning = {
|
||||
{0x0000000000000001, "--> Error Real time clock"},
|
||||
{0x0000000000000002, "--> Error Configuration Memory"},
|
||||
{0x0000000000000008, "--> Error Signature Module"},
|
||||
{0x0000000000000020, "--> Error Meter Configuration"},
|
||||
{0x0000000000000040, "--> Error Meter Communication"},
|
||||
{0x0000000000000080, "--> Error Meter Fatal"},
|
||||
{0x0000000000000100, "--> Error External Display Not Available"},
|
||||
{0x0000000000010000, "--> Status Real time clock not in sync"},
|
||||
{0x0000000000020000, "--> Status Charging"},
|
||||
{0x0000000000080000, "--> Status Compensated Mode Impedance"},
|
||||
{0x0000000000100000, "--> Status external Display used"},
|
||||
{0x0000000000200000, "--> Status Is Ready for Charging"},
|
||||
{0x0000000000400000, "--> Status Session Not Completed"},
|
||||
{0x0000000000800000, "--> Status Eichlog Is Full"},
|
||||
{0x0000000001000000, "--> Status Charge Process List is full"},
|
||||
{0x0000000400000000, "--> Status Word Identification Bit Okay"},
|
||||
{0x0000000800000000, "--> Error DC Line Loss Power Abnormal"},
|
||||
{0x0000001000000000, "--> Factory Jumper set"}};
|
||||
|
||||
std::uint32_t timepoint_to_uint32(date::utc_clock::time_point timepoint) {
|
||||
return static_cast<std::uint32_t>(
|
||||
std::chrono::duration_cast<std::chrono::seconds>(timepoint.time_since_epoch()).count());
|
||||
}
|
||||
|
||||
void insert_u16_as_u8s(std::vector<std::uint8_t>& vec, std::uint16_t u16) {
|
||||
std::uint8_t upper = std::uint8_t((u16 >> 8) & 0x00FF);
|
||||
std::uint8_t lower = std::uint8_t(u16 & 0x00FF);
|
||||
vec.push_back(lower);
|
||||
vec.push_back(upper);
|
||||
}
|
||||
|
||||
void insert_u32_as_u8s(std::vector<std::uint8_t>& vec, std::uint32_t u32) {
|
||||
vec.push_back(std::uint8_t(u32 & 0x000000FF));
|
||||
vec.push_back(std::uint8_t((u32 >> 8) & 0x000000FF));
|
||||
vec.push_back(std::uint8_t((u32 >> 16) & 0x000000FF));
|
||||
vec.push_back(std::uint8_t((u32 >> 24) & 0x000000FF));
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> AppLayer::create_command(app_layer::Command cmd) {
|
||||
std::vector<std::uint8_t> command_data{};
|
||||
|
||||
insert_u16_as_u8s(command_data, static_cast<std::uint16_t>(cmd.type));
|
||||
insert_u16_as_u8s(command_data, static_cast<std::uint16_t>(cmd.length));
|
||||
command_data.push_back(static_cast<std::uint8_t>(app_layer::CommandStatus::OK));
|
||||
|
||||
for (std::uint16_t i = 0; i < cmd.data.size(); i++) {
|
||||
command_data.push_back(cmd.data[i]);
|
||||
}
|
||||
return command_data;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> AppLayer::create_simple_command(app_layer::CommandType cmd_type) {
|
||||
app_layer::Command cmd{};
|
||||
|
||||
cmd.type = cmd_type;
|
||||
cmd.length = 0x0005;
|
||||
cmd.status = app_layer::CommandStatus::OK;
|
||||
|
||||
return create_command(cmd);
|
||||
}
|
||||
|
||||
int8_t AppLayer::get_utc_offset_in_quarter_hours(
|
||||
const std::chrono::time_point<std::chrono::system_clock>& timepoint_system_clock) {
|
||||
std::stringstream offset;
|
||||
std::int8_t offset_quarterhours = 0;
|
||||
|
||||
auto tm = std::chrono::system_clock::to_time_t(timepoint_system_clock);
|
||||
offset << std::put_time(std::localtime(&tm), "%z");
|
||||
int offset_int = std::stoi(offset.str());
|
||||
|
||||
int offset_h = offset_int / 100;
|
||||
int offset_remaining = offset_int % 100; // in case of timezones that are not full-hour offsets of UTC
|
||||
if (offset_remaining != 0) {
|
||||
std::int8_t offset_remaining_extra_hour = offset_remaining / 60;
|
||||
if (offset_remaining_extra_hour != 0) {
|
||||
offset_quarterhours += offset_remaining_extra_hour * 4; // can be positive or negative
|
||||
offset_remaining -= offset_remaining_extra_hour * 4;
|
||||
}
|
||||
std::int8_t offset_remaining_quarterhours = offset_remaining / 15;
|
||||
offset_quarterhours += offset_remaining_quarterhours;
|
||||
}
|
||||
offset_quarterhours += offset_h * 4;
|
||||
|
||||
return offset_quarterhours;
|
||||
}
|
||||
|
||||
void AppLayer::create_command_start_transaction(app_layer::UserIdStatus user_id_status,
|
||||
app_layer::UserIdType user_id_type, std::string user_id_data,
|
||||
std::vector<std::uint8_t>& command_data) {
|
||||
app_layer::Command cmd{};
|
||||
|
||||
cmd.type = app_layer::CommandType::START_TRANSACTION;
|
||||
cmd.length = 0x0034;
|
||||
cmd.status = app_layer::CommandStatus::OK;
|
||||
|
||||
std::chrono::time_point<std::chrono::system_clock> timepoint = std::chrono::system_clock::now();
|
||||
std::int8_t gmt_offset_quarters_of_an_hour = get_utc_offset_in_quarter_hours(timepoint);
|
||||
|
||||
insert_u32_as_u8s(cmd.data, timepoint_to_uint32(date::utc_clock::from_sys(timepoint)));
|
||||
cmd.data.push_back(gmt_offset_quarters_of_an_hour);
|
||||
cmd.data.push_back(static_cast<std::uint8_t>(user_id_status));
|
||||
cmd.data.push_back(static_cast<std::uint8_t>(user_id_type));
|
||||
|
||||
std::uint8_t byte_count = 0;
|
||||
for (std::uint8_t databyte : user_id_data) { // push up to 40 characters of user id name into command
|
||||
cmd.data.push_back(databyte);
|
||||
byte_count++;
|
||||
if (byte_count >= 40)
|
||||
break;
|
||||
}
|
||||
while (byte_count < 40) { // fill remaining user id name characters with zeros
|
||||
cmd.data.push_back(0x00);
|
||||
byte_count++;
|
||||
}
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_stop_transaction(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::STOP_TRANSACTION);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_time(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::TIME);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_set_time(date::utc_clock::time_point timepoint,
|
||||
std::int8_t gmt_offset_quarters_of_an_hour,
|
||||
std::vector<std::uint8_t>& command_data) {
|
||||
app_layer::Command cmd{};
|
||||
|
||||
cmd.type = app_layer::CommandType::TIME;
|
||||
cmd.length = 0x000A;
|
||||
cmd.status = app_layer::CommandStatus::OK;
|
||||
|
||||
insert_u32_as_u8s(cmd.data, timepoint_to_uint32(timepoint));
|
||||
cmd.data.push_back(gmt_offset_quarters_of_an_hour);
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
void AppLayer::create_command_get_bus_address(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::METER_BUS_ADDR);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_set_bus_address(std::uint8_t bus_address, std::vector<std::uint8_t>& command_data) {
|
||||
app_layer::Command cmd{};
|
||||
|
||||
cmd.type = app_layer::CommandType::METER_BUS_ADDR;
|
||||
cmd.length = 0x0006;
|
||||
cmd.status = app_layer::CommandStatus::OK;
|
||||
|
||||
cmd.data.push_back(bus_address);
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_voltage(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_VOLTAGE_L1);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_current(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_CURRENT_L1);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_import_power(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_IMPORT_DEV_POWER);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_total_dev_import_energy(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_TOTAL_IMPORT_DEV_ENERGY);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_total_power(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_TOTAL_DEV_POWER);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_total_start_import_energy(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_TOTAL_START_IMPORT_DEV_ENERGY);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_total_stop_import_energy(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_TOTAL_STOP_IMPORT_DEV_ENERGY);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_total_transaction_duration(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_TRANSACT_TOTAL_DURATION);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_pubkey_str16(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_PUBKEY_STR16);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_pubkey_asn1(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_PUBKEY_ASN1);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_ocmf_stats(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::OCMF_STATS);
|
||||
}
|
||||
|
||||
/* OCMF ID: 1..235000
|
||||
OCMF data from specified transaction will be at minimum import energy of transaction
|
||||
*/
|
||||
void AppLayer::create_command_get_transaction_ocmf(std::uint32_t ocmf_id, std::vector<std::uint8_t>& command_data) {
|
||||
app_layer::Command cmd{};
|
||||
|
||||
cmd.type = app_layer::CommandType::GET_OCMF;
|
||||
cmd.length = 0x0009;
|
||||
cmd.status = app_layer::CommandStatus::OK;
|
||||
|
||||
insert_u32_as_u8s(cmd.data, ocmf_id);
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_last_transaction_ocmf(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_LAST_OCMF);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_charge_point_id(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::CHARGE_POINT_ID);
|
||||
}
|
||||
|
||||
/* only works in "assembly mode" */
|
||||
void AppLayer::create_command_set_charge_point_id(app_layer::UserIdType id_type, std::string id_data,
|
||||
std::vector<std::uint8_t>& command_data) {
|
||||
app_layer::Command cmd{};
|
||||
|
||||
cmd.type = app_layer::CommandType::CHARGE_POINT_ID;
|
||||
cmd.length = 0x0013;
|
||||
cmd.status = app_layer::CommandStatus::OK;
|
||||
|
||||
cmd.data.push_back(static_cast<std::uint8_t>(id_type));
|
||||
|
||||
std::uint8_t byte_count = 0;
|
||||
for (std::uint8_t databyte : id_data) { // push up to 13 characters of id data into command
|
||||
cmd.data.push_back(databyte);
|
||||
byte_count++;
|
||||
if (byte_count >= 13)
|
||||
break;
|
||||
}
|
||||
while (byte_count < 13) { // fill remaining user id name characters with zeros
|
||||
cmd.data.push_back(0x00);
|
||||
byte_count++;
|
||||
}
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_log_stats(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_LOG_STATS);
|
||||
}
|
||||
|
||||
/* log entry ids: 1..2500 */
|
||||
void AppLayer::create_command_get_log_entry(std::uint32_t log_entry_id, std::vector<std::uint8_t>& command_data) {
|
||||
app_layer::Command cmd{};
|
||||
|
||||
cmd.type = app_layer::CommandType::GET_LOG_ENTRY;
|
||||
cmd.length = 0x0009;
|
||||
cmd.status = app_layer::CommandStatus::OK;
|
||||
|
||||
insert_u32_as_u8s(cmd.data, log_entry_id);
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_last_log_entry(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::GET_LAST_LOG_ENTRY);
|
||||
}
|
||||
|
||||
/* log entry ids: 1..2500
|
||||
thus: if 20 log entries and log_entry_id == 2, then log entry 18 will be returned
|
||||
*/
|
||||
void AppLayer::create_command_get_log_entry_reverse(std::uint32_t log_entry_id,
|
||||
std::vector<std::uint8_t>& command_data) {
|
||||
app_layer::Command cmd{};
|
||||
|
||||
cmd.type = app_layer::CommandType::GET_LOG_ENTRY_REVERSE;
|
||||
cmd.length = 0x0009;
|
||||
cmd.status = app_layer::CommandStatus::OK;
|
||||
|
||||
insert_u32_as_u8s(cmd.data, log_entry_id);
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_application_mode(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::APP_MODE);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_set_application_mode(app_layer::ApplicationBoardMode mode,
|
||||
std::vector<std::uint8_t>& command_data) {
|
||||
app_layer::Command cmd{};
|
||||
|
||||
cmd.type = app_layer::CommandType::APP_MODE;
|
||||
cmd.length = 0x0006;
|
||||
cmd.status = app_layer::CommandStatus::OK;
|
||||
|
||||
cmd.data.push_back(static_cast<std::uint8_t>(mode));
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_line_loss_impedance(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::LINE_LOSS_IMPEDANCE);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_set_line_loss_impedance(std::uint16_t ll_impedance,
|
||||
std::vector<std::uint8_t>& command_data) {
|
||||
app_layer::Command cmd{};
|
||||
|
||||
cmd.type = app_layer::CommandType::LINE_LOSS_IMPEDANCE;
|
||||
cmd.length = 0x0009;
|
||||
cmd.status = app_layer::CommandStatus::OK;
|
||||
|
||||
insert_u32_as_u8s(cmd.data, ll_impedance);
|
||||
|
||||
command_data = create_command(cmd);
|
||||
}
|
||||
|
||||
// diagnostics
|
||||
|
||||
void AppLayer::create_command_get_hardware_version(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::HW_VERSION);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_server_id(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::SERVER_ID);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_serial_number(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::SERIAL_NR);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_application_fw_version(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::APP_FW_VERSION);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_application_fw_checksum(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::APP_FW_CHECKSUM);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_application_fw_hash(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::APP_FW_HASH);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_status_word(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::STATUS_WORD);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_metering_fw_version(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::MT_FW_VERSION);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_metering_fw_checksum(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::MT_FW_CHECKSUM);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_metering_mode(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::MT_MODE);
|
||||
}
|
||||
|
||||
void AppLayer::create_command_get_bootloader_version(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::BOOTL_VERSION);
|
||||
}
|
||||
|
||||
/* doubles as OCMF "meter model name" */
|
||||
void AppLayer::create_command_get_device_type(std::vector<std::uint8_t>& command_data) {
|
||||
command_data = create_simple_command(app_layer::CommandType::DEVICE_TYPE);
|
||||
}
|
||||
|
||||
} // namespace app_layer
|
||||
@@ -0,0 +1,353 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/*
|
||||
This is an implementation for the GSH01 powermeter application layer
|
||||
*/
|
||||
#ifndef APP_LAYER
|
||||
#define APP_LAYER
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <everest/crc/crc.hpp>
|
||||
#include <everest/logging.hpp>
|
||||
#include <generated/types/powermeter.hpp>
|
||||
|
||||
namespace app_layer {
|
||||
|
||||
enum class CommandType : std::uint16_t {
|
||||
///---------------Device Information Registers-------------------
|
||||
APP_MODE = 0x4102, // get or set the mode of application
|
||||
TRANSPARENT_MODE = 0x4103, // get the transparent mode meter
|
||||
SERVER_ID = 0x4110, // get the server ID
|
||||
SERIAL_NR = 0x4111, // get the serial number
|
||||
HW_VERSION = 0x4112, // get the hardware version
|
||||
DEVICE_TYPE = 0x4113, // get the device type
|
||||
APP_FW_VERSION = 0x4114, // get the software version of application
|
||||
APP_FW_CHECKSUM = 0x4115, // get the checksum (crc16) of application
|
||||
MT_FW_VERSION = 0x4116, // get the software version of metering firmware
|
||||
MT_FW_CHECKSUM = 0x4117, // get the crc16 from software integrity check of metering firmware
|
||||
BOOTL_VERSION = 0x4118, // get the bootloader version
|
||||
APP_FW_HASH = 0x4119, // get the application firmware hash(crc16)
|
||||
MEASUREMENT_MODE = 0x4120, // get the measurement mode
|
||||
GET_NORMAL_VOLTAGE = 0x4121, // get the normal voltage
|
||||
GET_NORMAL_CURRENT = 0x4122, // get the normal current
|
||||
GET_MAX_CURRENT = 0x4123, // get the maximum current
|
||||
REVERSE_MODE = 0x4124, // get and set(?) the reverse mode
|
||||
CLEAR_METER_STATUS = 0x4125, // set the clear meter status command
|
||||
INIT_METER = 0x4126, // set the initialize meter command
|
||||
LINE_LOSS_IMPEDANCE = 0x4130, // get or set (in assembling mode) the line loss impedance
|
||||
LINE_LOSS_MEAS_MODE = 0x4131, // get the line loss energy measurement mode
|
||||
MT_MODE = 0x4133, // get the meter operation mode
|
||||
TIME = 0x4135, // get or set the device time
|
||||
STATUS_WORD = 0x4137, // get the statusword
|
||||
APP_CONFIG_COMPLETE = 0x4211, // set the application configuration complete command
|
||||
METER_BUS_ADDR = 0x4212, // get or set the meter bus address
|
||||
AS_CONFIG_COMPLETE = 0x4221, // set the assmbling configuration complete command
|
||||
///---------------Device Control Registers--------------------------
|
||||
GET_PUBKEY_STR16 = 0x0211, // get the public key in string format (base16)
|
||||
GET_PUBKEY_STR32 = 0x0212, // get the public key in string format (base32)
|
||||
GET_PUBKEY_CSTR16 = 0x0213, // get the public key compressed in string format (base16)
|
||||
GET_PUBKEY_CSTR32 = 0x0214, // get the public key compressed in string format (base32)
|
||||
START_TRANSACTION = 0x1501, // start transaction
|
||||
STOP_TRANSACTION = 0x1502, // stop transaction
|
||||
OCMF_STATS = 0x1510, // get the statistics of OCMF
|
||||
GET_OCMF = 0x1520, // get the OCMF
|
||||
GET_LAST_OCMF = 0x1521, // get last OCMF
|
||||
GET_OCMF_REVERSE = 0x1522, // get the OCMF reverse
|
||||
GET_PUBKEY_ASN1 = 0x1530, // get public key in ASN1 format
|
||||
CHARGE_POINT_ID = 0x1531, // get or set the charge point identification
|
||||
GET_TRANSACT_IMPORT_LINE_LOSS_ENERGY = 0x1540, // get total transaction import line loss energy
|
||||
GET_TRANSACT_TOTAL_IMPORT_DEV_ENERGY = 0x1550, // get total transaction import device energy
|
||||
GET_TRANSACT_TOTAL_IMPORT_MAINS_ENERGY = 0x1560, // get total transaction import mains energy
|
||||
GET_TRANSACT_TOTAL_DURATION = 0x156A, // get total transaction duration.
|
||||
GET_TOTAL_START_IMPORT_LINE_LOSS_ENERGY = 0x1570, // get total start import line loss energy
|
||||
GET_TOTAL_START_IMPORT_DEV_ENERGY = 0x1580, // get total start import device energy
|
||||
GET_TOTAL_START_IMPORT_MAINS_ENERGY = 0x1590, // get total start import mains energy
|
||||
GET_TOTAL_STOP_IMPORT_LINE_LOSS_ENERGY = 0x15A0, // get total stop import line loss energy
|
||||
GET_TOTAL_STOP_IMPORT_DEV_ENERGY = 0x15B0, // get total stop import device energy
|
||||
GET_TOTAL_STOP_IMPORT_MAINS_ENERGY = 0x15C0, // get total stop import mains energy
|
||||
GET_LOG_STATS = 0x1710, // get the statistic of log
|
||||
GET_LOG_ENTRY = 0x1720, // get the log entry
|
||||
GET_LAST_LOG_ENTRY = 0x1721, // get last log entry
|
||||
GET_LOG_ENTRY_REVERSE = 0x1722, // get the log entry reverse
|
||||
///----------------Instantaneous Registers------------------------
|
||||
GET_TOTAL_IMPORT_MAINS_ENERGY = 0x0110, // get total import mains energy
|
||||
GET_CURRENT_L1 = 0x0131, // get current (phase L1)
|
||||
GET_VOLTAGE_L1 = 0x0132, // get voltage (phase L1)
|
||||
GET_TOTAL_IMPORT_MAINS_POWER = 0x0133, // total import mains power
|
||||
GET_POS_DEV_VOLTAGE = 0x0135, // get positive device voltage
|
||||
GET_NEG_DEV_VOLTAGE = 0x0136, // get negative device voltage
|
||||
GET_TOTAL_VOLTAGE = 0x0137, // get total device voltage
|
||||
GET_IMPORT_DEV_POWER = 0x0138, // get import device power
|
||||
GET_IMPORT_LINE_LOSS_POWER = 0x013A, // get import line loss power
|
||||
GET_TOTAL_DEV_POWER = 0x013C, // get total device power
|
||||
GET_TOTAL_IMPORT_LINE_LOSS_ENERGY = 0x0160, // get the total import line loss energy
|
||||
GET_TOTAL_IMPORT_DEV_ENERGY = 0x0170, // get the total import device energy
|
||||
GET_SECOND_INDEX = 0x0180 // get the second index
|
||||
};
|
||||
|
||||
enum class CommandStatus : std::uint8_t {
|
||||
OK = 0, // no error
|
||||
GENERAL_ERROR = 1, // internal process has an error
|
||||
OUT_OF_RANGE = 2, // data is out of range
|
||||
SECURITY_ACCES_DENIED = 3, // the current mode of the slave cannot start this command
|
||||
REJECTED = 4, // error of communication between application and metering board
|
||||
LOGICAL_ADDRESS_NOT_FOUND = 5, // command not implemented
|
||||
FORMAL_INVALID = 6, // length in bytes is incorrect; e.g. received more data than expected
|
||||
NOT_AVAILABLE = 7, // data not available
|
||||
IS_BUSY = 8, // slave cannot process command; already processing long operation
|
||||
PUBKEY_MISSING = 9 // public key is not stored in internal memory
|
||||
};
|
||||
|
||||
enum class CommandResult : std::uint8_t {
|
||||
OK = 0,
|
||||
GENERAL_ERROR = 1, // read ERROR register (read with GET_ERRORS[0xA004]) for more information
|
||||
OUT_OF_RANGE = 2, //
|
||||
SECURITY_ACCESS_DENIED = 3, // application board is not in production mode
|
||||
REJECTED = 4, // metering board is not responding
|
||||
FORMAT_INVALID = 6, // payload is not empty
|
||||
NOT_AVAILABLE = 7, // data could not be read
|
||||
BUSY = 8, // transaction ongoing, metering board unavailable
|
||||
PUBLIC_KEY_MISSING = 9, //
|
||||
PROTOCOL_ERROR = 250, // error on reception at host: protocol error (SLIP protocol)
|
||||
TIMEOUT = 251, // no response during at least 1100 ms
|
||||
COMMUNICATION_FAILED = 254, // error on communication between PM and host device
|
||||
PENDING = 255 // special state for transaction commands
|
||||
};
|
||||
|
||||
inline std::string command_result_to_string(CommandResult res) {
|
||||
switch (res) {
|
||||
case CommandResult::OK:
|
||||
return "OK";
|
||||
case CommandResult::GENERAL_ERROR:
|
||||
return "General Error";
|
||||
case CommandResult::OUT_OF_RANGE:
|
||||
return "Out of Range";
|
||||
case CommandResult::SECURITY_ACCESS_DENIED:
|
||||
return "Security Access Denied";
|
||||
case CommandResult::REJECTED:
|
||||
return "Rejected";
|
||||
case CommandResult::FORMAT_INVALID:
|
||||
return "Format Invalid";
|
||||
case CommandResult::NOT_AVAILABLE:
|
||||
return "Not Available";
|
||||
case CommandResult::BUSY:
|
||||
return "Busy";
|
||||
case CommandResult::PUBLIC_KEY_MISSING:
|
||||
return "Public Key Missing";
|
||||
case CommandResult::PROTOCOL_ERROR:
|
||||
return "Protocol Error";
|
||||
case CommandResult::TIMEOUT:
|
||||
return "Timeout";
|
||||
case CommandResult::COMMUNICATION_FAILED:
|
||||
return "Communication Failed";
|
||||
case CommandResult::PENDING:
|
||||
return "Pending";
|
||||
}
|
||||
throw std::out_of_range("No known string conversion for provided enum of type CommandResult");
|
||||
}
|
||||
|
||||
enum class UserIdStatus : std::uint8_t {
|
||||
USER_NOT_ASSIGNED = 0x00,
|
||||
USER_ASSIGNED = 0x01
|
||||
};
|
||||
|
||||
enum class UserIdType : std::uint8_t {
|
||||
NONE = 0, // not available
|
||||
DENIED = 1, // not retrievable (e.g. two-factor-auth)
|
||||
UNDEFINED = 2, // type unknown / other
|
||||
ISO14443 = 10, // UID of RFID card according to ISO14443 (4 or 7 bytes HEX)
|
||||
ISO15693 = 11, // UID of RFID card according to ISO15693 (8 bytes HEX)
|
||||
EMAID = 20, // Electro-Mobility-Account-ID according to ISO/IEC15118 (14 or 15 bytes string)
|
||||
EVCCID = 21, // ID of an EV according to ISO/IEC15118 (max 6 bytes)
|
||||
EVCOID = 30, // EV-Contract-ID according to DIN91286
|
||||
ISO7812 = 40, // Identification-Card-Format according to ISO/IEC7812 (credit-/banking-cards, etc.)
|
||||
CAR_TXN_NR = 50, // Card-Transaction-Number (CardTxNbr) for credit- or banking-cards
|
||||
CENTRAL = 60, // centrally generated ID (no fixed format, e.g. UUID); OCPP 2.0
|
||||
CENTRAL_1 = 61, // centrally generated ID (no fixed format, e.g. start-via-SMS); (up to) OCPP 1.6
|
||||
CENTRAL_2 = 62, // centrally generated ID (no fixed format, e.g. start-by-operator); (up to) OCPP 1.6
|
||||
LOCAL = 70, // locally generated ID (no fixed format, e.g. UUID); OCPP 2.0
|
||||
LOCAL_1 = 71, // locally generated ID (no fixed format, e.g. generated by chargepoint); (up to) OCPP 1.6
|
||||
LOCAL_2 = 72, // locally generated ID (no fixed format, other); (up to) OCPP 1.6
|
||||
PHONE_NUMBER = 80, // international phone number (leading '+' with country code)
|
||||
KEY_CODE = 90 // private user key (no fixed format)
|
||||
};
|
||||
|
||||
enum class ErrorCategory : std::uint8_t {
|
||||
LAST = 0,
|
||||
LAST_CRITICAL = 1,
|
||||
FIRST = 2,
|
||||
FIRST_CRITICAL = 3
|
||||
};
|
||||
|
||||
enum class ErrorSource : std::uint8_t {
|
||||
SYSTEM = 0,
|
||||
COMMUNICATION = 1
|
||||
};
|
||||
|
||||
enum class ApplicationBoardMode : std::uint8_t {
|
||||
APPLICATION = 0,
|
||||
ASSEMBLY = 1
|
||||
};
|
||||
|
||||
enum class LogType : std::uint8_t {
|
||||
NONE = 0,
|
||||
LINE_LOSS_MEASUREMENT_MODE = 1,
|
||||
IMPEDANCE_CHANGED = 2,
|
||||
OPERATION_MODE_CHANGED = 3,
|
||||
ASSEMBLY_CONFIG_CHANGED = 4,
|
||||
FATAL_ERROR_EVENT = 5,
|
||||
TIME_DELTA_TOO_BIG_EVENT = 6,
|
||||
CHARGE_POINT_ID_CHANGED = 7,
|
||||
EXTERNAL_DISPLAY_PAIRED = 8,
|
||||
EXTERNAL_DISPLAY_FAILURE = 9,
|
||||
CHARGE_DATA_OUT_OF_MEMORY = 10,
|
||||
LOG_DATA_OUT_OF_MEMORY = 11,
|
||||
FW_VERSION_CHANGED = 12,
|
||||
PULSE_LED_SOURCE_CHANGED = 13
|
||||
};
|
||||
|
||||
inline std::string log_type_to_string(LogType log) {
|
||||
switch (log) {
|
||||
case LogType::NONE:
|
||||
return "None";
|
||||
case LogType::LINE_LOSS_MEASUREMENT_MODE:
|
||||
return "Line loss measurement mode";
|
||||
case LogType::IMPEDANCE_CHANGED:
|
||||
return "Impedance changed";
|
||||
case LogType::OPERATION_MODE_CHANGED:
|
||||
return "Operation mode changed";
|
||||
case LogType::ASSEMBLY_CONFIG_CHANGED:
|
||||
return "Assembly config changed";
|
||||
case LogType::FATAL_ERROR_EVENT:
|
||||
return "Fatal error event";
|
||||
case LogType::TIME_DELTA_TOO_BIG_EVENT:
|
||||
return "Time delta too big event";
|
||||
case LogType::CHARGE_POINT_ID_CHANGED:
|
||||
return "Charge point id changed";
|
||||
case LogType::EXTERNAL_DISPLAY_PAIRED:
|
||||
return "External display paired";
|
||||
case LogType::EXTERNAL_DISPLAY_FAILURE:
|
||||
return "External display failure";
|
||||
case LogType::CHARGE_DATA_OUT_OF_MEMORY:
|
||||
return "Charge data out of memory";
|
||||
case LogType::LOG_DATA_OUT_OF_MEMORY:
|
||||
return "Log data out of memory";
|
||||
case LogType::FW_VERSION_CHANGED:
|
||||
return "Fw version changed";
|
||||
case LogType::PULSE_LED_SOURCE_CHANGED:
|
||||
return "Pulse LED source changed";
|
||||
}
|
||||
throw std::out_of_range("No known string conversion for provided enum of type LogType");
|
||||
}
|
||||
|
||||
class LogEntry {
|
||||
public:
|
||||
app_layer::LogType type;
|
||||
std::uint32_t second_index{};
|
||||
std::uint32_t utc_time{};
|
||||
std::uint8_t utc_offset{};
|
||||
std::vector<std::uint8_t> old_value; // max. 10 elements
|
||||
std::vector<std::uint8_t> new_value; // max. 10 elements
|
||||
std::vector<std::uint8_t> server_id; // 10 elements
|
||||
std::vector<std::uint8_t> signature; // 64 elements
|
||||
};
|
||||
|
||||
class StatusWord {
|
||||
private:
|
||||
static std::map<std::uint64_t, std::string> bit_meaning;
|
||||
|
||||
public:
|
||||
static void print(std::uint64_t status) {
|
||||
for (const auto& [key, value] : bit_meaning) {
|
||||
if (status & key) {
|
||||
EVLOG_info << value;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class Command {
|
||||
public:
|
||||
app_layer::CommandType type;
|
||||
std::uint16_t length;
|
||||
app_layer::CommandStatus status;
|
||||
std::vector<std::uint8_t> data;
|
||||
};
|
||||
|
||||
static constexpr std::uint16_t PM_GSH01_MAX_RX_LENGTH = 1500;
|
||||
static constexpr std::uint16_t PM_GSH01_SERIAL_RX_INITIAL_TIMEOUT_MS = 1100;
|
||||
static constexpr std::uint16_t PM_GSH01_SERIAL_RX_WITHIN_MESSAGE_TIMEOUT_MS = 100;
|
||||
|
||||
class AppLayer {
|
||||
|
||||
public:
|
||||
void create_command_start_transaction(app_layer::UserIdStatus user_id_status, app_layer::UserIdType user_id_type,
|
||||
std::string user_id_data, std::vector<std::uint8_t>& command_data);
|
||||
void create_command_stop_transaction(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_time(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_set_time(date::utc_clock::time_point timepoint, std::int8_t gmt_offset_quarters_of_an_hour,
|
||||
std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_bus_address(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_set_bus_address(std::uint8_t bus_address, std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_voltage(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_current(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_import_power(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_total_power(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_total_start_import_energy(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_total_stop_import_energy(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_total_dev_import_energy(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_total_transaction_duration(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_pubkey_str16(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_pubkey_asn1(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_ocmf_stats(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_transaction_ocmf(std::uint32_t ocmf_id, std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_last_transaction_ocmf(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_charge_point_id(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_set_charge_point_id(app_layer::UserIdType id_type, std::string id_data,
|
||||
std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_log_stats(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_log_entry(std::uint32_t log_entry_id, std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_last_log_entry(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_log_entry_reverse(std::uint32_t log_entry_id, std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_application_mode(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_set_application_mode(app_layer::ApplicationBoardMode mode,
|
||||
std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_line_loss_impedance(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_set_line_loss_impedance(std::uint16_t ll_impedance, std::vector<std::uint8_t>& command_data);
|
||||
|
||||
void create_command_get_server_id(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_serial_number(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_hardware_version(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_device_type(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_bootloader_version(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_status_word(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_application_fw_version(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_application_fw_checksum(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_application_fw_hash(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_metering_fw_version(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_metering_fw_checksum(std::vector<std::uint8_t>& command_data);
|
||||
void create_command_get_metering_mode(std::vector<std::uint8_t>& command_data);
|
||||
|
||||
int8_t
|
||||
get_utc_offset_in_quarter_hours(const std::chrono::time_point<std::chrono::system_clock>& timepoint_system_clock);
|
||||
|
||||
private:
|
||||
std::vector<std::uint8_t> create_command(app_layer::Command cmd);
|
||||
std::vector<std::uint8_t> create_simple_command(app_layer::CommandType cmd_type);
|
||||
};
|
||||
|
||||
} // namespace app_layer
|
||||
#endif // APP_LAYER
|
||||
@@ -0,0 +1,240 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <diagnostics.hpp>
|
||||
|
||||
namespace module {
|
||||
|
||||
void to_json(json& j, const DeviceData& k) {
|
||||
j["UTC"] = module::conversions::u32_epoch_to_rfc3339(k.utc_time_s);
|
||||
j["GMT_offset_quarterhours"] = k.gmt_offset_quarterhours;
|
||||
j["total_start_import_energy_Wh"] = k.total_start_import_energy_Wh;
|
||||
j["total_stop_import_energy_Wh"] = k.total_stop_import_energy_Wh;
|
||||
j["total_transaction_duration_s"] = k.total_transaction_duration_s;
|
||||
j["OCMF_stats"] = json();
|
||||
j["OCMF_stats"]["number_transactions"] = k.ocmf_stats.number_transactions;
|
||||
j["OCMF_stats"]["timestamp_first_transaction"] = k.ocmf_stats.timestamp_first_transaction;
|
||||
j["OCMF_stats"]["timestamp_last_transaction"] = k.ocmf_stats.timestamp_last_transaction;
|
||||
j["OCMF_stats"]["max_number_of_transactions"] = k.ocmf_stats.max_number_of_transactions;
|
||||
j["last_ocmf_transaction"] = k.last_ocmf_transaction;
|
||||
j["requested_ocmf"] = k.requested_ocmf;
|
||||
j["total_dev_import_energy_Wh"] = k.total_dev_import_energy_Wh;
|
||||
j["status"] = module::conversions::to_bin_string(k.ab_status);
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const DeviceData& k) {
|
||||
os << json(k).dump(4);
|
||||
return os;
|
||||
}
|
||||
|
||||
void to_json(json& j, const DeviceDiagnostics& k) {
|
||||
j["charge_point_id"] = k.charge_point_id;
|
||||
j["charge_point_id_type"] = k.charge_point_id_type;
|
||||
j["log_stats"] = json();
|
||||
j["log_stats"]["number_log_entries"] = k.log_stats.number_log_entries;
|
||||
j["log_stats"]["timestamp_first_log"] = k.log_stats.timestamp_first_log;
|
||||
j["log_stats"]["timestamp_last_log"] = k.log_stats.timestamp_last_log;
|
||||
j["log_stats"]["max_number_of_logs"] = k.log_stats.max_number_of_logs;
|
||||
j["dev_info"] = json();
|
||||
j["dev_info"]["type"] = k.dev_info.type;
|
||||
j["dev_info"]["hw_ver"] = k.dev_info.hw_ver;
|
||||
j["dev_info"]["server_id"] = k.dev_info.server_id;
|
||||
j["dev_info"]["serial_nr"] = k.dev_info.serial_number;
|
||||
j["dev_info"]["application"] = json();
|
||||
j["dev_info"]["application"]["mode"] = k.dev_info.application.mode;
|
||||
j["dev_info"]["application"]["FW_ver"] = k.dev_info.application.fw_ver;
|
||||
j["dev_info"]["application"]["FW_CRC"] = module::conversions::hexdump(k.dev_info.application.fw_crc);
|
||||
j["dev_info"]["application"]["FW_hash"] = module::conversions::hexdump(k.dev_info.application.fw_hash);
|
||||
j["dev_info"]["metering"] = json();
|
||||
j["dev_info"]["metering"]["FW_ver"] = k.dev_info.metering.fw_ver;
|
||||
j["dev_info"]["metering"]["FW_CRC"] = module::conversions::hexdump(k.dev_info.metering.fw_crc);
|
||||
j["dev_info"]["metering"]["mode"] = k.dev_info.metering.mode;
|
||||
j["dev_info"]["bus_address"] = module::conversions::hexdump(k.dev_info.bus_address);
|
||||
j["dev_info"]["bootl_ver"] = k.dev_info.bootl_ver;
|
||||
j["pubkey"] = json();
|
||||
j["pubkey"]["asn1"] = json();
|
||||
j["pubkey"]["str16"] = json();
|
||||
j["pubkey"]["default"] = json();
|
||||
j["pubkey"]["asn1"]["key"] = k.pubkey_asn1;
|
||||
j["pubkey"]["str16"]["key"] = k.pubkey_str16;
|
||||
j["pubkey"]["str16"]["format"] = k.pubkey_str16_format;
|
||||
j["pubkey"]["default"]["key"] = k.pubkey;
|
||||
j["pubkey"]["default"]["format"] = k.pubkey_format;
|
||||
j["ocmf_config_table"] = json::array();
|
||||
if (k.ocmf_config_table.size() > 0) {
|
||||
for (std::uint8_t n = 0; n < k.ocmf_config_table.size(); n++) {
|
||||
j["ocmf_config_table"][n] =
|
||||
module::conversions::hexdump(static_cast<std::uint8_t>(k.ocmf_config_table.at(n)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void from_json(const json& j, DeviceDiagnostics& k) {
|
||||
EVLOG_error << "[DeviceDiagnostics][from_json()] not implemented";
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const DeviceDiagnostics& k) {
|
||||
os << json(k).dump(4);
|
||||
return os;
|
||||
}
|
||||
|
||||
void to_json(json& j, const Logging& k) {
|
||||
j["log"] = json();
|
||||
j["log"]["last"] = json();
|
||||
j["log"]["last"]["type"] = "" + std::to_string((int)k.last_log.type) + ": " + log_type_to_string(k.last_log.type);
|
||||
j["log"]["last"]["second_index"] = k.last_log.second_index;
|
||||
j["log"]["last"]["utc_time"] = module::conversions::u32_epoch_to_rfc3339(k.last_log.utc_time);
|
||||
j["log"]["last"]["utc_offset_quarterhours"] = k.last_log.utc_offset;
|
||||
j["log"]["last"]["old_value"] = module::conversions::hexdump(k.last_log.old_value);
|
||||
j["log"]["last"]["new_value"] = module::conversions::hexdump(k.last_log.new_value);
|
||||
j["log"]["last"]["server_id"] = module::conversions::hexdump(k.last_log.server_id);
|
||||
j["log"]["last"]["signature"] = module::conversions::hexdump(k.last_log.signature);
|
||||
|
||||
j["errors"] = json();
|
||||
j["errors"]["system"] = json();
|
||||
j["errors"]["system"]["last"] = json::array();
|
||||
for (std::uint8_t n = 0; n < 5; n++) {
|
||||
j["errors"]["system"]["last"][n]["id"] =
|
||||
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::SYSTEM)]
|
||||
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST)]
|
||||
.error[n]
|
||||
.id;
|
||||
j["errors"]["system"]["last"][n]["priority"] =
|
||||
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::SYSTEM)]
|
||||
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST)]
|
||||
.error[n]
|
||||
.priority;
|
||||
j["errors"]["system"]["last"][n]["counter"] =
|
||||
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::SYSTEM)]
|
||||
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST)]
|
||||
.error[n]
|
||||
.counter;
|
||||
}
|
||||
j["errors"]["system"]["last_critical"] = json::array();
|
||||
for (std::uint8_t n = 0; n < 5; n++) {
|
||||
j["errors"]["system"]["last_critical"][n]["id"] =
|
||||
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::SYSTEM)]
|
||||
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST_CRITICAL)]
|
||||
.error[n]
|
||||
.id;
|
||||
j["errors"]["system"]["last_critical"][n]["priority"] =
|
||||
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::SYSTEM)]
|
||||
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST_CRITICAL)]
|
||||
.error[n]
|
||||
.priority;
|
||||
j["errors"]["system"]["last_critical"][n]["counter"] =
|
||||
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::SYSTEM)]
|
||||
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST_CRITICAL)]
|
||||
.error[n]
|
||||
.counter;
|
||||
}
|
||||
j["errors"]["communication"] = json();
|
||||
j["errors"]["communication"]["last"] = json::array();
|
||||
for (std::uint8_t n = 0; n < 5; n++) {
|
||||
j["errors"]["communication"]["last"][n]["id"] =
|
||||
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::COMMUNICATION)]
|
||||
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST)]
|
||||
.error[n]
|
||||
.id;
|
||||
j["errors"]["communication"]["last"][n]["priority"] =
|
||||
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::COMMUNICATION)]
|
||||
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST)]
|
||||
.error[n]
|
||||
.priority;
|
||||
j["errors"]["communication"]["last"][n]["counter"] =
|
||||
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::COMMUNICATION)]
|
||||
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST)]
|
||||
.error[n]
|
||||
.counter;
|
||||
}
|
||||
j["errors"]["communication"]["last_critical"] = json::array();
|
||||
for (std::uint8_t n = 0; n < 5; n++) {
|
||||
j["errors"]["communication"]["last_critical"][n]["id"] =
|
||||
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::COMMUNICATION)]
|
||||
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST_CRITICAL)]
|
||||
.error[n]
|
||||
.id;
|
||||
j["errors"]["communication"]["last_critical"][n]["priority"] =
|
||||
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::COMMUNICATION)]
|
||||
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST_CRITICAL)]
|
||||
.error[n]
|
||||
.priority;
|
||||
j["errors"]["communication"]["last_critical"][n]["counter"] =
|
||||
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::COMMUNICATION)]
|
||||
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST_CRITICAL)]
|
||||
.error[n]
|
||||
.counter;
|
||||
}
|
||||
}
|
||||
|
||||
void from_json(const json& j, Logging& k) {
|
||||
// n/a
|
||||
EVLOG_error << "[Logging][from_json()] not implemented";
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const Logging& k) {
|
||||
os << json(k).dump(4);
|
||||
return os;
|
||||
}
|
||||
|
||||
namespace conversions {
|
||||
std::string hexdump(const std::vector<std::uint8_t>& msg) {
|
||||
std::stringstream ss;
|
||||
for (auto index : msg) {
|
||||
ss << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast<int>(index) << " ";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string hexdump(const std::vector<std::uint8_t>& msg, std::uint8_t start, std::uint8_t number_of_chars) {
|
||||
if ((start + number_of_chars) > msg.size())
|
||||
return std::string{};
|
||||
std::stringstream ss;
|
||||
for (std::uint8_t n = start; n < (start + number_of_chars); n++) {
|
||||
ss << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast<int>(msg.at(n));
|
||||
if (n < (start + number_of_chars - 1))
|
||||
ss << " ";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string hexdump(std::uint8_t msg) {
|
||||
std::stringstream ss;
|
||||
ss << "0x" << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast<int>(msg);
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string hexdump(std::uint16_t msg) {
|
||||
std::stringstream ss;
|
||||
ss << "0x" << std::hex << std::uppercase << std::setw(4) << std::setfill('0') << static_cast<std::uint16_t>(msg);
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string hexdump(std::uint64_t msg) {
|
||||
std::stringstream ss;
|
||||
ss << "0x" << std::hex << std::uppercase << std::setw(16) << std::setfill('0') << static_cast<std::uint64_t>(msg);
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string get_string(const std::vector<std::uint8_t>& vec) {
|
||||
std::string str{};
|
||||
for (std::uint16_t n = 0; n < vec.size(); n++) {
|
||||
if ((vec[n] < ' ') || (vec[n] > '~')) {
|
||||
str += " ";
|
||||
} else {
|
||||
str += vec[n];
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string u32_epoch_to_rfc3339(std::uint32_t epoch_time) {
|
||||
time_t tt = static_cast<time_t>(epoch_time);
|
||||
std::tm tm = *std::gmtime(&tt);
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S.000Z");
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
} // namespace conversions
|
||||
} // namespace module
|
||||
@@ -0,0 +1,148 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef DIAGNOSTICS_HPP
|
||||
#define DIAGNOSTICS_HPP
|
||||
|
||||
#include "app_layer.hpp"
|
||||
#include <bitset>
|
||||
#include <date/date.h>
|
||||
#include <date/tz.h>
|
||||
#include <everest/logging.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace module {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct OcmfStats {
|
||||
std::uint32_t number_transactions{};
|
||||
std::uint32_t timestamp_first_transaction{};
|
||||
std::uint32_t timestamp_last_transaction{};
|
||||
std::uint32_t max_number_of_transactions{};
|
||||
};
|
||||
|
||||
struct OcmfInfo {
|
||||
std::string gateway_id{};
|
||||
std::string manufacturer{};
|
||||
std::string model{};
|
||||
};
|
||||
|
||||
struct DeviceData {
|
||||
std::uint32_t utc_time_s{};
|
||||
std::uint8_t gmt_offset_quarterhours{};
|
||||
std::uint64_t total_start_import_energy_Wh{}; // meter value needs to be divided by 10 for Wh
|
||||
std::uint64_t total_stop_import_energy_Wh{}; // meter value needs to be divided by 10 for Wh
|
||||
std::uint32_t total_transaction_duration_s{}; // must be less than 27 days in total
|
||||
OcmfStats ocmf_stats;
|
||||
std::string last_ocmf_transaction;
|
||||
std::string requested_ocmf;
|
||||
std::uint64_t total_dev_import_energy_Wh{}; // meter value needs to be divided by 10 for Wh
|
||||
std::uint64_t ab_status{};
|
||||
};
|
||||
|
||||
void to_json(json& j, const DeviceData& k);
|
||||
void from_json(const json& j, DeviceData& k);
|
||||
std::ostream& operator<<(std::ostream& os, const DeviceData& k);
|
||||
|
||||
struct LogStats {
|
||||
std::uint32_t number_log_entries{};
|
||||
std::uint32_t timestamp_first_log{};
|
||||
std::uint32_t timestamp_last_log{};
|
||||
std::uint32_t max_number_of_logs{};
|
||||
};
|
||||
|
||||
struct ApplicationInfo {
|
||||
std::string mode;
|
||||
std::string fw_ver;
|
||||
std::uint16_t fw_crc{};
|
||||
std::uint16_t fw_hash{};
|
||||
};
|
||||
|
||||
struct MeteringInfo {
|
||||
std::string fw_ver;
|
||||
std::uint16_t fw_crc{};
|
||||
std::uint8_t mode{};
|
||||
};
|
||||
|
||||
struct DeviceInfo {
|
||||
std::string type;
|
||||
std::string hw_ver;
|
||||
std::string server_id;
|
||||
std::uint32_t serial_number{};
|
||||
std::uint8_t bus_address{};
|
||||
std::string bootl_ver;
|
||||
ApplicationInfo application;
|
||||
MeteringInfo metering;
|
||||
};
|
||||
|
||||
struct DeviceDiagnostics {
|
||||
std::string charge_point_id;
|
||||
std::uint8_t charge_point_id_type{0};
|
||||
DeviceInfo dev_info;
|
||||
LogStats log_stats;
|
||||
std::string pubkey_asn1;
|
||||
std::string pubkey_str16;
|
||||
std::string pubkey;
|
||||
std::uint8_t pubkey_str16_format{}; // 0x04 for uncompressed string
|
||||
std::uint8_t pubkey_format{}; // 0x04 for uncompressed string
|
||||
std::vector<std::uint8_t> ocmf_config_table;
|
||||
};
|
||||
|
||||
void to_json(json& j, const DeviceDiagnostics& k);
|
||||
void from_json(const json& j, DeviceDiagnostics& k);
|
||||
std::ostream& operator<<(std::ostream& os, const DeviceDiagnostics& k);
|
||||
|
||||
// TODO(LAD): add error data
|
||||
|
||||
struct ErrorData {
|
||||
std::uint32_t id{0};
|
||||
std::uint16_t priority{0};
|
||||
std::uint32_t counter{0};
|
||||
};
|
||||
|
||||
struct FiveErrors {
|
||||
ErrorData error[5];
|
||||
};
|
||||
|
||||
struct ErrorSet {
|
||||
FiveErrors category[4];
|
||||
};
|
||||
|
||||
struct Logging {
|
||||
app_layer::LogEntry last_log;
|
||||
ErrorSet source[2];
|
||||
};
|
||||
|
||||
void to_json(json& j, const Logging& k);
|
||||
void from_json(const json& j, Logging& k);
|
||||
std::ostream& operator<<(std::ostream& os, const Logging& k);
|
||||
|
||||
namespace conversions {
|
||||
|
||||
template <typename T> static std::string to_bin_string(const T& num) {
|
||||
std::stringstream ss{};
|
||||
for (std::uint8_t n = 0; n < sizeof(T); n++) {
|
||||
ss << std::bitset<8>(num >> ((sizeof(T) - n - 1) * 8));
|
||||
if (n % 2) {
|
||||
if (n != sizeof(T) - 1) {
|
||||
ss << " - ";
|
||||
}
|
||||
} else {
|
||||
ss << " ";
|
||||
}
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string hexdump(const std::vector<std::uint8_t>& msg);
|
||||
std::string hexdump(const std::vector<std::uint8_t>& msg, std::uint8_t start, std::uint8_t number_of_chars);
|
||||
std::string hexdump(std::uint8_t msg);
|
||||
std::string hexdump(std::uint16_t msg);
|
||||
std::string hexdump(std::uint64_t msg);
|
||||
std::string get_string(const std::vector<std::uint8_t>& vec);
|
||||
std::string u32_epoch_to_rfc3339(std::uint32_t epoch_time);
|
||||
|
||||
} // namespace conversions
|
||||
} // namespace module
|
||||
|
||||
#endif // DIAGNOSTICS_HPP
|
||||
@@ -0,0 +1,10 @@
|
||||
.. _everest_modules_handwritten_DZG_GSH01:
|
||||
|
||||
.. *********
|
||||
.. DZG_GSH01
|
||||
.. *********
|
||||
|
||||
This is a prototype driver for the DZG GSH01 powermeter. It has been tested and confirmed to work with EVerest.
|
||||
It does not fully implement all functionality of the operation manual.
|
||||
|
||||
Use with caution and report any issues or limitations encountered.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef MAIN_POWERMETER_IMPL_HPP
|
||||
#define MAIN_POWERMETER_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
#include "../DZG_GSH01.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
#include "app_layer.hpp"
|
||||
#include "diagnostics.hpp"
|
||||
#include "serial_device.hpp"
|
||||
#include "slip_protocol.hpp"
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
struct Conf {
|
||||
int powermeter_device_id;
|
||||
std::string serial_port;
|
||||
int baudrate;
|
||||
int parity;
|
||||
int rs485_direction_gpio;
|
||||
int num_of_retries;
|
||||
bool ignore_echo;
|
||||
int max_clock_diff_s;
|
||||
bool publish_device_data;
|
||||
bool publish_device_diagnostics;
|
||||
};
|
||||
|
||||
class powermeterImpl : public powermeterImplBase {
|
||||
public:
|
||||
powermeterImpl() = delete;
|
||||
powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<DZG_GSH01>& mod, Conf& config) :
|
||||
powermeterImplBase(ev, "main"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual types::powermeter::TransactionStartResponse
|
||||
handle_start_transaction(types::powermeter::TransactionReq& value) override;
|
||||
virtual types::powermeter::TransactionStopResponse handle_stop_transaction(std::string& transaction_id) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<DZG_GSH01>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
// insert your private definitions here
|
||||
|
||||
enum class MessageStatus : std::uint8_t {
|
||||
NONE = 0,
|
||||
SENT = 1,
|
||||
RECEIVED = 2
|
||||
};
|
||||
|
||||
std::atomic_bool communication_timeout{false};
|
||||
void cleanup_dangling_transaction(void);
|
||||
types::powermeter::TransactionStopResponse do_stop_transaction(const std::string& transaction_id);
|
||||
MessageStatus start_transaction_msg_status{MessageStatus::NONE};
|
||||
app_layer::CommandResult start_transact_result{};
|
||||
MessageStatus stop_transaction_msg_status{MessageStatus::NONE};
|
||||
app_layer::CommandResult stop_transact_result{};
|
||||
MessageStatus get_transaction_values_msg_status{MessageStatus::NONE};
|
||||
|
||||
bool charging_in_progress{false};
|
||||
bool no_charging_done{true};
|
||||
bool need_to_stop_transaction{false};
|
||||
|
||||
serial_device::SerialDevice serial_device{};
|
||||
slip_protocol::SlipProtocol slip{};
|
||||
app_layer::AppLayer app_layer{};
|
||||
|
||||
types::powermeter::Powermeter pm_last_values;
|
||||
|
||||
DeviceData device_data_obj{};
|
||||
DeviceDiagnostics device_diagnostics_obj{};
|
||||
Logging logging_obj{};
|
||||
app_layer::ErrorCategory category_requested{};
|
||||
app_layer::ErrorSource source_requested{};
|
||||
uint8_t error_diagnostics_target{0};
|
||||
std::string last_ocmf_str{};
|
||||
|
||||
void init_default_values();
|
||||
void read_powermeter_values();
|
||||
void time_sync();
|
||||
void get_device_time();
|
||||
void set_device_time();
|
||||
void get_meter_bus_address();
|
||||
void set_meter_bus_address(uint8_t old_bus_address, uint8_t new_bus_address);
|
||||
void get_status_word();
|
||||
// void set_device_charge_point_id(app_layer::UserIdType id_type, std::string charge_point_id);
|
||||
void read_device_data();
|
||||
void read_diagnostics_data();
|
||||
void publish_device_data_topic();
|
||||
void publish_device_diagnostics_topic();
|
||||
void publish_logging_topic();
|
||||
void get_device_public_key();
|
||||
void readRegisters();
|
||||
app_layer::CommandResult process_response(const std::vector<uint8_t>& register_message);
|
||||
void request_device_type();
|
||||
void get_app_fw_version();
|
||||
void get_application_operation_mode();
|
||||
void set_application_operation_mode(app_layer::ApplicationBoardMode mode);
|
||||
void get_line_loss_impedance();
|
||||
void set_line_loss_impedance(uint16_t ll_impedance);
|
||||
void request_error_diagnostics(uint8_t addr);
|
||||
void error_diagnostics(uint8_t addr);
|
||||
void send_receive(std::vector<uint8_t>& request);
|
||||
app_layer::CommandResult handle_response(std::vector<uint8_t>& response);
|
||||
std::string get_meter_ocmf();
|
||||
|
||||
static constexpr auto TIMEOUT_2s{std::chrono::seconds(2)};
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
|
||||
#endif // MAIN_POWERMETER_IMPL_HPP
|
||||
@@ -0,0 +1,67 @@
|
||||
description: Module that collects power and energy measurements from an GSH01 powermeter
|
||||
provides:
|
||||
main:
|
||||
description: Implementation of the driver functionality
|
||||
interface: powermeter
|
||||
config:
|
||||
powermeter_device_id:
|
||||
description: The powermeter's address on the serial bus, 255 is broadcast
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 255
|
||||
default: 255
|
||||
serial_port:
|
||||
description: Serial port the hardware is connected to
|
||||
type: string
|
||||
default: /dev/ttyUSB0
|
||||
baudrate:
|
||||
description: Baudrate
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 230400
|
||||
default: 115200
|
||||
parity:
|
||||
description: 'Parity bit: 0: None, 1: Odd, 2: Even'
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 2
|
||||
default: 0
|
||||
rs485_direction_gpio:
|
||||
description: GPIO to use for direction switching. Set to -1 to disable.
|
||||
type: integer
|
||||
default: -1
|
||||
num_of_retries:
|
||||
description: Number of retries sent via serial interface. Set to 0 to disable retry.
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 3
|
||||
default: 0
|
||||
ignore_echo:
|
||||
description: On some hardware every message that is sent is read back, this setting filters the sent message in the reply.
|
||||
type: boolean
|
||||
default: false
|
||||
max_clock_diff_s:
|
||||
description: Maximal time difference (in seconds) between meter clock and system clock, until new set_time command is sent
|
||||
type: integer
|
||||
minimum: 30
|
||||
maximum: 300
|
||||
default: 60
|
||||
publish_device_data:
|
||||
description: Regularly publish device data not covered by the "powermeter" interface (for debugging purposes).
|
||||
type: boolean
|
||||
default: false
|
||||
publish_device_diagnostics:
|
||||
description: Regularly publish device diagnostics (e.g. log messages, errors, SW/HW revisions; for debugging purposes).
|
||||
type: boolean
|
||||
default: false
|
||||
enable_external_mqtt: true
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Lars Dieckmann
|
||||
- Andreas Heinrich
|
||||
- Cornelius Claussen
|
||||
- Florin Mihut
|
||||
- Jan Christoph Habig
|
||||
- Kai-Uwe Hermann
|
||||
- Miriam Thome
|
||||
@@ -0,0 +1,208 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "serial_device.hpp"
|
||||
#include "everest/logging.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <endian.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/select.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
namespace {
|
||||
std::string hexdump(const std::uint8_t* const msg, int msg_len) {
|
||||
std::stringstream ss;
|
||||
for (int i = 0; i < msg_len; i++) {
|
||||
ss << std::hex << static_cast<int>(msg[i]) << " ";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string hexdump(const std::vector<std::uint8_t>& msg) {
|
||||
std::stringstream ss;
|
||||
|
||||
for (auto index : msg) {
|
||||
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(index) << " ";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace serial_device {
|
||||
|
||||
SerialDevice::~SerialDevice() {
|
||||
if (fd != 0) {
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
bool SerialDevice::open_device(const std::string& device, int _baud, bool _ignore_echo, std::uint8_t _num_of_retries) {
|
||||
|
||||
ignore_echo = _ignore_echo;
|
||||
retry_struct.num_of_retries = _num_of_retries;
|
||||
|
||||
fd = open(device.c_str(), O_RDWR | O_NOCTTY | O_SYNC);
|
||||
if (fd < 0) {
|
||||
EVLOG_error << fmt::format("Serial: error {} opening {}: {}\n", errno, device, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int baud;
|
||||
switch (_baud) {
|
||||
case 9600:
|
||||
baud = B9600;
|
||||
break;
|
||||
case 19200:
|
||||
baud = B19200;
|
||||
break;
|
||||
case 38400:
|
||||
baud = B38400;
|
||||
break;
|
||||
case 57600:
|
||||
baud = B57600;
|
||||
break;
|
||||
case 115200:
|
||||
baud = B115200;
|
||||
break;
|
||||
case 230400:
|
||||
baud = B230400;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
struct termios tty;
|
||||
if (tcgetattr(fd, &tty) != 0) {
|
||||
EVLOG_error << "Serial: errorfrom tcgetattr: " << errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
cfsetospeed(&tty, baud);
|
||||
cfsetispeed(&tty, baud);
|
||||
|
||||
tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8; // 8-bit chars
|
||||
// disable IGNBRK for mismatched speed tests; otherwise receive break
|
||||
// as \000 chars
|
||||
tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXOFF | IXANY);
|
||||
tty.c_lflag = 0; // no signaling chars, no echo,
|
||||
// no canonical processing
|
||||
tty.c_oflag = 0; // no remapping, no delays
|
||||
tty.c_cc[VMIN] = 1; // read blocks
|
||||
tty.c_cc[VTIME] = 1; // 0.1 seconds inter character read timeout after first byte was received
|
||||
|
||||
tty.c_cflag |= (CLOCAL | CREAD); // ignore modem controls,
|
||||
// enable reading
|
||||
tty.c_cflag &= ~(PARENB | PARODD); // shut off parity
|
||||
tty.c_cflag &= ~CSTOPB; // 1 Stop bit
|
||||
tty.c_cflag &= ~CRTSCTS;
|
||||
|
||||
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
|
||||
EVLOG_error << "Serial: errorfrom tcsetattr: " << errno;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
This function receives a byte array.
|
||||
*/
|
||||
int SerialDevice::rx(std::vector<std::uint8_t>& rxbuf, std::optional<int> initial_timeout_ms,
|
||||
std::optional<int> in_msg_timeout_ms) {
|
||||
std::scoped_lock lock(serial_mutex);
|
||||
int _initial_timeout = SERIAL_RX_INITIAL_TIMEOUT_MS;
|
||||
if (initial_timeout_ms.has_value()) {
|
||||
_initial_timeout = initial_timeout_ms.value();
|
||||
}
|
||||
int _in_msg_timeout = SERIAL_RX_WITHIN_MESSAGE_TIMEOUT_MS;
|
||||
if (in_msg_timeout_ms.has_value()) {
|
||||
_in_msg_timeout = in_msg_timeout_ms.value();
|
||||
}
|
||||
struct timeval timeout;
|
||||
timeout.tv_sec = 0;
|
||||
timeout.tv_usec = _initial_timeout * 1000; // intial timeout until device responds
|
||||
fd_set set;
|
||||
FD_ZERO(&set);
|
||||
FD_SET(fd, &set);
|
||||
|
||||
int bytes_read_total = 0;
|
||||
while (true) {
|
||||
int rv = select(fd + 1, &set, NULL, NULL, &timeout);
|
||||
timeout.tv_usec = _in_msg_timeout * 1000; // reduce timeout after first chunk,
|
||||
// no uneccesary waiting at the end of the message
|
||||
if (rv == -1) { // error in select function call
|
||||
perror("rx: select:");
|
||||
break;
|
||||
} else if (rv == 0) { // no more bytes to read within timeout, so transfer is complete
|
||||
EVLOG_debug << "No more bytes to read within timeout. (rv == 0)";
|
||||
break;
|
||||
} else { // received more bytes, add them to buffer
|
||||
// do we have space in the rx buffer left?
|
||||
if (bytes_read_total >= rxbuf.capacity()) {
|
||||
// no buffer space left, but more to read.
|
||||
EVLOG_info
|
||||
<< R"(No buffer space left, but more to read. (Did you mean to set "ignore_echo" to "false"?))";
|
||||
break;
|
||||
}
|
||||
|
||||
rxbuf.resize(rxbuf.capacity());
|
||||
int bytes_read =
|
||||
read(fd, (std::uint8_t*)(&rxbuf[0] + bytes_read_total), (size_t)(rxbuf.capacity() - bytes_read_total));
|
||||
|
||||
if (bytes_read > 0) {
|
||||
bytes_read_total += bytes_read;
|
||||
rxbuf.resize(bytes_read_total);
|
||||
} else if (bytes_read < 0) {
|
||||
EVLOG_error << "Error reading from device: " << strerror(errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bytes_read_total;
|
||||
}
|
||||
|
||||
/*
|
||||
This function transmits a byte vector.
|
||||
*/
|
||||
void SerialDevice::tx(const std::vector<std::uint8_t>& request) {
|
||||
{
|
||||
std::scoped_lock lock(serial_mutex);
|
||||
// clear input and output buffer
|
||||
tcflush(fd, TCIOFLUSH);
|
||||
|
||||
// write to serial port
|
||||
write(fd, request.data(), request.size());
|
||||
tcdrain(fd);
|
||||
}
|
||||
if (ignore_echo) {
|
||||
// read back echo of what we sent and ignore it
|
||||
std::vector<std::uint8_t> req_buf{};
|
||||
req_buf.reserve(request.size() + 1);
|
||||
rx(req_buf, std::nullopt, std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
int SerialDevice::tx_rx_blocking(const std::vector<std::uint8_t>& request, std::vector<std::uint8_t>& rxbuf,
|
||||
std::optional<int> initial_timeout_ms, std::optional<int> in_msg_timeout_ms) {
|
||||
std::scoped_lock lock(txrx_mutex);
|
||||
int bytes_rx = 0;
|
||||
|
||||
tx(request);
|
||||
bytes_rx = rx(rxbuf, initial_timeout_ms, in_msg_timeout_ms);
|
||||
|
||||
while (bytes_rx == 0 && retry_struct.num_of_retries_done < retry_struct.num_of_retries) {
|
||||
tx(request);
|
||||
bytes_rx = rx(rxbuf, initial_timeout_ms, in_msg_timeout_ms);
|
||||
retry_struct.num_of_retries_done++;
|
||||
}
|
||||
retry_struct.num_of_retries_done = 0;
|
||||
|
||||
return bytes_rx;
|
||||
}
|
||||
|
||||
} // namespace serial_device
|
||||
@@ -0,0 +1,55 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/*
|
||||
This is an implementation for custom serial communications
|
||||
*/
|
||||
#ifndef SERIAL_DEVICE
|
||||
#define SERIAL_DEVICE
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdint.h>
|
||||
#include <termios.h>
|
||||
|
||||
namespace serial_device {
|
||||
|
||||
constexpr int SERIAL_RX_INITIAL_TIMEOUT_MS = 500;
|
||||
constexpr int SERIAL_RX_WITHIN_MESSAGE_TIMEOUT_MS = 100;
|
||||
|
||||
struct Retry {
|
||||
std::uint8_t num_of_retries{0};
|
||||
std::uint8_t num_of_retries_done{0};
|
||||
};
|
||||
|
||||
class SerialDevice {
|
||||
|
||||
public:
|
||||
SerialDevice() = default;
|
||||
SerialDevice(const SerialDevice&) = delete;
|
||||
SerialDevice(SerialDevice&&) = delete;
|
||||
SerialDevice& operator=(const SerialDevice&) = delete;
|
||||
SerialDevice& operator=(SerialDevice&&) = delete;
|
||||
~SerialDevice();
|
||||
|
||||
bool open_device(const std::string& device, int baud, bool ignore_echo, std::uint8_t _num_of_retries);
|
||||
int tx_rx_blocking(const std::vector<std::uint8_t>& request, std::vector<std::uint8_t>& rxbuf,
|
||||
std::optional<int> initial_timeout_ms, std::optional<int> in_msg_timeout_ms);
|
||||
|
||||
private:
|
||||
// Serial interface
|
||||
int fd{0};
|
||||
bool ignore_echo{false};
|
||||
|
||||
void tx(const std::vector<std::uint8_t>& request);
|
||||
int rx(std::vector<std::uint8_t>& rxbuf, std::optional<int> initial_timeout_ms,
|
||||
std::optional<int> in_msg_timeout_ms);
|
||||
|
||||
std::mutex serial_mutex;
|
||||
std::mutex txrx_mutex;
|
||||
Retry retry_struct;
|
||||
};
|
||||
|
||||
} // namespace serial_device
|
||||
#endif // SERIAL_DEVICE
|
||||
@@ -0,0 +1,218 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "slip_protocol.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <endian.h>
|
||||
#include <everest/crc/crc.hpp>
|
||||
#include <everest/logging.hpp>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/select.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
namespace {
|
||||
inline void remove_start_and_stop_frame(std::vector<std::uint8_t>& vec) {
|
||||
vec.erase(vec.begin());
|
||||
vec.pop_back();
|
||||
}
|
||||
|
||||
// TODO: this is a duplicate of the implementation in AST_DC650, refactor into common library
|
||||
inline bool is_message_crc_correct(std::vector<std::uint8_t>& vec) {
|
||||
if (vec.size() < 2) {
|
||||
return false;
|
||||
}
|
||||
const auto crc_check = static_cast<std::uint16_t>((vec.at(vec.size() - 1) << 8) | vec.at(vec.size() - 2));
|
||||
// remove CRC tail from vector
|
||||
for (std::uint8_t i = 0; i < 2; i++) {
|
||||
vec.pop_back();
|
||||
}
|
||||
auto crc_calc = calculate_xModem_crc16(vec);
|
||||
return crc_check == crc_calc;
|
||||
}
|
||||
|
||||
inline void restore_special_characters(std::vector<std::uint8_t>& vec) {
|
||||
for (std::uint16_t j = 0; j < (vec.size() - 1);
|
||||
j++) { // can only go to vec.size() - 1 because two bytes will be checked
|
||||
if ((vec.at(j) == 0xDB) && (vec.at(j + 1) == 0xDC)) {
|
||||
vec.at(j) = 0xC0;
|
||||
vec.erase(vec.begin() + j + 1);
|
||||
} else if ((vec.at(j) == 0xDB) && (vec.at(j + 1) == 0xDD)) {
|
||||
vec.at(j) = 0xDB;
|
||||
vec.erase(vec.begin() + j + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace slip_protocol {
|
||||
|
||||
std::vector<std::uint8_t> SlipProtocol::package_single(const std::uint8_t address, std::vector<std::uint8_t>& payload) {
|
||||
std::vector<std::uint8_t> vec{};
|
||||
|
||||
// address to the front
|
||||
payload.insert(payload.begin(), address);
|
||||
|
||||
// CRC16 to the end
|
||||
std::uint16_t crc = calculate_xModem_crc16(payload);
|
||||
payload.push_back(static_cast<std::uint8_t>(crc & 0x00FF)); // LSB CRC16
|
||||
payload.push_back(static_cast<std::uint8_t>((crc >> 8) & 0x00FF)); // MSB CRC16
|
||||
|
||||
// replacement of special characters (including address and CRC)
|
||||
for (auto payload_byte : payload) {
|
||||
if (payload_byte == 0xC0) { // check for and replace special char 0xC0
|
||||
vec.push_back(0xDB);
|
||||
vec.push_back(0xDC);
|
||||
} else if (payload_byte == 0xDB) { // check for and replace special char 0xDB
|
||||
vec.push_back(0xDB);
|
||||
vec.push_back(0xDD);
|
||||
} else { // otherwise just use normal input
|
||||
vec.push_back(payload_byte);
|
||||
}
|
||||
}
|
||||
|
||||
// add start frame to front (can be done only after special characters have been replaced)
|
||||
vec.insert(vec.begin(), SLIP_START_END_FRAME);
|
||||
|
||||
// end frame
|
||||
vec.push_back(SLIP_START_END_FRAME);
|
||||
|
||||
return vec;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> SlipProtocol::package_multi(const std::uint8_t address,
|
||||
const std::vector<std::vector<std::uint8_t>>& multi_payload) {
|
||||
std::vector<std::uint8_t> payload{};
|
||||
|
||||
// concatenate requests
|
||||
for (auto multi_payload_part : multi_payload) {
|
||||
payload.insert(payload.end(), multi_payload_part.begin(), multi_payload_part.end());
|
||||
}
|
||||
|
||||
// ...and return as one long request
|
||||
return package_single(address, payload);
|
||||
}
|
||||
|
||||
SlipReturnStatus SlipProtocol::unpack(std::vector<std::uint8_t>& message, std::uint8_t listen_to_address) {
|
||||
SlipReturnStatus retval = SlipReturnStatus::SLIP_ERROR_UNINITIALIZED;
|
||||
std::uint8_t message_start_end_frame_counter = 0;
|
||||
|
||||
if (message.size() < 1) {
|
||||
return SlipReturnStatus::SLIP_ERROR_SIZE_ERROR;
|
||||
}
|
||||
|
||||
// check if first element is SLIP_START_END_FRAME and if not, drop first item(s) until start frame is first
|
||||
std::uint8_t i = 0;
|
||||
while (i < message.size()) {
|
||||
if (message.at(i) == SLIP_START_END_FRAME)
|
||||
break;
|
||||
i++;
|
||||
}
|
||||
if (i > 0) {
|
||||
std::uint8_t j = 0;
|
||||
while (message.size() > 0) {
|
||||
if (j == i)
|
||||
break;
|
||||
message.erase(message.begin());
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
// count number of SLIP_START_END_FRAMEs to check for multiple (or broken) messages
|
||||
for (std::uint16_t n = 0; n < message.size(); n++) {
|
||||
if (message.at(n) == SLIP_START_END_FRAME) {
|
||||
message_start_end_frame_counter++;
|
||||
}
|
||||
}
|
||||
|
||||
if (message_start_end_frame_counter != 2) { // unexpected/broken message -OR- multiple messages received
|
||||
|
||||
// split message vector into sub-vectors by delimiter (SLIP_START_END_FRAME)
|
||||
std::vector<std::vector<std::uint8_t>> sub_messages{};
|
||||
std::vector<std::uint8_t> current_sub_message{};
|
||||
for (std::uint16_t n = 0; n < message.size(); n++) {
|
||||
if (message.at(n) == SLIP_START_END_FRAME) {
|
||||
if (current_sub_message.size() > 0) {
|
||||
if (listen_to_address == 0xFF) {
|
||||
// listen_to_address is broadcast address: listen to all messages
|
||||
sub_messages.push_back(current_sub_message);
|
||||
} else {
|
||||
// dedicated client address selected, only listen to this address
|
||||
if (current_sub_message.at(0) == listen_to_address) {
|
||||
sub_messages.push_back(current_sub_message);
|
||||
}
|
||||
}
|
||||
current_sub_message.clear();
|
||||
} else {
|
||||
// intentionally do nothing on empty sub_message
|
||||
}
|
||||
} else {
|
||||
current_sub_message.push_back(message.at(n));
|
||||
}
|
||||
}
|
||||
|
||||
// from here on, we have all message parts as elements in sub_messages
|
||||
for (auto sub_message : sub_messages) {
|
||||
restore_special_characters(sub_message);
|
||||
// check all sub-messages' CRC and only process on match
|
||||
if (is_message_crc_correct(sub_message)) {
|
||||
// on correct CRC
|
||||
message_queue.push_back(sub_message);
|
||||
message_counter++;
|
||||
if (retval == SlipReturnStatus::SLIP_ERROR_UNINITIALIZED) { // only set SLIP_OK if no other error
|
||||
retval = SlipReturnStatus::SLIP_OK;
|
||||
}
|
||||
} else {
|
||||
retval = SlipReturnStatus::SLIP_ERROR_MALFORMED;
|
||||
EVLOG_error << "Malformed message received!";
|
||||
}
|
||||
}
|
||||
} else { // single message received
|
||||
|
||||
if (message.size() > 3) {
|
||||
// only process messages for correct client or if listen_to_address is broadcast address
|
||||
if ((message.at(1) == listen_to_address) || (0xFF == listen_to_address)) {
|
||||
// check if last element is SLIP_START_END_FRAME and if not, reduce message size
|
||||
while (message.at(message.size() - 1) != SLIP_START_END_FRAME) {
|
||||
if (message.size() <= 3)
|
||||
break;
|
||||
message.pop_back();
|
||||
}
|
||||
|
||||
remove_start_and_stop_frame(message);
|
||||
// check for special characters and restore to original contents
|
||||
restore_special_characters(message);
|
||||
if (is_message_crc_correct(message)) {
|
||||
message_queue.push_back(message);
|
||||
message_counter++;
|
||||
retval = SlipReturnStatus::SLIP_OK;
|
||||
} else {
|
||||
retval = SlipReturnStatus::SLIP_ERROR_CRC_MISMATCH;
|
||||
EVLOG_error << "CRC mismatch!";
|
||||
}
|
||||
} else {
|
||||
retval = SlipReturnStatus::SLIP_OK;
|
||||
}
|
||||
} else {
|
||||
retval = SlipReturnStatus::SLIP_ERROR_SIZE_ERROR;
|
||||
EVLOG_error << "Message broken: too short!";
|
||||
}
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> SlipProtocol::retrieve_single_message() {
|
||||
if (message_queue.size() > 0) {
|
||||
message_counter--;
|
||||
auto ret_vec = message_queue.back();
|
||||
message_queue.pop_back();
|
||||
return ret_vec;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace slip_protocol
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/*
|
||||
This is an implementation for the SLIP serial protocol
|
||||
*/
|
||||
#ifndef SLIP_PROTOCOL
|
||||
#define SLIP_PROTOCOL
|
||||
|
||||
#include <cstdint>
|
||||
#include <everest/crc/crc.hpp>
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
namespace slip_protocol {
|
||||
|
||||
constexpr int SLIP_START_END_FRAME = 0xC0;
|
||||
constexpr int SLIP_BROADCAST_ADDR = 0xFF;
|
||||
|
||||
constexpr std::uint16_t SLIP_SIZE_ON_ERROR = 1;
|
||||
|
||||
constexpr std::int8_t SLIP_ERROR_SIZE_ERROR = -1;
|
||||
constexpr std::int8_t SLIP_ERROR_MALFORMED = -2;
|
||||
constexpr std::int8_t SLIP_ERROR_CRC_MISMATCH = -3;
|
||||
|
||||
enum class SlipReturnStatus : std::int8_t {
|
||||
SLIP_ERROR_CRC_MISMATCH = -3,
|
||||
SLIP_ERROR_MALFORMED = -2,
|
||||
SLIP_ERROR_SIZE_ERROR = -1,
|
||||
SLIP_OK = 0,
|
||||
SLIP_ERROR_UNINITIALIZED = 1
|
||||
};
|
||||
|
||||
class SlipProtocol {
|
||||
public:
|
||||
std::vector<std::uint8_t> package_single(std::uint8_t address, std::vector<std::uint8_t>& payload);
|
||||
std::vector<std::uint8_t> package_multi(std::uint8_t address,
|
||||
const std::vector<std::vector<std::uint8_t>>& multi_payload);
|
||||
|
||||
SlipReturnStatus unpack(std::vector<std::uint8_t>& message, std::uint8_t listen_to_address);
|
||||
auto get_message_counter() const {
|
||||
return message_counter;
|
||||
}
|
||||
std::vector<std::uint8_t> retrieve_single_message();
|
||||
|
||||
private:
|
||||
std::vector<std::vector<std::uint8_t>> message_queue;
|
||||
std::uint8_t message_counter{0};
|
||||
};
|
||||
|
||||
} // namespace slip_protocol
|
||||
#endif // SLIP_PROTOCOL
|
||||
@@ -0,0 +1,11 @@
|
||||
load("//modules:module.bzl", "cc_everest_module")
|
||||
|
||||
IMPLS = [
|
||||
"main",
|
||||
]
|
||||
|
||||
cc_everest_module(
|
||||
name = "GenericPowermeter",
|
||||
deps = [],
|
||||
impls = IMPLS,
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
#
|
||||
# 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
|
||||
|
||||
install(
|
||||
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/models/
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/everest/modules/${MODULE_NAME}/models/"
|
||||
)
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"main/powermeterImpl.cpp"
|
||||
)
|
||||
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
# insert other things like install cmds etc here
|
||||
target_link_libraries(${MODULE_NAME} PRIVATE everest::yaml)
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include "GenericPowermeter.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
void GenericPowermeter::init() {
|
||||
invoke_init(*p_main);
|
||||
}
|
||||
|
||||
void GenericPowermeter::ready() {
|
||||
invoke_ready(*p_main);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef GENERIC_POWERMETER_HPP
|
||||
#define GENERIC_POWERMETER_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
// headers for required interface implementations
|
||||
#include <generated/interfaces/serial_communication_hub/Interface.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
// insert your custom include headers here
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class GenericPowermeter : public Everest::ModuleBase {
|
||||
public:
|
||||
GenericPowermeter() = delete;
|
||||
GenericPowermeter(const ModuleInfo& info, std::unique_ptr<powermeterImplBase> p_main,
|
||||
std::unique_ptr<serial_communication_hubIntf> r_serial_comm_hub, Conf& config) :
|
||||
ModuleBase(info), p_main(std::move(p_main)), r_serial_comm_hub(std::move(r_serial_comm_hub)), config(config){};
|
||||
|
||||
const std::unique_ptr<powermeterImplBase> p_main;
|
||||
const std::unique_ptr<serial_communication_hubIntf> r_serial_comm_hub;
|
||||
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
|
||||
// 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 // GENERIC_POWERMETER_HPP
|
||||
@@ -0,0 +1,96 @@
|
||||
.. _everest_modules_handwritten_GenericPowermeter:
|
||||
|
||||
.. *****************
|
||||
.. GenericPowermeter
|
||||
.. *****************
|
||||
|
||||
The module ``GenericPowermeter`` provides a connection to get data from
|
||||
powermeters that are connected via Modbus RTU and whose data is automatically
|
||||
updated (without startup configuration and/or request functions).
|
||||
|
||||
It supports both AC and DC powermeters.
|
||||
|
||||
To do so, a register configuration file is needed that describes which (ModbusRTU-)registers
|
||||
are available on the device and what data they contain. The available data will then be used
|
||||
to calculate the correct structure of data for the ``powermeter`` interface of EVerest.
|
||||
|
||||
Currently available powermeter configurations can be found in the module's ``models``
|
||||
subdirectory.
|
||||
|
||||
To add a new powermeter to the set of available devices, there exists a ``template.yaml``
|
||||
file in the ``models`` subdirectory which can be copied and filled with the new powermeter's
|
||||
data from the datasheet.
|
||||
|
||||
|
||||
Datasets in the configuration file
|
||||
==================================
|
||||
|
||||
Available datasets for the module ``GenericPowermeter``
|
||||
-------------------------------------------------------
|
||||
|
||||
This module can read the following parameters from a powermeter:
|
||||
|
||||
(for a description of the parameter values see interface ``powermeter``)
|
||||
|
||||
* energy_Wh_import
|
||||
* energy_Wh_export
|
||||
* power_W
|
||||
* voltage_V
|
||||
* VAR
|
||||
* current_A
|
||||
* frequency_Hz
|
||||
|
||||
Dataset description
|
||||
-------------------
|
||||
|
||||
A typical dataset consists of the following parameters:
|
||||
|
||||
(<start_register> of length <num_registers>) * <multiplier> * 10^(<exponent_register>)
|
||||
|
||||
* <start_register> = the device's ModbusRTU register at which the value for the data of this
|
||||
type is being stored (set to "0" if this value is not available in the powermeter)
|
||||
* <function_code_start_reg> = ModbusRTU function code used to obtain this register's value
|
||||
(currently implemented: ``3`` (``read holding registers``) and ``4`` (``read input registers``))
|
||||
* <num_registers> = the number of registers to read from the address of <start_register>
|
||||
* <multiplier> = a multiplier to manually (pre-)scale the register's value (i.e. set to ``0.001``
|
||||
when the powermeter provides energy in ``kWh``, as the ``powermeter`` interface uses energy
|
||||
in ``Wh``)
|
||||
* <exponent_register> = the device's ModbusRTU register at which the exponent for the
|
||||
register set is being stored (set to "0" if this value is not available in the powermeter)
|
||||
* <function_code_exp_reg> = ModbusRTU function code used to obtain this register's exponent
|
||||
(currently implemented: ``3`` (``read holding registers``) and ``4`` (``read input registers``))
|
||||
|
||||
|
||||
Structure of datasets in the configuration file
|
||||
-----------------------------------------------
|
||||
|
||||
Datasets are structured into two levels:
|
||||
|
||||
* first level : contains either DC value or, in AC case, sum total of all corresponding lines (L1/2/3)
|
||||
* second level : contains AC values, split into their corresponding lines (L1/2/3)
|
||||
|
||||
Other things to note
|
||||
--------------------
|
||||
|
||||
* if measuring AC, the first level of registers is always "total/sum" of a certain value and
|
||||
the L1/2/3 registers are for the distinct phases
|
||||
* if measuring DC, only use the first level of registers
|
||||
|
||||
Published variables
|
||||
===================
|
||||
|
||||
powermeter
|
||||
----------
|
||||
|
||||
The basic dataset of powermeter values as used in the EVerest ``powermeter`` interface.
|
||||
This dataset will be periodically published by the module.
|
||||
|
||||
|
||||
Provided commands
|
||||
=================
|
||||
|
||||
get_signed_meter_value
|
||||
----------------------
|
||||
|
||||
`currently not implemented`
|
||||
|
||||
@@ -0,0 +1,584 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "powermeterImpl.hpp"
|
||||
#include <fmt/core.h>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <utils/date.hpp>
|
||||
#include <utils/yaml_loader.hpp>
|
||||
|
||||
const std::string MODELS_SUB_DIR = "models";
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
void powermeterImpl::init() {
|
||||
|
||||
const std::size_t found = this->config.model.find(".."); // check for invalid path
|
||||
if (found != std::string::npos) {
|
||||
EVLOG_error << fmt::format("Error! Substring \"..\" not allowed in given model name '{}'!", this->config.model);
|
||||
throw std::runtime_error("Incorrect model name in GenericPowermeter config");
|
||||
}
|
||||
|
||||
const auto model = this->mod->info.paths.share / MODELS_SUB_DIR / fmt::format("{}.yaml", this->config.model);
|
||||
|
||||
try {
|
||||
const json powermeter_registers = Everest::load_yaml(model);
|
||||
this->init_register_assignments(std::move(powermeter_registers));
|
||||
this->init_default_values();
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "opening file \"" << this->config.model << ".yaml\" from path " << model
|
||||
<< " failed: " << e.what();
|
||||
throw std::runtime_error(fmt::format("Module \"GenericPowermeter\" could not be initialized: {}", e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void powermeterImpl::ready() {
|
||||
std::thread([this] {
|
||||
while (true) {
|
||||
this->read_powermeter_values();
|
||||
// Note: reading the power meter values may take several seconds already, so the complete loop
|
||||
// time of this function will be well beyond the sleep time given below.
|
||||
sleep(1);
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transaction(std::string& transaction_id) {
|
||||
return {types::powermeter::TransactionRequestStatus::NOT_SUPPORTED,
|
||||
{},
|
||||
{},
|
||||
"Generic powermeter does not support the stop_transaction command"};
|
||||
};
|
||||
|
||||
types::powermeter::TransactionStartResponse
|
||||
powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& value) {
|
||||
return {types::powermeter::TransactionRequestStatus::NOT_SUPPORTED,
|
||||
"Generic powermeter does not support the start_transaction command"};
|
||||
}
|
||||
|
||||
void powermeterImpl::init_default_values() {
|
||||
this->pm_last_values.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
|
||||
this->pm_last_values.meter_id = std::string(this->mod->info.id);
|
||||
|
||||
for (const auto& register_data : this->pm_configuration) {
|
||||
if (register_data.type == ENERGY_WH_IMPORT_TOTAL) {
|
||||
this->pm_last_values.energy_Wh_import.total = 0.0f;
|
||||
} else if (register_data.type == ENERGY_WH_IMPORT_L1) {
|
||||
this->pm_last_values.energy_Wh_import.L1 = 0.0f;
|
||||
} else if (register_data.type == ENERGY_WH_IMPORT_L2) {
|
||||
this->pm_last_values.energy_Wh_import.L2 = 0.0f;
|
||||
} else if (register_data.type == ENERGY_WH_IMPORT_L3) {
|
||||
this->pm_last_values.energy_Wh_import.L3 = 0.0f;
|
||||
} else if (register_data.type == ENERGY_WH_EXPORT_TOTAL) {
|
||||
types::units::Energy energy_out;
|
||||
if (this->pm_last_values.energy_Wh_export.has_value()) {
|
||||
energy_out = this->pm_last_values.energy_Wh_export.value();
|
||||
}
|
||||
energy_out.total = 0.0f;
|
||||
this->pm_last_values.energy_Wh_export = energy_out;
|
||||
} else if (register_data.type == ENERGY_WH_EXPORT_L1) {
|
||||
types::units::Energy energy_out;
|
||||
if (this->pm_last_values.energy_Wh_export.has_value()) {
|
||||
energy_out = this->pm_last_values.energy_Wh_export.value();
|
||||
}
|
||||
energy_out.L1 = 0.0f;
|
||||
this->pm_last_values.energy_Wh_export = energy_out;
|
||||
} else if (register_data.type == ENERGY_WH_EXPORT_L2) {
|
||||
types::units::Energy energy_out;
|
||||
if (this->pm_last_values.energy_Wh_export.has_value()) {
|
||||
energy_out = this->pm_last_values.energy_Wh_export.value();
|
||||
}
|
||||
energy_out.L2 = 0.0f;
|
||||
this->pm_last_values.energy_Wh_export = energy_out;
|
||||
} else if (register_data.type == ENERGY_WH_EXPORT_L3) {
|
||||
types::units::Energy energy_out;
|
||||
if (this->pm_last_values.energy_Wh_export.has_value()) {
|
||||
energy_out = this->pm_last_values.energy_Wh_export.value();
|
||||
}
|
||||
energy_out.L3 = 0.0f;
|
||||
this->pm_last_values.energy_Wh_export = energy_out;
|
||||
} else if (register_data.type == POWER_W_TOTAL) {
|
||||
types::units::Power pwr;
|
||||
if (this->pm_last_values.power_W.has_value()) {
|
||||
pwr = this->pm_last_values.power_W.value();
|
||||
}
|
||||
pwr.total = 0.0f;
|
||||
this->pm_last_values.power_W = pwr;
|
||||
} else if (register_data.type == POWER_W_L1) {
|
||||
types::units::Power pwr;
|
||||
if (this->pm_last_values.power_W.has_value()) {
|
||||
pwr = this->pm_last_values.power_W.value();
|
||||
}
|
||||
pwr.L1 = 0.0f;
|
||||
this->pm_last_values.power_W = pwr;
|
||||
} else if (register_data.type == POWER_W_L2) {
|
||||
types::units::Power pwr;
|
||||
if (this->pm_last_values.power_W.has_value()) {
|
||||
pwr = this->pm_last_values.power_W.value();
|
||||
}
|
||||
pwr.L2 = 0.0f;
|
||||
this->pm_last_values.power_W = pwr;
|
||||
} else if (register_data.type == POWER_W_L3) {
|
||||
types::units::Power pwr;
|
||||
if (this->pm_last_values.power_W.has_value()) {
|
||||
pwr = this->pm_last_values.power_W.value();
|
||||
}
|
||||
pwr.L3 = 0.0f;
|
||||
this->pm_last_values.power_W = pwr;
|
||||
} else if (register_data.type == VOLTAGE_V_DC) {
|
||||
types::units::Voltage volt;
|
||||
if (this->pm_last_values.voltage_V.has_value()) {
|
||||
volt = this->pm_last_values.voltage_V.value();
|
||||
}
|
||||
volt.DC = 0.0f;
|
||||
this->pm_last_values.voltage_V = volt;
|
||||
} else if (register_data.type == VOLTAGE_V_L1) {
|
||||
types::units::Voltage volt;
|
||||
if (this->pm_last_values.voltage_V.has_value()) {
|
||||
volt = this->pm_last_values.voltage_V.value();
|
||||
}
|
||||
volt.L1 = 0.0f;
|
||||
this->pm_last_values.voltage_V = volt;
|
||||
} else if (register_data.type == VOLTAGE_V_L2) {
|
||||
types::units::Voltage volt;
|
||||
if (this->pm_last_values.voltage_V.has_value()) {
|
||||
volt = this->pm_last_values.voltage_V.value();
|
||||
}
|
||||
volt.L2 = 0.0f;
|
||||
this->pm_last_values.voltage_V = volt;
|
||||
} else if (register_data.type == VOLTAGE_V_L3) {
|
||||
types::units::Voltage volt;
|
||||
if (this->pm_last_values.voltage_V.has_value()) {
|
||||
volt = this->pm_last_values.voltage_V.value();
|
||||
}
|
||||
volt.L3 = 0.0f;
|
||||
this->pm_last_values.voltage_V = volt;
|
||||
} else if (register_data.type == REACTIVE_POWER_VAR_TOTAL) {
|
||||
types::units::ReactivePower var;
|
||||
if (this->pm_last_values.VAR.has_value()) {
|
||||
var = this->pm_last_values.VAR.value();
|
||||
}
|
||||
var.total = 0.0f;
|
||||
this->pm_last_values.VAR = var;
|
||||
} else if (register_data.type == REACTIVE_POWER_VAR_L1) {
|
||||
types::units::ReactivePower var;
|
||||
if (this->pm_last_values.VAR.has_value()) {
|
||||
var = this->pm_last_values.VAR.value();
|
||||
}
|
||||
var.L1 = 0.0f;
|
||||
this->pm_last_values.VAR = var;
|
||||
} else if (register_data.type == REACTIVE_POWER_VAR_L2) {
|
||||
types::units::ReactivePower var;
|
||||
if (this->pm_last_values.VAR.has_value()) {
|
||||
var = this->pm_last_values.VAR.value();
|
||||
}
|
||||
var.L2 = 0.0f;
|
||||
this->pm_last_values.VAR = var;
|
||||
} else if (register_data.type == REACTIVE_POWER_VAR_L3) {
|
||||
types::units::ReactivePower var;
|
||||
if (this->pm_last_values.VAR.has_value()) {
|
||||
var = this->pm_last_values.VAR.value();
|
||||
}
|
||||
var.L3 = 0.0f;
|
||||
this->pm_last_values.VAR = var;
|
||||
} else if (register_data.type == CURRENT_A_DC) {
|
||||
types::units::Current amp;
|
||||
if (this->pm_last_values.current_A.has_value()) {
|
||||
amp = this->pm_last_values.current_A.value();
|
||||
}
|
||||
amp.DC = 0.0f;
|
||||
this->pm_last_values.current_A = amp;
|
||||
} else if (register_data.type == CURRENT_A_L1) {
|
||||
types::units::Current amp;
|
||||
if (this->pm_last_values.current_A.has_value()) {
|
||||
amp = this->pm_last_values.current_A.value();
|
||||
}
|
||||
amp.L1 = 0.0f;
|
||||
this->pm_last_values.current_A = amp;
|
||||
} else if (register_data.type == CURRENT_A_L2) {
|
||||
types::units::Current amp;
|
||||
if (this->pm_last_values.current_A.has_value()) {
|
||||
amp = this->pm_last_values.current_A.value();
|
||||
}
|
||||
amp.L2 = 0.0f;
|
||||
this->pm_last_values.current_A = amp;
|
||||
} else if (register_data.type == CURRENT_A_L3) {
|
||||
types::units::Current amp;
|
||||
if (this->pm_last_values.current_A.has_value()) {
|
||||
amp = this->pm_last_values.current_A.value();
|
||||
}
|
||||
amp.L3 = 0.0f;
|
||||
this->pm_last_values.current_A = amp;
|
||||
} else if (register_data.type == FREQUENCY_HZ_L1) {
|
||||
types::units::Frequency freq;
|
||||
if (this->pm_last_values.frequency_Hz.has_value()) {
|
||||
freq = this->pm_last_values.frequency_Hz.value();
|
||||
}
|
||||
freq.L1 = 0.0f;
|
||||
this->pm_last_values.frequency_Hz = freq;
|
||||
} else if (register_data.type == FREQUENCY_HZ_L2) {
|
||||
types::units::Frequency freq;
|
||||
if (this->pm_last_values.frequency_Hz.has_value()) {
|
||||
freq = this->pm_last_values.frequency_Hz.value();
|
||||
}
|
||||
freq.L2 = 0.0f;
|
||||
this->pm_last_values.frequency_Hz = freq;
|
||||
} else if (register_data.type == FREQUENCY_HZ_L3) {
|
||||
types::units::Frequency freq;
|
||||
if (this->pm_last_values.frequency_Hz.has_value()) {
|
||||
freq = this->pm_last_values.frequency_Hz.value();
|
||||
}
|
||||
freq.L3 = 0.0f;
|
||||
this->pm_last_values.frequency_Hz = freq;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void powermeterImpl::init_register_assignments(const json& loaded_registers) {
|
||||
bool failed{false};
|
||||
failed |= not this->assign_register_data(loaded_registers, ENERGY_WH_IMPORT_TOTAL, "energy_Wh_import");
|
||||
failed |= not this->assign_register_data(loaded_registers, ENERGY_WH_EXPORT_TOTAL, "energy_Wh_export");
|
||||
failed |= not this->assign_register_data(loaded_registers, POWER_W_TOTAL, "power_W");
|
||||
failed |= not this->assign_register_data(loaded_registers, VOLTAGE_V_DC, "voltage_V");
|
||||
failed |= not this->assign_register_data(loaded_registers, REACTIVE_POWER_VAR_TOTAL, "reactive_power_VAR");
|
||||
failed |= not this->assign_register_data(loaded_registers, CURRENT_A_DC, "current_A");
|
||||
failed |= not this->assign_register_data(loaded_registers, FREQUENCY_HZ_L1, "frequency_Hz");
|
||||
|
||||
if (failed) {
|
||||
EVLOG_error << "Could not load powermeter model configuration!";
|
||||
throw std::runtime_error("Could not load GenericPowermeter model configuration");
|
||||
}
|
||||
}
|
||||
|
||||
bool powermeterImpl::assign_register_data(const json& registers, const PowermeterRegisters register_type,
|
||||
const std::string& register_selector) {
|
||||
try {
|
||||
if (registers.contains(register_selector)) {
|
||||
if (registers.at(register_selector).at("num_registers") > 0) {
|
||||
RegisterData data = {};
|
||||
data.type = register_type;
|
||||
data.start_register = registers.at(register_selector).at("start_register");
|
||||
data.start_register_function = this->select_modbus_function(
|
||||
(const uint8_t)registers.at(register_selector).at("function_code_start_reg"));
|
||||
data.num_registers = registers.at(register_selector).at("num_registers");
|
||||
data.exponent_register = registers.at(register_selector).at("exponent_register");
|
||||
data.exponent_register_function = this->select_modbus_function(
|
||||
(const uint8_t)registers.at(register_selector).at("function_code_exp_reg"));
|
||||
data.multiplier = registers.at(register_selector).at("multiplier");
|
||||
this->pm_configuration.push_back(data);
|
||||
}
|
||||
if (registers.at(register_selector).contains("L1")) {
|
||||
if (registers.at(register_selector).at("L1").at("num_registers") > 0) {
|
||||
assign_register_sublevel_data(registers, register_type, register_selector, "L1", 1);
|
||||
}
|
||||
}
|
||||
if (registers.at(register_selector).contains("L2")) {
|
||||
if (registers.at(register_selector).at("L2").at("num_registers") > 0) {
|
||||
assign_register_sublevel_data(registers, register_type, register_selector, "L2", 2);
|
||||
}
|
||||
}
|
||||
if (registers.at(register_selector).contains("L3")) {
|
||||
if (registers.at(register_selector).at("L3").at("num_registers") > 0) {
|
||||
assign_register_sublevel_data(registers, register_type, register_selector, "L3", 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "Assigning configuration data for register \"" << register_selector << "\" failed: " << e.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void powermeterImpl::assign_register_sublevel_data(const json& registers, const PowermeterRegisters& register_type,
|
||||
const std::string& register_selector,
|
||||
const std::string& sublevel_selector, const uint8_t offset) {
|
||||
|
||||
RegisterData sublevel_data = {};
|
||||
sublevel_data.type = static_cast<PowermeterRegisters>((register_type + offset) % NUM_PM_REGISTERS);
|
||||
sublevel_data.start_register = registers.at(register_selector).at(sublevel_selector).at("start_register");
|
||||
sublevel_data.start_register_function = this->select_modbus_function(
|
||||
(const uint8_t)registers.at(register_selector).at(sublevel_selector).at("function_code_start_reg"));
|
||||
sublevel_data.num_registers = registers.at(register_selector).at(sublevel_selector).at("num_registers");
|
||||
sublevel_data.exponent_register = registers.at(register_selector).at(sublevel_selector).at("exponent_register");
|
||||
sublevel_data.exponent_register_function = this->select_modbus_function(
|
||||
(const uint8_t)registers.at(register_selector).at(sublevel_selector).at("function_code_exp_reg"));
|
||||
sublevel_data.multiplier = registers.at(register_selector).at(sublevel_selector).at("multiplier");
|
||||
this->pm_configuration.push_back(sublevel_data);
|
||||
}
|
||||
|
||||
powermeterImpl::ModbusFunctionType powermeterImpl::select_modbus_function(const uint8_t function_code) {
|
||||
switch (function_code) {
|
||||
case 0x03:
|
||||
return READ_HOLDING_REGISTER;
|
||||
break;
|
||||
|
||||
case 0x04:
|
||||
return READ_INPUT_REGISTER;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw std::runtime_error(fmt::format("Incorrect Modbus RTU function code {}!", function_code));
|
||||
break;
|
||||
}
|
||||
return REGISTER_TYPE_UNDEFINED;
|
||||
}
|
||||
|
||||
void powermeterImpl::read_powermeter_values() {
|
||||
static bool pm_values_are_complete{false};
|
||||
bool all_pm_registers_success{true};
|
||||
for (const auto& register_data : this->pm_configuration) {
|
||||
all_pm_registers_success &= this->read_register(register_data);
|
||||
}
|
||||
if (all_pm_registers_success) {
|
||||
pm_values_are_complete = true;
|
||||
}
|
||||
|
||||
if (not pm_values_are_complete) {
|
||||
EVLOG_warning << "No complete set of power meter values has been acquired yet. Not publishing.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->pm_last_values.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
|
||||
this->publish_powermeter(this->pm_last_values);
|
||||
}
|
||||
|
||||
bool powermeterImpl::read_register(const RegisterData& register_config) {
|
||||
|
||||
types::serial_comm_hub_requests::Result register_response{};
|
||||
std::optional<types::serial_comm_hub_requests::Result> exponent_response;
|
||||
|
||||
if (register_config.start_register_function == READ_HOLDING_REGISTER) {
|
||||
register_response = mod->r_serial_comm_hub->call_modbus_read_holding_registers(
|
||||
this->config.powermeter_device_id, register_config.start_register, register_config.num_registers);
|
||||
} else if (register_config.start_register_function == READ_INPUT_REGISTER) {
|
||||
register_response = mod->r_serial_comm_hub->call_modbus_read_input_registers(
|
||||
this->config.powermeter_device_id, register_config.start_register - this->config.modbus_base_address,
|
||||
register_config.num_registers);
|
||||
}
|
||||
|
||||
if (register_config.exponent_register != 0) {
|
||||
if (register_config.exponent_register_function == READ_HOLDING_REGISTER) {
|
||||
exponent_response = mod->r_serial_comm_hub->call_modbus_read_holding_registers(
|
||||
this->config.powermeter_device_id, register_config.exponent_register, register_config.num_registers);
|
||||
} else if (register_config.exponent_register_function == READ_INPUT_REGISTER) {
|
||||
exponent_response = mod->r_serial_comm_hub->call_modbus_read_input_registers(
|
||||
this->config.powermeter_device_id, register_config.exponent_register - this->config.modbus_base_address,
|
||||
register_config.num_registers);
|
||||
}
|
||||
return this->process_response(register_config, std::move(register_response), std::move(exponent_response));
|
||||
} else {
|
||||
// no exponent
|
||||
return this->process_response(register_config, std::move(register_response), std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
bool powermeterImpl::process_response(
|
||||
const RegisterData& register_data, const types::serial_comm_hub_requests::Result& register_message,
|
||||
std::optional<std::reference_wrapper<const types::serial_comm_hub_requests::Result>> exponent_message) {
|
||||
|
||||
if ((register_message.status_code != types::serial_comm_hub_requests::StatusCodeEnum::Success) ||
|
||||
(exponent_message &&
|
||||
exponent_message->get().status_code != types::serial_comm_hub_requests::StatusCodeEnum::Success)) {
|
||||
// error: message sending failed
|
||||
output_error_with_content(register_message);
|
||||
if (exponent_message) {
|
||||
output_error_with_content(exponent_message.value());
|
||||
}
|
||||
|
||||
// let's warn the user about the meter's unavailability once only
|
||||
// (since we keep trying communicating an 'error' is not justified)
|
||||
if (!meter_is_unavailable) {
|
||||
EVLOG_warning << "Lost communication with power meter.";
|
||||
meter_is_unavailable = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// SerialCommHub implementation should be preventing this in case of StatusCodeEnum::Success
|
||||
if (not register_message.value.has_value()) {
|
||||
EVLOG_warning << "Power meter reading returned without a value, skipping";
|
||||
return false;
|
||||
}
|
||||
if (exponent_message and not exponent_message->get().value.has_value()) {
|
||||
EVLOG_warning << "Power meter reading returned without an exponent value, skipping";
|
||||
return false;
|
||||
}
|
||||
if (register_message.value.value().size() == 0) {
|
||||
EVLOG_warning << "Power meter reading returned an empty value, skipping";
|
||||
return false;
|
||||
}
|
||||
if (exponent_message and exponent_message->get().value.value().size() == 0) {
|
||||
EVLOG_warning << "Power meter reading returned an empty exponent value, skipping";
|
||||
return false;
|
||||
}
|
||||
|
||||
// in case the meter was unavailable before and now the query succeeded,
|
||||
// we can tell the user about this good news and reset our flag
|
||||
if (meter_is_unavailable) {
|
||||
EVLOG_info << "Communication with power meter restored.";
|
||||
meter_is_unavailable = false;
|
||||
}
|
||||
|
||||
int16_t exponent = 0;
|
||||
if (exponent_message) {
|
||||
exponent = exponent_message->get().value.value().at(0);
|
||||
}
|
||||
|
||||
if (register_data.type == ENERGY_WH_IMPORT_TOTAL) {
|
||||
types::units::Energy energy_in = this->pm_last_values.energy_Wh_import;
|
||||
energy_in.total = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.energy_Wh_import = energy_in;
|
||||
} else if (register_data.type == ENERGY_WH_IMPORT_L1) {
|
||||
types::units::Energy energy_in = this->pm_last_values.energy_Wh_import;
|
||||
energy_in.L1 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.energy_Wh_import = energy_in;
|
||||
} else if (register_data.type == ENERGY_WH_IMPORT_L2) {
|
||||
types::units::Energy energy_in = this->pm_last_values.energy_Wh_import;
|
||||
energy_in.L2 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.energy_Wh_import = energy_in;
|
||||
} else if (register_data.type == ENERGY_WH_IMPORT_L3) {
|
||||
types::units::Energy energy_in = this->pm_last_values.energy_Wh_import;
|
||||
energy_in.L3 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.energy_Wh_import = energy_in;
|
||||
} else if (register_data.type == ENERGY_WH_EXPORT_TOTAL) {
|
||||
types::units::Energy energy_out = this->pm_last_values.energy_Wh_export.value();
|
||||
energy_out.total = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.energy_Wh_export = energy_out;
|
||||
} else if (register_data.type == ENERGY_WH_EXPORT_L1) {
|
||||
types::units::Energy energy_out = this->pm_last_values.energy_Wh_export.value();
|
||||
energy_out.L1 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.energy_Wh_export = energy_out;
|
||||
} else if (register_data.type == ENERGY_WH_EXPORT_L2) {
|
||||
types::units::Energy energy_out = this->pm_last_values.energy_Wh_export.value();
|
||||
energy_out.L2 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.energy_Wh_export = energy_out;
|
||||
} else if (register_data.type == ENERGY_WH_EXPORT_L3) {
|
||||
types::units::Energy energy_out = this->pm_last_values.energy_Wh_export.value();
|
||||
energy_out.L3 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.energy_Wh_export = energy_out;
|
||||
} else if (register_data.type == POWER_W_TOTAL) {
|
||||
types::units::Power power = this->pm_last_values.power_W.value();
|
||||
power.total = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.power_W = power;
|
||||
} else if (register_data.type == POWER_W_L1) {
|
||||
types::units::Power power = this->pm_last_values.power_W.value();
|
||||
power.L1 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.power_W = power;
|
||||
} else if (register_data.type == POWER_W_L2) {
|
||||
types::units::Power power = this->pm_last_values.power_W.value();
|
||||
power.L2 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.power_W = power;
|
||||
} else if (register_data.type == POWER_W_L3) {
|
||||
types::units::Power power = this->pm_last_values.power_W.value();
|
||||
power.L3 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.power_W = power;
|
||||
} else if (register_data.type == VOLTAGE_V_DC) {
|
||||
types::units::Voltage volt = this->pm_last_values.voltage_V.value();
|
||||
volt.DC = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.voltage_V = volt;
|
||||
} else if (register_data.type == VOLTAGE_V_L1) {
|
||||
types::units::Voltage volt = this->pm_last_values.voltage_V.value();
|
||||
volt.L1 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.voltage_V = volt;
|
||||
} else if (register_data.type == VOLTAGE_V_L2) {
|
||||
types::units::Voltage volt = this->pm_last_values.voltage_V.value();
|
||||
volt.L2 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.voltage_V = volt;
|
||||
} else if (register_data.type == VOLTAGE_V_L3) {
|
||||
types::units::Voltage volt = this->pm_last_values.voltage_V.value();
|
||||
volt.L3 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.voltage_V = volt;
|
||||
} else if (register_data.type == REACTIVE_POWER_VAR_TOTAL) {
|
||||
types::units::ReactivePower var = this->pm_last_values.VAR.value();
|
||||
var.total = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.VAR = var;
|
||||
} else if (register_data.type == REACTIVE_POWER_VAR_L1) {
|
||||
types::units::ReactivePower var = this->pm_last_values.VAR.value();
|
||||
var.L1 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.VAR = var;
|
||||
} else if (register_data.type == REACTIVE_POWER_VAR_L2) {
|
||||
types::units::ReactivePower var = this->pm_last_values.VAR.value();
|
||||
var.L2 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.VAR = var;
|
||||
} else if (register_data.type == REACTIVE_POWER_VAR_L3) {
|
||||
types::units::ReactivePower var = this->pm_last_values.VAR.value();
|
||||
var.L3 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.VAR = var;
|
||||
} else if (register_data.type == CURRENT_A_DC) {
|
||||
types::units::Current amp = this->pm_last_values.current_A.value();
|
||||
amp.DC = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.current_A = amp;
|
||||
} else if (register_data.type == CURRENT_A_L1) {
|
||||
types::units::Current amp = this->pm_last_values.current_A.value();
|
||||
amp.L1 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.current_A = amp;
|
||||
} else if (register_data.type == CURRENT_A_L2) {
|
||||
types::units::Current amp = this->pm_last_values.current_A.value();
|
||||
amp.L2 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.current_A = amp;
|
||||
} else if (register_data.type == CURRENT_A_L3) {
|
||||
types::units::Current amp = this->pm_last_values.current_A.value();
|
||||
amp.L3 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.current_A = amp;
|
||||
} else if (register_data.type == FREQUENCY_HZ_L1) {
|
||||
types::units::Frequency freq = this->pm_last_values.frequency_Hz.value();
|
||||
freq.L1 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.frequency_Hz = freq;
|
||||
} else if (register_data.type == FREQUENCY_HZ_L2) {
|
||||
types::units::Frequency freq = this->pm_last_values.frequency_Hz.value();
|
||||
freq.L2 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.frequency_Hz = freq;
|
||||
} else if (register_data.type == FREQUENCY_HZ_L3) {
|
||||
types::units::Frequency freq = this->pm_last_values.frequency_Hz.value();
|
||||
freq.L3 = this->merge_register_values_into_element(register_data, exponent, register_message);
|
||||
this->pm_last_values.frequency_Hz = freq;
|
||||
} else {
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
float powermeterImpl::merge_register_values_into_element(const RegisterData& reg_data, const int16_t exponent,
|
||||
const types::serial_comm_hub_requests::Result& reg_message) {
|
||||
uint32_t value{0};
|
||||
const auto& reg_value = reg_message.value.value();
|
||||
if (reg_data.num_registers == 1 && reg_value.size() == 1) {
|
||||
value = reg_value.at(0);
|
||||
} else if (reg_data.num_registers == 2 && reg_value.size() == 2) {
|
||||
value += reg_value.at(0) << 16;
|
||||
value += reg_value.at(1);
|
||||
} else {
|
||||
throw std::runtime_error("Values of more than 2 registers in size are currently not supported!");
|
||||
}
|
||||
|
||||
const auto val = *reinterpret_cast<float*>(&value);
|
||||
const auto val_scaled = float(val * reg_data.multiplier * pow(10.0, exponent));
|
||||
|
||||
return val_scaled;
|
||||
}
|
||||
|
||||
void powermeterImpl::output_error_with_content(const types::serial_comm_hub_requests::Result& response) {
|
||||
std::stringstream ss;
|
||||
|
||||
if (response.value) {
|
||||
for (size_t i = 0; i < response.value->size(); i++) {
|
||||
if (i != 0)
|
||||
ss << ", ";
|
||||
ss << "0x" << std::setfill('0') << std::setw(2) << std::hex << int(response.value.value()[i]);
|
||||
}
|
||||
}
|
||||
|
||||
EVLOG_debug << "received error response: " << status_code_enum_to_string(response.status_code) << " (" << ss.str()
|
||||
<< ")";
|
||||
}
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
@@ -0,0 +1,142 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef MAIN_POWERMETER_IMPL_HPP
|
||||
#define MAIN_POWERMETER_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
#include "../GenericPowermeter.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
#include <optional>
|
||||
#include <string>
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
struct Conf {
|
||||
std::string model;
|
||||
int powermeter_device_id;
|
||||
int modbus_base_address;
|
||||
};
|
||||
|
||||
class powermeterImpl : public powermeterImplBase {
|
||||
public:
|
||||
powermeterImpl() = delete;
|
||||
powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<GenericPowermeter>& mod, Conf& config) :
|
||||
powermeterImplBase(ev, "main"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual types::powermeter::TransactionStartResponse
|
||||
handle_start_transaction(types::powermeter::TransactionReq& value) override;
|
||||
virtual types::powermeter::TransactionStopResponse handle_stop_transaction(std::string& transaction_id) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<GenericPowermeter>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
enum PowermeterRegisters {
|
||||
// do not change order or index of these elements!
|
||||
ENERGY_WH_IMPORT_TOTAL,
|
||||
ENERGY_WH_IMPORT_L1,
|
||||
ENERGY_WH_IMPORT_L2,
|
||||
ENERGY_WH_IMPORT_L3,
|
||||
ENERGY_WH_EXPORT_TOTAL,
|
||||
ENERGY_WH_EXPORT_L1,
|
||||
ENERGY_WH_EXPORT_L2,
|
||||
ENERGY_WH_EXPORT_L3,
|
||||
POWER_W_TOTAL,
|
||||
POWER_W_L1,
|
||||
POWER_W_L2,
|
||||
POWER_W_L3,
|
||||
VOLTAGE_V_DC,
|
||||
VOLTAGE_V_L1,
|
||||
VOLTAGE_V_L2,
|
||||
VOLTAGE_V_L3,
|
||||
REACTIVE_POWER_VAR_TOTAL,
|
||||
REACTIVE_POWER_VAR_L1,
|
||||
REACTIVE_POWER_VAR_L2,
|
||||
REACTIVE_POWER_VAR_L3,
|
||||
CURRENT_A_DC,
|
||||
CURRENT_A_L1,
|
||||
CURRENT_A_L2,
|
||||
CURRENT_A_L3,
|
||||
FREQUENCY_HZ_L1,
|
||||
FREQUENCY_HZ_L2,
|
||||
FREQUENCY_HZ_L3,
|
||||
NUM_PM_REGISTERS
|
||||
};
|
||||
|
||||
enum ModbusFunctionType {
|
||||
READ_HOLDING_REGISTER,
|
||||
READ_INPUT_REGISTER,
|
||||
REGISTER_TYPE_UNDEFINED
|
||||
};
|
||||
|
||||
struct RegisterData {
|
||||
float multiplier;
|
||||
PowermeterRegisters type;
|
||||
uint16_t start_register;
|
||||
ModbusFunctionType start_register_function;
|
||||
uint16_t exponent_register;
|
||||
ModbusFunctionType exponent_register_function;
|
||||
uint16_t num_registers;
|
||||
};
|
||||
|
||||
std::vector<RegisterData> pm_configuration;
|
||||
|
||||
types::powermeter::Powermeter pm_last_values;
|
||||
|
||||
std::thread output_thread;
|
||||
|
||||
/// @brief Remember whether we already logged the meter's unavailability.
|
||||
bool meter_is_unavailable{false};
|
||||
|
||||
void init_default_values();
|
||||
void init_register_assignments(const json& loaded_registers);
|
||||
bool assign_register_data(const json& registers, const PowermeterRegisters register_type,
|
||||
const std::string& register_selector);
|
||||
void assign_register_sublevel_data(const json& registers, const PowermeterRegisters& register_type,
|
||||
const std::string& register_selector, const std::string& sublevel_selector,
|
||||
const uint8_t offset);
|
||||
powermeterImpl::ModbusFunctionType select_modbus_function(const uint8_t function_code);
|
||||
void read_powermeter_values();
|
||||
bool read_register(const RegisterData& register_config);
|
||||
bool process_response(
|
||||
const RegisterData& register_data, const types::serial_comm_hub_requests::Result& register_message,
|
||||
std::optional<std::reference_wrapper<const types::serial_comm_hub_requests::Result>> exponent_message);
|
||||
float merge_register_values_into_element(const RegisterData& reg_data, const int16_t exponent,
|
||||
const types::serial_comm_hub_requests::Result& reg_message);
|
||||
void process_current_rate_message(const types::serial_comm_hub_requests::Result message);
|
||||
void output_error_with_content(const types::serial_comm_hub_requests::Result& response);
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
|
||||
#endif // MAIN_POWERMETER_IMPL_HPP
|
||||
@@ -0,0 +1,29 @@
|
||||
description: Powermeter driver for various powermeter hardware
|
||||
provides:
|
||||
main:
|
||||
description: Implementation of the driver functionality
|
||||
interface: powermeter
|
||||
config:
|
||||
model:
|
||||
description: Selector for the powermeter configuration file to be used
|
||||
type: string
|
||||
default: test_dummy
|
||||
powermeter_device_id:
|
||||
description: The powermeter's address on the serial bus
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 247
|
||||
default: 1
|
||||
modbus_base_address:
|
||||
description: The base address for register access
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 65535
|
||||
default: 30001
|
||||
requires:
|
||||
serial_comm_hub:
|
||||
interface: serial_communication_hub
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Lars Dieckmann
|
||||
@@ -0,0 +1,206 @@
|
||||
# (<start_register> of length <num_registers>) * <multiplier> * 10^(<exponent_register>)
|
||||
#
|
||||
# if <start_register> is "0", then this value does not exist in the powermeter
|
||||
#
|
||||
# use <multiplier> to manually scale (e.g. set to 0.001 if device returns "kWh", but the parameter is "Wh") and <exponent_register> to scale by device value
|
||||
#
|
||||
# if <exponent_register> is "0", then no exponent register exists and multiplier needs to be set accordingly
|
||||
#
|
||||
# if measuring AC, the first level of registers is always "total/sum" of a certain value and the L1/2/3 registers are for the distinct phases
|
||||
# if measuring DC, only use the first level of registers
|
||||
energy_Wh_import:
|
||||
start_register: 30073
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 0.001
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
energy_Wh_export:
|
||||
start_register: 30075
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 0.001
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
power_W:
|
||||
start_register: 30013
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
voltage_V:
|
||||
start_register: 30001
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
reactive_power_VAR:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 0.001
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
current_A:
|
||||
start_register: 30007
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
frequency_Hz:
|
||||
start_register: 30071
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
@@ -0,0 +1,206 @@
|
||||
# (<start_register> of length <num_registers>) * <multiplier> * 10^(<exponent_register>)
|
||||
#
|
||||
# if <start_register> is "0", then this value does not exist in the powermeter
|
||||
#
|
||||
# use <multiplier> to manually scale (e.g. set to 0.001 if device returns "kWh", but the parameter is "Wh") and <exponent_register> to scale by device value
|
||||
#
|
||||
# if <exponent_register> is "0", then no exponent register exists and multiplier needs to be set accordingly
|
||||
#
|
||||
# if measuring AC, the first level of registers is always "total/sum" of a certain value and the L1/2/3 registers are for the distinct phases
|
||||
# if measuring DC, only use the first level of registers
|
||||
energy_Wh_import:
|
||||
start_register: 30073
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 30347
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 30349
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 30351
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
energy_Wh_export:
|
||||
start_register: 30075
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 30353
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 30355
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 30357
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
power_W:
|
||||
start_register: 30053
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 30013
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 30015
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 30017
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
voltage_V:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 30001
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 30003
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 30005
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
reactive_power_VAR:
|
||||
start_register: 30061
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 30025
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 30027
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 30029
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
current_A:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 30007
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 30009
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 30011
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
frequency_Hz:
|
||||
start_register: 30071
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
@@ -0,0 +1,206 @@
|
||||
# (<start_register> of length <num_registers>) * <multiplier> * 10^(<exponent_register>)
|
||||
#
|
||||
# if <start_register> is "0", then this value does not exist in the powermeter
|
||||
#
|
||||
# use <multiplier> to manually scale (e.g. set to 0.001 if device returns "kWh", but the parameter is "Wh") and <exponent_register> to scale by device value
|
||||
#
|
||||
# if <exponent_register> is "0", then no exponent register exists and multiplier needs to be set accordingly
|
||||
#
|
||||
# if measuring AC, the first level of registers is always "total/sum" of a certain value and the L1/2/3 registers are for the distinct phases
|
||||
# if measuring DC, only use the first level of registers
|
||||
energy_Wh_import:
|
||||
start_register: 30073
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
energy_Wh_export:
|
||||
start_register: 30075
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
power_W:
|
||||
start_register: 30053
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 30013
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 30015
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 30017
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
voltage_V:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 30001
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 30003
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 30005
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
reactive_power_VAR:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 0.001
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
current_A:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 30007
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 30009
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 30011
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
frequency_Hz:
|
||||
start_register: 30071
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
@@ -0,0 +1,31 @@
|
||||
# (<start_register> of length <num_registers>) * <multiplier> * 10^(<exponent_register>)
|
||||
#
|
||||
# if <start_register> is "0", then this value does not exist in the powermeter
|
||||
#
|
||||
# use <multiplier> to manually scale (e.g. set to 0.001 if device returns "kWh", but the parameter is "Wh") and <exponent_register> to scale by device value
|
||||
#
|
||||
# if <exponent_register> is "0", then no exponent register exists and multiplier needs to be set accordingly
|
||||
#
|
||||
# if measuring AC, the first level of registers is always "total/sum" of a certain value and the L1/2/3 registers are for the distinct phases
|
||||
# if measuring DC, only use the first level of registers
|
||||
energy_Wh_import:
|
||||
start_register: 30073
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
energy_Wh_export:
|
||||
start_register: 30075
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
power_W:
|
||||
start_register: 30053
|
||||
function_code_start_reg: 4
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
@@ -0,0 +1,220 @@
|
||||
# (<start_register> of length <num_registers>) * <multiplier> * 10^(<exponent_register>)
|
||||
#
|
||||
# if <start_register> is "0", then this value does not exist in the powermeter
|
||||
#
|
||||
# use <multiplier> to manually scale (e.g. set to 0.001 if device returns "kWh", but the parameter is "Wh") and <exponent_register> to scale by device value
|
||||
#
|
||||
# if <exponent_register> is "0", then no exponent register exists and multiplier needs to be set accordingly
|
||||
#
|
||||
# if measuring AC, the first level of registers is always "total/sum" of a certain value and the L1/2/3 registers are for the distinct phases
|
||||
# if measuring DC, only use the first level of registers
|
||||
#
|
||||
# Usage Example:
|
||||
# powermeter:
|
||||
# module: GenericPowermeter
|
||||
# config_implementation:
|
||||
# main:
|
||||
# model: Klefr_693x-694x
|
||||
# powermeter_device_id: 1
|
||||
# modbus_base_address: 0
|
||||
# connections:
|
||||
# serial_comm_hub:
|
||||
# - module_id: serialcommhub_x7
|
||||
# implementation_id: main
|
||||
#
|
||||
energy_Wh_import:
|
||||
start_register: 24588
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 24594
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 24596
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 24598
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
energy_Wh_export:
|
||||
start_register: 24600
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 24606
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 24608
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 24610
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
power_W:
|
||||
start_register: 20498
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 20500
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 20502
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 20504
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
voltage_V:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 20482
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 20484
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 20486
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
reactive_power_VAR:
|
||||
start_register: 20506
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 20508
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 20510
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 20512
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1000.0
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
current_A:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 20492
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 20494
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 20496
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
frequency_Hz:
|
||||
start_register: 20488
|
||||
function_code_start_reg: 3
|
||||
num_registers: 2
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 4
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 4
|
||||
@@ -0,0 +1,206 @@
|
||||
# (<start_register> of length <num_registers>) * <multiplier> * 10^(<exponent_register>)
|
||||
#
|
||||
# if <start_register> is "0", then this value does not exist in the powermeter
|
||||
#
|
||||
# use <multiplier> to manually scale (e.g. set to 0.001 if device returns "kWh", but the parameter is "Wh") and <exponent_register> to scale by device value
|
||||
#
|
||||
# if <exponent_register> is "0", then no exponent register exists and multiplier needs to be set accordingly
|
||||
#
|
||||
# if measuring AC, the first level of registers is always "total/sum" of a certain value and the L1/2/3 registers are for the distinct phases
|
||||
# if measuring DC, only use the first level of registers
|
||||
energy_Wh_import:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 0.001
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
energy_Wh_export:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 0.001
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
power_W:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
voltage_V:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
reactive_power_VAR:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 0.001
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
current_A:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
frequency_Hz:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
@@ -0,0 +1,206 @@
|
||||
# (<start_register> of length <num_registers>) * <multiplier> * 10^(<exponent_register>)
|
||||
#
|
||||
# if <start_register> is "0", then this value does not exist in the powermeter
|
||||
#
|
||||
# use <multiplier> to manually scale (e.g. set to 0.001 if device returns "kWh", but the parameter is "Wh") and <exponent_register> to scale by device value
|
||||
#
|
||||
# if <exponent_register> is "0", then no exponent register exists and multiplier needs to be set accordingly
|
||||
#
|
||||
# if measuring AC, the first level of registers is always "total/sum" of a certain value and the L1/2/3 registers are for the distinct phases
|
||||
# if measuring DC, only use the first level of registers
|
||||
energy_Wh_import:
|
||||
start_register: 40002
|
||||
function_code_start_reg: 3
|
||||
num_registers: 1
|
||||
multiplier: 0.001
|
||||
exponent_register: 40003
|
||||
function_code_exp_reg: 4
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
energy_Wh_export:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 0.001
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
power_W:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
voltage_V:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
reactive_power_VAR:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 0.001
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
current_A:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
frequency_Hz:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L1:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L2:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
L3:
|
||||
start_register: 0
|
||||
function_code_start_reg: 3
|
||||
num_registers: 0
|
||||
multiplier: 1
|
||||
exponent_register: 0
|
||||
function_code_exp_reg: 3
|
||||
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# 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
|
||||
CURL::libcurl
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
main/isabellenhuetteIemDcrController.cpp
|
||||
main/httpClient.cpp
|
||||
)
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"main/powermeterImpl.cpp"
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include "IsabellenhuetteIemDcr.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
void IsabellenhuetteIemDcr::init() {
|
||||
invoke_init(*p_main);
|
||||
}
|
||||
|
||||
void IsabellenhuetteIemDcr::ready() {
|
||||
invoke_ready(*p_main);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,71 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef ISABELLENHUETTE_IEM_DCR_HPP
|
||||
#define ISABELLENHUETTE_IEM_DCR_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
// insert your custom include headers here
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {
|
||||
std::string ip_address;
|
||||
int port_http;
|
||||
std::string timezone;
|
||||
bool timezone_handle_DST;
|
||||
int datetime_resync_interval;
|
||||
int resilience_initial_connection_retry_delay;
|
||||
int resilience_transaction_request_retries;
|
||||
int resilience_transaction_request_retry_delay;
|
||||
std::string CT;
|
||||
std::string CI;
|
||||
std::string TT_initial;
|
||||
bool US;
|
||||
};
|
||||
|
||||
class IsabellenhuetteIemDcr : public Everest::ModuleBase {
|
||||
public:
|
||||
IsabellenhuetteIemDcr() = delete;
|
||||
IsabellenhuetteIemDcr(const ModuleInfo& info, std::unique_ptr<powermeterImplBase> p_main, Conf& config) :
|
||||
ModuleBase(info), p_main(std::move(p_main)), config(config){};
|
||||
|
||||
const std::unique_ptr<powermeterImplBase> p_main;
|
||||
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
|
||||
// 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 // ISABELLENHUETTE_IEM_DCR_HPP
|
||||
@@ -0,0 +1,55 @@
|
||||
.. _everest_modules_handwritten_IsabellenhuetteIemDcr:
|
||||
|
||||
.. *********************
|
||||
.. IsabellenhuetteIemDcr
|
||||
.. *********************
|
||||
|
||||
Module implements Isabellenhuette IEM-DCR power meter driver, connecting via HTTP/REST.
|
||||
|
||||
Implementation details
|
||||
======================
|
||||
|
||||
This section offers some additional information on driver implementation. The underlying HTTP communication functionality
|
||||
is mainly duplicated from other open source powermeter modules of EVerest to support a standarization of this interface
|
||||
later on.
|
||||
|
||||
Initialization
|
||||
--------------
|
||||
It begins with checking some plausibility measures on the handed configuration. Its default values are given in manifest.yaml.
|
||||
Please make sure to explicitly specify values that deviate from default configuration before starting the driver. If there is no
|
||||
conspicuousness in configuration, HTTP communication is verified with GET requests on /gw node. In case of no success, several
|
||||
retries are performed (as specified in config). On success POST /gw is issued for transfering CI, CT and datetime to IEM-DCR.
|
||||
Please note, that issuing POST /gw is only possible once after IEM-DCR power-up. So CI and CT are frozen until next power-cycle
|
||||
and datetime will be automatically updated using another node (POST /datetime) in configurable intervals. Therefore a warning
|
||||
will appear on EVerest console if CI and CT are already written and could not be updated. After this procedure the initial tariff
|
||||
text is transferred as configured. This will show up on display before a charging transaction.
|
||||
|
||||
Live values
|
||||
-----------
|
||||
Each second the MQTT variable Powermeter is updated to current values of /metervalue node. Also the public key is made available
|
||||
via MQTT.
|
||||
|
||||
Start transaction
|
||||
-----------------
|
||||
Starting a transaction will terminate any other running transaction (if there is one). The status type TransactionRequestStatus::
|
||||
NOT_SUPPORTED is returned, if given evse_id does not match CI (which was already transfered in initialization phase) and if IEM-DCR
|
||||
is in error state. Please refer to retrurned TransactionStartResponse.error for distinguishing between them. Starting a charging
|
||||
transaction will engage POST /user and POST /receipt. Please note that IEM-DCR automatically handles signed data tuple pagination. So
|
||||
the only place for transaction id defined by the charging station is the OCMF ID attribute. It will be filled from this driver with
|
||||
TransactionReq.identification_data. If this optional attribute is not given or empty, TransactionReq.transaction_id will be used
|
||||
instead. Please note that a transaction cannot be started while the sensor unit detects a current above activation treshold.
|
||||
Please refer to operation manual for details.
|
||||
|
||||
Stop transaction
|
||||
----------------
|
||||
If a transaction is in progress, it will be stopped and its signed data tuple returned. If no transaction is running, the last signed
|
||||
data tuple will be returned. Therefore input parameter transaction_id of this routine has no impact on its operation. Please note that
|
||||
TransactionRequestStatus::UNEXPECTED_ERROR may be returned, if no transaction is in progress and there has also been no transaction
|
||||
before. Please also note that a transaction cannot be stopped while the sensor unit detects a current above activation treshold.
|
||||
Please refer to operation manual for details.
|
||||
|
||||
References
|
||||
==========
|
||||
`IEM-DCR-125 <https://www.isabellenhuette.com/de/loesungen/produkte/iem-dcr-125>`_
|
||||
`IEM-DCR-1000 <https://www.isabellenhuette.com/de/loesungen/produkte/iem-dcr-1000>`_
|
||||
`IEM-DCR-1500 <https://www.isabellenhuette.com/de/loesungen/produkte/iem-dcr-1500>`_
|
||||
@@ -0,0 +1,154 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "httpClient.hpp"
|
||||
#include <fmt/core.h>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace module::main {
|
||||
const char* CONTENT_TYPE_HEADER = "Content-Type: application/json";
|
||||
|
||||
struct PayloadInTransit {
|
||||
const std::string& data;
|
||||
size_t position;
|
||||
};
|
||||
|
||||
// Callback for receiving data, saves it into a string
|
||||
static size_t receive_data(char* ptr, size_t size, size_t nmemb, std::string* received_data) {
|
||||
received_data->append(ptr, size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
// Callback for sending data, fetches it from a string
|
||||
static size_t send_data(char* buffer, size_t size, size_t nitems, struct PayloadInTransit* payload) {
|
||||
if (payload->position >= payload->data.length()) {
|
||||
// Returning 0 signals to libcurl that we have no more data to send
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Send up to size*nitems bytes of data
|
||||
size_t payload_remaining_bytes = payload->data.length() - payload->position;
|
||||
size_t num_bytes_to_send = std::min(size * nitems, payload_remaining_bytes);
|
||||
std::memcpy(buffer, payload->data.c_str() + payload->position, num_bytes_to_send);
|
||||
payload->position += num_bytes_to_send;
|
||||
return num_bytes_to_send;
|
||||
}
|
||||
|
||||
static HttpClientError client_error(const std::string& host, unsigned int port, const char* method,
|
||||
const std::string& path, const std::string& message) {
|
||||
return HttpClientError(fmt::format("HTTP client error on {} {}:{}{} : {} ", method, host, port, path, message));
|
||||
}
|
||||
|
||||
static void setup_connection(CURL* connection, struct PayloadInTransit& request_payload, std::string& response_body,
|
||||
curl_slist*& headers) {
|
||||
// Override the Content-Type header
|
||||
headers = curl_slist_append(nullptr, CONTENT_TYPE_HEADER);
|
||||
if (curl_easy_setopt(connection, CURLOPT_HTTPHEADER, headers) != CURLE_OK) {
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
|
||||
// Set up callbacks for reading and writing
|
||||
curl_easy_setopt(connection, CURLOPT_WRITEFUNCTION, receive_data);
|
||||
curl_easy_setopt(connection, CURLOPT_WRITEDATA, &response_body);
|
||||
curl_easy_setopt(connection, CURLOPT_READFUNCTION, send_data);
|
||||
curl_easy_setopt(connection, CURLOPT_READDATA, &request_payload);
|
||||
|
||||
// Misc. settings come here
|
||||
curl_easy_setopt(connection, CURLOPT_FORBID_REUSE, 1);
|
||||
curl_easy_setopt(connection, CURLOPT_CONNECTTIMEOUT, 2);
|
||||
if (curl_easy_setopt(connection, CURLOPT_FOLLOWLOCATION, 0) != CURLE_OK) {
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
}
|
||||
|
||||
// Note: method_name and path are only there for the error message
|
||||
HttpResponse HttpClient::perform_request(CURL* connection, const std::string& request_body, const char* method_name,
|
||||
const std::string& path) const {
|
||||
// give curl a buffer to write its error messages to
|
||||
char curl_error_message[CURL_ERROR_SIZE] = {};
|
||||
curl_easy_setopt(connection, CURLOPT_ERRORBUFFER, curl_error_message);
|
||||
|
||||
// set up the connection options
|
||||
std::string response_body;
|
||||
struct PayloadInTransit request_payload {
|
||||
request_body, 0
|
||||
};
|
||||
struct curl_slist* headers;
|
||||
setup_connection(connection, request_payload, response_body, headers);
|
||||
|
||||
// perform the request
|
||||
CURLcode res = curl_easy_perform(connection);
|
||||
|
||||
// remember to free the headers list...
|
||||
curl_slist_free_all(headers);
|
||||
// check the result of the request and return
|
||||
if (res == CURLE_OK) {
|
||||
long response_code;
|
||||
curl_easy_getinfo(connection, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
return HttpResponse{(unsigned int)response_code, std::move(response_body)};
|
||||
} else {
|
||||
throw client_error(this->host, this->port, method_name, path, std::string(curl_error_message));
|
||||
}
|
||||
}
|
||||
|
||||
CURL* HttpClient::create_curl_handle_and_setup_url(const std::string& path) const {
|
||||
CURL* connection = curl_easy_init();
|
||||
if (!connection) {
|
||||
throw std::runtime_error("Could not create a CURL handle: curl_easy_init() returned null");
|
||||
}
|
||||
const char* protocol = "http";
|
||||
if (curl_easy_setopt(connection, CURLOPT_URL,
|
||||
fmt::format("{}://{}:{}{}", protocol, this->host, this->port, path).c_str()) != CURLE_OK) {
|
||||
throw std::runtime_error("Could not set CURLOPT_URL, likely ran out of memory");
|
||||
}
|
||||
if (curl_easy_setopt(connection, CURLOPT_PROTOCOLS_STR, protocol) != CURLE_OK) {
|
||||
throw std::runtime_error(std::string("Could not set supported protocol to ") + protocol +
|
||||
", is it enabled in libcurl?");
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::get(const std::string& path) const {
|
||||
CURL* connection = this->create_curl_handle_and_setup_url(path);
|
||||
|
||||
if (curl_easy_setopt(connection, CURLOPT_HTTPGET, 1) != CURLE_OK) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
|
||||
// perform_request() does not cleanup the connection on its own.
|
||||
// We do the cleanup here, and make sure to rethrow any exception that might've occurred.
|
||||
try {
|
||||
HttpResponse response = perform_request(connection, "", "GET", path);
|
||||
curl_easy_cleanup(connection);
|
||||
return response;
|
||||
} catch (std::exception& e) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::post(const std::string& path, const std::string& body) const {
|
||||
CURL* connection = this->create_curl_handle_and_setup_url(path);
|
||||
|
||||
if (curl_easy_setopt(connection, CURLOPT_POST, 1) != CURLE_OK) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
|
||||
// perform_request() does not cleanup the connection on its own.
|
||||
// We do the cleanup here, and make sure to rethrow any exception that might've occurred.
|
||||
try {
|
||||
HttpResponse response = perform_request(connection, body, "POST", path);
|
||||
curl_easy_cleanup(connection);
|
||||
return response;
|
||||
} catch (std::exception& e) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
} // namespace module::main
|
||||
@@ -0,0 +1,50 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_HTTPCLIENT_H
|
||||
#define EVEREST_CORE_MODULE_HTTPCLIENT_H
|
||||
|
||||
#include "fmt/format.h"
|
||||
#include "httpClientInterface.hpp"
|
||||
#include <curl/curl.h>
|
||||
#include <everest/logging.hpp>
|
||||
#include <regex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
class HttpClient : public HttpClientInterface {
|
||||
|
||||
public:
|
||||
HttpClient() = delete;
|
||||
|
||||
HttpClient(const std::string& host_arg, int port_arg) {
|
||||
// initialize libcurl - this is safe to do multiple times, if there are multiple HttpClients
|
||||
// Note: This is only thread-safe after libcurl 7.84.0, but we use 8.4.0, so it should be fine
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
// These are saved in the client to avoid making the controller pass them at every call
|
||||
host = host_arg;
|
||||
port = port_arg;
|
||||
}
|
||||
~HttpClient() override {
|
||||
// release the libcurl resources - this must be done once for every call to curl_global_init().
|
||||
// Note: This is only thread-safe after libcurl 7.84.0, but we use 8.4.0, so it should be fine
|
||||
curl_global_cleanup();
|
||||
}
|
||||
|
||||
[[nodiscard]] HttpResponse get(const std::string& path) const override;
|
||||
[[nodiscard]] HttpResponse post(const std::string& path, const std::string& body) const override;
|
||||
|
||||
private:
|
||||
std::string host;
|
||||
int port;
|
||||
|
||||
[[nodiscard]] CURL* create_curl_handle_and_setup_url(const std::string& path) const;
|
||||
HttpResponse perform_request(CURL* connection, const std::string& request_body, const char* method_name,
|
||||
const std::string& path) const;
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_HTTPCLIENT_H
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_HTTP_CLIENT_INTERFACE_H
|
||||
#define EVEREST_CORE_MODULE_HTTP_CLIENT_INTERFACE_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
class HttpClientError : public std::exception {
|
||||
public:
|
||||
[[nodiscard]] const char* what() const noexcept override {
|
||||
return this->reason.c_str();
|
||||
}
|
||||
explicit HttpClientError(std::string msg) {
|
||||
this->reason = std::move(msg);
|
||||
}
|
||||
explicit HttpClientError(const char* msg) {
|
||||
this->reason = std::string(msg);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct HttpResponse {
|
||||
unsigned int status_code;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
struct HttpClientInterface {
|
||||
|
||||
virtual ~HttpClientInterface() = default;
|
||||
|
||||
[[nodiscard]] virtual HttpResponse get(const std::string& path) const = 0;
|
||||
[[nodiscard]] virtual HttpResponse post(const std::string& path, const std::string& body) const = 0;
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_HTTP_CLIENT_INTERFACE_H
|
||||
@@ -0,0 +1,480 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "isabellenhuetteIemDcrController.hpp"
|
||||
#include <stdexcept>
|
||||
namespace module::main {
|
||||
|
||||
IsaIemDcrController::IsaIemDcrController(std::unique_ptr<HttpClientInterface> http_client,
|
||||
const SnapshotConfig& snap_config) :
|
||||
http_client(std::move(http_client)), snapshot_config(snap_config) {
|
||||
// Member Initializer List is used
|
||||
|
||||
// Further initialization
|
||||
zone_time_offset = helper_convert_timezone(snapshot_config.timezone);
|
||||
last_datetime_sync.store(std::chrono::steady_clock::now() - std::chrono::hours(48));
|
||||
}
|
||||
|
||||
bool IsaIemDcrController::init() {
|
||||
try {
|
||||
EVLOG_info << "Isabellenhuette IEM-DCR: Connecting to module...";
|
||||
// Check connection with polling REST node gw
|
||||
this->get_gw();
|
||||
// Send gw information
|
||||
try {
|
||||
this->post_gw();
|
||||
} catch (IsaIemDcrController::UnexpectedIemDcrResponseCode& error) {
|
||||
EVLOG_warning << "Node /gw seems to be already set. If those values should be updated, "
|
||||
"please restart IEM-DCR and then also this system.";
|
||||
}
|
||||
// Send initial tariff information
|
||||
try {
|
||||
if (snapshot_config.TT_initial.length() > 0) {
|
||||
this->post_tariff(snapshot_config.TT_initial);
|
||||
}
|
||||
} catch (IsaIemDcrController::UnexpectedIemDcrResponseCode& error) {
|
||||
EVLOG_warning << "Incorrect config: Value TT_initial could not be set. Please check its value.";
|
||||
}
|
||||
EVLOG_info << "Isabellenhuette IEM-DCR: Connected.";
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
json IsaIemDcrController::get_gw() {
|
||||
const std::string endpoint = "/counter/v1/ocmf/gw";
|
||||
auto response = this->http_client->get(endpoint);
|
||||
if (response.status_code == 200) {
|
||||
try {
|
||||
json data = json::parse(response.body);
|
||||
return data;
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedIemDcrResponseBody(
|
||||
endpoint, fmt::format("Json error {} for body {}", json_error.what(), response.body));
|
||||
}
|
||||
} else {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsaIemDcrController::check_gw_is_empty() {
|
||||
json gw_result = this->get_gw();
|
||||
return gw_result.at("CT").empty();
|
||||
}
|
||||
|
||||
void IsaIemDcrController::post_gw() {
|
||||
const std::string endpoint = "/counter/v1/ocmf/gw";
|
||||
const std::string payload = nlohmann::ordered_json{{"CT", snapshot_config.CT},
|
||||
{"CI", snapshot_config.CI},
|
||||
{"TM", helper_get_current_datetime()}}
|
||||
.dump();
|
||||
auto response = this->http_client->post(endpoint, payload);
|
||||
if (response.status_code == 200) {
|
||||
last_datetime_sync.store(std::chrono::steady_clock::now());
|
||||
} else {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
void IsaIemDcrController::post_tariff(std::string tariff_info) {
|
||||
const std::string endpoint = "/counter/v1/ocmf/tariff";
|
||||
const std::string payload = nlohmann::ordered_json{{"TT", tariff_info}}.dump();
|
||||
auto response = this->http_client->post(endpoint, payload);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
std::tuple<types::powermeter::Powermeter, std::string, bool> IsaIemDcrController::get_metervalue() {
|
||||
const std::string endpoint = "/counter/v1/ocmf/metervalue";
|
||||
auto response = this->http_client->get(endpoint);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
try {
|
||||
json data = json::parse(response.body);
|
||||
|
||||
types::powermeter::Powermeter powermeter;
|
||||
bool tmp_transaction_active = data.at("XT");
|
||||
powermeter.timestamp = data.at("TM");
|
||||
// Remove format specifier at the end (if available)
|
||||
if (powermeter.timestamp.length() > 28) {
|
||||
powermeter.timestamp = powermeter.timestamp.substr(0, 28);
|
||||
}
|
||||
powermeter.meter_id = data.at("MS");
|
||||
auto current = types::units::Current{};
|
||||
current.DC = data.at("I");
|
||||
powermeter.current_A.emplace(current);
|
||||
auto voltageU2 = types::units::Voltage{};
|
||||
voltageU2.DC = data.at("U2");
|
||||
powermeter.voltage_V.emplace(voltageU2);
|
||||
powermeter.power_W.emplace(types::units::Power{data.at("P").get<float>()});
|
||||
// Remove quotes before casting to float
|
||||
auto energy_kWh_import = helper_remove_first_and_last_char(data.at("RD").at(2).at("WV"));
|
||||
powermeter.energy_Wh_import = {std::stof(energy_kWh_import) * 1000.0f};
|
||||
// Remove quotes before casting to float
|
||||
auto energy_kWh_export = helper_remove_first_and_last_char(data.at("RD").at(3).at("WV"));
|
||||
powermeter.energy_Wh_export = {std::stof(energy_kWh_export) * 1000.0f};
|
||||
// Get status
|
||||
std::string status = data.at("XC");
|
||||
|
||||
return std::make_tuple(powermeter, status, tmp_transaction_active);
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedIemDcrResponseBody(endpoint,
|
||||
fmt::format("Json error {} for body {}", json_error.what(), response.body));
|
||||
}
|
||||
}
|
||||
|
||||
std::string IsaIemDcrController::get_publickey(bool allow_cached_value) {
|
||||
if (allow_cached_value && cached_public_key.length() > 0) {
|
||||
return cached_public_key;
|
||||
} else {
|
||||
const std::string endpoint = "/counter/v1/ocmf/publickey";
|
||||
auto response = this->http_client->get(endpoint);
|
||||
if (response.status_code != 200) {
|
||||
EVLOG_warning << "Response to retrieval of public key is not 200." << std::endl;
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
json data = json::parse(response.body);
|
||||
cached_public_key = data.at("PK");
|
||||
return cached_public_key;
|
||||
} catch (json::exception& json_error) {
|
||||
EVLOG_warning << "JSON error during parsing of public key" << std::endl;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string IsaIemDcrController::get_datetime() {
|
||||
const std::string endpoint = "/counter/v1/ocmf/datetime";
|
||||
auto response = this->http_client->get(endpoint);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
try {
|
||||
json data = json::parse(response.body);
|
||||
return data.at("TM");
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedIemDcrResponseBody(endpoint,
|
||||
fmt::format("Json error {} for body {}", json_error.what(), response.body));
|
||||
}
|
||||
}
|
||||
|
||||
void IsaIemDcrController::post_datetime() {
|
||||
const std::string endpoint = "/counter/v1/ocmf/datetime";
|
||||
const std::string payload = nlohmann::ordered_json{{"TM", helper_get_current_datetime()}}.dump();
|
||||
auto response = this->http_client->post(endpoint, payload);
|
||||
if (response.status_code == 200) {
|
||||
last_datetime_sync.store(std::chrono::steady_clock::now());
|
||||
} else {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
void IsaIemDcrController::refresh_datetime_if_required() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::hours>(now - last_datetime_sync.load());
|
||||
if (elapsed.count() >= snapshot_config.datetime_resync_interval) {
|
||||
try {
|
||||
this->post_datetime();
|
||||
EVLOG_info << "DateTime resynchronized.";
|
||||
} catch (...) {
|
||||
// On error: just retry on next call
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IsaIemDcrController::post_user(const types::powermeter::OCMFUserIdentificationStatus IS,
|
||||
const std::optional<types::powermeter::OCMFIdentificationLevel> IL,
|
||||
const std::vector<types::powermeter::OCMFIdentificationFlags>& IF,
|
||||
const types::powermeter::OCMFIdentificationType& IT,
|
||||
const std::optional<std::__cxx11::basic_string<char>>& ID,
|
||||
const std::optional<std::__cxx11::basic_string<char>>& TT) {
|
||||
|
||||
const std::string endpoint = "/counter/v1/ocmf/user";
|
||||
bool boolIS = helper_get_bool_from_OCMFUserIdentificationStatus(IS);
|
||||
std::string strIL = helper_get_string_from_OCMFIdentificationLevel(IL);
|
||||
std::string strIT = helper_get_string_from_OCMFIdentificationType(IT);
|
||||
std::string strID = static_cast<std::string>(ID.value_or(""));
|
||||
std::string strTT = static_cast<std::string>(TT.value_or(""));
|
||||
std::string payload = "";
|
||||
std::vector<std::string> vectIF;
|
||||
|
||||
// Fill vectIF
|
||||
for (const types::powermeter::OCMFIdentificationFlags& id_flag : IF) {
|
||||
vectIF.push_back(helper_get_string_from_OCMFIdentificationFlags(id_flag));
|
||||
}
|
||||
|
||||
if (strTT.length() > 0) {
|
||||
payload = nlohmann::ordered_json{{"IS", boolIS}, {"IL", strIL}, {"IF", vectIF},
|
||||
{"IT", strIT}, {"ID", strID}, {"US", snapshot_config.US},
|
||||
{"TT", strTT}}
|
||||
.dump();
|
||||
} else {
|
||||
payload = nlohmann::ordered_json{{"IS", boolIS}, {"IL", strIL}, {"IF", vectIF},
|
||||
{"IT", strIT}, {"ID", strID}, {"US", snapshot_config.US}}
|
||||
.dump();
|
||||
}
|
||||
auto response = this->http_client->post(endpoint, payload);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
types::units_signed::SignedMeterValue IsaIemDcrController::get_receipt() {
|
||||
const std::string endpoint = "/counter/v1/ocmf/receipt";
|
||||
return helper_get_signed_datatuple(endpoint);
|
||||
}
|
||||
|
||||
types::units_signed::SignedMeterValue IsaIemDcrController::get_transaction() {
|
||||
try {
|
||||
const std::string endpoint = "/counter/v1/ocmf/transaction";
|
||||
return helper_get_signed_datatuple(endpoint);
|
||||
} catch (UnexpectedIemDcrResponseCode& resp_error) {
|
||||
// Retry with newer api endpoint
|
||||
const std::string endpoint_v2 = "/counter/v2/ocmf/transaction";
|
||||
return helper_get_signed_datatuple(endpoint_v2);
|
||||
}
|
||||
}
|
||||
|
||||
void IsaIemDcrController::post_receipt(const std::string& TX) {
|
||||
const std::string endpoint = "/counter/v1/ocmf/receipt";
|
||||
const std::string payload = nlohmann::ordered_json{{"TX", TX}}.dump();
|
||||
auto response = this->http_client->post(endpoint, payload);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsaIemDcrController::helper_get_bool_from_OCMFUserIdentificationStatus(
|
||||
types::powermeter::OCMFUserIdentificationStatus IS) {
|
||||
return (IS == types::powermeter::OCMFUserIdentificationStatus::ASSIGNED);
|
||||
}
|
||||
|
||||
std::string IsaIemDcrController::helper_get_string_from_OCMFIdentificationLevel(
|
||||
std::optional<types::powermeter::OCMFIdentificationLevel> IL) {
|
||||
std::string result;
|
||||
types::powermeter::OCMFIdentificationLevel value_IL =
|
||||
IL.value_or(types::powermeter::OCMFIdentificationLevel::UNKNOWN);
|
||||
switch (value_IL) {
|
||||
case types::powermeter::OCMFIdentificationLevel::NONE:
|
||||
result = "NONE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::HEARSAY:
|
||||
result = "HEARSAY";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::TRUSTED:
|
||||
result = "TRUSTED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::VERIFIED:
|
||||
result = "VERIFIED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::CERTIFIED:
|
||||
result = "CERTIFIED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::SECURE:
|
||||
result = "SECURE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::MISMATCH:
|
||||
result = "MISMATCH";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::INVALID:
|
||||
result = "INVALID";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::OUTDATED:
|
||||
result = "OUTDATED";
|
||||
break;
|
||||
default:
|
||||
result = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string IsaIemDcrController::helper_get_string_from_OCMFIdentificationFlags(
|
||||
types::powermeter::OCMFIdentificationFlags id_flag) {
|
||||
std::string result;
|
||||
switch (id_flag) {
|
||||
case types::powermeter::OCMFIdentificationFlags::RFID_NONE:
|
||||
result = "RFID_NONE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::RFID_PLAIN:
|
||||
result = "RFID_PLAIN";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::RFID_RELATED:
|
||||
result = "RFID_RELATED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::RFID_PSK:
|
||||
result = "RFID_PSK";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_NONE:
|
||||
result = "OCPP_NONE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_RS:
|
||||
result = "OCPP_RS";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_AUTH:
|
||||
result = "OCPP_AUTH";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_RS_TLS:
|
||||
result = "OCPP_RS_TLS";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_AUTH_TLS:
|
||||
result = "OCPP_AUTH_TLS";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_CACHE:
|
||||
result = "OCPP_CACHE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_WHITELIST:
|
||||
result = "OCPP_WHITELIST";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_CERTIFIED:
|
||||
result = "OCPP_CERTIFIED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::ISO15118_NONE:
|
||||
result = "ISO15118_NONE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::ISO15118_PNC:
|
||||
result = "ISO15118_PNC";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::PLMN_NONE:
|
||||
result = "PLMN_NONE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::PLMN_RING:
|
||||
result = "PLMN_RING";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::PLMN_SMS:
|
||||
result = "PLMN_SMS";
|
||||
break;
|
||||
default:
|
||||
result = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string
|
||||
IsaIemDcrController::helper_get_string_from_OCMFIdentificationType(types::powermeter::OCMFIdentificationType IT) {
|
||||
std::string result;
|
||||
switch (IT) {
|
||||
case types::powermeter::OCMFIdentificationType::DENIED:
|
||||
result = "DENIED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::UNDEFINED:
|
||||
result = "UNDEFINED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::ISO14443:
|
||||
result = "ISO14443";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::ISO15693:
|
||||
result = "ISO15693";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::EMAID:
|
||||
result = "EMAID";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::EVCCID:
|
||||
result = "EVCCID";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::EVCOID:
|
||||
result = "EVCOID";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::ISO7812:
|
||||
result = "ISO7812";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::CARD_TXN_NR:
|
||||
result = "CARD_TXN_NR";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::CENTRAL:
|
||||
result = "CENTRAL";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::CENTRAL_1:
|
||||
result = "CENTRAL_1";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::CENTRAL_2:
|
||||
result = "CENTRAL_2";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::LOCAL:
|
||||
result = "LOCAL";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::LOCAL_1:
|
||||
result = "LOCAL_1";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::LOCAL_2:
|
||||
result = "LOCAL_2";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::PHONE_NUMBER:
|
||||
result = "PHONE_NUMBER";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::KEY_CODE:
|
||||
result = "KEY_CODE";
|
||||
break;
|
||||
default:
|
||||
result = "NONE";
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::chrono::minutes IsaIemDcrController::helper_convert_timezone(std::string& timezone) {
|
||||
const char sign_char = timezone[0];
|
||||
const int offset_hours = std::stoi(timezone.substr(1, 2));
|
||||
const int offset_minutes = std::stoi(timezone.substr(3, 2));
|
||||
const std::chrono::minutes time_offset = std::chrono::hours(offset_hours) + std::chrono::minutes(offset_minutes);
|
||||
if (sign_char == '+') {
|
||||
return time_offset;
|
||||
} else {
|
||||
return -time_offset;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsaIemDcrController::helper_is_daylight_saving_time() {
|
||||
const std::time_t now = std::time(nullptr);
|
||||
const std::tm* localTime = std::localtime(&now);
|
||||
return localTime->tm_isdst > 0;
|
||||
}
|
||||
|
||||
std::string IsaIemDcrController::helper_get_current_datetime() {
|
||||
// Get UTC time
|
||||
auto now = std::chrono::system_clock::now();
|
||||
// Add configured timezone information
|
||||
std::time_t now_with_offset = std::chrono::system_clock::to_time_t(now + zone_time_offset);
|
||||
// Add DST offset if configured
|
||||
if (snapshot_config.timezone_handle_DST && helper_is_daylight_saving_time()) {
|
||||
now_with_offset = now_with_offset + 3600;
|
||||
}
|
||||
// Generate and return time in correct format
|
||||
std::ostringstream oss;
|
||||
oss << std::put_time(gmtime(&now_with_offset), "%FT%T,000") << snapshot_config.timezone;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string IsaIemDcrController::helper_remove_first_and_last_char(const std::string& input) {
|
||||
if (input.length() <= 1) {
|
||||
return "";
|
||||
}
|
||||
return input.substr(1, input.length() - 1);
|
||||
}
|
||||
|
||||
types::units_signed::SignedMeterValue IsaIemDcrController::helper_get_signed_datatuple(const std::string& endpoint) {
|
||||
auto response = this->http_client->get(endpoint);
|
||||
types::units_signed::SignedMeterValue return_value;
|
||||
if (response.status_code == 200) {
|
||||
try {
|
||||
return_value.signed_meter_data = response.body;
|
||||
return_value.signing_method = "";
|
||||
return_value.encoding_method = "OCMF";
|
||||
return_value.public_key = get_publickey(true);
|
||||
|
||||
return return_value;
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedIemDcrResponseBody(
|
||||
endpoint, fmt::format("Json error {} for body {}", json_error.what(), response.body));
|
||||
}
|
||||
} else {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace module::main
|
||||
@@ -0,0 +1,170 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_ISAIEMDCRCONTROLLER_H
|
||||
#define EVEREST_CORE_MODULE_ISAIEMDCRCONTROLLER_H
|
||||
|
||||
#include "httpClientInterface.hpp"
|
||||
#include <functional>
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
class IsaIemDcrController {
|
||||
|
||||
public:
|
||||
struct SnapshotConfig {
|
||||
std::string timezone;
|
||||
bool timezone_handle_DST;
|
||||
int datetime_resync_interval;
|
||||
int resilience_initial_connection_retry_delay;
|
||||
int resilience_transaction_request_retries;
|
||||
int resilience_transaction_request_retry_delay;
|
||||
std::string CT;
|
||||
std::string CI;
|
||||
std::string TT_initial;
|
||||
bool US;
|
||||
};
|
||||
|
||||
class IemDcrUnexpectedResponseException : public std::exception {
|
||||
public:
|
||||
const char* what() {
|
||||
return this->reason.c_str();
|
||||
}
|
||||
|
||||
explicit IemDcrUnexpectedResponseException(std::string reason) : reason(std::move(reason)) {
|
||||
}
|
||||
|
||||
private:
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
class UnexpectedIemDcrResponseBody : public IemDcrUnexpectedResponseException {
|
||||
public:
|
||||
UnexpectedIemDcrResponseBody(std::string endpoint, std::string error) :
|
||||
IemDcrUnexpectedResponseException(
|
||||
fmt::format("Received unexpected response body from endpoint {}: {}", endpoint, error)),
|
||||
endpoint(std::move(endpoint)),
|
||||
error(std::move(error)) {
|
||||
}
|
||||
|
||||
private:
|
||||
std::string endpoint;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
class UnexpectedIemDcrResponseCode : public IemDcrUnexpectedResponseException {
|
||||
public:
|
||||
const std::string endpoint;
|
||||
const HttpResponse response;
|
||||
const std::string body;
|
||||
|
||||
UnexpectedIemDcrResponseCode(const std::string& endpoint, unsigned int expected_code,
|
||||
const HttpResponse& response) :
|
||||
IemDcrUnexpectedResponseException(fmt::format(
|
||||
"Received unexpected response from endpoint '{}': {} (expected {}) {}", endpoint, response.status_code,
|
||||
expected_code, !response.body.empty() ? " - body: " + response.body : "")),
|
||||
endpoint(endpoint),
|
||||
response(response) {
|
||||
}
|
||||
};
|
||||
|
||||
class ThreadSafeString {
|
||||
public:
|
||||
ThreadSafeString() : value("") {
|
||||
}
|
||||
|
||||
void store(const std::string& new_value) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
value = new_value;
|
||||
}
|
||||
|
||||
std::string load() const {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex mutex;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
bool init();
|
||||
json get_gw();
|
||||
bool check_gw_is_empty();
|
||||
void post_gw();
|
||||
void post_tariff(std::string tariff_info);
|
||||
std::tuple<types::powermeter::Powermeter, std::string, bool> get_metervalue();
|
||||
std::string get_publickey(bool allow_cached_value);
|
||||
std::string get_datetime();
|
||||
void post_datetime();
|
||||
void refresh_datetime_if_required();
|
||||
void post_user(const types::powermeter::OCMFUserIdentificationStatus IS,
|
||||
const std::optional<types::powermeter::OCMFIdentificationLevel> IL,
|
||||
const std::vector<types::powermeter::OCMFIdentificationFlags>& IF,
|
||||
const types::powermeter::OCMFIdentificationType& IT,
|
||||
const std::optional<std::__cxx11::basic_string<char>>& ID,
|
||||
const std::optional<std::__cxx11::basic_string<char>>& TT);
|
||||
types::units_signed::SignedMeterValue get_receipt();
|
||||
types::units_signed::SignedMeterValue get_transaction();
|
||||
void post_receipt(const std::string& TX);
|
||||
|
||||
IsaIemDcrController() = delete;
|
||||
IsaIemDcrController(std::unique_ptr<HttpClientInterface> http_client, const SnapshotConfig& snap_config);
|
||||
|
||||
template <typename Callable>
|
||||
static auto call_with_retry(Callable func, int number_of_retries, int retry_wait_in_milliseconds,
|
||||
bool retry_on_http_client_error = true, bool retry_on_iemdcr_reponse_error = true)
|
||||
-> decltype(func()) {
|
||||
std::exception_ptr last_exception = nullptr;
|
||||
for (int attempt = 0; attempt < 1 + number_of_retries; ++attempt) {
|
||||
try {
|
||||
return func();
|
||||
} catch (HttpClientError& http_client_error) {
|
||||
last_exception = std::current_exception();
|
||||
if (!retry_on_http_client_error) {
|
||||
std::rethrow_exception(last_exception);
|
||||
}
|
||||
EVLOG_warning << "HTTPClient request failed: " << http_client_error.what() << "; retry in "
|
||||
<< retry_wait_in_milliseconds << " milliseconds";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_wait_in_milliseconds));
|
||||
} catch (IemDcrUnexpectedResponseException& iemdcr_error) {
|
||||
last_exception = std::current_exception();
|
||||
if (!retry_on_iemdcr_reponse_error) {
|
||||
std::rethrow_exception(last_exception);
|
||||
}
|
||||
EVLOG_warning << "Unexpected IEM-DCR response: " << iemdcr_error.what() << "; retry in "
|
||||
<< retry_wait_in_milliseconds << " milliseconds";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_wait_in_milliseconds));
|
||||
}
|
||||
}
|
||||
std::rethrow_exception(last_exception);
|
||||
}
|
||||
|
||||
private:
|
||||
const std::unique_ptr<HttpClientInterface> http_client;
|
||||
SnapshotConfig snapshot_config;
|
||||
std::string cached_public_key = "";
|
||||
std::chrono::minutes zone_time_offset;
|
||||
std::atomic<std::chrono::time_point<std::chrono::steady_clock>> last_datetime_sync;
|
||||
|
||||
std::chrono::minutes helper_convert_timezone(std::string& timezone);
|
||||
bool helper_is_daylight_saving_time();
|
||||
std::string helper_get_current_datetime();
|
||||
std::string helper_remove_first_and_last_char(const std::string& input);
|
||||
bool helper_get_bool_from_OCMFUserIdentificationStatus(types::powermeter::OCMFUserIdentificationStatus IS);
|
||||
std::string
|
||||
helper_get_string_from_OCMFIdentificationLevel(std::optional<types::powermeter::OCMFIdentificationLevel> IL);
|
||||
std::string helper_get_string_from_OCMFIdentificationFlags(types::powermeter::OCMFIdentificationFlags id_flag);
|
||||
std::string helper_get_string_from_OCMFIdentificationType(types::powermeter::OCMFIdentificationType IT);
|
||||
types::units_signed::SignedMeterValue helper_get_signed_datatuple(const std::string& endpoint);
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_ISAIEMDCRCONTROLLER_H
|
||||
@@ -0,0 +1,272 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "powermeterImpl.hpp"
|
||||
#include "httpClient.hpp"
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
void powermeterImpl::init() {
|
||||
// Check Config values (essential plausibility checks)
|
||||
check_config();
|
||||
|
||||
// Dependency injection pattern: Create the HTTP client first,
|
||||
// then move it into the controller as a constructor argument
|
||||
auto http_client = std::make_unique<HttpClient>(mod->config.ip_address, mod->config.port_http);
|
||||
|
||||
// Create controller object
|
||||
this->controller = std::make_unique<IsaIemDcrController>(
|
||||
std::move(http_client),
|
||||
IsaIemDcrController::SnapshotConfig{
|
||||
mod->config.timezone, mod->config.timezone_handle_DST, mod->config.datetime_resync_interval,
|
||||
mod->config.resilience_initial_connection_retry_delay, mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay, mod->config.CT, mod->config.CI,
|
||||
mod->config.TT_initial, mod->config.US});
|
||||
}
|
||||
|
||||
void powermeterImpl::ready() {
|
||||
// Start the live_measure_publisher thread, which periodically publishes the live measurements of the device
|
||||
this->live_measure_publisher_thread = std::thread([this] {
|
||||
while (true) {
|
||||
try {
|
||||
// Wait for at least one second (more if handle_start_transaction() or handle_stop_transaction() active)
|
||||
do {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
} while (start_transaction_running.load() == true || stop_transaction_running.load() == true);
|
||||
// Init if needed
|
||||
if (is_initialized.load() == false) {
|
||||
is_initialized.store(this->controller->init());
|
||||
if (is_initialized.load() == true) {
|
||||
// Publish public key once after init
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
this->publish_public_key_ocmf(this->controller->get_publickey(false));
|
||||
} else {
|
||||
if (!this->error_state_monitor->is_error_active("powermeter/CommunicationFault",
|
||||
"Communication timed out")) {
|
||||
auto error = this->error_factory->create_error(
|
||||
"powermeter/CommunicationFault", "Communication timed out",
|
||||
"This error is raised due to communication timeout");
|
||||
raise_error(error);
|
||||
}
|
||||
EVLOG_warning << "Connecting to IEM-DCR failed. Retry in "
|
||||
<< mod->config.resilience_initial_connection_retry_delay << " milliseconds";
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(mod->config.resilience_initial_connection_retry_delay));
|
||||
}
|
||||
} else {
|
||||
// Publish metervalue node (named powermeter in EVerest) and update status information
|
||||
auto meter_value_response = this->controller->get_metervalue();
|
||||
types::powermeter::Powermeter tmp_powermeter;
|
||||
std::string tmp_status;
|
||||
bool tmp_transaction_active;
|
||||
std::tie(tmp_powermeter, tmp_status, tmp_transaction_active) = meter_value_response;
|
||||
this->publish_powermeter(tmp_powermeter);
|
||||
dcr_status.store(tmp_status);
|
||||
transaction_active.store(tmp_transaction_active);
|
||||
|
||||
// Debug output :)
|
||||
// EVLOG_info << this->controller->get_datetime();
|
||||
|
||||
// Update datetime in specified interval
|
||||
if (transaction_active.load() == false) {
|
||||
this->controller->refresh_datetime_if_required();
|
||||
}
|
||||
|
||||
// Reset previous error (if active)
|
||||
if (this->error_state_monitor->is_error_active("powermeter/CommunicationFault",
|
||||
"Communication timed out")) {
|
||||
clear_error("powermeter/CommunicationFault", "Communication timed out");
|
||||
}
|
||||
}
|
||||
} catch (HttpClientError& client_error) {
|
||||
is_initialized.store(false);
|
||||
if (!this->error_state_monitor->is_error_active("powermeter/CommunicationFault",
|
||||
"Communication timed out")) {
|
||||
EVLOG_error << "Failed to communicate with the powermeter due to http error: "
|
||||
<< client_error.what();
|
||||
auto error =
|
||||
this->error_factory->create_error("powermeter/CommunicationFault", "Communication timed out",
|
||||
"This error is raised due to communication timeout");
|
||||
raise_error(error);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
is_initialized.store(false);
|
||||
EVLOG_error << "Exception in cyclic IEM-DCR communication: " << e.what();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStartResponse
|
||||
powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& value) {
|
||||
// your code for cmd start_transaction goes here
|
||||
types::powermeter::TransactionStartResponse return_value;
|
||||
|
||||
start_transaction_running.store(true);
|
||||
|
||||
// Check preconditions
|
||||
if (value.evse_id != mod->config.CI && value.evse_id.length() > 0) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::NOT_SUPPORTED;
|
||||
return_value.error = "config: CI does not match evse_id. This is not allowed.";
|
||||
EVLOG_error << "Aborted: " << *return_value.error;
|
||||
return return_value;
|
||||
}
|
||||
if (is_initialized.load() == false) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = "Init of communication not finished yet. Please try again later.";
|
||||
EVLOG_error << "Aborted: " << *return_value.error;
|
||||
return return_value;
|
||||
}
|
||||
if (dcr_status.load() != "0x0000, 0x00000000, 0x00, 0x00") {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = "IEM-DCR is in error state. XC: " + dcr_status.load();
|
||||
EVLOG_error << "Aborted: " << *return_value.error;
|
||||
return return_value;
|
||||
}
|
||||
|
||||
// Perform action
|
||||
try {
|
||||
// Check if gw information is already set
|
||||
if (this->controller->check_gw_is_empty()) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = "Init seems to be missing. Re-Init triggered. Please try again later.";
|
||||
is_initialized.store(false);
|
||||
} else {
|
||||
// Stop transaction (if a transaction is still running)
|
||||
if (transaction_active.load() == true) {
|
||||
this->controller->call_with_retry([this]() { this->controller->post_receipt("E"); },
|
||||
mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay);
|
||||
} else {
|
||||
// Try to end transaction at least once.
|
||||
try {
|
||||
this->controller->post_receipt("E");
|
||||
} catch (...) {
|
||||
// Nothing to do here
|
||||
}
|
||||
}
|
||||
// Wait for being surely in idle mode
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
// Create user
|
||||
if ((static_cast<std::string>(value.identification_data.value_or(""))).length() <= 0) {
|
||||
this->controller->call_with_retry(
|
||||
[this, value]() {
|
||||
this->controller->post_user(value.identification_status, value.identification_level,
|
||||
value.identification_flags, value.identification_type,
|
||||
value.transaction_id, value.tariff_text);
|
||||
},
|
||||
mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay);
|
||||
} else {
|
||||
this->controller->call_with_retry(
|
||||
[this, value]() {
|
||||
this->controller->post_user(value.identification_status, value.identification_level,
|
||||
value.identification_flags, value.identification_type,
|
||||
value.identification_data, value.tariff_text);
|
||||
},
|
||||
mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay);
|
||||
}
|
||||
// Start transaction
|
||||
this->controller->call_with_retry([this]() { this->controller->post_receipt("B"); },
|
||||
mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay);
|
||||
// Prepare positive response
|
||||
transaction_active.store(true);
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::OK;
|
||||
return_value.error = "";
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = e.what();
|
||||
EVLOG_error << "Aborted: " << return_value.error.value_or("");
|
||||
}
|
||||
|
||||
start_transaction_running.store(false);
|
||||
|
||||
return return_value;
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transaction(std::string& transaction_id) {
|
||||
// your code for cmd stop_transaction goes here
|
||||
types::powermeter::TransactionStopResponse return_value;
|
||||
|
||||
stop_transaction_running.store(true);
|
||||
|
||||
if (is_initialized.load() == false) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = "Init of communication not finished yet.";
|
||||
EVLOG_error << "Aborted: " << *return_value.error;
|
||||
} else if (transaction_active.load() == true) {
|
||||
try {
|
||||
// Stop transaction
|
||||
this->controller->call_with_retry([this]() { this->controller->post_receipt("E"); },
|
||||
mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay);
|
||||
// Wait for signature calculation
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
// Read receipt
|
||||
return_value.signed_meter_value =
|
||||
this->controller->call_with_retry([this]() { return this->controller->get_receipt(); },
|
||||
mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay);
|
||||
// Prepare positive response
|
||||
transaction_active.store(false);
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::OK;
|
||||
return_value.error = "";
|
||||
} catch (const std::exception& e) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = e.what();
|
||||
EVLOG_error << "Aborted: " << return_value.error.value_or("");
|
||||
}
|
||||
} else {
|
||||
// No transaction running. So return last transaction (if available)
|
||||
try {
|
||||
return_value.signed_meter_value = this->controller->get_transaction();
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::OK;
|
||||
return_value.error = "";
|
||||
} catch (std::exception& e) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = std::string(e.what()) + " Maybe no transaction to return?";
|
||||
EVLOG_warning << "Aborted: " << return_value.error.value_or("");
|
||||
}
|
||||
}
|
||||
|
||||
stop_transaction_running.store(false);
|
||||
|
||||
return return_value;
|
||||
}
|
||||
|
||||
void powermeterImpl::check_config() {
|
||||
// Numeric range checks are aready covered by manifest minimum and maximum declaration
|
||||
if (mod->config.ip_address.length() <= 0) {
|
||||
EVLOG_error << "Incorrect module config: parameter ip_address is empty." << std::endl;
|
||||
throw std::runtime_error("ip_address invalid. Please check configuration.");
|
||||
}
|
||||
if (mod->config.timezone.length() != 5) {
|
||||
EVLOG_error
|
||||
<< "Incorrect module config: parameter timezone has invalid length. 5 characters expected, e.g. +0200"
|
||||
<< std::endl;
|
||||
throw std::runtime_error("Timezone invalid. Please check configuration.");
|
||||
}
|
||||
if (mod->config.timezone[0] != '+' && mod->config.timezone[0] != '-') {
|
||||
EVLOG_error << "Incorrect module config: parameter timezone has invalid format. It must begin with + or - char."
|
||||
<< std::endl;
|
||||
throw std::runtime_error("Timezone invalid. Please check configuration.");
|
||||
}
|
||||
if (mod->config.CT.length() <= 0) {
|
||||
EVLOG_error << "Incorrect module config: parameter CT is empty." << std::endl;
|
||||
throw std::runtime_error("CT invalid. Please check configuration.");
|
||||
}
|
||||
if (mod->config.CI.length() <= 0) {
|
||||
EVLOG_error << "Incorrect module config: parameter CI is empty." << std::endl;
|
||||
throw std::runtime_error("CI invalid. Please check configuration.");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
@@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef MAIN_POWERMETER_IMPL_HPP
|
||||
#define MAIN_POWERMETER_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
#include "../IsabellenhuetteIemDcr.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
#include "httpClientInterface.hpp"
|
||||
#include "isabellenhuetteIemDcrController.hpp"
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class powermeterImpl : public powermeterImplBase {
|
||||
public:
|
||||
powermeterImpl() = delete;
|
||||
powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<IsabellenhuetteIemDcr>& mod, Conf& config) :
|
||||
powermeterImplBase(ev, "main"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual types::powermeter::TransactionStartResponse
|
||||
handle_start_transaction(types::powermeter::TransactionReq& value) override;
|
||||
virtual types::powermeter::TransactionStopResponse handle_stop_transaction(std::string& transaction_id) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<IsabellenhuetteIemDcr>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
// insert your private definitions here
|
||||
IsaIemDcrController::ThreadSafeString dcr_status;
|
||||
std::atomic<bool> is_initialized = false;
|
||||
std::atomic<bool> transaction_active = true;
|
||||
std::atomic<bool> start_transaction_running = false;
|
||||
std::atomic<bool> stop_transaction_running = false;
|
||||
// At construction time, there is no controller and no HTTP client, so these are null pointers.
|
||||
// When init() is called, the controller is initialized.
|
||||
std::unique_ptr<IsaIemDcrController> controller = nullptr;
|
||||
// The live_measure_publisher thread handles the periodic (1/s) publication of the live measurements
|
||||
// Initially it's a default-constructed thread (which is valid, but doesn't represent an actual running thread)
|
||||
// In ready(), the live_measure_publisher thread is started and placed in this field.
|
||||
std::thread live_measure_publisher_thread;
|
||||
// private functions
|
||||
void check_config();
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
|
||||
#endif // MAIN_POWERMETER_IMPL_HPP
|
||||
@@ -0,0 +1,68 @@
|
||||
description: Module implements Isabellenhuette IEM-DCR power meter driver, connecting via HTTP/REST
|
||||
config:
|
||||
ip_address:
|
||||
description: IPv4 Address of the power meter API.
|
||||
type: string
|
||||
default: "192.168.60.12"
|
||||
port_http:
|
||||
description: HTTP-Port of the power meter API.
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 65535
|
||||
default: 80
|
||||
timezone:
|
||||
description: The timezone offset information according to ISO8601 (version without colon) for normal time.
|
||||
type: string
|
||||
default: "+0100"
|
||||
timezone_handle_DST:
|
||||
description: Controls whether daylight saving time (DST) is handled or normal time is used continuously.
|
||||
type: boolean
|
||||
default: true
|
||||
datetime_resync_interval:
|
||||
description: Interval for cyclic time resync in hours.
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 24
|
||||
default: 2
|
||||
resilience_initial_connection_retry_delay:
|
||||
description: For the controller resilience, the delay in milliseconds before a retry attempt at module initialization.
|
||||
type: integer
|
||||
minimum: 1000
|
||||
maximum: 65535
|
||||
default: 10000
|
||||
resilience_transaction_request_retries:
|
||||
description: For the controller resilience, the number of retries to connect to the powermeter at a transaction start or stop request.
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 5
|
||||
default: 3
|
||||
resilience_transaction_request_retry_delay:
|
||||
description: For the controller resilience, the delay in milliseconds before a retry attempt at a transaction start or stop request.
|
||||
type: integer
|
||||
minimum: 200
|
||||
maximum: 1000
|
||||
default: 250
|
||||
CT:
|
||||
description: Charge point identification type (part of the signed OCMF data tuple).
|
||||
type: string
|
||||
default: "EVSEID"
|
||||
CI:
|
||||
description: Charge point identification (part of the signed OCMF data tuple).
|
||||
type: string
|
||||
default: "1234"
|
||||
TT_initial:
|
||||
description: Initial tariff text. (Its current value is part of signed OCMF data tuple).
|
||||
type: string
|
||||
default: ""
|
||||
US:
|
||||
description: Controls whether UserID is shown on display or not.
|
||||
type: boolean
|
||||
default: false
|
||||
provides:
|
||||
main:
|
||||
description: This is the main unit of the module
|
||||
interface: powermeter
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Josef Herbert <josef.herbert@isabellenhuette.com>
|
||||
@@ -0,0 +1,35 @@
|
||||
set(TEST_TARGET_NAME ${PROJECT_NAME}_isabellenhuetteIemDcrController_tests)
|
||||
|
||||
set(MODULE_DIR "${PROJECT_SOURCE_DIR}/modules/HardwareDrivers/PowerMeters/IsabellenhuetteIemDcr")
|
||||
|
||||
set(TEST_SOURCES ${MODULE_DIR}/main/isabellenhuetteIemDcrController.cpp)
|
||||
|
||||
add_executable(${TEST_TARGET_NAME} test_isabellenhuetteIemDcrController.cpp ${TEST_SOURCES})
|
||||
|
||||
add_dependencies(${TEST_TARGET_NAME} ${MODULE_NAME})
|
||||
|
||||
set(INCLUDE_DIR
|
||||
"main"
|
||||
"tests"
|
||||
"${MODULE_DIR}/main"
|
||||
"${MODULE_DIR}/tests"
|
||||
)
|
||||
|
||||
get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR)
|
||||
|
||||
target_include_directories(${TEST_TARGET_NAME} PRIVATE
|
||||
tests
|
||||
${INCLUDE_DIR}
|
||||
${GENERATED_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(${TEST_TARGET_NAME} PRIVATE
|
||||
GTest::gmock
|
||||
GTest::gtest_main
|
||||
everest::timer
|
||||
everest::framework
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME})
|
||||
ev_register_test_target(${TEST_TARGET_NAME})
|
||||
@@ -0,0 +1,10 @@
|
||||
## Unit tests with GTest
|
||||
A series of unit tests checks the implemented business logic of the controller.
|
||||
The unit tests require GoogleTest (GTest). This can be installed via
|
||||
```bash
|
||||
apt install libgtest-dev
|
||||
```
|
||||
Please do not forget to compile GTest library.
|
||||
|
||||
To run the tests, please issue compiler switch -DBUILD_TESTING=ON which will
|
||||
enable EVEREST_CORE_BUILD_TESTING on which these unit tests relate.
|
||||
@@ -0,0 +1,470 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "httpClientInterface.hpp"
|
||||
#include "isabellenhuetteIemDcrController.hpp"
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
class HTTPClientMock : public HttpClientInterface {
|
||||
public:
|
||||
MOCK_METHOD(HttpResponse, get, (const std::string& path), (override, const));
|
||||
MOCK_METHOD(HttpResponse, post, (const std::string& path, const std::string& body), (override, const));
|
||||
};
|
||||
|
||||
// Fixture class providing
|
||||
// - a http client mock
|
||||
// - default responses & request objects
|
||||
class IsabellenhuetteIemDcrControllerTest : public ::testing::Test {
|
||||
|
||||
protected:
|
||||
std::unique_ptr<HTTPClientMock> http_client;
|
||||
|
||||
const std::string gw_response{R"({
|
||||
"CT": "",
|
||||
"CI": "",
|
||||
"TM": "2024-12-16T19:00:22,000+0200 U"
|
||||
})"};
|
||||
|
||||
const std::string metervalue_response{R"({
|
||||
"MS": "1ISA0200000001",
|
||||
"TM": "2024-12-15T22:42:28,000+0200 U",
|
||||
"EF": "t",
|
||||
"ST": "G",
|
||||
"XT": false,
|
||||
"RD": [
|
||||
{
|
||||
"RV": "00000446.540",
|
||||
"RI": "1-b:1.8.e",
|
||||
"RU": "kWh",
|
||||
"RT": "DC"
|
||||
},
|
||||
{
|
||||
"RV": "00000004.881",
|
||||
"RI": "1-b:2.8.e",
|
||||
"RU": "kWh",
|
||||
"RT": "DC"
|
||||
},
|
||||
{
|
||||
"WV": "00000001.040",
|
||||
"WI": "transaction_1-b:1.8.e",
|
||||
"WU": "kWh",
|
||||
"WT": "DC"
|
||||
},
|
||||
{
|
||||
"WV": "00000002.503",
|
||||
"WI": "transaction_1-b:2.8.e",
|
||||
"WU": "kWh",
|
||||
"WT": "DC"
|
||||
}
|
||||
],
|
||||
"U1": 1.089,
|
||||
"U2": 60.1,
|
||||
"U3": 0,
|
||||
"U4": 0,
|
||||
"I": 5,
|
||||
"P": 300,
|
||||
"XS": "0x0000004000000004",
|
||||
"XC": "0x0000, 0x00000000, 0x00, 0x00"
|
||||
})"};
|
||||
|
||||
const std::string publickey = "3059301306072a8648ce3d020106082a8648ce3d"
|
||||
"03010703420004A97A28BE22DEDF619A497288FF"
|
||||
"F217832B37E44B8B1F8918C48EB5FBF5CB8B5FBB"
|
||||
"717D32CD2211534D968CA4425B9FCBF5A93E60F2"
|
||||
"CE97BCD63F9CAD287F5E08";
|
||||
|
||||
const std::string publickey_response{R"({
|
||||
"SA": "ECDSA-secp256r1-SHA256",
|
||||
"PK": ")" + publickey + R"("
|
||||
})"};
|
||||
|
||||
const std::string publickey_response_alt{R"({
|
||||
"SA": "ECDSA-secp256r1-SHA256",
|
||||
"PK": "abc"
|
||||
})"};
|
||||
|
||||
const std::string receipt_response{R"({
|
||||
"OCMF|{"FV":"1.0","GI":"Isabellenhuette IEM-DCR-125-1000-32-00-006-B_000",
|
||||
"GS":"1ISA0200001132","GV":"DU-02.00.12_SU-02.00.08","MV":"Isabellenhuette",
|
||||
"MM":"IEM-DCR-125-1000-32-00-006-B_000","MS":"1ISA0200001132",
|
||||
"CT":"DC-Test-Charger","CI":"CP-DE-4711","IS":true,"IL":"CERTIFIED",
|
||||
"IF":["RFID_PSK","OCPP_CERTIFIED","ISO15118_NONE","PLMN_NONE"],
|
||||
"IT":"ISO15693","ID":"9109543224","PG":"T4","TT":"0,20 EUR/kWh",
|
||||
"RD":[{"TM":"2025-06-06T09:26:24,000+0200 I","TX":"B","EF":"",
|
||||
"ST":"G","RV":"00000446.540","RI":"1-b:1.8.e","RU":"kWh","RT":"DC"}
|
||||
,{"TM":"2025-06-06T09:26:45,000+0200 I","TX":"E","EF":"","ST":"G",
|
||||
"RV":"00000446.540","RI":"1-b:1.8.e","RU":"kWh","RT":"DC"}]}|
|
||||
{"SA":"ECDSA-secp256r1-SHA256","SD":"304402203DE5BF9E3A5960935
|
||||
47AAEDA8CCEAFD88CAB59AC65FE616BD33158F2002545960220B3ED5B4A32E
|
||||
F8028577EF0E4F823DACF30DE75CA744C9FF9560FAE0134D19ABF"}
|
||||
})"};
|
||||
|
||||
const IsaIemDcrController::SnapshotConfig controller_config{"+0100", true, 12, 10000, 2,
|
||||
250, "ctVal", "ciVal", "ttInitial", true};
|
||||
|
||||
void SetUp() override {
|
||||
this->http_client = std::make_unique<HTTPClientMock>();
|
||||
}
|
||||
};
|
||||
|
||||
//****************************************************************
|
||||
// Test init behavior
|
||||
|
||||
/// \brief Test init() interacts propely with HttpClient
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_init) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/gw"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Return(HttpResponse{200, this->gw_response}));
|
||||
EXPECT_CALL(
|
||||
*this->http_client,
|
||||
post("/counter/v1/ocmf/gw", testing::MatchesRegex(R"(\{.*"CT":")" + this->controller_config.CT + R"(","CI":")" +
|
||||
this->controller_config.CI + R"(","TM":.*\}.*)")))
|
||||
.Times(1)
|
||||
.WillOnce(testing::Return(HttpResponse{200, this->gw_response}));
|
||||
EXPECT_CALL(*this->http_client,
|
||||
post("/counter/v1/ocmf/tariff",
|
||||
testing::MatchesRegex(R"(\{.*"TT":")" + this->controller_config.TT_initial + R"("\}.*)")))
|
||||
.Times(1)
|
||||
.WillOnce(testing::Return(HttpResponse{200, ""}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act
|
||||
bool retVal = controller.init();
|
||||
|
||||
// Verify
|
||||
EXPECT_EQ(retVal, true);
|
||||
}
|
||||
|
||||
/// \brief Test init() interacts propely with HttpClient with gw already set
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_init_with_gw_already_set) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/gw"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Return(HttpResponse{200, this->gw_response}));
|
||||
EXPECT_CALL(
|
||||
*this->http_client,
|
||||
post("/counter/v1/ocmf/gw", testing::MatchesRegex(R"(\{.*"CT":")" + this->controller_config.CT + R"(","CI":")" +
|
||||
this->controller_config.CI + R"(","TM":.*\}.*)")))
|
||||
.Times(1)
|
||||
.WillOnce(testing::Return(HttpResponse{428, ""}));
|
||||
EXPECT_CALL(*this->http_client,
|
||||
post("/counter/v1/ocmf/tariff",
|
||||
testing::MatchesRegex(R"(\{.*"TT":")" + this->controller_config.TT_initial + R"("\}.*)")))
|
||||
.Times(1)
|
||||
.WillOnce(testing::Return(HttpResponse{200, ""}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act
|
||||
bool retVal = controller.init();
|
||||
|
||||
// Verify
|
||||
EXPECT_EQ(retVal, true);
|
||||
}
|
||||
|
||||
/// \brief Test init() returns false on missing connection
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_init_timeout) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/gw"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Return(HttpResponse{408, ""})); // 408 Request timeout
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act
|
||||
bool retVal = controller.init();
|
||||
|
||||
// Verify
|
||||
EXPECT_EQ(retVal, false);
|
||||
}
|
||||
|
||||
//****************************************************************
|
||||
// Test get powermeter behavior
|
||||
|
||||
/// \brief Test get_metervalue() returns correct values
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_get_metervalue) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/metervalue"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Return(HttpResponse{200, this->metervalue_response}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act
|
||||
auto meter_value_response = controller.get_metervalue();
|
||||
types::powermeter::Powermeter tmp_powermeter;
|
||||
std::string tmp_status;
|
||||
bool tmp_transaction_active;
|
||||
std::tie(tmp_powermeter, tmp_status, tmp_transaction_active) = meter_value_response;
|
||||
|
||||
// Verify
|
||||
EXPECT_EQ(tmp_transaction_active, false);
|
||||
EXPECT_EQ(tmp_status, "0x0000, 0x00000000, 0x00, 0x00");
|
||||
EXPECT_EQ(tmp_powermeter.timestamp, "2024-12-15T22:42:28,000+0200");
|
||||
EXPECT_THAT(tmp_powermeter.energy_Wh_import.total, testing::FloatEq(1040));
|
||||
EXPECT_THAT(tmp_powermeter.energy_Wh_export->total, testing::FloatEq(2503));
|
||||
EXPECT_THAT(tmp_powermeter.power_W->total, testing::FloatEq(300.0));
|
||||
EXPECT_THAT(tmp_powermeter.current_A->DC.value(), testing::FloatEq(5.0));
|
||||
EXPECT_THAT(tmp_powermeter.voltage_V->DC.value(), testing::FloatEq(60.1));
|
||||
EXPECT_THAT(tmp_powermeter.meter_id.value(), "1ISA0200000001");
|
||||
}
|
||||
|
||||
/// \brief Test get_metervalue() fails due to an invalid response status code
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_get_metervalue_invalid_response_code) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/metervalue"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Return(HttpResponse{403, this->metervalue_response}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
EXPECT_THROW(controller.get_metervalue(), IsaIemDcrController::UnexpectedIemDcrResponseCode);
|
||||
}
|
||||
|
||||
/// \brief Test get_metervalue() fails due to an invalid response status body
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_get_metervalue_fail_invalid_response_body) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/metervalue"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Return(HttpResponse{200, "invalid"}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
EXPECT_THROW(controller.get_metervalue(), IsaIemDcrController::UnexpectedIemDcrResponseBody);
|
||||
}
|
||||
|
||||
/// \brief get_metervalue() fails due to an http client error
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_get_metervalue_fail_http_error) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/metervalue"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Throw(HttpClientError("http client mock error")));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
EXPECT_THROW(controller.get_metervalue(), HttpClientError);
|
||||
}
|
||||
|
||||
//****************************************************************
|
||||
// Test get publickey behavior
|
||||
|
||||
/// \brief Test get_publickey() returns correct value with enabled caching
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_get_publickey) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/publickey"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Return(HttpResponse{200, this->publickey_response}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
auto retVal = controller.get_publickey(true);
|
||||
EXPECT_EQ(retVal, this->publickey);
|
||||
retVal = controller.get_publickey(true);
|
||||
EXPECT_EQ(retVal, this->publickey); // Expect previous value (only one GET request allowed above)
|
||||
}
|
||||
|
||||
/// \brief Test get_publickey() returns correct value with disabled caching
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_get_publickey_no_cache) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/publickey"))
|
||||
.Times(2)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Return(HttpResponse{200, this->publickey_response}))
|
||||
.WillOnce(testing::Return(HttpResponse{200, this->publickey_response_alt}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
auto retVal = controller.get_publickey(false);
|
||||
EXPECT_EQ(retVal, this->publickey);
|
||||
retVal = controller.get_publickey(false);
|
||||
EXPECT_EQ(retVal, "abc"); // Expect fresh value of 2nd call (publickey_response_alt)
|
||||
}
|
||||
|
||||
/// \brief get_publickey() fails due to an http client error
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_get_publickey_fail_http_error) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/publickey"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Throw(HttpClientError("http client mock error")));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
EXPECT_THROW(controller.get_publickey(false), HttpClientError);
|
||||
}
|
||||
|
||||
//****************************************************************
|
||||
// Test start transaction behavior
|
||||
|
||||
/// \brief Test post_receipt("B") starts transaction properly
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_post_receipt_B) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, post("/counter/v1/ocmf/receipt", testing::MatchesRegex(R"(\{.*"TX":"B"*\}.*)")))
|
||||
.Times(1)
|
||||
.WillOnce(testing::Return(HttpResponse{200, ""}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
controller.post_receipt("B");
|
||||
}
|
||||
|
||||
/// \brief Test post_receipt("B") fails due to an invalid response status code
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_post_receipt_B_invalid_response_code) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, post("/counter/v1/ocmf/receipt", testing::MatchesRegex(R"(\{.*"TX":"B"*\}.*)")))
|
||||
.Times(1)
|
||||
.WillOnce(testing::Return(HttpResponse{428, ""}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
EXPECT_THROW(controller.post_receipt("B"), IsaIemDcrController::UnexpectedIemDcrResponseCode);
|
||||
}
|
||||
|
||||
/// \brief Test post_receipt("B") fails due to an http client error
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_post_receipt_B_fail_http_error) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, post("/counter/v1/ocmf/receipt", testing::MatchesRegex(R"(\{.*"TX":"B"*\}.*)")))
|
||||
.Times(1)
|
||||
.WillOnce(testing::Throw(HttpClientError("http client mock error")));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
EXPECT_THROW(controller.post_receipt("B"), HttpClientError);
|
||||
}
|
||||
|
||||
//****************************************************************
|
||||
// Test end transaction behavior
|
||||
|
||||
/// \brief Test post_receipt("E") ends transaction properly
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_post_receipt_E) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, post("/counter/v1/ocmf/receipt", testing::MatchesRegex(R"(\{.*"TX":"E"*\}.*)")))
|
||||
.Times(1)
|
||||
.WillOnce(testing::Return(HttpResponse{200, ""}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
controller.post_receipt("E");
|
||||
}
|
||||
|
||||
/// \brief Test post_receipt("E") fails due to an invalid response status code
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_post_receipt_E_invalid_response_code) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, post("/counter/v1/ocmf/receipt", testing::MatchesRegex(R"(\{.*"TX":"E"*\}.*)")))
|
||||
.Times(1)
|
||||
.WillOnce(testing::Return(HttpResponse{428, ""}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
EXPECT_THROW(controller.post_receipt("E"), IsaIemDcrController::UnexpectedIemDcrResponseCode);
|
||||
}
|
||||
|
||||
/// \brief Test post_receipt("B") fails due to an http client error
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_post_receipt_E_fail_http_error) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, post("/counter/v1/ocmf/receipt", testing::MatchesRegex(R"(\{.*"TX":"E"*\}.*)")))
|
||||
.Times(1)
|
||||
.WillOnce(testing::Throw(HttpClientError("http client mock error")));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
EXPECT_THROW(controller.post_receipt("E"), HttpClientError);
|
||||
}
|
||||
|
||||
/// \brief Test get_receipt() returns correct values
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_get_receipt) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/receipt"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Return(HttpResponse{200, this->receipt_response}));
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/publickey"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Return(HttpResponse{200, this->publickey_response}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act
|
||||
auto signed_meter_value = controller.get_receipt();
|
||||
|
||||
// Verify
|
||||
EXPECT_EQ(signed_meter_value.signed_meter_data, this->receipt_response);
|
||||
EXPECT_EQ(signed_meter_value.signing_method, "");
|
||||
EXPECT_EQ(signed_meter_value.encoding_method, "OCMF");
|
||||
EXPECT_EQ(signed_meter_value.public_key, this->publickey);
|
||||
}
|
||||
|
||||
/// \brief Test get_receipt() fails due to an invalid response status code
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_get_receipt_invalid_response_code) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/receipt"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Return(HttpResponse{403, this->receipt_response}));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
EXPECT_THROW(controller.get_receipt(), IsaIemDcrController::UnexpectedIemDcrResponseCode);
|
||||
}
|
||||
|
||||
/// \brief get_receipt() fails due to an http client error
|
||||
TEST_F(IsabellenhuetteIemDcrControllerTest, test_get_receipt_fail_http_error) {
|
||||
|
||||
// Setup
|
||||
testing::Sequence seq;
|
||||
EXPECT_CALL(*this->http_client, get("/counter/v1/ocmf/receipt"))
|
||||
.Times(1)
|
||||
.InSequence(seq)
|
||||
.WillOnce(testing::Throw(HttpClientError("http client mock error")));
|
||||
IsaIemDcrController controller(std::move(this->http_client), this->controller_config);
|
||||
|
||||
// Act & Verify
|
||||
EXPECT_THROW(controller.get_receipt(), HttpClientError);
|
||||
}
|
||||
|
||||
} // namespace module::main
|
||||
@@ -0,0 +1,32 @@
|
||||
#
|
||||
# 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 CURL::libcurl)
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
main/lem_dcbm_400600_controller.cpp
|
||||
main/lem_dcbm_time_sync_helper.cpp
|
||||
main/http_client.cpp
|
||||
)
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"main/powermeterImpl.cpp"
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include "LemDCBM400600.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
void LemDCBM400600::init() {
|
||||
invoke_init(*p_main);
|
||||
}
|
||||
|
||||
void LemDCBM400600::ready() {
|
||||
invoke_ready(*p_main);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef LEM_DCBM400600_HPP
|
||||
#define LEM_DCBM400600_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
// insert your custom include headers here
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {
|
||||
std::string ip_address;
|
||||
int port;
|
||||
std::string interface;
|
||||
std::string meter_tls_certificate;
|
||||
std::string ntp_server_1_ip_addr;
|
||||
int ntp_server_1_port;
|
||||
std::string ntp_server_2_ip_addr;
|
||||
int ntp_server_2_port;
|
||||
std::string meter_timezone;
|
||||
std::string meter_dst;
|
||||
int resilience_initial_connection_retries;
|
||||
int resilience_initial_connection_retry_delay;
|
||||
int resilience_transaction_request_retries;
|
||||
int resilience_transaction_request_retry_delay;
|
||||
int cable_id;
|
||||
int tariff_id;
|
||||
int IT;
|
||||
int SC;
|
||||
std::string UV;
|
||||
std::string UD;
|
||||
double temperature_warning_level_C;
|
||||
double temperature_error_level_C;
|
||||
double temperature_hysteresis_K;
|
||||
int temperature_min_time_as_valid_ms;
|
||||
int command_timeout_ms;
|
||||
};
|
||||
|
||||
class LemDCBM400600 : public Everest::ModuleBase {
|
||||
public:
|
||||
LemDCBM400600() = delete;
|
||||
LemDCBM400600(const ModuleInfo& info, std::unique_ptr<powermeterImplBase> p_main, Conf& config) :
|
||||
ModuleBase(info), p_main(std::move(p_main)), config(config){};
|
||||
|
||||
const std::unique_ptr<powermeterImplBase> p_main;
|
||||
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
|
||||
// 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 // LEM_DCBM400600_HPP
|
||||
@@ -0,0 +1,166 @@
|
||||
.. _everest_modules_handwritten_LemDCBM400600:
|
||||
|
||||
.. ****************
|
||||
.. LEM DCBM 400/600
|
||||
.. ****************
|
||||
|
||||
Module implementing the LEM DCBM 400/600 power meter driver adapter via HTTP/HTTPS.
|
||||
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
The module consists of a single ``main`` implementation that serves the ``powermeter`` interface. Requests/commands
|
||||
to the meter are translated and forwarded to the device via HTTP/HTTPS.
|
||||
|
||||
|
||||
Initialization
|
||||
--------------
|
||||
|
||||
On module initialization, the driver fetches the device's metric id from the ``/v1/status`` api. Consequently, this also ensures
|
||||
connectivity to the device.
|
||||
The initialization will fail (with a thrown exception) in case this cannot be established (possibly after a limited amount of retries).
|
||||
|
||||
Furthermore, at initialization the initial time sync setup is scheduled after a 2 minute waiting time (which is then executed
|
||||
during the module's "ready" thread loop), cf. also the notes on time synchronization below.
|
||||
|
||||
Variable Powermeter
|
||||
-------------------
|
||||
|
||||
Publication of the ``powermeter`` var is done with approx. frequency 1/second. This fetches the current ``livemeasure``
|
||||
values from the device's ``/v1/livemeasure`` endpoint and injects the meter id as determined at initialization.
|
||||
|
||||
Command start_transaction
|
||||
-------------------------
|
||||
|
||||
A ``start_transaction`` command is directly forwarded via a ``POST`` to the ``/v1/legal`` endpoint with a copy of the transaction request
|
||||
as payload (up to renaming of attributes). It returns ``true``, if the device (possibly after a limited amount of retries) returns a success
|
||||
response with a valid payload that indicates a ``running`` transaction status, otherwised it returns ``false``.
|
||||
|
||||
|
||||
Command stop_transaction
|
||||
------------------------
|
||||
|
||||
A ``stop_transaction`` command results into two requets to the devie.
|
||||
|
||||
First, a ``PUT`` to the ``/v1/legal`` endpoint stops the transaction.
|
||||
|
||||
Then, a call to the ``/v1/ocmf/`` endpoint fetches the OCMF report for the provided transaction id. Note that this always
|
||||
fetches the report of the `last` transaction with this id (in case if multiple transactions with the same id had been
|
||||
running).
|
||||
|
||||
If both requests are successful (possibly after a limited amount of retries), the returned OCMF string is forward 1:1.
|
||||
|
||||
In case of an error, an empty string is returned.
|
||||
|
||||
|
||||
Module Configuration
|
||||
====================
|
||||
|
||||
The module has the following configuration parameters:
|
||||
|
||||
ip_address
|
||||
----------
|
||||
IP address (or DNS/Host name) of the device.
|
||||
|
||||
port (optional)
|
||||
---------------
|
||||
Port used to reach the device. Defaults to ``80``. Note that the default value of ``80`` is used independent on whether
|
||||
TLS is enabled or not (which is in coherence with the device`s behavior).
|
||||
|
||||
meter_tls_certificate (optional)
|
||||
--------------------------------
|
||||
The meter's TLS X.509 certificate in PEM format. If provided, TLS will be used for communication with the device. See
|
||||
:ref:`notes on TLS <TLS Notes>` below.
|
||||
|
||||
|
||||
NTP Settings (optional)
|
||||
-----------------------
|
||||
|
||||
If NTP servers are supposed to be used for time sync by the device,
|
||||
those can provided via
|
||||
- ``ntp_server_1_ip_addr``, ``ntp_server_1_port`` for the first NTP server, and
|
||||
- ``ntp_server_2_ip_addr``, ``ntp_server_2_port`` for the first NTP server.
|
||||
|
||||
If the first server is provided, NTP will be activated on module initialization. Otherwise, a
|
||||
regular time sync with the system time will be executed.
|
||||
|
||||
Note that the wording "ip_address" follows the operational manual (cf. 4.2.3. of the `Communication protocols manual`, see references below).
|
||||
However, according to this manual DNS names are allowed, too.
|
||||
|
||||
|
||||
|
||||
|
||||
Resilience Settings (optional)
|
||||
------------------------------
|
||||
The following optional settings may be set to adapt the resilience behavior behavior of the module:
|
||||
|
||||
- ``resilience_initial_connection_retries`` and ``resilience_initial_connection_retry_delay`` define the number of attempted
|
||||
retries and delay inbetween in milliseconds in case of an error (failed connection or unexpected response from the device) during the module
|
||||
initialization. This potentially delays module initialization, but may prevent a module failure at startup (e.g., if the device
|
||||
is not ready yet).
|
||||
- ``resilience_transaction_request_retries`` and ``resilience_transaction_request_retry_delay`` similarly
|
||||
define the according values but for connection attempts during a transaction start or stop command handling.
|
||||
In order to prevent a greater command return delay (and since the device is assumed to be set up and running when
|
||||
transactions are started), default values are considerably lower than the ones for initialization.
|
||||
|
||||
|
||||
|
||||
Notes
|
||||
=====
|
||||
|
||||
Time Sync
|
||||
---------
|
||||
|
||||
The powermeter device needs to be regularly time synced in order to function properly
|
||||
The module is capable of performing regular syncs with the system time, or -- alternatively --
|
||||
allows to setup NTP servers (cf. the configuration parameters above).
|
||||
|
||||
If no NTP server is provided, a sync right before each transaction start is ensured in order to
|
||||
allow for the maximum possible transaction duration of 48 hours. Cf. the `Operation Manual` section 7.8.1 for
|
||||
more details.
|
||||
|
||||
Also note the device's manual suggests a start-up time of 2 minutes before settings (such as
|
||||
time sync) should be persisted (cf. the `Communication protocols manual` section 4).
|
||||
This is payed regard to in the module.
|
||||
|
||||
Error Handling / Resilience
|
||||
---------------------------
|
||||
|
||||
In general responses are checked for a valid response code and body. In case of validation errors or an http error,
|
||||
requests are retried to provide some resilience.
|
||||
|
||||
For the initialization requests, 25 retry attempts are made with a 10 second delay.
|
||||
For start/stop transaction requests, 3 retry attempts with a 200ms delay are made.
|
||||
|
||||
|
||||
.. _TLS Notes:
|
||||
|
||||
TLS Notes & Limitations
|
||||
-----------------------
|
||||
|
||||
The device brings its own self-signed certificate. Since there is no manufacturer root CA, this certificate must be provided
|
||||
in order to establish a reasonable TLS connection. Note that the provided certificate uses a private key of 1024bit length, which
|
||||
in general is considered vulnerable.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'http://<DEVICE ADDRESS>:<DEVICE PORT>/v1/certificate'
|
||||
|
||||
TLS can be enabled via:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl --location --request PUT 'https://<DEVICE ADDRESS>:<DEVICE PORT>/v1/settings' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"http": {
|
||||
"tls_on": true
|
||||
}
|
||||
}'
|
||||
|
||||
References / Links
|
||||
==================
|
||||
- `Official product page https://www.lem.com/en/dcbm-400-600 <https://www.lem.com/en/dcbm-400-600>`_
|
||||
- `Operation Manual <https://www.lem.com/en/file/10314/download>`_
|
||||
- `Communication protocols manual <https://www.lem.com/en/file/11215/download>`_
|
||||
@@ -0,0 +1,214 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "http_client.hpp"
|
||||
#include <fmt/core.h>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace module::main {
|
||||
const char* CONTENT_TYPE_HEADER = "Content-Type: application/json";
|
||||
|
||||
struct payloadInTransit {
|
||||
const std::string& data;
|
||||
size_t position;
|
||||
};
|
||||
|
||||
// Callback for receiving data, saves it into a string
|
||||
static size_t receive_data(char* ptr, size_t size, size_t nmemb, std::string* received_data) {
|
||||
received_data->append(ptr, size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
// Callback for sending data, fetches it from a string
|
||||
static size_t send_data(char* buffer, size_t size, size_t nitems, struct payloadInTransit* payload) {
|
||||
if (payload->position >= payload->data.length()) {
|
||||
// Returning 0 signals to libcurl that we have no more data to send
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Send up to size*nitems bytes of data
|
||||
size_t payload_remaining_bytes = payload->data.length() - payload->position;
|
||||
size_t num_bytes_to_send = std::min(size * nitems, payload_remaining_bytes);
|
||||
std::memcpy(buffer, payload->data.c_str() + payload->position, num_bytes_to_send);
|
||||
payload->position += num_bytes_to_send;
|
||||
return num_bytes_to_send;
|
||||
}
|
||||
|
||||
static HttpClientError client_error(const std::string& host, unsigned int port, const char* method,
|
||||
const std::string& path, const std::string& message) {
|
||||
return HttpClientError(fmt::format("HTTP client error on {} {}:{}{} : {} ", method, host, port, path, message));
|
||||
}
|
||||
|
||||
static void setup_connection(CURL* connection, struct payloadInTransit& request_payload, std::string& response_body,
|
||||
curl_slist*& headers, const int command_timeout_ms) {
|
||||
// Override the Content-Type header
|
||||
headers = curl_slist_append(nullptr, CONTENT_TYPE_HEADER);
|
||||
if (curl_easy_setopt(connection, CURLOPT_HTTPHEADER, headers) != CURLE_OK) {
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
|
||||
// Set up callbacks for reading and writing
|
||||
curl_easy_setopt(connection, CURLOPT_WRITEFUNCTION, receive_data);
|
||||
curl_easy_setopt(connection, CURLOPT_WRITEDATA, &response_body);
|
||||
curl_easy_setopt(connection, CURLOPT_READFUNCTION, send_data);
|
||||
curl_easy_setopt(connection, CURLOPT_READDATA, &request_payload);
|
||||
curl_easy_setopt(connection, CURLOPT_TIMEOUT_MS, command_timeout_ms);
|
||||
|
||||
// Misc. settings come here
|
||||
curl_easy_setopt(connection, CURLOPT_FORBID_REUSE, 1);
|
||||
if (curl_easy_setopt(connection, CURLOPT_FOLLOWLOCATION, 0) != CURLE_OK) {
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
}
|
||||
|
||||
static void setup_libcurl_tls_options_for_connection(CURL* connection, struct curl_blob* dcbm_cert) {
|
||||
// Since the LEM DCBM uses a certificate of only 1024bit, we need to lower the security level
|
||||
curl_easy_setopt(connection, CURLOPT_SSL_CIPHER_LIST, "DEFAULT:@SECLEVEL=1");
|
||||
// However, we still want to enforce TLS 1.2 or higher
|
||||
if (curl_easy_setopt(connection, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_TLSv1_3) !=
|
||||
CURLE_OK) {
|
||||
throw std::runtime_error("Failed to set CURLOPT_SSLVERSION. Is libcurl built with TLS support?");
|
||||
}
|
||||
|
||||
// We do not want to verify the hostname
|
||||
if (curl_easy_setopt(connection, CURLOPT_SSL_VERIFYHOST, 0) != CURLE_OK) {
|
||||
throw std::runtime_error("Failed to set CURLOPT_SSL_VERIFYHOST. Is libcurl built with TLS support?");
|
||||
}
|
||||
// We do want to verify the peer's certificate
|
||||
if (curl_easy_setopt(connection, CURLOPT_SSL_VERIFYPEER, 1) != CURLE_OK) {
|
||||
throw std::runtime_error("Failed to set CURLOPT_SSL_VERIFYPEER. Is libcurl built with TLS support?");
|
||||
}
|
||||
// We do not want to use OCSP
|
||||
// Whether this option is supported or not depends on the SSL backend, so we don't check the error code here.
|
||||
curl_easy_setopt(connection, CURLOPT_SSL_VERIFYSTATUS, 0);
|
||||
|
||||
// Now pass the DCBM certificate to libcurl
|
||||
if (curl_easy_setopt(connection, CURLOPT_CAINFO_BLOB, dcbm_cert) != CURLE_OK) {
|
||||
throw std::runtime_error("Failed to set CURLOPT_CAINFO_BLOB, possibly due to running out of memory.");
|
||||
}
|
||||
}
|
||||
|
||||
// Note: method_name and path are only there for the error message
|
||||
HttpResponse HttpClient::perform_request(CURL* connection, const std::string& request_body, const char* method_name,
|
||||
const std::string& path) const {
|
||||
// give curl a buffer to write its error messages to
|
||||
char curl_error_message[CURL_ERROR_SIZE] = {};
|
||||
curl_easy_setopt(connection, CURLOPT_ERRORBUFFER, curl_error_message);
|
||||
|
||||
// set up the connection options
|
||||
std::string response_body;
|
||||
struct payloadInTransit request_payload {
|
||||
request_body, 0
|
||||
};
|
||||
struct curl_slist* headers;
|
||||
setup_connection(connection, request_payload, response_body, headers, command_timeout_ms);
|
||||
|
||||
// Set up TLS options if TLS is enabled
|
||||
// we define dcbm_cert outside the "if" statement to ensure it outlives curl_easy_perform().
|
||||
struct curl_blob dcbm_cert {
|
||||
(void*)this->dcbm_tls_certificate.c_str(), this->dcbm_tls_certificate.size(),
|
||||
CURL_BLOB_NOCOPY // curl does not need to copy the cert, since it's not in a temporary location
|
||||
};
|
||||
if (this->tls_enabled) {
|
||||
setup_libcurl_tls_options_for_connection(connection, &dcbm_cert);
|
||||
}
|
||||
|
||||
// perform the request
|
||||
CURLcode res = curl_easy_perform(connection);
|
||||
|
||||
// remember to free the headers list...
|
||||
curl_slist_free_all(headers);
|
||||
// check the result of the request and return
|
||||
if (res == CURLE_OK) {
|
||||
long response_code;
|
||||
curl_easy_getinfo(connection, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
return HttpResponse{(unsigned int)response_code, std::move(response_body)};
|
||||
} else {
|
||||
throw client_error(this->host, this->port, method_name, path, std::string(curl_error_message));
|
||||
}
|
||||
}
|
||||
|
||||
CURL* HttpClient::create_curl_handle_and_setup_url(const std::string& path) const {
|
||||
CURL* connection = curl_easy_init();
|
||||
if (!connection) {
|
||||
throw std::runtime_error("Could not create a CURL handle: curl_easy_init() returned null");
|
||||
}
|
||||
const char* protocol = this->tls_enabled ? "https" : "http";
|
||||
if (curl_easy_setopt(connection, CURLOPT_URL,
|
||||
fmt::format("{}://{}:{}{}", protocol, this->host, this->port, path).c_str()) != CURLE_OK) {
|
||||
throw std::runtime_error("Could not set CURLOPT_URL, likely ran out of memory");
|
||||
}
|
||||
if (curl_easy_setopt(connection, CURLOPT_PROTOCOLS_STR, protocol) != CURLE_OK) {
|
||||
throw std::runtime_error(std::string("Could not set supported protocol to ") + protocol +
|
||||
", is it enabled in libcurl?");
|
||||
}
|
||||
if (!this->network_interface.empty()) {
|
||||
if (curl_easy_setopt(connection, CURLOPT_INTERFACE, this->network_interface.c_str()) != CURLE_OK) {
|
||||
throw std::runtime_error("Could not bind to the specified network interface: " + this->network_interface);
|
||||
}
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::get(const std::string& path) const {
|
||||
CURL* connection = this->create_curl_handle_and_setup_url(path);
|
||||
|
||||
if (curl_easy_setopt(connection, CURLOPT_HTTPGET, 1) != CURLE_OK) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
|
||||
// perform_request() does not cleanup the connection on its own.
|
||||
// We do the cleanup here, and make sure to rethrow any exception that might've occurred.
|
||||
try {
|
||||
HttpResponse response = perform_request(connection, "", "GET", path);
|
||||
curl_easy_cleanup(connection);
|
||||
return response;
|
||||
} catch (std::exception& e) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::put(const std::string& path, const std::string& body) const {
|
||||
CURL* connection = this->create_curl_handle_and_setup_url(path);
|
||||
|
||||
curl_easy_setopt(connection, CURLOPT_UPLOAD, 1);
|
||||
|
||||
// perform_request() does not cleanup the connection on its own.
|
||||
// We do the cleanup here, and make sure to rethrow any exception that might've occurred.
|
||||
try {
|
||||
HttpResponse response = perform_request(connection, body, "PUT", path);
|
||||
curl_easy_cleanup(connection);
|
||||
return response;
|
||||
} catch (std::exception& e) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::post(const std::string& path, const std::string& body) const {
|
||||
CURL* connection = this->create_curl_handle_and_setup_url(path);
|
||||
|
||||
if (curl_easy_setopt(connection, CURLOPT_POST, 1) != CURLE_OK) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
|
||||
// perform_request() does not cleanup the connection on its own.
|
||||
// We do the cleanup here, and make sure to rethrow any exception that might've occurred.
|
||||
try {
|
||||
HttpResponse response = perform_request(connection, body, "POST", path);
|
||||
curl_easy_cleanup(connection);
|
||||
return response;
|
||||
} catch (std::exception& e) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
} // namespace module::main
|
||||
@@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_HTTPCLIENT_H
|
||||
#define EVEREST_CORE_MODULE_HTTPCLIENT_H
|
||||
|
||||
#include "fmt/format.h"
|
||||
#include "http_client_interface.hpp"
|
||||
#include <curl/curl.h>
|
||||
#include <everest/logging.hpp>
|
||||
#include <regex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
// The DCBM does not print its certificate correctly in its /certificate API.
|
||||
// In particular, the newlines after -----BEGIN CERTIFICATE----- and before -----END CERTIFICATE----- are missing there.
|
||||
// This function will add these newlines if they are missing.
|
||||
static void fixup_tls_certificate(std::string& tls_certificate) {
|
||||
tls_certificate = std::regex_replace(tls_certificate, std::regex("-----BEGIN CERTIFICATE-----\\s*([^\n])"),
|
||||
"-----BEGIN CERTIFICATE-----\n$1");
|
||||
tls_certificate = std::regex_replace(tls_certificate, std::regex("([^\n])\\s*-----END CERTIFICATE-----"),
|
||||
"$1\n-----END CERTIFICATE-----");
|
||||
}
|
||||
|
||||
class HttpClient : public HttpClientInterface {
|
||||
|
||||
public:
|
||||
HttpClient() = delete;
|
||||
|
||||
HttpClient(const std::string& host_arg, int port_arg, const std::string& tls_certificate,
|
||||
const std::string& network_interface = "") {
|
||||
// initialize libcurl - this is safe to do multiple times, if there are multiple HttpClients
|
||||
// Note: This is only thread-safe after libcurl 7.84.0, but we use 8.4.0, so it should be fine
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
// These are saved in the client to avoid making the controller pass them at every call
|
||||
host = host_arg;
|
||||
port = port_arg;
|
||||
dcbm_tls_certificate = tls_certificate;
|
||||
tls_enabled = !dcbm_tls_certificate.empty();
|
||||
fixup_tls_certificate(dcbm_tls_certificate);
|
||||
this->network_interface = network_interface;
|
||||
}
|
||||
|
||||
~HttpClient() override {
|
||||
// release the libcurl resources - this must be done once for every call to curl_global_init().
|
||||
// Note: This is only thread-safe after libcurl 7.84.0, but we use 8.4.0, so it should be fine
|
||||
curl_global_cleanup();
|
||||
}
|
||||
|
||||
void set_command_timeout(const int command_timeout_ms) override {
|
||||
this->command_timeout_ms = command_timeout_ms;
|
||||
}
|
||||
[[nodiscard]] HttpResponse get(const std::string& path) const override;
|
||||
[[nodiscard]] HttpResponse put(const std::string& path, const std::string& body) const override;
|
||||
[[nodiscard]] HttpResponse post(const std::string& path, const std::string& body) const override;
|
||||
|
||||
private:
|
||||
std::string host;
|
||||
int port;
|
||||
bool tls_enabled;
|
||||
std::string dcbm_tls_certificate;
|
||||
int command_timeout_ms = 5000; // default timeout in milliseconds
|
||||
std::string network_interface; // network interface
|
||||
|
||||
[[nodiscard]] CURL* create_curl_handle_and_setup_url(const std::string& path) const;
|
||||
HttpResponse perform_request(CURL* connection, const std::string& request_body, const char* method_name,
|
||||
const std::string& path) const;
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_HTTPCLIENT_H
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_HTTP_CLIENT_INTERFACE_H
|
||||
#define EVEREST_CORE_MODULE_HTTP_CLIENT_INTERFACE_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
class HttpClientError : public std::exception {
|
||||
public:
|
||||
[[nodiscard]] const char* what() const noexcept override {
|
||||
return this->reason.c_str();
|
||||
}
|
||||
explicit HttpClientError(std::string msg) {
|
||||
this->reason = std::move(msg);
|
||||
}
|
||||
explicit HttpClientError(const char* msg) {
|
||||
this->reason = std::string(msg);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct HttpResponse {
|
||||
unsigned int status_code;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
struct HttpClientInterface {
|
||||
|
||||
virtual ~HttpClientInterface() = default;
|
||||
|
||||
virtual void set_command_timeout(const int command_timeout_ms){};
|
||||
[[nodiscard]] virtual HttpResponse get(const std::string& path) const = 0;
|
||||
[[nodiscard]] virtual HttpResponse put(const std::string& path, const std::string& body) const = 0;
|
||||
[[nodiscard]] virtual HttpResponse post(const std::string& path, const std::string& body) const = 0;
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_HTTP_CLIENT_INTERFACE_H
|
||||
@@ -0,0 +1,409 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "lem_dcbm_400600_controller.hpp"
|
||||
#include <stdexcept>
|
||||
namespace module::main {
|
||||
|
||||
void LemDCBM400600Controller::init() {
|
||||
EVLOG_info << "LEM DCBM 400/600: Try to communicate with the device to initialize it.";
|
||||
this->time_sync_helper->set_time_config_params(config.meter_timezone, config.meter_dst);
|
||||
this->http_client->set_command_timeout(this->config.command_timeout_ms);
|
||||
|
||||
if (this->config.IT >= 0) {
|
||||
call_with_retry(
|
||||
[this]() {
|
||||
const int current_it = this->get_identification_type();
|
||||
if (current_it != this->config.IT) {
|
||||
EVLOG_info << "LEM DCBM 400/600: Setting OCMF Identification Type (IT) to: " << this->config.IT;
|
||||
this->set_identification_type(this->config.IT);
|
||||
} else {
|
||||
EVLOG_info << "LEM DCBM 400/600: OCMF Identification Type (IT) already set to " << current_it
|
||||
<< ", skipping write";
|
||||
}
|
||||
},
|
||||
this->config.init_number_of_http_retries, this->config.init_retry_wait_in_milliseconds);
|
||||
}
|
||||
|
||||
call_with_retry([this]() { this->fetch_meter_id_from_device(); }, this->config.init_number_of_http_retries,
|
||||
this->config.init_retry_wait_in_milliseconds);
|
||||
|
||||
this->time_sync_helper->restart_unsafe_period();
|
||||
EVLOG_info << "LEM DCBM 400/600: Device initialized successfully.";
|
||||
}
|
||||
|
||||
std::vector<std::string> split(const std::string& str, char delimiter) {
|
||||
std::vector<std::string> tokens;
|
||||
std::string token;
|
||||
std::stringstream ss(str);
|
||||
|
||||
while (std::getline(ss, token, delimiter)) {
|
||||
tokens.push_back(token);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
std::string LemDCBM400600Controller::get_current_transaction() {
|
||||
const std::string endpoint = v2_capable ? "/v2/legal" : "/v1/legal";
|
||||
auto response = this->http_client->get(endpoint);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode(endpoint, 200, response);
|
||||
}
|
||||
try {
|
||||
json data = json::parse(response.body);
|
||||
return data.at("transactionId");
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(endpoint, fmt::format("Json error '{}'", json_error.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBM400600Controller::update_lem_status() {
|
||||
// should call this after a communication error to figure out what has been happening
|
||||
auto status_response = this->http_client->get("/v1/status");
|
||||
|
||||
if (status_response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode("/v1/status", 200, status_response);
|
||||
}
|
||||
try {
|
||||
json data = json::parse(status_response.body);
|
||||
this->need_to_stop_transaction = data.at("status").at("bits").at("transactionIsOnGoing");
|
||||
this->current_transaction_id = get_current_transaction();
|
||||
if (this->need_to_stop_transaction) {
|
||||
// we need to get the current transaction id or the last known transaction id since we might
|
||||
// receive a stop transaction with id 0 or with the last known transaction if
|
||||
// meaning that the system had a failure and the transaction was started but id was not saved
|
||||
// and we try to recover from the error, thus we need to cancel the transaction
|
||||
EVLOG_warning << "LEM DCBM 400/600: A transaction is already ongoing and it has the id:"
|
||||
<< this->current_transaction_id;
|
||||
} else {
|
||||
EVLOG_info << "LEM DCBM 400/600: The last known transaction has the id:" << this->current_transaction_id;
|
||||
}
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(
|
||||
"/v1/status", fmt::format("Json error {} for body {}", json_error.what(), status_response.body));
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBM400600Controller::fetch_meter_id_from_device() {
|
||||
auto status_response = this->http_client->get("/v1/status");
|
||||
|
||||
if (status_response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode("/v1/status", 200, status_response);
|
||||
}
|
||||
try {
|
||||
json data = json::parse(status_response.body);
|
||||
this->meter_id = data.at("meterId");
|
||||
this->public_key_ocmf = data.at("publicKeyOcmf");
|
||||
this->need_to_stop_transaction = data.at("status").at("bits").at("transactionIsOnGoing");
|
||||
std::string version = data.at("version").at("applicationFirmwareVersion");
|
||||
auto components = split(version, '.');
|
||||
this->v2_capable =
|
||||
((components.size() == 4) && (components[1] > "1")); // the major version must be newer than 1
|
||||
this->current_transaction_id = get_current_transaction();
|
||||
if (this->need_to_stop_transaction) {
|
||||
// we need to get the current transaction id or the last known transaction id since we might
|
||||
// receive a stop transaction with id 0 or with the last known transaction if
|
||||
// meaning that the system had a failure and the transaction was started but id was not saved
|
||||
// and we try to recover from the error, thus we need to cancel the transaction
|
||||
EVLOG_warning << "LEM DCBM 400/600: A transaction is already ongoing and it has the id:"
|
||||
<< this->current_transaction_id;
|
||||
} else {
|
||||
EVLOG_info << "LEM DCBM 400/600: The last known transaction has the id:" << this->current_transaction_id;
|
||||
}
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(
|
||||
"/v1/status", fmt::format("Json error {} for body {}", json_error.what(), status_response.body));
|
||||
}
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStartResponse
|
||||
LemDCBM400600Controller::start_transaction(const types::powermeter::TransactionReq& value) {
|
||||
try {
|
||||
if (this->need_to_stop_transaction) {
|
||||
// there is already an ongoing transaction, something went wrong, we will clean
|
||||
// the current transaction
|
||||
EVLOG_error << "LEM DCBM 400/600: A transaction with the id " << this->current_transaction_id
|
||||
<< "already exists but the system is trying to start a new transaction with the id:"
|
||||
<< value.transaction_id << ", try to recover by closing the current transaction";
|
||||
try {
|
||||
// we will not return any response to stop transaction since this is a self triggered command
|
||||
this->request_device_to_stop_transaction(this->current_transaction_id);
|
||||
this->need_to_stop_transaction = false;
|
||||
} catch (UnexpectedDCBMResponseCode& error) {
|
||||
EVLOG_error << "LEM DCBM 400/600: Could not close the current transaction, got error:" << error.what();
|
||||
}
|
||||
}
|
||||
call_with_retry([this, value]() { this->request_device_to_start_transaction(value); },
|
||||
this->config.transaction_number_of_http_retries,
|
||||
this->config.transaction_retry_wait_in_milliseconds);
|
||||
this->current_transaction_id = value.transaction_id;
|
||||
this->need_to_stop_transaction = true;
|
||||
} catch (DCBMUnexpectedResponseException& error) {
|
||||
const std::string error_message =
|
||||
fmt::format("Failed to start transaction {}: {}", value.transaction_id, error.what());
|
||||
EVLOG_error << error_message;
|
||||
return {types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, error_message};
|
||||
} catch (HttpClientError& error) {
|
||||
const std::string error_message = fmt::format(
|
||||
"Failed to start transaction {} - connection to device failed: {}", value.transaction_id, error.what());
|
||||
EVLOG_error << error_message;
|
||||
return {types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, error_message};
|
||||
}
|
||||
|
||||
auto [transaction_min_stop_time, transaction_max_stop_time] = get_transaction_stop_time_bounds();
|
||||
|
||||
return {types::powermeter::TransactionRequestStatus::OK, {}, transaction_min_stop_time, transaction_max_stop_time};
|
||||
}
|
||||
|
||||
void LemDCBM400600Controller::request_device_to_start_transaction(const types::powermeter::TransactionReq& value) {
|
||||
this->time_sync_helper->sync(*this->http_client);
|
||||
|
||||
const std::string endpoint = v2_capable ? "/v2/legal" : "/v1/legal";
|
||||
const std::string payload = this->transaction_start_request_to_dcbm_payload(value);
|
||||
auto response = this->http_client->post(endpoint, payload);
|
||||
if (response.status_code != 201) {
|
||||
throw UnexpectedDCBMResponseCode(endpoint, 201, response);
|
||||
}
|
||||
try {
|
||||
bool running = json::parse(response.body).at("running");
|
||||
if (!running) {
|
||||
throw UnexpectedDCBMResponseBody(
|
||||
"/v1/legal", fmt::format("Created transaction {} has state running = false.", value.transaction_id));
|
||||
}
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(endpoint,
|
||||
fmt::format("Json error {} for body '{}'", json_error.what(), response.body));
|
||||
}
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStopResponse
|
||||
LemDCBM400600Controller::stop_transaction(const std::string& transaction_id) {
|
||||
std::string tid = transaction_id;
|
||||
bool need_to_execute_device_stop_transaction = true;
|
||||
if (!(this->need_to_stop_transaction) && transaction_id == this->current_transaction_id) {
|
||||
// transaction is not open but we need to provide OCMF information about it
|
||||
need_to_execute_device_stop_transaction = false;
|
||||
this->current_transaction_id = "";
|
||||
}
|
||||
if (!(this->need_to_stop_transaction) && transaction_id.empty()) {
|
||||
// return an error because there is no transaction initially ongoing (at start up time)
|
||||
return types::powermeter::TransactionStopResponse{
|
||||
types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, "No dangling transaction open"};
|
||||
}
|
||||
if (this->need_to_stop_transaction && transaction_id == this->current_transaction_id) {
|
||||
// transaction has been found and it is now going to close
|
||||
this->need_to_stop_transaction = false;
|
||||
this->current_transaction_id = "";
|
||||
}
|
||||
if (this->need_to_stop_transaction && transaction_id.empty()) {
|
||||
// transaction has NOT been found by the system, however the system is requesting a cleanup
|
||||
// thus we use the last known value of the transaction id to close the transaction
|
||||
tid = this->current_transaction_id;
|
||||
this->current_transaction_id = "";
|
||||
this->need_to_stop_transaction = false;
|
||||
}
|
||||
try {
|
||||
return call_with_retry(
|
||||
[this, need_to_execute_device_stop_transaction, tid]() {
|
||||
// special case if we started and a transaction is ongoing - the upper layers might not know the
|
||||
// transaction id
|
||||
if (need_to_execute_device_stop_transaction) {
|
||||
this->request_device_to_stop_transaction(tid);
|
||||
}
|
||||
auto signed_meter_value = types::units_signed::SignedMeterValue{fetch_ocmf_result(tid), "", "OCMF"};
|
||||
signed_meter_value.public_key.emplace(public_key_ocmf);
|
||||
return types::powermeter::TransactionStopResponse{types::powermeter::TransactionRequestStatus::OK,
|
||||
{}, // Empty start_signed_meter_value
|
||||
signed_meter_value};
|
||||
},
|
||||
this->config.transaction_number_of_http_retries, this->config.transaction_retry_wait_in_milliseconds);
|
||||
} catch (DCBMUnexpectedResponseException& error) {
|
||||
std::string error_message = fmt::format("Failed to stop transaction {}: {}", tid, error.what());
|
||||
EVLOG_error << error_message;
|
||||
return types::powermeter::TransactionStopResponse{
|
||||
types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, error_message};
|
||||
} catch (HttpClientError& error) {
|
||||
std::string error_message =
|
||||
fmt::format("Failed to stop transaction {} - connection to device failed: {}", tid, error.what());
|
||||
EVLOG_error << error_message;
|
||||
// if we have the last known OCMF value, we can return it
|
||||
if (current_signed_meter_value.public_key.has_value()) {
|
||||
EVLOG_warning << "LEM DCBM 400/600: Returning the last known OCMF value for transaction " << tid
|
||||
<< " with value: " << current_signed_meter_value;
|
||||
return types::powermeter::TransactionStopResponse{
|
||||
types::powermeter::TransactionRequestStatus::OK, {}, current_signed_meter_value};
|
||||
}
|
||||
current_signed_meter_value = types::units_signed::SignedMeterValue{};
|
||||
return types::powermeter::TransactionStopResponse{
|
||||
types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, error_message};
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBM400600Controller::request_device_to_stop_transaction(const std::string& transaction_id) {
|
||||
std::string endpoint = v2_capable ? fmt::format("/v2/legal?transactionId={}", transaction_id)
|
||||
: fmt::format("/v1/legal?transactionId={}", transaction_id);
|
||||
auto legal_api_response = this->http_client->put(endpoint, R"({"running": false})");
|
||||
|
||||
if (legal_api_response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode(endpoint, 200, legal_api_response);
|
||||
}
|
||||
|
||||
try {
|
||||
int status = json::parse(legal_api_response.body).at("meterValue").at("transactionStatus");
|
||||
bool transaction_is_ongoing = (status & 0b100) != 0; // third status bit "transactionIsOnGoing" must be false
|
||||
if (transaction_is_ongoing) {
|
||||
throw UnexpectedDCBMResponseBody(endpoint, fmt::format("Transaction stop request for transaction {} "
|
||||
"returned device status {}.",
|
||||
transaction_id, status));
|
||||
}
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(
|
||||
endpoint, fmt::format("Json error '{}' for body {}", json_error.what(), legal_api_response.body));
|
||||
}
|
||||
}
|
||||
|
||||
std::string LemDCBM400600Controller::fetch_ocmf_result(const std::string& transaction_id) {
|
||||
const std::string ocmf_endpoint = v2_capable ? fmt::format("/v2/ocmf?transactionId={}", transaction_id)
|
||||
: fmt::format("/v1/ocmf?transactionId={}", transaction_id);
|
||||
auto ocmf_api_response = this->http_client->get(ocmf_endpoint);
|
||||
|
||||
if (ocmf_api_response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode(ocmf_endpoint, 200, ocmf_api_response);
|
||||
}
|
||||
|
||||
if (ocmf_api_response.body.empty()) {
|
||||
throw UnexpectedDCBMResponseBody(ocmf_endpoint, "Returned empty body");
|
||||
}
|
||||
|
||||
return ocmf_api_response.body;
|
||||
}
|
||||
|
||||
types::powermeter::Powermeter LemDCBM400600Controller::get_powermeter() {
|
||||
this->time_sync_helper->sync_if_deadline_expired(*this->http_client);
|
||||
|
||||
const std::string endpoint = v2_capable ? "/v2/livemeasure" : "/v1/livemeasure";
|
||||
auto response = this->http_client->get(endpoint);
|
||||
types::powermeter::Powermeter powermeter_result;
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode(endpoint, 200, response);
|
||||
}
|
||||
try {
|
||||
powermeter_result = this->convert_livemeasure_to_powermeter(response.body);
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(endpoint, fmt::format("Json error '{}'", json_error.what()));
|
||||
}
|
||||
if (this->need_to_stop_transaction) {
|
||||
// if there is no ongoing transaction, we do need to fetch the signed meter value to have it available
|
||||
// for the upper layers, otherwise we will not have the OCMF value if we lose connection to the device
|
||||
try {
|
||||
current_signed_meter_value =
|
||||
types::units_signed::SignedMeterValue{fetch_ocmf_result(current_transaction_id), "", "OCMF"};
|
||||
current_signed_meter_value.public_key.emplace(public_key_ocmf);
|
||||
current_signed_meter_value.timestamp.emplace(powermeter_result.timestamp);
|
||||
} catch (UnexpectedDCBMResponseCode& error) {
|
||||
EVLOG_error << "LEM DCBM 400/600: Could not get the OCMF value: " << error.what();
|
||||
} catch (UnexpectedDCBMResponseBody& error) {
|
||||
EVLOG_error << "LEM DCBM 400/600: Invalid OCMF value: " << error.what();
|
||||
} catch (HttpClientError& error) {
|
||||
std::string error_message = fmt::format("Failed get the OCMF field {} - connection to device failed: {}",
|
||||
current_transaction_id, error.what());
|
||||
EVLOG_error << error_message;
|
||||
}
|
||||
}
|
||||
return powermeter_result;
|
||||
}
|
||||
|
||||
types::powermeter::Powermeter
|
||||
LemDCBM400600Controller::convert_livemeasure_to_powermeter(const std::string& livemeasure) {
|
||||
types::powermeter::Powermeter powermeter;
|
||||
json data = json::parse(livemeasure);
|
||||
powermeter.timestamp = data.at("timestamp");
|
||||
powermeter.meter_id.emplace(this->meter_id);
|
||||
powermeter.energy_Wh_import = {data.at("energyImportTotal").get<float>() * 1000.0f};
|
||||
powermeter.energy_Wh_export.emplace(types::units::Energy{data.at("energyExportTotal").get<float>() * 1000.0f});
|
||||
auto voltage = types::units::Voltage{};
|
||||
voltage.DC = data.at("voltage");
|
||||
powermeter.voltage_V.emplace(voltage);
|
||||
auto current = types::units::Current{};
|
||||
current.DC = data.at("current");
|
||||
powermeter.current_A.emplace(current);
|
||||
powermeter.power_W.emplace(types::units::Power{data.at("power").get<float>() * 1000.0f});
|
||||
powermeter.temperatures.emplace({types::temperature::Temperature{data.at("temperatureH"), "temperatureH"},
|
||||
types::temperature::Temperature{data.at("temperatureL"), "temperatureL"}});
|
||||
return powermeter;
|
||||
}
|
||||
|
||||
std::string
|
||||
LemDCBM400600Controller::transaction_start_request_to_dcbm_payload(const types::powermeter::TransactionReq& request) {
|
||||
std::string client_id = request.identification_data.value_or("") + ',' + request.transaction_id;
|
||||
const int max_length_client_id = 37; // as defined by LEM documentation
|
||||
client_id = (client_id.length() > max_length_client_id) ? client_id.substr(0, max_length_client_id) : client_id;
|
||||
if (this->v2_capable) {
|
||||
return nlohmann::ordered_json{{"evseId", request.evse_id},
|
||||
{"transactionId", request.transaction_id},
|
||||
{"clientId", client_id},
|
||||
{"tariffId", this->config.tariff_id},
|
||||
{"TT", request.tariff_text.value_or("")},
|
||||
{"UV", this->config.UV},
|
||||
{"UD", this->config.UD},
|
||||
{"cableId", this->config.cable_id},
|
||||
{"userData", ""},
|
||||
{"SC", this->config.SC}}
|
||||
.dump();
|
||||
} else {
|
||||
return nlohmann::ordered_json{
|
||||
{"evseId", request.evse_id}, {"transactionId", request.transaction_id}, {"clientId", client_id},
|
||||
{"tariffId", this->config.tariff_id}, {"cableId", this->config.cable_id}, {"userData", ""}}
|
||||
.dump();
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBM400600Controller::set_identification_type(int identification_type) {
|
||||
const std::string payload = nlohmann::ordered_json{{"ocmfId", {{"IT", identification_type}}}}.dump();
|
||||
auto response = this->http_client->put("/v1/settings", payload);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode("/v1/settings", 200, response);
|
||||
}
|
||||
try {
|
||||
bool success = nlohmann::json::parse(response.body).at("result") == 1;
|
||||
if (!success) {
|
||||
throw UnexpectedDCBMResponseBody("/v1/settings",
|
||||
"OCMF Identification Type setting was rejected by the device.");
|
||||
}
|
||||
} catch (nlohmann::json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(
|
||||
"/v1/settings", fmt::format("Json error '{}' for body '{}'", json_error.what(), response.body));
|
||||
}
|
||||
EVLOG_info << "LEM DCBM 400/600: OCMF Identification Type (IT) set to: " << identification_type;
|
||||
}
|
||||
|
||||
int LemDCBM400600Controller::get_identification_type() {
|
||||
auto response = this->http_client->get("/v1/settings");
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode("/v1/settings", 200, response);
|
||||
}
|
||||
try {
|
||||
return nlohmann::json::parse(response.body).at("ocmfId").at("IT").get<int>();
|
||||
} catch (nlohmann::json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(
|
||||
"/v1/settings", fmt::format("Json error '{}' for body '{}'", json_error.what(), response.body));
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<std::string, std::string> LemDCBM400600Controller::get_transaction_stop_time_bounds() {
|
||||
// The LEM DCBM 400/600 Operations manual (7.2.2.) states
|
||||
// "Minimum duration for transactions is 2 minutes, to prevent potential
|
||||
// memory storage weaknesses." Further, the communication protocol states
|
||||
// (4.2.9.): "If after a period of 48h the time was not set, time
|
||||
// synchronization expires (preventing new transactions and invalidating
|
||||
// on-going one)."" Since during an ongoing transaction, now time can synced,
|
||||
// the max duration is set to 48 hours (minus a small delta).
|
||||
auto now = std::chrono::time_point<date::utc_clock>::clock::now();
|
||||
return {
|
||||
Everest::Date::to_rfc3339(now + std::chrono::minutes(2)),
|
||||
Everest::Date::to_rfc3339(now + std::chrono::hours(48) - std::chrono::minutes(1)),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace module::main
|
||||
@@ -0,0 +1,170 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_LEMDCBM400600_H
|
||||
#define EVEREST_CORE_MODULE_LEMDCBM400600_H
|
||||
|
||||
#include "http_client_interface.hpp"
|
||||
#include "lem_dcbm_time_sync_helper.hpp"
|
||||
#include <functional>
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
class LemDCBM400600Controller {
|
||||
|
||||
public:
|
||||
struct Conf {
|
||||
// number of retries to connect to powermeter at initialization
|
||||
const int init_number_of_http_retries;
|
||||
// wait time before each retry during powermeter at initialization
|
||||
const int init_retry_wait_in_milliseconds;
|
||||
// number of retries for failed requests (due to HTTP or device errors) to start or stop a transaction
|
||||
const int transaction_number_of_http_retries;
|
||||
// wait time before each retry for transaction start/stop requests
|
||||
const int transaction_retry_wait_in_milliseconds;
|
||||
// The cable loss compensation level to use. This allows compensating the measurements of the DCBM with a
|
||||
// resistance.
|
||||
const int cable_id;
|
||||
// Used for a unique transaction tariff designation
|
||||
const int tariff_id;
|
||||
// meter time zone
|
||||
const std::string meter_timezone;
|
||||
// the meter Daylight Saving Time (DST) settings
|
||||
const std::string meter_dst;
|
||||
// SC
|
||||
const int SC;
|
||||
// UV
|
||||
const std::string UV;
|
||||
// UD
|
||||
const std::string UD;
|
||||
// OCMF Identification Type (set on device at startup via /settings/ocmfId/IT, -1 = not set)
|
||||
const int IT;
|
||||
// command timeout in milliseconds
|
||||
const int command_timeout_ms;
|
||||
};
|
||||
|
||||
class DCBMUnexpectedResponseException : public std::exception {
|
||||
public:
|
||||
const char* what() {
|
||||
return this->reason.c_str();
|
||||
}
|
||||
|
||||
explicit DCBMUnexpectedResponseException(std::string reason) : reason(std::move(reason)) {
|
||||
}
|
||||
|
||||
private:
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
class UnexpectedDCBMResponseBody : public DCBMUnexpectedResponseException {
|
||||
public:
|
||||
UnexpectedDCBMResponseBody(std::string endpoint, std::string error) :
|
||||
DCBMUnexpectedResponseException(
|
||||
fmt::format("Received unexpected response body from endpoint {}: {}", endpoint, error)),
|
||||
endpoint(std::move(endpoint)),
|
||||
error(std::move(error)) {
|
||||
}
|
||||
|
||||
private:
|
||||
std::string endpoint;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
class UnexpectedDCBMResponseCode : public DCBMUnexpectedResponseException {
|
||||
public:
|
||||
const std::string endpoint;
|
||||
const HttpResponse response;
|
||||
const std::string body;
|
||||
|
||||
UnexpectedDCBMResponseCode(const std::string& endpoint, unsigned int expected_code,
|
||||
const HttpResponse& response) :
|
||||
DCBMUnexpectedResponseException(fmt::format(
|
||||
"Received unexpected response from endpoint '{}': {} (expected {}) {}", endpoint, response.status_code,
|
||||
expected_code, !response.body.empty() ? " - body: " + response.body : "")),
|
||||
endpoint(endpoint),
|
||||
response(response) {
|
||||
}
|
||||
};
|
||||
|
||||
void update_lem_status();
|
||||
|
||||
private:
|
||||
const std::unique_ptr<HttpClientInterface> http_client;
|
||||
std::string meter_id;
|
||||
std::string public_key;
|
||||
std::string public_key_ocmf;
|
||||
std::string version;
|
||||
bool v2_capable = false;
|
||||
bool need_to_stop_transaction = false;
|
||||
std::string current_transaction_id;
|
||||
types::units_signed::SignedMeterValue current_signed_meter_value;
|
||||
std::unique_ptr<LemDCBMTimeSyncHelper> time_sync_helper;
|
||||
Conf config;
|
||||
|
||||
void fetch_meter_id_from_device();
|
||||
int get_identification_type();
|
||||
void set_identification_type(int identification_type);
|
||||
std::string get_current_transaction();
|
||||
void request_device_to_start_transaction(const types::powermeter::TransactionReq& value);
|
||||
void request_device_to_stop_transaction(const std::string& transaction_id);
|
||||
std::string fetch_ocmf_result(const std::string& transaction_id);
|
||||
types::powermeter::Powermeter convert_livemeasure_to_powermeter(const std::string& livemeasure);
|
||||
std::string transaction_start_request_to_dcbm_payload(const types::powermeter::TransactionReq& request);
|
||||
static std::pair<std::string, std::string> get_transaction_stop_time_bounds();
|
||||
|
||||
template <typename Callable>
|
||||
static auto call_with_retry(Callable func, int number_of_retries, int retry_wait_in_milliseconds,
|
||||
bool retry_on_http_client_error = true, bool retry_on_dcbm_reponse_error = true)
|
||||
-> decltype(func()) {
|
||||
std::exception_ptr lastException = nullptr;
|
||||
for (int attempt = 0; attempt < 1 + number_of_retries; ++attempt) {
|
||||
try {
|
||||
return func();
|
||||
} catch (HttpClientError& http_client_error) {
|
||||
lastException = std::current_exception();
|
||||
if (!retry_on_http_client_error) {
|
||||
std::rethrow_exception(lastException);
|
||||
}
|
||||
EVLOG_warning << "HTTPClient request failed: " << http_client_error.what() << "; retry in "
|
||||
<< retry_wait_in_milliseconds << " milliseconds";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_wait_in_milliseconds));
|
||||
} catch (DCBMUnexpectedResponseException& dcbm_error) {
|
||||
lastException = std::current_exception();
|
||||
if (!retry_on_dcbm_reponse_error) {
|
||||
std::rethrow_exception(lastException);
|
||||
}
|
||||
EVLOG_warning << "Unexpected DCBM response: " << dcbm_error.what() << "; retry in "
|
||||
<< retry_wait_in_milliseconds << " milliseconds";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_wait_in_milliseconds));
|
||||
}
|
||||
}
|
||||
std::rethrow_exception(lastException);
|
||||
}
|
||||
|
||||
public:
|
||||
LemDCBM400600Controller() = delete;
|
||||
|
||||
explicit LemDCBM400600Controller(std::unique_ptr<HttpClientInterface> http_client,
|
||||
std::unique_ptr<LemDCBMTimeSyncHelper> time_sync_helper, const Conf& config) :
|
||||
http_client(std::move(http_client)), time_sync_helper(std::move(time_sync_helper)), config(config) {
|
||||
}
|
||||
|
||||
void init();
|
||||
types::powermeter::TransactionStartResponse start_transaction(const types::powermeter::TransactionReq& value);
|
||||
types::powermeter::TransactionStopResponse stop_transaction(const std::string& transaction_id);
|
||||
types::powermeter::Powermeter get_powermeter();
|
||||
inline bool is_initialized() {
|
||||
return ("" != meter_id);
|
||||
}
|
||||
inline std::string get_public_key_ocmf() {
|
||||
return public_key_ocmf;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_LEMDCBM400600_H
|
||||
@@ -0,0 +1,174 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "lem_dcbm_time_sync_helper.hpp"
|
||||
#include "http_client_interface.hpp"
|
||||
#include "lem_dcbm_400600_controller.hpp"
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <date/tz.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <utils/date.hpp>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
std::string LemDCBMTimeSyncHelper::generate_dcbm_ntp_config() {
|
||||
nlohmann::ordered_json config_json = {
|
||||
{"ntp",
|
||||
{{"servers",
|
||||
{{{"ipAddress", ntp_spec.ip_addr_1}, {"port", ntp_spec.port_1}},
|
||||
{{"ipAddress", ntp_spec.ip_addr_2}, {"port", ntp_spec.port_2}}}},
|
||||
{"syncPeriod", 120},
|
||||
{"ntpActivated", true}}},
|
||||
};
|
||||
|
||||
return config_json.dump();
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::set_time_config_params(const std::string& meter_timezone, const std::string& meter_dst) {
|
||||
this->meter_timezone = meter_timezone;
|
||||
this->meter_dst = meter_dst;
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::sync_if_deadline_expired(const HttpClientInterface& httpClient) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(this->time_sync_state_lock);
|
||||
|
||||
if (this->ntp_spec.ntp_enabled && this->dcbm_ntp_settings_saved) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::chrono::steady_clock::now() >= this->deadline_for_next_sync) {
|
||||
try {
|
||||
this->sync(httpClient);
|
||||
} catch (LemDCBM400600Controller::DCBMUnexpectedResponseException& error) {
|
||||
EVLOG_warning << "Failed to sync time settings: " << error.what();
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::sync(const HttpClientInterface& httpClient) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(this->time_sync_state_lock);
|
||||
|
||||
if (this->ntp_spec.ntp_enabled && this->dcbm_ntp_settings_saved) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->ntp_spec.ntp_enabled) {
|
||||
this->set_ntp_settings_on_device(httpClient);
|
||||
this->sync_timezone(httpClient);
|
||||
this->sync_dst(httpClient);
|
||||
} else {
|
||||
this->sync_system_time(httpClient);
|
||||
this->sync_timezone(httpClient);
|
||||
this->sync_dst(httpClient);
|
||||
}
|
||||
}
|
||||
|
||||
bool LemDCBMTimeSyncHelper::is_setting_write_safe() const {
|
||||
if (!this->unsafe_period_start_time.has_value()) {
|
||||
EVLOG_warning << "LEM DCBM 400/600: Time sync was attempted, but the unsafe period start time is not set.";
|
||||
return false;
|
||||
}
|
||||
// According to LEM DCBM manual, no setting should be written earlier than 2 minutes after the DCBM is powered on
|
||||
bool sync_is_too_early = std::chrono::steady_clock::now() <
|
||||
unsafe_period_start_time.value() + timing_constants.min_time_before_setting_write_is_safe;
|
||||
if (sync_is_too_early) {
|
||||
EVLOG_warning << "LEM DCBM 400/600: Time sync was performed earlier than 2 minutes after initialization. "
|
||||
"Time will be synced regardless, but it may not be reliably saved.";
|
||||
}
|
||||
return !sync_is_too_early;
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::set_ntp_settings_on_device(const HttpClientInterface& httpClient) {
|
||||
HttpResponse response = httpClient.put("/v1/settings", this->generate_dcbm_ntp_config());
|
||||
if (response.status_code != 200) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseCode("/v1/settings", 200, response);
|
||||
}
|
||||
bool success = nlohmann::json::parse(response.body).at("result") == 1;
|
||||
if (!success) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseBody(
|
||||
"/v1/settings", "NTP setting was rejected by the device, e.g. because of an ongoing transaction.");
|
||||
}
|
||||
if (!is_setting_write_safe()) {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries;
|
||||
} else {
|
||||
this->dcbm_ntp_settings_saved = true;
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::sync_system_time(const HttpClientInterface& httpClient) {
|
||||
std::string time_update = Everest::Date::to_rfc3339(date::utc_clock::now());
|
||||
std::string payload = R"({"time":{"utc":")" + time_update + R"("}})";
|
||||
HttpResponse response = httpClient.put("/v1/settings", payload);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseCode("/v1/settings", 200, response);
|
||||
}
|
||||
bool success = nlohmann::json::parse(response.body).at("result") == 1;
|
||||
if (!success) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseBody(
|
||||
"/v1/settings", "Time setting was rejected by the device, e.g. because of an ongoing transaction.");
|
||||
}
|
||||
|
||||
if (is_setting_write_safe()) {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.deadline_increment_after_sync;
|
||||
} else {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries;
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::sync_timezone(const HttpClientInterface& httpClient) {
|
||||
std::string payload = std::string(R"({"time": {"tz":")") + meter_timezone + R"("}})";
|
||||
HttpResponse response = httpClient.put("/v1/settings", payload);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseCode("/v1/settings", 200, response);
|
||||
}
|
||||
bool success = nlohmann::json::parse(response.body).at("result") == 1;
|
||||
if (!success) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseBody(
|
||||
"/v1/settings", "Timezone setting was rejected by the device, e.g. because of an ongoing transaction.");
|
||||
}
|
||||
|
||||
if (is_setting_write_safe()) {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.deadline_increment_after_sync;
|
||||
} else {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries;
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::sync_dst(const HttpClientInterface& httpClient) {
|
||||
std::string payload = std::string(R"({"time": {"dst":)") + meter_dst + R"(}})";
|
||||
HttpResponse response = httpClient.put("/v1/settings", payload);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseCode("/v1/settings", 200, response);
|
||||
}
|
||||
bool success = nlohmann::json::parse(response.body).at("result") == 1;
|
||||
if (!success) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseBody(
|
||||
"/v1/settings",
|
||||
"Daylight saving setting was rejected by the device, e.g. because of an ongoing transaction.");
|
||||
}
|
||||
|
||||
if (is_setting_write_safe()) {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.deadline_increment_after_sync;
|
||||
} else {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries;
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::restart_unsafe_period() {
|
||||
this->unsafe_period_start_time = std::chrono::steady_clock::now();
|
||||
deadline_for_next_sync = unsafe_period_start_time.value() + timing_constants.min_time_before_setting_write_is_safe;
|
||||
}
|
||||
} // namespace module::main
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_LEM_DCBM_TIME_SYNC_HELPER_H
|
||||
#define EVEREST_CORE_MODULE_LEM_DCBM_TIME_SYNC_HELPER_H
|
||||
|
||||
#include "http_client_interface.hpp"
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
struct timing_config {
|
||||
// This is the time after powerup after which the DCBM guarantees that writing to settings will be reliable
|
||||
// Make sure to re-attempt any settings writes after this time has passed
|
||||
std::chrono::seconds min_time_before_setting_write_is_safe = std::chrono::minutes(2);
|
||||
// When performing regular syncs (e.g. as part of the livemeasure loop), this is the minimum duration between
|
||||
// retries.
|
||||
std::chrono::seconds min_time_between_sync_retries = std::chrono::minutes(1);
|
||||
// When a sync is successful, advance the deadline for regular syncs by this much:
|
||||
std::chrono::seconds deadline_increment_after_sync = std::chrono::hours(24);
|
||||
};
|
||||
|
||||
struct ntp_server_spec {
|
||||
const std::string ip_addr_1;
|
||||
const int port_1 = 123;
|
||||
const std::string ip_addr_2;
|
||||
const int port_2 = 123;
|
||||
const bool ntp_enabled = !ip_addr_1.empty();
|
||||
};
|
||||
|
||||
class LemDCBMTimeSyncHelper {
|
||||
|
||||
public:
|
||||
LemDCBMTimeSyncHelper() = delete;
|
||||
|
||||
explicit LemDCBMTimeSyncHelper(ntp_server_spec ntp_spec) :
|
||||
LemDCBMTimeSyncHelper(std::move(ntp_spec), timing_config{}) {
|
||||
}
|
||||
|
||||
explicit LemDCBMTimeSyncHelper(ntp_server_spec ntp_spec, timing_config tc) :
|
||||
ntp_spec(std::move(ntp_spec)),
|
||||
timing_constants(tc),
|
||||
meter_timezone(""),
|
||||
meter_dst(""),
|
||||
unsafe_period_start_time({}) {
|
||||
}
|
||||
|
||||
virtual ~LemDCBMTimeSyncHelper() = default;
|
||||
|
||||
void set_time_config_params(const std::string& meter_timezone, const std::string& meter_dst);
|
||||
|
||||
virtual void sync_if_deadline_expired(const HttpClientInterface& httpClient);
|
||||
|
||||
virtual void sync(const HttpClientInterface& httpClient);
|
||||
|
||||
virtual void restart_unsafe_period();
|
||||
|
||||
private:
|
||||
// CONFIGURATION VARIABLES
|
||||
const ntp_server_spec ntp_spec;
|
||||
// Timing constants (can be overridden in a special constructor, e.g. during testing)
|
||||
const timing_config timing_constants;
|
||||
// the meter timezone
|
||||
std::string meter_timezone;
|
||||
// the meter daylight saving time definition
|
||||
std::string meter_dst;
|
||||
|
||||
// RUNNING VARIABLES
|
||||
// The helper can be accessed by multiple threads, so we use a mutex to protect the data below
|
||||
std::recursive_mutex time_sync_state_lock;
|
||||
std::chrono::time_point<std::chrono::steady_clock> deadline_for_next_sync;
|
||||
std::optional<std::chrono::time_point<std::chrono::steady_clock>> unsafe_period_start_time;
|
||||
// True whenever the NTP config is successfully written to the device after min_time_before_setting_write_is_safe
|
||||
// has passed
|
||||
bool dcbm_ntp_settings_saved = false;
|
||||
|
||||
// sync_is_too_early is set if this is done earlier than MIN_TIME_BEFORE_SETTING_WRITE_IS_SAFE after init
|
||||
void set_ntp_settings_on_device(const HttpClientInterface& httpClient);
|
||||
|
||||
// sync_is_too_early is set if this is done earlier than MIN_TIME_BEFORE_SETTING_WRITE_IS_SAFE after init
|
||||
void sync_system_time(const HttpClientInterface& httpClient);
|
||||
void sync_timezone(const HttpClientInterface& httpClient);
|
||||
void sync_dst(const HttpClientInterface& httpClient);
|
||||
|
||||
std::string generate_dcbm_ntp_config();
|
||||
[[nodiscard]] bool is_setting_write_safe() const;
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_LEM_DCBM_TIME_SYNC_HELPER_H
|
||||
@@ -0,0 +1,150 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "powermeterImpl.hpp"
|
||||
#include "http_client.hpp"
|
||||
#include "lem_dcbm_time_sync_helper.hpp"
|
||||
#include <chrono>
|
||||
#include <everest/logging.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
void powermeterImpl::init() {
|
||||
// Dependency injection pattern: Create the HTTP client first,
|
||||
// then move it into the controller as a constructor argument
|
||||
auto http_client = std::make_unique<HttpClient>(mod->config.ip_address, mod->config.port,
|
||||
mod->config.meter_tls_certificate, mod->config.interface);
|
||||
|
||||
auto ntp_server_spec =
|
||||
module::main::ntp_server_spec{mod->config.ntp_server_1_ip_addr, mod->config.ntp_server_1_port,
|
||||
mod->config.ntp_server_2_ip_addr, mod->config.ntp_server_2_port};
|
||||
|
||||
this->controller = std::make_unique<LemDCBM400600Controller>(
|
||||
std::move(http_client), std::make_unique<LemDCBMTimeSyncHelper>(ntp_server_spec),
|
||||
LemDCBM400600Controller::Conf{
|
||||
mod->config.resilience_initial_connection_retries, mod->config.resilience_initial_connection_retry_delay,
|
||||
mod->config.resilience_transaction_request_retries, mod->config.resilience_transaction_request_retry_delay,
|
||||
mod->config.cable_id, mod->config.tariff_id, mod->config.meter_timezone, mod->config.meter_dst,
|
||||
mod->config.SC, mod->config.UV, mod->config.UD, mod->config.IT, mod->config.command_timeout_ms});
|
||||
|
||||
// Validate and normalize temperature thresholds for the monitor.
|
||||
// If the error level is configured below the warning level, clamp it and log a warning.
|
||||
double warning_level_C = mod->config.temperature_warning_level_C;
|
||||
double error_level_C = mod->config.temperature_error_level_C;
|
||||
if (error_level_C < warning_level_C) {
|
||||
EVLOG_warning << "LEM DCBM 400/600: temperature_error_level_C (" << error_level_C
|
||||
<< " °C) is below temperature_warning_level_C (" << warning_level_C
|
||||
<< " °C). Clamping error level to the warning level.";
|
||||
error_level_C = warning_level_C;
|
||||
}
|
||||
|
||||
this->temperature_monitor = std::make_unique<TemperatureMonitor>(
|
||||
TemperatureMonitor::Config{warning_level_C, error_level_C, mod->config.temperature_hysteresis_K,
|
||||
std::chrono::milliseconds(mod->config.temperature_min_time_as_valid_ms)});
|
||||
}
|
||||
|
||||
void powermeterImpl::ready() {
|
||||
// Start the live_measure_publisher thread, which periodically publishes the live measurements of the device
|
||||
this->live_measure_publisher_thread = std::thread([this] {
|
||||
while (true) {
|
||||
try {
|
||||
if (!this->controller->is_initialized()) {
|
||||
this->controller->init();
|
||||
this->publish_public_key_ocmf(this->controller->get_public_key_ocmf());
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(mod->config.resilience_initial_connection_retry_delay));
|
||||
} else {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
auto powermeter_data = this->controller->get_powermeter();
|
||||
this->publish_powermeter(powermeter_data);
|
||||
// if the communication error is set, clear the error
|
||||
if (this->error_state_monitor->is_error_active("powermeter/CommunicationFault",
|
||||
"Communication timed out")) {
|
||||
// need to update LEM status since we have recovered from a communication loss
|
||||
this->controller->update_lem_status();
|
||||
clear_error("powermeter/CommunicationFault", "Communication timed out");
|
||||
}
|
||||
|
||||
// Evaluate temperature thresholds
|
||||
if (powermeter_data.temperatures.has_value() && powermeter_data.temperatures->size() >= 2) {
|
||||
const double temp_H = powermeter_data.temperatures->at(0).temperature;
|
||||
const double temp_L = powermeter_data.temperatures->at(1).temperature;
|
||||
auto events = this->temperature_monitor->update(temp_H, temp_L);
|
||||
handle_temperature_events(events, this->temperature_monitor->last_max_temperature());
|
||||
}
|
||||
}
|
||||
} catch (LemDCBM400600Controller::DCBMUnexpectedResponseException& error) {
|
||||
EVLOG_error << "LEM DCBM 400/600: Failed to execute the powermeter ready loop due to an invalid device "
|
||||
"response: "
|
||||
<< error.what();
|
||||
} catch (HttpClientError& client_error) {
|
||||
if (!this->error_state_monitor->is_error_active("powermeter/CommunicationFault",
|
||||
"Communication timed out")) {
|
||||
EVLOG_error << "Failed to communicate with the powermeter due to http error: "
|
||||
<< client_error.what();
|
||||
auto error =
|
||||
this->error_factory->create_error("powermeter/CommunicationFault", "Communication timed out",
|
||||
"This error is raised due to communication timeout");
|
||||
raise_error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStartResponse
|
||||
powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& value) {
|
||||
if (!this->controller->is_initialized()) {
|
||||
return types::powermeter::TransactionStartResponse{
|
||||
types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, "Powermeter is not initialized"};
|
||||
}
|
||||
return this->controller->start_transaction(value);
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transaction(std::string& transaction_id) {
|
||||
if (!this->controller->is_initialized()) {
|
||||
return types::powermeter::TransactionStopResponse{
|
||||
types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, "Powermeter is not initialized"};
|
||||
}
|
||||
return this->controller->stop_transaction(transaction_id);
|
||||
}
|
||||
|
||||
void powermeterImpl::handle_temperature_events(const TemperatureMonitor::Events& events, double max_temperature) {
|
||||
if (events.warning_raised) {
|
||||
EVLOG_warning << fmt::format(
|
||||
"LEM DCBM 400/600: Temperature warning raised — max temperature {:.1f} °C exceeds warning level {:.1f} °C",
|
||||
max_temperature, mod->config.temperature_warning_level_C);
|
||||
auto error =
|
||||
this->error_factory->create_error("powermeter/VendorWarning", "TemperatureWarning",
|
||||
fmt::format("Max temperature {:.1f} °C exceeds warning level {:.1f} °C",
|
||||
max_temperature, mod->config.temperature_warning_level_C));
|
||||
raise_error(error);
|
||||
}
|
||||
if (events.warning_cleared) {
|
||||
EVLOG_info << fmt::format(
|
||||
"LEM DCBM 400/600: Temperature warning cleared — max temperature {:.1f} °C dropped below {:.1f} °C",
|
||||
max_temperature, mod->config.temperature_warning_level_C - mod->config.temperature_hysteresis_K);
|
||||
clear_error("powermeter/VendorWarning", "TemperatureWarning");
|
||||
}
|
||||
if (events.error_raised) {
|
||||
EVLOG_error << fmt::format(
|
||||
"LEM DCBM 400/600: Temperature error raised — max temperature {:.1f} °C exceeds error level {:.1f} °C",
|
||||
max_temperature, mod->config.temperature_error_level_C);
|
||||
auto error =
|
||||
this->error_factory->create_error("powermeter/VendorError", "TemperatureError",
|
||||
fmt::format("Max temperature {:.1f} °C exceeds error level {:.1f} °C",
|
||||
max_temperature, mod->config.temperature_error_level_C));
|
||||
raise_error(error);
|
||||
}
|
||||
if (events.error_cleared) {
|
||||
EVLOG_info << fmt::format(
|
||||
"LEM DCBM 400/600: Temperature error cleared — max temperature {:.1f} °C dropped below {:.1f} °C",
|
||||
max_temperature, mod->config.temperature_error_level_C - mod->config.temperature_hysteresis_K);
|
||||
clear_error("powermeter/VendorError", "TemperatureError");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace module::main
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user