From 9578416cfc2e33acc075a724ae31515523868574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piet=20G=C3=B6mpel?= Date: Mon, 18 Nov 2024 20:14:11 +0100 Subject: [PATCH] Moved DataTransfer functionality to DataTransfer functional block using the targeted design. Added test cases for the new functional block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Piet Gömpel --- include/ocpp/v201/charge_point.hpp | 5 +- .../v201/functional_blocks/data_transfer.hpp | 62 ++++++ lib/CMakeLists.txt | 1 + lib/ocpp/v201/charge_point.cpp | 66 +----- .../v201/functional_blocks/data_transfer.cpp | 80 +++++++ tests/lib/ocpp/v16/test_message_queue.cpp | 22 +- tests/lib/ocpp/v201/CMakeLists.txt | 2 + .../v201/functional_blocks/CMakeLists.txt | 6 + .../functional_blocks/test_data_transfer.cpp | 196 ++++++++++++++++++ 9 files changed, 369 insertions(+), 71 deletions(-) create mode 100644 include/ocpp/v201/functional_blocks/data_transfer.hpp create mode 100644 lib/ocpp/v201/functional_blocks/data_transfer.cpp create mode 100644 tests/lib/ocpp/v201/functional_blocks/CMakeLists.txt create mode 100644 tests/lib/ocpp/v201/functional_blocks/test_data_transfer.cpp diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index 7e4980573..11e971403 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -8,6 +8,7 @@ #include #include +#include #include @@ -388,6 +389,7 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa std::unique_ptr connectivity_manager; std::unique_ptr> message_dispatcher; + std::unique_ptr data_transfer; // utility std::shared_ptr> message_queue; @@ -759,9 +761,6 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void handle_set_display_message(Call call); void handle_clear_display_message(Call call); - // Functional Block P: DataTransfer - void handle_data_transfer_req(Call call); - // Generates async sending callbacks template std::function send_callback(MessageType expected_response_message_type) { diff --git a/include/ocpp/v201/functional_blocks/data_transfer.hpp b/include/ocpp/v201/functional_blocks/data_transfer.hpp new file mode 100644 index 000000000..474e93ee7 --- /dev/null +++ b/include/ocpp/v201/functional_blocks/data_transfer.hpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include +#include + +namespace ocpp { +namespace v201 { + +class DataTransferInterface { + +public: + /// \brief Sends a DataTransfer.req message to the CSMS using the given parameters + /// \param vendorId + /// \param messageId + /// \param data + /// \return DataTransferResponse containing the result from CSMS + virtual std::optional data_transfer_req(const CiString<255>& vendorId, + const std::optional>& messageId, + const std::optional& data) = 0; + + /// \brief Sends a DataTransfer.req message to the CSMS using the given \p request + /// \param request message shall be sent to the CSMS + /// \return DataTransferResponse containing the result from CSMS. In case no response is received from the CSMS + /// because the message timed out or the charging station is offline, std::nullopt is returned + virtual std::optional data_transfer_req(const DataTransferRequest& request) = 0; + + /// \brief Handles the given DataTransfer.req \p call by the CSMS by responding with a CallResult + virtual void handle_data_transfer_req(Call call) = 0; +}; + +class DataTransfer : public DataTransferInterface { + +private: + MessageDispatcherInterface& message_dispatcher; + std::optional> data_transfer_callback; + std::chrono::seconds response_timeout; + std::function is_websocket_connected; + +public: + DataTransfer(MessageDispatcherInterface& message_dispatcher, + const std::optional>& + data_transfer_callback, + const std::function is_websocket_connected, const std::chrono::seconds response_timeout) : + message_dispatcher(message_dispatcher), + data_transfer_callback(data_transfer_callback), + is_websocket_connected(is_websocket_connected), + response_timeout(response_timeout){}; + + void handle_data_transfer_req(Call call) override; + + std::optional data_transfer_req(const CiString<255>& vendorId, + const std::optional>& messageId, + const std::optional& data) override; + + std::optional data_transfer_req(const DataTransferRequest& request) override; +}; + +} // namespace v201 +} // namespace ocpp diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 9653406bc..34e03083f 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -82,6 +82,7 @@ if(LIBOCPP_ENABLE_V201) ocpp/v201/component_state_manager.cpp ocpp/v201/connectivity_manager.cpp ocpp/v201/message_dispatcher.cpp + ocpp/v201/functional_blocks/data_transfer.cpp ) add_subdirectory(ocpp/v201/messages) endif() diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index e20c03ccd..750d72322 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -258,8 +258,7 @@ ChargePoint::on_get_15118_ev_certificate_request(const Get15118EVCertificateRequ } EVLOG_debug << "Received Get15118EVCertificateRequest " << request; - auto future_res = this->message_dispatcher->dispatch_call_async( - ocpp::Call(request)); + auto future_res = this->message_dispatcher->dispatch_call_async(ocpp::Call(request)); if (future_res.wait_for(DEFAULT_WAIT_FOR_FUTURE_TIMEOUT) == std::future_status::timeout) { EVLOG_warning << "Waiting for Get15118EVCertificateRequest.conf future timed out!"; @@ -1171,6 +1170,12 @@ void ChargePoint::initialize(const std::map& evse_connector_st this->message_dispatcher = std::make_unique(*this->message_queue, *this->device_model, registration_status); + this->data_transfer = std::make_unique( + *this->message_dispatcher, this->callbacks.data_transfer_callback, + [&connectivity_manager = this->connectivity_manager]() { + return connectivity_manager->is_websocket_connected(); + }, + DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); if (this->callbacks.configure_network_connection_profile_callback.has_value()) { this->connectivity_manager->set_configure_network_connection_profile_callback( @@ -1290,7 +1295,7 @@ void ChargePoint::handle_message(const EnhancedMessage& messa this->handle_remote_stop_transaction_request(json_message); break; case MessageType::DataTransfer: - this->handle_data_transfer_req(json_message); + this->data_transfer->handle_data_transfer_req(json_message); break; case MessageType::GetLog: this->handle_get_log_req(json_message); @@ -4045,65 +4050,14 @@ void ChargePoint::handle_clear_display_message(const Callmessage_dispatcher->dispatch_call_result(call_result); } -void ChargePoint::handle_data_transfer_req(Call call) { - const auto msg = call.msg; - DataTransferResponse response; - - if (this->callbacks.data_transfer_callback.has_value()) { - response = this->callbacks.data_transfer_callback.value()(call.msg); - } else { - response.status = DataTransferStatusEnum::UnknownVendorId; - EVLOG_warning << "Received a DataTransferRequest but no data transfer callback was registered"; - } - - ocpp::CallResult call_result(response, call.uniqueId); - this->message_dispatcher->dispatch_call_result(call_result); -} - std::optional ChargePoint::data_transfer_req(const CiString<255>& vendorId, const std::optional>& messageId, const std::optional& data) { - DataTransferRequest req; - req.vendorId = vendorId; - req.messageId = messageId; - req.data = data; - - return this->data_transfer_req(req); + return this->data_transfer->data_transfer_req(vendorId, messageId, data); } std::optional ChargePoint::data_transfer_req(const DataTransferRequest& request) { - DataTransferResponse response; - response.status = DataTransferStatusEnum::Rejected; - - ocpp::Call call(request); - auto data_transfer_future = this->message_dispatcher->dispatch_call_async(call); - - if (this->connectivity_manager == nullptr or !this->connectivity_manager->is_websocket_connected()) { - return std::nullopt; - } - - if (data_transfer_future.wait_for(DEFAULT_WAIT_FOR_FUTURE_TIMEOUT) == std::future_status::timeout) { - EVLOG_warning << "Waiting for DataTransfer.conf future timed out"; - return std::nullopt; - } - - auto enhanced_message = data_transfer_future.get(); - if (enhanced_message.messageType == MessageType::DataTransferResponse) { - try { - ocpp::CallResult call_result = enhanced_message.message; - response = call_result.msg; - } catch (const EnumConversionException& e) { - EVLOG_error << "EnumConversionException during handling of message: " << e.what(); - auto call_error = CallError(enhanced_message.uniqueId, "FormationViolation", e.what(), json({})); - this->message_dispatcher->dispatch_call_error(call_error); - return std::nullopt; - } - } - if (enhanced_message.offline) { - return std::nullopt; - } - - return response; + return this->data_transfer->data_transfer_req(request); } void ChargePoint::handle_send_local_authorization_list_req(Call call) { diff --git a/lib/ocpp/v201/functional_blocks/data_transfer.cpp b/lib/ocpp/v201/functional_blocks/data_transfer.cpp new file mode 100644 index 000000000..f91d5cd19 --- /dev/null +++ b/lib/ocpp/v201/functional_blocks/data_transfer.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include +#include + +namespace ocpp { +namespace v201 { + +void DataTransfer::handle_data_transfer_req(Call call) { + const auto msg = call.msg; + DataTransferResponse response; + response.status = DataTransferStatusEnum::UnknownVendorId; + + if (this->data_transfer_callback.has_value()) { + response = this->data_transfer_callback.value()(call.msg); + } else { + response.status = DataTransferStatusEnum::UnknownVendorId; + EVLOG_warning << "Received a DataTransferRequest but no data transfer callback was registered"; + } + + ocpp::CallResult call_result(response, call.uniqueId); + this->message_dispatcher.dispatch_call_result(call_result); +} + +std::optional DataTransfer::data_transfer_req(const CiString<255>& vendorId, + const std::optional>& messageId, + const std::optional& data) { + DataTransferRequest req; + req.vendorId = vendorId; + req.messageId = messageId; + req.data = data; + + return this->data_transfer_req(req); +} + +std::optional DataTransfer::data_transfer_req(const DataTransferRequest& request) { + DataTransferResponse response; + response.status = DataTransferStatusEnum::Rejected; + + ocpp::Call call(request); + auto data_transfer_future = this->message_dispatcher.dispatch_call_async(call); + + if (not this->is_websocket_connected()) { + return std::nullopt; + } + + if (data_transfer_future.wait_for(this->response_timeout) == std::future_status::timeout) { + EVLOG_warning << "Waiting for DataTransfer.conf future timed out"; + return std::nullopt; + } + + auto enhanced_message = data_transfer_future.get(); + + if (enhanced_message.offline) { + return std::nullopt; + } + + if (enhanced_message.messageType == MessageType::DataTransferResponse) { + try { + ocpp::CallResult call_result = enhanced_message.message; + response = call_result.msg; + } catch (const EnumConversionException& e) { + EVLOG_error << "EnumConversionException during handling of message: " << e.what(); + auto call_error = CallError(enhanced_message.uniqueId, "FormationViolation", e.what(), json({})); + this->message_dispatcher.dispatch_call_error(call_error); + return std::nullopt; + } catch (const json::exception& e) { + EVLOG_error << "Unable to parse DataTransfer.conf from CSMS: " << enhanced_message.message; + auto call_error = CallError(enhanced_message.uniqueId, "FormationViolation", e.what(), json({})); + this->message_dispatcher.dispatch_call_error(call_error); + return std::nullopt; + } + } + + return response; +} + +}; // namespace v201 +} // namespace ocpp diff --git a/tests/lib/ocpp/v16/test_message_queue.cpp b/tests/lib/ocpp/v16/test_message_queue.cpp index 3a8a2a9a7..3de6c5281 100644 --- a/tests/lib/ocpp/v16/test_message_queue.cpp +++ b/tests/lib/ocpp/v16/test_message_queue.cpp @@ -26,12 +26,12 @@ class ControlMessageV16Test : public ::testing::Test { TEST_F(ControlMessageV16Test, test_is_transactional) { - EXPECT_TRUE(is_transaction_message( - (ControlMessage{Call{v16::StartTransactionRequest{}}} - .messageType))); - EXPECT_TRUE(is_transaction_message( - (ControlMessage{Call{v16::StopTransactionRequest{}}} - .messageType))); + EXPECT_TRUE(is_transaction_message((ControlMessage{ + Call{ + v16::StartTransactionRequest{}}}.messageType))); + EXPECT_TRUE(is_transaction_message((ControlMessage{ + Call{ + v16::StopTransactionRequest{}}}.messageType))); EXPECT_TRUE(is_transaction_message(ControlMessage{ Call{v16::SecurityEventNotificationRequest{}}} .messageType)); @@ -43,12 +43,10 @@ TEST_F(ControlMessageV16Test, test_is_transactional) { TEST_F(ControlMessageV16Test, test_is_transactional_update) { - EXPECT_TRUE( - !(ControlMessage{Call{v16::StartTransactionRequest{}}}) - .is_transaction_update_message()); - EXPECT_TRUE( - !(ControlMessage{Call{v16::StopTransactionRequest{}}}) - .is_transaction_update_message()); + EXPECT_TRUE(!(ControlMessage{Call{v16::StartTransactionRequest{}}}) + .is_transaction_update_message()); + EXPECT_TRUE(!(ControlMessage{Call{v16::StopTransactionRequest{}}}) + .is_transaction_update_message()); EXPECT_TRUE(!(ControlMessage{ Call{v16::SecurityEventNotificationRequest{}}}) .is_transaction_update_message()); diff --git a/tests/lib/ocpp/v201/CMakeLists.txt b/tests/lib/ocpp/v201/CMakeLists.txt index 723c3b43f..68235f24a 100644 --- a/tests/lib/ocpp/v201/CMakeLists.txt +++ b/tests/lib/ocpp/v201/CMakeLists.txt @@ -23,3 +23,5 @@ target_sources(libocpp_unit_tests PRIVATE # Copy the json files used for testing to the destination directory file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/json DESTINATION ${TEST_PROFILES_LOCATION_V201}) + +add_subdirectory(functional_blocks) \ No newline at end of file diff --git a/tests/lib/ocpp/v201/functional_blocks/CMakeLists.txt b/tests/lib/ocpp/v201/functional_blocks/CMakeLists.txt new file mode 100644 index 000000000..2537cfcad --- /dev/null +++ b/tests/lib/ocpp/v201/functional_blocks/CMakeLists.txt @@ -0,0 +1,6 @@ +target_include_directories(libocpp_unit_tests PUBLIC + ../mocks + ${CMAKE_CURRENT_SOURCE_DIR}) + +target_sources(libocpp_unit_tests PRIVATE + test_data_transfer.cpp) diff --git a/tests/lib/ocpp/v201/functional_blocks/test_data_transfer.cpp b/tests/lib/ocpp/v201/functional_blocks/test_data_transfer.cpp new file mode 100644 index 000000000..3aeb9f675 --- /dev/null +++ b/tests/lib/ocpp/v201/functional_blocks/test_data_transfer.cpp @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include +#include + +#include + +#include +#include + +using namespace ocpp::v201; +using ::testing::_; +using ::testing::Invoke; +using ::testing::Return; + +DataTransferRequest create_example_request() { + DataTransferRequest request; + request.vendorId = "TestVendor"; + request.messageId = "TestMessage"; + request.data = json{{"key", "value"}}; + return request; +} + +bool is_websocket_connected() { + return true; +} + +TEST(DataTransferTest, HandleDataTransferReq_NoCallback) { + MockMessageDispatcher mock_dispatcher; + DataTransfer data_transfer(mock_dispatcher, std::nullopt, is_websocket_connected, + ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); + + DataTransferRequest request = create_example_request(); + ocpp::Call call(request); + + EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) { + auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get(); + EXPECT_EQ(response.status, DataTransferStatusEnum::UnknownVendorId); + })); + + data_transfer.handle_data_transfer_req(call); +} + +TEST(DataTransferTest, HandleDataTransferReq_WithCallback) { + MockMessageDispatcher mock_dispatcher; + + auto callback = [](const DataTransferRequest&) { + DataTransferResponse response; + response.status = DataTransferStatusEnum::Accepted; + return response; + }; + + DataTransfer data_transfer(mock_dispatcher, callback, is_websocket_connected, + ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); + + DataTransferRequest request = create_example_request(); + ocpp::Call call(request); + + EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) { + auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get(); + EXPECT_EQ(response.status, DataTransferStatusEnum::Accepted); + })); + + data_transfer.handle_data_transfer_req(call); +} + +TEST(DataTransferTest, DataTransferReq_NotConnected) { + MockMessageDispatcher mock_dispatcher; + DataTransfer data_transfer( + mock_dispatcher, std::nullopt, []() { return false; }, ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); + + DataTransferRequest request = create_example_request(); + + EXPECT_CALL(mock_dispatcher, dispatch_call_async(_, _)).Times(1); + + auto response = data_transfer.data_transfer_req(request); + + EXPECT_FALSE(response.has_value()); +} + +TEST(DataTransferTest, DataTransferReq_Offline) { + MockMessageDispatcher mock_dispatcher; + DataTransfer data_transfer(mock_dispatcher, std::nullopt, is_websocket_connected, + ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); + + DataTransferRequest request = create_example_request(); + + ocpp::EnhancedMessage offline_message; + offline_message.offline = true; + + EXPECT_CALL(mock_dispatcher, dispatch_call_async(_, _)) + .WillOnce(Return(std::async(std::launch::deferred, [offline_message]() { return offline_message; }))); + + auto response = data_transfer.data_transfer_req(request); + + EXPECT_FALSE(response.has_value()); +} + +TEST(DataTransferTest, DataTransferReq_Timeout) { + MockMessageDispatcher mock_dispatcher; + DataTransfer data_transfer(mock_dispatcher, std::nullopt, is_websocket_connected, std::chrono::seconds(1)); + + DataTransferRequest request = create_example_request(); + + auto timeout_future = std::async(std::launch::async, []() -> ocpp::EnhancedMessage { + std::this_thread::sleep_for(std::chrono::seconds(2)); + return {}; + }); + + EXPECT_CALL(mock_dispatcher, dispatch_call_async(_, _)).WillOnce(Return(std::move(timeout_future))); + + auto response = data_transfer.data_transfer_req(request); + + EXPECT_FALSE(response.has_value()); +} + +TEST(DataTransferTest, DataTransferReq_Accepted) { + MockMessageDispatcher mock_dispatcher; + DataTransfer data_transfer(mock_dispatcher, std::nullopt, is_websocket_connected, + ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); + + DataTransferRequest request = create_example_request(); + + DataTransferResponse expected_response; + expected_response.status = DataTransferStatusEnum::Accepted; + + ocpp::CallResult call_result(expected_response, "uniqueId"); + + ocpp::EnhancedMessage enhanced_message; + enhanced_message.messageType = MessageType::DataTransferResponse; + enhanced_message.message = call_result; + + EXPECT_CALL(mock_dispatcher, dispatch_call_async(_, _)) + .WillOnce(Return(std::async(std::launch::deferred, [enhanced_message]() { return enhanced_message; }))); + + auto response = data_transfer.data_transfer_req(request.vendorId, request.messageId, request.data); + + ASSERT_TRUE(response.has_value()); + EXPECT_EQ(response->status, DataTransferStatusEnum::Accepted); +} + +TEST(DataTransferTest, DataTransferReq_EnumConversionException) { + MockMessageDispatcher mock_dispatcher; + DataTransfer data_transfer(mock_dispatcher, std::nullopt, is_websocket_connected, + ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); + + DataTransferRequest request = create_example_request(); + + ocpp::EnhancedMessage enhanced_message; + enhanced_message.offline = false; + enhanced_message.messageType = MessageType::DataTransferResponse; + enhanced_message.uniqueId = "unique-id-123"; + enhanced_message.message = + json::parse("[3, \"unique-id-123\", {\"status\": \"Wrong\"}]"); // will cause a throw of EnumConversionException + + EXPECT_CALL(mock_dispatcher, dispatch_call_async(_, _)) + .WillOnce(Return(std::async(std::launch::deferred, [enhanced_message]() -> ocpp::EnhancedMessage { + return enhanced_message; + }))); + + EXPECT_CALL(mock_dispatcher, dispatch_call_error(_)).WillOnce([](const ocpp::CallError& call_error) { + EXPECT_EQ(call_error.errorCode, "FormationViolation"); + }); + + auto result = data_transfer.data_transfer_req(request); + + EXPECT_FALSE(result.has_value()); +} + +TEST(DataTransferTest, DataTransferReq_JsonException) { + MockMessageDispatcher mock_dispatcher; + DataTransfer data_transfer(mock_dispatcher, std::nullopt, is_websocket_connected, + ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); + + DataTransferRequest request = create_example_request(); + + ocpp::EnhancedMessage enhanced_message; + enhanced_message.offline = false; + enhanced_message.messageType = MessageType::DataTransferResponse; + enhanced_message.uniqueId = "unique-id-123"; + enhanced_message.message = "{NoValidJson"; // will cause a throw of json exception + + EXPECT_CALL(mock_dispatcher, dispatch_call_async(_, _)) + .WillOnce(Return(std::async(std::launch::deferred, [enhanced_message]() -> ocpp::EnhancedMessage { + return enhanced_message; + }))); + + EXPECT_CALL(mock_dispatcher, dispatch_call_error(_)).WillOnce([](const ocpp::CallError& call_error) { + EXPECT_EQ(call_error.errorCode, "FormationViolation"); + }); + + auto result = data_transfer.data_transfer_req(request); + + EXPECT_FALSE(result.has_value()); +} \ No newline at end of file