X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2Focpp%2F1.6%2FOCPP16ServiceUtils.ts;h=b510051e2085a3ce0d006a1765f05be7603a3318;hb=f406808ff62ed65ae71e268884f8b8e78c6fdbc6;hp=e3899cd5db97ffca80caaa8dc2cbe0b88ddcb829;hpb=991fb26bd3814b583395f5d2623acfae7a3ee3f0;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 e3899cd5..b510051e 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -1,14 +1,27 @@ // 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 { type ChargingStation, getIdTagsFile } from '../../../charging-station'; +import { OCPP16Constants } from './OCPP16Constants'; +import { + type ChargingStation, + hasFeatureProfile, + hasReservationExpired, +} from '../../../charging-station'; import { OCPPError } from '../../../exception'; import { type ClearChargingProfileRequest, - type ConnectorStatus, CurrentType, ErrorType, + type GenericResponse, type JsonType, type MeasurandPerPhaseSampledValueTemplates, type MeasurandValues, @@ -16,9 +29,11 @@ import { MeterValueLocation, MeterValueUnit, OCPP16AuthorizationStatus, - type OCPP16AuthorizeRequest, - type OCPP16AuthorizeResponse, + OCPP16AvailabilityType, + type OCPP16ChangeAvailabilityResponse, + OCPP16ChargePointStatus, type OCPP16ChargingProfile, + type OCPP16ChargingSchedule, type OCPP16IncomingRequestCommand, type OCPP16MeterValue, OCPP16MeterValueMeasurand, @@ -26,6 +41,7 @@ import { OCPP16RequestCommand, type OCPP16SampledValue, OCPP16StandardParametersKey, + OCPP16StopTransactionReason, type OCPP16SupportedFeatureProfiles, OCPPVersion, type SampledValueTemplate, @@ -37,12 +53,10 @@ import { DCElectricUtils, convertToFloat, convertToInt, - formatDurationMilliSeconds, getRandomFloatFluctuatedRounded, getRandomFloatRounded, getRandomInteger, isNotEmptyArray, - isNotEmptyString, isNullOrUndefined, isUndefined, logger, @@ -56,7 +70,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { featureProfile: OCPP16SupportedFeatureProfiles, command: OCPP16RequestCommand | OCPP16IncomingRequestCommand, ): boolean { - if (!chargingStation.hasFeatureProfile(featureProfile)) { + if (!hasFeatureProfile(chargingStation, featureProfile)) { logger.warn( `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${ OCPP16StandardParametersKey.SupportedFeatureProfiles @@ -727,9 +741,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { `${chargingStation.logPrefix()} MeterValues measurand ${ meterValue.sampledValue[sampledValuesIndex].measurand ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${formatDurationMilliSeconds( - interval, - )}(${roundTo(interval, 4)}ms)`, + }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms`, ); } } @@ -796,6 +808,55 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return meterValues; } + public static remoteStopTransaction = async ( + chargingStation: ChargingStation, + connectorId: number, + ): Promise => { + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, + connectorId, + OCPP16ChargePointStatus.Finishing, + ); + const stopResponse = await chargingStation.stopTransactionOnConnector( + connectorId, + OCPP16StopTransactionReason.REMOTE, + ); + if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) { + return OCPP16Constants.OCPP_RESPONSE_ACCEPTED; + } + return OCPP16Constants.OCPP_RESPONSE_REJECTED; + }; + + public static changeAvailability = async ( + chargingStation: ChargingStation, + connectorIds: number[], + chargePointStatus: OCPP16ChargePointStatus, + availabilityType: OCPP16AvailabilityType, + ): Promise => { + const responses: OCPP16ChangeAvailabilityResponse[] = []; + for (const connectorId of connectorIds) { + let response: OCPP16ChangeAvailabilityResponse = + OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED; + const connectorStatus = chargingStation.getConnectorStatus(connectorId)!; + if (connectorStatus?.transactionStarted === true) { + response = OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED; + } + connectorStatus.availability = availabilityType; + if (response === OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) { + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, + connectorId, + chargePointStatus, + ); + } + responses.push(response); + } + if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED)) { + return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED; + } + return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED; + }; + public static setChargingProfile( chargingStation: ChargingStation, connectorId: number, @@ -811,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 = []; } @@ -838,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; } @@ -876,6 +932,80 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return clearedCP; }; + public static composeChargingSchedules = ( + chargingSchedule1: OCPP16ChargingSchedule | undefined, + chargingSchedule2: OCPP16ChargingSchedule | undefined, + targetInterval: Interval, + ): OCPP16ChargingSchedule | undefined => { + if (!chargingSchedule1 && !chargingSchedule2) { + return undefined; + } + if (chargingSchedule1 && !chargingSchedule2) { + return OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1, targetInterval); + } + if (!chargingSchedule1 && chargingSchedule2) { + return OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2, targetInterval); + } + const compositeChargingSchedule1: OCPP16ChargingSchedule | undefined = + OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1!, targetInterval); + const compositeChargingSchedule2: OCPP16ChargingSchedule | undefined = + OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2!, targetInterval); + const compositeChargingScheduleInterval1: Interval = { + start: compositeChargingSchedule1!.startSchedule!, + end: addSeconds( + compositeChargingSchedule1!.startSchedule!, + compositeChargingSchedule1!.duration!, + ), + }; + const compositeChargingScheduleInterval2: Interval = { + start: compositeChargingSchedule2!.startSchedule!, + end: addSeconds( + compositeChargingSchedule2!.startSchedule!, + compositeChargingSchedule2!.duration!, + ), + }; + if ( + !areIntervalsOverlapping( + compositeChargingScheduleInterval1, + compositeChargingScheduleInterval2, + ) + ) { + return { + ...OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1!, targetInterval)!, + ...OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2!, targetInterval)!, + }; + } + // FIXME: Handle overlapping intervals + }; + + 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, @@ -889,28 +1019,68 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ); } - public static async isIdTagAuthorized( - chargingStation: ChargingStation, - connectorId: number, - idTag: string, - ): Promise { - let authorized = false; - const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!; - if (OCPP16ServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag)) { - connectorStatus.localAuthorizeIdTag = idTag; - connectorStatus.idTagLocalAuthorized = true; - authorized = true; - } else if (chargingStation.getMustAuthorizeAtRemoteStart() === true) { - connectorStatus.authorizeIdTag = idTag; - authorized = await OCPP16ServiceUtils.isIdTagRemoteAuthorized(chargingStation, idTag); - } else { - logger.warn( - `${chargingStation.logPrefix()} The charging station configuration expects authorize at - remote start transaction but local authorization or authorize isn't enabled`, - ); + 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; } - return authorized; - } + }; private static buildSampledValue( sampledValueTemplate: SampledValueTemplate, @@ -966,51 +1136,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 isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean { - return ( - chargingStation.getLocalAuthListEnabled() === true && - chargingStation.hasIdTags() === true && - isNotEmptyString( - chargingStation.idTagsCache - .getIdTags(getIdTagsFile(chargingStation.stationInfo)!) - ?.find((tag) => tag === idTag), - ) - ); - } - - private static async isIdTagRemoteAuthorized( - chargingStation: ChargingStation, - idTag: string, - ): Promise { - const authorizeResponse: OCPP16AuthorizeResponse = - await chargingStation.ocppRequestService.requestHandler< - OCPP16AuthorizeRequest, - OCPP16AuthorizeResponse - >(chargingStation, OCPP16RequestCommand.AUTHORIZE, { - idTag: idTag, - }); - return authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED; - } + // 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; + // } + // } }