diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index ee9fff17d..7a50e6a3d 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -9,6 +9,7 @@ #include #include +#include #include @@ -30,7 +31,6 @@ #include "ocpp/v201/messages/Get15118EVCertificate.hpp" #include #include -#include #include #include #include @@ -62,7 +62,6 @@ #include #include #include -#include #include #include #include @@ -390,6 +389,7 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa std::unique_ptr> message_dispatcher; std::unique_ptr data_transfer; + std::unique_ptr reservation; // utility std::shared_ptr> message_queue; @@ -735,11 +735,6 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void handle_change_availability_req(Call call); void handle_heartbeat_response(CallResult call); - // Function Block H: Reservations - void handle_reserve_now_request(Call call); - void handle_cancel_reservation_callback(Call call); - void send_reserve_now_rejected_response(const MessageId& unique_id, const std::string& status_info); - // Functional Block I: TariffAndCost void handle_costupdated_req(const Call call); @@ -978,6 +973,8 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa const std::vector& get_network_connection_slots() const override; + void send_not_implemented_error(const MessageId unique_message_id, const MessageTypeId message_type_id); + /// \brief Requests a value of a VariableAttribute specified by combination of \p component_id and \p variable_id /// from the device model /// \tparam T datatype of the value that is requested diff --git a/include/ocpp/v201/evse.hpp b/include/ocpp/v201/evse.hpp index babc05071..1b1f52819 100644 --- a/include/ocpp/v201/evse.hpp +++ b/include/ocpp/v201/evse.hpp @@ -41,7 +41,7 @@ class EvseInterface { /// \param connector_type The connector type to check. /// \return True if connector type is unknown or this evse has the given connector type. /// - virtual bool does_connector_exist(ConnectorEnum connector_type) = 0; + virtual bool does_connector_exist(ConnectorEnum connector_type) const = 0; /// /// \brief Get connector status. @@ -121,7 +121,7 @@ class EvseInterface { virtual void clear_idle_meter_values() = 0; /// \brief Returns a pointer to the connector with ID \param connector_id in this EVSE. - virtual Connector* get_connector(int32_t connector_id) = 0; + virtual Connector* get_connector(int32_t connector_id) const = 0; /// \brief Gets the effective Operative/Inoperative status of this EVSE virtual OperationalStatusEnum get_effective_operational_status() = 0; @@ -223,7 +223,7 @@ class Evse : public EvseInterface { /// \param connector_id Connector id /// \return The connector type. If evse or connector id is not correct: std::nullopt. /// - std::optional get_evse_connector_type(const uint32_t connector_id); + std::optional get_evse_connector_type(const uint32_t connector_id) const; public: /// \brief Construct a new Evse object @@ -247,7 +247,7 @@ class Evse : public EvseInterface { int32_t get_id() const; uint32_t get_number_of_connectors() const; - bool does_connector_exist(const ConnectorEnum connector_type) override; + bool does_connector_exist(const ConnectorEnum connector_type) const override; std::optional get_connector_status(std::optional connector_type) override; void open_transaction(const std::string& transaction_id, const int32_t connector_id, const DateTime& timestamp, @@ -273,7 +273,7 @@ class Evse : public EvseInterface { MeterValue get_idle_meter_value(); void clear_idle_meter_values(); - Connector* get_connector(int32_t connector_id); + Connector* get_connector(int32_t connector_id) const; OperationalStatusEnum get_effective_operational_status(); void set_evse_operative_status(OperationalStatusEnum new_status, bool persist); diff --git a/include/ocpp/v201/evse_manager.hpp b/include/ocpp/v201/evse_manager.hpp index 9acee0556..22369f128 100644 --- a/include/ocpp/v201/evse_manager.hpp +++ b/include/ocpp/v201/evse_manager.hpp @@ -25,6 +25,12 @@ class EvseManagerInterface { /// \note If \p id is not present this could throw an EvseOutOfRangeException virtual const EvseInterface& get_evse(int32_t id) const = 0; + /// \brief Check if the connector exists on the given evse id. + /// \param evse_id The evse id to check for. + /// \param connector_type The connector type. + /// \return False if evse id does not exist or evse does not have the given connector type. + virtual bool does_connector_exist(const int32_t evse_id, ConnectorEnum connector_type) const = 0; + /// \brief Check if an evse with \p id exists virtual bool does_evse_exist(int32_t id) const = 0; @@ -52,6 +58,7 @@ class EvseManager : public EvseManagerInterface { EvseInterface& get_evse(int32_t id) override; const EvseInterface& get_evse(const int32_t id) const override; + virtual bool does_connector_exist(const int32_t evse_id, const ConnectorEnum connector_type) const override; bool does_evse_exist(const int32_t id) const override; size_t get_number_of_evses() const override; diff --git a/include/ocpp/v201/functional_blocks/reservation.hpp b/include/ocpp/v201/functional_blocks/reservation.hpp new file mode 100644 index 000000000..3de86b659 --- /dev/null +++ b/include/ocpp/v201/functional_blocks/reservation.hpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include +#include +#include +#include +#include + +#pragma once + +namespace ocpp::v201 { + +class EvseInterface; +class EvseManagerInterface; + +typedef std::function ReserveNowCallback; +typedef std::function CancelReservationCallback; +typedef std::function idToken, + const std::optional> groupIdToken)> + IsReservationForTokenCallback; + +class ReservationInterface : public MessageHandlerInterface { +public: + virtual ~ReservationInterface(){}; + virtual void on_reservation_status(const int32_t reservation_id, const ReservationUpdateStatusEnum status) = 0; + virtual ocpp::ReservationCheckStatus + is_evse_reserved_for_other(const EvseInterface& evse, const IdToken& id_token, + const std::optional& group_id_token) const = 0; + virtual void on_reserved(const int32_t evse_id, const int32_t connector_id) = 0; + virtual void on_reservation_cleared(const int32_t evse_id, const int32_t connector_id) = 0; +}; + +class Reservation : public ReservationInterface { +private: // Members + MessageDispatcherInterface& message_dispatcher; + DeviceModel& device_model; + EvseManagerInterface& evse_manager; + + /// \brief Callback function is called when a reservation request is received from the CSMS + ReserveNowCallback reserve_now_callback; + /// \brief Callback function is called when a cancel reservation request is received from the CSMS + CancelReservationCallback cancel_reservation_callback; + /// + /// \brief Check if the current reservation for the given evse id is made for the id token / group id token. + /// \return The reservation check status of this evse / id token. + /// + IsReservationForTokenCallback is_reservation_for_token_callback; + +public: + Reservation(MessageDispatcherInterface& message_dispatcher, DeviceModel& device_model, + EvseManagerInterface& evse_manager, ReserveNowCallback reserve_now_callback, + CancelReservationCallback cancel_reservation_callback, + const IsReservationForTokenCallback is_reservation_for_token_callback); + virtual void handle_message(const ocpp::EnhancedMessage& message) override; + + virtual void on_reservation_status(const int32_t reservation_id, const ReservationUpdateStatusEnum status) override; + virtual ocpp::ReservationCheckStatus + is_evse_reserved_for_other(const EvseInterface& evse, const IdToken& id_token, + const std::optional& group_id_token) const override; + virtual void on_reserved(const int32_t evse_id, const int32_t connector_id) override; + virtual void on_reservation_cleared(const int32_t evse_id, const int32_t connector_id) override; + +private: // Functions + void handle_reserve_now_request(Call call); + void handle_cancel_reservation_callback(Call call); + void send_reserve_now_rejected_response(const MessageId& unique_id, const std::string& status_info); +}; + +} // namespace ocpp::v201 diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 34e03083f..062c37482 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -83,6 +83,7 @@ if(LIBOCPP_ENABLE_V201) ocpp/v201/connectivity_manager.cpp ocpp/v201/message_dispatcher.cpp ocpp/v201/functional_blocks/data_transfer.cpp + ocpp/v201/functional_blocks/reservation.cpp ) add_subdirectory(ocpp/v201/messages) endif() diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index d30b99107..14a8cfdd7 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include @@ -737,11 +736,15 @@ void ChargePoint::on_fault_cleared(const int32_t evse_id, const int32_t connecto } void ChargePoint::on_reserved(const int32_t evse_id, const int32_t connector_id) { - this->evse_manager->get_evse(evse_id).submit_event(connector_id, ConnectorEvent::Reserve); + if (this->reservation != nullptr) { + this->reservation->on_reserved(evse_id, connector_id); + } } void ChargePoint::on_reservation_cleared(const int32_t evse_id, const int32_t connector_id) { - this->evse_manager->get_evse(evse_id).submit_event(connector_id, ConnectorEvent::ReservationCleared); + if (this->reservation != nullptr) { + this->reservation->on_reservation_cleared(evse_id, connector_id); + } } bool ChargePoint::on_charging_state_changed(const uint32_t evse_id, const ChargingStateEnum charging_state, @@ -1057,12 +1060,9 @@ void ChargePoint::on_variable_changed(const SetVariableData& set_variable_data) } void ChargePoint::on_reservation_status(const int32_t reservation_id, const ReservationUpdateStatusEnum status) { - ReservationStatusUpdateRequest req; - req.reservationId = reservation_id; - req.reservationUpdateStatus = status; - - ocpp::Call call(req); - this->message_dispatcher->dispatch_call(call); + if (reservation != nullptr) { + this->reservation->on_reservation_status(reservation_id, status); + } } void ChargePoint::initialize(const std::map& evse_connector_structure, @@ -1185,6 +1185,14 @@ void ChargePoint::initialize(const std::map& evse_connector_st this->data_transfer = std::make_unique( *this->message_dispatcher, this->callbacks.data_transfer_callback, DEFAULT_WAIT_FOR_FUTURE_TIMEOUT); + if (device_model->get_optional_value(ControllerComponentVariables::ReservationCtrlrAvailable) + .value_or(false)) { + this->reservation = std::make_unique( + *this->message_dispatcher, *this->device_model, *this->evse_manager, + this->callbacks.reserve_now_callback.value(), this->callbacks.cancel_reservation_callback.value(), + this->callbacks.is_reservation_for_token_callback); + } + if (this->callbacks.configure_network_connection_profile_callback.has_value()) { this->connectivity_manager->set_configure_network_connection_profile_callback( this->callbacks.configure_network_connection_profile_callback.value()); @@ -1281,10 +1289,12 @@ void ChargePoint::handle_message(const EnhancedMessage& messa this->handle_heartbeat_response(json_message); break; case MessageType::ReserveNow: - this->handle_reserve_now_request(json_message); - break; case MessageType::CancelReservation: - this->handle_cancel_reservation_callback(json_message); + if (this->reservation != nullptr) { + this->reservation->handle_message(message); + } else { + send_not_implemented_error(message.uniqueId, message.messageTypeId); + } break; case MessageType::SendLocalList: this->handle_send_local_authorization_list_req(json_message); @@ -1350,18 +1360,12 @@ void ChargePoint::handle_message(const EnhancedMessage& messa 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); - } + send_not_implemented_error(message.uniqueId, message.messageTypeId); 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); - } + send_not_implemented_error(message.uniqueId, message.messageTypeId); } } @@ -1930,10 +1934,11 @@ std::optional ChargePoint::get_transaction_evseid(const CiString<36>& t ocpp::ReservationCheckStatus ChargePoint::is_evse_reserved_for_other(EvseInterface& evse, const IdToken& id_token, const std::optional& group_id_token) const { - const std::optional> groupIdToken = - group_id_token.has_value() ? group_id_token.value().idToken : std::optional>{}; + if (this->reservation != nullptr) { + return this->reservation->is_evse_reserved_for_other(evse, id_token, group_id_token); + } - return callbacks.is_reservation_for_token_callback(evse.get_id(), id_token.idToken, groupIdToken); + return ReservationCheckStatus::NotReserved; } bool ChargePoint::is_evse_connector_available(EvseInterface& evse) const { @@ -3312,142 +3317,6 @@ void ChargePoint::handle_heartbeat_response(CallResult call) } } -void ChargePoint::handle_reserve_now_request(Call call) { - ReserveNowResponse response; - response.status = ReserveNowStatusEnum::Rejected; - bool reservation_available = true; - - std::string status_info; - - if (!this->callbacks.reserve_now_callback.has_value()) { - reservation_available = false; - status_info = "Reservation is not implemented"; - } else if (!this->device_model->get_optional_value(ControllerComponentVariables::ReservationCtrlrAvailable) - .value_or(false)) { - status_info = "Reservation is not available"; - reservation_available = false; - } else if (!this->device_model->get_optional_value(ControllerComponentVariables::ReservationCtrlrEnabled)) { - reservation_available = false; - status_info = "Reservation is not enabled"; - } - - if (!reservation_available) { - // Reservation not available / implemented, return 'Rejected'. - // H01.FR.01 - EVLOG_info << "Receiving a reservation request, but reservation is not enabled or implemented."; - send_reserve_now_rejected_response(call.uniqueId, status_info); - return; - } - - // Check if we need a specific evse id during a reservation and if that is the case, if we recevied an evse id. - const ReserveNowRequest request = call.msg; - if (!request.evseId.has_value() && - !this->device_model->get_optional_value(ControllerComponentVariables::ReservationCtrlrNonEvseSpecific) - .value_or(false)) { - // H01.FR.19 - EVLOG_warning << "Trying to make a reservation, but no evse id was given while it should be sent in the " - "request when NonEvseSpecific is disabled."; - send_reserve_now_rejected_response( - call.uniqueId, - "No evse id was given while it should be sent in the request when NonEvseSpecific is disabled"); - return; - } - - const std::optional evse_id = request.evseId; - - if (evse_id.has_value()) { - if (evse_id <= 0 || !evse_manager->does_evse_exist(evse_id.value())) { - EVLOG_error << "Trying to make a reservation, but evse " << evse_id.value() << " is not a valid evse id."; - send_reserve_now_rejected_response(call.uniqueId, "Evse id does not exist"); - return; - } - - // Check if there is a connector available for this evse id. - if (!does_connector_exist(static_cast(evse_id.value()), request.connectorType)) { - EVLOG_info << "Trying to make a reservation for connector type " - << conversions::connector_enum_to_string(request.connectorType.value_or(ConnectorEnum::Unknown)) - << " for evse " << evse_id.value() << ", but this connector type does not exist."; - send_reserve_now_rejected_response(call.uniqueId, "Connector type does not exist"); - return; - } - } else { - // No evse id. Just search for all evse's if there is something available for reservation - const uint64_t number_of_evses = evse_manager->get_number_of_evses(); - if (number_of_evses <= 0) { - send_reserve_now_rejected_response(call.uniqueId, "No evse's found in charging station"); - EVLOG_error << "Trying to make a reservation, but number of evse's is 0"; - return; - } - - bool connector_exists = false; - for (uint64_t i = 1; i <= number_of_evses; i++) { - if (this->does_connector_exist(i, request.connectorType)) { - connector_exists = true; - } - - if (this->is_connector_available(i, request.connectorType)) { - // There is at least one connector available! - break; - } - } - - if (!connector_exists) { - send_reserve_now_rejected_response(call.uniqueId, "Could not get status info from connector"); - return; - } - } - - // Connector exists and might or might not be available, but if the reservation id is already existing, reservation - // should be overwritten. - - // Call reserve now callback and wait for the response. - const ReserveNowRequest reservation_request = call.msg; - response.status = this->callbacks.reserve_now_callback.value()(reservation_request); - - // Reply with the response from the callback. - const ocpp::CallResult call_result(response, call.uniqueId); - this->message_dispatcher->dispatch_call_result(call_result); - - if (response.status == ReserveNowStatusEnum::Accepted) { - EVLOG_debug << "Reservation with id " << reservation_request.id << " for " - << (reservation_request.evseId.has_value() - ? " evse_id: " + std::to_string(reservation_request.evseId.value()) - : "") - << " is accepted"; - } -} - -void ChargePoint::handle_cancel_reservation_callback(Call call) { - - CancelReservationResponse response; - if (!this->callbacks.cancel_reservation_callback.has_value() || - !this->device_model->get_optional_value(ControllerComponentVariables::ReservationCtrlrAvailable) - .value_or(false) || - !this->device_model->get_optional_value(ControllerComponentVariables::ReservationCtrlrEnabled) - .value_or(false)) { - // Reservation not available / implemented, return 'Rejected'. - // H01.FR.01 - EVLOG_info << "Receiving a cancel reservation request, but reservation is not implemented."; - response.status = CancelReservationStatusEnum::Rejected; - } else { - response.status = (this->callbacks.cancel_reservation_callback.value()(call.msg.reservationId) - ? CancelReservationStatusEnum::Accepted - : CancelReservationStatusEnum::Rejected); - } - - const ocpp::CallResult call_result(response, call.uniqueId); - this->message_dispatcher->dispatch_call_result(call_result); -} - -void ChargePoint::send_reserve_now_rejected_response(const MessageId& unique_id, const std::string& status_info) { - ReserveNowResponse response; - response.status = ReserveNowStatusEnum::Rejected; - response.statusInfo = StatusInfo(); - response.statusInfo->additionalInfo = status_info; - const ocpp::CallResult call_result(response, unique_id); - this->message_dispatcher->dispatch_call_result(call_result); -} - void ChargePoint::handle_costupdated_req(const Call call) { CostUpdatedResponse response; ocpp::CallResult call_result(response, call.uniqueId); @@ -4666,6 +4535,13 @@ const std::vector& ChargePoint::get_network_connection_slots() const { return this->connectivity_manager->get_network_connection_slots(); } +void ChargePoint::send_not_implemented_error(const MessageId unique_message_id, const MessageTypeId message_type_id) { + if (message_type_id == MessageTypeId::CALL) { + const auto call_error = CallError(unique_message_id, "NotImplemented", "", json({})); + this->message_dispatcher->dispatch_call_error(call_error); + } +} + // Static functions /// diff --git a/lib/ocpp/v201/evse.cpp b/lib/ocpp/v201/evse.cpp index 6bc5407ee..8139c6426 100644 --- a/lib/ocpp/v201/evse.cpp +++ b/lib/ocpp/v201/evse.cpp @@ -86,7 +86,7 @@ uint32_t Evse::get_number_of_connectors() const { return static_cast(this->id_connector_map.size()); } -bool Evse::does_connector_exist(const ConnectorEnum connector_type) { +bool Evse::does_connector_exist(const ConnectorEnum connector_type) const { const uint32_t number_of_connectors = this->get_number_of_connectors(); if (number_of_connectors == 0) { return false; @@ -110,7 +110,7 @@ bool Evse::does_connector_exist(const ConnectorEnum connector_type) { continue; } - ConnectorEnum type = this->get_evse_connector_type(i).value_or(ConnectorEnum::Unknown); + const ConnectorEnum type = this->get_evse_connector_type(i).value_or(ConnectorEnum::Unknown); if (type == ConnectorEnum::Unknown || type == connector_type) { return true; } @@ -205,7 +205,7 @@ void Evse::delete_database_transaction() { } } -std::optional Evse::get_evse_connector_type(const uint32_t connector_id) { +std::optional Evse::get_evse_connector_type(const uint32_t connector_id) const { auto connector = this->get_connector(static_cast(connector_id)); if (connector == nullptr) { @@ -668,7 +668,7 @@ OperationalStatusEnum Evse::get_effective_operational_status() { return this->component_state_manager->get_evse_effective_operational_status(this->evse_id); } -Connector* Evse::get_connector(int32_t connector_id) { +Connector* Evse::get_connector(int32_t connector_id) const { if (connector_id <= 0 or connector_id > this->get_number_of_connectors()) { std::stringstream err_msg; err_msg << "ConnectorID " << connector_id << " out of bounds for EVSE " << this->evse_id; diff --git a/lib/ocpp/v201/evse_manager.cpp b/lib/ocpp/v201/evse_manager.cpp index fa795f615..28485ae50 100644 --- a/lib/ocpp/v201/evse_manager.cpp +++ b/lib/ocpp/v201/evse_manager.cpp @@ -43,6 +43,18 @@ const EvseInterface& EvseManager::get_evse(const int32_t id) const { return *this->evses.at(id - 1); } +bool EvseManager::does_connector_exist(const int32_t evse_id, const ConnectorEnum connector_type) const { + const EvseInterface* evse; + try { + evse = &this->get_evse(evse_id); + } catch (const EvseOutOfRangeException&) { + EVLOG_error << "Evse id " << evse_id << " is not a valid evse id."; + return false; + } + + return evse->does_connector_exist(connector_type); +} + bool EvseManager::does_evse_exist(const int32_t id) const { return id >= 0 && static_cast(id) <= this->evses.size(); } diff --git a/lib/ocpp/v201/functional_blocks/reservation.cpp b/lib/ocpp/v201/functional_blocks/reservation.cpp new file mode 100644 index 000000000..3381b3ae6 --- /dev/null +++ b/lib/ocpp/v201/functional_blocks/reservation.cpp @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include +#include +#include +#include +#include + +namespace ocpp::v201 { +Reservation::Reservation(MessageDispatcherInterface& message_dispatcher, DeviceModel& device_model, + EvseManagerInterface& evse_manager, ReserveNowCallback reserve_now_callback, + CancelReservationCallback cancel_reservation_callback, + const IsReservationForTokenCallback is_reservation_for_token_callback) : + message_dispatcher(message_dispatcher), + device_model(device_model), + evse_manager(evse_manager), + reserve_now_callback(reserve_now_callback), + cancel_reservation_callback(cancel_reservation_callback), + is_reservation_for_token_callback(is_reservation_for_token_callback) { +} + +void Reservation::handle_message(const ocpp::EnhancedMessage& message) { + const auto& json_message = message.message; + + switch (message.messageType) { + case MessageType::ReserveNow: + this->handle_reserve_now_request(json_message); + break; + case MessageType::CancelReservation: + this->handle_cancel_reservation_callback(json_message); + break; + default: + throw MessageTypeNotImplementedException(message.messageType); + } +} + +void Reservation::on_reservation_status(const int32_t reservation_id, const ReservationUpdateStatusEnum status) { + ReservationStatusUpdateRequest req; + req.reservationId = reservation_id; + req.reservationUpdateStatus = status; + + ocpp::Call call(req); + this->message_dispatcher.dispatch_call(call); +} + +ocpp::ReservationCheckStatus +Reservation::is_evse_reserved_for_other(const EvseInterface& evse, const IdToken& id_token, + const std::optional& group_id_token) const { + const std::optional> no = std::nullopt; + const std::optional> groupIdToken = group_id_token.has_value() ? group_id_token.value().idToken : no; + + return this->is_reservation_for_token_callback(evse.get_id(), id_token.idToken, groupIdToken); +} + +void Reservation::on_reserved(const int32_t evse_id, const int32_t connector_id) { + this->evse_manager.get_evse(evse_id).submit_event(connector_id, ConnectorEvent::Reserve); +} + +void Reservation::on_reservation_cleared(const int32_t evse_id, const int32_t connector_id) { + this->evse_manager.get_evse(evse_id).submit_event(connector_id, ConnectorEvent::ReservationCleared); +} + +void Reservation::handle_reserve_now_request(Call call) { + ReserveNowResponse response; + response.status = ReserveNowStatusEnum::Rejected; + bool reservation_available = true; + + std::string status_info; + + if (this->reserve_now_callback == nullptr) { + reservation_available = false; + status_info = "Reservation is not implemented"; + } else if (!this->device_model.get_optional_value(ControllerComponentVariables::ReservationCtrlrAvailable) + .value_or(false)) { + status_info = "Reservation is not available"; + reservation_available = false; + } else if (!this->device_model.get_optional_value(ControllerComponentVariables::ReservationCtrlrEnabled) + .value_or(false)) { + reservation_available = false; + status_info = "Reservation is not enabled"; + } + + if (!reservation_available) { + // Reservation not available / implemented, return 'Rejected'. + // H01.FR.01 + EVLOG_info << "Receiving a reservation request, but reservation is not enabled or implemented."; + send_reserve_now_rejected_response(call.uniqueId, status_info); + return; + } + + // Check if we need a specific evse id during a reservation and if that is the case, if we recevied an evse id. + const ReserveNowRequest request = call.msg; + if (!request.evseId.has_value() && + !this->device_model.get_optional_value(ControllerComponentVariables::ReservationCtrlrNonEvseSpecific) + .value_or(false)) { + // H01.FR.19 + EVLOG_warning << "Trying to make a reservation, but no evse id was given while it should be sent in the " + "request when NonEvseSpecific is disabled."; + send_reserve_now_rejected_response( + call.uniqueId, + "No evse id was given while it should be sent in the request when NonEvseSpecific is disabled"); + return; + } + + const std::optional evse_id = request.evseId; + + if (evse_id.has_value()) { + if (evse_id <= 0 || !evse_manager.does_evse_exist(evse_id.value())) { + EVLOG_error << "Trying to make a reservation, but evse " << evse_id.value() << " is not a valid evse id."; + send_reserve_now_rejected_response(call.uniqueId, "Evse id does not exist"); + return; + } + + // Check if there is a connector available for this evse id. + if (!this->evse_manager.does_connector_exist(static_cast(evse_id.value()), + request.connectorType.value_or(ConnectorEnum::Unknown))) { + EVLOG_info << "Trying to make a reservation for connector type " + << conversions::connector_enum_to_string(request.connectorType.value_or(ConnectorEnum::Unknown)) + << " for evse " << evse_id.value() << ", but this connector type does not exist."; + send_reserve_now_rejected_response(call.uniqueId, "Connector type does not exist"); + return; + } + } else { + // No evse id. Just search for all evse's if there is something available for reservation + const uint64_t number_of_evses = evse_manager.get_number_of_evses(); + if (number_of_evses == 0) { + send_reserve_now_rejected_response(call.uniqueId, "No evse's found in charging station"); + EVLOG_error << "Trying to make a reservation, but number of evse's is 0"; + return; + } + + bool connector_exists = false; + for (uint64_t i = 1; i <= number_of_evses; i++) { + if (this->evse_manager.does_connector_exist(i, request.connectorType.value_or(ConnectorEnum::Unknown))) { + connector_exists = true; + break; + } + } + + if (!connector_exists) { + send_reserve_now_rejected_response(call.uniqueId, "Could not get status info from connector"); + return; + } + } + + // Connector exists and might or might not be available, but if the reservation id is already existing, reservation + // should be overwritten. + + // Call reserve now callback and wait for the response. + const ReserveNowRequest reservation_request = call.msg; + response.status = this->reserve_now_callback(reservation_request); + + // Reply with the response from the callback. + const ocpp::CallResult call_result(response, call.uniqueId); + this->message_dispatcher.dispatch_call_result(call_result); + + if (response.status == ReserveNowStatusEnum::Accepted) { + EVLOG_debug << "Reservation with id " << reservation_request.id << " for " + << (reservation_request.evseId.has_value() + ? " evse_id: " + std::to_string(reservation_request.evseId.value()) + : "") + << " is accepted"; + } +} + +void Reservation::handle_cancel_reservation_callback(Call call) { + + CancelReservationResponse response; + if (this->cancel_reservation_callback == nullptr || + !this->device_model.get_optional_value(ControllerComponentVariables::ReservationCtrlrAvailable) + .value_or(false) || + !this->device_model.get_optional_value(ControllerComponentVariables::ReservationCtrlrEnabled) + .value_or(false)) { + // Reservation not available / implemented, return 'Rejected'. + // H01.FR.01 + EVLOG_info << "Receiving a cancel reservation request, but reservation is not implemented."; + response.status = CancelReservationStatusEnum::Rejected; + } else { + response.status = + (this->cancel_reservation_callback(call.msg.reservationId) ? CancelReservationStatusEnum::Accepted + : CancelReservationStatusEnum::Rejected); + } + + const ocpp::CallResult call_result(response, call.uniqueId); + this->message_dispatcher.dispatch_call_result(call_result); +} + +void Reservation::send_reserve_now_rejected_response(const MessageId& unique_id, const std::string& status_info) { + ReserveNowResponse response; + response.status = ReserveNowStatusEnum::Rejected; + response.statusInfo = StatusInfo(); + response.statusInfo->additionalInfo = status_info; + const ocpp::CallResult call_result(response, unique_id); + this->message_dispatcher.dispatch_call_result(call_result); +} + +} // namespace ocpp::v201 diff --git a/tests/lib/ocpp/v201/functional_blocks/CMakeLists.txt b/tests/lib/ocpp/v201/functional_blocks/CMakeLists.txt index 2537cfcad..8d6c8f99a 100644 --- a/tests/lib/ocpp/v201/functional_blocks/CMakeLists.txt +++ b/tests/lib/ocpp/v201/functional_blocks/CMakeLists.txt @@ -3,4 +3,5 @@ target_include_directories(libocpp_unit_tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_sources(libocpp_unit_tests PRIVATE - test_data_transfer.cpp) + test_data_transfer.cpp + test_reservation.cpp) diff --git a/tests/lib/ocpp/v201/functional_blocks/test_reservation.cpp b/tests/lib/ocpp/v201/functional_blocks/test_reservation.cpp new file mode 100644 index 000000000..50c49d707 --- /dev/null +++ b/tests/lib/ocpp/v201/functional_blocks/test_reservation.cpp @@ -0,0 +1,600 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +const static std::string MIGRATION_FILES_PATH = "./resources/v201/device_model_migration_files"; +const static std::string CONFIG_PATH = "./resources/example_config/v201/component_config"; +const static std::string DEVICE_MODEL_DB_IN_MEMORY_PATH = "file::memory:?cache=shared"; +const static uint32_t NR_OF_EVSES = 2; + +using namespace ocpp::v201; +using ::testing::_; +using ::testing::Invoke; +using ::testing::MockFunction; +using ::testing::Return; + +class ReservationTest : public ::testing::Test { +public: +protected: // Functions + ReservationTest() : + database_connection(std::make_unique(DEVICE_MODEL_DB_IN_MEMORY_PATH)) { + database_connection->open_connection(); + this->device_model = create_device_model(); + this->reservation = std::make_unique( + mock_dispatcher, *this->device_model, evse_manager, reserve_now_callback_mock.AsStdFunction(), + cancel_reservation_callback_mock.AsStdFunction(), is_reservation_for_token_callback_mock.AsStdFunction()); + default_test_token.idToken = "SOME_TOKEN"; + default_test_token.type = IdTokenEnum::ISO14443; + } + + /// + /// \brief Create the database for the device model and apply migrations. + /// \param path Database path. + /// + void create_device_model_db(const std::string& path) { + InitDeviceModelDb db(path, MIGRATION_FILES_PATH); + db.initialize_database(CONFIG_PATH, true); + } + + /// + /// \brief Create device model. + /// \param is_reservation_available Value of ReservationCtrlr variable 'Available' in the device model. + /// \param is_reservation_enabled Value of ReservationCtrlr variable 'enabled' in the device model. + /// \param non_evse_specific_enabled Enable/disable non evse specific reservations in the device model. + /// \return The created device model. + /// + std::unique_ptr create_device_model(const bool is_reservation_available = true, + const bool is_reservation_enabled = true, + const bool non_evse_specific_enabled = true) { + create_device_model_db(DEVICE_MODEL_DB_IN_MEMORY_PATH); + auto device_model_storage = std::make_unique(DEVICE_MODEL_DB_IN_MEMORY_PATH); + auto dm = std::make_unique(std::move(device_model_storage)); + // Defaults + set_reservation_available(dm.get(), is_reservation_available); + set_reservation_enabled(dm.get(), is_reservation_enabled); + set_non_evse_specific(dm.get(), non_evse_specific_enabled); + + // Check values + const bool reservation_available_in_device_model = + dm->get_optional_value(ControllerComponentVariables::ReservationCtrlrAvailable).value_or(false); + EXPECT_EQ(reservation_available_in_device_model, is_reservation_available); + + const bool reservation_enabled_in_device_model = + dm->get_optional_value(ControllerComponentVariables::ReservationCtrlrEnabled).value_or(false); + EXPECT_EQ(reservation_enabled_in_device_model, is_reservation_enabled); + + const bool non_evse_specific_enabled_device_model = + dm->get_optional_value(ControllerComponentVariables::ReservationCtrlrNonEvseSpecific).value_or(false); + EXPECT_EQ(non_evse_specific_enabled_device_model, non_evse_specific_enabled); + + return dm; + } + + /// + /// \brief Set value of ReservationCtrlr variable 'Enabled' in the device model. + /// \param device_model The device model to set the value in. + /// \param enabled True to set to enabled. + /// + void set_reservation_enabled(DeviceModel* device_model, const bool enabled) { + const auto& reservation_enabled = ControllerComponentVariables::ReservationCtrlrEnabled; + EXPECT_EQ(device_model->set_value(reservation_enabled.component, reservation_enabled.variable.value(), + AttributeEnum::Actual, enabled ? "true" : "false", "default", true), + SetVariableStatusEnum::Accepted); + } + + /// + /// \brief Set value of ReservationCtrlr variable 'Available' in the device model. + /// \param device_model The device model to set the value in. + /// \param available True to set to available. + /// + void set_reservation_available(DeviceModel* device_model, const bool available) { + const auto& reservation_available = ControllerComponentVariables::ReservationCtrlrAvailable; + EXPECT_EQ(device_model->set_value(reservation_available.component, reservation_available.variable.value(), + AttributeEnum::Actual, (available ? "true" : "false"), "default", true), + SetVariableStatusEnum::Accepted); + } + + /// + /// \brief Enable or disable non evse specific reservations in the device model. + /// \param device_model The device model to set the value in. + /// \param non_evse_specific_enabled True to enable non evse specific reservations. + /// + void set_non_evse_specific(DeviceModel* device_model, const bool non_evse_specific_enabled) { + const auto& non_evse_specific = ControllerComponentVariables::ReservationCtrlrNonEvseSpecific; + EXPECT_EQ(device_model->set_value(non_evse_specific.component, non_evse_specific.variable.value(), + AttributeEnum::Actual, (non_evse_specific_enabled ? "true" : "false"), + "default", true), + SetVariableStatusEnum::Accepted); + } + + /// + /// \brief Create example ReserveNow request to use in tests. + /// \param evse_id Optional evse id. + /// \param connector_type Optional connector type. + /// \return The request message. + /// + ocpp::EnhancedMessage + create_example_reserve_now_request(const std::optional evse_id = std::nullopt, + const std::optional connector_type = std::nullopt) { + ReserveNowRequest request; + request.connectorType = connector_type; + request.evseId = evse_id; + request.id = 1; + request.idToken = default_test_token; + ocpp::Call call(request); + ocpp::EnhancedMessage enhanced_message; + enhanced_message.messageType = MessageType::ReserveNow; + enhanced_message.message = call; + return enhanced_message; + } + + /// + /// \brief Create example CancelReservation request to use in tests. + /// \param reservation_id The reservation id. + /// \return The request message. + /// + ocpp::EnhancedMessage create_example_cancel_reservation_request(const int32_t reservation_id) { + CancelReservationRequest request; + request.reservationId = reservation_id; + ocpp::Call call(request); + ocpp::EnhancedMessage enhanced_message; + enhanced_message.messageType = MessageType::CancelReservation; + enhanced_message.message = call; + return enhanced_message; + } + +protected: // Members + // DatabaseConnection as member so the database keeps open and is not destroyed (because this is an in memory + // database). + std::unique_ptr database_connection; + MockMessageDispatcher mock_dispatcher; + EvseManagerFake evse_manager{NR_OF_EVSES}; + // Device model is a unique ptr here because of the database: it is stored in memory so as soon as the handle to + // the database closes, the database is removed. So the handle should be opened before creating the devide model. + // So the device model is initialized on nullptr, then the handle is opened, the devide model is created and the + // handle stays open until the whole test is destructed. + std::unique_ptr device_model; + MockFunction reserve_now_callback_mock; + MockFunction cancel_reservation_callback_mock; + MockFunction idToken, + const std::optional> groupIdToken)> + is_reservation_for_token_callback_mock; + // Make reservation a unique ptr so we can create it after creating the device model. + std::unique_ptr reservation; + + IdToken default_test_token; +}; + +TEST_F(ReservationTest, handle_reserve_now_reservation_not_available) { + // In the device model, reservation is set to not available. This should reject the request. + set_reservation_available(this->device_model.get(), false); + + 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, ReserveNowStatusEnum::Rejected); + ASSERT_TRUE(response.statusInfo.has_value()); + ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value()); + EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "Reservation is not available"); + })); + + EvseMock& m1 = evse_manager.get_mock(1); + EvseMock& m2 = evse_manager.get_mock(2); + + EXPECT_CALL(m1, get_connector_status(_)).Times(0); + EXPECT_CALL(m2, get_connector_status(_)).Times(0); + + const ocpp::EnhancedMessage request = create_example_reserve_now_request(); + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_reserve_now_callback_nullptr) { + // The callback to make the reservation is a nullptr. This should reject the request. + Reservation r{mock_dispatcher, + *this->device_model, + evse_manager, + nullptr, + cancel_reservation_callback_mock.AsStdFunction(), + is_reservation_for_token_callback_mock.AsStdFunction()}; + + 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, ReserveNowStatusEnum::Rejected); + ASSERT_TRUE(response.statusInfo.has_value()); + ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value()); + EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "Reservation is not implemented"); + })); + + EvseMock& m1 = evse_manager.get_mock(1); + EvseMock& m2 = evse_manager.get_mock(2); + + EXPECT_CALL(m1, get_connector_status(_)).Times(0); + EXPECT_CALL(m2, get_connector_status(_)).Times(0); + + const ocpp::EnhancedMessage request = create_example_reserve_now_request(); + r.handle_message(request); +} + +TEST_F(ReservationTest, handle_reserve_now_reservation_disabled) { + // In the device model, reservation is set to not enabled. This should reject the request. + set_reservation_enabled(this->device_model.get(), false); + + const bool reservation_enabled_in_device_model = + this->device_model->get_optional_value(ControllerComponentVariables::ReservationCtrlrEnabled) + .value_or(false); + EXPECT_EQ(reservation_enabled_in_device_model, false); + + 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, ReserveNowStatusEnum::Rejected); + ASSERT_TRUE(response.statusInfo.has_value()); + ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value()); + EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "Reservation is not enabled"); + })); + + EvseMock& m1 = evse_manager.get_mock(1); + EvseMock& m2 = evse_manager.get_mock(2); + + EXPECT_CALL(m1, get_connector_status(_)).Times(0); + EXPECT_CALL(m2, get_connector_status(_)).Times(0); + + const ocpp::EnhancedMessage request = create_example_reserve_now_request(); + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_reserve_now_non_evse_specific_disabled) { + // In the device model, non evse specific reservations are disabled. So when we try to make a reservation for + // a non specific evse (no evse id given), the request should be rejected. + set_non_evse_specific(this->device_model.get(), false); + + 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, ReserveNowStatusEnum::Rejected); + ASSERT_TRUE(response.statusInfo.has_value()); + ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value()); + EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), + "No evse id was given while it should be sent in the request when NonEvseSpecific is disabled"); + })); + + EvseMock& m1 = evse_manager.get_mock(1); + EvseMock& m2 = evse_manager.get_mock(2); + + EXPECT_CALL(m1, get_connector_status(_)).Times(0); + EXPECT_CALL(m2, get_connector_status(_)).Times(0); + + const ocpp::EnhancedMessage request = create_example_reserve_now_request(); + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_reserve_now_evse_not_existing) { + // Try to make a reservation with a not existing evse id. This should reject the 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, ReserveNowStatusEnum::Rejected); + ASSERT_TRUE(response.statusInfo.has_value()); + ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value()); + EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "Evse id does not exist"); + })); + + const ocpp::EnhancedMessage request = create_example_reserve_now_request(5); + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_reserve_now_connector_not_existing) { + // Try to make a reservation for a connector type that does not exist. This should reject the request. + EvseMock& m1 = evse_manager.get_mock(1); + EXPECT_CALL(m1, does_connector_exist(ConnectorEnum::Pan)).WillOnce(Return(false)); + + 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, ReserveNowStatusEnum::Rejected); + ASSERT_TRUE(response.statusInfo.has_value()); + ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value()); + EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "Connector type does not exist"); + })); + + const ocpp::EnhancedMessage request = create_example_reserve_now_request(1, ConnectorEnum::Pan); + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_reserve_now_connectors_not_existing) { + // Try to make a non evse specific reservation for a connector type that does not exist. This should reject the + // request. + EvseMock& m1 = evse_manager.get_mock(1); + ON_CALL(m1, does_connector_exist(ConnectorEnum::cG105)).WillByDefault(Return(false)); + + EvseMock& m2 = evse_manager.get_mock(2); + ON_CALL(m2, does_connector_exist(ConnectorEnum::cG105)).WillByDefault(Return(false)); + + 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, ReserveNowStatusEnum::Rejected); + ASSERT_TRUE(response.statusInfo.has_value()); + ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value()); + EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "Could not get status info from connector"); + })); + + const ocpp::EnhancedMessage request = + create_example_reserve_now_request(std::nullopt, ConnectorEnum::cG105); + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_reserve_now_one_connector_not_existing) { + // Try to make a non evse specific reservation. One connector does not have the given connector, but the other does, + // so the reservation request should be accepted. + const ocpp::EnhancedMessage request = + create_example_reserve_now_request(std::nullopt, ConnectorEnum::cTesla); + + EvseMock& m1 = evse_manager.get_mock(1); + EXPECT_CALL(m1, does_connector_exist(ConnectorEnum::cTesla)).WillOnce(Return(false)); + + EvseMock& m2 = evse_manager.get_mock(2); + EXPECT_CALL(m2, does_connector_exist(ConnectorEnum::cTesla)).WillOnce(Return(true)); + + EXPECT_CALL(reserve_now_callback_mock, Call(_)).WillOnce(Return(ReserveNowStatusEnum::Accepted)); + + 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, ReserveNowStatusEnum::Accepted); + })); + + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_reserve_now_all_connectors_not_available) { + // Try to make a reservation with all connectors unavailable. Since the evse manager has the last word in this, + // we try to do the request and it can be accepted anyway (or at least the correct reason for not accepting the + // reservation can be returned, if this is a real scenario). + const ocpp::EnhancedMessage request = + create_example_reserve_now_request(std::nullopt, ConnectorEnum::cTesla); + + EvseMock& m1 = evse_manager.get_mock(1); + EXPECT_CALL(m1, does_connector_exist(ConnectorEnum::cTesla)).WillOnce(Return(true)); + + EvseMock& m2 = evse_manager.get_mock(2); + EXPECT_CALL(m2, does_connector_exist(ConnectorEnum::cTesla)).Times(0); + + EXPECT_CALL(reserve_now_callback_mock, Call(_)).WillOnce(Return(ReserveNowStatusEnum::Accepted)); + + 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, ReserveNowStatusEnum::Accepted); + })); + + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_reserve_now_non_specific_evse_successful) { + // Try to make a non evse specific reservation which is accepted. + const ocpp::EnhancedMessage request = + create_example_reserve_now_request(std::nullopt, ConnectorEnum::cTesla); + + EvseMock& m1 = evse_manager.get_mock(1); + EXPECT_CALL(m1, does_connector_exist(ConnectorEnum::cTesla)).WillOnce(Return(true)); + + ON_CALL(reserve_now_callback_mock, Call(_)).WillByDefault(Invoke([](const ReserveNowRequest reserve_now_request) { + EXPECT_FALSE(reserve_now_request.evseId.has_value()); + return ReserveNowStatusEnum::Accepted; + })); + + 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, ReserveNowStatusEnum::Accepted); + })); + + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_reserve_now_specific_evse_successful) { + // Try to make a reservation for an existing evse, which is accepted. + std::optional tesla_connector_type = ConnectorEnum::cTesla; + + const ocpp::EnhancedMessage request = create_example_reserve_now_request(2, tesla_connector_type); + + EvseMock& m2 = evse_manager.get_mock(2); + ON_CALL(m2, does_connector_exist(ConnectorEnum::cTesla)).WillByDefault(Return(true)); + + ON_CALL(reserve_now_callback_mock, Call(_)).WillByDefault(Invoke([](const ReserveNowRequest reserve_now_request) { + EXPECT_TRUE(reserve_now_request.evseId.has_value()); + EXPECT_EQ(reserve_now_request.evseId.value(), 2); + return ReserveNowStatusEnum::Accepted; + })); + + 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, ReserveNowStatusEnum::Accepted); + })); + + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_reserve_now_specific_evse_occupied) { + // Try to make a reservation for a non specific evse, but all evse's are occupied. + std::optional tesla_connector_type = ConnectorEnum::cTesla; + + const ocpp::EnhancedMessage request = create_example_reserve_now_request(2, tesla_connector_type); + + EvseMock& m2 = evse_manager.get_mock(2); + ON_CALL(m2, does_connector_exist(ConnectorEnum::cTesla)).WillByDefault(Return(true)); + + EXPECT_CALL(reserve_now_callback_mock, Call(_)).WillOnce(Return(ReserveNowStatusEnum::Occupied)); + + 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, ReserveNowStatusEnum::Occupied); + })); + + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_cancel_reservation_reservation_not_available) { + // Try to cancel a reservation, while Reservations is not available in the device model. This will reject the + // request. + set_reservation_available(this->device_model.get(), false); + + 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, CancelReservationStatusEnum::Rejected); + })); + + const ocpp::EnhancedMessage request = create_example_cancel_reservation_request(2); + + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_cancel_reservation_reservation_not_enabled) { + // Try to cancel a reservation, while Reservations is not enabled in the device model. This will reject the request. + set_reservation_enabled(this->device_model.get(), false); + + 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, CancelReservationStatusEnum::Rejected); + })); + + const ocpp::EnhancedMessage request = create_example_cancel_reservation_request(2); + + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_cancel_reservation_callback_nullptr) { + // Try to cancel a reservation, while the cancel reservation callback is a nullptr. This will reject the request. + Reservation r{mock_dispatcher, *this->device_model, + evse_manager, reserve_now_callback_mock.AsStdFunction(), + nullptr, is_reservation_for_token_callback_mock.AsStdFunction()}; + + 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, CancelReservationStatusEnum::Rejected); + })); + + const ocpp::EnhancedMessage request = create_example_cancel_reservation_request(2); + + r.handle_message(request); +} + +TEST_F(ReservationTest, handle_cancel_reservation_accepted) { + // Try to cancel a reservation, which is accepted. + 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, CancelReservationStatusEnum::Accepted); + })); + + const ocpp::EnhancedMessage request = create_example_cancel_reservation_request(2); + + EXPECT_CALL(cancel_reservation_callback_mock, Call(_)).WillOnce(Return(true)); + + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_cancel_reservation_rejected) { + // Try to cancel a reservation, which is rejected. + 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, CancelReservationStatusEnum::Rejected); + })); + + const ocpp::EnhancedMessage request = create_example_cancel_reservation_request(2); + + EXPECT_CALL(cancel_reservation_callback_mock, Call(_)).WillOnce(Return(false)); + + this->reservation->handle_message(request); +} + +TEST_F(ReservationTest, handle_message_wrong_type) { + // Try to handle a message with the wrong type, should throw an exception. + ResetRequest request; + request.type = ResetEnum::Immediate; + ocpp::Call call(request); + ocpp::EnhancedMessage enhanced_message; + enhanced_message.messageType = MessageType::Reset; + enhanced_message.message = call; + + EXPECT_THROW(reservation->handle_message(enhanced_message), MessageTypeNotImplementedException); +} + +TEST_F(ReservationTest, handle_reserve_now_no_evses) { + // Try to make a 'global' reservation, but there are no evse's in the evse manager. + EvseManagerFake evse_manager_no_evses{0}; + Reservation r{mock_dispatcher, + *this->device_model, + evse_manager_no_evses, + reserve_now_callback_mock.AsStdFunction(), + cancel_reservation_callback_mock.AsStdFunction(), + is_reservation_for_token_callback_mock.AsStdFunction()}; + + const ocpp::EnhancedMessage request = + create_example_reserve_now_request(std::nullopt, ConnectorEnum::cTesla); + + 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, ReserveNowStatusEnum::Rejected); + ASSERT_TRUE(response.statusInfo.has_value()); + ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value()); + EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "No evse's found in charging station"); + })); + + r.handle_message(request); +} + +TEST_F(ReservationTest, on_reservation_status) { + // Call 'on_reservation_status' and check if the request is sent (to the dispatcher) + ReservationStatusUpdateRequest request; + request.reservationId = 3; + request.reservationUpdateStatus = ReservationUpdateStatusEnum::Removed; + ocpp::Call call(request); + ocpp::EnhancedMessage enhanced_message; + enhanced_message.messageType = MessageType::CancelReservation; + enhanced_message.message = call; + + EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([](const json& call, bool triggered) { + auto response = call[ocpp::CALL_PAYLOAD].get(); + EXPECT_EQ(response.reservationUpdateStatus, ReservationUpdateStatusEnum::Removed); + EXPECT_EQ(response.reservationId, 3); + EXPECT_FALSE(triggered); + })); + + reservation->on_reservation_status(3, ReservationUpdateStatusEnum::Removed); +} + +TEST_F(ReservationTest, is_evse_reserved_for_other) { + // Call 'is_evse_reserved_for_other' and check if callback is called and the correct value is returned. In this + // case: NotReserved. + EXPECT_CALL(is_reservation_for_token_callback_mock, Call(42, _, _)) + .WillOnce(Return(ocpp::ReservationCheckStatus::NotReserved)); + + EvseMock& m1 = evse_manager.get_mock(1); + IdToken id_token; + id_token.idToken = "ID_TOKEN_THINGIE"; + + EXPECT_CALL(m1, get_id).WillOnce(Return(42)); + + EXPECT_EQ(reservation->is_evse_reserved_for_other(m1, id_token, std::nullopt), + ocpp::ReservationCheckStatus::NotReserved); +} + +TEST_F(ReservationTest, on_reserved) { + // Call 'on_reserved' and check if the event is submitted to the evse. + EvseMock& m1 = evse_manager.get_mock(2); + EXPECT_CALL(m1, submit_event(1, ConnectorEvent::Reserve)).Times(1); + + reservation->on_reserved(2, 1); +} + +TEST_F(ReservationTest, on_reservation_cleared) { + // Cann 'on_reservation_cleared' and check if the event is submitted to the evse. + EvseMock& m1 = evse_manager.get_mock(1); + EXPECT_CALL(m1, submit_event(1, ConnectorEvent::ReservationCleared)).Times(1); + + reservation->on_reservation_cleared(1, 1); +} diff --git a/tests/lib/ocpp/v201/mocks/evse_manager_fake.hpp b/tests/lib/ocpp/v201/mocks/evse_manager_fake.hpp index c5fdb211a..6be516782 100644 --- a/tests/lib/ocpp/v201/mocks/evse_manager_fake.hpp +++ b/tests/lib/ocpp/v201/mocks/evse_manager_fake.hpp @@ -52,6 +52,14 @@ class EvseManagerFake : public EvseManagerInterface { return *this->evses.at(id - 1); } + virtual bool does_connector_exist(const int32_t evse_id, ConnectorEnum connector_type) const override { + if (evse_id > this->evses.size()) { + return false; + } + + return get_evse(evse_id).does_connector_exist(connector_type); + } + bool does_evse_exist(int32_t id) const override { return id <= this->evses.size(); } diff --git a/tests/lib/ocpp/v201/mocks/evse_mock.hpp b/tests/lib/ocpp/v201/mocks/evse_mock.hpp index 0117a93e7..2d27ac7a7 100644 --- a/tests/lib/ocpp/v201/mocks/evse_mock.hpp +++ b/tests/lib/ocpp/v201/mocks/evse_mock.hpp @@ -12,7 +12,7 @@ class EvseMock : public EvseInterface { public: MOCK_METHOD(int32_t, get_id, (), (const)); MOCK_METHOD(uint32_t, get_number_of_connectors, (), (const)); - MOCK_METHOD(bool, does_connector_exist, (ConnectorEnum connector_type)); + MOCK_METHOD(bool, does_connector_exist, (ConnectorEnum connector_type), (const)); MOCK_METHOD(std::optional, get_connector_status, (std::optional connector_type)); MOCK_METHOD(void, open_transaction, @@ -32,7 +32,7 @@ class EvseMock : public EvseInterface { MOCK_METHOD(MeterValue, get_meter_value, ()); MOCK_METHOD(MeterValue, get_idle_meter_value, ()); MOCK_METHOD(void, clear_idle_meter_values, ()); - MOCK_METHOD(Connector*, get_connector, (int32_t connector_id)); + MOCK_METHOD(Connector*, get_connector, (int32_t connector_id), (const)); MOCK_METHOD(OperationalStatusEnum, get_effective_operational_status, ()); MOCK_METHOD(void, set_evse_operative_status, (OperationalStatusEnum new_status, bool persist)); MOCK_METHOD(void, set_connector_operative_status, diff --git a/tests/lib/ocpp/v201/test_charge_point.cpp b/tests/lib/ocpp/v201/test_charge_point.cpp index 44c043993..266a58a18 100644 --- a/tests/lib/ocpp/v201/test_charge_point.cpp +++ b/tests/lib/ocpp/v201/test_charge_point.cpp @@ -195,6 +195,7 @@ class ChargePointCommonTestFixtureV201 : public DatabaseTestingUtils { testing::MockFunction set_charging_profiles_callback_mock; testing::MockFunction reserve_now_callback_mock; testing::MockFunction cancel_reservation_callback_mock; + ocpp::v201::Callbacks callbacks; };