// SPDX-License-Identifier: Apache-2.0 // Copyright chargebyte GmbH and Contributors to EVerest #include #include #include "../data/DataStore.hpp" #include "../helpers/ErrorHandler.hpp" #include "../helpers/JsonRpcUtils.hpp" #include "../helpers/RequestHandlerDummy.hpp" #include "../helpers/WebSocketTestClient.hpp" #include "../rpc/RpcHandler.hpp" #include "../server/WebsocketServer.hpp" using namespace server; using namespace rpc; using namespace json_rpc_utils; class RpcHandlerTest : public ::testing::Test { protected: int test_port = 8080; void SetUp() override { // Start the WebSocket server m_websocket_server = std::make_unique(false, test_port, "lo"); lws_set_log_level(LLL_ERR | LLL_WARN, NULL); // Create RpcHandler instance. Move the transport interfaces and request handler to the RpcHandler std::vector> transport_interfaces; request_handler = std::make_unique(data_store); transport_interfaces.push_back(std::shared_ptr(std::move(m_websocket_server))); m_rpc_handler = std::make_unique(std::move(transport_interfaces), data_store, std::move(request_handler)); m_rpc_handler->start_server(); initialize_data_store(); } void TearDown() override { m_rpc_handler->stop_server(); } void initialize_data_store() { // Set up the data store with test data RPCDataTypes::ChargerInfoObj charger_info; charger_info.firmware_version = "1.0.0"; charger_info.model = "Test Charger"; charger_info.serial = "123456789"; charger_info.vendor = "Test Vendor"; data_store.chargerinfo.set_data(charger_info); data_store.everest_version = "2025.1.0"; // Properly initialize EVSE objects data_store.evses.emplace_back(std::make_unique()); } void send_req_and_validate_res(WebSocketTestClient& client, const nlohmann::json& request, const nlohmann::json& expected_response, bool (*cmp_f)(const nlohmann::json&, const nlohmann::json&) = nullptr) { // Send the request client.send(request.dump()); // Wait for the response std::string data = client.wait_for_data(std::chrono::seconds(1)); // Check if the response is not empty ASSERT_FALSE(data.empty()); nlohmann::json response = nlohmann::json::parse(data); // Check if the response is valid if (cmp_f != nullptr) { // Compare the response with the expected response using the provided comparison function bool res = cmp_f(response, expected_response); if (!res) { // If the comparison fails, print the response and expected response for debugging EVLOG_error << "Expected equality of these values: response: " << response.dump(); EVLOG_error << "Expected response: " << expected_response.dump(); } ASSERT_TRUE(res); } else { // Compare the response with the expected response ASSERT_EQ(response, expected_response); } } std::unique_ptr m_websocket_server; std::unique_ptr m_rpc_handler; // Condition variable to wait for response std::condition_variable cv; std::mutex cv_mutex; // Data store object used to manage and access charger-related data, including EVSEs, connectors, and charger info. data::DataStoreCharger data_store; // Dummy request handler. Needed to create the responses of synchronous requests std::unique_ptr request_handler; }; // Test: Connect to WebSocket server and check if API.Hello timeout occurs TEST_F(RpcHandlerTest, ApiHelloTimeout) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Wait for the client hello timeout EVLOG_info << "Waiting for client hello timeout..."; std::this_thread::sleep_for(std::chrono::seconds(CLIENT_HELLO_TIMEOUT) + std::chrono::milliseconds(100)); // Check if the client is still connected ASSERT_FALSE(client.is_connected()); } // Test: Connect to WebSocket server and send API.Hello request TEST_F(RpcHandlerTest, ApiHelloReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up the expected response RPCDataTypes::HelloResObj result; result.authentication_required = false; result.api_version = API_VERSION; result.charger_info = data_store.chargerinfo.get_data().value(); result.everest_version = data_store.everest_version; nlohmann::json expected_response = {{"jsonrpc", JSON_RPC_SPEC_VERSION}, {"result", result}, {"id", 1}}; // Send Api.Hello request client.send_api_hello_req(); // Wait for the response std::string data = client.wait_for_data(std::chrono::seconds(1)); // Check if the response is not empty ASSERT_FALSE(data.empty()); // Check if the response is valid nlohmann::json response = nlohmann::json::parse(data); ASSERT_EQ(response, expected_response); // Check if the client is still connected ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); } // Test: Connect to WebSocket server and send EVSEInfo request TEST_F(RpcHandlerTest, ChargePointGetEVSEInfosReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up the data store with test data RPCDataTypes::ChargePointGetEVSEInfosResObj result; // Expected response data_store.evses.emplace_back(std::make_unique()); data_store.evses.emplace_back(std::make_unique()); RPCDataTypes::EVSEInfoObj evse_info; evse_info.index = 1; evse_info.available_connectors.emplace_back(); evse_info.available_connectors[0].index = 1; evse_info.available_connectors[0].type = types::json_rpc_api::ConnectorTypeEnum::cCCS2; evse_info.available_connectors.emplace_back(); evse_info.available_connectors[1].index = 2; evse_info.available_connectors[1].type = types::json_rpc_api::ConnectorTypeEnum::cCCS1; evse_info.description = "Test EVSE 1"; result.error = RPCDataTypes::ResponseErrorEnum::NoError; ///< No error // Set up request and expected response nlohmann::json charge_point_get_evse_infos_req = create_json_rpc_request("ChargePoint.GetEVSEInfos", {}, 1); nlohmann::json expected_error_no_data = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable)}}; // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send ChargePoint.GetEVSEInfos request and validate response, no data available send_req_and_validate_res(client, charge_point_get_evse_infos_req, expected_error_no_data, is_key_value_in_json_rpc_result); // Set up the data store with test data data_store.evses[0]->evseinfo.set_data(evse_info); result.infos.push_back(evse_info); evse_info.index = 2; evse_info.description = "Test EVSE 2"; data_store.evses[1]->evseinfo.set_data(evse_info); result.infos.push_back(evse_info); // Set up expected response nlohmann::json expected_response = create_json_rpc_response(result, 1); // Send ChargePoint.GetEVSEInfos request and validate response send_req_and_validate_res(client, charge_point_get_evse_infos_req, expected_response); } // Test: Connect to WebSocket server and send ChargePoint.GetActiveErrors request TEST_F(RpcHandlerTest, ChargePointGetActiveErrorsReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up the data store with test data RPCDataTypes::EVSEInfoObj evse_info; evse_info.index = 1; ///< Unique identifier evse_info.available_connectors.emplace_back(); evse_info.available_connectors[0].index = 1; evse_info.available_connectors[0].type = types::json_rpc_api::ConnectorTypeEnum::cCCS2; evse_info.description = "Test EVSE 1"; data_store.evses[0]->evseinfo.set_data(evse_info); // Add a second EVSE with a different index data_store.evses.emplace_back(std::make_unique()); evse_info.index = 2; ///< Unique identifier evse_info.description = "Test EVSE 2"; data_store.evses[1]->evseinfo.set_data(evse_info); // Set up the EVSE status for both EVSEs RPCDataTypes::EVSEStatusObj evse_status1, evse_status2; evse_status1.error_present = false; evse_status2.error_present = false; data_store.evses[0]->evsestatus.set_data(evse_status1); data_store.evses[1]->evsestatus.set_data(evse_status2); types::json_rpc_api::ErrorObj error0, error1, error2; error0.origin.evse_index = 1; error0.origin.connector_index = 0; error0.origin.module_id = "evse_1"; error0.origin.implementation_id = "board_support"; error0.message = "Test error message"; error0.description = "Test error description"; error0.uuid = "6db8758b-194d-48e1-99af-c8f0b1d2e3f3"; error0.severity = types::json_rpc_api::Severity::Low; error0.timestamp = "2025-01-01T12:00:00Z"; error0.type = "TestErrorType"; error1.origin.evse_index = 2; error1.origin.connector_index = 1; error1.origin.module_id = "evse_2"; error1.origin.implementation_id = "board_support"; error1.message = "Test error message"; error1.description = "Test error description"; error1.uuid = "7db8758b-194d-48e1-99af-c8f0b1d2e3f4"; error1.severity = types::json_rpc_api::Severity::Medium; error1.timestamp = "2025-01-01T12:00:00Z"; error1.type = "TestErrorType"; error2.origin.evse_index = 2; error2.origin.connector_index = 1; error2.origin.module_id = "evse_2"; error2.origin.implementation_id = "board_support"; error2.message = "Another test error message"; error2.description = "Another test error description"; error2.uuid = "8db8758b-194d-48e1-99af-c8f0b1d2e3f5"; error2.severity = types::json_rpc_api::Severity::High; error2.timestamp = "2025-01-01T12:00:01Z"; error2.type = "AnotherTestErrorType"; // Set up the data store with test data RPCDataTypes::ChargePointGetActiveErrorsResObj result; // Expected response result.active_errors.push_back(error1); result.active_errors.push_back(error2); result.error = RPCDataTypes::ResponseErrorEnum::NoError; ///< No error // Set up request and expected response nlohmann::json charge_point_get_active_errors_req = create_json_rpc_request("ChargePoint.GetActiveErrors", {}, 1); nlohmann::json expected_response = create_json_rpc_response(result, 1); // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Raise the error in the data store for the first EVSE helpers::handle_error_raised(data_store, error0); // Check if the error is set to present in the EVSE status auto tmp_evse_store_1 = data_store.get_evse_store(1); ASSERT_TRUE(tmp_evse_store_1 != nullptr); ASSERT_TRUE(tmp_evse_store_1->evsestatus.get_data().value().error_present); // Clear the error in the data store for the first EVSE helpers::handle_error_cleared(data_store, error0); // Check if the error is cleared in the EVSE status ASSERT_FALSE(tmp_evse_store_1->evsestatus.get_data().value().error_present); // Raise the second error in the data store for the second EVSE helpers::handle_error_raised(data_store, error1); helpers::handle_error_raised(data_store, error2); // Check if error is set to present in the EVSE status ASSERT_FALSE(tmp_evse_store_1->evsestatus.get_data().value().error_present); auto tmp_evse_store_2 = data_store.get_evse_store(2); ASSERT_TRUE(tmp_evse_store_2 != nullptr); ASSERT_TRUE(tmp_evse_store_2->evsestatus.get_data().value().error_present); // Send ChargePoint.GetActiveErrors request and validate response send_req_and_validate_res(client, charge_point_get_active_errors_req, expected_response); // Clear the errors for the second EVSE helpers::handle_error_cleared(data_store, error1); // Check if the error is still present in the EVSE status ASSERT_TRUE(tmp_evse_store_2->evsestatus.get_data().value().error_present); // Clear the second error helpers::handle_error_cleared(data_store, error2); // Check if error is cleared in the EVSE status ASSERT_FALSE(tmp_evse_store_1->evsestatus.get_data().value().error_present); ASSERT_FALSE(tmp_evse_store_2->evsestatus.get_data().value().error_present); } // Test: Connect to WebSocket server and send EVSE.Infos request with valid and invalid index TEST_F(RpcHandlerTest, EvseGetEVSEInfosReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up requests nlohmann::json evse_get_evse_infos_req_1 = create_json_rpc_request("EVSE.GetInfo", {{"evse_index", 1}}, 1); nlohmann::json evse_get_evse_infos_req_2 = create_json_rpc_request("EVSE.GetInfo", {{"evse_index", 2}}, 1); nlohmann::json evse_get_infos_req_invalid_index = create_json_rpc_request("EVSE.GetInfo", {{"evse_index", 99}}, 1); // Set up the data store with test data RPCDataTypes::EVSEInfoObj evse_info; evse_info.index = 1; ///< Unique identifier evse_info.available_connectors.emplace_back(); evse_info.available_connectors[0].index = 1; evse_info.available_connectors[0].type = types::json_rpc_api::ConnectorTypeEnum::cCCS2; evse_info.available_connectors.emplace_back(); evse_info.available_connectors[1].index = 2; evse_info.available_connectors[1].type = types::json_rpc_api::ConnectorTypeEnum::cCCS1; evse_info.description = "Test EVSE 1"; data_store.evses[0]->evseinfo.set_data(evse_info); // Expected response 1 RPCDataTypes::EVSEGetInfoResObj result_1; result_1.info = evse_info; result_1.error = RPCDataTypes::ResponseErrorEnum::NoError; ///< No error // Set up the second EVSE info evse_info.index = 2; evse_info.available_connectors[0].type = types::json_rpc_api::ConnectorTypeEnum::cType2; evse_info.available_connectors[1].type = types::json_rpc_api::ConnectorTypeEnum::sType2; data_store.evses.emplace_back(std::make_unique()); data_store.evses[1]->evseinfo.set_data(evse_info); // Set up the expected responses nlohmann::json expected_response_index_1 = create_json_rpc_response(result_1, 1); // Expected response 2 RPCDataTypes::EVSEGetInfoResObj result_2; result_2.info = evse_info; result_2.error = RPCDataTypes::ResponseErrorEnum::NoError; ///< No error nlohmann::json expected_response_index_2 = create_json_rpc_response(result_2, 1); // Expected error object in case of invalid ID nlohmann::json expected_error = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}}; // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send EVSE.GetEVSEInfos request 1 and validate response send_req_and_validate_res(client, evse_get_evse_infos_req_1, expected_response_index_1); // Send EVSE.GetEVSEInfos request 2 and validate response send_req_and_validate_res(client, evse_get_evse_infos_req_2, expected_response_index_2); // Send EVSE.GetEVSEInfos request with invalid ID and validate response send_req_and_validate_res(client, evse_get_infos_req_invalid_index, expected_error, is_key_value_in_json_rpc_result); } // Test: Connect to WebSocket server and send Evse.GetStatusReq request with valid and invalid index TEST_F(RpcHandlerTest, EvseGetStatusReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up the requests nlohmann::json evse_get_status_req_valid_index = create_json_rpc_request("EVSE.GetStatus", {{"evse_index", 1}}, 1); nlohmann::json evse_get_status_req_invalid_index = create_json_rpc_request("EVSE.GetStatus", {{"evse_index", 99}}, 1); // Set up the data store with test data RPCDataTypes::EVSEInfoObj evse_info; evse_info.index = 1; ///< Unique identifier data_store.evses[0]->evseinfo.set_data(evse_info); RPCDataTypes::EVSEStatusObj evse_status; evse_status.charged_energy_wh = 123.45; evse_status.discharged_energy_wh = 123.45; evse_status.charging_duration_s = 600; evse_status.charging_allowed = true; evse_status.available = true; evse_status.active_connector_index = 1; evse_status.error_present = false; evse_status.charge_protocol = types::json_rpc_api::ChargeProtocolEnum::ISO15118; ///< charge_protocol evse_status.state = types::json_rpc_api::EVSEStateEnum::Charging; evse_status.ac_charge_status.emplace().evse_active_phase_count = 3; // Set up the expected responses RPCDataTypes::EVSEGetStatusResObj res_valid_id; res_valid_id.status = evse_status; res_valid_id.error = RPCDataTypes::ResponseErrorEnum::NoError; nlohmann::json expected_error_no_data = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable)}}; nlohmann::json res_obj_invalid_index = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}}; nlohmann::json expected_response = create_json_rpc_response(res_valid_id, 1); // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send EVSE.GetStatus request with valid ID, but no data available send_req_and_validate_res(client, evse_get_status_req_valid_index, expected_error_no_data, is_key_value_in_json_rpc_result); // Set the EVSE status in the data store data_store.evses[0]->evsestatus.set_data(evse_status); // Send EVSE.GetStatus request with valid ID send_req_and_validate_res(client, evse_get_status_req_valid_index, expected_response); // Send EVSE.GetStatus request with invalid ID send_req_and_validate_res(client, evse_get_status_req_invalid_index, res_obj_invalid_index, is_key_value_in_json_rpc_result); } // Test: Connect to WebSocket server and send EVSE.GetHardwareCapabilities request with valid and invalid index TEST_F(RpcHandlerTest, EvseGetHardwareCapabilitiesReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up requests nlohmann::json evse_get_hardware_capabilities_req_valid_index = create_json_rpc_request("EVSE.GetHardwareCapabilities", {{"evse_index", 1}}, 1); nlohmann::json evse_get_hardware_capabilities_req_invalid_index = create_json_rpc_request("EVSE.GetHardwareCapabilities", {{"evse_index", 99}}, 1); // Set up the data store with test data RPCDataTypes::EVSEInfoObj evse_info; evse_info.index = 1; data_store.evses[0]->evseinfo.set_data(evse_info); RPCDataTypes::EVSEGetHardwareCapabilitiesResObj result; result.error = RPCDataTypes::ResponseErrorEnum::NoError; result.hardware_capabilities.max_current_A_export = 32.0; result.hardware_capabilities.max_current_A_import = 16.0; result.hardware_capabilities.max_phase_count_export = 3; result.hardware_capabilities.max_phase_count_import = 3; result.hardware_capabilities.min_current_A_export = 6.0; result.hardware_capabilities.min_current_A_import = 6.0; result.hardware_capabilities.min_phase_count_export = 1; result.hardware_capabilities.min_phase_count_import = 1; result.hardware_capabilities.phase_switch_during_charging = true; // Set up the expected responses nlohmann::json expected_response = create_json_rpc_response(result, 1); nlohmann::json expected_error_no_data = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable)}}; nlohmann::json expected_error_invalid_index = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}}; // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send EVSE.GetHardwareCapabilities request with valid ID, but no hardware capabilities available send_req_and_validate_res(client, evse_get_hardware_capabilities_req_valid_index, expected_error_no_data, is_key_value_in_json_rpc_result); // Set the hardware capabilities in the data store data_store.evses[0]->hardwarecapabilities.set_data(result.hardware_capabilities); // Send EVSE.GetHardwareCapabilities request with valid ID send_req_and_validate_res(client, evse_get_hardware_capabilities_req_valid_index, expected_response); // Send EVSE.GetHardwareCapabilities request with invalid ID send_req_and_validate_res(client, evse_get_hardware_capabilities_req_invalid_index, expected_error_invalid_index, is_key_value_in_json_rpc_result); } // Test: Connect to WebSocket server and send EVSE.SetChargingAllowed request with valid and invalid index TEST_F(RpcHandlerTest, EvseSetChargingAllowedReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up requests nlohmann::json evse_set_charging_allowed_req_valid_index = create_json_rpc_request("EVSE.SetChargingAllowed", {{"evse_index", 1}, {"charging_allowed", true}}, 1); nlohmann::json evse_set_charging_allowed_req_valid_index_false = create_json_rpc_request("EVSE.SetChargingAllowed", {{"evse_index", 1}, {"charging_allowed", false}}, 1); nlohmann::json evse_set_charging_allowed_req_invalid_index = create_json_rpc_request("EVSE.SetChargingAllowed", {{"evse_index", 99}, {"charging_allowed", true}}, 1); // Set up the data store with test data RPCDataTypes::EVSEInfoObj evse_info; evse_info.index = 1; data_store.evses[0]->evseinfo.set_data(evse_info); RPCDataTypes::EVSEStatusObj evse_status; evse_status.available = false; data_store.evses[0]->evsestatus.set_data(evse_status); // Set up the expected responses types::json_rpc_api::ErrorResObj result{RPCDataTypes::ResponseErrorEnum::NoError}; nlohmann::json expected_response = create_json_rpc_response(result, 1); nlohmann::json expected_error = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}}; // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send EVSE.SetChargingAllowed request with valid ID send_req_and_validate_res(client, evse_set_charging_allowed_req_valid_index, expected_response); // Check if the EVSE status is updated ASSERT_TRUE(data_store.evses[0]->evsestatus.get_data().has_value()); ASSERT_TRUE(data_store.evses[0]->evsestatus.get_data().value().charging_allowed); // Send EVSE.SetChargingAllowed request with valid ID and false send_req_and_validate_res(client, evse_set_charging_allowed_req_valid_index_false, expected_response); // Check if the EVSE status is updated ASSERT_TRUE(data_store.evses[0]->evsestatus.get_data().has_value()); ASSERT_FALSE(data_store.evses[0]->evsestatus.get_data().value().charging_allowed); // Send EVSE.SetChargingAllowed request with invalid ID send_req_and_validate_res(client, evse_set_charging_allowed_req_invalid_index, expected_error, is_key_value_in_json_rpc_result); } // Test: Connect to WebSocket server and send EVSE.MeterData request with valid and invalid index TEST_F(RpcHandlerTest, EvseMeterDataReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up requests nlohmann::json evse_meter_data_req_valid_index = create_json_rpc_request("EVSE.GetMeterData", {{"evse_index", 1}}, 1); nlohmann::json evse_meter_data_req_invalid_index = create_json_rpc_request("EVSE.GetMeterData", {{"evse_index", 99}}, 1); // Set up the data store with test data RPCDataTypes::EVSEInfoObj evse_info; evse_info.index = 1; data_store.evses[0]->evseinfo.set_data(evse_info); // Configure meter data, but do not set it in the data store RPCDataTypes::MeterDataObj meter_data{}; meter_data.energy_Wh_import.total = 123.45; meter_data.timestamp = "2025-06-10T09:51:56Z"; // Set up the expected responses types::json_rpc_api::EVSEGetMeterDataResObj result{{meter_data}, RPCDataTypes::ResponseErrorEnum::NoError}; nlohmann::json expected_response_no_error = create_json_rpc_response(result, 1); nlohmann::json expected_error_no_data = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorNoDataAvailable)}}; nlohmann::json expected_error_invalid_index = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}}; // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send EVSE.MeterData request with valid ID, but no meter data available send_req_and_validate_res(client, evse_meter_data_req_valid_index, expected_error_no_data, is_key_value_in_json_rpc_result); // Set the meter data in the data store data_store.evses[0]->meterdata.set_data(meter_data); // Send EVSE.MeterData request with valid ID and meter data available send_req_and_validate_res(client, evse_meter_data_req_valid_index, expected_response_no_error); // Send EVSE.MeterData request with invalid ID send_req_and_validate_res(client, evse_meter_data_req_invalid_index, expected_error_invalid_index, is_key_value_in_json_rpc_result); } // Test: Connect to WebSocket server and send EVSE.SetACCharging request with valid and invalid index TEST_F(RpcHandlerTest, EvseSetACChargingReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up requests nlohmann::json evse_set_ac_charging_req_valid_index = create_json_rpc_request( "EVSE.SetACCharging", {{"evse_index", 1}, {"charging_allowed", true}, {"max_current", 12.3}, {"phase_count", 3}}, 1); // As long as the method is not implemented, we expect an error response that the method is not implemented nlohmann::json expected_res = create_json_rpc_error_response(-32601, "method not found: EVSE.SetACCharging", 1); // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send EVSE.SetACCharging request with valid ID client.send(evse_set_ac_charging_req_valid_index.dump()); // Wait for the response std::string received_data = client.wait_for_data(std::chrono::seconds(1), false); // Check if the response is valid nlohmann::json response = nlohmann::json::parse(received_data); ASSERT_EQ(response, expected_res); } // Test: Connect to WebSocket server and send EVSE.SetACChargingCurrent request with valid and invalid index TEST_F(RpcHandlerTest, EvseSetACChargingCurrentReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up requests nlohmann::json evse_set_ac_charging_current_req_valid_index = create_json_rpc_request("EVSE.SetACChargingCurrent", {{"evse_index", 1}, {"max_current", 12.3}}, 1); nlohmann::json evse_set_ac_charging_current_req_invalid_index = create_json_rpc_request("EVSE.SetACChargingCurrent", {{"evse_index", 99}, {"max_current", 12.3}}, 1); nlohmann::json evse_set_ac_charging_current_req_invalid_max_current = create_json_rpc_request("EVSE.SetACChargingCurrent", {{"evse_index", 1}, {"max_current", 15.0}}, 1); // Set up the data store with test data RPCDataTypes::EVSEInfoObj evse_info{}; evse_info.index = 1; data_store.evses[0]->evseinfo.set_data(evse_info); RPCDataTypes::EVSEStatusObj evse_status{}; evse_status.charging_allowed = true; data_store.evses[0]->evsestatus.set_data(evse_status); data_store.evses[0]->evsestatus.set_ac_charge_param_evse_max_current(12.3); // Set up the expected responses types::json_rpc_api::ErrorResObj result{RPCDataTypes::ResponseErrorEnum::NoError}; nlohmann::json expected_response = create_json_rpc_response(result, 1); nlohmann::json expected_error_invalid_index = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}}; nlohmann::json expected_error_invalid_current = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorValuesNotApplied)}}; // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send EVSE.SetACChargingCurrent request with valid ID send_req_and_validate_res(client, evse_set_ac_charging_current_req_valid_index, expected_response); // Send EVSE.SetACChargingCurrent request with invalid ID send_req_and_validate_res(client, evse_set_ac_charging_current_req_invalid_index, expected_error_invalid_index, is_key_value_in_json_rpc_result); // Send EVSE.SetACChargingCurrent request with invalid AC charging current send_req_and_validate_res(client, evse_set_ac_charging_current_req_invalid_max_current, expected_error_invalid_current, is_key_value_in_json_rpc_result); } // Test: Connect to WebSocket server and send EVSE.SetACChargingPhaseCount request with valid and invalid index TEST_F(RpcHandlerTest, EvseSetACChargingPhaseCountReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up requests nlohmann::json evse_set_ac_charging_phase_count_req_valid_index = create_json_rpc_request("EVSE.SetACChargingPhaseCount", {{"evse_index", 1}, {"phase_count", 3}}, 1); nlohmann::json evse_set_ac_charging_phase_count_req_invalid_index = create_json_rpc_request("EVSE.SetACChargingPhaseCount", {{"evse_index", 99}, {"phase_count", 3}}, 1); // Set up the data store with test data RPCDataTypes::EVSEInfoObj evse_info; evse_info.index = 1; data_store.evses[0]->evseinfo.set_data(evse_info); RPCDataTypes::EVSEStatusObj evse_status; evse_status.available = false; evse_status.ac_charge_param.emplace(); evse_status.ac_charge_param->evse_max_current = 12.3; data_store.evses[0]->evsestatus.set_data(evse_status); RPCDataTypes::EVSEGetHardwareCapabilitiesResObj hw_cap; hw_cap.error = RPCDataTypes::ResponseErrorEnum::NoError; hw_cap.hardware_capabilities.max_current_A_export = 32.0; hw_cap.hardware_capabilities.max_current_A_import = 16.0; hw_cap.hardware_capabilities.max_phase_count_export = 3; hw_cap.hardware_capabilities.max_phase_count_import = 3; hw_cap.hardware_capabilities.min_current_A_export = 6.0; hw_cap.hardware_capabilities.min_current_A_import = 6.0; hw_cap.hardware_capabilities.min_phase_count_export = 1; hw_cap.hardware_capabilities.min_phase_count_import = 1; hw_cap.hardware_capabilities.phase_switch_during_charging = true; data_store.evses[0]->hardwarecapabilities.set_data(hw_cap.hardware_capabilities); // Set up the expected responses types::json_rpc_api::ErrorResObj result{RPCDataTypes::ResponseErrorEnum::NoError}; nlohmann::json expected_response = create_json_rpc_response(result, 1); nlohmann::json expected_error = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}}; // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send EVSE.SetACChargingPhaseCount request with valid ID send_req_and_validate_res(client, evse_set_ac_charging_phase_count_req_valid_index, expected_response); // Send EVSE.SetACChargingPhaseCount request with invalid ID send_req_and_validate_res(client, evse_set_ac_charging_phase_count_req_invalid_index, expected_error, is_key_value_in_json_rpc_result); } // Test: Connect to WebSocket server and send EVSE.SetACChargingPhaseCount request with invalid phases and disabled // phase switching TEST_F(RpcHandlerTest, EvseSetACChargingPhaseCountReqBadCases) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up requests nlohmann::json evse_set_ac_charging_phase_count_req_valid_phase_count = create_json_rpc_request("EVSE.SetACChargingPhaseCount", {{"evse_index", 1}, {"phase_count", 1}}, 1); nlohmann::json evse_set_ac_charging_phase_count_req_invalid_phase_count = create_json_rpc_request("EVSE.SetACChargingPhaseCount", {{"evse_index", 1}, {"phase_count", 2}}, 1); nlohmann::json evse_set_ac_charging_phase_count_req_out_of_range = create_json_rpc_request("EVSE.SetACChargingPhaseCount", {{"evse_index", 1}, {"phase_count", 3}}, 1); // Set up the data store with test data RPCDataTypes::EVSEInfoObj evse_info; evse_info.index = 1; data_store.evses[0]->evseinfo.set_data(evse_info); RPCDataTypes::EVSEStatusObj evse_status; evse_status.available = false; evse_status.ac_charge_param.emplace(); evse_status.ac_charge_param->evse_max_current = 12.3; evse_status.ac_charge_status.emplace(); evse_status.ac_charge_status->evse_active_phase_count = 1; data_store.evses[0]->evsestatus.set_data(evse_status); RPCDataTypes::EVSEGetHardwareCapabilitiesResObj hw_cap; hw_cap.error = RPCDataTypes::ResponseErrorEnum::NoError; hw_cap.hardware_capabilities.max_current_A_export = 32.0; hw_cap.hardware_capabilities.max_current_A_import = 16.0; hw_cap.hardware_capabilities.max_phase_count_export = 1; hw_cap.hardware_capabilities.max_phase_count_import = 1; hw_cap.hardware_capabilities.min_current_A_export = 6.0; hw_cap.hardware_capabilities.min_current_A_import = 6.0; hw_cap.hardware_capabilities.min_phase_count_export = 1; hw_cap.hardware_capabilities.min_phase_count_import = 1; hw_cap.hardware_capabilities.phase_switch_during_charging = false; data_store.evses[0]->hardwarecapabilities.set_data(hw_cap.hardware_capabilities); // Set up the expected responses types::json_rpc_api::ErrorResObj result{RPCDataTypes::ResponseErrorEnum::NoError}; nlohmann::json expected_no_error = create_json_rpc_response(result, 1); nlohmann::json expected_invalid_param = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidParameter)}}; nlohmann::json expected_error_out_of_range = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorOutOfRange)}}; nlohmann::json expected_error_operation_not_supported = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorOperationNotSupported)}}; // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send EVSE.SetACChargingPhaseCount request with phase count. This should not lead to an error, because // an initialization of the phase count should be still possible. send_req_and_validate_res(client, evse_set_ac_charging_phase_count_req_valid_phase_count, expected_no_error); // Try to switch phase count although phase switching is not allowed (phase_switch_during_charging == false) send_req_and_validate_res(client, evse_set_ac_charging_phase_count_req_invalid_phase_count, expected_error_operation_not_supported, is_key_value_in_json_rpc_result); // Send EVSE.SetACChargingPhaseCount request with phase count out of range // Enable phase switching, because otherwise it returns an ErrorOperationNotSupported error hw_cap.hardware_capabilities.phase_switch_during_charging = true; data_store.evses[0]->hardwarecapabilities.set_data(hw_cap.hardware_capabilities); send_req_and_validate_res(client, evse_set_ac_charging_phase_count_req_out_of_range, expected_error_out_of_range, is_key_value_in_json_rpc_result); // Invalid phase count error occurs when phase_count is configured to 2 hw_cap.hardware_capabilities.max_phase_count_export = 3; data_store.evses[0]->hardwarecapabilities.set_data(hw_cap.hardware_capabilities); send_req_and_validate_res(client, evse_set_ac_charging_phase_count_req_invalid_phase_count, expected_invalid_param, is_key_value_in_json_rpc_result); } // Test: Connect to WebSocket server and send EVSE.SetDCCharging request with valid and invalid index TEST_F(RpcHandlerTest, EvseSetDCChargingReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up requests nlohmann::json evse_set_dc_charging_req_valid_index = create_json_rpc_request( "EVSE.SetDCCharging", {{"evse_index", 1}, {"charging_allowed", true}, {"max_power", 12.3}}, 1); // As long as the method is not implemented, we expect an error response that the method is not implemented nlohmann::json expected_res = create_json_rpc_error_response(-32601, "method not found: EVSE.SetDCCharging", 1); // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send EVSE.SetDCCharging request with valid ID client.send(evse_set_dc_charging_req_valid_index.dump()); // Wait for the response std::string received_data = client.wait_for_data(std::chrono::seconds(1), false); // Check if the response is valid nlohmann::json response = nlohmann::json::parse(received_data); ASSERT_EQ(response, expected_res); } // Test: Connect to WebSocket server and send EVSE.SetDCChargingPower request with valid and invalid index TEST_F(RpcHandlerTest, EvseSetDCChargingPowerReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up requests nlohmann::json evse_set_dc_charging_power_req_valid_index = create_json_rpc_request("EVSE.SetDCChargingPower", {{"evse_index", 1}, {"max_power", 12.3}}, 1); nlohmann::json evse_set_dc_charging_power_req_invalid_index = create_json_rpc_request("EVSE.SetDCChargingPower", {{"evse_index", 99}, {"max_power", 12.3}}, 1); // Set up the data store with test data RPCDataTypes::EVSEInfoObj evse_info; evse_info.index = 1; data_store.evses[0]->evseinfo.set_data(evse_info); RPCDataTypes::EVSEStatusObj evse_status; evse_status.available = false; evse_status.dc_charge_param.emplace(); data_store.evses[0]->evsestatus.set_data(evse_status); // Set up the expected responses types::json_rpc_api::ErrorResObj result{RPCDataTypes::ResponseErrorEnum::NoError}; nlohmann::json expected_response = create_json_rpc_response(result, 1); nlohmann::json expected_error = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}}; // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send EVSE.SetDCChargingPower request with valid ID send_req_and_validate_res(client, evse_set_dc_charging_power_req_valid_index, expected_response); // Send EVSE.SetDCChargingPower request with invalid ID send_req_and_validate_res(client, evse_set_dc_charging_power_req_invalid_index, expected_error, is_key_value_in_json_rpc_result); } // Test: Connect to WebSocket server and send EVSE.EnableConnector request with valid and invalid index TEST_F(RpcHandlerTest, EvseEnableConnectorReq) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Set up requests nlohmann::json evse_enable_connector_req_valid_index = create_json_rpc_request( "EVSE.EnableConnector", {{"evse_index", 1}, {"enable", true}, {"priority", 1}, {"connector_index", 1}}, 1); nlohmann::json evse_enable_connector_req_invalid_index = create_json_rpc_request( "EVSE.EnableConnector", {{"evse_index", 99}, {"enable", true}, {"priority", 1}, {"connector_index", 1}}, 1); nlohmann::json evse_enable_connector_req_invalid_connector_index = create_json_rpc_request( "EVSE.EnableConnector", {{"evse_index", 1}, {"enable", true}, {"priority", 1}, {"connector_index", 99}}, 1); // Set up the expected responses types::json_rpc_api::ErrorResObj result{RPCDataTypes::ResponseErrorEnum::NoError}; nlohmann::json expected_response = create_json_rpc_response(result, 1); nlohmann::json expected_error = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidEVSEIndex)}}; nlohmann::json expected_error_invalid_connector_index = { {"error", response_error_enum_to_string(RPCDataTypes::ResponseErrorEnum::ErrorInvalidConnectorIndex)}}; // Set up the data store with test data RPCDataTypes::EVSEInfoObj evse_info; evse_info.index = 1; evse_info.available_connectors.emplace_back(); evse_info.available_connectors[0].index = 1; evse_info.available_connectors[0].type = types::json_rpc_api::ConnectorTypeEnum::cCCS2; data_store.evses[0]->evseinfo.set_data(evse_info); RPCDataTypes::EVSEStatusObj evse_status; evse_status.available = false; data_store.evses[0]->evsestatus.set_data(evse_status); // Send Api.Hello request client.send_api_hello_req(); client.wait_for_data(std::chrono::seconds(1)); // Send EVSE.EnableConnector request with valid ID send_req_and_validate_res(client, evse_enable_connector_req_valid_index, expected_response); // Send EVSE.EnableConnector request with invalid ID send_req_and_validate_res(client, evse_enable_connector_req_invalid_index, expected_error, is_key_value_in_json_rpc_result); // Send EVSE.EnableConnector request with invalid connector ID send_req_and_validate_res(client, evse_enable_connector_req_invalid_connector_index, expected_error_invalid_connector_index, is_key_value_in_json_rpc_result); } // Test: Connect to WebSocket server and send invalid request TEST_F(RpcHandlerTest, InvalidRequest) { WebSocketTestClient client("localhost", test_port); ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.wait_until_connected(std::chrono::milliseconds(100))); // Send Api.Hello request client.send_api_hello_req(); // Wait for the response client.wait_for_data(std::chrono::seconds(1)); // Send invalid request nlohmann::json invalid_request = create_json_rpc_request("API.InvalidMethod", {}, 1); // Expected response nlohmann::json expected_response = create_json_rpc_error_response(-32601, "method not found: API.InvalidMethod", 1); // Send invalid request client.send(invalid_request.dump()); // Wait for the response std::string received_data = client.wait_for_data(std::chrono::seconds(1), false); // Check if the response is not empty ASSERT_FALSE(received_data.empty()); // Check if the response is valid nlohmann::json response = nlohmann::json::parse(received_data); ASSERT_EQ(response, expected_response); }