Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ bool ChargingSchedule::calculateLimit(const Timestamp &t, const Timestamp &start
for (auto period = chargingSchedulePeriod.begin(); period != chargingSchedulePeriod.end(); period++) {
if (period->startPeriod > t_toBasis) {
// found the first period that comes after t_toBasis.
nextChange = basis + period->startPeriod;
nextChange = std::min(nextChange, basis + period->startPeriod);
const Timestamp candidate = basis + period->startPeriod;
nextChange = std::min(nextChange, candidate);
break; //The currently valid limit was set the iteration before
}
limit_res = period->limit;
Expand Down
53 changes: 32 additions & 21 deletions src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ void SmartChargingConnector::calculateLimit(const Timestamp &t, ChargeRate& limi
}

//if no TxProfile limits charging, check the TxDefaultProfiles for this connector
if (!txLimitDefined && trackTxStart < MAX_TIME) {
if (!txLimitDefined) {
for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) {
if (TxDefaultProfile[i]) {
ChargeRate crOut;
Expand All @@ -68,7 +68,7 @@ void SmartChargingConnector::calculateLimit(const Timestamp &t, ChargeRate& limi
}

//if no appropriate TxDefaultProfile is set for this connector, search in the general TxDefaultProfiles
if (!txLimitDefined && trackTxStart < MAX_TIME) {
if (!txLimitDefined) {
for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) {
if (ChargePointTxDefaultProfile[i]) {
ChargeRate crOut;
Expand Down Expand Up @@ -168,19 +168,19 @@ void SmartChargingConnector::loop(){
MO_DBG_INFO("New limit for connector %u, scheduled at = %s, nextChange = %s, limit = {%.1f, %.1f, %i}",
connectorId,
timestamp1, timestamp2,
limit.power != std::numeric_limits<float>::max() ? limit.power : -1.f,
limit.current != std::numeric_limits<float>::max() ? limit.current : -1.f,
limit.nphases != std::numeric_limits<int>::max() ? limit.nphases : -1);
limit.power != std::numeric_limits<float>::max() ? limit.power : MO_MaxChargingLimitPower,
limit.current != std::numeric_limits<float>::max() ? limit.current : MO_MaxChargingLimitCurrent,
limit.nphases != std::numeric_limits<int>::max() ? limit.nphases : MO_MaxChargingLimitNumberPhases);
}
#endif

if (trackLimitOutput != limit) {
if (limitOutput) {

limitOutput(
limit.power != std::numeric_limits<float>::max() ? limit.power : -1.f,
limit.current != std::numeric_limits<float>::max() ? limit.current : -1.f,
limit.nphases != std::numeric_limits<int>::max() ? limit.nphases : -1);
limit.power != std::numeric_limits<float>::max() ? limit.power : MO_MaxChargingLimitPower,
limit.current != std::numeric_limits<float>::max() ? limit.current : MO_MaxChargingLimitCurrent,
limit.nphases != std::numeric_limits<int>::max() ? limit.nphases : MO_MaxChargingLimitNumberPhases);
trackLimitOutput = limit;
}
}
Expand Down Expand Up @@ -252,6 +252,9 @@ std::unique_ptr<ChargingSchedule> SmartChargingConnector::getCompositeSchedule(i
Timestamp periodBegin = Timestamp(startSchedule);
Timestamp periodStop = Timestamp(startSchedule);

//remember last effective limit we actually *emitted*
ChargeRate lastLimit;

while (periodBegin - startSchedule < duration && periods.size() < MO_ChargingScheduleMaxPeriods) {

//calculate limit
Expand All @@ -267,11 +270,16 @@ std::unique_ptr<ChargingSchedule> SmartChargingConnector::getCompositeSchedule(i
}
}

periods.emplace_back();
float limit_opt = unit == ChargingRateUnitType_Optional::Watt ? limit.power : limit.current;
periods.back().limit = limit_opt != std::numeric_limits<float>::max() ? limit_opt : -1.f,
periods.back().numberPhases = limit.nphases != std::numeric_limits<int>::max() ? limit.nphases : -1;
periods.back().startPeriod = periodBegin - startSchedule;
//coalesce: only push when the effective limit actually *changes*
if (periods.empty() || limit != lastLimit) {
periods.emplace_back();
float limit_opt = unit == ChargingRateUnitType_Optional::Watt ? limit.power : limit.current;
float fallback_limit = unit == ChargingRateUnitType_Optional::Watt ? MO_MaxChargingLimitPower : MO_MaxChargingLimitCurrent;
periods.back().limit = limit_opt != std::numeric_limits<float>::max() ? limit_opt : fallback_limit;
periods.back().numberPhases = limit.nphases != std::numeric_limits<int>::max() ? limit.nphases : MO_MaxChargingLimitNumberPhases;
periods.back().startPeriod = periodBegin - startSchedule;
lastLimit = limit;
}

periodBegin = periodStop;
}
Expand Down Expand Up @@ -521,19 +529,19 @@ void SmartChargingService::loop(){
MO_DBG_INFO("New limit for connector %u, scheduled at = %s, nextChange = %s, limit = {%.1f, %.1f, %i}",
0,
timestamp1, timestamp2,
limit.power != std::numeric_limits<float>::max() ? limit.power : -1.f,
limit.current != std::numeric_limits<float>::max() ? limit.current : -1.f,
limit.nphases != std::numeric_limits<int>::max() ? limit.nphases : -1);
limit.power != std::numeric_limits<float>::max() ? limit.power : MO_MaxChargingLimitPower,
limit.current != std::numeric_limits<float>::max() ? limit.current : MO_MaxChargingLimitCurrent,
limit.nphases != std::numeric_limits<int>::max() ? limit.nphases : MO_MaxChargingLimitNumberPhases);
}
#endif

if (trackLimitOutput != limit) {
if (limitOutput) {

limitOutput(
limit.power != std::numeric_limits<float>::max() ? limit.power : -1.f,
limit.current != std::numeric_limits<float>::max() ? limit.current : -1.f,
limit.nphases != std::numeric_limits<int>::max() ? limit.nphases : -1);
limit.power != std::numeric_limits<float>::max() ? limit.power : MO_MaxChargingLimitPower,
limit.current != std::numeric_limits<float>::max() ? limit.current : MO_MaxChargingLimitCurrent,
limit.nphases != std::numeric_limits<int>::max() ? limit.nphases : MO_MaxChargingLimitNumberPhases);
trackLimitOutput = limit;
}
}
Expand Down Expand Up @@ -692,8 +700,9 @@ std::unique_ptr<ChargingSchedule> SmartChargingService::getCompositeSchedule(uns

periods.push_back(ChargingSchedulePeriod());
float limit_opt = unit == ChargingRateUnitType_Optional::Watt ? limit.power : limit.current;
periods.back().limit = limit_opt != std::numeric_limits<float>::max() ? limit_opt : -1.f;
periods.back().numberPhases = limit.nphases != std::numeric_limits<int>::max() ? limit.nphases : -1;
float fallback_limit = unit == ChargingRateUnitType_Optional::Watt ? MO_MaxChargingLimitPower : MO_MaxChargingLimitCurrent;
periods.back().limit = limit_opt != std::numeric_limits<float>::max() ? limit_opt : fallback_limit;
periods.back().numberPhases = limit.nphases != std::numeric_limits<int>::max() ? limit.nphases : MO_MaxChargingLimitNumberPhases;
periods.back().startPeriod = periodBegin - startSchedule;

periodBegin = periodStop;
Expand Down Expand Up @@ -764,6 +773,8 @@ bool SmartChargingServiceUtils::removeProfile(std::shared_ptr<FilesystemAdapter>
return false;
}

MO_DBG_DEBUG("Removing chargingProfile for connector %d, purpose %d, stack level %d, file %s", connectorId, (int) purpose, (int) stackLevel, fn);

return filesystem->remove(fn);
}

Expand Down
15 changes: 15 additions & 0 deletions src/MicroOcpp/Model/SmartCharging/SmartChargingService.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@
#ifndef SMARTCHARGINGSERVICE_H
#define SMARTCHARGINGSERVICE_H

// Per OCPP `GetCompositeScheduleResponse` schema, limit must be > 0.
// Please define a sane value in your implementation (e.g. 32A or 22kW-equivalent) instead of -1 to pass the OCTT tests.
#ifndef MO_MaxChargingLimitPower
#define MO_MaxChargingLimitPower -1.f
#endif

#ifndef MO_MaxChargingLimitCurrent
#define MO_MaxChargingLimitCurrent -1.f
#endif

// For numberPhases, the field is optional; if unknown you can default to 3 (typical three-phase) instead of -1.
#ifndef MO_MaxChargingLimitNumberPhases
#define MO_MaxChargingLimitNumberPhases -1
#endif

#include <functional>
#include <array>

Expand Down
22 changes: 22 additions & 0 deletions tests/SmartCharging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@

#define SCPROFILE_10_ABSOLUTE_LIMIT_5KW "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":10,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"chargingSchedule\":{\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"W\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":5000,\"numberPhases\":3}]}}}]"

#define TC_056_CS_TXDEFPROFILE_ABSOLUTE_6A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":1,\"csChargingProfiles\":{\"chargingProfileId\":41,\"stackLevel\":0,\"chargingProfilePurpose\":\"TxDefaultProfile\",\"chargingProfileKind\":\"Absolute\",\"validFrom\":\"2023-01-01T00:00:00Z\",\"validTo\":\"2023-01-01T00:06:00Z\",\"chargingSchedule\":{\"duration\":304,\"startSchedule\":\"2023-01-01T00:00:00Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":6.000000,\"numberPhases\":1}]}}}]"


using namespace MicroOcpp;

Expand Down Expand Up @@ -836,6 +838,26 @@ TEST_CASE( "SmartCharging" ) {
REQUIRE( checkProcessed );
}

SECTION("TC_056_CS - Ensures that TxDefaultProfile appears in composite schedule even without active transaction") {

loop();
// Ensure no active transaction
REQUIRE(getTransaction() == nullptr);

// Set the TxDefaultProfile as per OCTT TC_056_CS specification
loopback.sendTXT(TC_056_CS_TXDEFPROFILE_ABSOLUTE_6A, strlen(TC_056_CS_TXDEFPROFILE_ABSOLUTE_6A));
loop();

// Get composite schedule and verify the limit is 6A (not -1 indicating no limit)
auto schedule = scService->getCompositeSchedule(1, 300, ChargingRateUnitType_Optional::Amp);
REQUIRE(schedule);
REQUIRE(schedule->chargingSchedulePeriod.size() > 0);

// "Limit 1 should be 6.000000" as per OCTT test case
REQUIRE(schedule->chargingSchedulePeriod[0].limit == Approx(6.0f));
REQUIRE(schedule->chargingSchedulePeriod[0].numberPhases == 1);
}

scService->clearChargingProfile([] (int, int, ChargingProfilePurposeType, int) {
return true;
});
Expand Down