diff --git a/config/v201/core_migrations/5_down-charging_profiles_db.sql b/config/v201/core_migrations/5_down-charging_profiles_db.sql new file mode 100644 index 000000000..4adf87ac0 --- /dev/null +++ b/config/v201/core_migrations/5_down-charging_profiles_db.sql @@ -0,0 +1 @@ +DROP TABLE CHARGING_PROFILES; diff --git a/config/v201/core_migrations/5_up-charging_profiles_db.sql b/config/v201/core_migrations/5_up-charging_profiles_db.sql new file mode 100644 index 000000000..3f5849288 --- /dev/null +++ b/config/v201/core_migrations/5_up-charging_profiles_db.sql @@ -0,0 +1,7 @@ +CREATE TABLE CHARGING_PROFILES ( + ID INT PRIMARY KEY NOT NULL, + EVSE_ID INT NOT NULL, + STACK_LEVEL INT NOT NULL, + CHARGING_PROFILE_PURPOSE TEXT NOT NULL, + PROFILE TEXT NOT NULL +); diff --git a/doc/ocpp_201_status.md b/doc/ocpp_201_status.md index f0c2d3257..ec4aad037 100644 --- a/doc/ocpp_201_status.md +++ b/doc/ocpp_201_status.md @@ -1238,29 +1238,29 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | K01.FR.06 | 🌐 | | | K01.FR.07 | ⛽️ | Notified through the `signal_set_charging_profiles` callback. | | K01.FR.08 | 🌐 | `TxDefaultProfile`s are supported. | -| K01.FR.09 | | | +| K01.FR.09 | ✅ | | | K01.FR.10 | ✅ | | | K01.FR.11 | | | | K01.FR.12 | | | | K01.FR.13 | | | | K01.FR.14 | ✅ | | | K01.FR.15 | ✅ | | -| K01.FR.16 | | | +| K01.FR.16 | ✅ | | | K01.FR.17 | | | | K01.FR.19 | | | | K01.FR.20 | ✅ | Suggests `ACPhaseSwitchingSupported` should be per EVSE, conflicting with the rest of the spec. | | K01.FR.21 | | | | K01.FR.22 | | | -| K01.FR.26 | | | -| K01.FR.27 | | | -| K01.FR.28 | | | +| K01.FR.26 | ✅ | | +| K01.FR.27 | ✅ | | +| K01.FR.28 | ✅ | | | K01.FR.29 | | | | K01.FR.30 | | | | K01.FR.31 | | | | K01.FR.32 | ✅ | | -| K01.FR.33 | | | +| K01.FR.33 | ✅ | | | K01.FR.34 | ✅ | | -| K01.FR.35 | | | +| K01.FR.35 | ✅ | | | K01.FR.36 | ✅ | | | K01.FR.37 | | | | K01.FR.38 | 🌐 💂 | `ChargingStationMaxProfile`s with `Relative` for `chargingProfileKind` are rejected. | diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index 030bb8bff..b564d00c9 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -52,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -520,8 +522,6 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa /// device model void remove_network_connection_profiles_below_actual_security_profile(); - void handle_message(const EnhancedMessage& message); - void message_callback(const std::string& message); void update_aligned_data_interval(); @@ -719,6 +719,9 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void handle_change_availability_req(Call call); void handle_heartbeat_response(CallResult call); + // Functional Block K: Smart Charging + void handle_set_charging_profile_req(Call call); + // Functional Block L: Firmware management void handle_firmware_update_req(Call call); @@ -774,6 +777,12 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa /// If \param persist is set to true, the change will be persisted across a reboot void execute_change_availability_request(ChangeAvailabilityRequest request, bool persist); +protected: + std::shared_ptr smart_charging_handler; + + void handle_message(const EnhancedMessage& message); + void load_charging_profiles(); + public: /// \brief Construct a new ChargePoint object /// \param evse_connector_structure Map that defines the structure of EVSE and connectors of the chargepoint. The diff --git a/include/ocpp/v201/database_handler.hpp b/include/ocpp/v201/database_handler.hpp index ae7117b78..94ddd3c91 100644 --- a/include/ocpp/v201/database_handler.hpp +++ b/include/ocpp/v201/database_handler.hpp @@ -168,6 +168,20 @@ class DatabaseHandler : public common::DatabaseHandlerCommon { /// \param transaction_id transaction id of the transaction to clear from. /// \return true if succeeded void transaction_delete(const std::string& transaction_id); + + /// charging profiles + + /// \brief Inserts or updates the given \p profile to CHARGING_PROFILES table + void insert_or_update_charging_profile(const int evse_id, const v201::ChargingProfile& profile); + + /// \brief Deletes the profile with the given \p profile_id + void delete_charging_profile(const int profile_id); + + /// \brief Deletes all profiles from table CHARGING_PROFILES + void clear_charging_profiles(); + + /// \brief Retrieves all ChargingProfiles + virtual std::map> get_all_charging_profiles_group_by_evse(); }; } // namespace v201 diff --git a/include/ocpp/v201/smart_charging.hpp b/include/ocpp/v201/smart_charging.hpp index 63967fb74..9b5763c0c 100644 --- a/include/ocpp/v201/smart_charging.hpp +++ b/include/ocpp/v201/smart_charging.hpp @@ -49,13 +49,28 @@ namespace conversions { /// \brief Converts the given ProfileValidationResultEnum \p e to human readable string /// \returns a string representation of the ProfileValidationResultEnum std::string profile_validation_result_to_string(ProfileValidationResultEnum e); + +/// \brief Converts the given ProfileValidationResultEnum \p e to a OCPP reasonCode. +/// \returns a reasonCode +std::string profile_validation_result_to_reason_code(ProfileValidationResultEnum e); } // namespace conversions std::ostream& operator<<(std::ostream& os, const ProfileValidationResultEnum validation_result); +class SmartChargingHandlerInterface { +public: + virtual ~SmartChargingHandlerInterface() = default; + + virtual SetChargingProfileResponse validate_and_add_profile(ChargingProfile& profile, int32_t evse_id) = 0; + + virtual ProfileValidationResultEnum validate_profile(ChargingProfile& profile, int32_t evse_id) = 0; + + virtual SetChargingProfileResponse add_profile(ChargingProfile& profile, int32_t evse_id) = 0; +}; + /// \brief This class handles and maintains incoming ChargingProfiles and contains the logic /// to calculate the composite schedules -class SmartChargingHandler { +class SmartChargingHandler : public SmartChargingHandlerInterface { private: EvseManagerInterface& evse_manager; std::shared_ptr& device_model; @@ -65,19 +80,26 @@ class SmartChargingHandler { std::map> charging_profiles; public: - SmartChargingHandler(EvseManagerInterface& evse_manager, std::shared_ptr& device_model); + SmartChargingHandler(EvseManagerInterface& evse_manager, std::shared_ptr& device_model, + std::shared_ptr database_handler); + + /// + /// \brief validates the given \p profile according to the specification, + /// adding it to our stored list of profiles if valid. + /// + SetChargingProfileResponse validate_and_add_profile(ChargingProfile& profile, int32_t evse_id) override; /// /// \brief validates the given \p profile according to the specification. /// If a profile does not have validFrom or validTo set, we conform the values /// to a representation that fits the spec. /// - ProfileValidationResultEnum validate_profile(ChargingProfile& profile, int32_t evse_id); + ProfileValidationResultEnum validate_profile(ChargingProfile& profile, int32_t evse_id) override; /// /// \brief Adds a given \p profile and associated \p evse_id to our stored list of profiles /// - SetChargingProfileResponse add_profile(int32_t evse_id, ChargingProfile& profile); + SetChargingProfileResponse add_profile(ChargingProfile& profile, int32_t evse_id) override; /// /// \brief Retrieves existing profiles on system. @@ -117,7 +139,7 @@ class SmartChargingHandler { /// \brief Checks a given \p profile and associated \p evse_id validFrom and validTo range /// This method assumes that the existing profile will have dates set for validFrom and validTo /// - bool is_overlapping_validity_period(int evse_id, const ChargingProfile& profile) const; + bool is_overlapping_validity_period(const ChargingProfile& profile, int32_t evse_id) const; /// /// \brief Checks a given \p profile does not have an id that conflicts with an existing profile diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index c0bda44af..9cd21c3e1 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -178,6 +178,9 @@ ChargePoint::ChargePoint(const std::map& evse_connector_struct evse_connector_structure, *this->device_model, this->database_handler, component_state_manager, transaction_meter_value_callback, this->callbacks.pause_charging_callback); + this->smart_charging_handler = + std::make_shared(*this->evse_manager, this->device_model, this->database_handler); + // configure logging this->configure_message_logging_format(message_log_path); @@ -218,6 +221,8 @@ void ChargePoint::start(BootReasonEnum bootreason) { // get transaction messages from db (if there are any) so they can be sent again. this->message_queue->get_persisted_messages_from_db(); this->boot_notification_req(bootreason); + // K01.27 - call load_charging_profiles when system boots + this->load_charging_profiles(); this->start_websocket(); if (this->bootreason == BootReasonEnum::RemoteReset) { @@ -1307,6 +1312,9 @@ void ChargePoint::handle_message(const EnhancedMessage& messa case MessageType::CustomerInformation: this->handle_customer_information_req(json_message); break; + case MessageType::SetChargingProfile: + this->handle_set_charging_profile_req(json_message); + break; case MessageType::SetMonitoringBase: this->handle_set_monitoring_base_req(json_message); break; @@ -3147,6 +3155,39 @@ void ChargePoint::handle_heartbeat_response(CallResult call) } } +void ChargePoint::handle_set_charging_profile_req(Call call) { + EVLOG_debug << "Received SetChargingProfileRequest: " << call.msg << "\nwith messageId: " << call.uniqueId; + auto msg = call.msg; + SetChargingProfileResponse response; + response.status = ChargingProfileStatusEnum::Rejected; + + // K01.FR.22: Reject ChargingStationExternalConstraints profiles in SetChargingProfileRequest + if (msg.chargingProfile.chargingProfilePurpose == ChargingProfilePurposeEnum::ChargingStationExternalConstraints) { + response.statusInfo = StatusInfo(); + response.statusInfo->reasonCode = "InvalidValue"; + response.statusInfo->additionalInfo = "ChargingStationExternalConstraintsInSetChargingProfileRequest"; + EVLOG_debug << "Rejecting SetChargingProfileRequest:\n reasonCode: " << response.statusInfo->reasonCode.get() + << "\nadditionalInfo: " << response.statusInfo->additionalInfo->get(); + + ocpp::CallResult call_result(response, call.uniqueId); + this->send(call_result); + + return; + } + + response = this->smart_charging_handler->validate_and_add_profile(msg.chargingProfile, msg.evseId); + if (response.status == ChargingProfileStatusEnum::Accepted) { + EVLOG_debug << "Accepting SetChargingProfileRequest"; + this->callbacks.set_charging_profiles_callback(); + } else { + EVLOG_debug << "Rejecting SetChargingProfileRequest:\n reasonCode: " << response.statusInfo->reasonCode.get() + << "\nadditionalInfo: " << response.statusInfo->additionalInfo->get(); + } + + ocpp::CallResult call_result(response, call.uniqueId); + this->send(call_result); +} + void ChargePoint::handle_firmware_update_req(Call call) { EVLOG_debug << "Received UpdateFirmwareRequest: " << call.msg << "\nwith messageId: " << call.uniqueId; if (call.msg.firmware.signingCertificate.has_value() or call.msg.firmware.signature.has_value()) { @@ -3818,6 +3859,31 @@ void ChargePoint::execute_change_availability_request(ChangeAvailabilityRequest } } +// K01.27 - load profiles from database +void ChargePoint::load_charging_profiles() { + try { + auto evses = this->database_handler->get_all_charging_profiles_group_by_evse(); + EVLOG_info << "Found " << evses.size() << " evse in the database"; + for (const auto& [evse_id, profiles] : evses) { + for (auto profile : profiles) { + try { + if (this->smart_charging_handler->validate_profile(profile, evse_id) == + ProfileValidationResultEnum::Valid) { + this->smart_charging_handler->add_profile(profile, evse_id); + } else { + // delete if not valid anymore + this->database_handler->delete_charging_profile(profile.id); + } + } catch (const QueryExecutionException& e) { + EVLOG_warning << "Failed database operation for ChargingProfiles: " << e.what(); + } + } + } + } catch (const std::exception& e) { + EVLOG_warning << "Unknown error while loading charging profiles from database: " << e.what(); + } +} + std::vector ChargePoint::get_variables(const std::vector& get_variable_data_vector) { std::vector response; diff --git a/lib/ocpp/v201/database_handler.cpp b/lib/ocpp/v201/database_handler.cpp index bb83a652f..ff3c0b17e 100644 --- a/lib/ocpp/v201/database_handler.cpp +++ b/lib/ocpp/v201/database_handler.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +#include "everest/logging.hpp" #include #include #include @@ -719,5 +720,60 @@ void DatabaseHandler::transaction_delete(const std::string& transaction_id) { } } +void DatabaseHandler::insert_or_update_charging_profile(const int evse_id, const v201::ChargingProfile& profile) { + // add or replace + std::string sql = + "INSERT OR REPLACE INTO CHARGING_PROFILES (ID, EVSE_ID, STACK_LEVEL, CHARGING_PROFILE_PURPOSE, PROFILE) VALUES " + "(@id, @evse_id, @stack_level, @charging_profile_purpose, @profile)"; + auto stmt = this->database->new_statement(sql); + + json json_profile(profile); + + stmt->bind_int("@id", profile.id); + stmt->bind_int("@evse_id", evse_id); + stmt->bind_int("@stack_level", profile.stackLevel); + stmt->bind_text("@charging_profile_purpose", + conversions::charging_profile_purpose_enum_to_string(profile.chargingProfilePurpose)); + stmt->bind_text("@profile", json_profile.dump(), SQLiteString::Transient); + + if (stmt->step() != SQLITE_DONE) { + throw QueryExecutionException(this->database->get_error_message()); + } +} + +void DatabaseHandler::delete_charging_profile(const int profile_id) { + std::string sql = "DELETE FROM CHARGING_PROFILES WHERE ID = @profile_id;"; + auto stmt = this->database->new_statement(sql); + + stmt->bind_int("@profile_id", profile_id); + if (stmt->step() != SQLITE_DONE) { + throw QueryExecutionException(this->database->get_error_message()); + } +} + +void DatabaseHandler::clear_charging_profiles() { + this->database->clear_table("CHARGING_PROFILES"); +} + +std::map> DatabaseHandler::get_all_charging_profiles_group_by_evse() { + std::map> map; + + std::string sql = "SELECT EVSE_ID, PROFILE FROM CHARGING_PROFILES"; + + auto stmt = this->database->new_statement(sql); + + while (stmt->step() != SQLITE_DONE) { + auto evse_id = stmt->column_int(0); + auto profile = json::parse(stmt->column_text(1)); + + auto profiles = map[evse_id]; + profiles.emplace_back(profile); + + map[evse_id] = profiles; + } + + return map; +} + } // namespace v201 } // namespace ocpp diff --git a/lib/ocpp/v201/smart_charging.cpp b/lib/ocpp/v201/smart_charging.cpp index 7156c289e..e1ae46250 100644 --- a/lib/ocpp/v201/smart_charging.cpp +++ b/lib/ocpp/v201/smart_charging.cpp @@ -3,6 +3,7 @@ #include "date/tz.h" #include "everest/logging.hpp" +#include "ocpp/common/message_queue.hpp" #include "ocpp/common/types.hpp" #include "ocpp/v201/ctrlr_component_variables.hpp" #include "ocpp/v201/device_model.hpp" @@ -73,6 +74,45 @@ std::string profile_validation_result_to_string(ProfileValidationResultEnum e) { throw std::out_of_range("No known string conversion for provided enum of type ProfileValidationResultEnum"); } + +std::string profile_validation_result_to_reason_code(ProfileValidationResultEnum e) { + switch (e) { + case ProfileValidationResultEnum::Valid: + return "NoError"; + case ProfileValidationResultEnum::DuplicateProfileValidityPeriod: + case ProfileValidationResultEnum::DuplicateTxDefaultProfileFound: + case ProfileValidationResultEnum::ExistingChargingStationExternalConstraints: + return "DuplicateProfile"; + case ProfileValidationResultEnum::TxProfileTransactionNotOnEvse: + case ProfileValidationResultEnum::TxProfileEvseHasNoActiveTransaction: + return "TxNotFound"; + case ProfileValidationResultEnum::TxProfileConflictingStackLevel: + return "InvalidStackLevel"; + case ProfileValidationResultEnum::ChargingScheduleChargingRateUnitUnsupported: + return "UnsupportedRateUnit"; + case ProfileValidationResultEnum::ChargingProfileNoChargingSchedulePeriods: + case ProfileValidationResultEnum::ChargingProfileFirstStartScheduleIsNotZero: + case ProfileValidationResultEnum::ChargingProfileMissingRequiredStartSchedule: + case ProfileValidationResultEnum::ChargingProfileExtraneousStartSchedule: + case ProfileValidationResultEnum::ChargingSchedulePeriodsOutOfOrder: + case ProfileValidationResultEnum::ChargingSchedulePeriodInvalidPhaseToUse: + case ProfileValidationResultEnum::ChargingSchedulePeriodUnsupportedNumberPhases: + case ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues: + case ProfileValidationResultEnum::ChargingSchedulePeriodPhaseToUseACPhaseSwitchingUnsupported: + return "InvalidSchedule"; + case ProfileValidationResultEnum::TxProfileMissingTransactionId: + return "MissingParam"; + case ProfileValidationResultEnum::EvseDoesNotExist: + case ProfileValidationResultEnum::TxProfileEvseIdNotGreaterThanZero: + case ProfileValidationResultEnum::ChargingStationMaxProfileCannotBeRelative: + case ProfileValidationResultEnum::ChargingStationMaxProfileEvseIdGreaterThanZero: + return "InvalidValue"; + case ProfileValidationResultEnum::InvalidProfileType: + return "InternalError"; + } + + throw std::out_of_range("No applicable reason code for provided enum of type ProfileValidationResultEnum"); +} } // namespace conversions std::ostream& operator<<(std::ostream& os, const ProfileValidationResultEnum validation_result) { @@ -99,8 +139,25 @@ CurrentPhaseType SmartChargingHandler::get_current_phase_type(const std::optiona } SmartChargingHandler::SmartChargingHandler(EvseManagerInterface& evse_manager, - std::shared_ptr& device_model) : - evse_manager(evse_manager), device_model(device_model) { + std::shared_ptr& device_model, + std::shared_ptr database_handler) : + evse_manager(evse_manager), device_model(device_model), database_handler(database_handler) { +} + +SetChargingProfileResponse SmartChargingHandler::validate_and_add_profile(ChargingProfile& profile, int32_t evse_id) { + SetChargingProfileResponse response; + response.status = ChargingProfileStatusEnum::Rejected; + + auto result = this->validate_profile(profile, evse_id); + if (result == ProfileValidationResultEnum::Valid) { + response = this->add_profile(profile, evse_id); + } else { + response.statusInfo = StatusInfo(); + response.statusInfo->reasonCode = conversions::profile_validation_result_to_reason_code(result); + response.statusInfo->additionalInfo = conversions::profile_validation_result_to_string(result); + } + + return response; } ProfileValidationResultEnum SmartChargingHandler::validate_profile(ChargingProfile& profile, int32_t evse_id) { @@ -161,7 +218,7 @@ ProfileValidationResultEnum SmartChargingHandler::validate_charging_station_max_ return ProfileValidationResultEnum::InvalidProfileType; } - if (is_overlapping_validity_period(evse_id, profile)) { + if (is_overlapping_validity_period(profile, evse_id)) { return ProfileValidationResultEnum::DuplicateProfileValidityPeriod; } @@ -180,7 +237,7 @@ ProfileValidationResultEnum SmartChargingHandler::validate_tx_default_profile(Ch int32_t evse_id) const { auto profiles = evse_id == 0 ? get_evse_specific_tx_default_profiles() : get_station_wide_tx_default_profiles(); - if (is_overlapping_validity_period(evse_id, profile)) { + if (is_overlapping_validity_period(profile, evse_id)) { return ProfileValidationResultEnum::DuplicateProfileValidityPeriod; } @@ -245,6 +302,9 @@ ProfileValidationResultEnum SmartChargingHandler::validate_tx_profile(const Char ProfileValidationResultEnum SmartChargingHandler::validate_profile_schedules(ChargingProfile& profile, std::optional evse_opt) const { + auto charging_station_supply_phases = + this->device_model->get_value(ControllerComponentVariables::ChargingStationSupplyPhases); + for (auto& schedule : profile.chargingSchedule) { // K01.FR.26; We currently need to do string conversions for this manually because our DeviceModel class does // not let us get a vector of ChargingScheduleChargingRateUnits. @@ -298,7 +358,7 @@ SmartChargingHandler::validate_profile_schedules(ChargingProfile& profile, if (phase_type == CurrentPhaseType::AC) { // K01.FR.45; Once again rejecting invalid values if (charging_schedule_period.numberPhases.has_value() && - charging_schedule_period.numberPhases > DEFAULT_AND_MAX_NUMBER_PHASES) { + charging_schedule_period.numberPhases > charging_station_supply_phases) { return ProfileValidationResultEnum::ChargingSchedulePeriodUnsupportedNumberPhases; } @@ -322,26 +382,38 @@ SmartChargingHandler::validate_profile_schedules(ChargingProfile& profile, return ProfileValidationResultEnum::Valid; } -SetChargingProfileResponse SmartChargingHandler::add_profile(int32_t evse_id, ChargingProfile& profile) { +SetChargingProfileResponse SmartChargingHandler::add_profile(ChargingProfile& profile, int32_t evse_id) { SetChargingProfileResponse response; response.status = ChargingProfileStatusEnum::Accepted; - auto found_profile = false; - for (auto& [existing_evse_id, evse_profiles] : charging_profiles) { - for (auto it = evse_profiles.begin(); it != evse_profiles.end(); it++) { - if (profile.id == it->id) { - evse_profiles.erase(it); - found_profile = true; + + // K01.FR05 - replace non-ChargingStationExternalConstraints profiles if id exists. + try { + // K01.FR27 - add profiles to database when valid + this->database_handler->insert_or_update_charging_profile(evse_id, profile); + + auto found_profile = false; + for (auto& [existing_evse_id, evse_profiles] : charging_profiles) { + for (auto it = evse_profiles.begin(); it != evse_profiles.end(); it++) { + if (profile.id == it->id) { + evse_profiles.erase(it); + found_profile = true; + break; + } + } + + if (found_profile) { break; } } + charging_profiles[evse_id].push_back(profile); - if (found_profile) { - break; - } + } catch (const QueryExecutionException& e) { + EVLOG_error << "Could not store ChargingProfile in the database: " << e.what(); + response.status = ChargingProfileStatusEnum::Rejected; + response.statusInfo = StatusInfo(); + response.statusInfo->reasonCode = "InternalError"; } - charging_profiles[evse_id].push_back(profile); - return response; } @@ -391,8 +463,8 @@ std::vector SmartChargingHandler::get_station_wide_tx_default_p return station_wide_tx_default_profiles; } -bool SmartChargingHandler::is_overlapping_validity_period(int candidate_evse_id, - const ChargingProfile& candidate_profile) const { +bool SmartChargingHandler::is_overlapping_validity_period(const ChargingProfile& candidate_profile, + int candidate_evse_id) const { if (candidate_profile.chargingProfilePurpose == ChargingProfilePurposeEnum::TxProfile) { // This only applies to non TxProfile types. diff --git a/tests/lib/ocpp/v201/CMakeLists.txt b/tests/lib/ocpp/v201/CMakeLists.txt index 8ab50a805..97c2714f4 100644 --- a/tests/lib/ocpp/v201/CMakeLists.txt +++ b/tests/lib/ocpp/v201/CMakeLists.txt @@ -4,6 +4,7 @@ target_include_directories(libocpp_unit_tests PUBLIC target_sources(libocpp_unit_tests PRIVATE test_charge_point.cpp + test_database_handler.cpp test_database_migration_files.cpp test_device_model_storage_sqlite.cpp test_notify_report_requests_splitter.cpp diff --git a/tests/lib/ocpp/v201/comparators.cpp b/tests/lib/ocpp/v201/comparators.cpp index b7f28dca7..9940bfb62 100644 --- a/tests/lib/ocpp/v201/comparators.cpp +++ b/tests/lib/ocpp/v201/comparators.cpp @@ -18,4 +18,12 @@ bool operator==(const ::ocpp::v201::GetCertificateStatusRequest& a, a.ocspRequestData.responderURL == b.ocspRequestData.responderURL; } -} // namespace testing::internal \ No newline at end of file +} // namespace testing::internal + +namespace ocpp::v201 { + +bool operator==(const ChargingProfile& a, const ChargingProfile& b) { + return a.chargingProfileKind == b.chargingProfileKind && a.chargingProfilePurpose == b.chargingProfilePurpose && + a.id == b.id && a.stackLevel == b.stackLevel; +} +} // namespace ocpp::v201 \ No newline at end of file diff --git a/tests/lib/ocpp/v201/comparators.hpp b/tests/lib/ocpp/v201/comparators.hpp index 504ec9fe1..50ca639c8 100644 --- a/tests/lib/ocpp/v201/comparators.hpp +++ b/tests/lib/ocpp/v201/comparators.hpp @@ -15,4 +15,10 @@ bool operator==(const ::ocpp::v201::GetCertificateStatusRequest& a, const ::ocpp } // namespace testing::internal +namespace ocpp::v201 { + +bool operator==(const ChargingProfile& a, const ChargingProfile& b); + +} // namespace ocpp::v201 + #endif // TESTS_OCPP_COMPARATORS_H diff --git a/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp b/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp new file mode 100644 index 000000000..b352c34b2 --- /dev/null +++ b/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest + +#include "gmock/gmock.h" +#include +#include + +#include "ocpp/v201/messages/SetChargingProfile.hpp" +#include "ocpp/v201/smart_charging.hpp" + +namespace ocpp::v201 { +class SmartChargingHandlerMock : public SmartChargingHandlerInterface { +public: + MOCK_METHOD(SetChargingProfileResponse, validate_and_add_profile, (ChargingProfile & profile, int32_t evse_id)); + MOCK_METHOD(ProfileValidationResultEnum, validate_profile, (ChargingProfile & profile, int32_t evse_id)); + MOCK_METHOD(SetChargingProfileResponse, add_profile, (ChargingProfile & profile, int32_t evse_id)); +}; +} // namespace ocpp::v201 diff --git a/tests/lib/ocpp/v201/test_charge_point.cpp b/tests/lib/ocpp/v201/test_charge_point.cpp index 8b6f1836c..1ae57a57d 100644 --- a/tests/lib/ocpp/v201/test_charge_point.cpp +++ b/tests/lib/ocpp/v201/test_charge_point.cpp @@ -1,16 +1,180 @@ +#include "comparators.hpp" +#include "everest/logging.hpp" +#include "evse_security_mock.hpp" +#include "lib/ocpp/common/database_testing_utils.hpp" +#include "ocpp/common/call_types.hpp" +#include "ocpp/common/message_queue.hpp" #include "ocpp/v201/charge_point.hpp" +#include "ocpp/v201/device_model_storage_sqlite.hpp" +#include "ocpp/v201/init_device_model_db.hpp" +#include "ocpp/v201/messages/SetChargingProfile.hpp" +#include "ocpp/v201/smart_charging.hpp" +#include "ocpp/v201/types.hpp" +#include "smart_charging_handler_mock.hpp" #include "gmock/gmock.h" +#include +#include #include +#include +#include + +static const int DEFAULT_EVSE_ID = 1; +static const int DEFAULT_PROFILE_ID = 1; +static const int DEFAULT_STACK_LEVEL = 1; +static const std::string TEMP_OUTPUT_PATH = "/tmp/ocpp201"; +const static std::string MIGRATION_FILES_PATH = "./resources/v201/device_model_migration_files"; +const static std::string SCHEMAS_PATH = "./resources/example_config/v201/component_schemas"; +const static std::string CONFIG_PATH = "./resources/example_config/v201/config.json"; +const static std::string DEVICE_MODEL_DB_IN_MEMORY_PATH = "file::memory:?cache=shared"; +static const std::string DEFAULT_TX_ID = "10c75ff7-74f5-44f5-9d01-f649f3ac7b78"; namespace ocpp::v201 { -class ChargePointFixture : public testing::Test { +class TestChargePoint : public ChargePoint { +public: + using ChargePoint::handle_message; + using ChargePoint::smart_charging_handler; + + TestChargePoint(std::map& evse_connector_structure, + std::unique_ptr device_model_storage, const std::string& ocpp_main_path, + const std::string& core_database_path, const std::string& sql_init_path, + const std::string& message_log_path, const std::shared_ptr evse_security, + const Callbacks& callbacks, std::shared_ptr smart_charging_handler) : + ChargePoint(evse_connector_structure, std::move(device_model_storage), ocpp_main_path, core_database_path, + sql_init_path, message_log_path, evse_security, callbacks) { + this->smart_charging_handler = smart_charging_handler; + } +}; + +class ChargePointFixture : public DatabaseTestingUtils { public: ChargePointFixture() { } ~ChargePointFixture() { } + void SetUp() override { + charge_point->start(); + } + + void TearDown() override { + charge_point->stop(); + } + + void create_device_model_db(const std::string& path) { + InitDeviceModelDb db(path, MIGRATION_FILES_PATH); + db.initialize_database(SCHEMAS_PATH, true); + db.insert_config_and_default_values(SCHEMAS_PATH, CONFIG_PATH); + } + + std::shared_ptr + create_device_model(const std::optional ac_phase_switching_supported = "true") { + create_device_model_db(DEVICE_MODEL_DB_IN_MEMORY_PATH); + auto device_model_storage = std::make_unique(DEVICE_MODEL_DB_IN_MEMORY_PATH); + auto device_model = std::make_shared(std::move(device_model_storage)); + + // Defaults + const auto& charging_rate_unit_cv = ControllerComponentVariables::ChargingScheduleChargingRateUnit; + device_model->set_value(charging_rate_unit_cv.component, charging_rate_unit_cv.variable.value(), + AttributeEnum::Actual, "A,W", "test", true); + + const auto& ac_phase_switching_cv = ControllerComponentVariables::ACPhaseSwitchingSupported; + device_model->set_value(ac_phase_switching_cv.component, ac_phase_switching_cv.variable.value(), + AttributeEnum::Actual, ac_phase_switching_supported.value_or(""), "test", true); + + return device_model; + } + + std::unique_ptr create_charge_point() { + std::map evse_connector_structure = {{1, 1}, {2, 1}}; + std::unique_ptr device_model_storage = + std::make_unique(DEVICE_MODEL_DB_IN_MEMORY_PATH); + auto charge_point = std::make_unique(evse_connector_structure, std::move(device_model_storage), + "", TEMP_OUTPUT_PATH, MIGRATION_FILES_LOCATION_V201, + TEMP_OUTPUT_PATH, std::make_shared(), + create_callbacks_with_mocks(), smart_charging_handler); + return charge_point; + } + + std::vector create_charging_schedule_periods(std::vector start_periods) { + auto charging_schedule_periods = std::vector(); + for (auto start_period : start_periods) { + auto charging_schedule_period = ChargingSchedulePeriod{ + .startPeriod = start_period, + }; + charging_schedule_periods.push_back(charging_schedule_period); + } + + return charging_schedule_periods; + } + + ChargingSchedule create_charge_schedule(ChargingRateUnitEnum charging_rate_unit, + std::vector charging_schedule_period, + std::optional start_schedule = std::nullopt) { + int32_t id; + std::optional custom_data; + std::optional duration; + std::optional min_charging_rate; + std::optional sales_tariff; + + return ChargingSchedule{ + id, + charging_rate_unit, + charging_schedule_period, + custom_data, + start_schedule, + duration, + min_charging_rate, + sales_tariff, + }; + } + + ChargingProfile + create_charging_profile(int32_t charging_profile_id, ChargingProfilePurposeEnum charging_profile_purpose, + ChargingSchedule charging_schedule, std::optional transaction_id = {}, + ChargingProfileKindEnum charging_profile_kind = ChargingProfileKindEnum::Absolute, + int stack_level = DEFAULT_STACK_LEVEL, std::optional validFrom = {}, + std::optional validTo = {}) { + auto recurrency_kind = RecurrencyKindEnum::Daily; + std::vector charging_schedules = {charging_schedule}; + return ChargingProfile{.id = charging_profile_id, + .stackLevel = stack_level, + .chargingProfilePurpose = charging_profile_purpose, + .chargingProfileKind = charging_profile_kind, + .chargingSchedule = charging_schedules, + .customData = {}, + .recurrencyKind = recurrency_kind, + .validFrom = validFrom, + .validTo = validTo, + .transactionId = transaction_id}; + } + + ocpp::v201::Callbacks create_callbacks_with_mocks() { + ocpp::v201::Callbacks callbacks; + + callbacks.is_reset_allowed_callback = is_reset_allowed_callback_mock.AsStdFunction(); + callbacks.reset_callback = reset_callback_mock.AsStdFunction(); + callbacks.stop_transaction_callback = stop_transaction_callback_mock.AsStdFunction(); + callbacks.pause_charging_callback = pause_charging_callback_mock.AsStdFunction(); + callbacks.connector_effective_operative_status_changed_callback = + connector_effective_operative_status_changed_callback_mock.AsStdFunction(); + callbacks.get_log_request_callback = get_log_request_callback_mock.AsStdFunction(); + callbacks.unlock_connector_callback = unlock_connector_callback_mock.AsStdFunction(); + callbacks.remote_start_transaction_callback = remote_start_transaction_callback_mock.AsStdFunction(); + callbacks.is_reservation_for_token_callback = is_reservation_for_token_callback_mock.AsStdFunction(); + callbacks.update_firmware_request_callback = update_firmware_request_callback_mock.AsStdFunction(); + callbacks.security_event_callback = security_event_callback_mock.AsStdFunction(); + callbacks.set_charging_profiles_callback = set_charging_profiles_callback_mock.AsStdFunction(); + + return callbacks; + } + + sqlite3* db_handle; + std::shared_ptr device_model = create_device_model(); + std::shared_ptr smart_charging_handler = std::make_shared(); + std::unique_ptr charge_point = create_charge_point(); + boost::uuids::random_generator uuid_generator = boost::uuids::random_generator(); + void configure_callbacks_with_mocks() { callbacks.is_reset_allowed_callback = is_reset_allowed_callback_mock.AsStdFunction(); callbacks.reset_callback = reset_callback_mock.AsStdFunction(); @@ -27,6 +191,34 @@ class ChargePointFixture : public testing::Test { callbacks.set_charging_profiles_callback = set_charging_profiles_callback_mock.AsStdFunction(); } + std::string uuid() { + std::stringstream s; + s << uuid_generator(); + return s.str(); + } + + template void call_to_json(json& j, const ocpp::Call& call) { + j = json::array(); + j.push_back(MessageTypeId::CALL); + j.push_back(call.uniqueId.get()); + j.push_back(call.msg.get_type()); + j.push_back(json(call.msg)); + } + + template EnhancedMessage request_to_enhanced_message(const T& req) { + auto message_id = uuid(); + ocpp::Call call(req, message_id); + EnhancedMessage enhanced_message{ + .uniqueId = message_id, + .messageType = M, + .messageTypeId = MessageTypeId::CALL, + }; + + call_to_json(enhanced_message.message, call); + + return enhanced_message; + } + testing::MockFunction evse_id, const ResetEnum& reset_type)> is_reset_allowed_callback_mock; testing::MockFunction evse_id, const ResetEnum& reset_type)> @@ -340,4 +532,94 @@ TEST_F(ChargePointFixture, K01FR02_CallbacksValidityChecksIfOptionalTransactionE EXPECT_TRUE(callbacks.all_callbacks_valid()); } +TEST_F(ChargePointFixture, K01_SetChargingProfileRequest_ValidatesAndAddsProfile) { + auto periods = create_charging_schedule_periods({0, 1, 2}); + + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID); + + SetChargingProfileRequest req; + req.evseId = DEFAULT_EVSE_ID; + req.chargingProfile = profile; + + auto set_charging_profile_req = + request_to_enhanced_message(req); + + EXPECT_CALL(*smart_charging_handler, validate_and_add_profile(profile, DEFAULT_EVSE_ID)); + + charge_point->handle_message(set_charging_profile_req); +} + +TEST_F(ChargePointFixture, K01FR07_SetChargingProfileRequest_TriggersCallbackWhenValid) { + auto periods = create_charging_schedule_periods({0, 1, 2}); + + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID); + + SetChargingProfileRequest req; + req.evseId = DEFAULT_EVSE_ID; + req.chargingProfile = profile; + + auto set_charging_profile_req = + request_to_enhanced_message(req); + + SetChargingProfileResponse accept_response; + accept_response.status = ChargingProfileStatusEnum::Accepted; + + ON_CALL(*smart_charging_handler, validate_and_add_profile).WillByDefault(testing::Return(accept_response)); + EXPECT_CALL(set_charging_profiles_callback_mock, Call); + + charge_point->handle_message(set_charging_profile_req); +} + +TEST_F(ChargePointFixture, K01FR07_SetChargingProfileRequest_DoesNotTriggerCallbackWhenInvalid) { + auto periods = create_charging_schedule_periods({0, 1, 2}); + + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID); + + SetChargingProfileRequest req; + req.evseId = DEFAULT_EVSE_ID; + req.chargingProfile = profile; + + auto set_charging_profile_req = + request_to_enhanced_message(req); + + SetChargingProfileResponse reject_response; + reject_response.status = ChargingProfileStatusEnum::Rejected; + reject_response.statusInfo = StatusInfo(); + reject_response.statusInfo->reasonCode = conversions::profile_validation_result_to_reason_code( + ProfileValidationResultEnum::TxProfileEvseHasNoActiveTransaction); + reject_response.statusInfo->additionalInfo = conversions::profile_validation_result_to_string( + ProfileValidationResultEnum::TxProfileEvseHasNoActiveTransaction); + + ON_CALL(*smart_charging_handler, validate_and_add_profile).WillByDefault(testing::Return(reject_response)); + EXPECT_CALL(set_charging_profiles_callback_mock, Call).Times(0); + + charge_point->handle_message(set_charging_profile_req); +} + +TEST_F(ChargePointFixture, K01FR22_SetChargingProfileRequest_RejectsChargingStationExternalConstraints) { + auto periods = create_charging_schedule_periods({0, 1, 2}); + + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::ChargingStationExternalConstraints, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID); + + SetChargingProfileRequest req; + req.evseId = DEFAULT_EVSE_ID; + req.chargingProfile = profile; + + auto set_charging_profile_req = + request_to_enhanced_message(req); + + EXPECT_CALL(*smart_charging_handler, validate_and_add_profile).Times(0); + EXPECT_CALL(set_charging_profiles_callback_mock, Call).Times(0); + + charge_point->handle_message(set_charging_profile_req); +} + } // namespace ocpp::v201 diff --git a/tests/lib/ocpp/v201/test_database_handler.cpp b/tests/lib/ocpp/v201/test_database_handler.cpp index 17844462f..340dd42b1 100644 --- a/tests/lib/ocpp/v201/test_database_handler.cpp +++ b/tests/lib/ocpp/v201/test_database_handler.cpp @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest +#include "comparators.hpp" #include "database_testing_utils.hpp" +#include "ocpp/v201/enums.hpp" #include #include #include @@ -144,4 +146,221 @@ TEST_F(DatabaseHandlerTest, TransactionDelete) { TEST_F(DatabaseHandlerTest, TransactionDeleteNotFound) { EXPECT_NO_THROW(this->database_handler.transaction_delete("txIdNotFound")); -} \ No newline at end of file +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithNoData_InsertProfile) { + this->database_handler.insert_or_update_charging_profile( + 1, ChargingProfile{ + .id = 1, .stackLevel = 1, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}); + + auto sut = this->database_handler.get_all_charging_profiles_group_by_evse(); + + EXPECT_EQ(sut.size(), 1); + EXPECT_EQ(sut[1].size(), 1); // Access the profiles at EVSE_ID 1 + EXPECT_EQ(sut[1][0].id, 1); // Access the profiles at EVSE_ID 1 and get the first profile + EXPECT_EQ(sut[1][0].stackLevel, 1); // Access the profiles at EVSE_ID 1 and get the first profile +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithProfileData_UpdateProfile) { + this->database_handler.insert_or_update_charging_profile( + 1, ChargingProfile{ + .id = 2, .stackLevel = 1, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}); + this->database_handler.insert_or_update_charging_profile( + 1, ChargingProfile{ + .id = 2, .stackLevel = 2, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}); + + std::string sql = "SELECT COUNT(*) FROM CHARGING_PROFILES"; + auto select_stmt = this->database->new_statement(sql); + + EXPECT_EQ(select_stmt->step(), SQLITE_ROW); + + auto count = select_stmt->column_int(0); + EXPECT_EQ(count, 1); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithProfileData_InsertNewProfile) { + this->database_handler.insert_or_update_charging_profile( + 1, ChargingProfile{ + .id = 1, .stackLevel = 1, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}); + this->database_handler.insert_or_update_charging_profile( + 1, ChargingProfile{ + .id = 2, .stackLevel = 1, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}); + + std::string sql = "SELECT COUNT(*) FROM CHARGING_PROFILES"; + auto select_stmt = this->database->new_statement(sql); + + EXPECT_EQ(select_stmt->step(), SQLITE_ROW); + + auto count = select_stmt->column_int(0); + EXPECT_EQ(count, 2); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithProfileData_DeleteRemovesSpecifiedProfiles) { + this->database_handler.insert_or_update_charging_profile( + 1, ChargingProfile{ + .id = 1, .stackLevel = 1, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}); + this->database_handler.insert_or_update_charging_profile( + 1, ChargingProfile{ + .id = 2, .stackLevel = 1, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}); + + auto sql = "SELECT COUNT(*) FROM CHARGING_PROFILES"; + + auto select_stmt = this->database->new_statement(sql); + + EXPECT_NE(select_stmt->step(), SQLITE_DONE); + auto count = select_stmt->column_int(0); + EXPECT_EQ(count, 2); + + select_stmt->step(); + + this->database_handler.delete_charging_profile(1); + + select_stmt->reset(); + + EXPECT_NE(select_stmt->step(), SQLITE_DONE); + count = select_stmt->column_int(0); + EXPECT_EQ(count, 1); + select_stmt->step(); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithProfileData_DeleteAllRemovesAllProfiles) { + this->database_handler.insert_or_update_charging_profile( + 1, ChargingProfile{ + .id = 1, .stackLevel = 1, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}); + this->database_handler.insert_or_update_charging_profile( + 1, ChargingProfile{ + .id = 2, .stackLevel = 1, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}); + + auto sql = "SELECT COUNT(*) FROM CHARGING_PROFILES"; + + auto select_stmt = this->database->new_statement(sql); + + EXPECT_NE(select_stmt->step(), SQLITE_DONE); + auto count = select_stmt->column_int(0); + EXPECT_EQ(count, 2); + select_stmt->step(); + + this->database_handler.clear_charging_profiles(); + select_stmt->reset(); + + EXPECT_NE(select_stmt->step(), SQLITE_DONE); + count = select_stmt->column_int(0); + EXPECT_EQ(count, 0); + select_stmt->step(); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithNoProfileData_DeleteAllDoesNotFail) { + + auto sql = "SELECT COUNT(*) FROM CHARGING_PROFILES"; + + auto select_stmt = this->database->new_statement(sql); + + EXPECT_NE(select_stmt->step(), SQLITE_DONE); + auto count = select_stmt->column_int(0); + EXPECT_EQ(count, 0); + select_stmt->step(); + + this->database_handler.clear_charging_profiles(); + select_stmt->reset(); + + EXPECT_NE(select_stmt->step(), SQLITE_DONE); + count = select_stmt->column_int(0); + EXPECT_EQ(count, 0); + select_stmt->step(); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithSingleProfileData_LoadsChargingProfile) { + auto profile = ChargingProfile{ + .id = 1, .stackLevel = 1, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}; + this->database_handler.insert_or_update_charging_profile(1, profile); + + auto sut = this->database_handler.get_all_charging_profiles_group_by_evse(); + + EXPECT_EQ(sut.size(), 1); + + // The evse id is found + EXPECT_NE(sut.find(1), sut.end()); + + auto profiles = sut[1]; + + EXPECT_EQ(profiles.size(), 1); + EXPECT_EQ(profile, profiles[0]); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithMultipleProfileSameEvse_LoadsChargingProfile) { + auto p1 = ChargingProfile{ + .id = 1, .stackLevel = 1, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}; + + this->database_handler.insert_or_update_charging_profile(1, p1); + + auto p2 = ChargingProfile{ + .id = 2, .stackLevel = 2, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}; + this->database_handler.insert_or_update_charging_profile(1, p2); + + auto p3 = ChargingProfile{ + .id = 3, .stackLevel = 3, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}; + this->database_handler.insert_or_update_charging_profile(1, p3); + + auto sut = this->database_handler.get_all_charging_profiles_group_by_evse(); + + EXPECT_EQ(sut.size(), 1); + + // The evse id is found + EXPECT_NE(sut.find(1), sut.end()); + + auto profiles = sut[1]; + + EXPECT_EQ(profiles.size(), 3); + EXPECT_EQ(profiles[0], p1); + EXPECT_EQ(profiles[1], p2); + EXPECT_EQ(profiles[2], p3); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithMultipleProfileDiffEvse_LoadsChargingProfile) { + auto p1 = ChargingProfile{ + .id = 1, .stackLevel = 1, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}; + this->database_handler.insert_or_update_charging_profile(1, p1); + + auto p2 = + ChargingProfile{.id = 2, .stackLevel = 2, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxProfile}; + this->database_handler.insert_or_update_charging_profile(1, p2); + + auto p3 = ChargingProfile{ + .id = 3, .stackLevel = 3, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}; + this->database_handler.insert_or_update_charging_profile(2, p3); + auto p4 = + ChargingProfile{.id = 4, .stackLevel = 4, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxProfile}; + this->database_handler.insert_or_update_charging_profile(2, p4); + + auto p5 = ChargingProfile{ + .id = 5, .stackLevel = 5, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile}; + this->database_handler.insert_or_update_charging_profile(3, p5); + + auto p6 = + ChargingProfile{.id = 6, .stackLevel = 6, .chargingProfilePurpose = ChargingProfilePurposeEnum::TxProfile}; + this->database_handler.insert_or_update_charging_profile(3, p6); + + auto sut = this->database_handler.get_all_charging_profiles_group_by_evse(); + + EXPECT_EQ(sut.size(), 3); + + EXPECT_NE(sut.find(1), sut.end()); + EXPECT_NE(sut.find(2), sut.end()); + EXPECT_NE(sut.find(3), sut.end()); + + auto profiles1 = sut[1]; + auto profiles2 = sut[2]; + auto profiles3 = sut[3]; + + EXPECT_EQ(profiles1.size(), 2); + EXPECT_EQ(profiles1[0], p1); + EXPECT_EQ(profiles1[1], p2); + + EXPECT_EQ(profiles2.size(), 2); + EXPECT_EQ(profiles2[0], p3); + EXPECT_EQ(profiles2[1], p4); + + EXPECT_EQ(profiles3.size(), 2); + EXPECT_EQ(profiles3[0], p5); + EXPECT_EQ(profiles3[1], p6); +} diff --git a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp index ee15fbb67..3cbf7cb57 100644 --- a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp +++ b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp @@ -28,17 +28,12 @@ #include #include +#include "comparators.hpp" #include #include namespace ocpp::v201 { -bool operator==(const ChargingProfile& lhs, const ChargingProfile& rhs) { - return lhs.chargingProfileKind == rhs.chargingProfileKind && - lhs.chargingProfilePurpose == rhs.chargingProfilePurpose && lhs.id == rhs.id && - lhs.stackLevel == rhs.stackLevel; -} - static const int NR_OF_EVSES = 1; static const int STATION_WIDE_ID = 0; static const int DEFAULT_EVSE_ID = 1; @@ -189,7 +184,12 @@ class ChargepointTestFixtureV201 : public DatabaseTestingUtils { } TestSmartChargingHandler create_smart_charging_handler() { - return TestSmartChargingHandler(*this->evse_manager, device_model); + std::unique_ptr database_connection = + std::make_unique(fs::path("/tmp/ocpp201") / "cp.db"); + std::shared_ptr database_handler = + std::make_shared(std::move(database_connection), MIGRATION_FILES_LOCATION_V201); + database_handler->open_connection(); + return TestSmartChargingHandler(*this->evse_manager, device_model, database_handler); } std::string uuid() { @@ -204,7 +204,7 @@ class ChargepointTestFixtureV201 : public DatabaseTestingUtils { auto existing_profile = create_charging_profile( profile_id, ChargingProfilePurposeEnum::TxDefaultProfile, create_charge_schedule(ChargingRateUnitEnum::A), {}, ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, validFrom, validTo); - handler.add_profile(evse_id, existing_profile); + handler.add_profile(existing_profile, evse_id); } // Default values used within the tests @@ -231,7 +231,7 @@ TEST_F(ChargepointTestFixtureV201, auto external_constraints = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::ChargingStationExternalConstraints, create_charge_schedule(ChargingRateUnitEnum::A), {}); - handler.add_profile(STATION_WIDE_ID, external_constraints); + handler.add_profile(external_constraints, STATION_WIDE_ID); auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, create_charge_schedule(ChargingRateUnitEnum::A), {}); @@ -449,7 +449,7 @@ TEST_F(ChargepointTestFixtureV201, auto profile_2 = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxProfile, create_charge_schedule(ChargingRateUnitEnum::A), DEFAULT_TX_ID, ChargingProfileKindEnum::Absolute, same_stack_level); - handler.add_profile(DEFAULT_EVSE_ID, profile_2); + handler.add_profile(profile_2, DEFAULT_EVSE_ID); auto sut = handler.validate_tx_profile(profile_1, DEFAULT_EVSE_ID); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileConflictingStackLevel)); @@ -467,7 +467,7 @@ TEST_F(ChargepointTestFixtureV201, auto profile_2 = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxProfile, create_charge_schedule(ChargingRateUnitEnum::A), different_transaction_id, ChargingProfileKindEnum::Absolute, same_stack_level); - handler.add_profile(DEFAULT_EVSE_ID, profile_2); + handler.add_profile(profile_2, DEFAULT_EVSE_ID); auto sut = handler.validate_tx_profile(profile_1, DEFAULT_EVSE_ID); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); @@ -487,7 +487,7 @@ TEST_F(ChargepointTestFixtureV201, create_charge_schedule(ChargingRateUnitEnum::A), DEFAULT_TX_ID, ChargingProfileKindEnum::Absolute, stack_level_2); - handler.add_profile(DEFAULT_EVSE_ID, profile_2); + handler.add_profile(profile_2, DEFAULT_EVSE_ID); auto sut = handler.validate_tx_profile(profile_1, DEFAULT_EVSE_ID); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); @@ -686,7 +686,11 @@ TEST_F(ChargepointTestFixtureV201, K01FR44_IfPhaseToUseProvidedForDCChargingStat EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues)); } -TEST_F(ChargepointTestFixtureV201, K01FR45_IfNumberPhasesGreaterThanMaxNumberPhasesForACEVSE_ThenProfileIsInvalid) { +TEST_F(ChargepointTestFixtureV201, + K01FR45_IfNumberPhasesGreaterThanChargingStationSupplyPhasesForACEVSE_ThenProfileIsInvalid) { + device_model->set_value(ControllerComponentVariables::ChargingStationSupplyPhases.component, + ControllerComponentVariables::ChargingStationSupplyPhases.variable.value(), + AttributeEnum::Actual, std::to_string(0), "test", true); auto mock_evse = testing::NiceMock(); ON_CALL(mock_evse, get_current_phase_type).WillByDefault(testing::Return(CurrentPhaseType::AC)); @@ -701,7 +705,7 @@ TEST_F(ChargepointTestFixtureV201, K01FR45_IfNumberPhasesGreaterThanMaxNumberPha } TEST_F(ChargepointTestFixtureV201, - K01FR45_IfNumberPhasesGreaterThanMaxNumberPhasesForACChargingStation_ThenProfileIsInvalid) { + K01FR45_IfNumberPhasesGreaterThanChargingStationSupplyPhasesForACChargingStation_ThenProfileIsInvalid) { device_model->set_value(ControllerComponentVariables::ChargingStationSupplyPhases.component, ControllerComponentVariables::ChargingStationSupplyPhases.variable.value(), AttributeEnum::Actual, std::to_string(1), "test", true); @@ -944,7 +948,7 @@ TEST_F( auto external_constraints = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::ChargingStationExternalConstraints, create_charge_schedule(ChargingRateUnitEnum::A), {}); - handler.add_profile(STATION_WIDE_ID, external_constraints); + handler.add_profile(external_constraints, STATION_WIDE_ID); auto profile = create_charging_profile( DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxDefaultProfile, @@ -961,7 +965,7 @@ TEST_F(ChargepointTestFixtureV201, create_charge_schedule(ChargingRateUnitEnum::A), uuid(), ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL); - auto sut = handler.add_profile(STATION_WIDE_ID, profile); + auto sut = handler.add_profile(profile, STATION_WIDE_ID); EXPECT_THAT(sut.status, testing::Eq(ChargingProfileStatusEnum::Accepted)); EXPECT_THAT(handler.get_profiles(), testing::Contains(profile)); @@ -974,7 +978,7 @@ TEST_F(ChargepointTestFixtureV201, create_charge_schedule(ChargingRateUnitEnum::A), uuid(), ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL); - auto sut = handler.add_profile(DEFAULT_EVSE_ID, profile); + auto sut = handler.add_profile(profile, DEFAULT_EVSE_ID); EXPECT_THAT(sut.status, testing::Eq(ChargingProfileStatusEnum::Accepted)); EXPECT_THAT(handler.get_profiles(), testing::Contains(profile)); @@ -990,8 +994,8 @@ TEST_F(ChargepointTestFixtureV201, create_charge_schedule(ChargingRateUnitEnum::A), uuid(), ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL); - auto sut1 = handler.add_profile(DEFAULT_EVSE_ID, profile1); - auto sut2 = handler.add_profile(DEFAULT_EVSE_ID, profile2); + auto sut1 = handler.add_profile(profile1, DEFAULT_EVSE_ID); + auto sut2 = handler.add_profile(profile2, DEFAULT_EVSE_ID); auto profiles = handler.get_profiles(); @@ -1010,8 +1014,8 @@ TEST_F(ChargepointTestFixtureV201, create_charge_schedule(ChargingRateUnitEnum::A), uuid(), ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL); - auto sut1 = handler.add_profile(STATION_WIDE_ID, profile1); - auto sut2 = handler.add_profile(DEFAULT_EVSE_ID, profile2); + auto sut1 = handler.add_profile(profile1, STATION_WIDE_ID); + auto sut2 = handler.add_profile(profile2, DEFAULT_EVSE_ID); auto profiles = handler.get_profiles(); @@ -1030,8 +1034,8 @@ TEST_F(ChargepointTestFixtureV201, create_charge_schedule(ChargingRateUnitEnum::A), uuid(), ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL); - auto sut1 = handler.add_profile(DEFAULT_EVSE_ID, profile1); - auto sut2 = handler.add_profile(DEFAULT_EVSE_ID + 1, profile2); + auto sut1 = handler.add_profile(profile1, DEFAULT_EVSE_ID); + auto sut2 = handler.add_profile(profile2, DEFAULT_EVSE_ID + 1); auto profiles = handler.get_profiles(); @@ -1050,8 +1054,8 @@ TEST_F(ChargepointTestFixtureV201, create_charge_schedule(ChargingRateUnitEnum::A), uuid(), ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL); - auto sut1 = handler.add_profile(DEFAULT_EVSE_ID + 1, profile1); - auto sut2 = handler.add_profile(STATION_WIDE_ID, profile2); + auto sut1 = handler.add_profile(profile1, DEFAULT_EVSE_ID + 1); + auto sut2 = handler.add_profile(profile2, STATION_WIDE_ID); auto profiles = handler.get_profiles(); @@ -1070,8 +1074,8 @@ TEST_F(ChargepointTestFixtureV201, create_charge_schedule(ChargingRateUnitEnum::A), uuid(), ChargingProfileKindEnum::Relative, DEFAULT_STACK_LEVEL); - auto sut1 = handler.add_profile(STATION_WIDE_ID, profile1); - auto sut2 = handler.add_profile(STATION_WIDE_ID, profile2); + auto sut1 = handler.add_profile(profile1, STATION_WIDE_ID); + auto sut2 = handler.add_profile(profile2, STATION_WIDE_ID); auto profiles = handler.get_profiles(); @@ -1093,9 +1097,9 @@ TEST_F(ChargepointTestFixtureV201, create_charge_schedule(ChargingRateUnitEnum::A), uuid(), ChargingProfileKindEnum::Recurring, DEFAULT_STACK_LEVEL); - auto sut1 = handler.add_profile(DEFAULT_EVSE_ID, profile1); - auto sut2 = handler.add_profile(DEFAULT_EVSE_ID, profile2); - auto sut3 = handler.add_profile(STATION_WIDE_ID, profile3); + auto sut1 = handler.add_profile(profile1, DEFAULT_EVSE_ID); + auto sut2 = handler.add_profile(profile2, DEFAULT_EVSE_ID); + auto sut3 = handler.add_profile(profile3, STATION_WIDE_ID); auto profiles = handler.get_profiles(); @@ -1115,8 +1119,8 @@ TEST_F(ChargepointTestFixtureV201, create_charge_schedule(ChargingRateUnitEnum::A), uuid(), ChargingProfileKindEnum::Recurring, DEFAULT_STACK_LEVEL); - auto sut4 = handler.add_profile(STATION_WIDE_ID, profile4); - auto sut5 = handler.add_profile(DEFAULT_EVSE_ID, profile5); + auto sut4 = handler.add_profile(profile4, STATION_WIDE_ID); + auto sut5 = handler.add_profile(profile5, DEFAULT_EVSE_ID); profiles = handler.get_profiles(); @@ -1127,4 +1131,41 @@ TEST_F(ChargepointTestFixtureV201, EXPECT_THAT(profiles, testing::Contains(profile5)); } +TEST_F(ChargepointTestFixtureV201, K01_ValidateAndAdd_RejectsInvalidProfiles) { + auto periods = create_charging_schedule_periods(0); + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00"))); + + auto sut = handler.validate_and_add_profile(profile, DEFAULT_EVSE_ID); + auto status_info = sut.statusInfo; + EXPECT_THAT(sut.status, testing::Eq(ChargingProfileStatusEnum::Rejected)); + EXPECT_THAT(status_info->reasonCode.get(), testing::Eq(conversions::profile_validation_result_to_reason_code( + ProfileValidationResultEnum::TxProfileMissingTransactionId))); + + EXPECT_THAT(status_info->additionalInfo.has_value(), testing::IsTrue()); + EXPECT_THAT(status_info->additionalInfo->get(), testing::Eq(conversions::profile_validation_result_to_string( + ProfileValidationResultEnum::TxProfileMissingTransactionId))); + + auto profiles = handler.get_profiles(); + EXPECT_THAT(profiles, testing::Not(testing::Contains(profile))); +} + +TEST_F(ChargepointTestFixtureV201, K01_ValidateAndAdd_AddsValidProfiles) { + auto periods = create_charging_schedule_periods({0, 1, 2}); + + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID); + + auto sut = handler.validate_and_add_profile(profile, DEFAULT_EVSE_ID); + EXPECT_THAT(sut.status, testing::Eq(ChargingProfileStatusEnum::Accepted)); + EXPECT_THAT(sut.statusInfo.has_value(), testing::IsFalse()); + + auto profiles = handler.get_profiles(); + EXPECT_THAT(profiles, testing::Contains(profile)); +} + } // namespace ocpp::v201