X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2Focpp%2F1.6%2FOCPP16IncomingRequestService.ts;h=d24a2d4cd36e2716ab35b375af7b433017e4a9f3;hb=da332e702310d2a717d759040727e4e2a3f3fe87;hp=e3375d192d1615648f4c0910fcc19aa5a7c84f60;hpb=ad490d5f65e55103448a5c933cf9ea1ae4f512a5;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 e3375d19..d24a2d4c 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -8,10 +8,15 @@ import type { JSONSchemaType } from 'ajv'; import { Client, type FTPResponse } from 'basic-ftp'; import { addSeconds, + differenceInSeconds, + isAfter, + isBefore, isDate, isWithinInterval, + max, maxTime, min, + minTime, secondsToMilliseconds, } from 'date-fns'; import { create } from 'tar'; @@ -21,10 +26,10 @@ import { OCPP16ServiceUtils } from './OCPP16ServiceUtils'; import { type ChargingStation, canProceedChargingProfile, - canProceedRecurringChargingProfile, checkChargingStation, getConfigurationKey, - prepareRecurringChargingProfile, + getConnectorChargingProfiles, + prepareChargingProfileKind, removeExpiredReservations, setConfigurationKeyValue, } from '../../../charging-station'; @@ -32,8 +37,6 @@ import { OCPPError } from '../../../exception'; import { type ChangeConfigurationRequest, type ChangeConfigurationResponse, - ChargingProfileKindType, - ChargingRateUnitType, type ClearChargingProfileRequest, type ClearChargingProfileResponse, ErrorType, @@ -57,7 +60,9 @@ import { OCPP16ChargePointStatus, type OCPP16ChargingProfile, OCPP16ChargingProfilePurposeType, + OCPP16ChargingRateUnitType, type OCPP16ChargingSchedule, + type OCPP16ChargingSchedulePeriod, type OCPP16ClearCacheRequest, type OCPP16DataTransferRequest, type OCPP16DataTransferResponse, @@ -101,7 +106,6 @@ import { } from '../../../types'; import { Constants, - cloneObject, convertToDate, convertToInt, formatDurationMilliSeconds, @@ -682,12 +686,11 @@ 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; } - const connectorStatus = chargingStation.getConnectorStatus(connectorId); + const connectorStatus = chargingStation.getConnectorStatus(connectorId)!; if ( isEmptyArray( connectorStatus?.chargingProfiles && @@ -701,53 +704,63 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { 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 chargingProfile of cloneObject( - (connectorStatus?.chargingProfiles ?? []).concat( - chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [], - ), - ).sort((a, b) => b.stackLevel - a.stackLevel)) { + for (const storedChargingProfile of storedChargingProfiles) { if ( - connectorStatus?.transactionStarted && - isNullOrUndefined(chargingProfile.chargingSchedule?.startSchedule) + isNullOrUndefined(storedChargingProfile.chargingSchedule?.startSchedule) && + connectorStatus?.transactionStarted ) { logger.debug( `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${ - chargingProfile.chargingProfileId + 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 - chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart; + 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(chargingProfile.chargingSchedule?.startSchedule)) { + if (!isDate(storedChargingProfile.chargingSchedule?.startSchedule)) { logger.warn( `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${ - chargingProfile.chargingProfileId + storedChargingProfile.chargingProfileId } startSchedule property is not a Date object. Trying to convert it to a Date object`, ); - chargingProfile.chargingSchedule.startSchedule = convertToDate( - chargingProfile.chargingSchedule?.startSchedule, + storedChargingProfile.chargingSchedule.startSchedule = convertToDate( + storedChargingProfile.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 ( + !prepareChargingProfileKind( + connectorStatus, + storedChargingProfile, + interval.start as Date, + chargingStation.logPrefix(), + ) + ) { + continue; } if ( !canProceedChargingProfile( - chargingProfile, + storedChargingProfile, interval.start as Date, chargingStation.logPrefix(), ) @@ -756,45 +769,181 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { } // Add active charging profiles into chargingProfiles array if ( - isValidTime(chargingProfile.chargingSchedule?.startSchedule) && - isWithinInterval(chargingProfile.chargingSchedule.startSchedule!, interval) + isValidTime(storedChargingProfile.chargingSchedule?.startSchedule) && + isWithinInterval(storedChargingProfile.chargingSchedule.startSchedule!, interval) ) { - chargingProfiles.push(chargingProfile); + 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 compositeSchedule: OCPP16ChargingSchedule = { - startSchedule: min( - chargingProfiles.map( - (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime, - ), + const compositeScheduleStart: Date = min( + chargingProfiles.map( + (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime, ), - duration: Math.max( - ...chargingProfiles.map( - (chargingProfile) => chargingProfile.chargingSchedule.duration ?? -Infinity, - ), + ); + 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 === ChargingRateUnitType.AMPERE, + chargingProfile.chargingSchedule.chargingRateUnit === OCPP16ChargingRateUnitType.AMPERE, ) - ? ChargingRateUnitType.AMPERE + ? OCPP16ChargingRateUnitType.AMPERE : chargingProfiles.every( (chargingProfile) => - chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT, + chargingProfile.chargingSchedule.chargingRateUnit === OCPP16ChargingRateUnitType.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), - [], - ), + ? OCPP16ChargingRateUnitType.WATT + : OCPP16ChargingRateUnitType.AMPERE, + chargingSchedulePeriod: compositeSchedulePeriods, minChargeRate: Math.min( - ...chargingProfiles.map( - (chargingProfile) => chargingProfile.chargingSchedule.minChargeRate ?? Infinity, + ...chargingProfiles.map((chargingProfile) => + isNaN(chargingProfile.chargingSchedule.minChargeRate!) + ? Infinity + : chargingProfile.chargingSchedule.minChargeRate!, ), ), };