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

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 "../LemDCBM400600.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
#include "http_client_interface.hpp"
#include "lem_dcbm_400600_controller.hpp"
#include "temperature_monitor.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<LemDCBM400600>& 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<LemDCBM400600>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
// insert your private definitions here
// 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<LemDCBM400600Controller> 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;
// Temperature monitoring with warning/error thresholds and hysteresis
std::unique_ptr<TemperatureMonitor> temperature_monitor;
void handle_temperature_events(const TemperatureMonitor::Events& events, double max_temperature);
// 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,98 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef LEM_DCBM_TEMPERATURE_MONITOR_HPP
#define LEM_DCBM_TEMPERATURE_MONITOR_HPP
#include <algorithm>
#include <chrono>
#include <optional>
namespace module::main {
/// Monitors temperature readings against warning and error thresholds
/// with hysteresis and a minimum exceedance duration before raising events.
class TemperatureMonitor {
public:
struct Config {
double warning_level_C;
double error_level_C;
double hysteresis_K;
std::chrono::milliseconds min_time_as_valid;
};
struct Events {
bool warning_raised{false};
bool warning_cleared{false};
bool error_raised{false};
bool error_cleared{false};
};
explicit TemperatureMonitor(const Config& config) : config_(config) {
}
/// Feed new temperature readings and get back any state-change events.
/// The evaluation uses max(temperature_H, temperature_L).
Events update(double temperature_H_C, double temperature_L_C) {
last_max_temperature_ = std::max(temperature_H_C, temperature_L_C);
const auto now = std::chrono::steady_clock::now();
Events events;
evaluate_level(warning_active_, warning_exceeded_since_, config_.warning_level_C, now, events.warning_raised,
events.warning_cleared);
evaluate_level(error_active_, error_exceeded_since_, config_.error_level_C, now, events.error_raised,
events.error_cleared);
return events;
}
/// Returns the current max temperature from the last update (for logging).
[[nodiscard]] double last_max_temperature() const {
return last_max_temperature_;
}
private:
Config config_;
bool warning_active_{false};
std::optional<std::chrono::steady_clock::time_point> warning_exceeded_since_;
bool error_active_{false};
std::optional<std::chrono::steady_clock::time_point> error_exceeded_since_;
double last_max_temperature_{0.0};
void evaluate_level(bool& active, std::optional<std::chrono::steady_clock::time_point>& exceeded_since,
double threshold, std::chrono::steady_clock::time_point now, bool& raised_event,
bool& cleared_event) {
if (!active) {
// Not yet active — check if we should start or continue timing
if (last_max_temperature_ >= threshold) {
if (!exceeded_since.has_value()) {
// First time exceeding: start the timer
exceeded_since = now;
}
// Check if minimum exceedance duration has elapsed
if ((now - exceeded_since.value()) >= config_.min_time_as_valid) {
active = true;
exceeded_since.reset();
raised_event = true;
}
} else {
// Temperature dropped below threshold before timer expired — reset
exceeded_since.reset();
}
} else {
// Active — check if we should clear (with hysteresis)
if (last_max_temperature_ < (threshold - config_.hysteresis_K)) {
active = false;
exceeded_since.reset();
cleared_event = true;
}
}
}
};
} // namespace module::main
#endif // LEM_DCBM_TEMPERATURE_MONITOR_HPP