From 18ca071bad98feeacb52baa56cf4ef8345dcfd8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piet=20G=C3=B6mpel?= <37657534+Pietfried@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:11:11 +0100 Subject: [PATCH] Feature/data transfer functional block (#871) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added message handler interface to be implemented by specific functional blocks * Moved DataTransfer functionality to DataTransfer functional block using the targeted design. Added test cases for the new functional block --------- Signed-off-by: Piet Gömpel --- doc/message_dispatching.md | 9 + include/ocpp/v201/charge_point.hpp | 5 +- .../v201/functional_blocks/data_transfer.hpp | 60 ++++ include/ocpp/v201/message_handler.hpp | 39 +++ lib/CMakeLists.txt | 1 + lib/ocpp/v201/charge_point.cpp | 311 ++++++++---------- .../v201/functional_blocks/data_transfer.cpp | 82 +++++ tests/lib/ocpp/v201/CMakeLists.txt | 2 + .../v201/functional_blocks/CMakeLists.txt | 6 + .../functional_blocks/test_data_transfer.cpp | 191 +++++++++++ 10 files changed, 527 insertions(+), 179 deletions(-) create mode 100644 include/ocpp/v201/functional_blocks/data_transfer.hpp create mode 100644 include/ocpp/v201/message_handler.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/doc/message_dispatching.md b/doc/message_dispatching.md index 1d6b555a9..478c2ea37 100644 --- a/doc/message_dispatching.md +++ b/doc/message_dispatching.md @@ -22,6 +22,14 @@ class v201_MessageDispatcher { - RegistrationStatusEnum& registration_status } +class v201_MessageHandlerInterface { + +handle_message(EnhancedMessage~v201_MessageType~ message) +} + +class v16_MessageHandlerInterface { + +handle_message(EnhancedMessage~v16_MessageType~ message) +} + class v201_DataTransferInterface { +data_transfer_req(request: DataTransferRequest): std::optional~DataTransferResponse~ +handle_data_transfer_req(call: Call~DataTransferRequest~) @@ -44,6 +52,7 @@ class v16_ChargePoint { MessageDispatcherInterface <|-- v16_MessageDispatcher MessageDispatcherInterface <|-- v201_MessageDispatcher v201_DataTransferInterface <|-- v201_DataTransfer +v201_MessageHandlerInterface <|-- v201_DataTransferInterface MessageDispatcherInterface *-- v201_DataTransfer MessageDispatcherInterface *-- v201_ChargePoint v201_DataTransferInterface *-- v201_ChargePoint 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..79ea120f1 --- /dev/null +++ b/include/ocpp/v201/functional_blocks/data_transfer.hpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include +#include +#include + +namespace ocpp { +namespace v201 { + +class DataTransferInterface : public MessageHandlerInterface { + +public: + virtual ~DataTransferInterface(){}; + + /// \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; +}; + +class DataTransfer : public DataTransferInterface { + +private: + MessageDispatcherInterface& message_dispatcher; + std::optional> data_transfer_callback; + std::chrono::seconds response_timeout; + +public: + DataTransfer(MessageDispatcherInterface& message_dispatcher, + const std::optional>& + data_transfer_callback, + const std::chrono::seconds response_timeout) : + message_dispatcher(message_dispatcher), + data_transfer_callback(data_transfer_callback), + response_timeout(response_timeout){}; + + void handle_message(const EnhancedMessage& message) 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/include/ocpp/v201/message_handler.hpp b/include/ocpp/v201/message_handler.hpp new file mode 100644 index 000000000..2cb72a6f0 --- /dev/null +++ b/include/ocpp/v201/message_handler.hpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include + +namespace ocpp { +namespace v201 { + +/// \brief Interface for handling OCPP2.0.1 CALL messages from the CSMS. Classes implementing a functional block shall +/// extend this interface. +class MessageHandlerInterface { + +public: + virtual ~MessageHandlerInterface() { + } + /// \brief Handles the given \p message from the CSMS. This includes dispatching a CALLRESULT as a response to the + /// incoming \p message . + /// @param message + virtual void handle_message(const EnhancedMessage& message) = 0; +}; + +class MessageTypeNotImplementedException : public std::exception { +private: + std::string message; + +public: + MessageTypeNotImplementedException(MessageType message_type) : + message("Message is not implemented: " + conversions::messagetype_to_string(message_type)) { + } + + const char* what() const noexcept override { + return message.c_str(); + } +}; + +} // namespace v201 +} // namespace ocpp \ No newline at end of file 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 39ed7f358..16c2465fc 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -1170,6 +1170,8 @@ 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, DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); if (this->callbacks.configure_network_connection_profile_callback.has_value()) { this->connectivity_manager->set_configure_network_connection_profile_callback( @@ -1254,133 +1256,141 @@ void ChargePoint::remove_network_connection_profiles_below_actual_security_profi void ChargePoint::handle_message(const EnhancedMessage& message) { const auto& json_message = message.message; - switch (message.messageType) { - case MessageType::BootNotificationResponse: - this->handle_boot_notification_response(json_message); - break; - case MessageType::SetVariables: - this->handle_set_variables_req(json_message); - break; - case MessageType::GetVariables: - this->handle_get_variables_req(message); - break; - case MessageType::GetBaseReport: - this->handle_get_base_report_req(json_message); - break; - case MessageType::GetReport: - this->handle_get_report_req(message); - break; - case MessageType::Reset: - this->handle_reset_req(json_message); - break; - case MessageType::SetNetworkProfile: - this->handle_set_network_profile_req(json_message); - break; - case MessageType::ChangeAvailability: - this->handle_change_availability_req(json_message); - break; - case MessageType::TransactionEventResponse: - this->handle_transaction_event_response(message); - break; - case MessageType::RequestStartTransaction: - this->handle_remote_start_transaction_request(json_message); - break; - case MessageType::RequestStopTransaction: - this->handle_remote_stop_transaction_request(json_message); - break; - case MessageType::DataTransfer: - this->handle_data_transfer_req(json_message); - break; - case MessageType::GetLog: - this->handle_get_log_req(json_message); - break; - case MessageType::ClearCache: - this->handle_clear_cache_req(json_message); - break; - case MessageType::UpdateFirmware: - this->handle_firmware_update_req(json_message); - break; - case MessageType::UnlockConnector: - this->handle_unlock_connector(json_message); - break; - case MessageType::TriggerMessage: - this->handle_trigger_message(json_message); - break; - case MessageType::SignCertificateResponse: - this->handle_sign_certificate_response(json_message); - break; - case MessageType::HeartbeatResponse: - this->handle_heartbeat_response(json_message); - break; - case MessageType::SendLocalList: - this->handle_send_local_authorization_list_req(json_message); - break; - case MessageType::GetLocalListVersion: - this->handle_get_local_authorization_list_version_req(json_message); - break; - case MessageType::CertificateSigned: - this->handle_certificate_signed_req(json_message); - break; - case MessageType::GetTransactionStatus: - this->handle_get_transaction_status(json_message); - break; - case MessageType::GetInstalledCertificateIds: - this->handle_get_installed_certificate_ids_req(json_message); - break; - case MessageType::InstallCertificate: - this->handle_install_certificate_req(json_message); - break; - case MessageType::DeleteCertificate: - this->handle_delete_certificate_req(json_message); - break; - case MessageType::CustomerInformation: - this->handle_customer_information_req(json_message); - break; - case MessageType::SetChargingProfile: - this->handle_set_charging_profile_req(json_message); - break; - case MessageType::ClearChargingProfile: - this->handle_clear_charging_profile_req(json_message); - break; - case MessageType::GetChargingProfiles: - this->handle_get_charging_profiles_req(json_message); - break; - case MessageType::GetCompositeSchedule: - this->handle_get_composite_schedule_req(json_message); - break; - case MessageType::SetMonitoringBase: - this->handle_set_monitoring_base_req(json_message); - break; - case MessageType::SetMonitoringLevel: - this->handle_set_monitoring_level_req(json_message); - break; - case MessageType::SetVariableMonitoring: - this->handle_set_variable_monitoring_req(message); - break; - case MessageType::GetMonitoringReport: - this->handle_get_monitoring_report_req(json_message); - break; - case MessageType::ClearVariableMonitoring: - this->handle_clear_variable_monitoring_req(json_message); - break; - case MessageType::GetDisplayMessages: - this->handle_get_display_message(json_message); - break; - case MessageType::SetDisplayMessage: - this->handle_set_display_message(json_message); - break; - case MessageType::ClearDisplayMessage: - this->handle_clear_display_message(json_message); - break; - case MessageType::CostUpdated: - this->handle_costupdated_req(json_message); - break; - default: + try { + switch (message.messageType) { + case MessageType::BootNotificationResponse: + this->handle_boot_notification_response(json_message); + break; + case MessageType::SetVariables: + this->handle_set_variables_req(json_message); + break; + case MessageType::GetVariables: + this->handle_get_variables_req(message); + break; + case MessageType::GetBaseReport: + this->handle_get_base_report_req(json_message); + break; + case MessageType::GetReport: + this->handle_get_report_req(message); + break; + case MessageType::Reset: + this->handle_reset_req(json_message); + break; + case MessageType::SetNetworkProfile: + this->handle_set_network_profile_req(json_message); + break; + case MessageType::ChangeAvailability: + this->handle_change_availability_req(json_message); + break; + case MessageType::TransactionEventResponse: + this->handle_transaction_event_response(message); + break; + case MessageType::RequestStartTransaction: + this->handle_remote_start_transaction_request(json_message); + break; + case MessageType::RequestStopTransaction: + this->handle_remote_stop_transaction_request(json_message); + break; + case MessageType::DataTransfer: + this->data_transfer->handle_message(message); + break; + case MessageType::GetLog: + this->handle_get_log_req(json_message); + break; + case MessageType::ClearCache: + this->handle_clear_cache_req(json_message); + break; + case MessageType::UpdateFirmware: + this->handle_firmware_update_req(json_message); + break; + case MessageType::UnlockConnector: + this->handle_unlock_connector(json_message); + break; + case MessageType::TriggerMessage: + this->handle_trigger_message(json_message); + break; + case MessageType::SignCertificateResponse: + this->handle_sign_certificate_response(json_message); + break; + case MessageType::HeartbeatResponse: + this->handle_heartbeat_response(json_message); + break; + case MessageType::SendLocalList: + this->handle_send_local_authorization_list_req(json_message); + break; + case MessageType::GetLocalListVersion: + this->handle_get_local_authorization_list_version_req(json_message); + break; + case MessageType::CertificateSigned: + this->handle_certificate_signed_req(json_message); + break; + case MessageType::GetTransactionStatus: + this->handle_get_transaction_status(json_message); + break; + case MessageType::GetInstalledCertificateIds: + this->handle_get_installed_certificate_ids_req(json_message); + break; + case MessageType::InstallCertificate: + this->handle_install_certificate_req(json_message); + break; + case MessageType::DeleteCertificate: + this->handle_delete_certificate_req(json_message); + break; + case MessageType::CustomerInformation: + this->handle_customer_information_req(json_message); + break; + case MessageType::SetChargingProfile: + this->handle_set_charging_profile_req(json_message); + break; + case MessageType::ClearChargingProfile: + this->handle_clear_charging_profile_req(json_message); + break; + case MessageType::GetChargingProfiles: + this->handle_get_charging_profiles_req(json_message); + break; + case MessageType::GetCompositeSchedule: + this->handle_get_composite_schedule_req(json_message); + break; + case MessageType::SetMonitoringBase: + this->handle_set_monitoring_base_req(json_message); + break; + case MessageType::SetMonitoringLevel: + this->handle_set_monitoring_level_req(json_message); + break; + case MessageType::SetVariableMonitoring: + this->handle_set_variable_monitoring_req(message); + break; + case MessageType::GetMonitoringReport: + this->handle_get_monitoring_report_req(json_message); + break; + case MessageType::ClearVariableMonitoring: + this->handle_clear_variable_monitoring_req(json_message); + break; + case MessageType::GetDisplayMessages: + this->handle_get_display_message(json_message); + break; + case MessageType::SetDisplayMessage: + this->handle_set_display_message(json_message); + break; + case MessageType::ClearDisplayMessage: + this->handle_clear_display_message(json_message); + break; + case MessageType::CostUpdated: + this->handle_costupdated_req(json_message); + break; + default: + if (message.messageTypeId == MessageTypeId::CALL) { + const auto call_error = CallError(message.uniqueId, "NotImplemented", "", json({})); + this->message_dispatcher->dispatch_call_error(call_error); + } + break; + } + } catch (const MessageTypeNotImplementedException& e) { + EVLOG_warning << e.what(); if (message.messageTypeId == MessageTypeId::CALL) { const auto call_error = CallError(message.uniqueId, "NotImplemented", "", json({})); this->message_dispatcher->dispatch_call_error(call_error); } - break; } } @@ -4044,65 +4054,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..ada72d369 --- /dev/null +++ b/lib/ocpp/v201/functional_blocks/data_transfer.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include +#include + +namespace ocpp { +namespace v201 { + +void DataTransfer::handle_message(const EnhancedMessage& message) { + + if (message.messageType != MessageType::DataTransfer) { + throw MessageTypeNotImplementedException(message.messageType); + } + + Call call = message.message; + 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 (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/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..7b86d886b --- /dev/null +++ b/tests/lib/ocpp/v201/functional_blocks/test_data_transfer.cpp @@ -0,0 +1,191 @@ +// 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; +} + +TEST(DataTransferTest, HandleDataTransferReq_NotImplemented) { + MockMessageDispatcher mock_dispatcher; + DataTransfer data_transfer(mock_dispatcher, std::nullopt, ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); + + DataTransferRequest request = create_example_request(); + ocpp::Call call(request); + ocpp::EnhancedMessage enhanced_message; + enhanced_message.messageType = MessageType::Authorize; // this cant be handled by DataTransfer functional block + enhanced_message.message = call; + + EXPECT_THROW(data_transfer.handle_message(enhanced_message), MessageTypeNotImplementedException); +} + +TEST(DataTransferTest, HandleDataTransferReq_NoCallback) { + MockMessageDispatcher mock_dispatcher; + DataTransfer data_transfer(mock_dispatcher, std::nullopt, ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); + + DataTransferRequest request = create_example_request(); + ocpp::Call call(request); + ocpp::EnhancedMessage enhanced_message; + enhanced_message.messageType = MessageType::DataTransfer; + enhanced_message.message = call; + + 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_message(enhanced_message); +} + +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, ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); + + DataTransferRequest request = create_example_request(); + ocpp::Call call(request); + ocpp::EnhancedMessage enhanced_message; + enhanced_message.messageType = MessageType::DataTransfer; + enhanced_message.message = call; + + 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_message(enhanced_message); +} + +TEST(DataTransferTest, DataTransferReq_Offline) { + MockMessageDispatcher mock_dispatcher; + DataTransfer data_transfer(mock_dispatcher, std::nullopt, 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, 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, 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, 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, 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