X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;ds=inline;f=src%2Fcharging-station%2Focpp%2F1.6%2FOCPP16IncomingRequestService.ts;h=d24a2d4cd36e2716ab35b375af7b433017e4a9f3;hb=da332e702310d2a717d759040727e4e2a3f3fe87;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..d24a2d4c 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 { 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, + 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'; @@ -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,277 @@ 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; + } + 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 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. + const currentDate = new Date(); + const interval: Interval = { + start: currentDate, + end: addSeconds(currentDate, duration), + }; + // 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 ( - isWithinInterval(chargingProfile.chargingSchedule.startSchedule!, { - start: startDate, - end: endDate, - }) + isNullOrUndefined(storedChargingProfile.chargingSchedule?.startSchedule) && + connectorStatus?.transactionStarted ) { - compositeSchedule = chargingProfile.chargingSchedule; - break; + 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 ( + !isNullOrUndefined(storedChargingProfile.chargingSchedule?.startSchedule) && + isNullOrUndefined(storedChargingProfile.chargingSchedule?.duration) + ) { + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${ + storedChargingProfile.chargingProfileId + } has no duration defined and will be set to the maximum time allowed`, + ); + // OCPP specifies that if duration is not defined, it should be infinite + storedChargingProfile.chargingSchedule.duration = differenceInSeconds( + maxTime, + storedChargingProfile.chargingSchedule.startSchedule!, + ); + } + 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 ( + !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 + storedChargingProfile.chargingSchedule.chargingSchedulePeriod = + storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter( + (schedulePeriod, index) => { + if ( + isWithinInterval( + addSeconds( + storedChargingProfile.chargingSchedule.startSchedule!, + schedulePeriod.startPeriod, + ), + { + start: chargingProfilesInterval.end, + end: interval.end, + }, + ) + ) { + return true; + } + if ( + !isWithinInterval( + addSeconds( + storedChargingProfile.chargingSchedule.startSchedule!, + schedulePeriod.startPeriod, + ), + { + start: chargingProfilesInterval.end, + end: interval.end, + }, + ) && + index < + storedChargingProfile.chargingSchedule.chargingSchedulePeriod.length - 1 && + isWithinInterval( + addSeconds( + storedChargingProfile.chargingSchedule.startSchedule!, + storedChargingProfile.chargingSchedule.chargingSchedulePeriod[index + 1] + .startPeriod, + ), + { + start: chargingProfilesInterval.end, + end: interval.end, + }, + ) + ) { + return true; + } + return false; + }, + ); + 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, };