diff --git a/config/v201/component_config/standardized/InternalCtrlr.json b/config/v201/component_config/standardized/InternalCtrlr.json index f27668ad4..28ce3b60f 100644 --- a/config/v201/component_config/standardized/InternalCtrlr.json +++ b/config/v201/component_config/standardized/InternalCtrlr.json @@ -752,6 +752,24 @@ "description": "If enabled the transactions that were active before shutdown will be resumed, if possible", "default": false, "type": "boolean" + }, + "NetworkConfigTimeout": { + "variable_name": "NetworkConfigTimeout", + "characteristics": { + "supportsMonitoring": true, + "dataType": "integer", + "minLimit": 1 + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadWrite" + } + ], + "description": "Timeout value in seconds to wait for a response from a network configuration request", + "minimum": 1, + "default": "60", + "type": "integer" } }, "required": [ diff --git a/doc/networkconnectivity/README.md b/doc/networkconnectivity/README.md new file mode 100644 index 000000000..45e9fb5c1 --- /dev/null +++ b/doc/networkconnectivity/README.md @@ -0,0 +1,103 @@ +# Network connection profile interface + +libocpp automatically tries to connect using the given network connection profiles. +However, if you want more control, you can use the callbacks provided for the network connection. + +libocpp will automatically connect to the network profile with the highest priority. +If this fails, it will network profile with the second highest priority, and so on. + +## Set up interface (optional) + +A callback can be implemented to set up the interface. For example, if the interface is a modem, it must first be +be activated before it is possible to connect to this interface. To do this, you can implement the callback +`std::future(configure_network_connection_profile_callback(configuration_slot, NetworkConnectionProfile))` + +In the implementation of this callback, you have to create a promise and return the future to the promise: +```cpp +std::promise promise(); +std::future future = promise.get_future(); +return future; +``` + +If the network was setup successfully, you can set the values in the promise with +```cpp +promise.set_value(configNetworkResult); +``` +This way, libocpp knows that it can connect to the given interface and will try to do so. +A timeout can be configured using `NetworkConfigTimeout' to wait longer or shorter than the default 60 seconds. + +### Bind to a specific interface + +In some cases there are multiple network interfaces available and you may want to connect to a specific one. +In `ConfigNetworkResult` you can specify which interface you want the websocket to bind to. +Sometimes an interface has more than one IP address (in the case of a local/auto link for example). +In this case you want the websocket to bind to a specific IP address. The `interface_address` in ConfigNetworkResult supports both. +It will bind to the given network interface (a string containing the name of the interface) or the given ip address (a string containing the ip address in human readable format). + +## Connect to higher network connection profile priority (optional) + +Normally, when libocpp is connected with a network connection profile, it will not disconnect. +However, there may be a situation where libocpp is connected to a profile with priority 2 or lower, and you find out at system level that an interface (with a higher priority) has changed and is now up. +A call is added so that you can suggest that libocpp switch to this profile: `bool on_try_switch_network_connection_profile(const int32_t configuration_slot)`. +libocpp will inform the caller by the return value if it tries to switch to this profile. + +## Disconnected / connected callbacks + +libocpp provides two callbacks for when the websocket is connected and disconnected. It will provide the network slot +in these callbacks, so you can keep the network connection in use (e.g. not disable the modem), or disable the network connection (example again: disable the modem). + +## Sequence diagram + +'core' can be read as any application that implements libocpp + +For step 9, ping is one way to check if a CSMS is up, but you of course can implement a way to check this yourself. + +```mermaid +sequenceDiagram +participant csms +participant libocpp +participant core +autonumber + +note over csms,libocpp: libocpp wants to connect to network connection profile + +libocpp ->>+ core: std::future(configure_network_connection_profile_callback(
configuration_slot, NetworkConnectionProfile)) +note over core: ... possible delay ... +core ->> core: Setup network, e.g. setup modem +core ->>- libocpp: promise.set_value(status,
ip_address, etc) + +alt within timeout + + %% core ->> libocpp: on_network_update (ip address) + libocpp ->> csms: connect websocket (ip address) + csms ->> libocpp: ACK + libocpp ->> core: websocket_connected_callback(configuration_slot, NetworkConnectionProfile) + core ->> core: disable unneeded interfaces,
e.g. disable modem +else timeout reached, next network connection profile selected + libocpp -->> core: std::future(configure_network_connection_profile_callback(
configuration_slot, NetworkConnectionProfile)) (see 1) +end + + +note over libocpp: CSMS is connected via connection profile prio 2 (for example modem) but prio 1 (for example eth0) comes up + +loop until prio 1 csms is found + core ->> csms: ping +end + +core ->> libocpp: on_try_switch_networkconnectionprofile(configuration_slot) +libocpp -->> core: std::future(configure_network_connection_profile_callback(
configuration_slot, NetworkConnectionProfile)) (see 1) + + +note over csms,libocpp: Network is disconnected (for example networkcable removed) + +core ->> libocpp: disconnect csms (on_network_disconnected(configuration_slot, OCPPInterfaceEnum) +libocpp ->> core: websocket_disconnected_callback(configuration_slot, NetworkConnectionProfile) +libocpp -->> core: std::future(configure_network_connection_profile_callback(
configuration_slot, NetworkConnectionProfile)) (see 1) + + +note over csms,libocpp: Network is disconnected (websocket timeout) + +libocpp ->> libocpp: disconnect csms +libocpp ->> core: websocket_disconnected_callback(configuration_slot, NetworkConnectionProfile) +libocpp -->> core: std::future(configure_network_connection_profile_callback(
configuration_slot, NetworkConnectionProfile)) (see 1) +``` diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index 1dcc368e4..e2804b0c6 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -11,7 +11,6 @@ #include #include -#include #include #include #include @@ -109,6 +108,34 @@ class ChargePointInterface { /// \brief Disconnects the the websocket connection to the CSMS if it is connected virtual void disconnect_websocket() = 0; + /// + /// \brief Can be called when a network is disconnected, for example when an ethernet cable is removed. + /// + /// This is introduced because the websocket can take several minutes to timeout when a network interface becomes + /// unavailable, whereas the system can detect this sooner. + /// + /// \param configuration_slot The slot of the network connection profile that is disconnected. + /// + virtual void on_network_disconnected(int32_t configuration_slot) = 0; + + /// + /// \brief Can be called when a network is disconnected, for example when an ethernet cable is removed. + /// + /// This is introduced because the websocket can take several minutes to timeout when a network interface becomes + /// unavailable, whereas the system can detect this sooner. + /// + /// \param ocpp_interface The interface that is disconnected. + /// + virtual void on_network_disconnected(OCPPInterfaceEnum ocpp_interface) = 0; + + /// \brief Switch to a specific network connection profile given the configuration slot. + /// + /// Switch will only be done when the configuration slot has a higher priority. + /// + /// \param configuration_slot Slot in which the configuration is stored + /// \return true if the switch is possible. + virtual bool on_try_switch_network_connection_profile(const int32_t configuration_slot) = 0; + /// \brief Chargepoint notifies about new firmware update status firmware_update_status. This function should be /// called during a Firmware Update to indicate the current firmware_update_status. /// \param request_id The request_id. When it is -1, it will not be included in the request. @@ -316,6 +343,26 @@ class ChargePointInterface { /// \return vector of composite schedules, one for each evse_id including 0. virtual std::vector get_all_composite_schedules(const int32_t duration, const ChargingRateUnitEnum& unit) = 0; + + /// \brief Gets the configured NetworkConnectionProfile based on the given \p configuration_slot . The + /// central system uri of the connection options will not contain ws:// or wss:// because this method removes it if + /// present. This returns the value from the cached network connection profiles. \param + /// network_configuration_priority \return + virtual std::optional + get_network_connection_profile(const int32_t configuration_slot) = 0; + + /// \brief Get the priority of the given configuration slot. + /// \param configuration_slot The configuration slot to get the priority from. + /// \return The priority if the configuration slot exists. + /// + virtual std::optional get_configuration_slot_priority(const int configuration_slot) = 0; + + /// @brief Get the network connection priorities. + /// Each item in the vector contains the configured configuration slots, where the slot with index 0 has the highest + /// priority. + /// @return The network connection priorities + /// + virtual const std::vector& get_network_connection_priorities() const = 0; }; /// \brief Class implements OCPP2.0.1 Charging Station @@ -413,8 +460,10 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void init_certificate_expiration_check_timers(); void scheduled_check_client_certificate_expiration(); void scheduled_check_v2g_certificate_expiration(); - void websocket_connected_callback(const int security_profile); - void websocket_disconnected_callback(); + void websocket_connected_callback(const int configuration_slot, + const NetworkConnectionProfile& network_connection_profile); + void websocket_disconnected_callback(const int configuration_slot, + const NetworkConnectionProfile& network_connection_profile); void websocket_connection_failed(ConnectionFailedReason reason); void update_dm_availability_state(const int32_t evse_id, const int32_t connector_id, const ConnectorStatusEnum status); @@ -799,6 +848,12 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa virtual void connect_websocket() override; virtual void disconnect_websocket() override; + void on_network_disconnected(int32_t configuration_slot) override; + + void on_network_disconnected(OCPPInterfaceEnum ocpp_interface) override; + + bool on_try_switch_network_connection_profile(const int32_t configuration_slot) override; + void on_firmware_update_status_notification(int32_t request_id, const FirmwareStatusEnum& firmware_update_status) override; @@ -883,6 +938,12 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa std::vector get_all_composite_schedules(const int32_t duration, const ChargingRateUnitEnum& unit) override; + std::optional get_network_connection_profile(const int32_t configuration_slot) override; + + std::optional get_configuration_slot_priority(const int configuration_slot) override; + + const std::vector& get_network_connection_priorities() const override; + /// \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/charge_point_callbacks.hpp b/include/ocpp/v201/charge_point_callbacks.hpp index 83a114e4b..a154f6e69 100644 --- a/include/ocpp/v201/charge_point_callbacks.hpp +++ b/include/ocpp/v201/charge_point_callbacks.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -84,8 +85,7 @@ struct Callbacks { std::optional> validate_network_profile_callback; - std::optional> - configure_network_connection_profile_callback; + std::optional configure_network_connection_profile_callback; std::optional> time_sync_callback; /// \brief callback to be called to congfigure ocpp message logging @@ -135,7 +135,9 @@ struct Callbacks { transaction_event_response_callback; /// \brief Callback function is called when the websocket connection status changes - std::optional> connection_state_changed_callback; + std::optional> + connection_state_changed_callback; /// \brief Callback functions called for get / set / clear display messages std::optional(const GetDisplayMessagesRequest& request)>> diff --git a/include/ocpp/v201/connectivity_manager.hpp b/include/ocpp/v201/connectivity_manager.hpp index 3b2c23ec1..e72360209 100644 --- a/include/ocpp/v201/connectivity_manager.hpp +++ b/include/ocpp/v201/connectivity_manager.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -14,11 +15,11 @@ namespace v201 { class DeviceModel; -using WebsocketConnectedCallback = std::function; -using WebsocketDisconnectedCallback = std::function; +using WebsocketConnectionCallback = + std::function; using WebsocketConnectionFailedCallback = std::function; -using ConfigureNetworkConnectionProfileCallback = - std::function; +using ConfigureNetworkConnectionProfileCallback = std::function( + const int32_t configuration_slot, const NetworkConnectionProfile& network_connection_profile)>; class ConnectivityManager { private: @@ -33,9 +34,9 @@ class ConnectivityManager { /// \brief The message callback std::function message_callback; /// \brief Callback that is called when the websocket is connected successfully - std::optional websocket_connected_callback; + std::optional websocket_connected_callback; /// \brief Callback that is called when the websocket connection is disconnected - std::optional websocket_disconnected_callback; + std::optional websocket_disconnected_callback; /// \brief Callback that is called when the websocket could not connect with a specific reason std::optional websocket_connection_failed_callback; /// \brief Callback that is called to configure a network connection profile when none is configured @@ -46,8 +47,8 @@ class ConnectivityManager { int network_configuration_priority; /// @brief Local cached network connection profiles std::vector network_connection_profiles; - /// @brief local cached network conenction priorities - std::vector network_connection_priorities; + /// @brief local cached network connection priorities + std::vector network_connection_priorities; WebsocketConnectionOptions current_connection_options{}; public: @@ -69,11 +70,11 @@ class ConnectivityManager { /// \brief Set the \p callback that is called when the websocket is connected. /// - void set_websocket_connected_callback(WebsocketConnectedCallback callback); + void set_websocket_connected_callback(WebsocketConnectionCallback callback); /// \brief Set the \p callback that is called when the websocket is disconnected. /// - void set_websocket_disconnected_callback(WebsocketDisconnectedCallback callback); + void set_websocket_disconnected_callback(WebsocketConnectionCallback callback); /// \brief Set the \p callback that is called when the websocket could not connect with a specific reason /// @@ -84,11 +85,24 @@ class ConnectivityManager { void set_configure_network_connection_profile_callback(ConfigureNetworkConnectionProfileCallback callback); /// \brief Gets the configured NetworkConnectionProfile based on the given \p configuration_slot . The - /// central system uri ofthe connection options will not contain ws:// or wss:// because this method removes it if + /// central system uri of the connection options will not contain ws:// or wss:// because this method removes it if /// present. This returns the value from the cached network connection profiles. \param /// network_configuration_priority \return std::optional get_network_connection_profile(const int32_t configuration_slot); + /// \brief Get the priority of the given configuration slot. + /// \param configuration_slot The configuration slot to get the priority from. + /// \return The priority if the configuration slot exists. + /// + std::optional get_configuration_slot_priority(const int configuration_slot); + + /// @brief Get the network connection priorities. + /// Each item in the vector contains the configured configuration slots, where the slot with index 0 has the highest + /// priority. + /// @return The network connection priorities + /// + const std::vector& get_network_connection_priorities() const; + /// \brief Check if the websocket is connected /// \return True is the websocket is connected, else false /// @@ -115,6 +129,34 @@ class ConnectivityManager { /// bool send_to_websocket(const std::string& message); + /// + /// \brief Can be called when a network is disconnected, for example when an ethernet cable is removed. + /// + /// This is introduced because the websocket can take several minutes to timeout when a network interface becomes + /// unavailable, whereas the system can detect this sooner. + /// + /// \param configuration_slot The slot of the network connection profile that is disconnected. + /// + void on_network_disconnected(int32_t configuration_slot); + + /// + /// \brief Can be called when a network is disconnected, for example when an ethernet cable is removed. + /// + /// This is introduced because the websocket can take several minutes to timeout when a network interface becomes + /// unavailable, whereas the system can detect this sooner. + /// + /// \param ocpp_interface The interface that is disconnected. + /// + void on_network_disconnected(OCPPInterfaceEnum ocpp_interface); + + /// \brief Switch to a specific network connection profile given the configuration slot. + /// + /// Switch will only be done when the configuration slot has a higher priority. + /// + /// \param configuration_slot Slot in which the configuration is stored + /// \return true if the switch is possible. + bool on_try_switch_network_connection_profile(const int32_t configuration_slot); + private: /// \brief Init the websocket /// @@ -125,6 +167,36 @@ class ConnectivityManager { /// WebsocketConnectionOptions get_ws_connection_options(const int32_t configuration_slot); + /// \brief Function invoked when the web socket connected with the \p security_profile + /// + void on_websocket_connected(const int security_profile); + + /// \brief Function invoked when the web socket disconnected + /// + void on_websocket_disconnected(); + + /// \brief Function invoked when the web socket closes + /// + void on_websocket_closed(ocpp::WebsocketCloseReason reason); + + /// \brief Reconnect with the give websocket \p reason + /// + void reconnect(WebsocketCloseReason reason, std::optional next_priority = std::nullopt); + + /// + /// \brief Returns true if the provided configuration slot is of higher priority compared to the one currently + /// in use. + /// \param new_configuration_slot The configuration slot to check. + /// \return True when given slot is of higher priority. + /// + bool is_higher_priority_profile(const int new_configuration_slot); + + /// + /// \brief Get the active network configuration slot in use. + /// \return The active slot the network is connected to or the pending slot. + /// + int get_active_network_configuration_slot(); + /// \brief Moves websocket network_configuration_priority to next profile /// void next_network_configuration_priority(); diff --git a/include/ocpp/v201/ctrlr_component_variables.hpp b/include/ocpp/v201/ctrlr_component_variables.hpp index 485bcf84b..8099f5aa7 100644 --- a/include/ocpp/v201/ctrlr_component_variables.hpp +++ b/include/ocpp/v201/ctrlr_component_variables.hpp @@ -64,6 +64,7 @@ extern const ComponentVariable& LogRotationMaximumFileCount; extern const ComponentVariable& SupportedChargingProfilePurposeTypes; extern const ComponentVariable& SupportedCriteria; extern const ComponentVariable& RoundClockAlignedTimestamps; +extern const ComponentVariable& NetworkConfigTimeout; extern const ComponentVariable& MaxCompositeScheduleDuration; extern const RequiredComponentVariable& NumberOfConnectors; extern const ComponentVariable& UseSslDefaultVerifyPaths; diff --git a/include/ocpp/v201/ocpp_types.hpp b/include/ocpp/v201/ocpp_types.hpp index 38ee2399b..5c76097e8 100644 --- a/include/ocpp/v201/ocpp_types.hpp +++ b/include/ocpp/v201/ocpp_types.hpp @@ -889,6 +889,13 @@ void from_json(const json& j, SetMonitoringResult& k); /// \returns an output stream with the SetMonitoringResult written to std::ostream& operator<<(std::ostream& os, const SetMonitoringResult& k); +/// @brief The result of a configuration of a network profile. +struct ConfigNetworkResult { + uint8_t network_profile_slot; ///< @brief Network profile slot. + std::optional interface_address; ///< ip address or interface string + bool success; ///< true if the configuration was successful +}; + struct SetVariableData { CiString<1000> attributeValue; Component component; diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index b49155196..08be6786f 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -201,6 +201,18 @@ void ChargePoint::disconnect_websocket() { this->connectivity_manager->disconnect_websocket(); } +void ChargePoint::on_network_disconnected(int32_t configuration_slot) { + this->connectivity_manager->on_network_disconnected(configuration_slot); +} + +void ChargePoint::on_network_disconnected(OCPPInterfaceEnum ocpp_interface) { + this->connectivity_manager->on_network_disconnected(ocpp_interface); +} + +bool ChargePoint::on_try_switch_network_connection_profile(const int32_t configuration_slot) { + return this->connectivity_manager->on_try_switch_network_connection_profile(configuration_slot); +} + void ChargePoint::on_firmware_update_status_notification(int32_t request_id, const FirmwareStatusEnum& firmware_update_status) { if (this->firmware_status == firmware_update_status) { @@ -1152,9 +1164,9 @@ void ChargePoint::initialize(const std::map& evse_connector_st std::bind(&ChargePoint::message_callback, this, std::placeholders::_1)); this->connectivity_manager->set_websocket_connected_callback( - std::bind(&ChargePoint::websocket_connected_callback, this, std::placeholders::_1)); + std::bind(&ChargePoint::websocket_connected_callback, this, std::placeholders::_1, std::placeholders::_2)); this->connectivity_manager->set_websocket_disconnected_callback( - std::bind(&ChargePoint::websocket_disconnected_callback, this)); + std::bind(&ChargePoint::websocket_disconnected_callback, this, std::placeholders::_1, std::placeholders::_2)); this->connectivity_manager->set_websocket_connection_failed_callback( std::bind(&ChargePoint::websocket_connection_failed, this, std::placeholders::_1)); @@ -4204,14 +4216,15 @@ void ChargePoint::scheduled_check_v2g_certificate_expiration() { .value_or(12 * 60 * 60))); } -void ChargePoint::websocket_connected_callback(const int security_profile) { +void ChargePoint::websocket_connected_callback(const int configuration_slot, + const NetworkConnectionProfile& network_connection_profile) { this->message_queue->resume(this->message_queue_resume_delay); const auto& security_profile_cv = ControllerComponentVariables::SecurityProfile; if (security_profile_cv.variable.has_value()) { - this->device_model->set_read_only_value(security_profile_cv.component, security_profile_cv.variable.value(), - AttributeEnum::Actual, std::to_string(security_profile), - VARIABLE_ATTRIBUTE_VALUE_SOURCE_INTERNAL); + this->device_model->set_read_only_value( + security_profile_cv.component, security_profile_cv.variable.value(), AttributeEnum::Actual, + std::to_string(network_connection_profile.securityProfile), VARIABLE_ATTRIBUTE_VALUE_SOURCE_INTERNAL); } if (this->registration_status == RegistrationStatusEnum::Accepted and @@ -4241,11 +4254,12 @@ void ChargePoint::websocket_connected_callback(const int security_profile) { this->skip_invalid_csms_certificate_notifications = false; if (this->callbacks.connection_state_changed_callback.has_value()) { - this->callbacks.connection_state_changed_callback.value()(true); + this->callbacks.connection_state_changed_callback.value()(true, configuration_slot, network_connection_profile); } } -void ChargePoint::websocket_disconnected_callback() { +void ChargePoint::websocket_disconnected_callback(const int configuration_slot, + const NetworkConnectionProfile& network_connection_profile) { this->message_queue->pause(); // check if offline threshold has been defined @@ -4257,7 +4271,8 @@ void ChargePoint::websocket_disconnected_callback() { this->client_certificate_expiration_check_timer.stop(); this->v2g_certificate_expiration_check_timer.stop(); if (this->callbacks.connection_state_changed_callback.has_value()) { - this->callbacks.connection_state_changed_callback.value()(false); + this->callbacks.connection_state_changed_callback.value()(false, configuration_slot, + network_connection_profile); } } @@ -4531,6 +4546,18 @@ std::vector ChargePoint::get_all_composite_schedules(const in return composite_schedules; } +std::optional ChargePoint::get_network_connection_profile(const int32_t configuration_slot) { + return this->connectivity_manager->get_network_connection_profile(configuration_slot); +} + +std::optional ChargePoint::get_configuration_slot_priority(const int configuration_slot) { + return this->connectivity_manager->get_configuration_slot_priority(configuration_slot); +} + +const std::vector& ChargePoint::get_network_connection_priorities() const { + return this->connectivity_manager->get_network_connection_priorities(); +} + // Static functions /// diff --git a/lib/ocpp/v201/connectivity_manager.cpp b/lib/ocpp/v201/connectivity_manager.cpp index 491a21921..8dd4a1b5c 100644 --- a/lib/ocpp/v201/connectivity_manager.cpp +++ b/lib/ocpp/v201/connectivity_manager.cpp @@ -10,6 +10,9 @@ namespace { const auto WEBSOCKET_INIT_DELAY = std::chrono::seconds(2); const std::string VARIABLE_ATTRIBUTE_VALUE_SOURCE_INTERNAL = "internal"; +/// \brief Default timeout for the return value (future) of the `configure_network_connection_profile_callback` +/// function. +constexpr int32_t default_network_config_timeout_seconds = 60; } // namespace namespace ocpp { @@ -41,16 +44,16 @@ void ConnectivityManager::set_websocket_connection_options(const WebsocketConnec } void ConnectivityManager::set_websocket_connection_options_without_reconnect() { - const auto configuration_slot = this->network_connection_priorities.at(this->network_configuration_priority); - const auto connection_options = this->get_ws_connection_options(std::stoi(configuration_slot)); + const int configuration_slot = get_active_network_configuration_slot(); + const auto connection_options = this->get_ws_connection_options(configuration_slot); this->set_websocket_connection_options(connection_options); } -void ConnectivityManager::set_websocket_connected_callback(WebsocketConnectedCallback callback) { +void ConnectivityManager::set_websocket_connected_callback(WebsocketConnectionCallback callback) { this->websocket_connected_callback = callback; } -void ConnectivityManager::set_websocket_disconnected_callback(WebsocketDisconnectedCallback callback) { +void ConnectivityManager::set_websocket_disconnected_callback(WebsocketConnectionCallback callback) { this->websocket_disconnected_callback = callback; } @@ -82,6 +85,20 @@ ConnectivityManager::get_network_connection_profile(const int32_t configuration_ return std::nullopt; } +std::optional ConnectivityManager::get_configuration_slot_priority(const int configuration_slot) { + auto it = std::find(this->network_connection_priorities.begin(), this->network_connection_priorities.end(), + configuration_slot); + if (it != network_connection_priorities.end()) { + // Index is iterator - begin iterator + return it - network_connection_priorities.begin(); + } + return std::nullopt; +} + +const std::vector& ConnectivityManager::get_network_connection_priorities() const { + return this->network_connection_priorities; +} + bool ConnectivityManager::is_websocket_connected() { return this->websocket != nullptr && this->websocket->is_connected(); } @@ -89,6 +106,7 @@ bool ConnectivityManager::is_websocket_connected() { void ConnectivityManager::start() { init_websocket(); if (websocket != nullptr) { + this->disable_automatic_websocket_reconnects = false; websocket->connect(); } } @@ -123,6 +141,57 @@ bool ConnectivityManager::send_to_websocket(const std::string& message) { return this->websocket->send(message); } +void ConnectivityManager::on_network_disconnected(int32_t configuration_slot) { + const int actual_configuration_slot = get_active_network_configuration_slot(); + std::optional network_connection_profile = + this->get_network_connection_profile(actual_configuration_slot); + + if (!network_connection_profile.has_value()) { + EVLOG_warning << "Network disconnected. No network connection profile configured"; + } else if (configuration_slot == actual_configuration_slot) { + // Since there is no connection anymore: disconnect the websocket, the manager will try to connect with the next + // available network connection profile as we enable reconnects. + this->disconnect_websocket(ocpp::WebsocketCloseReason::GoingAway); + this->disable_automatic_websocket_reconnects = false; + } +} + +void ConnectivityManager::on_network_disconnected(OCPPInterfaceEnum ocpp_interface) { + + const int actual_configuration_slot = get_active_network_configuration_slot(); + std::optional network_connection_profile = + this->get_network_connection_profile(actual_configuration_slot); + + if (!network_connection_profile.has_value()) { + EVLOG_warning << "Network disconnected. No network connection profile configured"; + } else if (ocpp_interface == network_connection_profile.value().ocppInterface) { + // Since there is no connection anymore: disconnect the websocket, the manager will try to connect with the next + // available network connection profile as we enable reconnects. + this->disconnect_websocket(ocpp::WebsocketCloseReason::GoingAway); + this->disable_automatic_websocket_reconnects = false; + } +} + +bool ConnectivityManager::on_try_switch_network_connection_profile(const int32_t configuration_slot) { + if (!is_higher_priority_profile(configuration_slot)) { + return false; + } + + EVLOG_info << "Trying to connect with higher priority network connection profile (configuration slots: " + << this->get_active_network_configuration_slot() << " --> " << configuration_slot << ")."; + + const std::optional network_connection_profile_opt = + this->get_network_connection_profile(configuration_slot); + if (!network_connection_profile_opt.has_value()) { + EVLOG_warning << "Could not find network connection profile belonging to configuration slot " + << configuration_slot; + return false; + } + this->disconnect_websocket(WebsocketCloseReason::Normal); + reconnect(WebsocketCloseReason::Normal, get_configuration_slot_priority(configuration_slot)); + return true; +} + void ConnectivityManager::init_websocket() { if (this->device_model.get_value(ControllerComponentVariables::ChargePointId).find(':') != std::string::npos) { @@ -132,16 +201,50 @@ void ConnectivityManager::init_websocket() { // cache the network profiles on initialization cache_network_connection_profiles(); - const auto configuration_slot = this->network_connection_priorities.at(this->network_configuration_priority); - const auto connection_options = this->get_ws_connection_options(std::stoi(configuration_slot)); - - const auto network_connection_profile = this->get_network_connection_profile(std::stoi(configuration_slot)); + const int config_slot_int = this->network_connection_priorities.at(this->network_configuration_priority); + + const auto network_connection_profile = this->get_network_connection_profile(config_slot_int); + // Not const as the iface member can be set by the configure network connection profile callback + auto connection_options = this->get_ws_connection_options(config_slot_int); + bool can_use_connection_profile = true; + + if (!network_connection_profile.has_value()) { + EVLOG_warning << "No network connection profile configured for " << config_slot_int; + can_use_connection_profile = false; + } else if (this->configure_network_connection_profile_callback.has_value()) { + EVLOG_debug << "Request to configure network connection profile " << config_slot_int; + + std::future config_status = this->configure_network_connection_profile_callback.value()( + config_slot_int, network_connection_profile.value()); + const int32_t config_timeout = + this->device_model.get_optional_value(ControllerComponentVariables::NetworkConfigTimeout) + .value_or(default_network_config_timeout_seconds); + + std::future_status status = config_status.wait_for(std::chrono::seconds(config_timeout)); + + switch (status) { + case std::future_status::deferred: + case std::future_status::timeout: { + EVLOG_warning << "Timeout configuring config slot: " << config_slot_int; + can_use_connection_profile = false; + break; + } + case std::future_status::ready: { + ConfigNetworkResult result = config_status.get(); + if (result.success and result.network_profile_slot == config_slot_int) { + EVLOG_debug << "Config slot " << config_slot_int << " is configured"; + // Set interface or ip to connection options. + connection_options.iface = result.interface_address; + } else { + EVLOG_warning << "Could not configure config slot " << config_slot_int; + can_use_connection_profile = false; + } + break; + } + } + } - if (!network_connection_profile.has_value() or - (this->configure_network_connection_profile_callback.has_value() and - !this->configure_network_connection_profile_callback.value()(network_connection_profile.value()))) { - EVLOG_warning << "NetworkConnectionProfile could not be retrieved or configuration of network with the given " - "profile failed"; + if (!can_use_connection_profile) { this->websocket_timer.timeout( [this]() { this->next_network_configuration_priority(); @@ -152,13 +255,13 @@ void ConnectivityManager::init_websocket() { } EVLOG_info << "Open websocket with NetworkConfigurationPriority: " << this->network_configuration_priority + 1 - << " which is configurationSlot " << configuration_slot; + << " which is configurationSlot " << config_slot_int; if (const auto& active_network_profile_cv = ControllerComponentVariables::ActiveNetworkProfile; active_network_profile_cv.variable.has_value()) { - this->device_model.set_read_only_value(active_network_profile_cv.component, - active_network_profile_cv.variable.value(), AttributeEnum::Actual, - configuration_slot, VARIABLE_ATTRIBUTE_VALUE_SOURCE_INTERNAL); + this->device_model.set_read_only_value( + active_network_profile_cv.component, active_network_profile_cv.variable.value(), AttributeEnum::Actual, + std::to_string(config_slot_int), VARIABLE_ATTRIBUTE_VALUE_SOURCE_INTERNAL); } if (const auto& security_profile_cv = ControllerComponentVariables::SecurityProfile; @@ -171,31 +274,11 @@ void ConnectivityManager::init_websocket() { this->websocket = std::make_unique(connection_options, this->evse_security, this->logging); - if (this->websocket_connected_callback.has_value()) { - this->websocket->register_connected_callback(websocket_connected_callback.value()); - } - - if (this->websocket_disconnected_callback.has_value()) { - this->websocket->register_disconnected_callback(websocket_disconnected_callback.value()); - } - + this->websocket->register_connected_callback( + std::bind(&ConnectivityManager::on_websocket_connected, this, std::placeholders::_1)); + this->websocket->register_disconnected_callback(std::bind(&ConnectivityManager::on_websocket_disconnected, this)); this->websocket->register_closed_callback( - [this, connection_options, configuration_slot](const WebsocketCloseReason reason) { - EVLOG_warning << "Closed websocket of NetworkConfigurationPriority: " - << this->network_configuration_priority + 1 << " which is configurationSlot " - << configuration_slot; - - if (!this->disable_automatic_websocket_reconnects) { - this->websocket_timer.timeout( - [this, reason]() { - if (reason != WebsocketCloseReason::ServiceRestart) { - this->next_network_configuration_priority(); - } - this->start(); - }, - WEBSOCKET_INIT_DELAY); - } - }); + std::bind(&ConnectivityManager::on_websocket_closed, this, std::placeholders::_1)); if (websocket_connection_failed_callback.has_value()) { this->websocket->register_connection_failed_callback(websocket_connection_failed_callback.value()); @@ -250,6 +333,87 @@ WebsocketConnectionOptions ConnectivityManager::get_ws_connection_options(const return connection_options; } +void ConnectivityManager::on_websocket_connected([[maybe_unused]] int security_profile) { + const int actual_configuration_slot = get_active_network_configuration_slot(); + std::optional network_connection_profile = + this->get_network_connection_profile(actual_configuration_slot); + + if (this->websocket_connected_callback.has_value() and network_connection_profile.has_value()) { + this->websocket_connected_callback.value()(actual_configuration_slot, network_connection_profile.value()); + } +} + +void ConnectivityManager::on_websocket_disconnected() { + std::optional network_connection_profile = + this->get_network_connection_profile(this->get_active_network_configuration_slot()); + + if (this->websocket_disconnected_callback.has_value() and network_connection_profile.has_value()) { + this->websocket_disconnected_callback.value()(this->get_active_network_configuration_slot(), + network_connection_profile.value()); + } +} + +void ConnectivityManager::on_websocket_closed(ocpp::WebsocketCloseReason reason) { + EVLOG_warning << "Closed websocket of NetworkConfigurationPriority: " << this->network_configuration_priority + 1 + << " which is configurationSlot " << this->get_active_network_configuration_slot(); + + if (!this->disable_automatic_websocket_reconnects) { + reconnect(reason); + } +} + +void ConnectivityManager::reconnect(WebsocketCloseReason reason, std::optional next_priority) { + this->websocket_timer.timeout( + [this, reason, next_priority]() { + if (reason != WebsocketCloseReason::ServiceRestart) { + if (!next_priority.has_value()) { + this->next_network_configuration_priority(); + } else { + this->network_configuration_priority = next_priority.value(); + } + } + this->start(); + }, + WEBSOCKET_INIT_DELAY); +} + +bool ConnectivityManager::is_higher_priority_profile(const int new_configuration_slot) { + + const int current_slot = get_active_network_configuration_slot(); + if (current_slot == 0) { + // No slot in use, new is always higher priority. + return true; + } + + if (current_slot == new_configuration_slot) { + // Slot is the same, probably already connected + return false; + } + + const std::optional new_priority = get_configuration_slot_priority(new_configuration_slot); + if (!new_priority.has_value()) { + // Slot not found. + return false; + } + + const std::optional current_priority = get_configuration_slot_priority(current_slot); + if (!current_priority.has_value()) { + // Slot not found. + return false; + } + + if (new_priority.value() < current_priority.value()) { + // Priority is indeed higher (lower index means higher priority) + return true; + } + + return false; +} + +int ConnectivityManager::get_active_network_configuration_slot() { + return this->network_connection_priorities.at(this->network_configuration_priority); +} + void ConnectivityManager::next_network_configuration_priority() { // retrieve priorities from cache @@ -271,8 +435,12 @@ void ConnectivityManager::cache_network_connection_profiles() { this->network_connection_profiles = json::parse(this->device_model.get_value(ControllerComponentVariables::NetworkConnectionProfiles)); - this->network_connection_priorities = ocpp::split_string( - this->device_model.get_value(ControllerComponentVariables::NetworkConfigurationPriority), ','); + for (const std::string& str : ocpp::split_string( + this->device_model.get_value(ControllerComponentVariables::NetworkConfigurationPriority), + ',')) { + int num = std::stoi(str); + this->network_connection_priorities.push_back(num); + } if (this->network_connection_priorities.empty()) { EVLOG_AND_THROW(std::runtime_error("NetworkConfigurationPriority must not be empty")); diff --git a/lib/ocpp/v201/ctrlr_component_variables.cpp b/lib/ocpp/v201/ctrlr_component_variables.cpp index 93cba64f1..a4fcbc308 100644 --- a/lib/ocpp/v201/ctrlr_component_variables.cpp +++ b/lib/ocpp/v201/ctrlr_component_variables.cpp @@ -198,6 +198,13 @@ const ComponentVariable& RoundClockAlignedTimestamps = { "RoundClockAlignedTimestamps", }), }; +const ComponentVariable& NetworkConfigTimeout = { + ControllerComponents::InternalCtrlr, + std::nullopt, + std::optional({ + "NetworkConfigTimeout", + }), +}; const ComponentVariable& SupportedChargingProfilePurposeTypes = { ControllerComponents::InternalCtrlr, std::nullopt, diff --git a/tests/lib/ocpp/v201/test_charge_point.cpp b/tests/lib/ocpp/v201/test_charge_point.cpp index 1ce4c4851..451a2096f 100644 --- a/tests/lib/ocpp/v201/test_charge_point.cpp +++ b/tests/lib/ocpp/v201/test_charge_point.cpp @@ -342,8 +342,7 @@ TEST_F(ChargePointCommonTestFixtureV201, callbacks.configure_network_connection_profile_callback = nullptr; EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); - testing::MockFunction - configure_network_connection_profile_callback_mock; + testing::MockFunction configure_network_connection_profile_callback_mock; callbacks.configure_network_connection_profile_callback = configure_network_connection_profile_callback_mock.AsStdFunction(); EXPECT_TRUE(callbacks.all_callbacks_valid(device_model));