From 90aceaf6e0cd749bc5feb4e50c89f2a213c53108 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Mon, 31 Jul 2023 19:26:53 +0200 Subject: [PATCH] fix: fix reservationId payload field filling at start transaction MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- src/charging-station/ChargingStation.ts | 54 ++++++------------- src/charging-station/Helpers.ts | 37 ++++++++++++- .../ChargingStationWorkerBroadcastChannel.ts | 2 +- src/charging-station/index.ts | 6 ++- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 27 ++-------- .../ocpp/1.6/OCPP16RequestService.ts | 13 +++++ .../ocpp/1.6/OCPP16ResponseService.ts | 36 +++++++++---- .../ocpp/1.6/OCPP16ServiceUtils.ts | 29 +++++++++- .../ui-server/UIServerUtils.ts | 2 +- src/utils/Constants.ts | 2 +- 10 files changed, 130 insertions(+), 78 deletions(-) diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index d3b53bd2..b6aa7b98 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -31,7 +31,6 @@ import { checkConnectorsConfiguration, checkStationInfoConnectorStatus, checkTemplate, - countReservableConnectors, createBootNotificationRequest, createSerialNumber, getAmperageLimitationUnitDivider, @@ -42,10 +41,13 @@ import { getHashId, getIdTagsFile, getMaxNumberOfEvses, + getNumberOfReservableConnectors, getPhaseRotationValue, hasFeatureProfile, + hasReservationExpired, initializeConnectorsMapStatus, propagateSerialNumber, + removeExpiredReservations, stationTemplateToStationInfo, warnTemplateKeysDeprecation, } from './Helpers'; @@ -962,7 +964,7 @@ export class ChargingStation { public async removeReservation( reservation: Reservation, - reason?: ReservationTerminationReason, + reason: ReservationTerminationReason, ): Promise { const connector = this.getConnectorStatus(reservation.connectorId)!; switch (reason) { @@ -983,7 +985,8 @@ export class ChargingStation { delete connector.reservation; break; default: - break; + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Unknown reservation termination reason '${reason}'`); } } @@ -1013,12 +1016,16 @@ export class ChargingStation { idTag?: string, connectorId?: number, ): boolean { - const reservationExists = !isUndefined(this.getReservationBy('reservationId', reservationId)); + const reservation = this.getReservationBy('reservationId', reservationId); + const reservationExists = !isUndefined(reservation) && !hasReservationExpired(reservation!); if (arguments.length === 1) { return !reservationExists; } else if (arguments.length > 1) { + const userReservation = !isUndefined(idTag) + ? this.getReservationBy('idTag', idTag!) + : undefined; const userReservationExists = - !isUndefined(idTag) && isUndefined(this.getReservationBy('idTag', idTag!)) ? false : true; + !isUndefined(userReservation) && !hasReservationExpired(userReservation!); const notConnectorZero = isUndefined(connectorId) ? true : connectorId! > 0; const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0; return ( @@ -1038,34 +1045,7 @@ export class ChargingStation { )}`, ); this.reservationExpirationSetInterval = setInterval((): void => { - const currentDate = new Date(); - if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - for (const connectorStatus of evseStatus.connectors.values()) { - if ( - connectorStatus.reservation && - connectorStatus.reservation.expiryDate < currentDate - ) { - this.removeReservation( - connectorStatus.reservation, - ReservationTerminationReason.EXPIRED, - ).catch(Constants.EMPTY_FUNCTION); - } - } - } - } else { - for (const connectorStatus of this.connectors.values()) { - if ( - connectorStatus.reservation && - connectorStatus.reservation.expiryDate < currentDate - ) { - this.removeReservation( - connectorStatus.reservation, - ReservationTerminationReason.EXPIRED, - ).catch(Constants.EMPTY_FUNCTION); - } - } - } + removeExpiredReservations(this).catch(Constants.EMPTY_FUNCTION); }, interval); } } @@ -1082,15 +1062,15 @@ export class ChargingStation { // } private getNumberOfReservableConnectors(): number { - let reservableConnectors = 0; + let numberOfReservableConnectors = 0; if (this.hasEvses) { for (const evseStatus of this.evses.values()) { - reservableConnectors += countReservableConnectors(evseStatus.connectors); + numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors); } } else { - reservableConnectors = countReservableConnectors(this.connectors); + numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors); } - return reservableConnectors - this.getNumberOfReservationsOnConnectorZero(); + return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero(); } private getNumberOfReservationsOnConnectorZero(): number { diff --git a/src/charging-station/Helpers.ts b/src/charging-station/Helpers.ts index 6cd270ff..d2b1b926 100644 --- a/src/charging-station/Helpers.ts +++ b/src/charging-station/Helpers.ts @@ -14,6 +14,7 @@ import { isAfter, isBefore, isDate, + isPast, isWithinInterval, toDate, } from 'date-fns'; @@ -42,6 +43,8 @@ import { type OCPP20BootNotificationRequest, OCPPVersion, RecurrencyKindType, + type Reservation, + ReservationTerminationReason, StandardParametersKey, SupportedFeatureProfiles, Voltage, @@ -82,7 +85,39 @@ export const getChargingStationId = ( )}${idSuffix}`; }; -export const countReservableConnectors = (connectors: Map) => { +export const hasReservationExpired = (reservation: Reservation): boolean => { + return isPast(reservation.expiryDate); +}; + +export const removeExpiredReservations = async ( + chargingStation: ChargingStation, +): Promise => { + if (chargingStation.hasEvses) { + for (const evseStatus of chargingStation.evses.values()) { + for (const connectorStatus of evseStatus.connectors.values()) { + if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) { + await chargingStation.removeReservation( + connectorStatus.reservation, + ReservationTerminationReason.EXPIRED, + ); + } + } + } + } else { + for (const connectorStatus of chargingStation.connectors.values()) { + if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) { + await chargingStation.removeReservation( + connectorStatus.reservation, + ReservationTerminationReason.EXPIRED, + ); + } + } + } +}; + +export const getNumberOfReservableConnectors = ( + connectors: Map, +): number => { let reservableConnectors = 0; for (const [connectorId, connectorStatus] of connectors) { if (connectorId === 0) { diff --git a/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts b/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts index 83684c64..fff823c0 100644 --- a/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts +++ b/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts @@ -317,7 +317,7 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne this.cleanRequestPayload(command, requestPayload); return this.commandHandlers.get(command)!(requestPayload); } - throw new BaseError(`Unknown worker broadcast channel command: ${command}`); + throw new BaseError(`Unknown worker broadcast channel command: '${command}'`); } private cleanRequestPayload( diff --git a/src/charging-station/index.ts b/src/charging-station/index.ts index 0d15d676..e1627f99 100644 --- a/src/charging-station/index.ts +++ b/src/charging-station/index.ts @@ -6,8 +6,10 @@ export { setConfigurationKeyValue, } from './ConfigurationKeyUtils'; export { - getIdTagsFile, checkChargingStation, - resetConnectorStatus, + getIdTagsFile, hasFeatureProfile, + hasReservationExpired, + removeExpiredReservations, + resetConnectorStatus, } from './Helpers'; diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 567c3179..7a9e1a4e 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -15,6 +15,7 @@ import { type ChargingStation, checkChargingStation, getConfigurationKey, + removeExpiredReservations, setConfigurationKeyValue, } from '../../../charging-station'; import { OCPPError } from '../../../exception'; @@ -833,15 +834,6 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { idTag, ); } - if ( - (chargingStation.getConnectorStatus(transactionConnectorId)?.status === - OCPP16ChargePointStatus.Reserved && - chargingStation.getReservationBy('connectorId', transactionConnectorId)?.idTag !== idTag) || - (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved && - chargingStation.getReservationBy('connectorId', 0)?.idTag !== idTag) - ) { - return OCPP16Constants.OCPP_RESPONSE_REJECTED; - } const remoteStartTransactionLogMsg = ` ${chargingStation.logPrefix()} Transaction remotely STARTED on ${ chargingStation.stationInfo.chargingStationId @@ -874,12 +866,6 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, { connectorId: transactionConnectorId, idTag, - reservationId: chargingStation.getReservationBy( - 'connectorId', - chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved - ? 0 - : transactionConnectorId, - )!, }) ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED ) { @@ -915,12 +901,6 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, { connectorId: transactionConnectorId, idTag, - reservationId: chargingStation.getReservationBy( - 'connectorId', - chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved - ? 0 - : transactionConnectorId, - )!, }) ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED ) { @@ -1527,6 +1507,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { if (!(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, connectorId, idTag))) { return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED; } + await removeExpiredReservations(chargingStation); switch (chargingStation.getConnectorStatus(connectorId)!.status) { case OCPP16ChargePointStatus.Faulted: response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED; @@ -1588,8 +1569,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { const { reservationId } = commandPayload; const reservation = chargingStation.getReservationBy('reservationId', reservationId); if (isUndefined(reservation)) { - logger.error( - `${chargingStation.logPrefix()} Reservation with ID ${reservationId} + logger.debug( + `${chargingStation.logPrefix()} Reservation with id ${reservationId} does not exist on charging station`, ); return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED; diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index ac1c1ac3..5c1c129f 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -12,6 +12,7 @@ import { type JsonType, type OCPP16AuthorizeRequest, type OCPP16BootNotificationRequest, + OCPP16ChargePointStatus, type OCPP16DataTransferRequest, type OCPP16DiagnosticsStatusNotificationRequest, type OCPP16FirmwareStatusNotificationRequest, @@ -183,6 +184,18 @@ export class OCPP16RequestService extends OCPPRequestService { true, ), timestamp: new Date(), + ...(OCPP16ServiceUtils.hasReservation( + chargingStation, + commandParams?.connectorId as number, + commandParams?.idTag as string, + ) && { + reservationId: chargingStation.getReservationBy( + 'connectorId', + chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved + ? 0 + : (commandParams?.connectorId as number), + )!.reservationId, + }), ...commandParams, } as unknown as Request; case OCPP16RequestCommand.STOP_TRANSACTION: diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index ae8b6287..f1a17068 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -10,6 +10,7 @@ import { type ChargingStation, addConfigurationKey, getConfigurationKey, + hasReservationExpired, resetConnectorStatus, } from '../../../charging-station'; import { OCPPError } from '../../../exception'; @@ -639,18 +640,31 @@ export class OCPP16ResponseService extends OCPPResponseService { transactionConnectorId, requestPayload.meterStart, ); - const reservedOnConnectorZero = - chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved; - if ( - chargingStation.getConnectorStatus(transactionConnectorId)?.status === - OCPP16ChargePointStatus.Reserved || - reservedOnConnectorZero - ) { + if (requestPayload.reservationId) { + const reservation = chargingStation.getReservationBy( + 'reservationId', + requestPayload.reservationId, + )!; + if (reservation.idTag !== requestPayload.idTag) { + logger.warn( + `${chargingStation.logPrefix()} Transaction reserved ${ + payload.transactionId + } started with a different idTag ${requestPayload.idTag} than the reservation one ${ + reservation.idTag + }`, + ); + } + if (hasReservationExpired(reservation)) { + logger.warn( + `${chargingStation.logPrefix()} Transaction reserved ${ + payload.transactionId + } started with expired reservation ${ + requestPayload.reservationId + } (expiry date: ${reservation.expiryDate.toISOString()}))`, + ); + } await chargingStation.removeReservation( - chargingStation.getReservationBy( - 'connectorId', - reservedOnConnectorZero ? 0 : transactionConnectorId, - )!, + reservation, ReservationTerminationReason.TRANSACTION_STARTED, ); } diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index b70be6fa..b6229b90 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -3,7 +3,11 @@ import type { JSONSchemaType } from 'ajv'; 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, @@ -924,6 +928,29 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return clearedCP; }; + 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 && + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (hasReservationExpired(connectorReservation) || connectorReservation?.idTag !== idTag)) || + (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved && + chargingStationReservation && + (hasReservationExpired(chargingStationReservation) || + chargingStationReservation?.idTag !== idTag)) + ) { + return false; + } + return true; + }; + public static parseJsonSchemaFile( relativePath: string, moduleName?: string, diff --git a/src/charging-station/ui-server/UIServerUtils.ts b/src/charging-station/ui-server/UIServerUtils.ts index 79f8c34a..95bbba56 100644 --- a/src/charging-station/ui-server/UIServerUtils.ts +++ b/src/charging-station/ui-server/UIServerUtils.ts @@ -26,7 +26,7 @@ export class UIServerUtils { logger.error( `${logPrefix( ' UI WebSocket Server |', - )} Unsupported protocol: ${protocol} or protocol version: ${version}`, + )} Unsupported protocol: '${protocol}' or protocol version: '${version}'`, ); return false; }; diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index c4ce0ff8..8ab45e63 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -55,7 +55,7 @@ export class Constants { /* This is intentional */ }); - static readonly DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL = 5000; // Ms + static readonly DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL = 60000; // Ms private constructor() { // This is intentional -- 2.34.1