Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SetChargingProfileRequest and basic database storage/loading #711

Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
066f26e
smart_charging: Make profile and evse_id order consistent
christopher-davis-afs Jul 8, 2024
9be6660
charge_point: Make `handle_message` protected
christopher-davis-afs Jul 1, 2024
bd312b6
charge_point: Add SmartChargingHandler
christopher-davis-afs Jul 1, 2024
006dcb5
smart_charging: Make SmartChargingHandler mockable
christopher-davis-afs Jul 1, 2024
5ebeede
charge_point: Handle SetChargingProfileRequest
christopher-davis-afs Jul 1, 2024
52759a2
doc: Update SmartCharging status
christopher-davis-afs Jul 3, 2024
957b0e5
charge_point: Test that set_charging_profile callback is called
christopher-davis-afs Jul 16, 2024
c4538f8
added ability to load profile from database
couryrr-afs Jun 17, 2024
dc23c52
charge_point: Reject ChargingStationExternalConstraints profiles in S…
christopher-davis-afs Jul 24, 2024
f42910e
updated database statements for tests
couryrr-afs Jul 29, 2024
ef5c5cf
added columns to database and updated equality checks
couryrr-afs Jul 30, 2024
4786317
smart_charging: Add combined validation and add method
christopher-davis-afs Jul 31, 2024
550b9a8
Updated FR45 to use supplyPhases
Giavotto Aug 1, 2024
48ab57f
database_handler: Correct spelling in database tests
christopher-davis-afs Aug 5, 2024
ac7cdf5
database_handler: Test profile equality
christopher-davis-afs Aug 5, 2024
c10087f
database_handler: Rename function to get all profiles grouped by evse id
christopher-davis-afs Aug 5, 2024
44e41f0
moved charing profile equality to test since it is not a full comparison
couryrr-afs Aug 6, 2024
de40df0
removed duplicate include
couryrr-afs Aug 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE CHARGING_PROFILES;

Check failure on line 1 in config/v201/core_migrations/5_down-charging_profiles_db.sql

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/v201/core_migrations/5_down-charging_profiles_db.sql#L1

Expected SET ANSI_NULLS ON near top of file

Check failure on line 1 in config/v201/core_migrations/5_down-charging_profiles_db.sql

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/v201/core_migrations/5_down-charging_profiles_db.sql#L1

Expected SET QUOTED_IDENTIFIER ON near top of file

Check failure on line 1 in config/v201/core_migrations/5_down-charging_profiles_db.sql

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/v201/core_migrations/5_down-charging_profiles_db.sql#L1

Expected SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED near top of file
7 changes: 7 additions & 0 deletions config/v201/core_migrations/5_up-charging_profiles_db.sql
christopher-davis-afs marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE CHARGING_PROFILES (

Check failure on line 1 in config/v201/core_migrations/5_up-charging_profiles_db.sql

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/v201/core_migrations/5_up-charging_profiles_db.sql#L1

Expected SET ANSI_NULLS ON near top of file

Check failure on line 1 in config/v201/core_migrations/5_up-charging_profiles_db.sql

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/v201/core_migrations/5_up-charging_profiles_db.sql#L1

Expected SET QUOTED_IDENTIFIER ON near top of file

Check failure on line 1 in config/v201/core_migrations/5_up-charging_profiles_db.sql

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/v201/core_migrations/5_up-charging_profiles_db.sql#L1

Expected SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED near top of file

Check failure on line 1 in config/v201/core_migrations/5_up-charging_profiles_db.sql

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/v201/core_migrations/5_up-charging_profiles_db.sql#L1

Expected table to use data compression
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
);
14 changes: 7 additions & 7 deletions doc/ocpp_201_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | | |
christopher-davis-afs marked this conversation as resolved.
Show resolved Hide resolved
| K01.FR.10 | ✅ | |
| K01.FR.11 | | |
| K01.FR.12 | | |
| K01.FR.13 | | |
| K01.FR.14 | ✅ | |
| K01.FR.15 | ✅ | |
| K01.FR.16 | | |
| K01.FR.16 | | |
christopher-davis-afs marked this conversation as resolved.
Show resolved Hide resolved
| 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. |
Expand Down
14 changes: 12 additions & 2 deletions include/ocpp/v201/charge_point.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <ocpp/v201/monitoring_updater.hpp>
#include <ocpp/v201/ocpp_types.hpp>
#include <ocpp/v201/ocsp_updater.hpp>
#include <ocpp/v201/smart_charging.hpp>
#include <ocpp/v201/types.hpp>
#include <ocpp/v201/utils.hpp>

Expand Down Expand Up @@ -52,6 +53,7 @@
#include <ocpp/v201/messages/Reset.hpp>
#include <ocpp/v201/messages/SecurityEventNotification.hpp>
#include <ocpp/v201/messages/SendLocalList.hpp>
#include <ocpp/v201/messages/SetChargingProfile.hpp>
#include <ocpp/v201/messages/SetMonitoringBase.hpp>
#include <ocpp/v201/messages/SetMonitoringLevel.hpp>
#include <ocpp/v201/messages/SetNetworkProfile.hpp>
Expand All @@ -65,6 +67,7 @@
#include <ocpp/v201/messages/UpdateFirmware.hpp>

#include "component_state_manager.hpp"
#include "ocpp/v201/smart_charging.hpp"
christopher-davis-afs marked this conversation as resolved.
Show resolved Hide resolved

namespace ocpp {
namespace v201 {
Expand Down Expand Up @@ -520,8 +523,6 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa
/// device model
void remove_network_connection_profiles_below_actual_security_profile();

void handle_message(const EnhancedMessage<v201::MessageType>& message);

christopher-davis-afs marked this conversation as resolved.
Show resolved Hide resolved
void message_callback(const std::string& message);
void update_aligned_data_interval();

Expand Down Expand Up @@ -719,6 +720,9 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa
void handle_change_availability_req(Call<ChangeAvailabilityRequest> call);
void handle_heartbeat_response(CallResult<HeartbeatResponse> call);

// Functional Block K: Smart Charging
void handle_set_charging_profile_req(Call<SetChargingProfileRequest> call);
christopher-davis-afs marked this conversation as resolved.
Show resolved Hide resolved

// Functional Block L: Firmware management
void handle_firmware_update_req(Call<UpdateFirmwareRequest> call);

Expand Down Expand Up @@ -774,6 +778,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<SmartChargingHandlerInterface> smart_charging_handler;
marcemmers marked this conversation as resolved.
Show resolved Hide resolved

void handle_message(const EnhancedMessage<v201::MessageType>& 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
Expand Down
14 changes: 14 additions & 0 deletions include/ocpp/v201/database_handler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int32_t, std::vector<v201::ChargingProfile>> get_all_charging_profiles_group_by_evse();
};

} // namespace v201
Expand Down
32 changes: 27 additions & 5 deletions include/ocpp/v201/smart_charging.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<DeviceModel>& device_model;
Expand All @@ -65,19 +80,26 @@ class SmartChargingHandler {
std::map<int32_t, std::vector<ChargingProfile>> charging_profiles;

public:
SmartChargingHandler(EvseManagerInterface& evse_manager, std::shared_ptr<DeviceModel>& device_model);
SmartChargingHandler(EvseManagerInterface& evse_manager, std::shared_ptr<DeviceModel>& device_model,
std::shared_ptr<ocpp::v201::DatabaseHandler> 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.
Expand Down Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions lib/ocpp/v201/charge_point.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ ChargePoint::ChargePoint(const std::map<int32_t, int32_t>& 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<SmartChargingHandler>(*this->evse_manager, this->device_model, this->database_handler);

// configure logging
this->configure_message_logging_format(message_log_path);

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1307,6 +1312,9 @@ void ChargePoint::handle_message(const EnhancedMessage<v201::MessageType>& 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;
Expand Down Expand Up @@ -3147,6 +3155,39 @@ void ChargePoint::handle_heartbeat_response(CallResult<HeartbeatResponse> call)
}
}

void ChargePoint::handle_set_charging_profile_req(Call<SetChargingProfileRequest> 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) {
christopher-davis-afs marked this conversation as resolved.
Show resolved Hide resolved
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<SetChargingProfileResponse> call_result(response, call.uniqueId);
this->send<SetChargingProfileResponse>(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<SetChargingProfileResponse> call_result(response, call.uniqueId);
this->send<SetChargingProfileResponse>(call_result);
}

void ChargePoint::handle_firmware_update_req(Call<UpdateFirmwareRequest> 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()) {
Expand Down Expand Up @@ -3818,6 +3859,31 @@ void ChargePoint::execute_change_availability_request(ChangeAvailabilityRequest
}
}

// K01.27 - load profiles from database
void ChargePoint::load_charging_profiles() {
try {
marcemmers marked this conversation as resolved.
Show resolved Hide resolved
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<GetVariableResult>
ChargePoint::get_variables(const std::vector<GetVariableData>& get_variable_data_vector) {
std::vector<GetVariableResult> response;
Expand Down
56 changes: 56 additions & 0 deletions lib/ocpp/v201/database_handler.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest

#include "everest/logging.hpp"
#include <ocpp/common/message_queue.hpp>
#include <ocpp/v201/database_handler.hpp>
#include <ocpp/v201/types.hpp>
Expand Down Expand Up @@ -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<int32_t, std::vector<v201::ChargingProfile>> DatabaseHandler::get_all_charging_profiles_group_by_evse() {
std::map<int32_t, std::vector<v201::ChargingProfile>> 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
Loading
Loading