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,154 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "httpClient.hpp"
|
||||
#include <fmt/core.h>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace module::main {
|
||||
const char* CONTENT_TYPE_HEADER = "Content-Type: application/json";
|
||||
|
||||
struct PayloadInTransit {
|
||||
const std::string& data;
|
||||
size_t position;
|
||||
};
|
||||
|
||||
// Callback for receiving data, saves it into a string
|
||||
static size_t receive_data(char* ptr, size_t size, size_t nmemb, std::string* received_data) {
|
||||
received_data->append(ptr, size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
// Callback for sending data, fetches it from a string
|
||||
static size_t send_data(char* buffer, size_t size, size_t nitems, struct PayloadInTransit* payload) {
|
||||
if (payload->position >= payload->data.length()) {
|
||||
// Returning 0 signals to libcurl that we have no more data to send
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Send up to size*nitems bytes of data
|
||||
size_t payload_remaining_bytes = payload->data.length() - payload->position;
|
||||
size_t num_bytes_to_send = std::min(size * nitems, payload_remaining_bytes);
|
||||
std::memcpy(buffer, payload->data.c_str() + payload->position, num_bytes_to_send);
|
||||
payload->position += num_bytes_to_send;
|
||||
return num_bytes_to_send;
|
||||
}
|
||||
|
||||
static HttpClientError client_error(const std::string& host, unsigned int port, const char* method,
|
||||
const std::string& path, const std::string& message) {
|
||||
return HttpClientError(fmt::format("HTTP client error on {} {}:{}{} : {} ", method, host, port, path, message));
|
||||
}
|
||||
|
||||
static void setup_connection(CURL* connection, struct PayloadInTransit& request_payload, std::string& response_body,
|
||||
curl_slist*& headers) {
|
||||
// Override the Content-Type header
|
||||
headers = curl_slist_append(nullptr, CONTENT_TYPE_HEADER);
|
||||
if (curl_easy_setopt(connection, CURLOPT_HTTPHEADER, headers) != CURLE_OK) {
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
|
||||
// Set up callbacks for reading and writing
|
||||
curl_easy_setopt(connection, CURLOPT_WRITEFUNCTION, receive_data);
|
||||
curl_easy_setopt(connection, CURLOPT_WRITEDATA, &response_body);
|
||||
curl_easy_setopt(connection, CURLOPT_READFUNCTION, send_data);
|
||||
curl_easy_setopt(connection, CURLOPT_READDATA, &request_payload);
|
||||
|
||||
// Misc. settings come here
|
||||
curl_easy_setopt(connection, CURLOPT_FORBID_REUSE, 1);
|
||||
curl_easy_setopt(connection, CURLOPT_CONNECTTIMEOUT, 2);
|
||||
if (curl_easy_setopt(connection, CURLOPT_FOLLOWLOCATION, 0) != CURLE_OK) {
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
}
|
||||
|
||||
// Note: method_name and path are only there for the error message
|
||||
HttpResponse HttpClient::perform_request(CURL* connection, const std::string& request_body, const char* method_name,
|
||||
const std::string& path) const {
|
||||
// give curl a buffer to write its error messages to
|
||||
char curl_error_message[CURL_ERROR_SIZE] = {};
|
||||
curl_easy_setopt(connection, CURLOPT_ERRORBUFFER, curl_error_message);
|
||||
|
||||
// set up the connection options
|
||||
std::string response_body;
|
||||
struct PayloadInTransit request_payload {
|
||||
request_body, 0
|
||||
};
|
||||
struct curl_slist* headers;
|
||||
setup_connection(connection, request_payload, response_body, headers);
|
||||
|
||||
// perform the request
|
||||
CURLcode res = curl_easy_perform(connection);
|
||||
|
||||
// remember to free the headers list...
|
||||
curl_slist_free_all(headers);
|
||||
// check the result of the request and return
|
||||
if (res == CURLE_OK) {
|
||||
long response_code;
|
||||
curl_easy_getinfo(connection, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
return HttpResponse{(unsigned int)response_code, std::move(response_body)};
|
||||
} else {
|
||||
throw client_error(this->host, this->port, method_name, path, std::string(curl_error_message));
|
||||
}
|
||||
}
|
||||
|
||||
CURL* HttpClient::create_curl_handle_and_setup_url(const std::string& path) const {
|
||||
CURL* connection = curl_easy_init();
|
||||
if (!connection) {
|
||||
throw std::runtime_error("Could not create a CURL handle: curl_easy_init() returned null");
|
||||
}
|
||||
const char* protocol = "http";
|
||||
if (curl_easy_setopt(connection, CURLOPT_URL,
|
||||
fmt::format("{}://{}:{}{}", protocol, this->host, this->port, path).c_str()) != CURLE_OK) {
|
||||
throw std::runtime_error("Could not set CURLOPT_URL, likely ran out of memory");
|
||||
}
|
||||
if (curl_easy_setopt(connection, CURLOPT_PROTOCOLS_STR, protocol) != CURLE_OK) {
|
||||
throw std::runtime_error(std::string("Could not set supported protocol to ") + protocol +
|
||||
", is it enabled in libcurl?");
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::get(const std::string& path) const {
|
||||
CURL* connection = this->create_curl_handle_and_setup_url(path);
|
||||
|
||||
if (curl_easy_setopt(connection, CURLOPT_HTTPGET, 1) != CURLE_OK) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
|
||||
// perform_request() does not cleanup the connection on its own.
|
||||
// We do the cleanup here, and make sure to rethrow any exception that might've occurred.
|
||||
try {
|
||||
HttpResponse response = perform_request(connection, "", "GET", path);
|
||||
curl_easy_cleanup(connection);
|
||||
return response;
|
||||
} catch (std::exception& e) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::post(const std::string& path, const std::string& body) const {
|
||||
CURL* connection = this->create_curl_handle_and_setup_url(path);
|
||||
|
||||
if (curl_easy_setopt(connection, CURLOPT_POST, 1) != CURLE_OK) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw std::runtime_error(
|
||||
"libcurl signals that HTTP is unsupported. Your build or linkage might be misconfigured.");
|
||||
}
|
||||
|
||||
// perform_request() does not cleanup the connection on its own.
|
||||
// We do the cleanup here, and make sure to rethrow any exception that might've occurred.
|
||||
try {
|
||||
HttpResponse response = perform_request(connection, body, "POST", path);
|
||||
curl_easy_cleanup(connection);
|
||||
return response;
|
||||
} catch (std::exception& e) {
|
||||
curl_easy_cleanup(connection);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
} // namespace module::main
|
||||
@@ -0,0 +1,50 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_HTTPCLIENT_H
|
||||
#define EVEREST_CORE_MODULE_HTTPCLIENT_H
|
||||
|
||||
#include "fmt/format.h"
|
||||
#include "httpClientInterface.hpp"
|
||||
#include <curl/curl.h>
|
||||
#include <everest/logging.hpp>
|
||||
#include <regex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
class HttpClient : public HttpClientInterface {
|
||||
|
||||
public:
|
||||
HttpClient() = delete;
|
||||
|
||||
HttpClient(const std::string& host_arg, int port_arg) {
|
||||
// initialize libcurl - this is safe to do multiple times, if there are multiple HttpClients
|
||||
// Note: This is only thread-safe after libcurl 7.84.0, but we use 8.4.0, so it should be fine
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
// These are saved in the client to avoid making the controller pass them at every call
|
||||
host = host_arg;
|
||||
port = port_arg;
|
||||
}
|
||||
~HttpClient() override {
|
||||
// release the libcurl resources - this must be done once for every call to curl_global_init().
|
||||
// Note: This is only thread-safe after libcurl 7.84.0, but we use 8.4.0, so it should be fine
|
||||
curl_global_cleanup();
|
||||
}
|
||||
|
||||
[[nodiscard]] HttpResponse get(const std::string& path) const override;
|
||||
[[nodiscard]] HttpResponse post(const std::string& path, const std::string& body) const override;
|
||||
|
||||
private:
|
||||
std::string host;
|
||||
int port;
|
||||
|
||||
[[nodiscard]] CURL* create_curl_handle_and_setup_url(const std::string& path) const;
|
||||
HttpResponse perform_request(CURL* connection, const std::string& request_body, const char* method_name,
|
||||
const std::string& path) const;
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_HTTPCLIENT_H
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_HTTP_CLIENT_INTERFACE_H
|
||||
#define EVEREST_CORE_MODULE_HTTP_CLIENT_INTERFACE_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
class HttpClientError : public std::exception {
|
||||
public:
|
||||
[[nodiscard]] const char* what() const noexcept override {
|
||||
return this->reason.c_str();
|
||||
}
|
||||
explicit HttpClientError(std::string msg) {
|
||||
this->reason = std::move(msg);
|
||||
}
|
||||
explicit HttpClientError(const char* msg) {
|
||||
this->reason = std::string(msg);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct HttpResponse {
|
||||
unsigned int status_code;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
struct HttpClientInterface {
|
||||
|
||||
virtual ~HttpClientInterface() = default;
|
||||
|
||||
[[nodiscard]] virtual HttpResponse get(const std::string& path) const = 0;
|
||||
[[nodiscard]] virtual HttpResponse post(const std::string& path, const std::string& body) const = 0;
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_HTTP_CLIENT_INTERFACE_H
|
||||
@@ -0,0 +1,480 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "isabellenhuetteIemDcrController.hpp"
|
||||
#include <stdexcept>
|
||||
namespace module::main {
|
||||
|
||||
IsaIemDcrController::IsaIemDcrController(std::unique_ptr<HttpClientInterface> http_client,
|
||||
const SnapshotConfig& snap_config) :
|
||||
http_client(std::move(http_client)), snapshot_config(snap_config) {
|
||||
// Member Initializer List is used
|
||||
|
||||
// Further initialization
|
||||
zone_time_offset = helper_convert_timezone(snapshot_config.timezone);
|
||||
last_datetime_sync.store(std::chrono::steady_clock::now() - std::chrono::hours(48));
|
||||
}
|
||||
|
||||
bool IsaIemDcrController::init() {
|
||||
try {
|
||||
EVLOG_info << "Isabellenhuette IEM-DCR: Connecting to module...";
|
||||
// Check connection with polling REST node gw
|
||||
this->get_gw();
|
||||
// Send gw information
|
||||
try {
|
||||
this->post_gw();
|
||||
} catch (IsaIemDcrController::UnexpectedIemDcrResponseCode& error) {
|
||||
EVLOG_warning << "Node /gw seems to be already set. If those values should be updated, "
|
||||
"please restart IEM-DCR and then also this system.";
|
||||
}
|
||||
// Send initial tariff information
|
||||
try {
|
||||
if (snapshot_config.TT_initial.length() > 0) {
|
||||
this->post_tariff(snapshot_config.TT_initial);
|
||||
}
|
||||
} catch (IsaIemDcrController::UnexpectedIemDcrResponseCode& error) {
|
||||
EVLOG_warning << "Incorrect config: Value TT_initial could not be set. Please check its value.";
|
||||
}
|
||||
EVLOG_info << "Isabellenhuette IEM-DCR: Connected.";
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
json IsaIemDcrController::get_gw() {
|
||||
const std::string endpoint = "/counter/v1/ocmf/gw";
|
||||
auto response = this->http_client->get(endpoint);
|
||||
if (response.status_code == 200) {
|
||||
try {
|
||||
json data = json::parse(response.body);
|
||||
return data;
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedIemDcrResponseBody(
|
||||
endpoint, fmt::format("Json error {} for body {}", json_error.what(), response.body));
|
||||
}
|
||||
} else {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsaIemDcrController::check_gw_is_empty() {
|
||||
json gw_result = this->get_gw();
|
||||
return gw_result.at("CT").empty();
|
||||
}
|
||||
|
||||
void IsaIemDcrController::post_gw() {
|
||||
const std::string endpoint = "/counter/v1/ocmf/gw";
|
||||
const std::string payload = nlohmann::ordered_json{{"CT", snapshot_config.CT},
|
||||
{"CI", snapshot_config.CI},
|
||||
{"TM", helper_get_current_datetime()}}
|
||||
.dump();
|
||||
auto response = this->http_client->post(endpoint, payload);
|
||||
if (response.status_code == 200) {
|
||||
last_datetime_sync.store(std::chrono::steady_clock::now());
|
||||
} else {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
void IsaIemDcrController::post_tariff(std::string tariff_info) {
|
||||
const std::string endpoint = "/counter/v1/ocmf/tariff";
|
||||
const std::string payload = nlohmann::ordered_json{{"TT", tariff_info}}.dump();
|
||||
auto response = this->http_client->post(endpoint, payload);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
std::tuple<types::powermeter::Powermeter, std::string, bool> IsaIemDcrController::get_metervalue() {
|
||||
const std::string endpoint = "/counter/v1/ocmf/metervalue";
|
||||
auto response = this->http_client->get(endpoint);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
try {
|
||||
json data = json::parse(response.body);
|
||||
|
||||
types::powermeter::Powermeter powermeter;
|
||||
bool tmp_transaction_active = data.at("XT");
|
||||
powermeter.timestamp = data.at("TM");
|
||||
// Remove format specifier at the end (if available)
|
||||
if (powermeter.timestamp.length() > 28) {
|
||||
powermeter.timestamp = powermeter.timestamp.substr(0, 28);
|
||||
}
|
||||
powermeter.meter_id = data.at("MS");
|
||||
auto current = types::units::Current{};
|
||||
current.DC = data.at("I");
|
||||
powermeter.current_A.emplace(current);
|
||||
auto voltageU2 = types::units::Voltage{};
|
||||
voltageU2.DC = data.at("U2");
|
||||
powermeter.voltage_V.emplace(voltageU2);
|
||||
powermeter.power_W.emplace(types::units::Power{data.at("P").get<float>()});
|
||||
// Remove quotes before casting to float
|
||||
auto energy_kWh_import = helper_remove_first_and_last_char(data.at("RD").at(2).at("WV"));
|
||||
powermeter.energy_Wh_import = {std::stof(energy_kWh_import) * 1000.0f};
|
||||
// Remove quotes before casting to float
|
||||
auto energy_kWh_export = helper_remove_first_and_last_char(data.at("RD").at(3).at("WV"));
|
||||
powermeter.energy_Wh_export = {std::stof(energy_kWh_export) * 1000.0f};
|
||||
// Get status
|
||||
std::string status = data.at("XC");
|
||||
|
||||
return std::make_tuple(powermeter, status, tmp_transaction_active);
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedIemDcrResponseBody(endpoint,
|
||||
fmt::format("Json error {} for body {}", json_error.what(), response.body));
|
||||
}
|
||||
}
|
||||
|
||||
std::string IsaIemDcrController::get_publickey(bool allow_cached_value) {
|
||||
if (allow_cached_value && cached_public_key.length() > 0) {
|
||||
return cached_public_key;
|
||||
} else {
|
||||
const std::string endpoint = "/counter/v1/ocmf/publickey";
|
||||
auto response = this->http_client->get(endpoint);
|
||||
if (response.status_code != 200) {
|
||||
EVLOG_warning << "Response to retrieval of public key is not 200." << std::endl;
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
json data = json::parse(response.body);
|
||||
cached_public_key = data.at("PK");
|
||||
return cached_public_key;
|
||||
} catch (json::exception& json_error) {
|
||||
EVLOG_warning << "JSON error during parsing of public key" << std::endl;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string IsaIemDcrController::get_datetime() {
|
||||
const std::string endpoint = "/counter/v1/ocmf/datetime";
|
||||
auto response = this->http_client->get(endpoint);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
try {
|
||||
json data = json::parse(response.body);
|
||||
return data.at("TM");
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedIemDcrResponseBody(endpoint,
|
||||
fmt::format("Json error {} for body {}", json_error.what(), response.body));
|
||||
}
|
||||
}
|
||||
|
||||
void IsaIemDcrController::post_datetime() {
|
||||
const std::string endpoint = "/counter/v1/ocmf/datetime";
|
||||
const std::string payload = nlohmann::ordered_json{{"TM", helper_get_current_datetime()}}.dump();
|
||||
auto response = this->http_client->post(endpoint, payload);
|
||||
if (response.status_code == 200) {
|
||||
last_datetime_sync.store(std::chrono::steady_clock::now());
|
||||
} else {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
void IsaIemDcrController::refresh_datetime_if_required() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::hours>(now - last_datetime_sync.load());
|
||||
if (elapsed.count() >= snapshot_config.datetime_resync_interval) {
|
||||
try {
|
||||
this->post_datetime();
|
||||
EVLOG_info << "DateTime resynchronized.";
|
||||
} catch (...) {
|
||||
// On error: just retry on next call
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IsaIemDcrController::post_user(const types::powermeter::OCMFUserIdentificationStatus IS,
|
||||
const std::optional<types::powermeter::OCMFIdentificationLevel> IL,
|
||||
const std::vector<types::powermeter::OCMFIdentificationFlags>& IF,
|
||||
const types::powermeter::OCMFIdentificationType& IT,
|
||||
const std::optional<std::__cxx11::basic_string<char>>& ID,
|
||||
const std::optional<std::__cxx11::basic_string<char>>& TT) {
|
||||
|
||||
const std::string endpoint = "/counter/v1/ocmf/user";
|
||||
bool boolIS = helper_get_bool_from_OCMFUserIdentificationStatus(IS);
|
||||
std::string strIL = helper_get_string_from_OCMFIdentificationLevel(IL);
|
||||
std::string strIT = helper_get_string_from_OCMFIdentificationType(IT);
|
||||
std::string strID = static_cast<std::string>(ID.value_or(""));
|
||||
std::string strTT = static_cast<std::string>(TT.value_or(""));
|
||||
std::string payload = "";
|
||||
std::vector<std::string> vectIF;
|
||||
|
||||
// Fill vectIF
|
||||
for (const types::powermeter::OCMFIdentificationFlags& id_flag : IF) {
|
||||
vectIF.push_back(helper_get_string_from_OCMFIdentificationFlags(id_flag));
|
||||
}
|
||||
|
||||
if (strTT.length() > 0) {
|
||||
payload = nlohmann::ordered_json{{"IS", boolIS}, {"IL", strIL}, {"IF", vectIF},
|
||||
{"IT", strIT}, {"ID", strID}, {"US", snapshot_config.US},
|
||||
{"TT", strTT}}
|
||||
.dump();
|
||||
} else {
|
||||
payload = nlohmann::ordered_json{{"IS", boolIS}, {"IL", strIL}, {"IF", vectIF},
|
||||
{"IT", strIT}, {"ID", strID}, {"US", snapshot_config.US}}
|
||||
.dump();
|
||||
}
|
||||
auto response = this->http_client->post(endpoint, payload);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
types::units_signed::SignedMeterValue IsaIemDcrController::get_receipt() {
|
||||
const std::string endpoint = "/counter/v1/ocmf/receipt";
|
||||
return helper_get_signed_datatuple(endpoint);
|
||||
}
|
||||
|
||||
types::units_signed::SignedMeterValue IsaIemDcrController::get_transaction() {
|
||||
try {
|
||||
const std::string endpoint = "/counter/v1/ocmf/transaction";
|
||||
return helper_get_signed_datatuple(endpoint);
|
||||
} catch (UnexpectedIemDcrResponseCode& resp_error) {
|
||||
// Retry with newer api endpoint
|
||||
const std::string endpoint_v2 = "/counter/v2/ocmf/transaction";
|
||||
return helper_get_signed_datatuple(endpoint_v2);
|
||||
}
|
||||
}
|
||||
|
||||
void IsaIemDcrController::post_receipt(const std::string& TX) {
|
||||
const std::string endpoint = "/counter/v1/ocmf/receipt";
|
||||
const std::string payload = nlohmann::ordered_json{{"TX", TX}}.dump();
|
||||
auto response = this->http_client->post(endpoint, payload);
|
||||
if (response.status_code != 200) {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsaIemDcrController::helper_get_bool_from_OCMFUserIdentificationStatus(
|
||||
types::powermeter::OCMFUserIdentificationStatus IS) {
|
||||
return (IS == types::powermeter::OCMFUserIdentificationStatus::ASSIGNED);
|
||||
}
|
||||
|
||||
std::string IsaIemDcrController::helper_get_string_from_OCMFIdentificationLevel(
|
||||
std::optional<types::powermeter::OCMFIdentificationLevel> IL) {
|
||||
std::string result;
|
||||
types::powermeter::OCMFIdentificationLevel value_IL =
|
||||
IL.value_or(types::powermeter::OCMFIdentificationLevel::UNKNOWN);
|
||||
switch (value_IL) {
|
||||
case types::powermeter::OCMFIdentificationLevel::NONE:
|
||||
result = "NONE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::HEARSAY:
|
||||
result = "HEARSAY";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::TRUSTED:
|
||||
result = "TRUSTED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::VERIFIED:
|
||||
result = "VERIFIED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::CERTIFIED:
|
||||
result = "CERTIFIED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::SECURE:
|
||||
result = "SECURE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::MISMATCH:
|
||||
result = "MISMATCH";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::INVALID:
|
||||
result = "INVALID";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationLevel::OUTDATED:
|
||||
result = "OUTDATED";
|
||||
break;
|
||||
default:
|
||||
result = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string IsaIemDcrController::helper_get_string_from_OCMFIdentificationFlags(
|
||||
types::powermeter::OCMFIdentificationFlags id_flag) {
|
||||
std::string result;
|
||||
switch (id_flag) {
|
||||
case types::powermeter::OCMFIdentificationFlags::RFID_NONE:
|
||||
result = "RFID_NONE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::RFID_PLAIN:
|
||||
result = "RFID_PLAIN";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::RFID_RELATED:
|
||||
result = "RFID_RELATED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::RFID_PSK:
|
||||
result = "RFID_PSK";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_NONE:
|
||||
result = "OCPP_NONE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_RS:
|
||||
result = "OCPP_RS";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_AUTH:
|
||||
result = "OCPP_AUTH";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_RS_TLS:
|
||||
result = "OCPP_RS_TLS";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_AUTH_TLS:
|
||||
result = "OCPP_AUTH_TLS";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_CACHE:
|
||||
result = "OCPP_CACHE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_WHITELIST:
|
||||
result = "OCPP_WHITELIST";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::OCPP_CERTIFIED:
|
||||
result = "OCPP_CERTIFIED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::ISO15118_NONE:
|
||||
result = "ISO15118_NONE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::ISO15118_PNC:
|
||||
result = "ISO15118_PNC";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::PLMN_NONE:
|
||||
result = "PLMN_NONE";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::PLMN_RING:
|
||||
result = "PLMN_RING";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationFlags::PLMN_SMS:
|
||||
result = "PLMN_SMS";
|
||||
break;
|
||||
default:
|
||||
result = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string
|
||||
IsaIemDcrController::helper_get_string_from_OCMFIdentificationType(types::powermeter::OCMFIdentificationType IT) {
|
||||
std::string result;
|
||||
switch (IT) {
|
||||
case types::powermeter::OCMFIdentificationType::DENIED:
|
||||
result = "DENIED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::UNDEFINED:
|
||||
result = "UNDEFINED";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::ISO14443:
|
||||
result = "ISO14443";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::ISO15693:
|
||||
result = "ISO15693";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::EMAID:
|
||||
result = "EMAID";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::EVCCID:
|
||||
result = "EVCCID";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::EVCOID:
|
||||
result = "EVCOID";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::ISO7812:
|
||||
result = "ISO7812";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::CARD_TXN_NR:
|
||||
result = "CARD_TXN_NR";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::CENTRAL:
|
||||
result = "CENTRAL";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::CENTRAL_1:
|
||||
result = "CENTRAL_1";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::CENTRAL_2:
|
||||
result = "CENTRAL_2";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::LOCAL:
|
||||
result = "LOCAL";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::LOCAL_1:
|
||||
result = "LOCAL_1";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::LOCAL_2:
|
||||
result = "LOCAL_2";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::PHONE_NUMBER:
|
||||
result = "PHONE_NUMBER";
|
||||
break;
|
||||
case types::powermeter::OCMFIdentificationType::KEY_CODE:
|
||||
result = "KEY_CODE";
|
||||
break;
|
||||
default:
|
||||
result = "NONE";
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::chrono::minutes IsaIemDcrController::helper_convert_timezone(std::string& timezone) {
|
||||
const char sign_char = timezone[0];
|
||||
const int offset_hours = std::stoi(timezone.substr(1, 2));
|
||||
const int offset_minutes = std::stoi(timezone.substr(3, 2));
|
||||
const std::chrono::minutes time_offset = std::chrono::hours(offset_hours) + std::chrono::minutes(offset_minutes);
|
||||
if (sign_char == '+') {
|
||||
return time_offset;
|
||||
} else {
|
||||
return -time_offset;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsaIemDcrController::helper_is_daylight_saving_time() {
|
||||
const std::time_t now = std::time(nullptr);
|
||||
const std::tm* localTime = std::localtime(&now);
|
||||
return localTime->tm_isdst > 0;
|
||||
}
|
||||
|
||||
std::string IsaIemDcrController::helper_get_current_datetime() {
|
||||
// Get UTC time
|
||||
auto now = std::chrono::system_clock::now();
|
||||
// Add configured timezone information
|
||||
std::time_t now_with_offset = std::chrono::system_clock::to_time_t(now + zone_time_offset);
|
||||
// Add DST offset if configured
|
||||
if (snapshot_config.timezone_handle_DST && helper_is_daylight_saving_time()) {
|
||||
now_with_offset = now_with_offset + 3600;
|
||||
}
|
||||
// Generate and return time in correct format
|
||||
std::ostringstream oss;
|
||||
oss << std::put_time(gmtime(&now_with_offset), "%FT%T,000") << snapshot_config.timezone;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string IsaIemDcrController::helper_remove_first_and_last_char(const std::string& input) {
|
||||
if (input.length() <= 1) {
|
||||
return "";
|
||||
}
|
||||
return input.substr(1, input.length() - 1);
|
||||
}
|
||||
|
||||
types::units_signed::SignedMeterValue IsaIemDcrController::helper_get_signed_datatuple(const std::string& endpoint) {
|
||||
auto response = this->http_client->get(endpoint);
|
||||
types::units_signed::SignedMeterValue return_value;
|
||||
if (response.status_code == 200) {
|
||||
try {
|
||||
return_value.signed_meter_data = response.body;
|
||||
return_value.signing_method = "";
|
||||
return_value.encoding_method = "OCMF";
|
||||
return_value.public_key = get_publickey(true);
|
||||
|
||||
return return_value;
|
||||
} catch (json::exception& json_error) {
|
||||
throw UnexpectedIemDcrResponseBody(
|
||||
endpoint, fmt::format("Json error {} for body {}", json_error.what(), response.body));
|
||||
}
|
||||
} else {
|
||||
throw UnexpectedIemDcrResponseCode(endpoint, 200, response);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace module::main
|
||||
@@ -0,0 +1,170 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVEREST_CORE_MODULE_ISAIEMDCRCONTROLLER_H
|
||||
#define EVEREST_CORE_MODULE_ISAIEMDCRCONTROLLER_H
|
||||
|
||||
#include "httpClientInterface.hpp"
|
||||
#include <functional>
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace module::main {
|
||||
|
||||
class IsaIemDcrController {
|
||||
|
||||
public:
|
||||
struct SnapshotConfig {
|
||||
std::string timezone;
|
||||
bool timezone_handle_DST;
|
||||
int datetime_resync_interval;
|
||||
int resilience_initial_connection_retry_delay;
|
||||
int resilience_transaction_request_retries;
|
||||
int resilience_transaction_request_retry_delay;
|
||||
std::string CT;
|
||||
std::string CI;
|
||||
std::string TT_initial;
|
||||
bool US;
|
||||
};
|
||||
|
||||
class IemDcrUnexpectedResponseException : public std::exception {
|
||||
public:
|
||||
const char* what() {
|
||||
return this->reason.c_str();
|
||||
}
|
||||
|
||||
explicit IemDcrUnexpectedResponseException(std::string reason) : reason(std::move(reason)) {
|
||||
}
|
||||
|
||||
private:
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
class UnexpectedIemDcrResponseBody : public IemDcrUnexpectedResponseException {
|
||||
public:
|
||||
UnexpectedIemDcrResponseBody(std::string endpoint, std::string error) :
|
||||
IemDcrUnexpectedResponseException(
|
||||
fmt::format("Received unexpected response body from endpoint {}: {}", endpoint, error)),
|
||||
endpoint(std::move(endpoint)),
|
||||
error(std::move(error)) {
|
||||
}
|
||||
|
||||
private:
|
||||
std::string endpoint;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
class UnexpectedIemDcrResponseCode : public IemDcrUnexpectedResponseException {
|
||||
public:
|
||||
const std::string endpoint;
|
||||
const HttpResponse response;
|
||||
const std::string body;
|
||||
|
||||
UnexpectedIemDcrResponseCode(const std::string& endpoint, unsigned int expected_code,
|
||||
const HttpResponse& response) :
|
||||
IemDcrUnexpectedResponseException(fmt::format(
|
||||
"Received unexpected response from endpoint '{}': {} (expected {}) {}", endpoint, response.status_code,
|
||||
expected_code, !response.body.empty() ? " - body: " + response.body : "")),
|
||||
endpoint(endpoint),
|
||||
response(response) {
|
||||
}
|
||||
};
|
||||
|
||||
class ThreadSafeString {
|
||||
public:
|
||||
ThreadSafeString() : value("") {
|
||||
}
|
||||
|
||||
void store(const std::string& new_value) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
value = new_value;
|
||||
}
|
||||
|
||||
std::string load() const {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex mutex;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
bool init();
|
||||
json get_gw();
|
||||
bool check_gw_is_empty();
|
||||
void post_gw();
|
||||
void post_tariff(std::string tariff_info);
|
||||
std::tuple<types::powermeter::Powermeter, std::string, bool> get_metervalue();
|
||||
std::string get_publickey(bool allow_cached_value);
|
||||
std::string get_datetime();
|
||||
void post_datetime();
|
||||
void refresh_datetime_if_required();
|
||||
void post_user(const types::powermeter::OCMFUserIdentificationStatus IS,
|
||||
const std::optional<types::powermeter::OCMFIdentificationLevel> IL,
|
||||
const std::vector<types::powermeter::OCMFIdentificationFlags>& IF,
|
||||
const types::powermeter::OCMFIdentificationType& IT,
|
||||
const std::optional<std::__cxx11::basic_string<char>>& ID,
|
||||
const std::optional<std::__cxx11::basic_string<char>>& TT);
|
||||
types::units_signed::SignedMeterValue get_receipt();
|
||||
types::units_signed::SignedMeterValue get_transaction();
|
||||
void post_receipt(const std::string& TX);
|
||||
|
||||
IsaIemDcrController() = delete;
|
||||
IsaIemDcrController(std::unique_ptr<HttpClientInterface> http_client, const SnapshotConfig& snap_config);
|
||||
|
||||
template <typename Callable>
|
||||
static auto call_with_retry(Callable func, int number_of_retries, int retry_wait_in_milliseconds,
|
||||
bool retry_on_http_client_error = true, bool retry_on_iemdcr_reponse_error = true)
|
||||
-> decltype(func()) {
|
||||
std::exception_ptr last_exception = nullptr;
|
||||
for (int attempt = 0; attempt < 1 + number_of_retries; ++attempt) {
|
||||
try {
|
||||
return func();
|
||||
} catch (HttpClientError& http_client_error) {
|
||||
last_exception = std::current_exception();
|
||||
if (!retry_on_http_client_error) {
|
||||
std::rethrow_exception(last_exception);
|
||||
}
|
||||
EVLOG_warning << "HTTPClient request failed: " << http_client_error.what() << "; retry in "
|
||||
<< retry_wait_in_milliseconds << " milliseconds";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_wait_in_milliseconds));
|
||||
} catch (IemDcrUnexpectedResponseException& iemdcr_error) {
|
||||
last_exception = std::current_exception();
|
||||
if (!retry_on_iemdcr_reponse_error) {
|
||||
std::rethrow_exception(last_exception);
|
||||
}
|
||||
EVLOG_warning << "Unexpected IEM-DCR response: " << iemdcr_error.what() << "; retry in "
|
||||
<< retry_wait_in_milliseconds << " milliseconds";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_wait_in_milliseconds));
|
||||
}
|
||||
}
|
||||
std::rethrow_exception(last_exception);
|
||||
}
|
||||
|
||||
private:
|
||||
const std::unique_ptr<HttpClientInterface> http_client;
|
||||
SnapshotConfig snapshot_config;
|
||||
std::string cached_public_key = "";
|
||||
std::chrono::minutes zone_time_offset;
|
||||
std::atomic<std::chrono::time_point<std::chrono::steady_clock>> last_datetime_sync;
|
||||
|
||||
std::chrono::minutes helper_convert_timezone(std::string& timezone);
|
||||
bool helper_is_daylight_saving_time();
|
||||
std::string helper_get_current_datetime();
|
||||
std::string helper_remove_first_and_last_char(const std::string& input);
|
||||
bool helper_get_bool_from_OCMFUserIdentificationStatus(types::powermeter::OCMFUserIdentificationStatus IS);
|
||||
std::string
|
||||
helper_get_string_from_OCMFIdentificationLevel(std::optional<types::powermeter::OCMFIdentificationLevel> IL);
|
||||
std::string helper_get_string_from_OCMFIdentificationFlags(types::powermeter::OCMFIdentificationFlags id_flag);
|
||||
std::string helper_get_string_from_OCMFIdentificationType(types::powermeter::OCMFIdentificationType IT);
|
||||
types::units_signed::SignedMeterValue helper_get_signed_datatuple(const std::string& endpoint);
|
||||
};
|
||||
|
||||
} // namespace module::main
|
||||
|
||||
#endif // EVEREST_CORE_MODULE_ISAIEMDCRCONTROLLER_H
|
||||
@@ -0,0 +1,272 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "powermeterImpl.hpp"
|
||||
#include "httpClient.hpp"
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
void powermeterImpl::init() {
|
||||
// Check Config values (essential plausibility checks)
|
||||
check_config();
|
||||
|
||||
// Dependency injection pattern: Create the HTTP client first,
|
||||
// then move it into the controller as a constructor argument
|
||||
auto http_client = std::make_unique<HttpClient>(mod->config.ip_address, mod->config.port_http);
|
||||
|
||||
// Create controller object
|
||||
this->controller = std::make_unique<IsaIemDcrController>(
|
||||
std::move(http_client),
|
||||
IsaIemDcrController::SnapshotConfig{
|
||||
mod->config.timezone, mod->config.timezone_handle_DST, mod->config.datetime_resync_interval,
|
||||
mod->config.resilience_initial_connection_retry_delay, mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay, mod->config.CT, mod->config.CI,
|
||||
mod->config.TT_initial, mod->config.US});
|
||||
}
|
||||
|
||||
void powermeterImpl::ready() {
|
||||
// Start the live_measure_publisher thread, which periodically publishes the live measurements of the device
|
||||
this->live_measure_publisher_thread = std::thread([this] {
|
||||
while (true) {
|
||||
try {
|
||||
// Wait for at least one second (more if handle_start_transaction() or handle_stop_transaction() active)
|
||||
do {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
} while (start_transaction_running.load() == true || stop_transaction_running.load() == true);
|
||||
// Init if needed
|
||||
if (is_initialized.load() == false) {
|
||||
is_initialized.store(this->controller->init());
|
||||
if (is_initialized.load() == true) {
|
||||
// Publish public key once after init
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
this->publish_public_key_ocmf(this->controller->get_publickey(false));
|
||||
} else {
|
||||
if (!this->error_state_monitor->is_error_active("powermeter/CommunicationFault",
|
||||
"Communication timed out")) {
|
||||
auto error = this->error_factory->create_error(
|
||||
"powermeter/CommunicationFault", "Communication timed out",
|
||||
"This error is raised due to communication timeout");
|
||||
raise_error(error);
|
||||
}
|
||||
EVLOG_warning << "Connecting to IEM-DCR failed. Retry in "
|
||||
<< mod->config.resilience_initial_connection_retry_delay << " milliseconds";
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(mod->config.resilience_initial_connection_retry_delay));
|
||||
}
|
||||
} else {
|
||||
// Publish metervalue node (named powermeter in EVerest) and update status information
|
||||
auto meter_value_response = this->controller->get_metervalue();
|
||||
types::powermeter::Powermeter tmp_powermeter;
|
||||
std::string tmp_status;
|
||||
bool tmp_transaction_active;
|
||||
std::tie(tmp_powermeter, tmp_status, tmp_transaction_active) = meter_value_response;
|
||||
this->publish_powermeter(tmp_powermeter);
|
||||
dcr_status.store(tmp_status);
|
||||
transaction_active.store(tmp_transaction_active);
|
||||
|
||||
// Debug output :)
|
||||
// EVLOG_info << this->controller->get_datetime();
|
||||
|
||||
// Update datetime in specified interval
|
||||
if (transaction_active.load() == false) {
|
||||
this->controller->refresh_datetime_if_required();
|
||||
}
|
||||
|
||||
// Reset previous error (if active)
|
||||
if (this->error_state_monitor->is_error_active("powermeter/CommunicationFault",
|
||||
"Communication timed out")) {
|
||||
clear_error("powermeter/CommunicationFault", "Communication timed out");
|
||||
}
|
||||
}
|
||||
} catch (HttpClientError& client_error) {
|
||||
is_initialized.store(false);
|
||||
if (!this->error_state_monitor->is_error_active("powermeter/CommunicationFault",
|
||||
"Communication timed out")) {
|
||||
EVLOG_error << "Failed to communicate with the powermeter due to http error: "
|
||||
<< client_error.what();
|
||||
auto error =
|
||||
this->error_factory->create_error("powermeter/CommunicationFault", "Communication timed out",
|
||||
"This error is raised due to communication timeout");
|
||||
raise_error(error);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
is_initialized.store(false);
|
||||
EVLOG_error << "Exception in cyclic IEM-DCR communication: " << e.what();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStartResponse
|
||||
powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& value) {
|
||||
// your code for cmd start_transaction goes here
|
||||
types::powermeter::TransactionStartResponse return_value;
|
||||
|
||||
start_transaction_running.store(true);
|
||||
|
||||
// Check preconditions
|
||||
if (value.evse_id != mod->config.CI && value.evse_id.length() > 0) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::NOT_SUPPORTED;
|
||||
return_value.error = "config: CI does not match evse_id. This is not allowed.";
|
||||
EVLOG_error << "Aborted: " << *return_value.error;
|
||||
return return_value;
|
||||
}
|
||||
if (is_initialized.load() == false) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = "Init of communication not finished yet. Please try again later.";
|
||||
EVLOG_error << "Aborted: " << *return_value.error;
|
||||
return return_value;
|
||||
}
|
||||
if (dcr_status.load() != "0x0000, 0x00000000, 0x00, 0x00") {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = "IEM-DCR is in error state. XC: " + dcr_status.load();
|
||||
EVLOG_error << "Aborted: " << *return_value.error;
|
||||
return return_value;
|
||||
}
|
||||
|
||||
// Perform action
|
||||
try {
|
||||
// Check if gw information is already set
|
||||
if (this->controller->check_gw_is_empty()) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = "Init seems to be missing. Re-Init triggered. Please try again later.";
|
||||
is_initialized.store(false);
|
||||
} else {
|
||||
// Stop transaction (if a transaction is still running)
|
||||
if (transaction_active.load() == true) {
|
||||
this->controller->call_with_retry([this]() { this->controller->post_receipt("E"); },
|
||||
mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay);
|
||||
} else {
|
||||
// Try to end transaction at least once.
|
||||
try {
|
||||
this->controller->post_receipt("E");
|
||||
} catch (...) {
|
||||
// Nothing to do here
|
||||
}
|
||||
}
|
||||
// Wait for being surely in idle mode
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
// Create user
|
||||
if ((static_cast<std::string>(value.identification_data.value_or(""))).length() <= 0) {
|
||||
this->controller->call_with_retry(
|
||||
[this, value]() {
|
||||
this->controller->post_user(value.identification_status, value.identification_level,
|
||||
value.identification_flags, value.identification_type,
|
||||
value.transaction_id, value.tariff_text);
|
||||
},
|
||||
mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay);
|
||||
} else {
|
||||
this->controller->call_with_retry(
|
||||
[this, value]() {
|
||||
this->controller->post_user(value.identification_status, value.identification_level,
|
||||
value.identification_flags, value.identification_type,
|
||||
value.identification_data, value.tariff_text);
|
||||
},
|
||||
mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay);
|
||||
}
|
||||
// Start transaction
|
||||
this->controller->call_with_retry([this]() { this->controller->post_receipt("B"); },
|
||||
mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay);
|
||||
// Prepare positive response
|
||||
transaction_active.store(true);
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::OK;
|
||||
return_value.error = "";
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = e.what();
|
||||
EVLOG_error << "Aborted: " << return_value.error.value_or("");
|
||||
}
|
||||
|
||||
start_transaction_running.store(false);
|
||||
|
||||
return return_value;
|
||||
}
|
||||
|
||||
types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transaction(std::string& transaction_id) {
|
||||
// your code for cmd stop_transaction goes here
|
||||
types::powermeter::TransactionStopResponse return_value;
|
||||
|
||||
stop_transaction_running.store(true);
|
||||
|
||||
if (is_initialized.load() == false) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = "Init of communication not finished yet.";
|
||||
EVLOG_error << "Aborted: " << *return_value.error;
|
||||
} else if (transaction_active.load() == true) {
|
||||
try {
|
||||
// Stop transaction
|
||||
this->controller->call_with_retry([this]() { this->controller->post_receipt("E"); },
|
||||
mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay);
|
||||
// Wait for signature calculation
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
// Read receipt
|
||||
return_value.signed_meter_value =
|
||||
this->controller->call_with_retry([this]() { return this->controller->get_receipt(); },
|
||||
mod->config.resilience_transaction_request_retries,
|
||||
mod->config.resilience_transaction_request_retry_delay);
|
||||
// Prepare positive response
|
||||
transaction_active.store(false);
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::OK;
|
||||
return_value.error = "";
|
||||
} catch (const std::exception& e) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = e.what();
|
||||
EVLOG_error << "Aborted: " << return_value.error.value_or("");
|
||||
}
|
||||
} else {
|
||||
// No transaction running. So return last transaction (if available)
|
||||
try {
|
||||
return_value.signed_meter_value = this->controller->get_transaction();
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::OK;
|
||||
return_value.error = "";
|
||||
} catch (std::exception& e) {
|
||||
return_value.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR;
|
||||
return_value.error = std::string(e.what()) + " Maybe no transaction to return?";
|
||||
EVLOG_warning << "Aborted: " << return_value.error.value_or("");
|
||||
}
|
||||
}
|
||||
|
||||
stop_transaction_running.store(false);
|
||||
|
||||
return return_value;
|
||||
}
|
||||
|
||||
void powermeterImpl::check_config() {
|
||||
// Numeric range checks are aready covered by manifest minimum and maximum declaration
|
||||
if (mod->config.ip_address.length() <= 0) {
|
||||
EVLOG_error << "Incorrect module config: parameter ip_address is empty." << std::endl;
|
||||
throw std::runtime_error("ip_address invalid. Please check configuration.");
|
||||
}
|
||||
if (mod->config.timezone.length() != 5) {
|
||||
EVLOG_error
|
||||
<< "Incorrect module config: parameter timezone has invalid length. 5 characters expected, e.g. +0200"
|
||||
<< std::endl;
|
||||
throw std::runtime_error("Timezone invalid. Please check configuration.");
|
||||
}
|
||||
if (mod->config.timezone[0] != '+' && mod->config.timezone[0] != '-') {
|
||||
EVLOG_error << "Incorrect module config: parameter timezone has invalid format. It must begin with + or - char."
|
||||
<< std::endl;
|
||||
throw std::runtime_error("Timezone invalid. Please check configuration.");
|
||||
}
|
||||
if (mod->config.CT.length() <= 0) {
|
||||
EVLOG_error << "Incorrect module config: parameter CT is empty." << std::endl;
|
||||
throw std::runtime_error("CT invalid. Please check configuration.");
|
||||
}
|
||||
if (mod->config.CI.length() <= 0) {
|
||||
EVLOG_error << "Incorrect module config: parameter CI is empty." << std::endl;
|
||||
throw std::runtime_error("CI invalid. Please check configuration.");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
@@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef MAIN_POWERMETER_IMPL_HPP
|
||||
#define MAIN_POWERMETER_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/powermeter/Implementation.hpp>
|
||||
|
||||
#include "../IsabellenhuetteIemDcr.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
#include "httpClientInterface.hpp"
|
||||
#include "isabellenhuetteIemDcrController.hpp"
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class powermeterImpl : public powermeterImplBase {
|
||||
public:
|
||||
powermeterImpl() = delete;
|
||||
powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<IsabellenhuetteIemDcr>& mod, Conf& config) :
|
||||
powermeterImplBase(ev, "main"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual types::powermeter::TransactionStartResponse
|
||||
handle_start_transaction(types::powermeter::TransactionReq& value) override;
|
||||
virtual types::powermeter::TransactionStopResponse handle_stop_transaction(std::string& transaction_id) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<IsabellenhuetteIemDcr>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
// insert your private definitions here
|
||||
IsaIemDcrController::ThreadSafeString dcr_status;
|
||||
std::atomic<bool> is_initialized = false;
|
||||
std::atomic<bool> transaction_active = true;
|
||||
std::atomic<bool> start_transaction_running = false;
|
||||
std::atomic<bool> stop_transaction_running = false;
|
||||
// At construction time, there is no controller and no HTTP client, so these are null pointers.
|
||||
// When init() is called, the controller is initialized.
|
||||
std::unique_ptr<IsaIemDcrController> controller = nullptr;
|
||||
// The live_measure_publisher thread handles the periodic (1/s) publication of the live measurements
|
||||
// Initially it's a default-constructed thread (which is valid, but doesn't represent an actual running thread)
|
||||
// In ready(), the live_measure_publisher thread is started and placed in this field.
|
||||
std::thread live_measure_publisher_thread;
|
||||
// private functions
|
||||
void check_config();
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
|
||||
#endif // MAIN_POWERMETER_IMPL_HPP
|
||||
Reference in New Issue
Block a user