Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

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

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 meters 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
```

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

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

View File

@@ -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"}

View File

@@ -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()

View File

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

View File

@@ -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}"

View File

@@ -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);
}

View File

@@ -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());
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,11 @@
load("//modules:module.bzl", "cc_everest_module")
IMPLS = [
"main",
]
cc_everest_module(
name = "GenericPowermeter",
deps = [],
impls = IMPLS,
)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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`

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>`_

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>`_

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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