Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
12 changes: 12 additions & 0 deletions lib/everest/ocpp/doc/v2/ocpp_2x_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -2705,6 +2705,18 @@ CSMS setpoint and Dynamic charging profiles from K01. There are no specific requ
| Q10.FR.04 | | |
| Q10.FR.05 | | |

## Bidirectional Power Transfer - Going offline during V2X operation

| ID | Status | Remark |
| --- | ------ | ------ |
| Q11 | ✅ | |

## Bidirectional Power Transfer - Resuming a V2X operation after an offline period

| ID | Status | Remark |
| --- | ------ | ------ |
| Q12 | ✅ | |

## DER Control - Starting a V2X session with DER control in EVSE (New in OCPP 2.1)

| ID | Status | Remark |
Expand Down
2 changes: 0 additions & 2 deletions lib/everest/ocpp/include/ocpp/v2/charge_point.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -399,8 +399,6 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa
}
};

std::chrono::time_point<std::chrono::steady_clock> time_disconnected;

// callback struct
Callbacks callbacks;

Expand Down
10 changes: 10 additions & 0 deletions lib/everest/ocpp/include/ocpp/v2/connectivity_manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
#include <ocpp/v2/messages/SetNetworkProfile.hpp>
#include <ocpp/v2/ocpp_types.hpp>

#include <chrono>

Check warning on line 10 in lib/everest/ocpp/include/ocpp/v2/connectivity_manager.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/everest/ocpp/include/ocpp/v2/connectivity_manager.hpp#L10

Include file: <chrono> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <functional>
#include <future>
#include <optional>

namespace ocpp {
namespace v2 {

Expand Down Expand Up @@ -84,6 +86,11 @@
///
virtual bool is_websocket_connected() = 0;

/// @brief Get the time the websocket has been disconnected.
/// @return The time the websocket has been disconnected
///
virtual std::optional<std::chrono::time_point<std::chrono::steady_clock>> get_time_disconnected() const = 0;

/// \brief Connect to the websocket
/// \param configuration_slot Optional the network_profile_slot to connect to. std::nullopt will select the slot
/// internally.
Expand Down Expand Up @@ -168,6 +175,7 @@
std::optional<std::int32_t> get_priority_from_configuration_slot(const int configuration_slot) const override;
const std::vector<int>& get_network_connection_slots() const override;
bool is_websocket_connected() override;
std::optional<std::chrono::time_point<std::chrono::steady_clock>> get_time_disconnected() const override;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should wrap this in a monitor as it can now be used from multiple threads

void connect(std::optional<std::int32_t> network_profile_slot = std::nullopt) override;
void disconnect() override;
bool send_to_websocket(const std::string& message) override;
Expand All @@ -176,6 +184,8 @@
void confirm_successful_connection() override;

private:
std::optional<std::chrono::time_point<std::chrono::steady_clock>> time_disconnected{};

/// \brief Initializes the websocket and tries to connect
///
void try_connect_websocket();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#pragma once

#include <utility>

Check warning on line 6 in lib/everest/ocpp/include/ocpp/v2/functional_blocks/smart_charging.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/everest/ocpp/include/ocpp/v2/functional_blocks/smart_charging.hpp#L6

Include file: <utility> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include <ocpp/v2/message_handler.hpp>

#include <ocpp/v2/evse.hpp>
Expand Down Expand Up @@ -305,6 +307,17 @@
///
ProfileValidationResultEnum verify_rate_limit(const ChargingProfile& profile);

///
/// \brief Validate ChargingProfile regarding the offline time, only for OCPP 2.1
///
/// When the offline time is higher than maxOfflineDuration, the profile is invalid. When
/// invalidAfterOfflineDuration is set to true, the profile should never be used again an can be cleared (Q11).
///
/// \param profile Charging profile
/// \return Pair of valid and clear flags.
///
std::pair<bool, bool> validate_profile_with_offline_time(const ChargingProfile& profile);

///
/// \brief Check if DCInputPhaseControl is enabled for this evse id.
///
Expand Down
12 changes: 3 additions & 9 deletions lib/everest/ocpp/lib/ocpp/v2/charge_point.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1092,10 +1092,11 @@ void ChargePoint::websocket_connected_callback(const int configuration_slot,
if (this->registration_status == RegistrationStatusEnum::Accepted) {
this->connectivity_manager->confirm_successful_connection();

if (this->time_disconnected.time_since_epoch() != 0s) {
if (const auto time_disconnected = this->connectivity_manager->get_time_disconnected();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the check on the variable here since we always set time disconnected

time_disconnected.has_value()) {
// handle offline threshold
// Get the current time point using steady_clock
auto offline_duration = std::chrono::steady_clock::now() - this->time_disconnected;
const auto offline_duration = std::chrono::steady_clock::now() - time_disconnected.value();

// B04.FR.01
// If offline period exceeds offline threshold then send the status notification for all connectors
Expand All @@ -1113,7 +1114,6 @@ void ChargePoint::websocket_connected_callback(const int configuration_slot,
this->security->init_certificate_expiration_check_timers(); // re-init as timers are stopped on disconnect
}
}
this->time_disconnected = std::chrono::time_point<std::chrono::steady_clock>();

// We have a connection again so next time it fails we should send the notification again
this->skip_invalid_csms_certificate_notifications = false;
Expand All @@ -1128,12 +1128,6 @@ void ChargePoint::websocket_disconnected_callback(const int configuration_slot,
const NetworkConnectionProfile& network_connection_profile) {
this->message_queue->pause();

// check if offline threshold has been defined
if (this->device_model->get_value<int>(ControllerComponentVariables::OfflineThreshold) != 0) {
// Get the current time point using steady_clock
this->time_disconnected = std::chrono::steady_clock::now();
}
Comment on lines -1132 to -1135
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a regression on this functionality as we now always set the time disconnected. We should most likely remove the optional from time_disconnected and a check on this variable when we do the reconnection check. See other comment


this->security->stop_certificate_expiration_check_timers();
if (this->callbacks.connection_state_changed_callback.has_value()) {
this->callbacks.connection_state_changed_callback.value()(false, configuration_slot, network_connection_profile,
Expand Down
8 changes: 8 additions & 0 deletions lib/everest/ocpp/lib/ocpp/v2/connectivity_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ bool ConnectivityManager::is_websocket_connected() {
return this->websocket != nullptr && this->websocket->is_connected();
}

std::optional<std::chrono::time_point<std::chrono::steady_clock>> ConnectivityManager::get_time_disconnected() const {
return this->time_disconnected;
}

void ConnectivityManager::connect(std::optional<std::int32_t> network_profile_slot) {
if (this->network_connection_slots.empty()) {
EVLOG_warning << "No network connection profiles configured, aborting websocket connection.";
Expand Down Expand Up @@ -406,11 +410,15 @@ void ConnectivityManager::on_websocket_connected(OcppProtocolVersion protocol) {
this->websocket_connected_callback.value()(actual_configuration_slot, network_connection_profile.value(),
this->connected_ocpp_version);
}
this->time_disconnected = std::nullopt;
}

void ConnectivityManager::on_websocket_disconnected() {
std::optional<NetworkConnectionProfile> network_connection_profile =
this->get_network_connection_profile(this->get_active_network_configuration_slot());
if (!this->time_disconnected.has_value()) {
this->time_disconnected = std::chrono::steady_clock::now();
}

if (this->websocket_disconnected_callback.has_value() and network_connection_profile.has_value()) {
this->websocket_disconnected_callback.value()(this->get_active_network_configuration_slot(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,28 @@ ProfileValidationResultEnum SmartCharging::verify_rate_limit(const ChargingProfi
return result;
}

std::pair<bool, bool> SmartCharging::validate_profile_with_offline_time(const ChargingProfile& profile) {
const auto time_disconnected = this->context.connectivity_manager.get_time_disconnected();
// Being online means the profile is valid
if (!time_disconnected.has_value()) {
return {true, false};
}

// Absent maxOfflineDuration means the profile is valid independent of the offline time
if (!profile.maxOfflineDuration.has_value()) {
return {true, false};
}

// Not being offline for long enough means profile is valid
if (std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - time_disconnected.value())
.count() <= profile.maxOfflineDuration.value()) {
return {true, false};
}

// Profile must be cleared when we are offline for too long and invalidAfterOfflineDuration is set
return {false, profile.invalidAfterOfflineDuration.value_or(false)};
}

bool SmartCharging::has_dc_input_phase_control(const std::int32_t evse_id) const {
if (evse_id == 0) {
for (EvseManagerInterface::EvseIterator it = context.evse_manager.begin(); it != context.evse_manager.end();
Expand Down Expand Up @@ -1397,6 +1419,17 @@ SmartCharging::get_valid_profiles_for_evse(std::int32_t evse_id,

auto evse_profiles = this->context.database_handler.get_charging_profiles_for_evse(evse_id);
for (auto profile : evse_profiles) {
// Q11
if (const auto [valid, clear] = this->validate_profile_with_offline_time(profile); !valid) {
if (clear) {
// Q12
EVLOG_debug << "Clearing profile with ID: " << profile.id
<< ", because it is invalid after offline duration";
this->context.database_handler.clear_charging_profiles_matching_criteria(profile.id, std::nullopt);
}
continue;
}

if (this->conform_and_validate_profile(profile, evse_id) == ProfileValidationResultEnum::Valid and
std::find(std::begin(purposes_to_ignore), std::end(purposes_to_ignore), profile.chargingProfilePurpose) ==
std::end(purposes_to_ignore)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Offline Duration

Check notice on line 1 in lib/everest/ocpp/tests/lib/ocpp/v2/json/offline_duration/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/everest/ocpp/tests/lib/ocpp/v2/json/offline_duration/README.md#L1

Expected: [None]; Actual: # Offline Duration

This scenario layers profiles with maxOfflineDuration and invalidAfterOfflineDuration on top of profiles without.

Check notice on line 3 in lib/everest/ocpp/tests/lib/ocpp/v2/json/offline_duration/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/everest/ocpp/tests/lib/ocpp/v2/json/offline_duration/README.md#L3

Expected: 80; Actual: 113

Used by:

* OfflineDuration_Online
* OfflineDuration_OfflineNotLongEnough
* OfflineDuration_OfflineTooLong
* OfflineDuration_OfflineTooLong_ValidAfterOfflineDuration
* OfflineDuration_OfflineTooLong_InvalidAfterOfflineDuration
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"id": 1,
"chargingProfileKind": "Absolute",
"chargingProfilePurpose": "TxProfile",
"chargingSchedule": [
{
"id": 0,
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{
"limit": 2000.0,
"numberPhases": 3,
"startPeriod": 0
}
],
"startSchedule": "2024-01-17T18:00:00.000Z"
}
],
"stackLevel": 1,
"transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"id": 3,
"chargingProfileKind": "Absolute",
"chargingProfilePurpose": "TxProfile",
"chargingSchedule": [
{
"id": 0,
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{
"limit": 2000.0,
"numberPhases": 3,
"operationMode": "CentralSetpoint",
"setpoint": -2000.0,
"startPeriod": 0
}
],
"startSchedule": "2024-01-17T18:00:00.000Z"
}
],
"invalidAfterOfflineDuration": true,
"maxOfflineDuration": 600,
"stackLevel": 2,
"transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"id": 1,
"chargingProfileKind": "Absolute",
"chargingProfilePurpose": "TxProfile",
"chargingSchedule": [
{
"id": 0,
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{
"limit": 2000.0,
"numberPhases": 3,
"startPeriod": 0
}
],
"startSchedule": "2024-01-17T18:00:00.000Z"
}
],
"stackLevel": 1,
"transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"id": 2,
"chargingProfileKind": "Absolute",
"chargingProfilePurpose": "TxProfile",
"chargingSchedule": [
{
"id": 0,
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{
"limit": 2000.0,
"numberPhases": 3,
"operationMode": "CentralSetpoint",
"setpoint": -2000.0,
"startPeriod": 0
}
],
"startSchedule": "2024-01-17T18:00:00.000Z"
}
],
"maxOfflineDuration": 600,
"stackLevel": 2,
"transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7"
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ConnectivityManagerMock : public ConnectivityManagerInterface {
(const));
MOCK_METHOD(const std::vector<int>&, get_network_connection_slots, (), (const));
MOCK_METHOD(bool, is_websocket_connected, ());
MOCK_METHOD(std::optional<std::chrono::time_point<std::chrono::steady_clock>>, get_time_disconnected, (), (const));
MOCK_METHOD(void, connect, (std::optional<std::int32_t> network_profile_slot));
MOCK_METHOD(void, disconnect, ());
MOCK_METHOD(bool, send_to_websocket, (const std::string& message));
Expand Down
Loading
Loading