diff --git a/config/v201/core_migrations/1_up-initial.sql b/config/v201/core_migrations/1_up-initial.sql index bced0048c9..7c43eaf7a3 100644 --- a/config/v201/core_migrations/1_up-initial.sql +++ b/config/v201/core_migrations/1_up-initial.sql @@ -86,3 +86,9 @@ CREATE TABLE METER_VALUE_ITEMS ( ENCODING_METHOD TEXT, PUBLIC_KEY TEXT ); + +CREATE TABLE CHARGING_PROFILES ( + ID INT PRIMARY KEY NOT NULL, + EVSE_ID INT NOT NULL, + PROFILE TEXT NOT NULL +); diff --git a/doc/ocpp_201_status.md b/doc/ocpp_201_status.md index 9846ad6cf5..d12d756576 100644 --- a/doc/ocpp_201_status.md +++ b/doc/ocpp_201_status.md @@ -1252,7 +1252,7 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | K01.FR.21 | | | | K01.FR.22 | | | | K01.FR.26 | ✅ | | -| K01.FR.27 | | | +| K01.FR.27 | ✅ | | | K01.FR.28 | ✅ | | | K01.FR.29 | | | | K01.FR.30 | | | diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index 4484d7450b..b6c4f76b6b 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -67,6 +67,7 @@ #include #include "component_state_manager.hpp" +#include "ocpp/v201/smart_charging.hpp" namespace ocpp { namespace v201 { @@ -781,6 +782,7 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa std::shared_ptr smart_charging_handler; void handle_message(const EnhancedMessage& message); + void load_charging_profiles(); public: /// \brief Construct a new ChargePoint object diff --git a/include/ocpp/v201/database_handler.hpp b/include/ocpp/v201/database_handler.hpp index ae7117b78c..ad6495d8dd 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 + virtual void insert_or_update_charging_profile(const int evse_id, const v201::ChargingProfile& profile); + + /// \brief Deletes the profile with the given \p profile_id + virtual void delete_charging_profile(const int profile_id); + + /// \brief Deletes all profiles from table CHARGING_PROFILES + void delete_charging_profiles(); + + /// \brief Retrieves all ChargingProfiles + virtual std::map> get_all_charging_profiles_by_evse(); }; } // namespace v201 diff --git a/include/ocpp/v201/smart_charging.hpp b/include/ocpp/v201/smart_charging.hpp index 256da28ff4..8ac8457c41 100644 --- a/include/ocpp/v201/smart_charging.hpp +++ b/include/ocpp/v201/smart_charging.hpp @@ -80,7 +80,8 @@ class SmartChargingHandler : public SmartChargingHandlerInterface { 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. diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index c56320b0a1..c07b124284 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -178,7 +178,8 @@ 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->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); @@ -220,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) { @@ -3833,6 +3836,36 @@ 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_by_evse(); + EVLOG_info << "Found " << evses.size() << " evse in the database"; + for (auto& profiles : evses) { + try { + auto evse_id = profiles.first; + for (auto profile : profiles.second) { + 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 (common::RequiredEntryNotFoundException& e) { + EVLOG_warning << "Could not get connector id from database: " << e.what(); + } catch (const QueryExecutionException& e) { + EVLOG_warning << "Could not get connector id from database: " << e.what(); + } + } + } catch (const QueryExecutionException& e) { + EVLOG_warning << "Could not load charging profiles from database: " << 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 bb83a652f7..1dd9b9dbdc 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,57 @@ 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, PROFILE) VALUES " + "(@id, @evse_id, @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_text("@profile", json_profile.dump(), SQLiteString::Transient); + + if (stmt->step() != SQLITE_DONE) { + EVLOG_error << "Could not insert into table: " << this->database->get_error_message(); + throw std::runtime_error("db access error"); + } +} + +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) { + EVLOG_error << "Could not delete from table: " << this->database->get_error_message(); + } +} + +void DatabaseHandler::delete_charging_profiles() { + this->database->clear_table("CHARGING_PROFILES"); +} + +std::map> DatabaseHandler::get_all_charging_profiles_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 15ee860cad..4d6a70f339 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" @@ -141,8 +142,9 @@ SmartChargingHandlerInterface::~SmartChargingHandlerInterface() { } 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) { } ProfileValidationResultEnum SmartChargingHandler::validate_profile(ChargingProfile& profile, int32_t evse_id) { diff --git a/tests/lib/ocpp/v201/CMakeLists.txt b/tests/lib/ocpp/v201/CMakeLists.txt index 8ab50a805e..97c2714f44 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/test_database_handler.cpp b/tests/lib/ocpp/v201/test_database_handler.cpp index 17844462f3..1e169e2d7b 100644 --- a/tests/lib/ocpp/v201/test_database_handler.cpp +++ b/tests/lib/ocpp/v201/test_database_handler.cpp @@ -144,4 +144,184 @@ 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{1, 1}); + std::string sql = "SELECT COUNT(*) FROM CHARGING_PROFILES"; + auto select_stmt = this->database->new_statement(sql); + + ASSERT_TRUE(select_stmt->step() == SQLITE_ROW); + auto count = select_stmt->column_int(0); + ASSERT_EQ(count, 1); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithProfileData_UpdateProfile) { + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{2, 1}); + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{2, 2}); + + std::string sql = "SELECT COUNT(*) FROM CHARGING_PROFILES"; + auto select_stmt = this->database->new_statement(sql); + + ASSERT_TRUE(select_stmt->step() == SQLITE_ROW); + + auto count = select_stmt->column_int(0); + ASSERT_EQ(count, 1); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithProfileData_InsertNewProfile) { + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{1, 1}); + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{2, 1}); + + std::string sql = "SELECT COUNT(*) FROM CHARGING_PROFILES"; + auto select_stmt = this->database->new_statement(sql); + + ASSERT_TRUE(select_stmt->step() == SQLITE_ROW); + + auto count = select_stmt->column_int(0); + ASSERT_EQ(count, 2); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithProfileData_DeleteRemovesSpecifiedProfiles) { + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{.id = 1, .stackLevel = 1}); + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{.id = 2, .stackLevel = 1}); + + auto sql = "SELECT COUNT(*) FROM CHARGING_PROFILES"; + + auto select_stmt = this->database->new_statement(sql); + + do { + ASSERT_TRUE(select_stmt->step() == SQLITE_ROW); + auto count = select_stmt->column_int(0); + ASSERT_EQ(count, 2); + } while (select_stmt->step() != SQLITE_DONE); + + this->database_handler.delete_charging_profile(1); + + do { + ASSERT_TRUE(select_stmt->step() == SQLITE_ROW); + auto count = select_stmt->column_int(0); + ASSERT_EQ(count, 1); + } while (select_stmt->step() != SQLITE_DONE); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithProfileData_DeleteAllRemovesAllProfiles) { + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{.id = 1, .stackLevel = 1}); + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{.id = 2, .stackLevel = 1}); + + auto sql = "SELECT COUNT(*) FROM CHARGING_PROFILES"; + + auto select_stmt = this->database->new_statement(sql); + + do { + ASSERT_TRUE(select_stmt->step() == SQLITE_ROW); + auto count = select_stmt->column_int(0); + ASSERT_EQ(count, 2); + } while (select_stmt->step() != SQLITE_DONE); + + this->database_handler.delete_charging_profiles(); + + do { + ASSERT_TRUE(select_stmt->step() == SQLITE_ROW); + auto count = select_stmt->column_int(0); + ASSERT_EQ(count, 0); + } while (select_stmt->step() != SQLITE_DONE); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithNoProfileData_DeleteAllDoesNotFail) { + + auto sql = "SELECT COUNT(*) FROM CHARGING_PROFILES"; + + auto select_stmt = this->database->new_statement(sql); + + do { + ASSERT_TRUE(select_stmt->step() == SQLITE_ROW); + auto count = select_stmt->column_int(0); + ASSERT_EQ(count, 0); + } while (select_stmt->step() != SQLITE_DONE); + + this->database_handler.delete_charging_profiles(); + + do { + ASSERT_TRUE(select_stmt->step() == SQLITE_ROW); + auto count = select_stmt->column_int(0); + ASSERT_EQ(count, 0); + } while (select_stmt->step() != SQLITE_DONE); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseNoProfileData_DeleteAllDoesNotFail) { + auto sql = "SELECT COUNT(*) FROM CHARGING_PROFILES"; + + auto select_stmt = this->database->new_statement(sql); + + do { + ASSERT_TRUE(select_stmt->step() == SQLITE_ROW); + auto count = select_stmt->column_int(0); + ASSERT_EQ(count, 0); + } while (select_stmt->step() != SQLITE_DONE); + + this->database_handler.delete_charging_profiles(); + + do { + ASSERT_TRUE(select_stmt->step() == SQLITE_ROW); + auto count = select_stmt->column_int(0); + ASSERT_EQ(count, 0); + } while (select_stmt->step() != SQLITE_DONE); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithSingleProfileData_LoadsCharingProfile) { + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{1, 1}); + + auto sut = this->database_handler.get_all_charging_profiles_by_evse(); + + ASSERT_EQ(sut.size(), 1); + + // The evse id is found + ASSERT_TRUE(sut.find(1) != sut.end()); + + auto profiles = sut[1]; + + ASSERT_EQ(profiles.size(), 1); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithMultipleProfileSameEvse_LoadsCharingProfile) { + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{.id = 1, .stackLevel = 1}); + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{.id = 2, .stackLevel = 2}); + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{.id = 3, .stackLevel = 3}); + + auto sut = this->database_handler.get_all_charging_profiles_by_evse(); + + ASSERT_EQ(sut.size(), 1); + + // The evse id is found + ASSERT_TRUE(sut.find(1) != sut.end()); + + auto profiles = sut[1]; + + ASSERT_EQ(profiles.size(), 3); +} + +TEST_F(DatabaseHandlerTest, KO1_FR27_DatabaseWithMultipleProfileDiffEvse_LoadsCharingProfile) { + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{.id = 1, .stackLevel = 1}); + this->database_handler.insert_or_update_charging_profile(1, ChargingProfile{.id = 2, .stackLevel = 2}); + this->database_handler.insert_or_update_charging_profile(2, ChargingProfile{.id = 3, .stackLevel = 3}); + this->database_handler.insert_or_update_charging_profile(2, ChargingProfile{.id = 4, .stackLevel = 4}); + this->database_handler.insert_or_update_charging_profile(3, ChargingProfile{.id = 5, .stackLevel = 5}); + this->database_handler.insert_or_update_charging_profile(3, ChargingProfile{.id = 6, .stackLevel = 6}); + + auto sut = this->database_handler.get_all_charging_profiles_by_evse(); + + ASSERT_EQ(sut.size(), 3); + + ASSERT_TRUE(sut.find(1) != sut.end()); + ASSERT_TRUE(sut.find(2) != sut.end()); + ASSERT_TRUE(sut.find(3) != sut.end()); + + auto profiles1 = sut[1]; + auto profiles2 = sut[2]; + auto profiles3 = sut[3]; + + ASSERT_EQ(profiles1.size(), 2); + ASSERT_EQ(profiles2.size(), 2); + ASSERT_EQ(profiles3.size(), 2); +} diff --git a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp index fa0ef65ef0..b22d160ce0 100644 --- a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp +++ b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp @@ -189,7 +189,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() {