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