X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2Focpp%2F1.6%2FOCPP16IncomingRequestService.ts;h=1941b665f00a5f7da58756c8186850abbb24b5ec;hb=ec54600d41e798a66e61e6311ee07cccfb1aea2b;hp=3305f4c71e7e37fd4665e8a39b7b799d3807e5c1;hpb=0d1f33bab47bbcd712ea6ae7e26293917c8bc398;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 3305f4c7..1941b665 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -6,15 +6,29 @@ import { URL, fileURLToPath } from 'node:url'; import type { JSONSchemaType } from 'ajv'; import { Client, type FTPResponse } from 'basic-ftp'; -import { isWithinInterval, 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, + prepareChargingProfileKind, removeExpiredReservations, setConfigurationKeyValue, } from '../../../charging-station'; @@ -45,7 +59,9 @@ import { OCPP16ChargePointStatus, type OCPP16ChargingProfile, OCPP16ChargingProfilePurposeType, + OCPP16ChargingRateUnitType, type OCPP16ChargingSchedule, + type OCPP16ChargingSchedulePeriod, type OCPP16ClearCacheRequest, type OCPP16DataTransferRequest, type OCPP16DataTransferResponse, @@ -89,6 +105,7 @@ import { } from '../../../types'; import { Constants, + cloneObject, convertToDate, convertToInt, formatDurationMilliSeconds, @@ -98,6 +115,7 @@ import { isNotEmptyString, isNullOrUndefined, isUndefined, + isValidTime, logger, sleep, } from '../../../utils'; @@ -653,7 +671,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ) { return OCPP16Constants.OCPP_RESPONSE_REJECTED; } - const { connectorId, duration } = commandPayload; + const { connectorId, duration, chargingRateUnit } = commandPayload; if (chargingStation.hasConnector(connectorId) === false) { logger.error( `${chargingStation.logPrefix()} Trying to get composite schedule to a @@ -661,28 +679,230 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ); return OCPP16Constants.OCPP_RESPONSE_REJECTED; } - if (isEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) { + if (connectorId === 0) { + logger.error( + `${chargingStation.logPrefix()} Get composite schedule on connector id ${connectorId} is not yet supported`, + ); return OCPP16Constants.OCPP_RESPONSE_REJECTED; } - const startDate = new Date(); - const endDate = new Date(startDate.getTime() + secondsToMilliseconds(duration)); - let compositeSchedule: OCPP16ChargingSchedule | undefined; - for (const chargingProfile of chargingStation.getConnectorStatus(connectorId)! - .chargingProfiles!) { - // FIXME: build the composite schedule including the local power limit, the stack level, the charging rate unit, etc. + if (chargingRateUnit) { + logger.warn( + `${chargingStation.logPrefix()} Get composite schedule with a specified rate unit is not yet supported, no conversion will be done`, + ); + } + const connectorStatus = chargingStation.getConnectorStatus(connectorId)!; + if ( + isEmptyArray( + connectorStatus?.chargingProfiles && + isEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles), + ) + ) { + return OCPP16Constants.OCPP_RESPONSE_REJECTED; + } + const currentDate = new Date(); + const interval: Interval = { + start: currentDate, + end: addSeconds(currentDate, duration), + }; + const storedChargingProfiles: OCPP16ChargingProfile[] = cloneObject( + (connectorStatus?.chargingProfiles ?? []).concat( + chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [], + ), + ).sort((a, b) => b.stackLevel - a.stackLevel); + const chargingProfiles: OCPP16ChargingProfile[] = []; + for (const storedChargingProfile of storedChargingProfiles) { + if ( + connectorStatus?.transactionStarted && + isNullOrUndefined(storedChargingProfile.chargingSchedule?.startSchedule) + ) { + 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 ( - isWithinInterval(chargingProfile.chargingSchedule.startSchedule!, { - start: startDate, - end: endDate, - }) + !prepareChargingProfileKind( + connectorStatus, + storedChargingProfile, + interval.start as Date, + chargingStation.logPrefix(), + ) ) { - compositeSchedule = chargingProfile.chargingSchedule; - break; + continue; + } + if ( + !canProceedChargingProfile( + storedChargingProfile, + interval.start as Date, + chargingStation.logPrefix(), + ) + ) { + 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, };