Skip to content

Commit 1b3d211

Browse files
JB-CompleoCSDR-CompleoCS
authored andcommitted
lib: ocpp/v2/smart_charging: take offline time into account
Signed-off-by: Jan Bruchhaus <jan.bruchhaus@compleo-cs.com>
1 parent ac64716 commit 1b3d211

File tree

8 files changed

+339
-0
lines changed

8 files changed

+339
-0
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
#pragma once
55

6+
#include <utility>
7+
68
#include <ocpp/v2/message_handler.hpp>
79

810
#include <ocpp/v2/evse.hpp>
@@ -305,6 +307,17 @@ class SmartCharging : public SmartChargingInterface {
305307
///
306308
ProfileValidationResultEnum verify_rate_limit(const ChargingProfile& profile);
307309

310+
///
311+
/// \brief Validate ChargingProfile regarding the offline time, only for OCPP 2.1
312+
///
313+
/// When the offline time is higher than maxOfflineDuration, the profile is invalid. When
314+
/// invalidAfterOfflineDuration is set to true, the profile should never be used again an can be cleared (Q11).
315+
///
316+
/// \param profile Charging profile
317+
/// \return Pair of valid and clear flags.
318+
///
319+
std::pair<bool, bool> validate_profile_with_offline_time(const ChargingProfile& profile);
320+
308321
///
309322
/// \brief Check if DCInputPhaseControl is enabled for this evse id.
310323
///

lib/everest/ocpp/lib/ocpp/v2/functional_blocks/smart_charging.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,28 @@ ProfileValidationResultEnum SmartCharging::verify_rate_limit(const ChargingProfi
339339
return result;
340340
}
341341

342+
std::pair<bool, bool> SmartCharging::validate_profile_with_offline_time(const ChargingProfile& profile) {
343+
const auto time_disconnected = this->context.connectivity_manager.get_time_disconnected();
344+
// Being online means the profile is valid
345+
if (!time_disconnected.has_value()) {
346+
return {true, false};
347+
}
348+
349+
// Absent maxOfflineDuration means the profile is valid independent of the offline time
350+
if (!profile.maxOfflineDuration.has_value()) {
351+
return {true, false};
352+
}
353+
354+
// Not being offline for long enough means profile is valid
355+
if (std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - *time_disconnected)
356+
.count() <= *profile.maxOfflineDuration) {
357+
return {true, false};
358+
}
359+
360+
// Profile must be cleared when we are offline for too long and invalidAfterOfflineDuration is set
361+
return {false, profile.invalidAfterOfflineDuration.value_or(false)};
362+
}
363+
342364
bool SmartCharging::has_dc_input_phase_control(const std::int32_t evse_id) const {
343365
if (evse_id == 0) {
344366
for (EvseManagerInterface::EvseIterator it = context.evse_manager.begin(); it != context.evse_manager.end();
@@ -1397,6 +1419,17 @@ SmartCharging::get_valid_profiles_for_evse(std::int32_t evse_id,
13971419

13981420
auto evse_profiles = this->context.database_handler.get_charging_profiles_for_evse(evse_id);
13991421
for (auto profile : evse_profiles) {
1422+
// Q11
1423+
if (const auto [valid, clear] = this->validate_profile_with_offline_time(profile); !valid) {
1424+
if (clear) {
1425+
// Q12
1426+
EVLOG_debug << "Clearing profile with ID: " << profile.id
1427+
<< ", because of it is invalid after offline duration";
1428+
this->context.database_handler.clear_charging_profiles_matching_criteria(profile.id, std::nullopt);
1429+
}
1430+
continue;
1431+
}
1432+
14001433
if (this->conform_and_validate_profile(profile, evse_id) == ProfileValidationResultEnum::Valid and
14011434
std::find(std::begin(purposes_to_ignore), std::end(purposes_to_ignore), profile.chargingProfilePurpose) ==
14021435
std::end(purposes_to_ignore)) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Offline Duration
2+
3+
This scenario layers profiles with maxOfflineDuration and invalidAfterOfflineDuration on top of profiles without.
4+
5+
Used by:
6+
7+
* OfflineDuration_Online
8+
* OfflineDuration_OfflineNotLongEnough
9+
* OfflineDuration_OfflineTooLong
10+
* OfflineDuration_OfflineTooLong_ValidAfterOfflineDuration
11+
* OfflineDuration_OfflineTooLong_InvalidAfterOfflineDuration
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"id": 1,
3+
"chargingProfileKind": "Absolute",
4+
"chargingProfilePurpose": "TxProfile",
5+
"chargingSchedule": [
6+
{
7+
"id": 0,
8+
"chargingRateUnit": "W",
9+
"chargingSchedulePeriod": [
10+
{
11+
"limit": 2000.0,
12+
"numberPhases": 3,
13+
"startPeriod": 0
14+
}
15+
],
16+
"startSchedule": "2024-01-17T18:00:00.000Z"
17+
}
18+
],
19+
"stackLevel": 1,
20+
"transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7"
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"id": 3,
3+
"chargingProfileKind": "Absolute",
4+
"chargingProfilePurpose": "TxProfile",
5+
"chargingSchedule": [
6+
{
7+
"id": 0,
8+
"chargingRateUnit": "W",
9+
"chargingSchedulePeriod": [
10+
{
11+
"limit": 2000.0,
12+
"numberPhases": 3,
13+
"operationMode": "CentralSetpoint",
14+
"setpoint": -2000.0,
15+
"startPeriod": 0
16+
}
17+
],
18+
"startSchedule": "2024-01-17T18:00:00.000Z"
19+
}
20+
],
21+
"invalidAfterOfflineDuration": true,
22+
"maxOfflineDuration": 600,
23+
"stackLevel": 2,
24+
"transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7"
25+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"id": 1,
3+
"chargingProfileKind": "Absolute",
4+
"chargingProfilePurpose": "TxProfile",
5+
"chargingSchedule": [
6+
{
7+
"id": 0,
8+
"chargingRateUnit": "W",
9+
"chargingSchedulePeriod": [
10+
{
11+
"limit": 2000.0,
12+
"numberPhases": 3,
13+
"startPeriod": 0
14+
}
15+
],
16+
"startSchedule": "2024-01-17T18:00:00.000Z"
17+
}
18+
],
19+
"stackLevel": 1,
20+
"transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7"
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"id": 2,
3+
"chargingProfileKind": "Absolute",
4+
"chargingProfilePurpose": "TxProfile",
5+
"chargingSchedule": [
6+
{
7+
"id": 0,
8+
"chargingRateUnit": "W",
9+
"chargingSchedulePeriod": [
10+
{
11+
"limit": 2000.0,
12+
"numberPhases": 3,
13+
"operationMode": "CentralSetpoint",
14+
"setpoint": -2000.0,
15+
"startPeriod": 0
16+
}
17+
],
18+
"startSchedule": "2024-01-17T18:00:00.000Z"
19+
}
20+
],
21+
"maxOfflineDuration": 600,
22+
"stackLevel": 2,
23+
"transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7"
24+
}

lib/everest/ocpp/tests/lib/ocpp/v2/test_composite_schedule.cpp

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "smart_charging_test_utils.hpp"
2929
#include <smart_charging_matchers.hpp>
3030

31+
#include <chrono>
3132
#include <sstream>
3233
#include <vector>
3334

@@ -1248,4 +1249,194 @@ TEST_F(CompositeScheduleTestFixtureV2, ZeroDuration) {
12481249
EXPECT_THAT(result.chargingSchedulePeriod, testing::ElementsAre(PeriodEquals(0, expected_limit)));
12491250
}
12501251

1252+
TEST_F(CompositeScheduleTestFixtureV2, OfflineDuration_Online) {
1253+
this->load_charging_profiles_for_evse(BASE_JSON_PATH_V2 + "/offline_duration/valid_after_offline_duration/",
1254+
DEFAULT_EVSE_ID);
1255+
1256+
evse_manager->open_transaction(DEFAULT_EVSE_ID, TX_ID);
1257+
1258+
EXPECT_CALL(*database_handler, get_charging_profiles_for_evse(testing::_)).Times(testing::AnyNumber());
1259+
EXPECT_CALL(*database_handler, new_statement(testing::_)).Times(testing::AnyNumber());
1260+
EXPECT_CALL(evse_manager->get_mock(1), get_current_phase_type).Times(testing::AnyNumber());
1261+
1262+
const auto start_time = ocpp::DateTime{"2024-01-17T18:00:00"};
1263+
const auto end_time = ocpp::DateTime{"2024-01-18T06:00:00"};
1264+
1265+
// Profile with CentralSetpoint has higher StackLevel and therefore is preferred
1266+
ChargingSchedulePeriod expected_period{};
1267+
expected_period.limit = 2000.0;
1268+
expected_period.numberPhases = 3;
1269+
expected_period.setpoint = -2000.0;
1270+
expected_period.startPeriod = 0;
1271+
1272+
CompositeSchedule expected_schedule{};
1273+
expected_schedule.chargingSchedulePeriod = {expected_period};
1274+
expected_schedule.evseId = DEFAULT_EVSE_ID;
1275+
expected_schedule.duration = 43200;
1276+
expected_schedule.scheduleStart = start_time;
1277+
expected_schedule.chargingRateUnit = ChargingRateUnitEnum::W;
1278+
1279+
const auto actual_schedule = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID,
1280+
ChargingRateUnitEnum::W, false, false);
1281+
1282+
ASSERT_EQ(actual_schedule, expected_schedule);
1283+
}
1284+
1285+
TEST_F(CompositeScheduleTestFixtureV2, OfflineDuration_OfflineNotLongEnough) {
1286+
this->load_charging_profiles_for_evse(BASE_JSON_PATH_V2 + "/offline_duration/valid_after_offline_duration/",
1287+
DEFAULT_EVSE_ID);
1288+
1289+
evse_manager->open_transaction(DEFAULT_EVSE_ID, TX_ID);
1290+
1291+
EXPECT_CALL(*database_handler, get_charging_profiles_for_evse(testing::_)).Times(testing::AnyNumber());
1292+
EXPECT_CALL(*database_handler, new_statement(testing::_)).Times(testing::AnyNumber());
1293+
EXPECT_CALL(evse_manager->get_mock(1), get_current_phase_type).Times(testing::AnyNumber());
1294+
1295+
EXPECT_CALL(connectivity_manager, get_time_disconnected())
1296+
.WillRepeatedly(testing::Return(std::chrono::steady_clock::now() - std::chrono::seconds(300)));
1297+
1298+
const auto start_time = ocpp::DateTime{"2024-01-17T18:00:00"};
1299+
const auto end_time = ocpp::DateTime{"2024-01-18T06:00:00"};
1300+
1301+
// Profile with CentralSetpoint is preferred, because it is still valid in the offline case
1302+
ChargingSchedulePeriod expected_period{};
1303+
expected_period.limit = 2000.0;
1304+
expected_period.numberPhases = 3;
1305+
expected_period.setpoint = -2000.0;
1306+
expected_period.startPeriod = 0;
1307+
1308+
CompositeSchedule expected_schedule{};
1309+
expected_schedule.chargingSchedulePeriod = {expected_period};
1310+
expected_schedule.evseId = DEFAULT_EVSE_ID;
1311+
expected_schedule.duration = 43200;
1312+
expected_schedule.scheduleStart = start_time;
1313+
expected_schedule.chargingRateUnit = ChargingRateUnitEnum::W;
1314+
1315+
const auto actual_schedule = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID,
1316+
ChargingRateUnitEnum::W, false, false);
1317+
1318+
ASSERT_EQ(actual_schedule, expected_schedule);
1319+
}
1320+
1321+
TEST_F(CompositeScheduleTestFixtureV2, OfflineDuration_OfflineTooLong) {
1322+
this->load_charging_profiles_for_evse(BASE_JSON_PATH_V2 + "/offline_duration/valid_after_offline_duration/",
1323+
DEFAULT_EVSE_ID);
1324+
1325+
evse_manager->open_transaction(DEFAULT_EVSE_ID, TX_ID);
1326+
1327+
EXPECT_CALL(*database_handler, get_charging_profiles_for_evse(testing::_)).Times(testing::AnyNumber());
1328+
EXPECT_CALL(*database_handler, new_statement(testing::_)).Times(testing::AnyNumber());
1329+
EXPECT_CALL(evse_manager->get_mock(1), get_current_phase_type).Times(testing::AnyNumber());
1330+
1331+
EXPECT_CALL(connectivity_manager, get_time_disconnected())
1332+
.WillRepeatedly(testing::Return(std::chrono::steady_clock::now() - std::chrono::seconds(900)));
1333+
1334+
const auto start_time = ocpp::DateTime{"2024-01-17T18:00:00"};
1335+
const auto end_time = ocpp::DateTime{"2024-01-18T06:00:00"};
1336+
1337+
// Profile with ChargingOnly is preferred, because the other one is invalid now that we have been offline for too
1338+
// long
1339+
ChargingSchedulePeriod expected_period{};
1340+
expected_period.limit = 2000.0;
1341+
expected_period.numberPhases = 3;
1342+
expected_period.startPeriod = 0;
1343+
1344+
CompositeSchedule expected_schedule{};
1345+
expected_schedule.chargingSchedulePeriod = {expected_period};
1346+
expected_schedule.evseId = DEFAULT_EVSE_ID;
1347+
expected_schedule.duration = 43200;
1348+
expected_schedule.scheduleStart = start_time;
1349+
expected_schedule.chargingRateUnit = ChargingRateUnitEnum::W;
1350+
1351+
const auto actual_schedule = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID,
1352+
ChargingRateUnitEnum::W, false, false);
1353+
1354+
ASSERT_EQ(actual_schedule, expected_schedule);
1355+
}
1356+
1357+
TEST_F(CompositeScheduleTestFixtureV2, OfflineDuration_OfflineTooLong_ValidAfterOfflineDuration) {
1358+
this->load_charging_profiles_for_evse(BASE_JSON_PATH_V2 + "/offline_duration/valid_after_offline_duration/",
1359+
DEFAULT_EVSE_ID);
1360+
1361+
evse_manager->open_transaction(DEFAULT_EVSE_ID, TX_ID);
1362+
1363+
EXPECT_CALL(*database_handler, get_charging_profiles_for_evse(testing::_)).Times(testing::AnyNumber());
1364+
EXPECT_CALL(*database_handler, new_statement(testing::_)).Times(testing::AnyNumber());
1365+
EXPECT_CALL(evse_manager->get_mock(1), get_current_phase_type).Times(testing::AnyNumber());
1366+
1367+
EXPECT_CALL(connectivity_manager, get_time_disconnected())
1368+
.WillRepeatedly(testing::Return(std::chrono::steady_clock::now() - std::chrono::seconds(900)));
1369+
1370+
const auto start_time = ocpp::DateTime{"2024-01-17T18:00:00"};
1371+
const auto end_time = ocpp::DateTime{"2024-01-18T06:00:00"};
1372+
1373+
// Profile with ChargingOnly is preferred after being offline for too long
1374+
ChargingSchedulePeriod expected_period{};
1375+
expected_period.limit = 2000.0;
1376+
expected_period.numberPhases = 3;
1377+
expected_period.startPeriod = 0;
1378+
1379+
CompositeSchedule expected_schedule{};
1380+
expected_schedule.chargingSchedulePeriod = {expected_period};
1381+
expected_schedule.evseId = DEFAULT_EVSE_ID;
1382+
expected_schedule.duration = 43200;
1383+
expected_schedule.scheduleStart = start_time;
1384+
expected_schedule.chargingRateUnit = ChargingRateUnitEnum::W;
1385+
1386+
auto actual_schedule = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID,
1387+
ChargingRateUnitEnum::W, false, false);
1388+
1389+
testing::Mock::VerifyAndClearExpectations(&connectivity_manager);
1390+
1391+
ASSERT_EQ(actual_schedule, expected_schedule);
1392+
1393+
EXPECT_CALL(connectivity_manager, get_time_disconnected()).WillRepeatedly(testing::Return(std::nullopt));
1394+
1395+
// Profile with CentralSetpoint is preferred after being online again
1396+
expected_period.setpoint = -2000.0;
1397+
expected_schedule.chargingSchedulePeriod = {expected_period};
1398+
1399+
actual_schedule = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID,
1400+
ChargingRateUnitEnum::W, false, false);
1401+
1402+
ASSERT_EQ(actual_schedule, expected_schedule);
1403+
}
1404+
1405+
TEST_F(CompositeScheduleTestFixtureV2, OfflineDuration_OfflineTooLong_InvalidAfterOfflineDuration) {
1406+
this->load_charging_profiles_for_evse(BASE_JSON_PATH_V2 + "/offline_duration/invalid_after_offline_duration/",
1407+
DEFAULT_EVSE_ID);
1408+
1409+
evse_manager->open_transaction(DEFAULT_EVSE_ID, TX_ID);
1410+
1411+
EXPECT_CALL(*database_handler, get_charging_profiles_for_evse(testing::_)).Times(testing::AnyNumber());
1412+
EXPECT_CALL(*database_handler, new_statement(testing::_)).Times(testing::AnyNumber());
1413+
EXPECT_CALL(evse_manager->get_mock(1), get_current_phase_type).Times(testing::AnyNumber());
1414+
1415+
EXPECT_CALL(connectivity_manager, get_time_disconnected())
1416+
.WillRepeatedly(testing::Return(std::chrono::steady_clock::now() - std::chrono::seconds(900)));
1417+
1418+
// Profile with invalidAfterOfflineDuration gets cleared when being offline for too long
1419+
EXPECT_CALL(*database_handler, clear_charging_profiles_matching_criteria(std::optional<int>{3}, testing::_));
1420+
1421+
const auto start_time = ocpp::DateTime{"2024-01-17T18:00:00"};
1422+
const auto end_time = ocpp::DateTime{"2024-01-18T06:00:00"};
1423+
1424+
ChargingSchedulePeriod expected_period{};
1425+
expected_period.limit = 2000.0;
1426+
expected_period.numberPhases = 3;
1427+
expected_period.startPeriod = 0;
1428+
1429+
CompositeSchedule expected_schedule{};
1430+
expected_schedule.chargingSchedulePeriod = {expected_period};
1431+
expected_schedule.evseId = DEFAULT_EVSE_ID;
1432+
expected_schedule.duration = 43200;
1433+
expected_schedule.scheduleStart = start_time;
1434+
expected_schedule.chargingRateUnit = ChargingRateUnitEnum::W;
1435+
1436+
auto actual_schedule = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID,
1437+
ChargingRateUnitEnum::W, false, false);
1438+
1439+
ASSERT_EQ(actual_schedule, expected_schedule);
1440+
}
1441+
12511442
} // namespace ocpp::v2

0 commit comments

Comments
 (0)