From ad490d5f65e55103448a5c933cf9ea1ae4f512a5 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Tue, 1 Aug 2023 18:47:53 +0200 Subject: [PATCH] feat: make get composite schedule closer to OCPP 1.6 specs MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- src/charging-station/Helpers.ts | 31 ++-- src/charging-station/index.ts | 3 + .../ocpp/1.6/OCPP16IncomingRequestService.ts | 159 +++++++++++++----- 3 files changed, 131 insertions(+), 62 deletions(-) diff --git a/src/charging-station/Helpers.ts b/src/charging-station/Helpers.ts index 6d274251..05773f01 100644 --- a/src/charging-station/Helpers.ts +++ b/src/charging-station/Helpers.ts @@ -520,18 +520,11 @@ export const getChargingStationConnectorChargingProfilesPowerLimit = ( ): number | undefined => { let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined; // Get charging profiles for connector id and sort by stack level - const chargingProfiles = - cloneObject( - chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!, - )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? []; - // Get charging profiles on connector 0 and sort by stack level - if (isNotEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)) { - chargingProfiles.push( - ...cloneObject( - chargingStation.getConnectorStatus(0)!.chargingProfiles!, - ).sort((a, b) => b.stackLevel - a.stackLevel), - ); - } + const chargingProfiles = cloneObject( + (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? []).concat( + chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [], + ), + ).sort((a, b) => b.stackLevel - a.stackLevel); if (isNotEmptyArray(chargingProfiles)) { const result = getLimitFromChargingProfiles( chargingStation, @@ -741,6 +734,12 @@ const getLimitFromChargingProfiles = ( const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`; const currentDate = new Date(); const connectorStatus = chargingStation.getConnectorStatus(connectorId); + if (!isArraySorted(chargingProfiles, (a, b) => b.stackLevel - a.stackLevel)) { + logger.warn( + `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profiles are not sorted by stack level. Trying to sort them`, + ); + chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel); + } for (const chargingProfile of chargingProfiles) { const chargingSchedule = chargingProfile.chargingSchedule; if (connectorStatus?.transactionStarted && isNullOrUndefined(chargingSchedule?.startSchedule)) { @@ -860,7 +859,7 @@ const getLimitFromChargingProfiles = ( } }; -const canProceedChargingProfile = ( +export const canProceedChargingProfile = ( chargingProfile: ChargingProfile, currentDate: Date, logPrefix: string, @@ -879,7 +878,7 @@ const canProceedChargingProfile = ( const chargingSchedule = chargingProfile.chargingSchedule; if (isNullOrUndefined(chargingSchedule?.startSchedule)) { logger.error( - `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`, + `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`, ); return false; } @@ -892,7 +891,7 @@ const canProceedChargingProfile = ( return true; }; -const canProceedRecurringChargingProfile = ( +export const canProceedRecurringChargingProfile = ( chargingProfile: ChargingProfile, logPrefix: string, ): boolean => { @@ -915,7 +914,7 @@ const canProceedRecurringChargingProfile = ( * @param currentDate - * @param logPrefix - */ -const prepareRecurringChargingProfile = ( +export const prepareRecurringChargingProfile = ( chargingProfile: ChargingProfile, currentDate: Date, logPrefix: string, diff --git a/src/charging-station/index.ts b/src/charging-station/index.ts index e1627f99..07dbe9b6 100644 --- a/src/charging-station/index.ts +++ b/src/charging-station/index.ts @@ -6,10 +6,13 @@ export { setConfigurationKeyValue, } from './ConfigurationKeyUtils'; export { + canProceedChargingProfile, + canProceedRecurringChargingProfile, checkChargingStation, getIdTagsFile, hasFeatureProfile, hasReservationExpired, + prepareRecurringChargingProfile, removeExpiredReservations, resetConnectorStatus, } from './Helpers'; diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 81f7540f..e3375d19 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -6,15 +6,25 @@ import { URL, fileURLToPath } from 'node:url'; import type { JSONSchemaType } from 'ajv'; import { Client, type FTPResponse } from 'basic-ftp'; -import { addSeconds, isWithinInterval, max, secondsToMilliseconds } from 'date-fns'; +import { + addSeconds, + isDate, + isWithinInterval, + maxTime, + min, + secondsToMilliseconds, +} from 'date-fns'; import { create } from 'tar'; import { OCPP16Constants } from './OCPP16Constants'; import { OCPP16ServiceUtils } from './OCPP16ServiceUtils'; import { type ChargingStation, + canProceedChargingProfile, + canProceedRecurringChargingProfile, checkChargingStation, getConfigurationKey, + prepareRecurringChargingProfile, removeExpiredReservations, setConfigurationKeyValue, } from '../../../charging-station'; @@ -22,6 +32,8 @@ import { OCPPError } from '../../../exception'; import { type ChangeConfigurationRequest, type ChangeConfigurationResponse, + ChargingProfileKindType, + ChargingRateUnitType, type ClearChargingProfileRequest, type ClearChargingProfileResponse, ErrorType, @@ -89,6 +101,7 @@ import { } from '../../../types'; import { Constants, + cloneObject, convertToDate, convertToInt, formatDurationMilliSeconds, @@ -98,6 +111,7 @@ import { isNotEmptyString, isNullOrUndefined, isUndefined, + isValidTime, logger, sleep, } from '../../../utils'; @@ -673,67 +687,120 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ); return OCPP16Constants.OCPP_RESPONSE_REJECTED; } - if (isEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) { + const connectorStatus = chargingStation.getConnectorStatus(connectorId); + if ( + isEmptyArray( + connectorStatus?.chargingProfiles && + isEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles), + ) + ) { return OCPP16Constants.OCPP_RESPONSE_REJECTED; } - const startDate = new Date(); + const currentDate = new Date(); const interval: Interval = { - start: startDate, - end: addSeconds(startDate, duration), + start: currentDate, + end: addSeconds(currentDate, duration), }; - let compositeSchedule: OCPP16ChargingSchedule | undefined; - for (const chargingProfile of chargingStation.getConnectorStatus(connectorId)! - .chargingProfiles!) { + const chargingProfiles: OCPP16ChargingProfile[] = []; + for (const chargingProfile of cloneObject( + (connectorStatus?.chargingProfiles ?? []).concat( + chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [], + ), + ).sort((a, b) => b.stackLevel - a.stackLevel)) { if ( - compositeSchedule?.chargingRateUnit && - compositeSchedule.chargingRateUnit !== chargingProfile.chargingSchedule.chargingRateUnit + connectorStatus?.transactionStarted && + isNullOrUndefined(chargingProfile.chargingSchedule?.startSchedule) ) { - logger.error( - `${chargingStation.logPrefix()} Building composite schedule with different charging rate units is not yet supported, skipping charging profile id ${ + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${ chargingProfile.chargingProfileId - }`, + } has no startSchedule defined. Trying to set it to the connector current transaction start date`, ); - continue; + // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction + chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart; + } + if (!isDate(chargingProfile.chargingSchedule?.startSchedule)) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${ + chargingProfile.chargingProfileId + } startSchedule property is not a Date object. Trying to convert it to a Date object`, + ); + chargingProfile.chargingSchedule.startSchedule = convertToDate( + chargingProfile.chargingSchedule?.startSchedule, + )!; + } + switch (chargingProfile.chargingProfileKind) { + case ChargingProfileKindType.RECURRING: + if (!canProceedRecurringChargingProfile(chargingProfile, chargingStation.logPrefix())) { + continue; + } + prepareRecurringChargingProfile( + chargingProfile, + interval.start as Date, + chargingStation.logPrefix(), + ); + break; + case ChargingProfileKindType.RELATIVE: + connectorStatus?.transactionStarted && + (chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart); + break; } if ( - isWithinInterval(chargingProfile.chargingSchedule.startSchedule!, interval) && - isWithinInterval( - addSeconds( - chargingProfile.chargingSchedule.startSchedule!, - chargingProfile.chargingSchedule.duration!, - ), - interval, + !canProceedChargingProfile( + chargingProfile, + interval.start as Date, + chargingStation.logPrefix(), ) ) { - compositeSchedule = { - startSchedule: max([ - compositeSchedule?.startSchedule ?? interval.start, - chargingProfile.chargingSchedule.startSchedule!, - ]), - duration: Math.max( - compositeSchedule?.duration ?? -Infinity, - chargingProfile.chargingSchedule.duration!, - ), - chargingRateUnit: chargingProfile.chargingSchedule.chargingRateUnit, - ...(compositeSchedule?.chargingSchedulePeriod === undefined - ? { chargingSchedulePeriod: [] } - : { - chargingSchedulePeriod: compositeSchedule.chargingSchedulePeriod.concat( - ...chargingProfile.chargingSchedule.chargingSchedulePeriod, - ), - }), - ...(chargingProfile.chargingSchedule.minChargeRate && { - minChargeRate: Math.min( - compositeSchedule?.minChargeRate ?? Infinity, - chargingProfile.chargingSchedule.minChargeRate, - ), - }), - }; + continue; + } + // Add active charging profiles into chargingProfiles array + if ( + isValidTime(chargingProfile.chargingSchedule?.startSchedule) && + isWithinInterval(chargingProfile.chargingSchedule.startSchedule!, interval) + ) { + chargingProfiles.push(chargingProfile); } } + const compositeSchedule: OCPP16ChargingSchedule = { + startSchedule: min( + chargingProfiles.map( + (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime, + ), + ), + duration: Math.max( + ...chargingProfiles.map( + (chargingProfile) => chargingProfile.chargingSchedule.duration ?? -Infinity, + ), + ), + chargingRateUnit: chargingProfiles.every( + (chargingProfile) => + chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.AMPERE, + ) + ? ChargingRateUnitType.AMPERE + : chargingProfiles.every( + (chargingProfile) => + chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT, + ) + ? ChargingRateUnitType.WATT + : ChargingRateUnitType.AMPERE, + // FIXME: remove overlapping charging schedule periods + chargingSchedulePeriod: chargingProfiles + .map((chargingProfile) => chargingProfile.chargingSchedule.chargingSchedulePeriod) + .reduce( + (accumulator, value) => + accumulator.concat(value).sort((a, b) => a.startPeriod - b.startPeriod), + [], + ), + minChargeRate: Math.min( + ...chargingProfiles.map( + (chargingProfile) => chargingProfile.chargingSchedule.minChargeRate ?? Infinity, + ), + ), + }; return { status: GenericStatus.Accepted, - scheduleStart: compositeSchedule?.startSchedule, + scheduleStart: compositeSchedule.startSchedule!, connectorId, chargingSchedule: compositeSchedule, }; -- 2.34.1