X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2Focpp%2F1.6%2FOCPP16IncomingRequestService.ts;h=66a6ee5960a95b7050a1888122e9bcef4cfe7a24;hb=6fc0c6f3db444377c0fdea238183a14823278046;hp=81f7540f74cf382c07482347a23adfead9a117a6;hpb=d372f6da34cd27ce947ea2457dc37646a7edb472;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 81f7540f..66a6ee59 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -6,15 +6,30 @@ 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, + differenceInSeconds, + isAfter, + isBefore, + isDate, + isWithinInterval, + max, + maxTime, + min, + minTime, + secondsToMilliseconds, +} from 'date-fns'; import { create } from 'tar'; import { OCPP16Constants } from './OCPP16Constants'; import { OCPP16ServiceUtils } from './OCPP16ServiceUtils'; import { type ChargingStation, + canProceedChargingProfile, checkChargingStation, getConfigurationKey, + getConnectorChargingProfiles, + prepareChargingProfileKind, removeExpiredReservations, setConfigurationKeyValue, } from '../../../charging-station'; @@ -45,7 +60,9 @@ import { OCPP16ChargePointStatus, type OCPP16ChargingProfile, OCPP16ChargingProfilePurposeType, + OCPP16ChargingRateUnitType, type OCPP16ChargingSchedule, + type OCPP16ChargingSchedulePeriod, type OCPP16ClearCacheRequest, type OCPP16DataTransferRequest, type OCPP16DataTransferResponse, @@ -98,6 +115,7 @@ import { isNotEmptyString, isNullOrUndefined, isUndefined, + isValidTime, logger, sleep, } from '../../../utils'; @@ -668,72 +686,223 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { return OCPP16Constants.OCPP_RESPONSE_REJECTED; } if (chargingRateUnit) { - logger.error( - `${chargingStation.logPrefix()} Get composite schedule with a specified rate unit is not yet supported`, + logger.warn( + `${chargingStation.logPrefix()} Get composite schedule with a specified rate unit is not yet supported, no conversion will be done`, ); - 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!) { + // Get charging profiles sorted by connector id then stack level + const storedChargingProfiles: OCPP16ChargingProfile[] = getConnectorChargingProfiles( + chargingStation, + connectorId, + ); + const chargingProfiles: OCPP16ChargingProfile[] = []; + for (const storedChargingProfile of storedChargingProfiles) { if ( - compositeSchedule?.chargingRateUnit && - compositeSchedule.chargingRateUnit !== chargingProfile.chargingSchedule.chargingRateUnit + connectorStatus?.transactionStarted && + isNullOrUndefined(storedChargingProfile.chargingSchedule?.startSchedule) ) { - logger.error( - `${chargingStation.logPrefix()} Building composite schedule with different charging rate units is not yet supported, skipping charging profile id ${ - chargingProfile.chargingProfileId - }`, + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${ + storedChargingProfile.chargingProfileId + } has no startSchedule defined. Trying to set it to the connector current transaction start date`, ); + // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction + storedChargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart; + } + if (!isDate(storedChargingProfile.chargingSchedule?.startSchedule)) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${ + storedChargingProfile.chargingProfileId + } startSchedule property is not a Date object. Trying to convert it to a Date object`, + ); + storedChargingProfile.chargingSchedule.startSchedule = convertToDate( + storedChargingProfile.chargingSchedule?.startSchedule, + )!; + } + if ( + !prepareChargingProfileKind( + connectorStatus, + storedChargingProfile, + interval.start as Date, + chargingStation.logPrefix(), + ) + ) { continue; } if ( - isWithinInterval(chargingProfile.chargingSchedule.startSchedule!, interval) && - isWithinInterval( - addSeconds( - chargingProfile.chargingSchedule.startSchedule!, - chargingProfile.chargingSchedule.duration!, - ), - interval, + !canProceedChargingProfile( + storedChargingProfile, + 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(storedChargingProfile.chargingSchedule?.startSchedule) && + isWithinInterval(storedChargingProfile.chargingSchedule.startSchedule!, interval) + ) { + if (isEmptyArray(chargingProfiles)) { + if ( + isAfter( + addSeconds( + storedChargingProfile.chargingSchedule.startSchedule!, + storedChargingProfile.chargingSchedule.duration!, + ), + interval.end, + ) + ) { + storedChargingProfile.chargingSchedule.chargingSchedulePeriod = + storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter( + (schedulePeriod) => + isWithinInterval( + addSeconds( + storedChargingProfile.chargingSchedule.startSchedule!, + schedulePeriod.startPeriod, + )!, + interval, + ), + ); + storedChargingProfile.chargingSchedule.duration = differenceInSeconds( + interval.end, + storedChargingProfile.chargingSchedule.startSchedule!, + ); + } + chargingProfiles.push(storedChargingProfile); + } else if (isNotEmptyArray(chargingProfiles)) { + const chargingProfilesInterval: Interval = { + start: min( + chargingProfiles.map( + (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime, + ), + ), + end: max( + chargingProfiles.map( + (chargingProfile) => + addSeconds( + chargingProfile.chargingSchedule.startSchedule!, + chargingProfile.chargingSchedule.duration!, + ) ?? minTime, + ), ), - }), - }; + }; + let addChargingProfile = false; + if ( + isBefore(interval.start, chargingProfilesInterval.start) && + isBefore( + storedChargingProfile.chargingSchedule.startSchedule!, + chargingProfilesInterval.start, + ) + ) { + // Remove charging schedule periods that are after the start of the active profiles interval + storedChargingProfile.chargingSchedule.chargingSchedulePeriod = + storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter( + (schedulePeriod) => + isWithinInterval( + addSeconds( + storedChargingProfile.chargingSchedule.startSchedule!, + schedulePeriod.startPeriod, + ), + { + start: interval.start, + end: chargingProfilesInterval.start, + }, + ), + ); + addChargingProfile = true; + } + if ( + isBefore(chargingProfilesInterval.end, interval.end) && + isAfter( + addSeconds( + storedChargingProfile.chargingSchedule.startSchedule!, + storedChargingProfile.chargingSchedule.duration!, + ), + chargingProfilesInterval.end, + ) + ) { + // Remove charging schedule periods that are before the end of the active profiles interval + // FIXME: can lead to a gap in the charging schedule: chargingProfilesInterval.end -> first matching schedulePeriod.startPeriod + storedChargingProfile.chargingSchedule.chargingSchedulePeriod = + storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter( + (schedulePeriod) => + isWithinInterval( + addSeconds( + storedChargingProfile.chargingSchedule.startSchedule!, + schedulePeriod.startPeriod, + ), + { + start: chargingProfilesInterval.end, + end: interval.end, + }, + ), + ); + addChargingProfile = true; + } + addChargingProfile && chargingProfiles.push(storedChargingProfile); + } } } + const compositeScheduleStart: Date = min( + chargingProfiles.map( + (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime, + ), + ); + const compositeScheduleDuration: number = Math.max( + ...chargingProfiles.map((chargingProfile) => + isNaN(chargingProfile.chargingSchedule.duration!) + ? -Infinity + : chargingProfile.chargingSchedule.duration!, + ), + ); + const compositeSchedulePeriods: OCPP16ChargingSchedulePeriod[] = chargingProfiles + .map((chargingProfile) => chargingProfile.chargingSchedule.chargingSchedulePeriod) + .reduce( + (accumulator, value) => + accumulator.concat(value).sort((a, b) => a.startPeriod - b.startPeriod), + [], + ); + const compositeSchedule: OCPP16ChargingSchedule = { + startSchedule: compositeScheduleStart, + duration: compositeScheduleDuration, + chargingRateUnit: chargingProfiles.every( + (chargingProfile) => + chargingProfile.chargingSchedule.chargingRateUnit === OCPP16ChargingRateUnitType.AMPERE, + ) + ? OCPP16ChargingRateUnitType.AMPERE + : chargingProfiles.every( + (chargingProfile) => + chargingProfile.chargingSchedule.chargingRateUnit === OCPP16ChargingRateUnitType.WATT, + ) + ? OCPP16ChargingRateUnitType.WATT + : OCPP16ChargingRateUnitType.AMPERE, + chargingSchedulePeriod: compositeSchedulePeriods, + minChargeRate: Math.min( + ...chargingProfiles.map((chargingProfile) => + isNaN(chargingProfile.chargingSchedule.minChargeRate!) + ? Infinity + : chargingProfile.chargingSchedule.minChargeRate!, + ), + ), + }; return { status: GenericStatus.Accepted, - scheduleStart: compositeSchedule?.startSchedule, + scheduleStart: compositeSchedule.startSchedule!, connectorId, chargingSchedule: compositeSchedule, };