From 173af8dd4e4ac0ed67e924ea6a7f4265642b8b90 Mon Sep 17 00:00:00 2001 From: Maaike Zijderveld Date: Tue, 26 Nov 2024 10:48:08 +0100 Subject: [PATCH] Feature/327 use case h01 reservation (#854) * H01 reservations implemented * Some changes are made in the interface as well. Signed-off-by: Maaike Zijderveld, iolar --- CMakeLists.txt | 2 +- README.md | 2 +- .../standardized/ReservationCtrlr.json | 3 +- doc/v201/ocpp_201_status.md | 58 ++--- include/ocpp/common/types.hpp | 9 + include/ocpp/v16/charge_point.hpp | 2 +- include/ocpp/v16/charge_point_impl.hpp | 5 +- include/ocpp/v201/charge_point.hpp | 49 +++- include/ocpp/v201/charge_point_callbacks.hpp | 14 +- include/ocpp/v201/evse.hpp | 29 +++ include/ocpp/v201/evse_manager.hpp | 4 +- lib/ocpp/v16/charge_point.cpp | 2 +- lib/ocpp/v16/charge_point_configuration.cpp | 2 +- lib/ocpp/v16/charge_point_impl.cpp | 19 +- lib/ocpp/v201/charge_point.cpp | 224 ++++++++++++++++-- lib/ocpp/v201/evse.cpp | 115 ++++++++- lib/ocpp/v201/evse_manager.cpp | 10 +- tests/lib/ocpp/v201/mocks/evse_mock.hpp | 3 + tests/lib/ocpp/v201/test_charge_point.cpp | 4 +- 19 files changed, 467 insertions(+), 89 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1dd244a7a..684ba2c24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) project(ocpp - VERSION 0.20.0 + VERSION 0.21.0 DESCRIPTION "A C++ implementation of the Open Charge Point Protocol" LANGUAGES CXX ) diff --git a/README.md b/README.md index 831603012..74688b9ee 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ The following table shows the known CSMS with which this library was tested. | E. Transactions | ✅ yes | | F. RemoteControl | ✅ yes | | G. Availability | ✅ yes | -| H. Reservation | WIP | +| H. Reservation | ✅ yes | | I. TariffAndCost | ✅ yes | | J. MeterValues | ✅ yes | | K. SmartCharging | WIP | diff --git a/config/v201/component_config/standardized/ReservationCtrlr.json b/config/v201/component_config/standardized/ReservationCtrlr.json index 940b2d36c..b887d8545 100644 --- a/config/v201/component_config/standardized/ReservationCtrlr.json +++ b/config/v201/component_config/standardized/ReservationCtrlr.json @@ -45,7 +45,8 @@ "attributes": [ { "type": "Actual", - "mutability": "ReadOnly" + "mutability": "ReadOnly", + "value": true } ], "description": "If this configuration variable is present and set to true: Charging Station supports Reservation where EVSE id is not specified.", diff --git a/doc/v201/ocpp_201_status.md b/doc/v201/ocpp_201_status.md index 1f60bf410..e5d5594cd 100644 --- a/doc/v201/ocpp_201_status.md +++ b/doc/v201/ocpp_201_status.md @@ -1071,21 +1071,21 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | ID | Status | Remark | |-----------|--------|--------| -| H01.FR.01 | | | -| H01.FR.02 | | | -| H01.FR.03 | | | -| H01.FR.04 | | | -| H01.FR.06 | | | -| H01.FR.07 | | | -| H01.FR.09 | | | -| H01.FR.11 | | | -| H01.FR.12 | | | -| H01.FR.14 | | | -| H01.FR.15 | | | -| H01.FR.16 | | | -| H01.FR.17 | | | -| H01.FR.18 | | | -| H01.FR.19 | | | +| H01.FR.01 | ✅ | | +| H01.FR.02 | ✅ | | +| H01.FR.03 | ✅ | | +| H01.FR.04 | ✅ | | +| H01.FR.06 | ✅ | | +| H01.FR.07 | ✅ | | +| H01.FR.09 | ✅ | | +| H01.FR.11 | ✅ | | +| H01.FR.12 | ✅ | | +| H01.FR.14 | ✅ | | +| H01.FR.15 | ✅ | | +| H01.FR.16 | ✅ | | +| H01.FR.17 | ✅ | | +| H01.FR.18 | ✅ | | +| H01.FR.19 | ✅ | | | H01.FR.20 | | | | H01.FR.23 | | | | H01.FR.24 | | | @@ -1094,29 +1094,31 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | ID | Status | Remark | |-----------|--------|--------| -| H02.FR.01 | | | -| H02.FR.02 | | | +| H02.FR.01 | ✅ | | +| H02.FR.02 | ✅ | | ## Reservation - Use a reserved EVSE | ID | Status | Remark | |-----------|--------|--------| -| H03.FR.01 | | | -| H03.FR.02 | | | -| H03.FR.03 | | | -| H03.FR.04 | | | -| H03.FR.05 | | | -| H03.FR.06 | | | -| H03.FR.07 | | | -| H03.FR.08 | | | +| H03.FR.01 | ✅ | | +| H03.FR.02 | ✅ | | +| H03.FR.03 | ✅ | | +| H03.FR.04 | ✅ | | +| H03.FR.05 | ✅ | | +| H03.FR.06 | ✅ | | +| H03.FR.07 | ⛽️ | | +| H03.FR.08 | ⛽️ | | +| H03.FR.09 | ✅ | | +| H03.FR.10 | ✅ | | ## Reservation - Reservation Ended, not used | ID | Status | Remark | |-----------|--------|--------| -| H04.FR.01 | | | -| H04.FR.02 | | | -| H04.FR.03 | | | +| H04.FR.01 | ✅ | | +| H04.FR.02 | ✅ | | +| H04.FR.03 | | Not all the connectors maybe? | ## TariffAndCost - Show EV Driver-specific Tariff Information diff --git a/include/ocpp/common/types.hpp b/include/ocpp/common/types.hpp index 9c48d096a..4f3932c19 100644 --- a/include/ocpp/common/types.hpp +++ b/include/ocpp/common/types.hpp @@ -834,6 +834,15 @@ struct CompositeScheduleDefaultLimits { int32_t number_phases; }; +/// \brief Status of a reservation check. +enum class ReservationCheckStatus { + NotReserved, ///< @brief No reservation of this evse and / or id token + ReservedForToken, ///< @brief Reservation for this token. + ReservedForOtherToken, ///< @brief Reserved for other token and reservation has no parent token or parent token does + ///< not match. + ReservedForOtherTokenAndHasParentToken, ///< @brief Reserved for other token but reservation has a parent token. +}; + } // namespace ocpp #endif diff --git a/include/ocpp/v16/charge_point.hpp b/include/ocpp/v16/charge_point.hpp index 67746d726..7817159ac 100644 --- a/include/ocpp/v16/charge_point.hpp +++ b/include/ocpp/v16/charge_point.hpp @@ -563,7 +563,7 @@ class ChargePoint { /// \param callback /// \ingroup ocpp16_callbacks void register_is_token_reserved_for_connector_callback( - const std::function& callback); + const std::function& callback); /// \brief Registers a callback function for the session cost datatransfer message (California Pricing Requirements) /// \param session_cost_callback The callback. diff --git a/include/ocpp/v16/charge_point_impl.hpp b/include/ocpp/v16/charge_point_impl.hpp index 0f79a59d9..000c246fe 100644 --- a/include/ocpp/v16/charge_point_impl.hpp +++ b/include/ocpp/v16/charge_point_impl.hpp @@ -187,7 +187,8 @@ class ChargePointImpl : ocpp::ChargingStationBase { transaction_updated_callback; std::function transaction_stopped_callback; - std::function is_token_reserved_for_connector_callback; + std::function + is_token_reserved_for_connector_callback; // iso15118 callback std::function& callback); + const std::function& callback); void register_session_cost_callback( const std::function& diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index 11e971403..aa751c584 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -30,6 +30,7 @@ #include "ocpp/v201/messages/Get15118EVCertificate.hpp" #include #include +#include #include #include #include @@ -61,6 +62,7 @@ #include #include #include +#include #include #include #include @@ -233,6 +235,11 @@ class ChargePointInterface { /// \param connector_id Reserved connector id virtual void on_reserved(const int32_t evse_id, const int32_t connector_id) = 0; + /// \brief Event handler that should be called when the reservation of the connector is cleared. + /// \param evse_id Cleared EVSE id + /// \param connector_id Cleared connector id. + virtual void on_reservation_cleared(const int32_t evse_id, const int32_t connector_id) = 0; + /// \brief Event handler that will update the charging state internally when it has been changed. /// \param evse_id The evse id of which the charging state has changed. /// \param charging_state The new charging state. @@ -274,6 +281,11 @@ class ChargePointInterface { /// virtual void on_variable_changed(const SetVariableData& set_variable_data) = 0; + /// \brief Event handler that will send a ReservationStatusUpdate request. + /// \param reservation_id The reservation id. + /// \param status The status. + virtual void on_reservation_status(const int32_t reservation_id, const ReservationUpdateStatusEnum status) = 0; + /// @} // End handlers group /// @} @@ -558,16 +570,10 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa /// \param evse The evse id that must be checked. Reservation will be checked for all connectors. /// \param id_token The id token to check if it is reserved for that token. /// \param group_id_token The group id token to check if it is reserved for that group id. - /// \return True when one of the EVSE connectors is reserved for another id token or group id token than the given - /// tokens. - /// If id_token is different than reserved id_token, but group_id_token is equal to reserved group_id_token, - /// returns true. - /// If both are different, returns true. - /// If id_token is equal to reserved id_token or group_id_token is equal, return false. - /// If there is no reservation, return false. + /// \return The status of the reservation for this evse, id token and group id token. /// - bool is_evse_reserved_for_other(EvseInterface& evse, const IdToken& id_token, - const std::optional& group_id_token) const; + ReservationCheckStatus is_evse_reserved_for_other(EvseInterface& evse, const IdToken& id_token, + const std::optional& group_id_token) const; /// /// \brief Check if one of the connectors of the evse is available (both connectors faulted or unavailable or on of @@ -585,6 +591,22 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa /// void set_evse_connectors_unavailable(EvseInterface& evse, bool persist); + /// + /// \brief Check if there is a connector available with the given connector type. + /// \param evse_id The evse to check for. + /// \param connector_type The connector type. + /// \return True when a connector is available and the evse id exists. + /// + bool is_connector_available(const uint32_t evse_id, std::optional connector_type); + + /// + /// \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. + /// + bool does_connector_exist(const uint32_t evse_id, std::optional connector_type); + /// \brief Get the value optional offline flag /// \return true if the charge point is offline. std::nullopt if it is online; bool is_offline(); @@ -729,6 +751,11 @@ 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); @@ -914,6 +941,8 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void on_reserved(const int32_t evse_id, const int32_t connector_id) override; + void on_reservation_cleared(const int32_t evse_id, const int32_t connector_id) override; + bool on_charging_state_changed( const uint32_t evse_id, const ChargingStateEnum charging_state, const TriggerReasonEnum trigger_reason = TriggerReasonEnum::ChargingStateChanged) override; @@ -933,6 +962,8 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void on_variable_changed(const SetVariableData& set_variable_data) override; + void on_reservation_status(const int32_t reservation_id, const ReservationUpdateStatusEnum status) override; + std::optional data_transfer_req(const CiString<255>& vendorId, const std::optional>& messageId, const std::optional& data) override; diff --git a/include/ocpp/v201/charge_point_callbacks.hpp b/include/ocpp/v201/charge_point_callbacks.hpp index e4ac99471..7da44d87d 100644 --- a/include/ocpp/v201/charge_point_callbacks.hpp +++ b/include/ocpp/v201/charge_point_callbacks.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -78,13 +79,13 @@ struct Callbacks { std::function remote_start_transaction_callback; + /// /// \brief Check if the current reservation for the given evse id is made for the id token / group id token. - /// \return True if evse is reserved for the given id token / group id token, false if it is reserved for another - /// one. + /// \return The reservation check status of this evse / id token. /// - std::function idToken, - const std::optional> groupIdToken)> + std::function idToken, + const std::optional> groupIdToken)> is_reservation_for_token_callback; std::function update_firmware_request_callback; // callback to be called when a variable has been changed by the CSMS @@ -160,6 +161,11 @@ struct Callbacks { std::optional currency_code)>> set_running_cost_callback; + /// \brief Callback function is called when a reservation request is received from the CSMS + std::optional> reserve_now_callback; + /// \brief Callback function is called when a cancel reservation request is received from the CSMS + std::optional> cancel_reservation_callback; + /// @} // End ocpp 201 callbacks group / topic /// @} // End group diff --git a/include/ocpp/v201/evse.hpp b/include/ocpp/v201/evse.hpp index f53cf7d19..babc05071 100644 --- a/include/ocpp/v201/evse.hpp +++ b/include/ocpp/v201/evse.hpp @@ -36,6 +36,26 @@ class EvseInterface { /// \return virtual uint32_t get_number_of_connectors() const = 0; + /// + /// \brief Check if the given connector type exists on this evse. + /// \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; + + /// + /// \brief Get connector status. + /// + /// This will search if there is a connector on this evse with status 'Available'. It will search through all + /// connectors, optionally filtering by connector type, and return on the first connector that is 'Available'. If + /// there is no 'Available' connector, it will return the status of the last found connector with the given + /// connector type. + /// + /// \param connector_type The connector type to filter on (optional). + /// \return Connector status. If connector type is given and does not exist, std::nullopt. + /// + virtual std::optional get_connector_status(std::optional connector_type) = 0; + /// \brief Opens a new transaction /// \param transaction_id id of the transaction /// \param connector_id id of the connector @@ -198,6 +218,13 @@ class Evse : public EvseInterface { /// \brief Component responsible for maintaining and persisting the operational status of CS, EVSEs, and connectors. std::shared_ptr component_state_manager; + /// + /// \brief Get connector type of Connector + /// \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); + public: /// \brief Construct a new Evse object /// \param evse_id id of the evse @@ -220,6 +247,8 @@ 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; + 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, const MeterValue& meter_start, const std::optional& id_token, diff --git a/include/ocpp/v201/evse_manager.hpp b/include/ocpp/v201/evse_manager.hpp index f2da7ba88..9acee0556 100644 --- a/include/ocpp/v201/evse_manager.hpp +++ b/include/ocpp/v201/evse_manager.hpp @@ -50,9 +50,9 @@ class EvseManager : public EvseManagerInterface { const std::function& pause_charging_callback); EvseInterface& get_evse(int32_t id) override; - const EvseInterface& get_evse(int32_t id) const override; + const EvseInterface& get_evse(const int32_t id) const override; - bool does_evse_exist(int32_t id) const override; + bool does_evse_exist(const int32_t id) const override; size_t get_number_of_evses() const override; diff --git a/lib/ocpp/v16/charge_point.cpp b/lib/ocpp/v16/charge_point.cpp index 69cc94194..b73467f8c 100644 --- a/lib/ocpp/v16/charge_point.cpp +++ b/lib/ocpp/v16/charge_point.cpp @@ -328,7 +328,7 @@ void ChargePoint::register_security_event_callback( } void ChargePoint::register_is_token_reserved_for_connector_callback( - const std::function& callback) { + const std::function& callback) { this->charge_point->register_is_token_reserved_for_connector_callback(callback); } diff --git a/lib/ocpp/v16/charge_point_configuration.cpp b/lib/ocpp/v16/charge_point_configuration.cpp index d2daba2bc..fc990741f 100644 --- a/lib/ocpp/v16/charge_point_configuration.cpp +++ b/lib/ocpp/v16/charge_point_configuration.cpp @@ -1512,7 +1512,7 @@ std::optional ChargePointConfiguration::getReserveConnectorZeroSupport KeyValue kv; kv.key = "ReserveConnectorZeroSupported"; kv.readonly = true; - kv.value.emplace(std::to_string(reserve_connector_zero_supported.value())); + kv.value.emplace(ocpp::conversions::bool_to_string(reserve_connector_zero_supported.value())); reserve_connector_zero_supported_kv.emplace(kv); } return reserve_connector_zero_supported_kv; diff --git a/lib/ocpp/v16/charge_point_impl.cpp b/lib/ocpp/v16/charge_point_impl.cpp index 07fc23f8c..3f1399590 100644 --- a/lib/ocpp/v16/charge_point_impl.cpp +++ b/lib/ocpp/v16/charge_point_impl.cpp @@ -1957,11 +1957,16 @@ void ChargePointImpl::handleRemoteStartTransactionRequest(ocpp::Callis_token_reserved_for_connector_callback != nullptr && - this->status->get_state(connector) == ChargePointStatus::Reserved && - !this->is_token_reserved_for_connector_callback(connector, call.msg.idTag.get())) { - obtainable = false; - continue; + if (this->is_token_reserved_for_connector_callback != nullptr) { + const ocpp::ReservationCheckStatus reservation_status = + is_token_reserved_for_connector_callback(connector, call.msg.idTag.get()); + + const bool is_reserved = (reservation_status == ocpp::ReservationCheckStatus::ReservedForOtherToken); + + if (this->status->get_state(connector) == ChargePointStatus::Reserved && is_reserved) { + obtainable = false; + continue; + } } if (obtainable) { @@ -2018,7 +2023,7 @@ void ChargePointImpl::handleRemoteStartTransactionRequest(ocpp::Callprovide_token_callback(call.msg.idTag.get(), referenced_connectors, true); // prevalidated } - }; + } } bool ChargePointImpl::validate_against_cache_entries(CiString<20> id_tag) { @@ -4488,7 +4493,7 @@ void ChargePointImpl::register_security_event_callback( } void ChargePointImpl::register_is_token_reserved_for_connector_callback( - const std::function& callback) { + const std::function& callback) { this->is_token_reserved_for_connector_callback = callback; } diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index 16c2465fc..4e2759db7 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -745,6 +746,10 @@ 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); } +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); +} + bool ChargePoint::on_charging_state_changed(const uint32_t evse_id, const ChargingStateEnum charging_state, const TriggerReasonEnum trigger_reason) { auto& evse = this->evse_manager->get_evse(evse_id); @@ -1057,6 +1062,15 @@ void ChargePoint::on_variable_changed(const SetVariableData& set_variable_data) this->handle_variable_changed(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); +} + void ChargePoint::initialize(const std::map& evse_connector_structure, const std::string& message_log_path) { this->device_model->check_integrity(evse_connector_structure); @@ -1315,6 +1329,12 @@ void ChargePoint::handle_message(const EnhancedMessage& messa case MessageType::HeartbeatResponse: 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); + break; case MessageType::SendLocalList: this->handle_send_local_authorization_list_req(json_message); break; @@ -1957,22 +1977,13 @@ std::optional ChargePoint::get_transaction_evseid(const CiString<36>& t return std::nullopt; } -bool ChargePoint::is_evse_reserved_for_other(EvseInterface& evse, const IdToken& id_token, - const std::optional& group_id_token) const { - const uint32_t connectors = evse.get_number_of_connectors(); - for (uint32_t i = 1; i <= connectors; ++i) { - const ConnectorStatusEnum status = - evse.get_connector(static_cast(i))->get_effective_connector_status(); - if (status == ConnectorStatusEnum::Reserved) { - const std::optional> groupIdToken = - group_id_token.has_value() ? group_id_token.value().idToken : std::optional>{}; +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 (!callbacks.is_reservation_for_token_callback(evse.get_id(), id_token.idToken, groupIdToken)) { - return true; - } - } - } - return false; + return callbacks.is_reservation_for_token_callback(evse.get_id(), id_token.idToken, groupIdToken); } bool ChargePoint::is_evse_connector_available(EvseInterface& evse) const { @@ -2005,6 +2016,36 @@ void ChargePoint::set_evse_connectors_unavailable(EvseInterface& evse, bool pers } } +bool ChargePoint::is_connector_available(const uint32_t evse_id, std::optional connector_type) { + EvseInterface* evse; + try { + evse = &evse_manager->get_evse(static_cast(evse_id)); + } catch (const EvseOutOfRangeException&) { + EVLOG_error << "Evse id " << evse_id << " is not a valid evse id."; + return false; + } + + std::optional status = + evse->get_connector_status(connector_type.value_or(ConnectorEnum::Unknown)); + if (!status.has_value()) { + return false; + } + + return status.value() == ConnectorStatusEnum::Available; +} + +bool ChargePoint::does_connector_exist(const uint32_t evse_id, std::optional connector_type) { + EvseInterface* evse; + try { + evse = &evse_manager->get_evse(static_cast(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.value_or(ConnectorEnum::Unknown)); +} + bool ChargePoint::is_offline() { return !this->connectivity_manager->is_websocket_connected(); } @@ -3148,9 +3189,12 @@ void ChargePoint::handle_remote_start_transaction_request(Call 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); @@ -3481,9 +3661,10 @@ void ChargePoint::handle_get_charging_profiles_req(Call evse_ids; // will contain all evse_ids of the profiles std::set sources; // will contain all sources of the profiles @@ -4504,7 +4685,8 @@ std::vector ChargePoint::get_all_composite_schedules(const in composite_schedule_response.schedule.has_value()) { composite_schedules.push_back(composite_schedule_response.schedule.value()); } else { - EVLOG_warning << "Could not internally retrieve composite schedule: " << composite_schedule_response; + EVLOG_warning << "Could not internally retrieve composite schedule for evse id " << evse_id << ": " + << composite_schedule_response; } } diff --git a/lib/ocpp/v201/evse.cpp b/lib/ocpp/v201/evse.cpp index 6260131dd..06a50ab60 100644 --- a/lib/ocpp/v201/evse.cpp +++ b/lib/ocpp/v201/evse.cpp @@ -86,6 +86,90 @@ 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) { + const uint32_t number_of_connectors = this->get_number_of_connectors(); + if (number_of_connectors == 0) { + return false; + } + + if (connector_type == ConnectorEnum::Unknown) { + return true; + } + + for (uint32_t i = 1; i <= number_of_connectors; ++i) { + Connector* connector; + try { + connector = this->get_connector(static_cast(i)); + } catch (const std::logic_error&) { + EVLOG_error << "Connector with id " << i << " does not exist"; + continue; + } + + if (connector == nullptr) { + EVLOG_error << "Connector with id " << i << " does not exist"; + continue; + } + + ConnectorEnum type = this->get_evse_connector_type(i).value_or(ConnectorEnum::Unknown); + if (type == ConnectorEnum::Unknown || type == connector_type) { + return true; + } + } + + return false; +} + +std::optional Evse::get_connector_status(std::optional connector_type) { + bool type_found = false; + ConnectorStatusEnum found_status = ConnectorStatusEnum::Unavailable; + const uint32_t number_of_connectors = this->get_number_of_connectors(); + if (number_of_connectors == 0) { + return std::nullopt; + } + + for (uint32_t i = 1; i <= number_of_connectors; ++i) { + Connector* connector; + try { + connector = this->get_connector(static_cast(i)); + } catch (const std::logic_error&) { + EVLOG_error << "Connector with id " << i << " does not exist"; + continue; + } + + if (connector == nullptr) { + EVLOG_error << "Connector with id " << i << " does not exist"; + continue; + } + + const ConnectorStatusEnum connector_status = connector->get_effective_connector_status(); + + const ConnectorEnum evse_connector_type = this->get_evse_connector_type(i).value_or(ConnectorEnum::Unknown); + const ConnectorEnum input_connector_type = connector_type.value_or(ConnectorEnum::Unknown); + const bool connector_type_unknown = + evse_connector_type == ConnectorEnum::Unknown || input_connector_type == ConnectorEnum::Unknown; + + if (connector_type_unknown || evse_connector_type == input_connector_type) { + type_found = true; + // We found an available connector, also store the status. + found_status = connector_status; + if (found_status == ConnectorStatusEnum::Available) { + // There is an available connector with the correct type and status: we don't have to search + // any further. + return found_status; + } + + // If status is not available, we keep on searching. If no connector is available, the status of + // (at least one of) the connectors is stored to return that later if no available connector is found. + } + } + + if (!type_found) { + return std::nullopt; + } + + return found_status; +} + void Evse::try_resume_transaction() { // Get an open transactions from the database and resume it if there is one auto transaction = this->database_handler->transaction_get(evse_id); @@ -121,6 +205,30 @@ void Evse::delete_database_transaction() { } } +std::optional Evse::get_evse_connector_type(const uint32_t connector_id) { + + auto connector = this->get_connector(static_cast(connector_id)); + if (connector == nullptr) { + return std::nullopt; + } + + ComponentVariable connector_cv = ConnectorComponentVariables::get_component_variable( + this->evse_id, connector_id, ConnectorComponentVariables::Type); + + const std::optional connector_type = + this->device_model.get_optional_value(connector_cv, AttributeEnum::Actual); + if (!connector_type.has_value()) { + return std::nullopt; + } + + try { + return conversions::string_to_connector_enum(connector_type.value()); + } catch (const StringToEnumException& e) { + EVLOG_warning << "Could not convert to ConnectorEnum: " << connector_type.value(); + return std::nullopt; + } +} + void Evse::open_transaction(const std::string& transaction_id, const int32_t connector_id, const DateTime& timestamp, const MeterValue& meter_start, const std::optional& id_token, const std::optional& group_id_token, const std::optional reservation_id, @@ -157,7 +265,8 @@ void Evse::open_transaction(const std::string& transaction_id, const int32_t con try { this->database_handler->transaction_insert(*this->transaction.get(), this->evse_id); } catch (const QueryExecutionException& e) { - // Delete previous transactions that should not exist anyway and try again. Otherwise throw to higher level + // Delete previous transactions that should not exist anyway and try again. Otherwise throw to higher + // level this->delete_database_transaction(); this->database_handler->transaction_insert(*this->transaction.get(), this->evse_id); } @@ -492,8 +601,8 @@ void Evse::send_meter_value_on_pricing_trigger(const MeterValue& meter_value) { } } - // Check if there is a power kw trigger and if that is triggered. For the power kw trigger, we added hysterisis to - // prevent constant triggering. + // Check if there is a power kw trigger and if that is triggered. For the power kw trigger, we added hysterisis + // to prevent constant triggering. const std::optional active_power_meter_value = utils::get_total_power_active_import(meter_value); if (!this->trigger_metervalue_on_power_kw.has_value() or !active_power_meter_value.has_value()) { diff --git a/lib/ocpp/v201/evse_manager.cpp b/lib/ocpp/v201/evse_manager.cpp index 71b7714e8..fa795f615 100644 --- a/lib/ocpp/v201/evse_manager.cpp +++ b/lib/ocpp/v201/evse_manager.cpp @@ -30,21 +30,21 @@ EvseManager::EvseIterator EvseManager::end() { } EvseInterface& EvseManager::get_evse(int32_t id) { - if (id == 0 or id > this->evses.size()) { + if (id <= 0 or id > this->evses.size()) { throw EvseOutOfRangeException(id); } return *this->evses.at(id - 1); } -const EvseInterface& EvseManager::get_evse(int32_t id) const { - if (id == 0 or id > this->evses.size()) { +const EvseInterface& EvseManager::get_evse(const int32_t id) const { + if (id <= 0 or id > this->evses.size()) { throw EvseOutOfRangeException(id); } return *this->evses.at(id - 1); } -bool EvseManager::does_evse_exist(int32_t id) const { - return id <= this->evses.size(); +bool EvseManager::does_evse_exist(const int32_t id) const { + return id >= 0 && static_cast(id) <= this->evses.size(); } size_t EvseManager::get_number_of_evses() const { diff --git a/tests/lib/ocpp/v201/mocks/evse_mock.hpp b/tests/lib/ocpp/v201/mocks/evse_mock.hpp index da05da0ac..0117a93e7 100644 --- a/tests/lib/ocpp/v201/mocks/evse_mock.hpp +++ b/tests/lib/ocpp/v201/mocks/evse_mock.hpp @@ -12,6 +12,9 @@ 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(std::optional, get_connector_status, + (std::optional connector_type)); MOCK_METHOD(void, open_transaction, (const std::string& transaction_id, const int32_t connector_id, const DateTime& timestamp, const MeterValue& meter_start, const std::optional& id_token, diff --git a/tests/lib/ocpp/v201/test_charge_point.cpp b/tests/lib/ocpp/v201/test_charge_point.cpp index c41ea3657..bd4428b30 100644 --- a/tests/lib/ocpp/v201/test_charge_point.cpp +++ b/tests/lib/ocpp/v201/test_charge_point.cpp @@ -183,8 +183,8 @@ class ChargePointCommonTestFixtureV201 : public DatabaseTestingUtils { testing::MockFunction remote_start_transaction_callback_mock; - testing::MockFunction idToken, - const std::optional> groupIdToken)> + testing::MockFunction idToken, + const std::optional> groupIdToken)> is_reservation_for_token_callback_mock; testing::MockFunction update_firmware_request_callback_mock;