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);
Copy link
Owner

Choose a reason for hiding this comment

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

Good catch!

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) {
Copy link
Owner

Choose a reason for hiding this comment

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

This fixes the composite schedules. But also affects the ordinary charge limit output if no transaction is running. I cannot think of scenarios where this would be super bad though.

The usage of the global trackTxStart in calculateLimit() seems not to work well with composite schedules. In MO v2, it would be a possibility to add a new function parameter to calculateLimit() to alter the limit determination logic depending on where the output is used

Looks good for now!

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);
Copy link
Owner

Choose a reason for hiding this comment

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

Would be nice to preserve the -1 value for "undefined" limit. It could make a difference on a charger if no charging schedule is set at all, or if a charging schedule is set and its limit just happens to match the electric rating

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But do you really think it matters? What would the user-land code do? it still needs to set that limit on the PWM duty-cycle, no matter if default or set.

Anyhow, do you have an idea how to preserve this AND also keep the fixes for OCTT? It would be strange to use those defs only in GetCompositeSchedule....

Copy link
Owner

Choose a reason for hiding this comment

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

For sure it doesn't matter much. There are few edge cases I can think of:

  • You want to show on a device screen that Smart Charging is being used
  • There are competing load balancing mechanisms and OCPP should overrule other mechanisms, if a schedule is defined
  • I'm feeling a bit insecure about further edge cases I just don't know (happened a few times when I made breaking changes which put things to the worse for some users)

On the other hand, negative values as undefined values are easy to understand. And the application firmware needs some logic to sanitize the values anyway, because MO doesn't do a range check (you could define a 100A limit on a 32A charger at the moment).

The GetCompositeSchedule use case alone is a super valid reason to add a new build flag. Maybe a name like MO_CompositeScheduleDefaultPower would be a better fit then?

}
#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);
Copy link
Owner

Choose a reason for hiding this comment

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

See above

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;
}
Copy link
Owner

Choose a reason for hiding this comment

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

Nice!


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

Copy link
Owner

Choose a reason for hiding this comment

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

Good and simple solution for most use cases

#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
Loading