diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp index 3c71d539..7eaf947d 100644 --- a/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp @@ -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; diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp index 762bd6ac..f0a6febd 100644 --- a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp @@ -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; @@ -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; @@ -168,9 +168,9 @@ 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::max() ? limit.power : -1.f, - limit.current != std::numeric_limits::max() ? limit.current : -1.f, - limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); + limit.power != std::numeric_limits::max() ? limit.power : MO_MaxChargingLimitPower, + limit.current != std::numeric_limits::max() ? limit.current : MO_MaxChargingLimitCurrent, + limit.nphases != std::numeric_limits::max() ? limit.nphases : MO_MaxChargingLimitNumberPhases); } #endif @@ -178,9 +178,9 @@ void SmartChargingConnector::loop(){ if (limitOutput) { limitOutput( - limit.power != std::numeric_limits::max() ? limit.power : -1.f, - limit.current != std::numeric_limits::max() ? limit.current : -1.f, - limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); + limit.power != std::numeric_limits::max() ? limit.power : MO_MaxChargingLimitPower, + limit.current != std::numeric_limits::max() ? limit.current : MO_MaxChargingLimitCurrent, + limit.nphases != std::numeric_limits::max() ? limit.nphases : MO_MaxChargingLimitNumberPhases); trackLimitOutput = limit; } } @@ -252,6 +252,9 @@ std::unique_ptr 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 @@ -267,11 +270,16 @@ std::unique_ptr 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::max() ? limit_opt : -1.f, - periods.back().numberPhases = limit.nphases != std::numeric_limits::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::max() ? limit_opt : fallback_limit; + periods.back().numberPhases = limit.nphases != std::numeric_limits::max() ? limit.nphases : MO_MaxChargingLimitNumberPhases; + periods.back().startPeriod = periodBegin - startSchedule; + lastLimit = limit; + } periodBegin = periodStop; } @@ -521,9 +529,9 @@ 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::max() ? limit.power : -1.f, - limit.current != std::numeric_limits::max() ? limit.current : -1.f, - limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); + limit.power != std::numeric_limits::max() ? limit.power : MO_MaxChargingLimitPower, + limit.current != std::numeric_limits::max() ? limit.current : MO_MaxChargingLimitCurrent, + limit.nphases != std::numeric_limits::max() ? limit.nphases : MO_MaxChargingLimitNumberPhases); } #endif @@ -531,9 +539,9 @@ void SmartChargingService::loop(){ if (limitOutput) { limitOutput( - limit.power != std::numeric_limits::max() ? limit.power : -1.f, - limit.current != std::numeric_limits::max() ? limit.current : -1.f, - limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); + limit.power != std::numeric_limits::max() ? limit.power : MO_MaxChargingLimitPower, + limit.current != std::numeric_limits::max() ? limit.current : MO_MaxChargingLimitCurrent, + limit.nphases != std::numeric_limits::max() ? limit.nphases : MO_MaxChargingLimitNumberPhases); trackLimitOutput = limit; } } @@ -692,8 +700,9 @@ std::unique_ptr 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::max() ? limit_opt : -1.f; - periods.back().numberPhases = limit.nphases != std::numeric_limits::max() ? limit.nphases : -1; + float fallback_limit = unit == ChargingRateUnitType_Optional::Watt ? MO_MaxChargingLimitPower : MO_MaxChargingLimitCurrent; + periods.back().limit = limit_opt != std::numeric_limits::max() ? limit_opt : fallback_limit; + periods.back().numberPhases = limit.nphases != std::numeric_limits::max() ? limit.nphases : MO_MaxChargingLimitNumberPhases; periods.back().startPeriod = periodBegin - startSchedule; periodBegin = periodStop; @@ -764,6 +773,8 @@ bool SmartChargingServiceUtils::removeProfile(std::shared_ptr 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); } diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h index 57952a72..76b43fbb 100644 --- a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h @@ -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 #include diff --git a/tests/SmartCharging.cpp b/tests/SmartCharging.cpp index 7cf53b32..1988a074 100644 --- a/tests/SmartCharging.cpp +++ b/tests/SmartCharging.cpp @@ -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; @@ -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; });