Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "http_client.hpp"
|
||||
#include <fmt/core.h>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace module::main {
|
||||
const char* CONTENT_TYPE_HEADER = "Content-Type: application/json";
|
||||
|
||||
struct payloadInTransit {
|
||||
const std::string& data;
|
||||
size_t position;
|
||||
};
|
||||
|
||||
// Callback for receiving data, saves it into a string
|
||||
static size_t receive_data(char* ptr, size_t size, size_t nmemb, std::string* received_data) {
|
||||
received_data->append(ptr, size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
// Callback for sending data, fetches it from a string
|
||||
static size_t send_data(char* buffer, size_t size, size_t nitems, struct payloadInTransit* payload) {
|
||||
if (payload->position >= payload->data.length()) {
|
||||
// Returning 0 signals to libcurl that we have no more data to send
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Send up to size*nitems bytes of data
|
||||
size_t payload_remaining_bytes = payload->data.length() - payload->position;
|
||||
size_t num_bytes_to_send = std::min(size * nitems, payload_remaining_bytes);
|
||||
std::memcpy(buffer, payload->data.c_str() + payload->position, num_bytes_to_send);
|
||||
payload->position += num_bytes_to_send;
|
||||
return num_bytes_to_send;
|
||||
}
|
||||
|
||||
static HttpClientError client_error(const std::string& host, unsigned int port, const char* method,
|
||||
const std::string& path, const std::string& message) {
|
||||
return HttpClientError(fmt::format("HTTP client error on {} {}:{}{} : {} ", method, host, port, path, message));
|
||||
}
|
||||
|
||||
static void setup_connection(CURL* connection, struct payloadInTransit& request_payload, std::string& response_body,
|
||||
curl_slist*& headers, const int command_timeout_ms) {
|
||||
// Override the Content-Type header
|
||||
headers = curl_slist_append(nullptr, CONTENT_TYPE_HEADER);
|
||||
if (curl_easy_setopt(connection, CURLOPT_HTTPHEADER, headers) != CURLE_OK) {
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
|
||||
// Set up callbacks for reading and writing
|
||||
curl_easy_setopt(connection, CURLOPT_WRITEFUNCTION, receive_data);
|
||||
curl_easy_setopt(connection, CURLOPT_WRITEDATA, &response_body);
|
||||
curl_easy_setopt(connection, CURLOPT_READFUNCTION, send_data);
|
||||
curl_easy_setopt(connection, CURLOPT_READDATA, &request_payload);
|
||||
curl_easy_setopt(connection, CURLOPT_TIMEOUT_MS, command_timeout_ms);
|
||||
|
||||
// Misc. settings come here
|
||||
curl_easy_setopt(connection, CURLOPT_FORBID_REUSE, 1);
|
||||
if (curl_easy_setopt(connection, CURLOPT_FOLLOWLOCATION, 0) != CURLE_OK) {
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
}
|
||||
|
||||
static void setup_libcurl_tls_options_for_connection(CURL* connection, struct curl_blob* dcbm_cert) {
|
||||
// Since the LEM DCBM uses a certificate of only 1024bit, we need to lower the security level
|
||||
curl_easy_setopt(connection, CURLOPT_SSL_CIPHER_LIST, "DEFAULT:@SECLEVEL=1");
|
||||
// However, we still want to enforce TLS 1.2 or higher
|
||||
if (curl_easy_setopt(connection, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_TLSv1_3) !=
|
||||
CURLE_OK) {
|
||||
throw std::runtime_error("Failed to set CURLOPT_SSLVERSION. Is libcurl built with TLS support?");
|
||||
}
|
||||
|
||||
// We do not want to verify the hostname
|
||||
if (curl_easy_setopt(connection, CURLOPT_SSL_VERIFYHOST, 0) != CURLE_OK) {
|
||||
throw std::runtime_error("Failed to set CURLOPT_SSL_VERIFYHOST. Is libcurl built with TLS support?");
|
||||
}
|
||||
// We do want to verify the peer's certificate
|
||||
if (curl_easy_setopt(connection, CURLOPT_SSL_VERIFYPEER, 1) != CURLE_OK) {
|
||||
throw std::runtime_error("Failed to set CURLOPT_SSL_VERIFYPEER. Is libcurl built with TLS support?");
|
||||
}
|
||||
// We do not want to use OCSP
|
||||
// Whether this option is supported or not depends on the SSL backend, so we don't check the error code here.
|
||||
curl_easy_setopt(connection, CURLOPT_SSL_VERIFYSTATUS, 0);
|
||||
|
||||
// Now pass the DCBM certificate to libcurl
|
||||
if (curl_easy_setopt(connection, CURLOPT_CAINFO_BLOB, dcbm_cert) != CURLE_OK) {
|
||||
throw std::runtime_error("Failed to set CURLOPT_CAINFO_BLOB, possibly due to running out of memory.");
|
||||
}
|
||||
}
|
||||
|
||||
// Note: method_name and path are only there for the error message
|
||||
HttpResponse HttpClient::perform_request(CURL* connection, const std::string& request_body, const char* method_name,
|
||||
const std::string& path) const {
|
||||
// give curl a buffer to write its error messages to
|
||||
char curl_error_message[CURL_ERROR_SIZE] = {};
|
||||
curl_easy_setopt(connection, CURLOPT_ERRORBUFFER, curl_error_message);
|
||||
|
||||
// set up the connection options
|
||||
std::string response_body;
|
||||
struct payloadInTransit request_payload {
|
||||
request_body, 0
|
||||
};
|
||||
struct curl_slist* headers;
|
||||
setup_connection(connection, request_payload, response_body, headers, command_timeout_ms);
|
||||
|
||||
// Set up TLS options if TLS is enabled
|
||||
// we define dcbm_cert outside the "if" statement to ensure it outlives curl_easy_perform().
|
||||
struct curl_blob dcbm_cert {
|
||||
(void*)this->dcbm_tls_certificate.c_str(), this->dcbm_tls_certificate.size(),
|
||||
CURL_BLOB_NOCOPY // curl does not need to copy the cert, since it's not in a temporary location
|
||||
};
|
||||
if (this->tls_enabled) {
|
||||
setup_libcurl_tls_options_for_connection(connection, &dcbm_cert);
|
||||
}
|
||||
|
||||
// perform the request
|
||||
CURLcode res = curl_easy_perform(connection);
|
||||
|
||||
// remember to free the headers list...
|
||||
curl_slist_free_all(headers);
|
||||
// check the result of the request and return
|
||||
if (res == CURLE_OK) {
|
||||
long response_code;
|
||||
curl_easy_getinfo(connection, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
return HttpResponse{(unsigned int)response_code, std::move(response_body)};
|
||||
} else {
|
||||
throw client_error(this->host, this->port, method_name, path, std::string(curl_error_message));
|
||||
}
|
||||
}
|
||||
|
||||
CURL* HttpClient::create_curl_handle_and_setup_url(const std::string& path) const {
|
||||
CURL* connection = curl_easy_init();
|
||||
if (!connection) {
|
||||
throw std::runtime_error("Could not create a CURL handle: curl_easy_init() returned null");
|
||||
}
|
||||
const char* protocol = this->tls_enabled ? "https" : "http";
|
||||
if (curl_easy_setopt(connection, CURLOPT_URL,
|
||||
fmt::format("{}://{}:{}{}", protocol, this->host, this->port, path).c_str()) != CURLE_OK) {
|
||||
throw std::runtime_error("Could not set CURLOPT_URL, likely ran out of memory");
|
||||
}
|
||||
if (curl_easy_setopt(connection, CURLOPT_PROTOCOLS_STR, protocol) != CURLE_OK) {
|
||||
throw std::runtime_error(std::string("Could not set supported protocol to ") + protocol +
|
||||
", is it enabled in libcurl?");
|
||||
}
|
||||
if (!this->network_interface.empty()) {
|
||||
if (curl_easy_setopt(connection, CURLOPT_INTERFACE, this->network_interface.c_str()) != CURLE_OK) {
|
||||
throw std::runtime_error("Could not bind to the specified network interface: " + this->network_interface);
|
||||
}
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::get(const std::string& path) const {
|
||||
CURL* connection = this->create_curl_handle_and_setup_url(path);
|
||||
|
||||
if (curl_easy_setopt(connection, CURLOPT_HTTPGET, 1) != CURLE_OK) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
|
||||
// perform_request() does not cleanup the connection on its own.
|
||||
// We do the cleanup here, and make sure to rethrow any exception that might've occurred.
|
||||
try {
|
||||
HttpResponse response = perform_request(connection, "", "GET", path);
|
||||
curl_easy_cleanup(connection);
|
||||
return response;
|
||||
} catch (std::exception& e) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::put(const std::string& path, const std::string& body) const {
|
||||
CURL* connection = this->create_curl_handle_and_setup_url(path);
|
||||
|
||||
curl_easy_setopt(connection, CURLOPT_UPLOAD, 1);
|
||||
|
||||
// perform_request() does not cleanup the connection on its own.
|
||||
// We do the cleanup here, and make sure to rethrow any exception that might've occurred.
|
||||
try {
|
||||
HttpResponse response = perform_request(connection, body, "PUT", path);
|
||||
curl_easy_cleanup(connection);
|
||||
return response;
|
||||
} catch (std::exception& e) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::post(const std::string& path, const std::string& body) const {
|
||||
CURL* connection = this->create_curl_handle_and_setup_url(path);
|
||||
|
||||
if (curl_easy_setopt(connection, CURLOPT_POST, 1) != CURLE_OK) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
|
||||
// perform_request() does not cleanup the connection on its own.
|
||||
// We do the cleanup here, and make sure to rethrow any exception that might've occurred.
|
||||
try {
|
||||
HttpResponse response = perform_request(connection, body, "POST", path);
|
||||
curl_easy_cleanup(connection);
|
||||
return response;
|
||||
} catch (std::exception& e) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
} // namespace module::main
|
||||
@@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_HTTPCLIENT_H
|
||||
#define EVEREST_CORE_MODULE_HTTPCLIENT_H
|
||||
|
||||
#include "fmt/format.h"
|
||||
#include "http_client_interface.hpp"
|
||||
#include <curl/curl.h>
|
||||
#include <everest/logging.hpp>
|
||||
#include <regex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
// The DCBM does not print its certificate correctly in its /certificate API.
|
||||
// In particular, the newlines after -----BEGIN CERTIFICATE----- and before -----END CERTIFICATE----- are missing there.
|
||||
// This function will add these newlines if they are missing.
|
||||
static void fixup_tls_certificate(std::string& tls_certificate) {
|
||||
tls_certificate = std::regex_replace(tls_certificate, std::regex("-----BEGIN CERTIFICATE-----\\s*([^\n])"),
|
||||
"-----BEGIN CERTIFICATE-----\n$1");
|
||||
tls_certificate = std::regex_replace(tls_certificate, std::regex("([^\n])\\s*-----END CERTIFICATE-----"),
|
||||
"$1\n-----END CERTIFICATE-----");
|
||||
}
|
||||
|
||||
class HttpClient : public HttpClientInterface {
|
||||
|
||||
public:
|
||||
HttpClient() = delete;
|
||||
|
||||
HttpClient(const std::string& host_arg, int port_arg, const std::string& tls_certificate,
|
||||
const std::string& network_interface = "") {
|
||||
// initialize libcurl - this is safe to do multiple times, if there are multiple HttpClients
|
||||
// Note: This is only thread-safe after libcurl 7.84.0, but we use 8.4.0, so it should be fine
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
// These are saved in the client to avoid making the controller pass them at every call
|
||||
host = host_arg;
|
||||
port = port_arg;
|
||||
dcbm_tls_certificate = tls_certificate;
|
||||
tls_enabled = !dcbm_tls_certificate.empty();
|
||||
fixup_tls_certificate(dcbm_tls_certificate);
|
||||
this->network_interface = network_interface;
|
||||
}
|
||||
|
||||
~HttpClient() override {
|
||||
// release the libcurl resources - this must be done once for every call to curl_global_init().
|
||||
// Note: This is only thread-safe after libcurl 7.84.0, but we use 8.4.0, so it should be fine
|
||||
curl_global_cleanup();
|
||||
}
|
||||
|
||||
void set_command_timeout(const int command_timeout_ms) override {
|
||||
this->command_timeout_ms = command_timeout_ms;
|
||||
}
|
||||
[[nodiscard]] HttpResponse get(const std::string& path) const override;
|
||||
[[nodiscard]] HttpResponse put(const std::string& path, const std::string& body) const override;
|
||||
[[nodiscard]] HttpResponse post(const std::string& path, const std::string& body) const override;
|
||||
|
||||
private:
|
||||
std::string host;
|
||||
int port;
|
||||
bool tls_enabled;
|
||||
std::string dcbm_tls_certificate;
|
||||
int command_timeout_ms = 5000; // default timeout in milliseconds
|
||||
std::string network_interface; // network interface
|
||||
|
||||
[[nodiscard]] CURL* create_curl_handle_and_setup_url(const std::string& path) const;
|
||||
HttpResponse perform_request(CURL* connection, const std::string& request_body, const char* method_name,
|
||||
const std::string& path) const;
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_HTTPCLIENT_H
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_HTTP_CLIENT_INTERFACE_H
|
||||
#define EVEREST_CORE_MODULE_HTTP_CLIENT_INTERFACE_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
class HttpClientError : public std::exception {
|
||||
public:
|
||||
[[nodiscard]] const char* what() const noexcept override {
|
||||
return this->reason.c_str();
|
||||
}
|
||||
explicit HttpClientError(std::string msg) {
|
||||
this->reason = std::move(msg);
|
||||
}
|
||||
explicit HttpClientError(const char* msg) {
|
||||
this->reason = std::string(msg);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct HttpResponse {
|
||||
unsigned int status_code;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
struct HttpClientInterface {
|
||||
|
||||
virtual ~HttpClientInterface() = default;
|
||||
|
||||
virtual void set_command_timeout(const int command_timeout_ms){};
|
||||
[[nodiscard]] virtual HttpResponse get(const std::string& path) const = 0;
|
||||
[[nodiscard]] virtual HttpResponse put(const std::string& path, const std::string& body) const = 0;
|
||||
[[nodiscard]] virtual HttpResponse post(const std::string& path, const std::string& body) const = 0;
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_HTTP_CLIENT_INTERFACE_H
|
||||
@@ -0,0 +1,409 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "lem_dcbm_400600_controller.hpp"
|
||||
#include <stdexcept>
|
||||
namespace module::main {
|
||||
|
||||
void LemDCBM400600Controller::init() {
|
||||
EVLOG_info << "LEM DCBM 400/600: Try to communicate with the device to initialize it.";
|
||||
this->time_sync_helper->set_time_config_params(config.meter_timezone, config.meter_dst);
|
||||
this->http_client->set_command_timeout(this->config.command_timeout_ms);
|
||||
|
||||
if (this->config.IT >= 0) {
|
||||
call_with_retry(
|
||||
[this]() {
|
||||
const int current_it = this->get_identification_type();
|
||||
if (current_it != this->config.IT) {
|
||||
EVLOG_info << "LEM DCBM 400/600: Setting OCMF Identification Type (IT) to: " << this->config.IT;
|
||||
this->set_identification_type(this->config.IT);
|
||||
} else {
|
||||
EVLOG_info << "LEM DCBM 400/600: OCMF Identification Type (IT) already set to " << current_it
|
||||
<< ", skipping write";
|
||||
}
|
||||
},
|
||||
this->config.init_number_of_http_retries, this->config.init_retry_wait_in_milliseconds);
|
||||
}
|
||||
|
||||
call_with_retry([this]() { this->fetch_meter_id_from_device(); }, this->config.init_number_of_http_retries,
|
||||
this->config.init_retry_wait_in_milliseconds);
|
||||
|
||||
this->time_sync_helper->restart_unsafe_period();
|
||||
EVLOG_info << "LEM DCBM 400/600: Device initialized successfully.";
|
||||
}
|
||||
|
||||
std::vector<std::string> split(const std::string& str, char delimiter) {
|
||||
std::vector<std::string> tokens;
|
||||
std::string token;
|
||||
std::stringstream ss(str);
|
||||
|
||||
while (std::getline(ss, token, delimiter)) {
|
||||
tokens.push_back(token);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
std::string LemDCBM400600Controller::get_current_transaction() {
|
||||
const std::string endpoint = v2_capable ? "/v2/legal" : "/v1/legal";
|
||||
auto response = this->http_client->get(endpoint);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode(endpoint, 200, response);
|
||||
}
|
||||
try {
|
||||
json data = json::parse(response.body);
|
||||
return data.at("transactionId");
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(endpoint, fmt::format("Json error '{}'", json_error.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBM400600Controller::update_lem_status() {
|
||||
// should call this after a communication error to figure out what has been happening
|
||||
auto status_response = this->http_client->get("/v1/status");
|
||||
|
||||
if (status_response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode("/v1/status", 200, status_response);
|
||||
}
|
||||
try {
|
||||
json data = json::parse(status_response.body);
|
||||
this->need_to_stop_transaction = data.at("status").at("bits").at("transactionIsOnGoing");
|
||||
this->current_transaction_id = get_current_transaction();
|
||||
if (this->need_to_stop_transaction) {
|
||||
// we need to get the current transaction id or the last known transaction id since we might
|
||||
// receive a stop transaction with id 0 or with the last known transaction if
|
||||
// meaning that the system had a failure and the transaction was started but id was not saved
|
||||
// and we try to recover from the error, thus we need to cancel the transaction
|
||||
EVLOG_warning << "LEM DCBM 400/600: A transaction is already ongoing and it has the id:"
|
||||
<< this->current_transaction_id;
|
||||
} else {
|
||||
EVLOG_info << "LEM DCBM 400/600: The last known transaction has the id:" << this->current_transaction_id;
|
||||
}
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(
|
||||
"/v1/status", fmt::format("Json error {} for body {}", json_error.what(), status_response.body));
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBM400600Controller::fetch_meter_id_from_device() {
|
||||
auto status_response = this->http_client->get("/v1/status");
|
||||
|
||||
if (status_response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode("/v1/status", 200, status_response);
|
||||
}
|
||||
try {
|
||||
json data = json::parse(status_response.body);
|
||||
this->meter_id = data.at("meterId");
|
||||
this->public_key_ocmf = data.at("publicKeyOcmf");
|
||||
this->need_to_stop_transaction = data.at("status").at("bits").at("transactionIsOnGoing");
|
||||
std::string version = data.at("version").at("applicationFirmwareVersion");
|
||||
auto components = split(version, '.');
|
||||
this->v2_capable =
|
||||
((components.size() == 4) && (components[1] > "1")); // the major version must be newer than 1
|
||||
this->current_transaction_id = get_current_transaction();
|
||||
if (this->need_to_stop_transaction) {
|
||||
// we need to get the current transaction id or the last known transaction id since we might
|
||||
// receive a stop transaction with id 0 or with the last known transaction if
|
||||
// meaning that the system had a failure and the transaction was started but id was not saved
|
||||
// and we try to recover from the error, thus we need to cancel the transaction
|
||||
EVLOG_warning << "LEM DCBM 400/600: A transaction is already ongoing and it has the id:"
|
||||
<< this->current_transaction_id;
|
||||
} else {
|
||||
EVLOG_info << "LEM DCBM 400/600: The last known transaction has the id:" << this->current_transaction_id;
|
||||
}
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(
|
||||
"/v1/status", fmt::format("Json error {} for body {}", json_error.what(), status_response.body));
|
||||
}
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStartResponse
|
||||
LemDCBM400600Controller::start_transaction(const types::powermeter::TransactionReq& value) {
|
||||
try {
|
||||
if (this->need_to_stop_transaction) {
|
||||
// there is already an ongoing transaction, something went wrong, we will clean
|
||||
// the current transaction
|
||||
EVLOG_error << "LEM DCBM 400/600: A transaction with the id " << this->current_transaction_id
|
||||
<< "already exists but the system is trying to start a new transaction with the id:"
|
||||
<< value.transaction_id << ", try to recover by closing the current transaction";
|
||||
try {
|
||||
// we will not return any response to stop transaction since this is a self triggered command
|
||||
this->request_device_to_stop_transaction(this->current_transaction_id);
|
||||
this->need_to_stop_transaction = false;
|
||||
} catch (UnexpectedDCBMResponseCode& error) {
|
||||
EVLOG_error << "LEM DCBM 400/600: Could not close the current transaction, got error:" << error.what();
|
||||
}
|
||||
}
|
||||
call_with_retry([this, value]() { this->request_device_to_start_transaction(value); },
|
||||
this->config.transaction_number_of_http_retries,
|
||||
this->config.transaction_retry_wait_in_milliseconds);
|
||||
this->current_transaction_id = value.transaction_id;
|
||||
this->need_to_stop_transaction = true;
|
||||
} catch (DCBMUnexpectedResponseException& error) {
|
||||
const std::string error_message =
|
||||
fmt::format("Failed to start transaction {}: {}", value.transaction_id, error.what());
|
||||
EVLOG_error << error_message;
|
||||
return {types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, error_message};
|
||||
} catch (HttpClientError& error) {
|
||||
const std::string error_message = fmt::format(
|
||||
"Failed to start transaction {} - connection to device failed: {}", value.transaction_id, error.what());
|
||||
EVLOG_error << error_message;
|
||||
return {types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, error_message};
|
||||
}
|
||||
|
||||
auto [transaction_min_stop_time, transaction_max_stop_time] = get_transaction_stop_time_bounds();
|
||||
|
||||
return {types::powermeter::TransactionRequestStatus::OK, {}, transaction_min_stop_time, transaction_max_stop_time};
|
||||
}
|
||||
|
||||
void LemDCBM400600Controller::request_device_to_start_transaction(const types::powermeter::TransactionReq& value) {
|
||||
this->time_sync_helper->sync(*this->http_client);
|
||||
|
||||
const std::string endpoint = v2_capable ? "/v2/legal" : "/v1/legal";
|
||||
const std::string payload = this->transaction_start_request_to_dcbm_payload(value);
|
||||
auto response = this->http_client->post(endpoint, payload);
|
||||
if (response.status_code != 201) {
|
||||
throw UnexpectedDCBMResponseCode(endpoint, 201, response);
|
||||
}
|
||||
try {
|
||||
bool running = json::parse(response.body).at("running");
|
||||
if (!running) {
|
||||
throw UnexpectedDCBMResponseBody(
|
||||
"/v1/legal", fmt::format("Created transaction {} has state running = false.", value.transaction_id));
|
||||
}
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(endpoint,
|
||||
fmt::format("Json error {} for body '{}'", json_error.what(), response.body));
|
||||
}
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStopResponse
|
||||
LemDCBM400600Controller::stop_transaction(const std::string& transaction_id) {
|
||||
std::string tid = transaction_id;
|
||||
bool need_to_execute_device_stop_transaction = true;
|
||||
if (!(this->need_to_stop_transaction) && transaction_id == this->current_transaction_id) {
|
||||
// transaction is not open but we need to provide OCMF information about it
|
||||
need_to_execute_device_stop_transaction = false;
|
||||
this->current_transaction_id = "";
|
||||
}
|
||||
if (!(this->need_to_stop_transaction) && transaction_id.empty()) {
|
||||
// return an error because there is no transaction initially ongoing (at start up time)
|
||||
return types::powermeter::TransactionStopResponse{
|
||||
types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, "No dangling transaction open"};
|
||||
}
|
||||
if (this->need_to_stop_transaction && transaction_id == this->current_transaction_id) {
|
||||
// transaction has been found and it is now going to close
|
||||
this->need_to_stop_transaction = false;
|
||||
this->current_transaction_id = "";
|
||||
}
|
||||
if (this->need_to_stop_transaction && transaction_id.empty()) {
|
||||
// transaction has NOT been found by the system, however the system is requesting a cleanup
|
||||
// thus we use the last known value of the transaction id to close the transaction
|
||||
tid = this->current_transaction_id;
|
||||
this->current_transaction_id = "";
|
||||
this->need_to_stop_transaction = false;
|
||||
}
|
||||
try {
|
||||
return call_with_retry(
|
||||
[this, need_to_execute_device_stop_transaction, tid]() {
|
||||
// special case if we started and a transaction is ongoing - the upper layers might not know the
|
||||
// transaction id
|
||||
if (need_to_execute_device_stop_transaction) {
|
||||
this->request_device_to_stop_transaction(tid);
|
||||
}
|
||||
auto signed_meter_value = types::units_signed::SignedMeterValue{fetch_ocmf_result(tid), "", "OCMF"};
|
||||
signed_meter_value.public_key.emplace(public_key_ocmf);
|
||||
return types::powermeter::TransactionStopResponse{types::powermeter::TransactionRequestStatus::OK,
|
||||
{}, // Empty start_signed_meter_value
|
||||
signed_meter_value};
|
||||
},
|
||||
this->config.transaction_number_of_http_retries, this->config.transaction_retry_wait_in_milliseconds);
|
||||
} catch (DCBMUnexpectedResponseException& error) {
|
||||
std::string error_message = fmt::format("Failed to stop transaction {}: {}", tid, error.what());
|
||||
EVLOG_error << error_message;
|
||||
return types::powermeter::TransactionStopResponse{
|
||||
types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, error_message};
|
||||
} catch (HttpClientError& error) {
|
||||
std::string error_message =
|
||||
fmt::format("Failed to stop transaction {} - connection to device failed: {}", tid, error.what());
|
||||
EVLOG_error << error_message;
|
||||
// if we have the last known OCMF value, we can return it
|
||||
if (current_signed_meter_value.public_key.has_value()) {
|
||||
EVLOG_warning << "LEM DCBM 400/600: Returning the last known OCMF value for transaction " << tid
|
||||
<< " with value: " << current_signed_meter_value;
|
||||
return types::powermeter::TransactionStopResponse{
|
||||
types::powermeter::TransactionRequestStatus::OK, {}, current_signed_meter_value};
|
||||
}
|
||||
current_signed_meter_value = types::units_signed::SignedMeterValue{};
|
||||
return types::powermeter::TransactionStopResponse{
|
||||
types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, error_message};
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBM400600Controller::request_device_to_stop_transaction(const std::string& transaction_id) {
|
||||
std::string endpoint = v2_capable ? fmt::format("/v2/legal?transactionId={}", transaction_id)
|
||||
: fmt::format("/v1/legal?transactionId={}", transaction_id);
|
||||
auto legal_api_response = this->http_client->put(endpoint, R"({"running": false})");
|
||||
|
||||
if (legal_api_response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode(endpoint, 200, legal_api_response);
|
||||
}
|
||||
|
||||
try {
|
||||
int status = json::parse(legal_api_response.body).at("meterValue").at("transactionStatus");
|
||||
bool transaction_is_ongoing = (status & 0b100) != 0; // third status bit "transactionIsOnGoing" must be false
|
||||
if (transaction_is_ongoing) {
|
||||
throw UnexpectedDCBMResponseBody(endpoint, fmt::format("Transaction stop request for transaction {} "
|
||||
"returned device status {}.",
|
||||
transaction_id, status));
|
||||
}
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(
|
||||
endpoint, fmt::format("Json error '{}' for body {}", json_error.what(), legal_api_response.body));
|
||||
}
|
||||
}
|
||||
|
||||
std::string LemDCBM400600Controller::fetch_ocmf_result(const std::string& transaction_id) {
|
||||
const std::string ocmf_endpoint = v2_capable ? fmt::format("/v2/ocmf?transactionId={}", transaction_id)
|
||||
: fmt::format("/v1/ocmf?transactionId={}", transaction_id);
|
||||
auto ocmf_api_response = this->http_client->get(ocmf_endpoint);
|
||||
|
||||
if (ocmf_api_response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode(ocmf_endpoint, 200, ocmf_api_response);
|
||||
}
|
||||
|
||||
if (ocmf_api_response.body.empty()) {
|
||||
throw UnexpectedDCBMResponseBody(ocmf_endpoint, "Returned empty body");
|
||||
}
|
||||
|
||||
return ocmf_api_response.body;
|
||||
}
|
||||
|
||||
types::powermeter::Powermeter LemDCBM400600Controller::get_powermeter() {
|
||||
this->time_sync_helper->sync_if_deadline_expired(*this->http_client);
|
||||
|
||||
const std::string endpoint = v2_capable ? "/v2/livemeasure" : "/v1/livemeasure";
|
||||
auto response = this->http_client->get(endpoint);
|
||||
types::powermeter::Powermeter powermeter_result;
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode(endpoint, 200, response);
|
||||
}
|
||||
try {
|
||||
powermeter_result = this->convert_livemeasure_to_powermeter(response.body);
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(endpoint, fmt::format("Json error '{}'", json_error.what()));
|
||||
}
|
||||
if (this->need_to_stop_transaction) {
|
||||
// if there is no ongoing transaction, we do need to fetch the signed meter value to have it available
|
||||
// for the upper layers, otherwise we will not have the OCMF value if we lose connection to the device
|
||||
try {
|
||||
current_signed_meter_value =
|
||||
types::units_signed::SignedMeterValue{fetch_ocmf_result(current_transaction_id), "", "OCMF"};
|
||||
current_signed_meter_value.public_key.emplace(public_key_ocmf);
|
||||
current_signed_meter_value.timestamp.emplace(powermeter_result.timestamp);
|
||||
} catch (UnexpectedDCBMResponseCode& error) {
|
||||
EVLOG_error << "LEM DCBM 400/600: Could not get the OCMF value: " << error.what();
|
||||
} catch (UnexpectedDCBMResponseBody& error) {
|
||||
EVLOG_error << "LEM DCBM 400/600: Invalid OCMF value: " << error.what();
|
||||
} catch (HttpClientError& error) {
|
||||
std::string error_message = fmt::format("Failed get the OCMF field {} - connection to device failed: {}",
|
||||
current_transaction_id, error.what());
|
||||
EVLOG_error << error_message;
|
||||
}
|
||||
}
|
||||
return powermeter_result;
|
||||
}
|
||||
|
||||
types::powermeter::Powermeter
|
||||
LemDCBM400600Controller::convert_livemeasure_to_powermeter(const std::string& livemeasure) {
|
||||
types::powermeter::Powermeter powermeter;
|
||||
json data = json::parse(livemeasure);
|
||||
powermeter.timestamp = data.at("timestamp");
|
||||
powermeter.meter_id.emplace(this->meter_id);
|
||||
powermeter.energy_Wh_import = {data.at("energyImportTotal").get<float>() * 1000.0f};
|
||||
powermeter.energy_Wh_export.emplace(types::units::Energy{data.at("energyExportTotal").get<float>() * 1000.0f});
|
||||
auto voltage = types::units::Voltage{};
|
||||
voltage.DC = data.at("voltage");
|
||||
powermeter.voltage_V.emplace(voltage);
|
||||
auto current = types::units::Current{};
|
||||
current.DC = data.at("current");
|
||||
powermeter.current_A.emplace(current);
|
||||
powermeter.power_W.emplace(types::units::Power{data.at("power").get<float>() * 1000.0f});
|
||||
powermeter.temperatures.emplace({types::temperature::Temperature{data.at("temperatureH"), "temperatureH"},
|
||||
types::temperature::Temperature{data.at("temperatureL"), "temperatureL"}});
|
||||
return powermeter;
|
||||
}
|
||||
|
||||
std::string
|
||||
LemDCBM400600Controller::transaction_start_request_to_dcbm_payload(const types::powermeter::TransactionReq& request) {
|
||||
std::string client_id = request.identification_data.value_or("") + ',' + request.transaction_id;
|
||||
const int max_length_client_id = 37; // as defined by LEM documentation
|
||||
client_id = (client_id.length() > max_length_client_id) ? client_id.substr(0, max_length_client_id) : client_id;
|
||||
if (this->v2_capable) {
|
||||
return nlohmann::ordered_json{{"evseId", request.evse_id},
|
||||
{"transactionId", request.transaction_id},
|
||||
{"clientId", client_id},
|
||||
{"tariffId", this->config.tariff_id},
|
||||
{"TT", request.tariff_text.value_or("")},
|
||||
{"UV", this->config.UV},
|
||||
{"UD", this->config.UD},
|
||||
{"cableId", this->config.cable_id},
|
||||
{"userData", ""},
|
||||
{"SC", this->config.SC}}
|
||||
.dump();
|
||||
} else {
|
||||
return nlohmann::ordered_json{
|
||||
{"evseId", request.evse_id}, {"transactionId", request.transaction_id}, {"clientId", client_id},
|
||||
{"tariffId", this->config.tariff_id}, {"cableId", this->config.cable_id}, {"userData", ""}}
|
||||
.dump();
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBM400600Controller::set_identification_type(int identification_type) {
|
||||
const std::string payload = nlohmann::ordered_json{{"ocmfId", {{"IT", identification_type}}}}.dump();
|
||||
auto response = this->http_client->put("/v1/settings", payload);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode("/v1/settings", 200, response);
|
||||
}
|
||||
try {
|
||||
bool success = nlohmann::json::parse(response.body).at("result") == 1;
|
||||
if (!success) {
|
||||
throw UnexpectedDCBMResponseBody("/v1/settings",
|
||||
"OCMF Identification Type setting was rejected by the device.");
|
||||
}
|
||||
} catch (nlohmann::json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(
|
||||
"/v1/settings", fmt::format("Json error '{}' for body '{}'", json_error.what(), response.body));
|
||||
}
|
||||
EVLOG_info << "LEM DCBM 400/600: OCMF Identification Type (IT) set to: " << identification_type;
|
||||
}
|
||||
|
||||
int LemDCBM400600Controller::get_identification_type() {
|
||||
auto response = this->http_client->get("/v1/settings");
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedDCBMResponseCode("/v1/settings", 200, response);
|
||||
}
|
||||
try {
|
||||
return nlohmann::json::parse(response.body).at("ocmfId").at("IT").get<int>();
|
||||
} catch (nlohmann::json::exception& json_error) {
|
||||
throw UnexpectedDCBMResponseBody(
|
||||
"/v1/settings", fmt::format("Json error '{}' for body '{}'", json_error.what(), response.body));
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<std::string, std::string> LemDCBM400600Controller::get_transaction_stop_time_bounds() {
|
||||
// The LEM DCBM 400/600 Operations manual (7.2.2.) states
|
||||
// "Minimum duration for transactions is 2 minutes, to prevent potential
|
||||
// memory storage weaknesses." Further, the communication protocol states
|
||||
// (4.2.9.): "If after a period of 48h the time was not set, time
|
||||
// synchronization expires (preventing new transactions and invalidating
|
||||
// on-going one)."" Since during an ongoing transaction, now time can synced,
|
||||
// the max duration is set to 48 hours (minus a small delta).
|
||||
auto now = std::chrono::time_point<date::utc_clock>::clock::now();
|
||||
return {
|
||||
Everest::Date::to_rfc3339(now + std::chrono::minutes(2)),
|
||||
Everest::Date::to_rfc3339(now + std::chrono::hours(48) - std::chrono::minutes(1)),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace module::main
|
||||
@@ -0,0 +1,170 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_LEMDCBM400600_H
|
||||
#define EVEREST_CORE_MODULE_LEMDCBM400600_H
|
||||
|
||||
#include "http_client_interface.hpp"
|
||||
#include "lem_dcbm_time_sync_helper.hpp"
|
||||
#include <functional>
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
class LemDCBM400600Controller {
|
||||
|
||||
public:
|
||||
struct Conf {
|
||||
// number of retries to connect to powermeter at initialization
|
||||
const int init_number_of_http_retries;
|
||||
// wait time before each retry during powermeter at initialization
|
||||
const int init_retry_wait_in_milliseconds;
|
||||
// number of retries for failed requests (due to HTTP or device errors) to start or stop a transaction
|
||||
const int transaction_number_of_http_retries;
|
||||
// wait time before each retry for transaction start/stop requests
|
||||
const int transaction_retry_wait_in_milliseconds;
|
||||
// The cable loss compensation level to use. This allows compensating the measurements of the DCBM with a
|
||||
// resistance.
|
||||
const int cable_id;
|
||||
// Used for a unique transaction tariff designation
|
||||
const int tariff_id;
|
||||
// meter time zone
|
||||
const std::string meter_timezone;
|
||||
// the meter Daylight Saving Time (DST) settings
|
||||
const std::string meter_dst;
|
||||
// SC
|
||||
const int SC;
|
||||
// UV
|
||||
const std::string UV;
|
||||
// UD
|
||||
const std::string UD;
|
||||
// OCMF Identification Type (set on device at startup via /settings/ocmfId/IT, -1 = not set)
|
||||
const int IT;
|
||||
// command timeout in milliseconds
|
||||
const int command_timeout_ms;
|
||||
};
|
||||
|
||||
class DCBMUnexpectedResponseException : public std::exception {
|
||||
public:
|
||||
const char* what() {
|
||||
return this->reason.c_str();
|
||||
}
|
||||
|
||||
explicit DCBMUnexpectedResponseException(std::string reason) : reason(std::move(reason)) {
|
||||
}
|
||||
|
||||
private:
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
class UnexpectedDCBMResponseBody : public DCBMUnexpectedResponseException {
|
||||
public:
|
||||
UnexpectedDCBMResponseBody(std::string endpoint, std::string error) :
|
||||
DCBMUnexpectedResponseException(
|
||||
fmt::format("Received unexpected response body from endpoint {}: {}", endpoint, error)),
|
||||
endpoint(std::move(endpoint)),
|
||||
error(std::move(error)) {
|
||||
}
|
||||
|
||||
private:
|
||||
std::string endpoint;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
class UnexpectedDCBMResponseCode : public DCBMUnexpectedResponseException {
|
||||
public:
|
||||
const std::string endpoint;
|
||||
const HttpResponse response;
|
||||
const std::string body;
|
||||
|
||||
UnexpectedDCBMResponseCode(const std::string& endpoint, unsigned int expected_code,
|
||||
const HttpResponse& response) :
|
||||
DCBMUnexpectedResponseException(fmt::format(
|
||||
"Received unexpected response from endpoint '{}': {} (expected {}) {}", endpoint, response.status_code,
|
||||
expected_code, !response.body.empty() ? " - body: " + response.body : "")),
|
||||
endpoint(endpoint),
|
||||
response(response) {
|
||||
}
|
||||
};
|
||||
|
||||
void update_lem_status();
|
||||
|
||||
private:
|
||||
const std::unique_ptr<HttpClientInterface> http_client;
|
||||
std::string meter_id;
|
||||
std::string public_key;
|
||||
std::string public_key_ocmf;
|
||||
std::string version;
|
||||
bool v2_capable = false;
|
||||
bool need_to_stop_transaction = false;
|
||||
std::string current_transaction_id;
|
||||
types::units_signed::SignedMeterValue current_signed_meter_value;
|
||||
std::unique_ptr<LemDCBMTimeSyncHelper> time_sync_helper;
|
||||
Conf config;
|
||||
|
||||
void fetch_meter_id_from_device();
|
||||
int get_identification_type();
|
||||
void set_identification_type(int identification_type);
|
||||
std::string get_current_transaction();
|
||||
void request_device_to_start_transaction(const types::powermeter::TransactionReq& value);
|
||||
void request_device_to_stop_transaction(const std::string& transaction_id);
|
||||
std::string fetch_ocmf_result(const std::string& transaction_id);
|
||||
types::powermeter::Powermeter convert_livemeasure_to_powermeter(const std::string& livemeasure);
|
||||
std::string transaction_start_request_to_dcbm_payload(const types::powermeter::TransactionReq& request);
|
||||
static std::pair<std::string, std::string> get_transaction_stop_time_bounds();
|
||||
|
||||
template <typename Callable>
|
||||
static auto call_with_retry(Callable func, int number_of_retries, int retry_wait_in_milliseconds,
|
||||
bool retry_on_http_client_error = true, bool retry_on_dcbm_reponse_error = true)
|
||||
-> decltype(func()) {
|
||||
std::exception_ptr lastException = nullptr;
|
||||
for (int attempt = 0; attempt < 1 + number_of_retries; ++attempt) {
|
||||
try {
|
||||
return func();
|
||||
} catch (HttpClientError& http_client_error) {
|
||||
lastException = std::current_exception();
|
||||
if (!retry_on_http_client_error) {
|
||||
std::rethrow_exception(lastException);
|
||||
}
|
||||
EVLOG_warning << "HTTPClient request failed: " << http_client_error.what() << "; retry in "
|
||||
<< retry_wait_in_milliseconds << " milliseconds";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_wait_in_milliseconds));
|
||||
} catch (DCBMUnexpectedResponseException& dcbm_error) {
|
||||
lastException = std::current_exception();
|
||||
if (!retry_on_dcbm_reponse_error) {
|
||||
std::rethrow_exception(lastException);
|
||||
}
|
||||
EVLOG_warning << "Unexpected DCBM response: " << dcbm_error.what() << "; retry in "
|
||||
<< retry_wait_in_milliseconds << " milliseconds";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_wait_in_milliseconds));
|
||||
}
|
||||
}
|
||||
std::rethrow_exception(lastException);
|
||||
}
|
||||
|
||||
public:
|
||||
LemDCBM400600Controller() = delete;
|
||||
|
||||
explicit LemDCBM400600Controller(std::unique_ptr<HttpClientInterface> http_client,
|
||||
std::unique_ptr<LemDCBMTimeSyncHelper> time_sync_helper, const Conf& config) :
|
||||
http_client(std::move(http_client)), time_sync_helper(std::move(time_sync_helper)), config(config) {
|
||||
}
|
||||
|
||||
void init();
|
||||
types::powermeter::TransactionStartResponse start_transaction(const types::powermeter::TransactionReq& value);
|
||||
types::powermeter::TransactionStopResponse stop_transaction(const std::string& transaction_id);
|
||||
types::powermeter::Powermeter get_powermeter();
|
||||
inline bool is_initialized() {
|
||||
return ("" != meter_id);
|
||||
}
|
||||
inline std::string get_public_key_ocmf() {
|
||||
return public_key_ocmf;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_LEMDCBM400600_H
|
||||
@@ -0,0 +1,174 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "lem_dcbm_time_sync_helper.hpp"
|
||||
#include "http_client_interface.hpp"
|
||||
#include "lem_dcbm_400600_controller.hpp"
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <date/tz.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <utils/date.hpp>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
std::string LemDCBMTimeSyncHelper::generate_dcbm_ntp_config() {
|
||||
nlohmann::ordered_json config_json = {
|
||||
{"ntp",
|
||||
{{"servers",
|
||||
{{{"ipAddress", ntp_spec.ip_addr_1}, {"port", ntp_spec.port_1}},
|
||||
{{"ipAddress", ntp_spec.ip_addr_2}, {"port", ntp_spec.port_2}}}},
|
||||
{"syncPeriod", 120},
|
||||
{"ntpActivated", true}}},
|
||||
};
|
||||
|
||||
return config_json.dump();
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::set_time_config_params(const std::string& meter_timezone, const std::string& meter_dst) {
|
||||
this->meter_timezone = meter_timezone;
|
||||
this->meter_dst = meter_dst;
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::sync_if_deadline_expired(const HttpClientInterface& httpClient) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(this->time_sync_state_lock);
|
||||
|
||||
if (this->ntp_spec.ntp_enabled && this->dcbm_ntp_settings_saved) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::chrono::steady_clock::now() >= this->deadline_for_next_sync) {
|
||||
try {
|
||||
this->sync(httpClient);
|
||||
} catch (LemDCBM400600Controller::DCBMUnexpectedResponseException& error) {
|
||||
EVLOG_warning << "Failed to sync time settings: " << error.what();
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::sync(const HttpClientInterface& httpClient) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(this->time_sync_state_lock);
|
||||
|
||||
if (this->ntp_spec.ntp_enabled && this->dcbm_ntp_settings_saved) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->ntp_spec.ntp_enabled) {
|
||||
this->set_ntp_settings_on_device(httpClient);
|
||||
this->sync_timezone(httpClient);
|
||||
this->sync_dst(httpClient);
|
||||
} else {
|
||||
this->sync_system_time(httpClient);
|
||||
this->sync_timezone(httpClient);
|
||||
this->sync_dst(httpClient);
|
||||
}
|
||||
}
|
||||
|
||||
bool LemDCBMTimeSyncHelper::is_setting_write_safe() const {
|
||||
if (!this->unsafe_period_start_time.has_value()) {
|
||||
EVLOG_warning << "LEM DCBM 400/600: Time sync was attempted, but the unsafe period start time is not set.";
|
||||
return false;
|
||||
}
|
||||
// According to LEM DCBM manual, no setting should be written earlier than 2 minutes after the DCBM is powered on
|
||||
bool sync_is_too_early = std::chrono::steady_clock::now() <
|
||||
unsafe_period_start_time.value() + timing_constants.min_time_before_setting_write_is_safe;
|
||||
if (sync_is_too_early) {
|
||||
EVLOG_warning << "LEM DCBM 400/600: Time sync was performed earlier than 2 minutes after initialization. "
|
||||
"Time will be synced regardless, but it may not be reliably saved.";
|
||||
}
|
||||
return !sync_is_too_early;
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::set_ntp_settings_on_device(const HttpClientInterface& httpClient) {
|
||||
HttpResponse response = httpClient.put("/v1/settings", this->generate_dcbm_ntp_config());
|
||||
if (response.status_code != 200) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseCode("/v1/settings", 200, response);
|
||||
}
|
||||
bool success = nlohmann::json::parse(response.body).at("result") == 1;
|
||||
if (!success) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseBody(
|
||||
"/v1/settings", "NTP setting was rejected by the device, e.g. because of an ongoing transaction.");
|
||||
}
|
||||
if (!is_setting_write_safe()) {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries;
|
||||
} else {
|
||||
this->dcbm_ntp_settings_saved = true;
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::sync_system_time(const HttpClientInterface& httpClient) {
|
||||
std::string time_update = Everest::Date::to_rfc3339(date::utc_clock::now());
|
||||
std::string payload = R"({"time":{"utc":")" + time_update + R"("}})";
|
||||
HttpResponse response = httpClient.put("/v1/settings", payload);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseCode("/v1/settings", 200, response);
|
||||
}
|
||||
bool success = nlohmann::json::parse(response.body).at("result") == 1;
|
||||
if (!success) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseBody(
|
||||
"/v1/settings", "Time setting was rejected by the device, e.g. because of an ongoing transaction.");
|
||||
}
|
||||
|
||||
if (is_setting_write_safe()) {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.deadline_increment_after_sync;
|
||||
} else {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries;
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::sync_timezone(const HttpClientInterface& httpClient) {
|
||||
std::string payload = std::string(R"({"time": {"tz":")") + meter_timezone + R"("}})";
|
||||
HttpResponse response = httpClient.put("/v1/settings", payload);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseCode("/v1/settings", 200, response);
|
||||
}
|
||||
bool success = nlohmann::json::parse(response.body).at("result") == 1;
|
||||
if (!success) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseBody(
|
||||
"/v1/settings", "Timezone setting was rejected by the device, e.g. because of an ongoing transaction.");
|
||||
}
|
||||
|
||||
if (is_setting_write_safe()) {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.deadline_increment_after_sync;
|
||||
} else {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries;
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::sync_dst(const HttpClientInterface& httpClient) {
|
||||
std::string payload = std::string(R"({"time": {"dst":)") + meter_dst + R"(}})";
|
||||
HttpResponse response = httpClient.put("/v1/settings", payload);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseCode("/v1/settings", 200, response);
|
||||
}
|
||||
bool success = nlohmann::json::parse(response.body).at("result") == 1;
|
||||
if (!success) {
|
||||
throw LemDCBM400600Controller::UnexpectedDCBMResponseBody(
|
||||
"/v1/settings",
|
||||
"Daylight saving setting was rejected by the device, e.g. because of an ongoing transaction.");
|
||||
}
|
||||
|
||||
if (is_setting_write_safe()) {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.deadline_increment_after_sync;
|
||||
} else {
|
||||
this->deadline_for_next_sync =
|
||||
std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries;
|
||||
}
|
||||
}
|
||||
|
||||
void LemDCBMTimeSyncHelper::restart_unsafe_period() {
|
||||
this->unsafe_period_start_time = std::chrono::steady_clock::now();
|
||||
deadline_for_next_sync = unsafe_period_start_time.value() + timing_constants.min_time_before_setting_write_is_safe;
|
||||
}
|
||||
} // namespace module::main
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_LEM_DCBM_TIME_SYNC_HELPER_H
|
||||
#define EVEREST_CORE_MODULE_LEM_DCBM_TIME_SYNC_HELPER_H
|
||||
|
||||
#include "http_client_interface.hpp"
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
struct timing_config {
|
||||
// This is the time after powerup after which the DCBM guarantees that writing to settings will be reliable
|
||||
// Make sure to re-attempt any settings writes after this time has passed
|
||||
std::chrono::seconds min_time_before_setting_write_is_safe = std::chrono::minutes(2);
|
||||
// When performing regular syncs (e.g. as part of the livemeasure loop), this is the minimum duration between
|
||||
// retries.
|
||||
std::chrono::seconds min_time_between_sync_retries = std::chrono::minutes(1);
|
||||
// When a sync is successful, advance the deadline for regular syncs by this much:
|
||||
std::chrono::seconds deadline_increment_after_sync = std::chrono::hours(24);
|
||||
};
|
||||
|
||||
struct ntp_server_spec {
|
||||
const std::string ip_addr_1;
|
||||
const int port_1 = 123;
|
||||
const std::string ip_addr_2;
|
||||
const int port_2 = 123;
|
||||
const bool ntp_enabled = !ip_addr_1.empty();
|
||||
};
|
||||
|
||||
class LemDCBMTimeSyncHelper {
|
||||
|
||||
public:
|
||||
LemDCBMTimeSyncHelper() = delete;
|
||||
|
||||
explicit LemDCBMTimeSyncHelper(ntp_server_spec ntp_spec) :
|
||||
LemDCBMTimeSyncHelper(std::move(ntp_spec), timing_config{}) {
|
||||
}
|
||||
|
||||
explicit LemDCBMTimeSyncHelper(ntp_server_spec ntp_spec, timing_config tc) :
|
||||
ntp_spec(std::move(ntp_spec)),
|
||||
timing_constants(tc),
|
||||
meter_timezone(""),
|
||||
meter_dst(""),
|
||||
unsafe_period_start_time({}) {
|
||||
}
|
||||
|
||||
virtual ~LemDCBMTimeSyncHelper() = default;
|
||||
|
||||
void set_time_config_params(const std::string& meter_timezone, const std::string& meter_dst);
|
||||
|
||||
virtual void sync_if_deadline_expired(const HttpClientInterface& httpClient);
|
||||
|
||||
virtual void sync(const HttpClientInterface& httpClient);
|
||||
|
||||
virtual void restart_unsafe_period();
|
||||
|
||||
private:
|
||||
// CONFIGURATION VARIABLES
|
||||
const ntp_server_spec ntp_spec;
|
||||
// Timing constants (can be overridden in a special constructor, e.g. during testing)
|
||||
const timing_config timing_constants;
|
||||
// the meter timezone
|
||||
std::string meter_timezone;
|
||||
// the meter daylight saving time definition
|
||||
std::string meter_dst;
|
||||
|
||||
// RUNNING VARIABLES
|
||||
// The helper can be accessed by multiple threads, so we use a mutex to protect the data below
|
||||
std::recursive_mutex time_sync_state_lock;
|
||||
std::chrono::time_point<std::chrono::steady_clock> deadline_for_next_sync;
|
||||
std::optional<std::chrono::time_point<std::chrono::steady_clock>> unsafe_period_start_time;
|
||||
// True whenever the NTP config is successfully written to the device after min_time_before_setting_write_is_safe
|
||||
// has passed
|
||||
bool dcbm_ntp_settings_saved = false;
|
||||
|
||||
// sync_is_too_early is set if this is done earlier than MIN_TIME_BEFORE_SETTING_WRITE_IS_SAFE after init
|
||||
void set_ntp_settings_on_device(const HttpClientInterface& httpClient);
|
||||
|
||||
// sync_is_too_early is set if this is done earlier than MIN_TIME_BEFORE_SETTING_WRITE_IS_SAFE after init
|
||||
void sync_system_time(const HttpClientInterface& httpClient);
|
||||
void sync_timezone(const HttpClientInterface& httpClient);
|
||||
void sync_dst(const HttpClientInterface& httpClient);
|
||||
|
||||
std::string generate_dcbm_ntp_config();
|
||||
[[nodiscard]] bool is_setting_write_safe() const;
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_LEM_DCBM_TIME_SYNC_HELPER_H
|
||||
@@ -0,0 +1,150 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "powermeterImpl.hpp"
|
||||
#include "http_client.hpp"
|
||||
#include "lem_dcbm_time_sync_helper.hpp"
|
||||
#include <chrono>
|
||||
#include <everest/logging.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
void powermeterImpl::init() {
|
||||
// Dependency injection pattern: Create the HTTP client first,
|
||||
// then move it into the controller as a constructor argument
|
||||
auto http_client = std::make_unique<HttpClient>(mod->config.ip_address, mod->config.port,
|
||||
mod->config.meter_tls_certificate, mod->config.interface);
|
||||
|
||||
auto ntp_server_spec =
|
||||
module::main::ntp_server_spec{mod->config.ntp_server_1_ip_addr, mod->config.ntp_server_1_port,
|
||||
mod->config.ntp_server_2_ip_addr, mod->config.ntp_server_2_port};
|
||||
|
||||
this->controller = std::make_unique<LemDCBM400600Controller>(
|
||||
std::move(http_client), std::make_unique<LemDCBMTimeSyncHelper>(ntp_server_spec),
|
||||
LemDCBM400600Controller::Conf{
|
||||
mod->config.resilience_initial_connection_retries, mod->config.resilience_initial_connection_retry_delay,
|
||||
mod->config.resilience_transaction_request_retries, mod->config.resilience_transaction_request_retry_delay,
|
||||
mod->config.cable_id, mod->config.tariff_id, mod->config.meter_timezone, mod->config.meter_dst,
|
||||
mod->config.SC, mod->config.UV, mod->config.UD, mod->config.IT, mod->config.command_timeout_ms});
|
||||
|
||||
// Validate and normalize temperature thresholds for the monitor.
|
||||
// If the error level is configured below the warning level, clamp it and log a warning.
|
||||
double warning_level_C = mod->config.temperature_warning_level_C;
|
||||
double error_level_C = mod->config.temperature_error_level_C;
|
||||
if (error_level_C < warning_level_C) {
|
||||
EVLOG_warning << "LEM DCBM 400/600: temperature_error_level_C (" << error_level_C
|
||||
<< " °C) is below temperature_warning_level_C (" << warning_level_C
|
||||
<< " °C). Clamping error level to the warning level.";
|
||||
error_level_C = warning_level_C;
|
||||
}
|
||||
|
||||
this->temperature_monitor = std::make_unique<TemperatureMonitor>(
|
||||
TemperatureMonitor::Config{warning_level_C, error_level_C, mod->config.temperature_hysteresis_K,
|
||||
std::chrono::milliseconds(mod->config.temperature_min_time_as_valid_ms)});
|
||||
}
|
||||
|
||||
void powermeterImpl::ready() {
|
||||
// Start the live_measure_publisher thread, which periodically publishes the live measurements of the device
|
||||
this->live_measure_publisher_thread = std::thread([this] {
|
||||
while (true) {
|
||||
try {
|
||||
if (!this->controller->is_initialized()) {
|
||||
this->controller->init();
|
||||
this->publish_public_key_ocmf(this->controller->get_public_key_ocmf());
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(mod->config.resilience_initial_connection_retry_delay));
|
||||
} else {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
auto powermeter_data = this->controller->get_powermeter();
|
||||
this->publish_powermeter(powermeter_data);
|
||||
// if the communication error is set, clear the error
|
||||
if (this->error_state_monitor->is_error_active("powermeter/CommunicationFault",
|
||||
"Communication timed out")) {
|
||||
// need to update LEM status since we have recovered from a communication loss
|
||||
this->controller->update_lem_status();
|
||||
clear_error("powermeter/CommunicationFault", "Communication timed out");
|
||||
}
|
||||
|
||||
// Evaluate temperature thresholds
|
||||
if (powermeter_data.temperatures.has_value() && powermeter_data.temperatures->size() >= 2) {
|
||||
const double temp_H = powermeter_data.temperatures->at(0).temperature;
|
||||
const double temp_L = powermeter_data.temperatures->at(1).temperature;
|
||||
auto events = this->temperature_monitor->update(temp_H, temp_L);
|
||||
handle_temperature_events(events, this->temperature_monitor->last_max_temperature());
|
||||
}
|
||||
}
|
||||
} catch (LemDCBM400600Controller::DCBMUnexpectedResponseException& error) {
|
||||
EVLOG_error << "LEM DCBM 400/600: Failed to execute the powermeter ready loop due to an invalid device "
|
||||
"response: "
|
||||
<< error.what();
|
||||
} catch (HttpClientError& client_error) {
|
||||
if (!this->error_state_monitor->is_error_active("powermeter/CommunicationFault",
|
||||
"Communication timed out")) {
|
||||
EVLOG_error << "Failed to communicate with the powermeter due to http error: "
|
||||
<< client_error.what();
|
||||
auto error =
|
||||
this->error_factory->create_error("powermeter/CommunicationFault", "Communication timed out",
|
||||
"This error is raised due to communication timeout");
|
||||
raise_error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStartResponse
|
||||
powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& value) {
|
||||
if (!this->controller->is_initialized()) {
|
||||
return types::powermeter::TransactionStartResponse{
|
||||
types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, "Powermeter is not initialized"};
|
||||
}
|
||||
return this->controller->start_transaction(value);
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transaction(std::string& transaction_id) {
|
||||
if (!this->controller->is_initialized()) {
|
||||
return types::powermeter::TransactionStopResponse{
|
||||
types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, "Powermeter is not initialized"};
|
||||
}
|
||||
return this->controller->stop_transaction(transaction_id);
|
||||
}
|
||||
|
||||
void powermeterImpl::handle_temperature_events(const TemperatureMonitor::Events& events, double max_temperature) {
|
||||
if (events.warning_raised) {
|
||||
EVLOG_warning << fmt::format(
|
||||
"LEM DCBM 400/600: Temperature warning raised — max temperature {:.1f} °C exceeds warning level {:.1f} °C",
|
||||
max_temperature, mod->config.temperature_warning_level_C);
|
||||
auto error =
|
||||
this->error_factory->create_error("powermeter/VendorWarning", "TemperatureWarning",
|
||||
fmt::format("Max temperature {:.1f} °C exceeds warning level {:.1f} °C",
|
||||
max_temperature, mod->config.temperature_warning_level_C));
|
||||
raise_error(error);
|
||||
}
|
||||
if (events.warning_cleared) {
|
||||
EVLOG_info << fmt::format(
|
||||
"LEM DCBM 400/600: Temperature warning cleared — max temperature {:.1f} °C dropped below {:.1f} °C",
|
||||
max_temperature, mod->config.temperature_warning_level_C - mod->config.temperature_hysteresis_K);
|
||||
clear_error("powermeter/VendorWarning", "TemperatureWarning");
|
||||
}
|
||||
if (events.error_raised) {
|
||||
EVLOG_error << fmt::format(
|
||||
"LEM DCBM 400/600: Temperature error raised — max temperature {:.1f} °C exceeds error level {:.1f} °C",
|
||||
max_temperature, mod->config.temperature_error_level_C);
|
||||
auto error =
|
||||
this->error_factory->create_error("powermeter/VendorError", "TemperatureError",
|
||||
fmt::format("Max temperature {:.1f} °C exceeds error level {:.1f} °C",
|
||||
max_temperature, mod->config.temperature_error_level_C));
|
||||
raise_error(error);
|
||||
}
|
||||
if (events.error_cleared) {
|
||||
EVLOG_info << fmt::format(
|
||||
"LEM DCBM 400/600: Temperature error cleared — max temperature {:.1f} °C dropped below {:.1f} °C",
|
||||
max_temperature, mod->config.temperature_error_level_C - mod->config.temperature_hysteresis_K);
|
||||
clear_error("powermeter/VendorError", "TemperatureError");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace module::main
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user