From b1f947e027b5b054d35a4676bd036286c747abaa Mon Sep 17 00:00:00 2001 From: WilcodenBesten Date: Mon, 4 Nov 2024 13:30:30 +0100 Subject: [PATCH 1/7] Allow empty network connection priorities. Replace throw with a clear warning message. (#855) * Allow empty network connection priorities. Replace throw with a clear warning message Signed-off-by: Wilco den Besten * Improve warning message Signed-off-by: Wilco den Besten --------- Signed-off-by: Wilco den Besten --- include/ocpp/v201/connectivity_manager.hpp | 3 ++- lib/ocpp/v201/connectivity_manager.cpp | 15 ++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/include/ocpp/v201/connectivity_manager.hpp b/include/ocpp/v201/connectivity_manager.hpp index e72360209..03c45bc6f 100644 --- a/include/ocpp/v201/connectivity_manager.hpp +++ b/include/ocpp/v201/connectivity_manager.hpp @@ -202,7 +202,8 @@ class ConnectivityManager { void next_network_configuration_priority(); /// @brief Cache all the network connection profiles. Must be called once during initialization - void cache_network_connection_profiles(); + /// \return True if the network connection profiles could be cached, else False. + bool cache_network_connection_profiles(); }; } // namespace v201 diff --git a/lib/ocpp/v201/connectivity_manager.cpp b/lib/ocpp/v201/connectivity_manager.cpp index 8dd4a1b5c..336c7c6a0 100644 --- a/lib/ocpp/v201/connectivity_manager.cpp +++ b/lib/ocpp/v201/connectivity_manager.cpp @@ -199,9 +199,12 @@ void ConnectivityManager::init_websocket() { } // cache the network profiles on initialization - cache_network_connection_profiles(); + if (!cache_network_connection_profiles()) { + EVLOG_warning << "No network connection profiles configured, aborting websocket connection."; + return; + } - const int config_slot_int = this->network_connection_priorities.at(this->network_configuration_priority); + const int config_slot_int = this->get_active_network_configuration_slot(); 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 @@ -424,11 +427,11 @@ void ConnectivityManager::next_network_configuration_priority() { (this->network_configuration_priority + 1) % (this->network_connection_priorities.size()); } -void ConnectivityManager::cache_network_connection_profiles() { +bool ConnectivityManager::cache_network_connection_profiles() { if (!this->network_connection_profiles.empty()) { EVLOG_debug << " Network connection profiles already cached"; - return; + return true; } // get all the network connection profiles from the device model and cache them @@ -442,9 +445,7 @@ void ConnectivityManager::cache_network_connection_profiles() { this->network_connection_priorities.push_back(num); } - if (this->network_connection_priorities.empty()) { - EVLOG_AND_THROW(std::runtime_error("NetworkConfigurationPriority must not be empty")); - } + return !this->network_connection_priorities.empty(); } } // namespace v201 } // namespace ocpp From d41bfc173d88ce98f83af4f825c40a0347eb6be1 Mon Sep 17 00:00:00 2001 From: James Chapman <147724513+james-ctc@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:57:44 +0000 Subject: [PATCH 2/7] OCPP 1.6 add schema validation checks on updates to custom keys (#853) * fix: add schema validation checks on updates to custom keys There was a missing check on receipt of ChangeConfiguration.req for custom keys where a value could be set that would fail schema validation and prevent EVerest from successfully restarting. This update checks the new value before updating. Validation failures are noitfied to the CSMS via the ChangeConfiguration.conf Signed-off-by: James Chapman * fix: added license header Signed-off-by: James Chapman --------- Signed-off-by: James Chapman --- include/ocpp/common/schemas.hpp | 4 + lib/ocpp/common/schemas.cpp | 12 ++ lib/ocpp/v16/charge_point_configuration.cpp | 20 ++- tests/lib/ocpp/v16/CMakeLists.txt | 3 +- tests/lib/ocpp/v16/test_config_validation.cpp | 126 ++++++++++++++++++ 5 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 tests/lib/ocpp/v16/test_config_validation.cpp diff --git a/include/ocpp/common/schemas.hpp b/include/ocpp/common/schemas.hpp index becbf7dac..c332ab5fc 100644 --- a/include/ocpp/common/schemas.hpp +++ b/include/ocpp/common/schemas.hpp @@ -38,6 +38,10 @@ class Schemas { public: /// \brief Creates a new Schemas object looking for the root schema file in relation to the provided \p main_dir explicit Schemas(fs::path schemas_path); + /// \brief Creates a new Schemas object using the supplied JSON schema + explicit Schemas(const json& schema_in); + /// \brief Creates a new Schemas object using the supplied JSON schema + explicit Schemas(json&& schema_in); /// \brief Provides the config schema /// \returns the config schema as as json object diff --git a/lib/ocpp/common/schemas.cpp b/lib/ocpp/common/schemas.cpp index 95b62f96b..5df2df417 100644 --- a/lib/ocpp/common/schemas.cpp +++ b/lib/ocpp/common/schemas.cpp @@ -21,6 +21,18 @@ Schemas::Schemas(fs::path schemas_path) : schemas_path(schemas_path) { } } +Schemas::Schemas(const json& schema_in) : schema(schema_in) { + validator = std::make_shared( + [this](const json_uri& uri, json& schema) { this->loader(uri, schema); }, Schemas::format_checker); + validator->set_root_schema(this->schema); +} + +Schemas::Schemas(json&& schema_in) : schema(std::move(schema_in)) { + validator = std::make_shared( + [this](const json_uri& uri, json& schema) { this->loader(uri, schema); }, Schemas::format_checker); + validator->set_root_schema(this->schema); +} + void Schemas::load_root_schema() { fs::path config_schema_path = this->schemas_path / "Config.json"; diff --git a/lib/ocpp/v16/charge_point_configuration.cpp b/lib/ocpp/v16/charge_point_configuration.cpp index 7de14c0cd..fd1b14542 100644 --- a/lib/ocpp/v16/charge_point_configuration.cpp +++ b/lib/ocpp/v16/charge_point_configuration.cpp @@ -2858,20 +2858,28 @@ ConfigurationStatus ChargePointConfiguration::setCustomKey(CiString<50> key, CiS } std::lock_guard lock(this->configuration_mutex); try { - const auto type = this->custom_schema["properties"][key]["type"]; + const auto type = custom_schema["properties"][key]["type"]; + json new_value; if (type == "integer") { - this->config["Custom"][key] = std::stoi(value.get()); + new_value = std::stoi(value.get()); } else if (type == "number") { - this->config["Custom"][key] = std::stof(value.get()); + new_value = std::stof(value.get()); } else if (type == "string" or type == "array") { - this->config["Custom"][key] = value.get(); + new_value = value.get(); } else if (type == "boolean") { - this->config["Custom"][key] = ocpp::conversions::string_to_bool(value.get()); + new_value = ocpp::conversions::string_to_bool(value.get()); } else { return ConfigurationStatus::Rejected; } + + // validate the updated key against the schema + Schemas schema(custom_schema); + json model; + model[key] = new_value; + schema.get_validator()->validate(model); // throws exception on error + config["Custom"][key] = new_value; } catch (const std::exception& e) { - EVLOG_warning << "Could not set custom configuration key"; + EVLOG_warning << "Could not set custom configuration key: " << e.what(); return ConfigurationStatus::Rejected; } diff --git a/tests/lib/ocpp/v16/CMakeLists.txt b/tests/lib/ocpp/v16/CMakeLists.txt index ec7864a10..8ac06fd0c 100644 --- a/tests/lib/ocpp/v16/CMakeLists.txt +++ b/tests/lib/ocpp/v16/CMakeLists.txt @@ -1,4 +1,4 @@ -target_include_directories(libocpp_unit_tests PUBLIC +target_include_directories(libocpp_unit_tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) @@ -13,6 +13,7 @@ target_sources(libocpp_unit_tests PRIVATE test_message_queue.cpp test_charge_point_state_machine.cpp test_composite_schedule.cpp + test_config_validation.cpp ) # Copy the json files used for testing to the destination directory diff --git a/tests/lib/ocpp/v16/test_config_validation.cpp b/tests/lib/ocpp/v16/test_config_validation.cpp new file mode 100644 index 000000000..f661a0a7b --- /dev/null +++ b/tests/lib/ocpp/v16/test_config_validation.cpp @@ -0,0 +1,126 @@ + +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest + +#include + +#include +#include + +#include +#include +#include + +namespace { +using nlohmann::basic_json; +using nlohmann::json; +using nlohmann::json_uri; +using nlohmann::json_schema::basic_error_handler; +using nlohmann::json_schema::json_validator; + +constexpr const char* test_schema = R"({ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Json schema for Custom configuration keys", + "$comment": "This is just an example schema and can be modified according to custom requirements", + "type": "object", + "required": [], + "properties": { + "ConnectorType": { + "type": "string", + "enum": [ + "cType2", + "sType2" + ], + "default": "sType2", + "description": "Used to indicate the type of connector used by the unit", + "readOnly": true + }, + "ConfigLastUpdatedBy": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "LOCAL", + "CPMS" + ] + }, + "description": "Variable used to indicate how the Charge Points configuration was last updated", + "readOnly": true + } + } +})"; + +class SchemaTest : public testing::Test { + static void format_checker(const std::string& format, const std::string& value) { + EVLOG_error << "format_checker: '" << format << "' '" << value << '\''; + } + + static void loader(const json_uri& uri, json& schema) { + schema = nlohmann::json_schema::draft7_schema_builtin; + } + + class custom_error_handler : public basic_error_handler { + private: + void error(const json::json_pointer& pointer, const json& instance, const std::string& message) override { + basic_error_handler::error(pointer, instance, message); + EVLOG_error << "'" << pointer << "' - '" << instance << "': " << message; + errors = true; + } + + public: + bool errors{false}; + constexpr bool has_errors() const { + return errors; + } + }; + +protected: + std::unique_ptr validator; + json schema; + custom_error_handler err; + + void SetUp() override { + schema = json::parse(test_schema); + validator = std::make_unique(&loader, &format_checker); + validator->set_root_schema(schema); + err.errors = false; + } +}; + +TEST_F(SchemaTest, ValidationText) { + json model = R"({"ConnectorType":"cType2"})"_json; + validator->validate(model, err); + EXPECT_FALSE(err.has_errors()); +} + +TEST_F(SchemaTest, ValidationObj) { + json model; + model["ConnectorType"] = "cType2"; + validator->validate(model, err); + EXPECT_FALSE(err.has_errors()); +} + +TEST_F(SchemaTest, ValidationObjErr) { + json model; + model["ConnectorType"] = "cType3"; + validator->validate(model, err); + EXPECT_TRUE(err.has_errors()); +} + +TEST(SchemaObj, Success) { + ocpp::Schemas schema(std::move(json::parse(test_schema))); + auto validator = schema.get_validator(); + json model; + model["ConnectorType"] = "cType2"; + EXPECT_NO_THROW(validator->validate(model)); +} + +TEST(SchemaObj, Fail) { + ocpp::Schemas schema(std::move(json::parse(test_schema))); + auto validator = schema.get_validator(); + json model; + model["ConnectorType"] = "cType3"; + EXPECT_ANY_THROW(validator->validate(model)); +} + +} // namespace From c7755aafe4d20007b1485bd74266d4c1534669a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piet=20G=C3=B6mpel?= <37657534+Pietfried@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:28:40 +0100 Subject: [PATCH 3/7] Feature for SmartCharging Offline Behavior (#783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added configuration variable to 1.6 and 2.0.1 to allow ignoring profile in case the charging station is offline when the composite schedules are calculated by requests of a libocpp consumer application * Made IgnoredProfilePurposesOffline ReadWrite * In OCPP1.6 calling signal_set_charging_profiles_callback also in case of websocket connect or disconnect in case IgnoredProfilePurposesOffline are empty, since this can influence the composite schedules --------- Signed-off-by: pietfried Signed-off-by: Piet Gömpel --- config/v16/profile_schemas/Internal.json | 5 ++ .../standardized/SmartChargingCtrlr.json | 17 ++++ .../ocpp/v16/charge_point_configuration.hpp | 3 + include/ocpp/v16/smart_charging.hpp | 9 ++- include/ocpp/v201/charge_point.hpp | 4 +- .../ocpp/v201/ctrlr_component_variables.hpp | 1 + include/ocpp/v201/smart_charging.hpp | 13 +++- include/ocpp/v201/utils.hpp | 5 ++ lib/ocpp/v16/charge_point_configuration.cpp | 77 ++++++++++++++++++- lib/ocpp/v16/charge_point_impl.cpp | 29 ++++++- lib/ocpp/v16/smart_charging.cpp | 49 +++++++----- lib/ocpp/v201/charge_point.cpp | 20 ++++- lib/ocpp/v201/ctrlr_component_variables.cpp | 7 ++ lib/ocpp/v201/smart_charging.cpp | 16 ++-- lib/ocpp/v201/utils.cpp | 18 +++++ .../mocks/smart_charging_handler_mock.hpp | 3 +- tests/lib/ocpp/v201/test_charge_point.cpp | 7 +- 17 files changed, 238 insertions(+), 45 deletions(-) diff --git a/config/v16/profile_schemas/Internal.json b/config/v16/profile_schemas/Internal.json index 276aec336..f9e5aed42 100644 --- a/config/v16/profile_schemas/Internal.json +++ b/config/v16/profile_schemas/Internal.json @@ -197,6 +197,11 @@ "TxProfile" ] }, + "IgnoredProfilePurposesOffline": { + "$comment": "Allows configuration of comma seperated list of ChargingProfilePurposes that are ignored in the composite schedule caluclation when offline.", + "type": "string", + "readOnly": false + }, "MaxCompositeScheduleDuration": { "$comment": "Maximum duration in seconds of GetCompositeSchedule.req. For GetCompositeSchedule.req with a greater duration the schedule for only the MaxCompositeScheduleDuration will be calculated", "type": "integer", diff --git a/config/v201/component_config/standardized/SmartChargingCtrlr.json b/config/v201/component_config/standardized/SmartChargingCtrlr.json index ebd7d5bdc..bb268618d 100644 --- a/config/v201/component_config/standardized/SmartChargingCtrlr.json +++ b/config/v201/component_config/standardized/SmartChargingCtrlr.json @@ -253,6 +253,23 @@ "description": "Supply voltage of the grid. This value is only used in case a conversion between smart charging amp and watt limits is required" } }, + "IgnoredProfilePurposesOffline": { + "variable_name": "IgnoredProfilePurposesOffline", + "characteristics": { + "valuesList": "ChargingStationMaxProfile,TxDefaultProfile,TxProfile", + "supportsMonitoring": true, + "dataType": "MemberList" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadWrite", + "value": "" + } + ], + "description": "Allows configuration of comma seperated list of ChargingProfilePurposes that are ignored in the composite schedule caluclation when offline.", + "type": "string" + }, "required": [ "ChargingProfileMaxStackLevel", "ChargingScheduleChargingRateUnit", diff --git a/include/ocpp/v16/charge_point_configuration.hpp b/include/ocpp/v16/charge_point_configuration.hpp index 7e9171a3b..948c2d08f 100644 --- a/include/ocpp/v16/charge_point_configuration.hpp +++ b/include/ocpp/v16/charge_point_configuration.hpp @@ -84,6 +84,9 @@ class ChargePointConfiguration { KeyValue getLogRotationMaximumFileCountKeyValue(); std::vector getSupportedChargingProfilePurposeTypes(); KeyValue getSupportedChargingProfilePurposeTypesKeyValue(); + std::vector getIgnoredProfilePurposesOffline(); + std::optional getIgnoredProfilePurposesOfflineKeyValue(); + bool setIgnoredProfilePurposesOffline(const std::string& ignored_profile_purposes_offline); int32_t getMaxCompositeScheduleDuration(); KeyValue getMaxCompositeScheduleDurationKeyValue(); std::optional getCompositeScheduleDefaultLimitAmps(); diff --git a/include/ocpp/v16/smart_charging.hpp b/include/ocpp/v16/smart_charging.hpp index 251999df6..44a5fda31 100644 --- a/include/ocpp/v16/smart_charging.hpp +++ b/include/ocpp/v16/smart_charging.hpp @@ -98,11 +98,12 @@ class SmartChargingHandler { void clear_all_profiles(); /// - /// \brief Gets all valid profiles within the given absoulte \p start_time and absolute \p end_time for the given \p - /// connector_id + /// \brief Gets all valid profiles within the given absoulte \p start_time and absolute \p end_time for the given + /// \p connector_id . Only profiles that are not contained in \p purposes_to_ignore are included in the response. /// - std::vector get_valid_profiles(const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, - const int connector_id); + std::vector + get_valid_profiles(const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, const int connector_id, + const std::set& purposes_to_ignore = {}); /// /// \brief Calculates the enhanced composite schedule for the given \p valid_profiles and the given \p connector_id /// diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index e2804b0c6..2bd12cf01 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -471,7 +471,9 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void trigger_authorization_cache_cleanup(); void cache_cleanup_handler(); - GetCompositeScheduleResponse get_composite_schedule_internal(const GetCompositeScheduleRequest& request); + GetCompositeScheduleResponse + get_composite_schedule_internal(const GetCompositeScheduleRequest& request, + const std::set& profiles_to_ignore = {}); /// \brief Removes all network connection profiles below the actual security profile and stores the new list in the /// device model diff --git a/include/ocpp/v201/ctrlr_component_variables.hpp b/include/ocpp/v201/ctrlr_component_variables.hpp index 8099f5aa7..93af79ac1 100644 --- a/include/ocpp/v201/ctrlr_component_variables.hpp +++ b/include/ocpp/v201/ctrlr_component_variables.hpp @@ -216,6 +216,7 @@ extern const RequiredComponentVariable& SupplyVoltage; extern const ComponentVariable& Phases3to1; extern const RequiredComponentVariable& ChargingProfileMaxStackLevel; extern const RequiredComponentVariable& ChargingScheduleChargingRateUnit; +extern const ComponentVariable& IgnoredProfilePurposesOffline; extern const ComponentVariable& TariffCostCtrlrAvailableTariff; extern const ComponentVariable& TariffCostCtrlrAvailableCost; extern const RequiredComponentVariable& TariffCostCtrlrCurrency; diff --git a/include/ocpp/v201/smart_charging.hpp b/include/ocpp/v201/smart_charging.hpp index 4f2105057..d37eb0b2a 100644 --- a/include/ocpp/v201/smart_charging.hpp +++ b/include/ocpp/v201/smart_charging.hpp @@ -91,7 +91,8 @@ class SmartChargingHandlerInterface { virtual std::vector get_reported_profiles(const GetChargingProfilesRequest& request) const = 0; - virtual std::vector get_valid_profiles(int32_t evse_id) = 0; + virtual std::vector + get_valid_profiles(int32_t evse_id, const std::set& purposes_to_ignore = {}) = 0; virtual CompositeSchedule calculate_composite_schedule(std::vector& valid_profiles, const ocpp::DateTime& start_time, @@ -153,9 +154,12 @@ class SmartChargingHandler : public SmartChargingHandlerInterface { std::vector get_reported_profiles(const GetChargingProfilesRequest& request) const override; - /// \brief Retrieves all profiles that should be considered for calculating the composite schedule. + /// \brief Retrieves all profiles that should be considered for calculating the composite schedule. Only profiles + /// that belong to the given \p evse_id and that are not contained in \p purposes_to_ignore are included in the + /// response. /// - std::vector get_valid_profiles(int32_t evse_id) override; + std::vector + get_valid_profiles(int32_t evse_id, const std::set& purposes_to_ignore = {}) override; /// /// \brief Calculates the composite schedule for the given \p valid_profiles and the given \p connector_id @@ -215,7 +219,8 @@ class SmartChargingHandler : public SmartChargingHandlerInterface { private: std::vector get_evse_specific_tx_default_profiles() const; std::vector get_station_wide_tx_default_profiles() const; - std::vector get_valid_profiles_for_evse(int32_t evse_id); + std::vector + get_valid_profiles_for_evse(int32_t evse_id, const std::set& purposes_to_ignore = {}); void conform_schedule_number_phases(int32_t profile_id, ChargingSchedulePeriod& charging_schedule_period) const; void conform_validity_periods(ChargingProfile& profile) const; CurrentPhaseType get_current_phase_type(const std::optional evse_opt) const; diff --git a/include/ocpp/v201/utils.hpp b/include/ocpp/v201/utils.hpp index ef23aa24a..4a68a9cb5 100644 --- a/include/ocpp/v201/utils.hpp +++ b/include/ocpp/v201/utils.hpp @@ -3,6 +3,8 @@ #ifndef V201_UTILS_HPP #define V201_UTILS_HPP +#include + #include #include namespace ocpp { @@ -71,6 +73,9 @@ std::optional get_total_power_active_import(const MeterValue& meter_value /// \brief Determines if a given \p security_event is critical as defined in the OCPP 2.0.1 appendix bool is_critical(const std::string& security_event); +/// \brief Converts the given \p csl of ChargingProfilePurpose strings into a std::set +std::set get_purposes_to_ignore(const std::string& csl, const bool is_offline); + } // namespace utils } // namespace v201 } // namespace ocpp diff --git a/lib/ocpp/v16/charge_point_configuration.cpp b/lib/ocpp/v16/charge_point_configuration.cpp index fd1b14542..d2daba2bc 100644 --- a/lib/ocpp/v16/charge_point_configuration.cpp +++ b/lib/ocpp/v16/charge_point_configuration.cpp @@ -341,11 +341,52 @@ std::vector ChargePointConfiguration::getSupportedCh std::vector supported_purpose_types; const auto str_list = this->config["Internal"]["SupportedChargingProfilePurposeTypes"]; for (const auto& str : str_list) { - supported_purpose_types.push_back(conversions::string_to_charging_profile_purpose_type(str)); + try { + supported_purpose_types.push_back(conversions::string_to_charging_profile_purpose_type(str)); + } catch (const StringToEnumException& e) { + EVLOG_warning << "Could not convert element of SupportedChargingProfilePurposeTypes: " << str; + } } return supported_purpose_types; } +std::vector ChargePointConfiguration::getIgnoredProfilePurposesOffline() { + if (not this->config["Internal"].contains("IgnoredProfilePurposesOffline")) { + return {}; + } + + std::vector purpose_types; + const auto str_list = split_string(this->config["Internal"]["IgnoredProfilePurposesOffline"], ','); + for (const auto& str : str_list) { + try { + purpose_types.push_back(conversions::string_to_charging_profile_purpose_type(str)); + } catch (const StringToEnumException& e) { + EVLOG_warning << "Could not convert element of IgnoredProfilePurposesOffline: " << str; + } + } + return purpose_types; +} + +bool ChargePointConfiguration::setIgnoredProfilePurposesOffline(const std::string& ignored_profile_purposes_offline) { + if (this->getIgnoredProfilePurposesOfflineKeyValue() == std::nullopt) { + return false; + } + + const auto profile_purposes = split_string(ignored_profile_purposes_offline, ','); + for (const auto purpose : profile_purposes) { + try { + conversions::string_to_charging_profile_purpose_type(purpose); + } catch (const StringToEnumException& e) { + EVLOG_warning << "Could not convert element of IgnoredProfilePurposesOffline: " << purpose; + return false; + } + } + + this->config["Internal"]["IgnoredProfilePurposesOffline"] = ignored_profile_purposes_offline; + this->setInUserConfig("Internal", "IgnoredProfilePurposesOffline", ignored_profile_purposes_offline); + return true; +} + int32_t ChargePointConfiguration::getMaxCompositeScheduleDuration() { return this->config["Internal"]["MaxCompositeScheduleDuration"]; } @@ -664,7 +705,31 @@ KeyValue ChargePointConfiguration::getSupportedChargingProfilePurposeTypesKeyVal kv.readonly = true; std::vector purpose_types; for (const auto& entry : this->getSupportedChargingProfilePurposeTypes()) { - purpose_types.push_back(conversions::charging_profile_purpose_type_to_string(entry)); + try { + purpose_types.push_back(conversions::charging_profile_purpose_type_to_string(entry)); + } catch (const EnumToStringException& e) { + EVLOG_warning << "Could not convert element of SupportedChargingProfilePurposeTypes to string"; + } + } + kv.value.emplace(to_csl(purpose_types)); + return kv; +} + +std::optional ChargePointConfiguration::getIgnoredProfilePurposesOfflineKeyValue() { + if (not this->config["Internal"].contains("IgnoredProfilePurposesOffline")) { + return std::nullopt; + } + + KeyValue kv; + kv.key = "IgnoredProfilePurposesOffline"; + kv.readonly = false; + std::vector purpose_types; + for (const auto& entry : this->getIgnoredProfilePurposesOffline()) { + try { + purpose_types.push_back(conversions::charging_profile_purpose_type_to_string(entry)); + } catch (const EnumToStringException& e) { + EVLOG_warning << "Could not convert element of IgnoredProfilePurposesOffline to string"; + } } kv.value.emplace(to_csl(purpose_types)); return kv; @@ -2950,6 +3015,9 @@ std::optional ChargePointConfiguration::get(CiString<50> key) { if (key == "SupportedChargingProfilePurposeTypes") { return this->getSupportedChargingProfilePurposeTypesKeyValue(); } + if (key == "IgnoredProfilePurposesOffline") { + return this->getIgnoredProfilePurposesOfflineKeyValue(); + } if (key == "MaxCompositeScheduleDuration") { return this->getMaxCompositeScheduleDurationKeyValue(); } @@ -3271,6 +3339,11 @@ std::vector ChargePointConfiguration::get_all_key_value() { ConfigurationStatus ChargePointConfiguration::set(CiString<50> key, CiString<500> value) { std::lock_guard lock(this->configuration_mutex); + if (key == "IgnoredProfilePurposesOffline") { + if (this->setIgnoredProfilePurposesOffline(value) == false) { + return ConfigurationStatus::Rejected; + } + } if (key == "AllowOfflineTxForUnknownId") { if (this->getAllowOfflineTxForUnknownId() == std::nullopt) { return ConfigurationStatus::NotSupported; diff --git a/lib/ocpp/v16/charge_point_impl.cpp b/lib/ocpp/v16/charge_point_impl.cpp index 7bef0fac9..05ab20ab5 100644 --- a/lib/ocpp/v16/charge_point_impl.cpp +++ b/lib/ocpp/v16/charge_point_impl.cpp @@ -292,6 +292,13 @@ void ChargePointImpl::init_websocket() { } this->message_queue->resume(this->message_queue_resume_delay); this->connected_callback(); + + // signal_set_charging_profiles_callback since composite schedule could have changed if + // IgnoredProfilePurposesOffline are configured when becoming online + if (this->signal_set_charging_profiles_callback != nullptr and + not this->configuration->getIgnoredProfilePurposesOffline().empty()) { + this->signal_set_charging_profiles_callback(); + } }); this->websocket->register_disconnected_callback([this]() { if (this->connection_state_changed_callback != nullptr) { @@ -307,6 +314,12 @@ void ChargePointImpl::init_websocket() { if (this->v2g_certificate_timer != nullptr) { this->v2g_certificate_timer->stop(); } + // signal_set_charging_profiles_callback since composite schedule could have changed if + // IgnoredProfilePurposesOffline are configured when becoming offline + if (this->signal_set_charging_profiles_callback != nullptr and + not this->configuration->getIgnoredProfilePurposesOffline().empty()) { + this->signal_set_charging_profiles_callback(); + } }); this->websocket->register_closed_callback([this](const WebsocketCloseReason reason) { if (this->switch_security_profile_callback != nullptr) { @@ -3353,6 +3366,12 @@ std::map ChargePointImpl::get_all_composite_charging_ const ChargingRateUnit unit) { std::map charging_schedules; + std::set purposes_to_ignore; + + if (not this->websocket->is_connected()) { + const auto purposes_to_ignore_vec = this->configuration->getIgnoredProfilePurposesOffline(); + purposes_to_ignore.insert(purposes_to_ignore_vec.begin(), purposes_to_ignore_vec.end()); + } for (int connector_id = 0; connector_id <= this->configuration->getNumberOfConnectors(); connector_id++) { const auto start_time = ocpp::DateTime(); @@ -3360,7 +3379,7 @@ std::map ChargePointImpl::get_all_composite_charging_ const auto end_time = ocpp::DateTime(start_time.to_time_point() + duration); const auto valid_profiles = - this->smart_charging_handler->get_valid_profiles(start_time, end_time, connector_id); + this->smart_charging_handler->get_valid_profiles(start_time, end_time, connector_id, purposes_to_ignore); const auto composite_schedule = this->smart_charging_handler->calculate_composite_schedule( valid_profiles, start_time, end_time, connector_id, unit); charging_schedules[connector_id] = composite_schedule; @@ -3373,6 +3392,12 @@ std::map ChargePointImpl::get_all_enhanced_composite_charging_schedules(const int32_t duration_s, const ChargingRateUnit unit) { std::map charging_schedules; + std::set purposes_to_ignore; + + if (this->websocket == nullptr or not this->websocket->is_connected()) { + const auto purposes_to_ignore_vec = this->configuration->getIgnoredProfilePurposesOffline(); + purposes_to_ignore.insert(purposes_to_ignore_vec.begin(), purposes_to_ignore_vec.end()); + } for (int connector_id = 0; connector_id <= this->configuration->getNumberOfConnectors(); connector_id++) { const auto start_time = ocpp::DateTime(); @@ -3380,7 +3405,7 @@ ChargePointImpl::get_all_enhanced_composite_charging_schedules(const int32_t dur const auto end_time = ocpp::DateTime(start_time.to_time_point() + duration); const auto valid_profiles = - this->smart_charging_handler->get_valid_profiles(start_time, end_time, connector_id); + this->smart_charging_handler->get_valid_profiles(start_time, end_time, connector_id, purposes_to_ignore); const auto composite_schedule = this->smart_charging_handler->calculate_enhanced_composite_schedule( valid_profiles, start_time, end_time, connector_id, unit); charging_schedules[connector_id] = composite_schedule; diff --git a/lib/ocpp/v16/smart_charging.cpp b/lib/ocpp/v16/smart_charging.cpp index aaff886ae..918b7f948 100644 --- a/lib/ocpp/v16/smart_charging.cpp +++ b/lib/ocpp/v16/smart_charging.cpp @@ -386,16 +386,20 @@ void SmartChargingHandler::clear_all_profiles() { } } -std::vector SmartChargingHandler::get_valid_profiles(const ocpp::DateTime& start_time, - const ocpp::DateTime& end_time, - const int connector_id) { +std::vector +SmartChargingHandler::get_valid_profiles(const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, + const int connector_id, + const std::set& purposes_to_ignore) { std::vector valid_profiles; { std::lock_guard lk(charge_point_max_profiles_map_mutex); - for (const auto& [stack_level, profile] : stack_level_charge_point_max_profiles_map) { - valid_profiles.push_back(profile); + if (std::find(std::begin(purposes_to_ignore), std::end(purposes_to_ignore), + ChargingProfilePurposeType::ChargePointMaxProfile) == std::end(purposes_to_ignore)) { + for (const auto& [stack_level, profile] : stack_level_charge_point_max_profiles_map) { + valid_profiles.push_back(profile); + } } } @@ -413,26 +417,33 @@ std::vector SmartChargingHandler::get_valid_profiles(const ocpp std::lock_guard lk_txd(tx_default_profiles_map_mutex); std::lock_guard lk_tx(tx_profiles_map_mutex); - for (const auto& [stack_level, profile] : itt->second->stack_level_tx_profiles_map) { - // only include profiles that match the transactionId (when there is one) - bool b_add{false}; - if (profile.transactionId) { - if ((transactionId) && (transactionId.value() == profile.transactionId.value())) { - // there is a session/transaction in progress and the ID matches the profile + if (std::find(std::begin(purposes_to_ignore), std::end(purposes_to_ignore), + ChargingProfilePurposeType::TxProfile) == std::end(purposes_to_ignore)) { + for (const auto& [stack_level, profile] : itt->second->stack_level_tx_profiles_map) { + // only include profiles that match the transactionId (when there is one) + bool b_add{false}; + + if (profile.transactionId) { + if ((transactionId) && (transactionId.value() == profile.transactionId.value())) { + // there is a session/transaction in progress and the ID matches the profile + b_add = true; + } + } else { + // profile doesn't have a transaction ID specified b_add = true; } - } else { - // profile doesn't have a transaction ID specified - b_add = true; - } - if (b_add) { - valid_profiles.push_back(profile); + if (b_add) { + valid_profiles.push_back(profile); + } } } - for (const auto& [stack_level, profile] : itt->second->stack_level_tx_default_profiles_map) { - valid_profiles.push_back(profile); + if (std::find(std::begin(purposes_to_ignore), std::end(purposes_to_ignore), + ChargingProfilePurposeType::TxDefaultProfile) == std::end(purposes_to_ignore)) { + for (const auto& [stack_level, profile] : itt->second->stack_level_tx_default_profiles_map) { + valid_profiles.push_back(profile); + } } } } diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index 8fd80edf4..8fc4f5387 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -4352,7 +4352,9 @@ void ChargePoint::cache_cleanup_handler() { } } -GetCompositeScheduleResponse ChargePoint::get_composite_schedule_internal(const GetCompositeScheduleRequest& request) { +GetCompositeScheduleResponse +ChargePoint::get_composite_schedule_internal(const GetCompositeScheduleRequest& request, + const std::set& profiles_to_ignore) { GetCompositeScheduleResponse response; response.status = GenericStatusEnum::Rejected; @@ -4366,7 +4368,8 @@ GetCompositeScheduleResponse ChargePoint::get_composite_schedule_internal(const auto start_time = ocpp::DateTime(); auto end_time = ocpp::DateTime(start_time.to_time_point() + std::chrono::seconds(request.duration)); - std::vector valid_profiles = this->smart_charging_handler->get_valid_profiles(request.evseId); + std::vector valid_profiles = + this->smart_charging_handler->get_valid_profiles(request.evseId, profiles_to_ignore); auto schedule = this->smart_charging_handler->calculate_composite_schedule( valid_profiles, start_time, end_time, request.evseId, request.chargingRateUnit); @@ -4520,13 +4523,22 @@ ChargePoint::set_variables(const std::vector& set_variable_data } GetCompositeScheduleResponse ChargePoint::get_composite_schedule(const GetCompositeScheduleRequest& request) { - return this->get_composite_schedule_internal(request); + std::set purposes_to_ignore = utils::get_purposes_to_ignore( + this->device_model->get_optional_value(ControllerComponentVariables::IgnoredProfilePurposesOffline) + .value_or(""), + this->is_offline()); + return this->get_composite_schedule_internal(request, purposes_to_ignore); } std::vector ChargePoint::get_all_composite_schedules(const int32_t duration_s, const ChargingRateUnitEnum& unit) { std::vector composite_schedules; + std::set purposes_to_ignore = utils::get_purposes_to_ignore( + this->device_model->get_optional_value(ControllerComponentVariables::IgnoredProfilePurposesOffline) + .value_or(""), + this->is_offline()); + const auto number_of_evses = this->evse_manager->get_number_of_evses(); // get all composite schedules including the one for evse_id == 0 for (int32_t evse_id = 0; evse_id <= number_of_evses; evse_id++) { @@ -4534,7 +4546,7 @@ std::vector ChargePoint::get_all_composite_schedules(const in request.duration = duration_s; request.evseId = evse_id; request.chargingRateUnit = unit; - auto composite_schedule_response = this->get_composite_schedule_internal(request); + auto composite_schedule_response = this->get_composite_schedule_internal(request, purposes_to_ignore); if (composite_schedule_response.status == GenericStatusEnum::Accepted and composite_schedule_response.schedule.has_value()) { composite_schedules.push_back(composite_schedule_response.schedule.value()); diff --git a/lib/ocpp/v201/ctrlr_component_variables.cpp b/lib/ocpp/v201/ctrlr_component_variables.cpp index a4fcbc308..2b564b18f 100644 --- a/lib/ocpp/v201/ctrlr_component_variables.cpp +++ b/lib/ocpp/v201/ctrlr_component_variables.cpp @@ -1211,6 +1211,13 @@ const RequiredComponentVariable& ChargingScheduleChargingRateUnit = { "RateUnit", }), }; +const ComponentVariable& IgnoredProfilePurposesOffline = { + ControllerComponents::SmartChargingCtrlr, + std::nullopt, + std::optional({ + "IgnoredProfilePurposesOffline", + }), +}; const ComponentVariable& TariffCostCtrlrAvailableTariff = { ControllerComponents::TariffCostCtrlr, std::nullopt, diff --git a/lib/ocpp/v201/smart_charging.cpp b/lib/ocpp/v201/smart_charging.cpp index 25d470808..29201b71f 100644 --- a/lib/ocpp/v201/smart_charging.cpp +++ b/lib/ocpp/v201/smart_charging.cpp @@ -459,12 +459,16 @@ SmartChargingHandler::get_reported_profiles(const GetChargingProfilesRequest& re return this->database_handler->get_charging_profiles_matching_criteria(request.evseId, request.chargingProfile); } -std::vector SmartChargingHandler::get_valid_profiles_for_evse(int32_t evse_id) { +std::vector +SmartChargingHandler::get_valid_profiles_for_evse(int32_t evse_id, + const std::set& purposes_to_ignore) { std::vector valid_profiles; auto evse_profiles = this->database_handler->get_charging_profiles_for_evse(evse_id); for (auto profile : evse_profiles) { - if (this->conform_and_validate_profile(profile, evse_id) == ProfileValidationResultEnum::Valid) { + if (this->conform_and_validate_profile(profile, evse_id) == ProfileValidationResultEnum::Valid and + std::find(std::begin(purposes_to_ignore), std::end(purposes_to_ignore), profile.chargingProfilePurpose) == + std::end(purposes_to_ignore)) { valid_profiles.push_back(profile); } } @@ -472,11 +476,13 @@ std::vector SmartChargingHandler::get_valid_profiles_for_evse(i return valid_profiles; } -std::vector SmartChargingHandler::get_valid_profiles(int32_t evse_id) { - std::vector valid_profiles = get_valid_profiles_for_evse(evse_id); +std::vector +SmartChargingHandler::get_valid_profiles(int32_t evse_id, + const std::set& purposes_to_ignore) { + std::vector valid_profiles = get_valid_profiles_for_evse(evse_id, purposes_to_ignore); if (evse_id != STATION_WIDE_ID) { - auto station_wide_profiles = get_valid_profiles_for_evse(STATION_WIDE_ID); + auto station_wide_profiles = get_valid_profiles_for_evse(STATION_WIDE_ID, purposes_to_ignore); valid_profiles.insert(valid_profiles.end(), station_wide_profiles.begin(), station_wide_profiles.end()); } diff --git a/lib/ocpp/v201/utils.cpp b/lib/ocpp/v201/utils.cpp index 914cb1b6a..426d2563f 100644 --- a/lib/ocpp/v201/utils.cpp +++ b/lib/ocpp/v201/utils.cpp @@ -192,6 +192,24 @@ bool is_critical(const std::string& security_event) { return false; } +std::set get_purposes_to_ignore(const std::string& csl, const bool is_offline) { + if (not is_offline or csl.empty()) { + return {}; + } + + std::set purposes_to_ignore; + const auto purposes_to_ignore_vec = split_string(csl, ','); + + for (const auto purpose : purposes_to_ignore_vec) { + try { + purposes_to_ignore.insert(conversions::string_to_charging_profile_purpose_enum(purpose)); + } catch (std::out_of_range& e) { + EVLOG_warning << "Error while converting charging profile purpose to ignore: " << purpose; + } + } + return purposes_to_ignore; +} + } // namespace utils } // namespace v201 } // namespace ocpp diff --git a/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp b/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp index 4b9ea9846..482b53c9a 100644 --- a/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp +++ b/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp @@ -23,7 +23,8 @@ class SmartChargingHandlerMock : public SmartChargingHandlerInterface { MOCK_METHOD(ClearChargingProfileResponse, clear_profiles, (const ClearChargingProfileRequest& request), (override)); MOCK_METHOD(std::vector, get_reported_profiles, (const GetChargingProfilesRequest& request), (const, override)); - MOCK_METHOD(std::vector, get_valid_profiles, (int32_t evse_id)); + MOCK_METHOD(std::vector, get_valid_profiles, + (int32_t evse_id, const std::set&)); MOCK_METHOD(CompositeSchedule, calculate_composite_schedule, (std::vector & valid_profiles, const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, const int32_t evse_id, diff --git a/tests/lib/ocpp/v201/test_charge_point.cpp b/tests/lib/ocpp/v201/test_charge_point.cpp index 0fa45be8c..c96fd7673 100644 --- a/tests/lib/ocpp/v201/test_charge_point.cpp +++ b/tests/lib/ocpp/v201/test_charge_point.cpp @@ -851,7 +851,8 @@ TEST_F(ChargePointFunctionalityTestFixtureV201, DEFAULT_TX_ID), }; - ON_CALL(*smart_charging_handler, get_valid_profiles(DEFAULT_EVSE_ID)).WillByDefault(testing::Return(profiles)); + ON_CALL(*smart_charging_handler, get_valid_profiles(DEFAULT_EVSE_ID, testing::_)) + .WillByDefault(testing::Return(profiles)); EXPECT_CALL(*smart_charging_handler, calculate_composite_schedule(profiles, testing::_, testing::_, DEFAULT_EVSE_ID, req.chargingRateUnit)); @@ -867,7 +868,7 @@ TEST_F(ChargePointFunctionalityTestFixtureV201, auto get_composite_schedule_req = request_to_enhanced_message(req); - EXPECT_CALL(*smart_charging_handler, get_valid_profiles(testing::_)).Times(0); + EXPECT_CALL(*smart_charging_handler, get_valid_profiles(testing::_, testing::_)).Times(0); EXPECT_CALL(*smart_charging_handler, calculate_composite_schedule(testing::_, testing::_, testing::_, testing::_, testing::_)) .Times(0); @@ -888,7 +889,7 @@ TEST_F(ChargePointFunctionalityTestFixtureV201, device_model->set_value(charging_rate_unit_cv.component, charging_rate_unit_cv.variable.value(), AttributeEnum::Actual, "A", "test", true); - EXPECT_CALL(*smart_charging_handler, get_valid_profiles(testing::_)).Times(0); + EXPECT_CALL(*smart_charging_handler, get_valid_profiles(testing::_, testing::_)).Times(0); EXPECT_CALL(*smart_charging_handler, calculate_composite_schedule(testing::_, testing::_, testing::_, testing::_, testing::_)) .Times(0); From 034b2f034ad59ccf62b42ae0d2624106f2ea5cd4 Mon Sep 17 00:00:00 2001 From: Kai Hermann Date: Thu, 7 Nov 2024 11:41:35 +0100 Subject: [PATCH 4/7] Bump version to 0.19.0 (#860) Signed-off-by: Kai-Uwe Hermann --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e2471129..9f8181071 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) project(ocpp - VERSION 0.18.0 + VERSION 0.19.0 DESCRIPTION "A C++ implementation of the Open Charge Point Protocol" LANGUAGES CXX ) From 4b0ab7228527f18fc39f300e2d277ad7b63da27b Mon Sep 17 00:00:00 2001 From: WilcodenBesten Date: Thu, 7 Nov 2024 17:19:47 +0100 Subject: [PATCH 5/7] Improve network related logging (#859) Signed-off-by: Wilco den Besten --- lib/ocpp/common/websocket/websocket_libwebsockets.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/ocpp/common/websocket/websocket_libwebsockets.cpp b/lib/ocpp/common/websocket/websocket_libwebsockets.cpp index 33f32a4c1..06293d65e 100644 --- a/lib/ocpp/common/websocket/websocket_libwebsockets.cpp +++ b/lib/ocpp/common/websocket/websocket_libwebsockets.cpp @@ -293,7 +293,7 @@ static int callback_minimal(struct lws* wsi, enum lws_callback_reasons reason, v if (owner not_eq nullptr) { return data->get_owner()->process_callback(wsi, static_cast(reason), user, in, len); } else { - EVLOG_warning << "callback_minimal called, but data->owner is nullptr. Reason: " << reason; + EVLOG_debug << "callback_minimal called, but data->owner is nullptr. Reason: " << reason; } } } @@ -838,7 +838,9 @@ void WebsocketLibwebsockets::reconnect(long delay) { } void WebsocketLibwebsockets::close(const WebsocketCloseReason code, const std::string& reason) { - EVLOG_info << "Closing websocket: " << reason; + if (!reason.empty()) { + EVLOG_info << "Closing websocket: " << reason; + } { std::lock_guard lk(this->reconnect_mutex); From e29e52908b79b93c786ca54b5d7746ef315abd91 Mon Sep 17 00:00:00 2001 From: Kai Hermann Date: Thu, 7 Nov 2024 17:39:53 +0100 Subject: [PATCH 6/7] Bump libevse-security to 0.9.1 as used in everest-core already (#861) Signed-off-by: Kai-Uwe Hermann --- dependencies.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.yaml b/dependencies.yaml index ae64eedf0..93091d29b 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -27,7 +27,7 @@ date: options: ["BUILD_TZ_LIB ON", "HAS_REMOTE_API 0", "USE_AUTOLOAD 0", "USE_SYSTEM_TZ_DB ON"] libevse-security: git: https://github.com/EVerest/libevse-security.git - git_tag: v0.8.0 + git_tag: v0.9.1 libwebsockets: git: https://github.com/warmcat/libwebsockets.git git_tag: v4.3.3 From af3d08e4d4b63ccab5aa1446cafc476485551e39 Mon Sep 17 00:00:00 2001 From: Kai Hermann Date: Mon, 11 Nov 2024 11:36:06 +0100 Subject: [PATCH 7/7] Fix reporting of log rotation status (#862) Accidental use of comparison operator when an assignment was needed Signed-off-by: Kai-Uwe Hermann --- lib/ocpp/common/ocpp_logging.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ocpp/common/ocpp_logging.cpp b/lib/ocpp/common/ocpp_logging.cpp index e9e9e5ae6..86c5816ed 100644 --- a/lib/ocpp/common/ocpp_logging.cpp +++ b/lib/ocpp/common/ocpp_logging.cpp @@ -193,7 +193,7 @@ LogRotationStatus MessageLogging::rotate_log(const std::string& file_basename) { std::filesystem::rename(file, new_file_name); EVLOG_info << "Renaming: " << file.string() << " -> " << new_file_name.string(); if (status == LogRotationStatus::NotRotated) { - status == LogRotationStatus::Rotated; + status = LogRotationStatus::Rotated; } } else { // try parsing the .x extension and log an error if this was not possible @@ -210,7 +210,7 @@ LogRotationStatus MessageLogging::rotate_log(const std::string& file_basename) { EVLOG_info << "Renaming: " << file.string() << " -> " << new_file_name.string(); std::filesystem::rename(file, new_file_name); if (status == LogRotationStatus::NotRotated) { - status == LogRotationStatus::Rotated; + status = LogRotationStatus::Rotated; } }