From 4141b5ae15b32f07e0ad23d10f5171b1a8687001 Mon Sep 17 00:00:00 2001 From: Josef Herbert Date: Thu, 20 Feb 2025 14:27:59 +0100 Subject: [PATCH 1/2] Added IEM-DCR module --- modules/IsabellenhuetteIemDcr/.clang-format | 138 ++++++ modules/IsabellenhuetteIemDcr/.eslintrc.json | 43 ++ modules/IsabellenhuetteIemDcr/CMakeLists.txt | 31 ++ .../IsabellenhuetteIemDcr.cpp | 15 + .../IsabellenhuetteIemDcr.hpp | 71 +++ modules/IsabellenhuetteIemDcr/LICENSE | 201 +++++++++ modules/IsabellenhuetteIemDcr/doc.rst | 66 +++ .../main/http_client.cpp | 153 +++++++ .../main/http_client.hpp | 50 +++ .../main/http_client_interface.hpp | 42 ++ .../isabellenhuette_IemDcr_controller.cpp | 408 ++++++++++++++++++ .../isabellenhuette_IemDcr_controller.hpp | 165 +++++++ .../main/powermeterImpl.cpp | 258 +++++++++++ .../main/powermeterImpl.hpp | 78 ++++ modules/IsabellenhuetteIemDcr/manifest.yaml | 58 +++ 15 files changed, 1777 insertions(+) create mode 100644 modules/IsabellenhuetteIemDcr/.clang-format create mode 100644 modules/IsabellenhuetteIemDcr/.eslintrc.json create mode 100644 modules/IsabellenhuetteIemDcr/CMakeLists.txt create mode 100644 modules/IsabellenhuetteIemDcr/IsabellenhuetteIemDcr.cpp create mode 100644 modules/IsabellenhuetteIemDcr/IsabellenhuetteIemDcr.hpp create mode 100644 modules/IsabellenhuetteIemDcr/LICENSE create mode 100644 modules/IsabellenhuetteIemDcr/doc.rst create mode 100644 modules/IsabellenhuetteIemDcr/main/http_client.cpp create mode 100644 modules/IsabellenhuetteIemDcr/main/http_client.hpp create mode 100644 modules/IsabellenhuetteIemDcr/main/http_client_interface.hpp create mode 100644 modules/IsabellenhuetteIemDcr/main/isabellenhuette_IemDcr_controller.cpp create mode 100644 modules/IsabellenhuetteIemDcr/main/isabellenhuette_IemDcr_controller.hpp create mode 100644 modules/IsabellenhuetteIemDcr/main/powermeterImpl.cpp create mode 100644 modules/IsabellenhuetteIemDcr/main/powermeterImpl.hpp create mode 100644 modules/IsabellenhuetteIemDcr/manifest.yaml diff --git a/modules/IsabellenhuetteIemDcr/.clang-format b/modules/IsabellenhuetteIemDcr/.clang-format new file mode 100644 index 000000000..42df3fa66 --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/.clang-format @@ -0,0 +1,138 @@ +--- +Language: Cpp +# BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: true +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: AfterColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + - Regex: '.*' + Priority: 1 + SortPriority: 0 +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +Standard: Latest +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +... + diff --git a/modules/IsabellenhuetteIemDcr/.eslintrc.json b/modules/IsabellenhuetteIemDcr/.eslintrc.json new file mode 100644 index 000000000..35bee0fa0 --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/.eslintrc.json @@ -0,0 +1,43 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es2021": true + }, + "extends": [ + "airbnb-base" + ], + "parserOptions": { + "ecmaVersion": 12 + }, + "rules": { + "camelcase": "off", + "eqeqeq": [ + "error", + "smart" + ], + "comma-dangle": [ + "warn", + { + "objects": "always-multiline", + "arrays": "always-multiline", + "functions": "never" + } + ], + "import/no-unresolved": [ + 2, + { + "ignore": [ + "everestjs" + ] + } + ], + "max-len": [ + "warn", + { + "code": 120, + "tabWidth": 2 + } + ] + } +} \ No newline at end of file diff --git a/modules/IsabellenhuetteIemDcr/CMakeLists.txt b/modules/IsabellenhuetteIemDcr/CMakeLists.txt new file mode 100644 index 000000000..b9dbeca35 --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/CMakeLists.txt @@ -0,0 +1,31 @@ +# +# AUTO GENERATED - MARKED REGIONS WILL BE KEPT +# template version 3 +# + +# module setup: +# - ${MODULE_NAME}: module name +ev_setup_cpp_module() + +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 +# insert your custom targets and additional config variables here +target_link_libraries(${MODULE_NAME} PRIVATE + CURL::libcurl + nlohmann_json::nlohmann_json +) + +target_sources(${MODULE_NAME} + PRIVATE + main/isabellenhuette_IemDcr_controller.cpp + main/http_client.cpp +) +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 + +target_sources(${MODULE_NAME} + PRIVATE + "main/powermeterImpl.cpp" +) + +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 +# insert other things like install cmds etc here +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/IsabellenhuetteIemDcr/IsabellenhuetteIemDcr.cpp b/modules/IsabellenhuetteIemDcr/IsabellenhuetteIemDcr.cpp new file mode 100644 index 000000000..b7f9c866d --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/IsabellenhuetteIemDcr.cpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include "IsabellenhuetteIemDcr.hpp" + +namespace module { + +void IsabellenhuetteIemDcr::init() { + invoke_init(*p_main); +} + +void IsabellenhuetteIemDcr::ready() { + invoke_ready(*p_main); +} + +} // namespace module diff --git a/modules/IsabellenhuetteIemDcr/IsabellenhuetteIemDcr.hpp b/modules/IsabellenhuetteIemDcr/IsabellenhuetteIemDcr.hpp new file mode 100644 index 000000000..8be325eca --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/IsabellenhuetteIemDcr.hpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef ISABELLENHUETTE_IEM_DCR_HPP +#define ISABELLENHUETTE_IEM_DCR_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 2 +// + +#include "ld-ev.hpp" + +// headers for provided interface implementations +#include + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +// insert your custom include headers here +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 + +namespace module { + +struct Conf { + std::string ip_address; + int port_http; + std::string timezone; + int datetime_resync_interval; + int resilience_initial_connection_retries; + int resilience_initial_connection_retry_delay; + int resilience_transaction_request_retries; + int resilience_transaction_request_retry_delay; + std::string CT; + std::string CI; + std::string TT_initial; + bool US; +}; + +class IsabellenhuetteIemDcr : public Everest::ModuleBase { +public: + IsabellenhuetteIemDcr() = delete; + IsabellenhuetteIemDcr(const ModuleInfo& info, std::unique_ptr p_main, Conf& config) : + ModuleBase(info), p_main(std::move(p_main)), config(config){}; + + const std::unique_ptr p_main; + const Conf& config; + + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + // insert your public definitions here + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + +protected: + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + // insert your protected definitions here + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + +private: + friend class LdEverest; + void init(); + void ready(); + + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 + // insert your private definitions here + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 +}; + +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 +// insert other definitions here +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 + +} // namespace module + +#endif // ISABELLENHUETTE_IEM_DCR_HPP diff --git a/modules/IsabellenhuetteIemDcr/LICENSE b/modules/IsabellenhuetteIemDcr/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/modules/IsabellenhuetteIemDcr/doc.rst b/modules/IsabellenhuetteIemDcr/doc.rst new file mode 100644 index 000000000..d53480b31 --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/doc.rst @@ -0,0 +1,66 @@ +.. _everest_modules_handwritten_IsabellenhuetteIemDcr: + +.. This file is a placeholder for an optional single file + handwritten documentation for the IsabellenhuetteIemDcr module. + Please decide whether you want to use this single file, + or a set of files in the doc/ directory. + In the latter case, you can delete this file. + In the former case, you can delete the doc/ directory. + +.. This handwritten documentation is optional. In case + you do not want to write it, you can delete this file + and the doc/ directory. + +.. The documentation can be written in reStructuredText, + and will be converted to HTML and PDF by Sphinx. + +******************************************* +IsabellenhuetteIemDcr +******************************************* + +:ref:`Link ` to the module's reference. +Module implements Isabellenhuette IEM-DCR power meter driver, connecting via HTTP/REST. + +Implementation details +=========== + +This section offers some additional information on driver implementation. The underlying HTTP communication functionality +is mainly duplicated from other open source powermeter modules of EVerest to support a standarization for this interface +later on. + +Initialization +------------ +It begins with checking some plausibility measures on the handed configuration. Its default values are given in manifest.yaml. +Please make shure to explicit specify values that deviate from default configuration before starting the driver. If there is no +conspicuousness in configuration, HTTP communication is verified with GET requests on /gw node. In case of no success, several +retries are performed (as specified in config). On success POST /gw is issued for transfering CI, CT and datetime to IEM-DCR. +Please note, that issuing POST /gw is only possible once after IEM-DCR power-up. So CI and CT are freezed until next power-cycle +and datetime will be automatically updated using another node (POST /datetime) in configurable intervals. Therefore a warning +will appear on EVerest console if CI and CT are already written and could not be updated. After this procedure the initial tariff +text is transferred as configured. This will show up on display before a charging transaction. + +Live values +------------ +Each second the MQTT variable Powermeter is updated to current values of /metervalue node. Also the public key is made available +via MQTT. + +Start transaction +------------ +Starting a transaction is not possible if a transaction is already in progress. This will return TransactionRequestStatus::NOT_SUPPORTED. +The same status type is also returned, if given evse_id does not match CI (which was already transfered in initialization phase) and if +IEM-DCR is in error state. Please refer to TransactionStartResponse.error for distinguishing between the errors. Starting a charging +transaction will engage POST /user and POST /receipt. Please note that IEM-DCR automatically handles signed data tuple pagination. So +the only place for transaction id defined by the charging station is the OCMF ID attribute. It will be filled from this driver with +TransactionReq.identification_data. If this optional attribute is not given or empty, TransactionReq.transaction_id will be used +instead. + +Stop transaction +------------ +If a transaction is in progress, it will be stopped and its signed data tuple returned. If no transaction is running, the last signed +data tuple will be returned. Therefore input parameter transaction_id of this routine has no impact on its operation. Please note that +TransactionRequestStatus::UNEXPECTED_ERROR may be returned, if no transaction is in progress and there has also been no transaction +before. + +References +============ +`Official website `_ diff --git a/modules/IsabellenhuetteIemDcr/main/http_client.cpp b/modules/IsabellenhuetteIemDcr/main/http_client.cpp new file mode 100644 index 000000000..57faca709 --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/main/http_client.cpp @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "http_client.hpp" +#include +#include + +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); + 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 diff --git a/modules/IsabellenhuetteIemDcr/main/http_client.hpp b/modules/IsabellenhuetteIemDcr/main/http_client.hpp new file mode 100644 index 000000000..eb6c7b69b --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/main/http_client.hpp @@ -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 "http_client_interface.hpp" +#include +#include +#include +#include +#include + +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 diff --git a/modules/IsabellenhuetteIemDcr/main/http_client_interface.hpp b/modules/IsabellenhuetteIemDcr/main/http_client_interface.hpp new file mode 100644 index 000000000..f879bc66e --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/main/http_client_interface.hpp @@ -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 + +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 diff --git a/modules/IsabellenhuetteIemDcr/main/isabellenhuette_IemDcr_controller.cpp b/modules/IsabellenhuetteIemDcr/main/isabellenhuette_IemDcr_controller.cpp new file mode 100644 index 000000000..82697374b --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/main/isabellenhuette_IemDcr_controller.cpp @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "isabellenhuette_IemDcr_controller.hpp" +#include +namespace module::main { + +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); + } +} + +void IsaIemDcrController::post_gw() { + const std::string endpoint = "/counter/v1/ocmf/gw"; + const std::string payload = nlohmann::ordered_json{{"CT", snapshotConfig.CT}, + {"CI", snapshotConfig.CI}, + {"TM", helper_get_current_datetime()}} + .dump(); + auto response = this->http_client->post(endpoint, payload); + if (response.status_code != 200) { + throw UnexpectedIemDcrResponseCode(endpoint, 200, response); + } +} + +void IsaIemDcrController::post_tariff(std::string tariffInfo) { + const std::string endpoint = "/counter/v1/ocmf/tariff"; + const std::string payload = nlohmann::ordered_json{{"TT", tariffInfo}} + .dump(); + auto response = this->http_client->post(endpoint, payload); + if (response.status_code != 200) { + throw UnexpectedIemDcrResponseCode(endpoint, 200, response); + } +} + +std::tuple 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 transactionActive = 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()}); + //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, transactionActive); + } 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 allowCachedValue) { + if(allowCachedValue && cachedPublicKey.length() > 0) { + return cachedPublicKey; + } else { + const std::string endpoint = "/counter/v1/ocmf/publickey"; + auto response = this->http_client->get(endpoint); + if (response.status_code != 200) { + EVLOG_warning << "Response is not 200." << std::endl; + return ""; + } + try { + json data = json::parse(response.body); + cachedPublicKey = data.at("PK"); + return cachedPublicKey; + } catch (json::exception& json_error) { + EVLOG_warning << "JSON error" << 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) { + throw UnexpectedIemDcrResponseCode(endpoint, 200, response); + } +} + +void IsaIemDcrController::post_user(const types::powermeter::OCMFUserIdentificationStatus IS, + const std::optional IL, + const std::vector IF, + const types::powermeter::OCMFIdentificationType& IT, + const std::optional>& ID, + const std::optional>& 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(ID.value_or("")); + std::string strTT = static_cast(TT.value_or("")); + std::string payload = ""; + std::vector vectIF; + + //Fill vectIF + for (const types::powermeter::OCMFIdentificationFlags& idFlag : IF) { + vectIF.push_back(helper_get_string_from_OCMFIdentificationFlags(idFlag)); + } + + if(strTT.length() > 0) { + payload = nlohmann::ordered_json{{"IS", boolIS}, + {"IL", strIL}, + {"IF", vectIF}, + {"IT", strIT}, + {"ID", strID}, + {"US", snapshotConfig.US}, + {"TT", strTT}} + .dump(); + } else { + payload = nlohmann::ordered_json{{"IS", boolIS}, + {"IL", strIL}, + {"IF", vectIF}, + {"IT", strIT}, + {"ID", strID}, + {"US", snapshotConfig.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() { + return helper_get_signed_datatuple("/counter/v1/ocmf/receipt"); +} + +types::units_signed::SignedMeterValue IsaIemDcrController::get_transaction() { + return helper_get_signed_datatuple("/counter/v1/ocmf/transaction"); +} + +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 optIL) { + std::string result; + types::powermeter::OCMFIdentificationLevel IL = optIL.value_or(types::powermeter::OCMFIdentificationLevel::UNKNOWN); + switch(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 idFlag) { + std::string result; + switch(idFlag) { + 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::string IsaIemDcrController::helper_get_current_datetime() { + //Get UTC time + auto now = std::chrono::system_clock::now(); + //Add configured timezone information + char signChar = snapshotConfig.timezone[0]; + int offsetHours = std::stoi(snapshotConfig.timezone.substr(1, 2)); + int offsetMinutes = std::stoi(snapshotConfig.timezone.substr(3, 2)); + auto timeOffset = std::chrono::hours(offsetHours) + std::chrono::minutes(offsetMinutes); + std::time_t nowWithOffset; + if(signChar == '+') { + nowWithOffset = std::chrono::system_clock::to_time_t(now + timeOffset); + } else if(signChar == '-') { + nowWithOffset = std::chrono::system_clock::to_time_t(now - timeOffset); + } else { + throw std::runtime_error("manifest.yaml: Format of timezone not supported. Expected: something like \"+0100\"."); + } + //Generate and return time in correct format + std::ostringstream ss; + ss << std::put_time(gmtime(&nowWithOffset), "%FT%T,000") << snapshotConfig.timezone; + return ss.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(std::string endpoint) { + auto response = this->http_client->get(endpoint); + types::units_signed::SignedMeterValue retVal; + if (response.status_code == 200) { + try { + retVal.signed_meter_data = response.body; + retVal.signing_method = ""; + retVal.encoding_method = "OCMF"; + retVal.public_key = get_publickey(true); + + return retVal; + } 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 diff --git a/modules/IsabellenhuetteIemDcr/main/isabellenhuette_IemDcr_controller.hpp b/modules/IsabellenhuetteIemDcr/main/isabellenhuette_IemDcr_controller.hpp new file mode 100644 index 000000000..ae770d104 --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/main/isabellenhuette_IemDcr_controller.hpp @@ -0,0 +1,165 @@ +// 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 "http_client_interface.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace module::main { + +class IsaIemDcrController { + +public: + struct SnapshotConfig { + std::string timezone; + int datetime_resync_interval; + int resilience_initial_connection_retries; + 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& newValue) { + std::lock_guard lock(mutex); + value = newValue; + } + + std::string load() const { + std::lock_guard lock(mutex); + return value; + } + + private: + mutable std::mutex mutex; + std::string value; + }; + + json get_gw(); + void post_gw(); + void post_tariff(std::string tariffInfo); + std::tuple get_metervalue(); + std::string get_publickey(bool allowCachedValue); + std::string get_datetime(); + void post_datetime(); + void post_user(const types::powermeter::OCMFUserIdentificationStatus IS, + const std::optional IL, + const std::vector IF, + const types::powermeter::OCMFIdentificationType& IT, + const std::optional>& ID, + const std::optional>& TT); + types::units_signed::SignedMeterValue get_receipt(); + types::units_signed::SignedMeterValue get_transaction(); + void post_receipt(const std::string& TX); + + IsaIemDcrController() = delete; + explicit IsaIemDcrController(std::unique_ptr http_client, const SnapshotConfig& snapConfig) : + http_client(std::move(http_client)), snapshotConfig(snapConfig) { + //Member Initializer List is used + } + + template + 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 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 (IemDcrUnexpectedResponseException& iemdcr_error) { + lastException = std::current_exception(); + if (!retry_on_iemdcr_reponse_error) { + std::rethrow_exception(lastException); + } + 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(lastException); + } + +private: + + const std::unique_ptr http_client; + SnapshotConfig snapshotConfig; + std::string cachedPublicKey = ""; + + 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 IL); + std::string helper_get_string_from_OCMFIdentificationFlags(types::powermeter::OCMFIdentificationFlags idFlag); + std::string helper_get_string_from_OCMFIdentificationType(types::powermeter::OCMFIdentificationType IT); + types::units_signed::SignedMeterValue helper_get_signed_datatuple(std::string endpoint); + +}; + +} // namespace module::main + +#endif // EVEREST_CORE_MODULE_ISAIEMDCRCONTROLLER_H diff --git a/modules/IsabellenhuetteIemDcr/main/powermeterImpl.cpp b/modules/IsabellenhuetteIemDcr/main/powermeterImpl.cpp new file mode 100644 index 000000000..ef78cafba --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/main/powermeterImpl.cpp @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "powermeterImpl.hpp" +#include "http_client.hpp" +#include +#include +#include + +namespace module { +namespace main { + +void powermeterImpl::init() { + EVLOG_info << "Isabellenhuette IEM-DCR: Init started..."; + + //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(mod->config.ip_address, mod->config.port_http); + + //Create controller object + this->controller = std::make_unique(std::move(http_client), + IsaIemDcrController::SnapshotConfig{mod->config.timezone, mod->config.datetime_resync_interval, + 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.CT, mod->config.CI, mod->config.TT_initial, mod->config.US}); + + //Store datetime resync interval in a threadsafe manner + dateTimeResyncInterval.store(mod->config.datetime_resync_interval); + + //Check connection with polling REST node gw + this->controller->call_with_retry([this]() { this->controller->get_gw(); }, + mod->config.resilience_initial_connection_retries, + mod->config.resilience_initial_connection_retry_delay); + + //Send gw information + try { + this->controller->post_gw(); + lastDateTimeSync.store(std::chrono::steady_clock::now()); + } 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."; + //If gw is already set, not TM information is transfered here. So mark time as invalid for later update in ready function + lastDateTimeSync.store(std::chrono::steady_clock::now() - std::chrono::hours(48)); + } + + //Send initial tariff information + try { + if(mod->config.TT_initial.length() > 0) { + this->controller->post_tariff(mod->config.TT_initial); + } + } catch (IsaIemDcrController::UnexpectedIemDcrResponseCode& error) { + EVLOG_error << "Incorrect config: Value TT_initial could not be set. Please check its value."; + } +} + +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) { + //Wait for one second + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + try { + //Publish public key + this->publish_public_key_ocmf(this->controller->get_publickey(true)); + + //Publish metervalue node (named powermeter in EVerest) and update status information + auto meterValueResponse = this->controller->get_metervalue(); + types::powermeter::Powermeter tmpPowermeter; + std::string tmpErrorState; + bool tmpTransactionActive; + std::tie(tmpPowermeter, tmpErrorState, tmpTransactionActive) = meterValueResponse; + this->publish_powermeter(tmpPowermeter); + errorState.store(tmpErrorState); + transactionActive.store(tmpTransactionActive); + + //Debug output :) + //EVLOG_info << this->controller->get_datetime(); + + //Update datetime in specified interval + if(transactionActive.load() == false) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - lastDateTimeSync.load()); + if (elapsed.count() >= dateTimeResyncInterval.load()) { + this->controller->post_datetime(); + lastDateTimeSync.store(now); + EVLOG_info << "DateTime resynchronized."; + } + } + + } catch (HttpClientError& client_error) { + if (!this->error_state_monitor->is_error_active("powermeter/CommunicationFault", + "Communication timeout")) { + 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) { + 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 retVal; + + EVLOG_info << "handle_start_transaction() called."; + + //Check preconditions + if(value.evse_id != mod->config.CI && value.evse_id.length() > 0) { + retVal.status = types::powermeter::TransactionRequestStatus::NOT_SUPPORTED; + retVal.error = "config: CI does not match evse_id. This is not allowed."; + EVLOG_error << "Aborted: " << *retVal.error; + return retVal; + } + if(errorState.load() != "0x0000, 0x00000000, 0x00, 0x00") { + retVal.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR; + retVal.error = "IEM-DCR is in error state. XC: " + errorState.load(); + EVLOG_error << "Aborted: " << *retVal.error; + return retVal; + } + + //Perform action + try { + //Stop transaction (if a transaction is still running) + if(transactionActive) { + this->controller->call_with_retry([this]() { this->controller->post_receipt("E"); }, + mod->config.resilience_transaction_request_retries, + mod->config.resilience_transaction_request_retry_delay); + } + //Create user + if((static_cast(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 + retVal.status = types::powermeter::TransactionRequestStatus::OK; + retVal.error = ""; + } catch (const std::exception& e) { + retVal.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR; + retVal.error = e.what(); + EVLOG_error << "Aborted: " << retVal.error.value_or(""); + } + + return retVal; +} + +types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transaction(std::string& transaction_id) { + // your code for cmd stop_transaction goes here + types::powermeter::TransactionStopResponse retVal; + + EVLOG_info << "handle_stop_transaction() called."; + + if(transactionActive) { + 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(200)); + //Read receipt + retVal.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 + retVal.status = types::powermeter::TransactionRequestStatus::OK; + retVal.error = ""; + } catch (const std::exception& e) { + retVal.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR; + retVal.error = e.what(); + EVLOG_error << "Aborted: " << retVal.error.value_or(""); + } + } else { + //No transaction running. So return last transaction (if available) + try { + retVal.signed_meter_value = this->controller->get_transaction(); + retVal.status = types::powermeter::TransactionRequestStatus::OK; + retVal.error = ""; + } catch (std::exception& e) { + retVal.status = types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR; + retVal.error = std::string(e.what()) + " Maybe no transaction to return?"; + EVLOG_warning << "Aborted: " << retVal.error.value_or(""); + } + } + + return retVal; +} + +void powermeterImpl::check_config() { + 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.port_http < 0) { + EVLOG_error << "Incorrect module config: parameter port_http has a negative value." << std::endl; + throw std::runtime_error("port_http invalid. Please check configuration."); + } + if(mod->config.timezone.length() != 5) { + EVLOG_error << "Incorrect module config: parameter timezone has invalid length. 5 characters expected." << std::endl; + throw std::runtime_error("Timezone invalid. Please check configuration."); + } + if(mod->config.datetime_resync_interval <= 0) { + EVLOG_error << "Incorrect module config: The value of parameter datetime_resync_interval must be 1 or greater." << std::endl; + throw std::runtime_error("datetime_resync_interval invalid. Please check configuration."); + } + if(mod->config.resilience_initial_connection_retries < 0) { + EVLOG_error << "Incorrect module config: parameter resilience_initial_connection_retries has a negative value." << std::endl; + throw std::runtime_error("port_http resilience_initial_connection_retries. Please check configuration."); + } + if(mod->config.resilience_initial_connection_retry_delay < 0) { + EVLOG_error << "Incorrect module config: parameter resilience_initial_connection_retry_delay has a negative value." << std::endl; + throw std::runtime_error("port_http resilience_initial_connection_retry_delay. Please check configuration."); + } + if(mod->config.resilience_transaction_request_retries < 0) { + EVLOG_error << "Incorrect module config: parameter resilience_transaction_request_retries has a negative value." << std::endl; + throw std::runtime_error("port_http resilience_transaction_request_retries. Please check configuration."); + } + if(mod->config.resilience_transaction_request_retry_delay < 0) { + EVLOG_error << "Incorrect module config: parameter resilience_transaction_request_retry_delay has a negative value." << std::endl; + throw std::runtime_error("port_http resilience_transaction_request_retry_delay. 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 diff --git a/modules/IsabellenhuetteIemDcr/main/powermeterImpl.hpp b/modules/IsabellenhuetteIemDcr/main/powermeterImpl.hpp new file mode 100644 index 000000000..48a87535e --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/main/powermeterImpl.hpp @@ -0,0 +1,78 @@ +// 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 + +#include "../IsabellenhuetteIemDcr.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +// insert your custom include headers here +#include "http_client_interface.hpp" +#include "isabellenhuette_IemDcr_controller.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& mod, Conf& config) : + powermeterImplBase(ev, "main"), mod(mod), config(config), errorState(){}; + + // 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& 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 errorState; + std::atomic transactionActive = false; + std::atomic dateTimeResyncInterval = 12; + std::atomic> lastDateTimeSync; + // 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 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 diff --git a/modules/IsabellenhuetteIemDcr/manifest.yaml b/modules/IsabellenhuetteIemDcr/manifest.yaml new file mode 100644 index 000000000..30b8c791f --- /dev/null +++ b/modules/IsabellenhuetteIemDcr/manifest.yaml @@ -0,0 +1,58 @@ +description: Module implements Isabellenhuette IEM-DCR power meter driver, connecting via HTTP/REST +config: + ip_address: + description: IPv4 Address of the power meter API. + type: string + default: "192.168.60.12" + port_http: + description: HTTP-Port of the power meter API. + type: integer + default: 80 + timezone: + description: The timezone offset information according to ISO8601 (version without colon). + type: string + default: "+0100" + datetime_resync_interval: + description: Interval for cyclic time resync in hours. + type: integer + default: 12 + resilience_initial_connection_retries: + description: For the controller resilience, the number of retries to connect to the powermeter at module initialization. + type: integer + default: 25 + resilience_initial_connection_retry_delay: + description: For the controller resilience, the delay in milliseconds before a retry attempt at module initialization. + type: integer + default: 10000 + resilience_transaction_request_retries: + description: For the controller resilience, the number of retries to connect to the powermeter at a transaction start or stop request. + type: integer + default: 3 + resilience_transaction_request_retry_delay: + description: For the controller resilience, the delay in milliseconds before a retry attempt at a transaction start or stop request. + type: integer + default: 250 + CT: + description: Charge point identification type (part of the signed OCMF data tuple). + type: string + default: "EVSEID" + CI: + description: Charge point identification (part of the signed OCMF data tuple). + type: string + default: "1234" + TT_initial: + description: Initial tariff text. (Its current value is part of signed OCMF data tuple). + type: string + default: "" + US: + description: Controls whether UserID is shown on display or not. + type: boolean + default: false +provides: + main: + description: This is the main unit of the module + interface: powermeter +metadata: + license: https://opensource.org/licenses/Apache-2.0 + authors: + - Josef Herbert, josef.herbert@isabellenhuette.com From ba47ec6460de511167b4429cdcdd7d26fe04f149 Mon Sep 17 00:00:00 2001 From: jherbert-isa Date: Thu, 20 Feb 2025 14:53:12 +0100 Subject: [PATCH 2/2] Update doc.rst Deprecated information on starting a transaction while another is running is now corrected. Signed-off-by: jherbert-isa --- modules/IsabellenhuetteIemDcr/doc.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/IsabellenhuetteIemDcr/doc.rst b/modules/IsabellenhuetteIemDcr/doc.rst index d53480b31..64604ec26 100644 --- a/modules/IsabellenhuetteIemDcr/doc.rst +++ b/modules/IsabellenhuetteIemDcr/doc.rst @@ -25,7 +25,7 @@ Implementation details =========== This section offers some additional information on driver implementation. The underlying HTTP communication functionality -is mainly duplicated from other open source powermeter modules of EVerest to support a standarization for this interface +is mainly duplicated from other open source powermeter modules of EVerest to support a standarization of this interface later on. Initialization @@ -46,9 +46,9 @@ via MQTT. Start transaction ------------ -Starting a transaction is not possible if a transaction is already in progress. This will return TransactionRequestStatus::NOT_SUPPORTED. -The same status type is also returned, if given evse_id does not match CI (which was already transfered in initialization phase) and if -IEM-DCR is in error state. Please refer to TransactionStartResponse.error for distinguishing between the errors. Starting a charging +Starting a transaction will terminate any other running transaction (if there is one). The status type TransactionRequestStatus:: +NOT_SUPPORTED is returned, if given evse_id does not match CI (which was already transfered in initialization phase) and if IEM-DCR +is in error state. Please refer to retrurned TransactionStartResponse.error for distinguishing between them. Starting a charging transaction will engage POST /user and POST /receipt. Please note that IEM-DCR automatically handles signed data tuple pagination. So the only place for transaction id defined by the charging station is the OCMF ID attribute. It will be filled from this driver with TransactionReq.identification_data. If this optional attribute is not given or empty, TransactionReq.transaction_id will be used