X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2Focpp%2F1.6%2FOCPP16ServiceUtils.ts;h=af2f6d03a1ce9c944ebc1e87116735f30c47019e;hb=4190ce4e00f15beb8160ed04e496875e10bd2828;hp=35da1670922aa1ee60212334e7f90fb562eec9a6;hpb=225e32b0e38c3b120188f4c93418d4f98f8d1b28;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 35da1670..af2f6d03 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -1,9 +1,21 @@ // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved. import type { JSONSchemaType } from 'ajv'; +import { + addSeconds, + areIntervalsOverlapping, + differenceInSeconds, + isAfter, + isBefore, + isWithinInterval, +} from 'date-fns'; import { OCPP16Constants } from './OCPP16Constants'; -import { type ChargingStation, hasFeatureProfile } from '../../../charging-station'; +import { + type ChargingStation, + hasFeatureProfile, + hasReservationExpired, +} from '../../../charging-station'; import { OCPPError } from '../../../exception'; import { type ClearChargingProfileRequest, @@ -21,6 +33,7 @@ import { type OCPP16ChangeAvailabilityResponse, OCPP16ChargePointStatus, type OCPP16ChargingProfile, + type OCPP16ChargingSchedule, type OCPP16IncomingRequestCommand, type OCPP16MeterValue, OCPP16MeterValueMeasurand, @@ -838,13 +851,8 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { } responses.push(response); } - if ( - responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED) && - !responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED) - ) { + if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED)) { return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED; - } else if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED)) { - return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED; } return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED; }; @@ -864,7 +872,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false ) { logger.error( - `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an improper attribute type for the charging profiles array, applying proper type initialization`, + `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an improper attribute type for the charging profiles array, applying proper type deferred initialization`, ); chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = []; } @@ -891,28 +899,23 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { commandPayload: ClearChargingProfileRequest, chargingProfiles: OCPP16ChargingProfile[] | undefined, ): boolean => { + const { id, chargingProfilePurpose, stackLevel } = commandPayload; let clearedCP = false; if (isNotEmptyArray(chargingProfiles)) { chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => { let clearCurrentCP = false; - if (chargingProfile.chargingProfileId === commandPayload.id) { + if (chargingProfile.chargingProfileId === id) { clearCurrentCP = true; } - if ( - !commandPayload.chargingProfilePurpose && - chargingProfile.stackLevel === commandPayload.stackLevel - ) { + if (!chargingProfilePurpose && chargingProfile.stackLevel === stackLevel) { clearCurrentCP = true; } - if ( - !chargingProfile.stackLevel && - chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose - ) { + if (!stackLevel && chargingProfile.chargingProfilePurpose === chargingProfilePurpose) { clearCurrentCP = true; } if ( - chargingProfile.stackLevel === commandPayload.stackLevel && - chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose + chargingProfile.stackLevel === stackLevel && + chargingProfile.chargingProfilePurpose === chargingProfilePurpose ) { clearCurrentCP = true; } @@ -929,6 +932,197 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return clearedCP; }; + public static composeChargingSchedules = ( + chargingScheduleHigher: OCPP16ChargingSchedule | undefined, + chargingScheduleLower: OCPP16ChargingSchedule | undefined, + targetInterval: Interval, + ): OCPP16ChargingSchedule | undefined => { + if (!chargingScheduleHigher && !chargingScheduleLower) { + return undefined; + } + if (chargingScheduleHigher && !chargingScheduleLower) { + return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher, targetInterval); + } + if (!chargingScheduleHigher && chargingScheduleLower) { + return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower, targetInterval); + } + const compositeChargingScheduleHigher: OCPP16ChargingSchedule | undefined = + OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher!, targetInterval); + const compositeChargingScheduleLower: OCPP16ChargingSchedule | undefined = + OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower!, targetInterval); + const compositeChargingScheduleHigherInterval: Interval = { + start: compositeChargingScheduleHigher!.startSchedule!, + end: addSeconds( + compositeChargingScheduleHigher!.startSchedule!, + compositeChargingScheduleHigher!.duration!, + ), + }; + const compositeChargingScheduleLowerInterval: Interval = { + start: compositeChargingScheduleLower!.startSchedule!, + end: addSeconds( + compositeChargingScheduleLower!.startSchedule!, + compositeChargingScheduleLower!.duration!, + ), + }; + const higherFirst = isBefore( + compositeChargingScheduleHigherInterval.start, + compositeChargingScheduleLowerInterval.start, + ); + if ( + !areIntervalsOverlapping( + compositeChargingScheduleHigherInterval, + compositeChargingScheduleLowerInterval, + ) + ) { + return { + ...compositeChargingScheduleLower, + ...compositeChargingScheduleHigher!, + startSchedule: higherFirst + ? (compositeChargingScheduleHigherInterval.start as Date) + : (compositeChargingScheduleLowerInterval.start as Date), + duration: higherFirst + ? differenceInSeconds( + compositeChargingScheduleLowerInterval.end, + compositeChargingScheduleHigherInterval.start, + ) + : differenceInSeconds( + compositeChargingScheduleHigherInterval.end, + compositeChargingScheduleLowerInterval.start, + ), + chargingSchedulePeriod: [ + ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => { + return { + ...schedulePeriod, + startPeriod: higherFirst + ? 0 + : schedulePeriod.startPeriod + + differenceInSeconds( + compositeChargingScheduleHigherInterval.start, + compositeChargingScheduleLowerInterval.start, + ), + }; + }), + ...compositeChargingScheduleLower!.chargingSchedulePeriod.map((schedulePeriod) => { + return { + ...schedulePeriod, + startPeriod: higherFirst + ? schedulePeriod.startPeriod + + differenceInSeconds( + compositeChargingScheduleLowerInterval.start, + compositeChargingScheduleHigherInterval.start, + ) + : 0, + }; + }), + ].sort((a, b) => a.startPeriod - b.startPeriod), + }; + } + return { + ...compositeChargingScheduleLower, + ...compositeChargingScheduleHigher!, + startSchedule: higherFirst + ? (compositeChargingScheduleHigherInterval.start as Date) + : (compositeChargingScheduleLowerInterval.start as Date), + duration: higherFirst + ? differenceInSeconds( + compositeChargingScheduleLowerInterval.end, + compositeChargingScheduleHigherInterval.start, + ) + : differenceInSeconds( + compositeChargingScheduleHigherInterval.end, + compositeChargingScheduleLowerInterval.start, + ), + chargingSchedulePeriod: [ + ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => { + return { + ...schedulePeriod, + startPeriod: higherFirst + ? 0 + : schedulePeriod.startPeriod + + differenceInSeconds( + compositeChargingScheduleHigherInterval.start, + compositeChargingScheduleLowerInterval.start, + ), + }; + }), + ...compositeChargingScheduleLower!.chargingSchedulePeriod + .filter((schedulePeriod) => { + if ( + higherFirst && + isWithinInterval( + addSeconds( + compositeChargingScheduleLowerInterval.start, + schedulePeriod.startPeriod, + ), + { + start: compositeChargingScheduleLowerInterval.start, + end: compositeChargingScheduleHigherInterval.end, + }, + ) + ) { + return false; + } + if ( + !higherFirst && + isWithinInterval( + addSeconds( + compositeChargingScheduleLowerInterval.start, + schedulePeriod.startPeriod, + ), + { + start: compositeChargingScheduleHigherInterval.start, + end: compositeChargingScheduleLowerInterval.end, + }, + ) + ) { + return false; + } + return true; + }) + .map((schedulePeriod) => { + return { + ...schedulePeriod, + startPeriod: higherFirst + ? schedulePeriod.startPeriod + + differenceInSeconds( + compositeChargingScheduleLowerInterval.start, + compositeChargingScheduleHigherInterval.start, + ) + : 0, + }; + }), + ].sort((a, b) => a.startPeriod - b.startPeriod), + }; + }; + + public static hasReservation = ( + chargingStation: ChargingStation, + connectorId: number, + idTag: string, + ): boolean => { + const connectorReservation = chargingStation.getReservationBy('connectorId', connectorId); + const chargingStationReservation = chargingStation.getReservationBy('connectorId', 0); + if ( + (chargingStation.getConnectorStatus(connectorId)?.status === + OCPP16ChargePointStatus.Reserved && + connectorReservation && + !hasReservationExpired(connectorReservation) && + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + connectorReservation?.idTag === idTag) || + (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved && + chargingStationReservation && + !hasReservationExpired(chargingStationReservation) && + chargingStationReservation?.idTag === idTag) + ) { + logger.debug( + `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`, + connectorReservation ?? chargingStationReservation, + ); + return true; + } + return false; + }; + public static parseJsonSchemaFile( relativePath: string, moduleName?: string, @@ -942,6 +1136,69 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ); } + private static composeChargingSchedule = ( + chargingSchedule: OCPP16ChargingSchedule, + targetInterval: Interval, + ): OCPP16ChargingSchedule | undefined => { + const chargingScheduleInterval: Interval = { + start: chargingSchedule.startSchedule!, + end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!), + }; + if (areIntervalsOverlapping(chargingScheduleInterval, targetInterval)) { + chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod); + if (isBefore(chargingScheduleInterval.start, targetInterval.start)) { + return { + ...chargingSchedule, + startSchedule: targetInterval.start as Date, + duration: differenceInSeconds(chargingScheduleInterval.end, targetInterval.start as Date), + chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter( + (schedulePeriod, index) => { + if ( + isWithinInterval( + addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!, + targetInterval, + ) + ) { + return true; + } + if ( + index < chargingSchedule.chargingSchedulePeriod.length - 1 && + !isWithinInterval( + addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod), + targetInterval, + ) && + isWithinInterval( + addSeconds( + chargingScheduleInterval.start, + chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod, + ), + targetInterval, + ) + ) { + schedulePeriod.startPeriod = 0; + return true; + } + return false; + }, + ), + }; + } + if (isAfter(chargingScheduleInterval.end, targetInterval.end)) { + return { + ...chargingSchedule, + duration: differenceInSeconds(targetInterval.end as Date, chargingScheduleInterval.start), + chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) => + isWithinInterval( + addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!, + targetInterval, + ), + ), + }; + } + return chargingSchedule; + } + }; + private static buildSampledValue( sampledValueTemplate: SampledValueTemplate, value: number, @@ -996,25 +1253,25 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { } } - private static getMeasurandDefaultUnit( - measurandType: OCPP16MeterValueMeasurand, - ): MeterValueUnit | undefined { - switch (measurandType) { - case OCPP16MeterValueMeasurand.CURRENT_EXPORT: - case OCPP16MeterValueMeasurand.CURRENT_IMPORT: - case OCPP16MeterValueMeasurand.CURRENT_OFFERED: - return MeterValueUnit.AMP; - case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER: - case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER: - return MeterValueUnit.WATT_HOUR; - case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT: - case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT: - case OCPP16MeterValueMeasurand.POWER_OFFERED: - return MeterValueUnit.WATT; - case OCPP16MeterValueMeasurand.STATE_OF_CHARGE: - return MeterValueUnit.PERCENT; - case OCPP16MeterValueMeasurand.VOLTAGE: - return MeterValueUnit.VOLT; - } - } + // private static getMeasurandDefaultUnit( + // measurandType: OCPP16MeterValueMeasurand, + // ): MeterValueUnit | undefined { + // switch (measurandType) { + // case OCPP16MeterValueMeasurand.CURRENT_EXPORT: + // case OCPP16MeterValueMeasurand.CURRENT_IMPORT: + // case OCPP16MeterValueMeasurand.CURRENT_OFFERED: + // return MeterValueUnit.AMP; + // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER: + // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER: + // return MeterValueUnit.WATT_HOUR; + // case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT: + // case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT: + // case OCPP16MeterValueMeasurand.POWER_OFFERED: + // return MeterValueUnit.WATT; + // case OCPP16MeterValueMeasurand.STATE_OF_CHARGE: + // return MeterValueUnit.PERCENT; + // case OCPP16MeterValueMeasurand.VOLTAGE: + // return MeterValueUnit.VOLT; + // } + // } }