From: Jérôme Benoit Date: Mon, 5 Jun 2023 15:41:23 +0000 (+0200) Subject: Merge pull request #574 from JulianHBuecher/reservation-feature X-Git-Tag: v1.2.16~13 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=6e3d9d04815abfe85d31735fc38bdf0253d85026;hp=1135386576395b90768d5e03a16fb44d340361e0;p=e-mobility-charging-stations-simulator.git Merge pull request #574 from JulianHBuecher/reservation-feature Merge as is with trivial errors to be fixed in a subsequent commit. --- diff --git a/README.md b/README.md index e3a202ea..04771de2 100644 --- a/README.md +++ b/README.md @@ -395,8 +395,8 @@ make SUBMODULES_INIT=true #### Reservation Profile -- :x: CancelReservation -- :x: ReserveNow +- :white_check_mark: CancelReservation +- :white_check_mark: ReserveNow #### Smart Charging Profile @@ -466,7 +466,7 @@ All kind of OCPP parameters are supported in charging station configuration or c #### Reservation Profile -- _none_ +- :white_check_mark: ReserveConnectorZeroSupported (type: boolean) (units: -) #### Smart Charging Profile diff --git a/src/assets/station-templates/abb-atg.station-template.json b/src/assets/station-templates/abb-atg.station-template.json index 9414a19e..456089bf 100644 --- a/src/assets/station-templates/abb-atg.station-template.json +++ b/src/assets/station-templates/abb-atg.station-template.json @@ -29,7 +29,7 @@ { "key": "SupportedFeatureProfiles", "readonly": true, - "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger" + "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation" }, { "key": "LocalAuthListEnabled", @@ -45,6 +45,11 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "false" } ] }, diff --git a/src/assets/station-templates/abb.station-template.json b/src/assets/station-templates/abb.station-template.json index 0c6b233a..a95517e7 100644 --- a/src/assets/station-templates/abb.station-template.json +++ b/src/assets/station-templates/abb.station-template.json @@ -29,7 +29,7 @@ { "key": "SupportedFeatureProfiles", "readonly": true, - "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger" + "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation" }, { "key": "LocalAuthListEnabled", @@ -45,6 +45,11 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "false" } ] }, diff --git a/src/assets/station-templates/chargex.station-template.json b/src/assets/station-templates/chargex.station-template.json index 6ff7562d..fca57094 100644 --- a/src/assets/station-templates/chargex.station-template.json +++ b/src/assets/station-templates/chargex.station-template.json @@ -69,6 +69,11 @@ { "key": "TransactionMessageRetryInterval", "value": "20" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "false" } ] }, diff --git a/src/assets/station-templates/evlink.station-template.json b/src/assets/station-templates/evlink.station-template.json index fda88bfb..5b9f9eac 100644 --- a/src/assets/station-templates/evlink.station-template.json +++ b/src/assets/station-templates/evlink.station-template.json @@ -30,7 +30,7 @@ { "key": "SupportedFeatureProfiles", "readonly": true, - "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger" + "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation" }, { "key": "LocalAuthListEnabled", @@ -46,6 +46,11 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "false" } ] }, diff --git a/src/assets/station-templates/keba.station-template.json b/src/assets/station-templates/keba.station-template.json index 0d958365..6c0e3437 100644 --- a/src/assets/station-templates/keba.station-template.json +++ b/src/assets/station-templates/keba.station-template.json @@ -27,7 +27,7 @@ { "key": "SupportedFeatureProfiles", "readonly": true, - "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger" + "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation" }, { "key": "LocalAuthListEnabled", @@ -43,6 +43,11 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "false" } ] }, diff --git a/src/assets/station-templates/schneider-evses.station-template.json b/src/assets/station-templates/schneider-evses.station-template.json index 7017f1e4..f292519e 100644 --- a/src/assets/station-templates/schneider-evses.station-template.json +++ b/src/assets/station-templates/schneider-evses.station-template.json @@ -43,6 +43,11 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "false" } ] }, diff --git a/src/assets/station-templates/schneider-imredd.station-template.json b/src/assets/station-templates/schneider-imredd.station-template.json index 0369d0ac..a4411047 100644 --- a/src/assets/station-templates/schneider-imredd.station-template.json +++ b/src/assets/station-templates/schneider-imredd.station-template.json @@ -29,7 +29,7 @@ { "key": "SupportedFeatureProfiles", "readonly": true, - "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger" + "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation" }, { "key": "LocalAuthListEnabled", @@ -45,6 +45,11 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "false" } ] }, diff --git a/src/assets/station-templates/schneider.station-template.json b/src/assets/station-templates/schneider.station-template.json index 4dd71473..8fb25abe 100644 --- a/src/assets/station-templates/schneider.station-template.json +++ b/src/assets/station-templates/schneider.station-template.json @@ -29,7 +29,7 @@ { "key": "SupportedFeatureProfiles", "readonly": true, - "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger" + "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation" }, { "key": "LocalAuthListEnabled", @@ -45,6 +45,11 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "false" } ] }, diff --git a/src/assets/station-templates/siemens.station-template.json b/src/assets/station-templates/siemens.station-template.json index aca529ac..439b5f92 100644 --- a/src/assets/station-templates/siemens.station-template.json +++ b/src/assets/station-templates/siemens.station-template.json @@ -24,7 +24,7 @@ { "key": "SupportedFeatureProfiles", "readonly": true, - "value": "Core,LocalAuthListManagement" + "value": "Core,LocalAuthListManagement,Reservation" }, { "key": "LocalAuthListEnabled", @@ -40,6 +40,11 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "false" } ] }, diff --git a/src/assets/station-templates/virtual-simple-atg.station-template.json b/src/assets/station-templates/virtual-simple-atg.station-template.json index 0dbd6782..aedd27cb 100644 --- a/src/assets/station-templates/virtual-simple-atg.station-template.json +++ b/src/assets/station-templates/virtual-simple-atg.station-template.json @@ -24,7 +24,7 @@ { "key": "SupportedFeatureProfiles", "readonly": true, - "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger" + "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation" }, { "key": "LocalAuthListEnabled", @@ -40,6 +40,11 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "false" } ] }, diff --git a/src/assets/station-templates/virtual-simple.station-template.json b/src/assets/station-templates/virtual-simple.station-template.json index d94b5d39..37cbe8ad 100644 --- a/src/assets/station-templates/virtual-simple.station-template.json +++ b/src/assets/station-templates/virtual-simple.station-template.json @@ -24,7 +24,7 @@ { "key": "SupportedFeatureProfiles", "readonly": true, - "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger" + "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation" }, { "key": "LocalAuthListEnabled", @@ -40,6 +40,11 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "false" } ] }, diff --git a/src/assets/station-templates/virtual.station-template.json b/src/assets/station-templates/virtual.station-template.json index 3980cba6..09086f8e 100644 --- a/src/assets/station-templates/virtual.station-template.json +++ b/src/assets/station-templates/virtual.station-template.json @@ -24,7 +24,7 @@ { "key": "SupportedFeatureProfiles", "readonly": true, - "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger" + "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation" }, { "key": "LocalAuthListEnabled", @@ -40,6 +40,11 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "false" } ] }, diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 9a8f468b..a3f67696 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -67,6 +67,9 @@ import { PowerUnits, RegistrationStatusEnumType, RequestCommand, + type Reservation, + ReservationFilterKey, + ReservationTerminationReason, type Response, StandardParametersKey, type Status, @@ -135,6 +138,7 @@ export class ChargingStation { private readonly sharedLRUCache: SharedLRUCache; private webSocketPingSetInterval!: NodeJS.Timeout; private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel; + private reservationExpiryDateSetInterval?: NodeJS.Timeout; constructor(index: number, templateFile: string) { this.started = false; @@ -551,7 +555,8 @@ export class ChargingStation { ); } else { logger.error( - `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat` + `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, + not starting the heartbeat` ); } } @@ -579,13 +584,15 @@ export class ChargingStation { } if (!this.getConnectorStatus(connectorId)) { logger.error( - `${this.logPrefix()} Trying to start MeterValues on non existing connector id ${connectorId.toString()}` + `${this.logPrefix()} Trying to start MeterValues on non existing connector id + ${connectorId.toString()}` ); return; } if (this.getConnectorStatus(connectorId)?.transactionStarted === false) { logger.error( - `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started` + `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} + with no transaction started` ); return; } else if ( @@ -593,7 +600,8 @@ export class ChargingStation { Utils.isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId) ) { logger.error( - `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id` + `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} + with no transaction id` ); return; } @@ -645,6 +653,9 @@ export class ChargingStation { if (this.getEnableStatistics() === true) { this.performanceStatistics?.start(); } + if (this.hasFeatureProfile(SupportedFeatureProfiles.Reservation)) { + this.startReservationExpiryDateSetInterval(); + } this.openWSConnection(); // Monitor charging station template file this.templateFileWatcher = watchJsonFile( @@ -754,7 +765,8 @@ export class ChargingStation { params = { ...{ closeOpened: false, terminateOpened: false }, ...params }; if (this.started === false && this.starting === false) { logger.warn( - `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()} on stopped charging station` + `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()} + on stopped charging station` ); return; } @@ -773,7 +785,8 @@ export class ChargingStation { if (this.isWebSocketConnectionOpened() === true) { logger.warn( - `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened` + `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} + is already opened` ); return; } @@ -908,6 +921,199 @@ export class ChargingStation { ); } + public getReservationOnConnectorId0Enabled(): boolean { + return Utils.convertToBoolean( + ChargingStationConfigurationUtils.getConfigurationKey( + this, + StandardParametersKey.ReserveConnectorZeroSupported + ).value + ); + } + + public async addReservation(reservation: Reservation): Promise { + const [exists, reservationFound] = this.doesReservationExists(reservation); + if (exists) { + await this.removeReservation(reservationFound); + } + const connectorStatus = this.getConnectorStatus(reservation.connectorId); + connectorStatus.reservation = reservation; + connectorStatus.status = ConnectorStatusEnum.Reserved; + if (reservation.connectorId === 0) { + return; + } + await this.ocppRequestService.requestHandler< + StatusNotificationRequest, + StatusNotificationResponse + >( + this, + RequestCommand.STATUS_NOTIFICATION, + OCPPServiceUtils.buildStatusNotificationRequest( + this, + reservation.connectorId, + ConnectorStatusEnum.Reserved + ) + ); + } + + public async removeReservation( + reservation: Reservation, + reason?: ReservationTerminationReason + ): Promise { + const connector = this.getConnectorStatus(reservation.connectorId); + switch (reason) { + case ReservationTerminationReason.TRANSACTION_STARTED: { + delete connector.reservation; + if (reservation.connectorId === 0) { + connector.status = ConnectorStatusEnum.Available; + } + break; + } + case ReservationTerminationReason.CONNECTOR_STATE_CHANGED: { + delete connector.reservation; + break; + } + default: { + // ReservationTerminationReason.EXPIRED, ReservationTerminationReason.CANCELED + connector.status = ConnectorStatusEnum.Available; + delete connector.reservation; + await this.ocppRequestService.requestHandler< + StatusNotificationRequest, + StatusNotificationResponse + >( + this, + RequestCommand.STATUS_NOTIFICATION, + OCPPServiceUtils.buildStatusNotificationRequest( + this, + reservation.connectorId, + ConnectorStatusEnum.Available + ) + ); + break; + } + } + } + + public getReservationBy(key: string, value: number | string): Reservation { + if (this.hasEvses) { + for (const evse of this.evses.values()) { + for (const connector of evse.connectors.values()) { + if (connector?.reservation?.[key] === value) { + return connector.reservation; + } + } + } + } else { + for (const connector of this.connectors.values()) { + if (connector?.reservation?.[key] === value) { + return connector.reservation; + } + } + } + } + + public doesReservationExists(reservation: Partial): [boolean, Reservation] { + const foundReservation = this.getReservationBy( + ReservationFilterKey.RESERVATION_ID, + reservation?.id + ); + return Utils.isUndefined(foundReservation) ? [false, null] : [true, foundReservation]; + } + + public startReservationExpiryDateSetInterval(customInterval?: number): void { + const interval = + customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL; + logger.info( + `${this.logPrefix()} Reservation expiration date interval is set to ${interval} + and starts on CS now` + ); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this.reservationExpiryDateSetInterval = setInterval(async (): Promise => { + if (this.hasEvses) { + for (const evse of this.evses.values()) { + for (const connector of evse.connectors.values()) { + if (connector?.reservation?.expiryDate.toString() < new Date().toISOString()) { + await this.removeReservation(connector.reservation); + } + } + } + } else { + for (const connector of this.connectors.values()) { + if (connector?.reservation?.expiryDate.toString() < new Date().toISOString()) { + await this.removeReservation(connector.reservation); + } + } + } + }, interval); + } + + public restartReservationExpiryDateSetInterval(): void { + this.stopReservationExpiryDateSetInterval(); + this.startReservationExpiryDateSetInterval(); + } + + public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean { + const reservation = this.getReservationBy(ReservationFilterKey.CONNECTOR_ID, connectorId); + return !Utils.isUndefined(reservation) && reservation.idTag === idTag; + } + + public isConnectorReservable( + reservationId: number, + idTag?: string, + connectorId?: number + ): boolean { + const [alreadyExists] = this.doesReservationExists({ id: reservationId }); + if (alreadyExists) { + return alreadyExists; + } + const userReservedAlready = Utils.isUndefined( + this.getReservationBy(ReservationFilterKey.ID_TAG, idTag) + ) + ? false + : true; + const notConnectorZero = Utils.isUndefined(connectorId) ? true : connectorId > 0; + const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0; + return !alreadyExists && !userReservedAlready && notConnectorZero && freeConnectorsAvailable; + } + + private getNumberOfReservableConnectors(): number { + let reservableConnectors = 0; + if (this.hasEvses) { + for (const evse of this.evses.values()) { + reservableConnectors = this.countReservableConnectors(evse.connectors); + } + } else { + reservableConnectors = this.countReservableConnectors(this.connectors); + } + return reservableConnectors - this.getNumberOfReservationsOnConnectorZero(); + } + + private countReservableConnectors(connectors: Map) { + let reservableConnectors = 0; + for (const [id, connector] of connectors) { + if (id === 0) { + continue; + } + if (connector.status === ConnectorStatusEnum.Available) { + ++reservableConnectors; + } + } + return reservableConnectors; + } + + private getNumberOfReservationsOnConnectorZero(): number { + let numberOfReservations = 0; + if (this.hasEvses) { + for (const evse of this.evses.values()) { + if (evse.connectors.get(0)?.reservation) { + ++numberOfReservations; + } + } + } else if (this.connectors.get(0)?.reservation) { + ++numberOfReservations; + } + return numberOfReservations; + } + private flushMessageBuffer(): void { if (this.messageBuffer.size > 0) { for (const message of this.messageBuffer.values()) { @@ -935,6 +1141,12 @@ export class ChargingStation { return this.stationInfo.supervisionUrlOcppConfiguration ?? false; } + private stopReservationExpiryDateSetInterval(): void { + if (this.reservationExpiryDateSetInterval) { + clearInterval(this.reservationExpiryDateSetInterval); + } + } + private getSupervisionUrlOcppKey(): string { return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl; } @@ -1083,7 +1295,8 @@ export class ChargingStation { } private handleUnsupportedVersion(version: OCPPVersion) { - const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`; + const errorMsg = `Unsupported protocol version '${version}' configured + in template file ${this.templateFile}`; logger.error(`${this.logPrefix()} ${errorMsg}`); throw new BaseError(errorMsg); } diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index f01034a3..27045c89 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -24,6 +24,7 @@ import { type ClearChargingProfileRequest, type ClearChargingProfileResponse, type ConnectorStatus, + ConnectorStatusEnum, ErrorType, type GenericResponse, GenericStatus, @@ -35,11 +36,11 @@ import { type JsonObject, type JsonType, OCPP16AuthorizationStatus, - type OCPP16AuthorizeRequest, - type OCPP16AuthorizeResponse, OCPP16AvailabilityType, type OCPP16BootNotificationRequest, type OCPP16BootNotificationResponse, + type OCPP16CancelReservationRequest, + type OCPP16CancelReservationResponse, OCPP16ChargePointErrorCode, OCPP16ChargePointStatus, type OCPP16ChargingProfile, @@ -62,6 +63,8 @@ import { OCPP16IncomingRequestCommand, OCPP16MessageTrigger, OCPP16RequestCommand, + type OCPP16ReserveNowRequest, + type OCPP16ReserveNowResponse, OCPP16StandardParametersKey, type OCPP16StartTransactionRequest, type OCPP16StartTransactionResponse, @@ -77,13 +80,17 @@ import { OCPPVersion, type RemoteStartTransactionRequest, type RemoteStopTransactionRequest, + ReservationFilterKey, + ReservationTerminationReason, type ResetRequest, type SetChargingProfileRequest, type SetChargingProfileResponse, + type StartTransactionRequest, type UnlockConnectorRequest, type UnlockConnectorResponse, } from '../../../types'; import { Constants, Utils, logger } from '../../../utils'; +import { OCPPConstants } from '../OCPPConstants'; import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService'; const moduleName = 'OCPP16IncomingRequestService'; @@ -137,6 +144,11 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { [OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, this.handleRequestTriggerMessage.bind(this)], [OCPP16IncomingRequestCommand.DATA_TRANSFER, this.handleRequestDataTransfer.bind(this)], [OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, this.handleRequestUpdateFirmware.bind(this)], + [OCPP16IncomingRequestCommand.RESERVE_NOW, this.handleRequestReserveNow.bind(this)], + [ + OCPP16IncomingRequestCommand.CANCEL_RESERVATION, + this.handleRequestCancelReservation.bind(this), + ], ]); this.jsonSchemas = new Map>([ [ @@ -259,6 +271,22 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { 'constructor' ), ], + [ + OCPP16IncomingRequestCommand.RESERVE_NOW, + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ReserveNow.json', + moduleName, + 'constructor' + ), + ], + [ + OCPP16IncomingRequestCommand.CANCEL_RESERVATION, + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/CancelReservation.json', + moduleName, + 'constructor' + ), + ], ]); this.validatePayload = this.validatePayload.bind(this) as ( chargingStation: ChargingStation, @@ -310,7 +338,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { } catch (error) { // Log logger.error( - `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`, + `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: + Handle incoming request error:`, error ); throw error; @@ -363,7 +392,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ); } logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command '${commandName}' PDU validation` + `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found + for command '${commandName}' PDU validation` ); return false; } @@ -384,9 +414,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { logger.info( `${chargingStation.logPrefix()} ${ commandPayload.type - } reset command received, simulating it. The station will be back online in ${Utils.formatDurationMilliSeconds( - chargingStation.stationInfo.resetTime - )}` + } reset command received, simulating it. The station will be + back online in ${Utils.formatDurationMilliSeconds(chargingStation.stationInfo.resetTime)}` ); return OCPP16Constants.OCPP_RESPONSE_ACCEPTED; } @@ -398,7 +427,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { const connectorId = commandPayload.connectorId; if (chargingStation.hasConnector(connectorId) === false) { logger.error( - `${chargingStation.logPrefix()} Trying to unlock a non existing connector id ${connectorId.toString()}` + `${chargingStation.logPrefix()} Trying to unlock a non existing + connector id ${connectorId.toString()}` ); return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED; } @@ -545,9 +575,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { } if (chargingStation.hasConnector(commandPayload.connectorId) === false) { logger.error( - `${chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector id ${ - commandPayload.connectorId - }` + `${chargingStation.logPrefix()} Trying to set charging profile(s) to a + non existing connector id ${commandPayload.connectorId}` ); return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED; } @@ -566,9 +595,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { false) ) { logger.error( - `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${ - commandPayload.connectorId - } without a started transaction` + `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) + on connector ${commandPayload.connectorId} without a started transaction` ); return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED; } @@ -601,9 +629,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { } if (chargingStation.hasConnector(commandPayload.connectorId) === false) { logger.error( - `${chargingStation.logPrefix()} Trying to get composite schedule to a non existing connector id ${ - commandPayload.connectorId - }` + `${chargingStation.logPrefix()} Trying to get composite schedule to a + non existing connector id ${commandPayload.connectorId}` ); return OCPP16Constants.OCPP_RESPONSE_REJECTED; } @@ -651,9 +678,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { } if (chargingStation.hasConnector(commandPayload.connectorId) === false) { logger.error( - `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector id ${ - commandPayload.connectorId - }` + `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to + a non existing connector id ${commandPayload.connectorId}` ); return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN; } @@ -736,7 +762,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { const connectorId: number = commandPayload.connectorId; if (chargingStation.hasConnector(connectorId) === false) { logger.error( - `${chargingStation.logPrefix()} Trying to change the availability of a non existing connector id ${connectorId.toString()}` + `${chargingStation.logPrefix()} Trying to change the availability of a + non existing connector id ${connectorId.toString()}` ); return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED; } @@ -797,27 +824,40 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { chargingStation: ChargingStation, commandPayload: RemoteStartTransactionRequest ): Promise { - const transactionConnectorId = commandPayload.connectorId; + const { connectorId: transactionConnectorId, idTag, chargingProfile } = commandPayload; + const reserved = + chargingStation.getConnectorStatus(transactionConnectorId).status === + OCPP16ChargePointStatus.Reserved; + const reservedOnConnectorZero = + chargingStation.getConnectorStatus(0).status === OCPP16ChargePointStatus.Reserved; + if ( + (reserved && + !chargingStation.validateIncomingRequestWithReservation(transactionConnectorId, idTag)) || + (reservedOnConnectorZero && !chargingStation.validateIncomingRequestWithReservation(0, idTag)) + ) { + return OCPP16Constants.OCPP_RESPONSE_REJECTED; + } if (chargingStation.hasConnector(transactionConnectorId) === false) { return this.notifyRemoteStartTransactionRejected( chargingStation, transactionConnectorId, - commandPayload.idTag + idTag ); } if ( - chargingStation.isChargingStationAvailable() === false || - chargingStation.isConnectorAvailable(transactionConnectorId) === false + !chargingStation.isChargingStationAvailable() || + !chargingStation.isConnectorAvailable(transactionConnectorId) ) { return this.notifyRemoteStartTransactionRejected( chargingStation, transactionConnectorId, - commandPayload.idTag + idTag ); } - const remoteStartTransactionLogMsg = `${chargingStation.logPrefix()} Transaction remotely STARTED on ${ + const remoteStartTransactionLogMsg = ` + ${chargingStation.logPrefix()} Transaction remotely STARTED on ${ chargingStation.stationInfo.chargingStationId - }#${transactionConnectorId.toString()} for idTag '${commandPayload.idTag}'`; + }#${transactionConnectorId.toString()} for idTag '${idTag}'`; await OCPP16ServiceUtils.sendAndSetConnectorStatus( chargingStation, transactionConnectorId, @@ -825,77 +865,55 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ); const connectorStatus = chargingStation.getConnectorStatus(transactionConnectorId); // Check if authorized - if (chargingStation.getAuthorizeRemoteTxRequests() === true) { - let authorized = false; + if ( + chargingStation.getAuthorizeRemoteTxRequests() && + (await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, transactionConnectorId, idTag)) + ) { + // Authorization successful, start transaction if ( - chargingStation.getLocalAuthListEnabled() === true && - chargingStation.hasIdTags() === true && - Utils.isNotEmptyString( - chargingStation.idTagsCache - .getIdTags(ChargingStationUtils.getIdTagsFile(chargingStation.stationInfo)) - ?.find((idTag) => idTag === commandPayload.idTag) - ) + this.setRemoteStartTransactionChargingProfile( + chargingStation, + transactionConnectorId, + chargingProfile + ) === true ) { - connectorStatus.localAuthorizeIdTag = commandPayload.idTag; - connectorStatus.idTagLocalAuthorized = true; - authorized = true; - } else if (chargingStation.getMustAuthorizeAtRemoteStart() === true) { - connectorStatus.authorizeIdTag = commandPayload.idTag; - const authorizeResponse: OCPP16AuthorizeResponse = - await chargingStation.ocppRequestService.requestHandler< - OCPP16AuthorizeRequest, - OCPP16AuthorizeResponse - >(chargingStation, OCPP16RequestCommand.AUTHORIZE, { - idTag: commandPayload.idTag, - }); - if (authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) { - authorized = true; + connectorStatus.transactionRemoteStarted = true; + const startTransactionPayload: Partial = { + connectorId: transactionConnectorId, + idTag: idTag, + }; + if (reserved || reservedOnConnectorZero) { + const reservation = chargingStation.getReservationBy( + ReservationFilterKey.CONNECTOR_ID, + reservedOnConnectorZero ? 0 : transactionConnectorId + ); + startTransactionPayload.reservationId = reservation.id; + await chargingStation.removeReservation( + reservation, + ReservationTerminationReason.TRANSACTION_STARTED + ); } - } else { - logger.warn( - `${chargingStation.logPrefix()} The charging station configuration expects authorize at remote start transaction but local authorization or authorize isn't enabled` - ); - } - if (authorized === true) { - // Authorization successful, start transaction if ( - this.setRemoteStartTransactionChargingProfile( - chargingStation, - transactionConnectorId, - commandPayload.chargingProfile - ) === true + ( + await chargingStation.ocppRequestService.requestHandler< + OCPP16StartTransactionRequest, + OCPP16StartTransactionResponse + >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, startTransactionPayload) + ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED ) { - connectorStatus.transactionRemoteStarted = true; - if ( - ( - await chargingStation.ocppRequestService.requestHandler< - OCPP16StartTransactionRequest, - OCPP16StartTransactionResponse - >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, { - connectorId: transactionConnectorId, - idTag: commandPayload.idTag, - }) - ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED - ) { - logger.debug(remoteStartTransactionLogMsg); - return OCPP16Constants.OCPP_RESPONSE_ACCEPTED; - } - return this.notifyRemoteStartTransactionRejected( - chargingStation, - transactionConnectorId, - commandPayload.idTag - ); + logger.debug(remoteStartTransactionLogMsg); + return OCPP16Constants.OCPP_RESPONSE_ACCEPTED; } return this.notifyRemoteStartTransactionRejected( chargingStation, transactionConnectorId, - commandPayload.idTag + idTag ); } return this.notifyRemoteStartTransactionRejected( chargingStation, transactionConnectorId, - commandPayload.idTag + idTag ); } // No authorization check required, start transaction @@ -903,7 +921,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { this.setRemoteStartTransactionChargingProfile( chargingStation, transactionConnectorId, - commandPayload.chargingProfile + chargingProfile ) === true ) { connectorStatus.transactionRemoteStarted = true; @@ -914,7 +932,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { OCPP16StartTransactionResponse >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, { connectorId: transactionConnectorId, - idTag: commandPayload.idTag, + idTag, }) ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED ) { @@ -924,13 +942,13 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { return this.notifyRemoteStartTransactionRejected( chargingStation, transactionConnectorId, - commandPayload.idTag + idTag ); } return this.notifyRemoteStartTransactionRejected( chargingStation, transactionConnectorId, - commandPayload.idTag + idTag ); } @@ -949,7 +967,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ); } logger.warn( - `${chargingStation.logPrefix()} Remote starting transaction REJECTED on connector id ${connectorId.toString()}, idTag '${idTag}', availability '${ + `${chargingStation.logPrefix()} Remote starting transaction REJECTED on connector id + ${connectorId.toString()}, idTag '${idTag}', availability '${ chargingStation.getConnectorStatus(connectorId)?.availability }', status '${chargingStation.getConnectorStatus(connectorId)?.status}'` ); @@ -964,7 +983,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { if (cp && cp.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE) { OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, cp); logger.debug( - `${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction on connector id ${connectorId}: %j`, + `${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction + on connector id ${connectorId}: %j`, cp ); return true; @@ -1021,7 +1041,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { } } logger.warn( - `${chargingStation.logPrefix()} Trying to remote stop a non existing transaction with id: ${transactionId.toString()}` + `${chargingStation.logPrefix()} Trying to remote stop a non existing transaction with id: + ${transactionId.toString()}` ); return OCPP16Constants.OCPP_RESPONSE_REJECTED; } @@ -1038,7 +1059,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ) === false ) { logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: feature profile not supported` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: + Cannot simulate firmware update: feature profile not supported` ); return OCPP16Constants.OCPP_RESPONSE_EMPTY; } @@ -1047,7 +1069,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { chargingStation.stationInfo.firmwareStatus !== OCPP16FirmwareStatus.Installed ) { logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: firmware update is already in progress` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: + Cannot simulate firmware update: firmware update is already in progress` ); return OCPP16Constants.OCPP_RESPONSE_EMPTY; } @@ -1146,7 +1169,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { if (runningTransactions > 0) { const waitTime = 15 * 1000; logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.updateFirmwareSimulation: ${runningTransactions} transaction(s) in progress, waiting ${ + `${chargingStation.logPrefix()} ${moduleName}.updateFirmwareSimulation: + ${runningTransactions} transaction(s) in progress, waiting ${ waitTime / 1000 } seconds before continuing firmware update simulation` ); @@ -1234,7 +1258,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ) === false ) { logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Cannot get diagnostics: feature profile not supported` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: + Cannot get diagnostics: feature profile not supported` ); return OCPP16Constants.OCPP_RESPONSE_EMPTY; } @@ -1272,9 +1297,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { }) .catch((error) => { logger.error( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Error while sending '${ - OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION - }'`, + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: + Error while sending '${OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION}'`, error ); }); @@ -1499,4 +1523,104 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ); } } + + private async handleRequestReserveNow( + chargingStation: ChargingStation, + commandPayload: OCPP16ReserveNowRequest + ): Promise { + if ( + !OCPP16ServiceUtils.checkFeatureProfile( + chargingStation, + OCPP16SupportedFeatureProfiles.Reservation, + OCPP16IncomingRequestCommand.RESERVE_NOW + ) + ) { + return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED; + } + const { reservationId, idTag, connectorId } = commandPayload; + let response: OCPP16ReserveNowResponse; + try { + if (!chargingStation.isConnectorAvailable(connectorId) && connectorId > 0) { + return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED; + } + if (connectorId === 0 && !chargingStation.getReservationOnConnectorId0Enabled()) { + return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED; + } + if (!(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, connectorId, idTag))) { + return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED; + } + switch (chargingStation.getConnectorStatus(connectorId).status) { + case ConnectorStatusEnum.Faulted: + response = OCPPConstants.OCPP_RESERVATION_RESPONSE_FAULTED; + break; + case ConnectorStatusEnum.Occupied: + response = OCPPConstants.OCPP_RESERVATION_RESPONSE_OCCUPIED; + break; + case ConnectorStatusEnum.Unavailable: + response = OCPPConstants.OCPP_RESERVATION_RESPONSE_UNAVAILABLE; + break; + case ConnectorStatusEnum.Reserved: + if (!chargingStation.isConnectorReservable(reservationId, idTag, connectorId)) { + response = OCPPConstants.OCPP_RESERVATION_RESPONSE_OCCUPIED; + break; + } + // eslint-disable-next-line no-fallthrough + default: + if (!chargingStation.isConnectorReservable(reservationId, idTag)) { + response = OCPPConstants.OCPP_RESERVATION_RESPONSE_OCCUPIED; + break; + } + await chargingStation.addReservation({ + id: commandPayload.reservationId, + ...commandPayload, + }); + response = OCPPConstants.OCPP_RESERVATION_RESPONSE_ACCEPTED; + break; + } + return response; + } catch (error) { + chargingStation.getConnectorStatus(connectorId).status = ConnectorStatusEnum.Available; + return this.handleIncomingRequestError( + chargingStation, + OCPP16IncomingRequestCommand.RESERVE_NOW, + error as Error, + { errorResponse: OCPPConstants.OCPP_RESERVATION_RESPONSE_FAULTED } + ); + } + } + + private async handleRequestCancelReservation( + chargingStation: ChargingStation, + commandPayload: OCPP16CancelReservationRequest + ): Promise { + if ( + !OCPP16ServiceUtils.checkFeatureProfile( + chargingStation, + OCPP16SupportedFeatureProfiles.Reservation, + OCPP16IncomingRequestCommand.CANCEL_RESERVATION + ) + ) { + return OCPPConstants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED; + } + try { + const { reservationId } = commandPayload; + const [exists, reservation] = chargingStation.doesReservationExists({ id: reservationId }); + if (!exists) { + logger.error( + `${chargingStation.logPrefix()} Reservation with ID ${reservationId} + does not exist on charging station` + ); + return OCPPConstants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED; + } + await chargingStation.removeReservation(reservation); + return OCPPConstants.OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED; + } catch (error) { + return this.handleIncomingRequestError( + chargingStation, + OCPP16IncomingRequestCommand.CANCEL_RESERVATION, + error as Error, + { errorResponse: OCPPConstants.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 7ff1fdb8..b82599b7 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -24,6 +24,10 @@ import { OCPPVersion, type RequestParams, } from '../../../types'; +import type { + OCPP16CancelReservationRequest, + OCPP16ReserveNowRequest, +} from '../../../types/ocpp/1.6/Requests'; import { Constants, Utils } from '../../../utils'; import { OCPPRequestService } from '../OCPPRequestService'; import type { OCPPResponseService } from '../OCPPResponseService'; @@ -119,6 +123,22 @@ export class OCPP16RequestService extends OCPPRequestService { 'constructor' ), ], + [ + OCPP16RequestCommand.RESERVE_NOW, + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ReserveNow.json', + moduleName, + 'constructor' + ), + ], + [ + OCPP16RequestCommand.CANCEL_RESERVATION, + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/CancelReservation.json', + moduleName, + 'constructor' + ), + ], ]); this.buildRequestPayload = this.buildRequestPayload.bind(this) as ( chargingStation: ChargingStation, diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index 117c3686..9e8a2d5f 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -25,6 +25,7 @@ import { type OCPP16AuthorizeRequest, type OCPP16AuthorizeResponse, type OCPP16BootNotificationResponse, + type OCPP16CancelReservationResponse, OCPP16ChargePointStatus, type OCPP16DataTransferResponse, type OCPP16DiagnosticsStatusNotificationResponse, @@ -35,6 +36,7 @@ import { type OCPP16MeterValuesRequest, type OCPP16MeterValuesResponse, OCPP16RequestCommand, + type OCPP16ReserveNowResponse, OCPP16StandardParametersKey, type OCPP16StartTransactionRequest, type OCPP16StartTransactionResponse, @@ -161,6 +163,22 @@ export class OCPP16ResponseService extends OCPPResponseService { 'constructor' ), ], + [ + OCPP16RequestCommand.RESERVE_NOW, + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ReserveNowResponse.json', + moduleName, + 'constructor' + ), + ], + [ + OCPP16RequestCommand.CANCEL_RESERVATION, + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/CancelReservationResponse.json', + moduleName, + 'constructor' + ), + ], ]); this.jsonIncomingRequestResponseSchemas = new Map([ [ diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 23381f9e..c73c32dc 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -2,7 +2,7 @@ import type { JSONSchemaType } from 'ajv'; -import type { ChargingStation } from '../../../charging-station'; +import { type ChargingStation, ChargingStationUtils } from '../../../charging-station'; import { OCPPError } from '../../../exception'; import { CurrentType, @@ -13,6 +13,9 @@ import { MeterValueContext, MeterValueLocation, MeterValueUnit, + OCPP16AuthorizationStatus, + type OCPP16AuthorizeRequest, + type OCPP16AuthorizeResponse, type OCPP16ChargingProfile, type OCPP16IncomingRequestCommand, type OCPP16MeterValue, @@ -837,6 +840,30 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ); } + public static async isIdTagAuthorized( + chargingStation: ChargingStation, + connectorId: number, + idTag: string, + parentIdTag?: string + ): Promise { + let authorized = false; + const 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` + ); + } + return authorized; + } + private static buildSampledValue( sampledValueTemplate: SampledValueTemplate, value: number, @@ -912,4 +939,30 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return MeterValueUnit.VOLT; } } + + private static isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean { + return ( + chargingStation.getLocalAuthListEnabled() === true && + chargingStation.hasIdTags() === true && + Utils.isNotEmptyString( + chargingStation.idTagsCache + .getIdTags(ChargingStationUtils.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; + } } diff --git a/src/charging-station/ocpp/OCPPConstants.ts b/src/charging-station/ocpp/OCPPConstants.ts index e82b7d70..b3f4dd32 100644 --- a/src/charging-station/ocpp/OCPPConstants.ts +++ b/src/charging-station/ocpp/OCPPConstants.ts @@ -9,6 +9,7 @@ import { TriggerMessageStatus, UnlockStatus, } from '../../types'; +import { ReservationStatus } from '../../types/ocpp/Responses'; import { Constants } from '../../utils'; export class OCPPConstants { @@ -104,11 +105,39 @@ export class OCPPConstants { static readonly OCPP_DATA_TRANSFER_RESPONSE_REJECTED = Object.freeze({ status: DataTransferStatus.REJECTED, }); - + static readonly OCPP_DATA_TRANSFER_RESPONSE_UNKNOWN_VENDOR_ID = Object.freeze({ status: DataTransferStatus.UNKNOWN_VENDOR_ID, }); + static readonly OCPP_RESERVATION_RESPONSE_ACCEPTED = Object.freeze({ + status: ReservationStatus.ACCEPTED, + }); // Reservation has been made + + static readonly OCPP_RESERVATION_RESPONSE_FAULTED = Object.freeze({ + status: ReservationStatus.FAULTED, + }); // Reservation has not been made, because of connector in FAULTED state + + static readonly OCPP_RESERVATION_RESPONSE_OCCUPIED = Object.freeze({ + status: ReservationStatus.OCCUPIED, + }); // Reservation has not been made, because all connectors are OCCUPIED + + static readonly OCPP_RESERVATION_RESPONSE_REJECTED = Object.freeze({ + status: ReservationStatus.REJECTED, + }); // Reservation has not been made, because CS is not configured to accept reservations + + static readonly OCPP_RESERVATION_RESPONSE_UNAVAILABLE = Object.freeze({ + status: ReservationStatus.UNAVAILABLE, + }); // Reservation has not been made, because connectors are spec. connector is in UNAVAILABLE state + + static readonly OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED = Object.freeze({ + status: GenericStatus.Accepted, + }); // Reservation for id has been cancelled has been made + + static readonly OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED = Object.freeze({ + status: GenericStatus.Rejected, + }); // Reservation could not be cancelled, because there is no reservation active for id + protected constructor() { // This is intentional } diff --git a/src/types/ConnectorStatus.ts b/src/types/ConnectorStatus.ts index d7698107..33c72242 100644 --- a/src/types/ConnectorStatus.ts +++ b/src/types/ConnectorStatus.ts @@ -3,6 +3,7 @@ import type { ChargingProfile } from './ocpp/ChargingProfile'; import type { ConnectorStatusEnum } from './ocpp/ConnectorStatusEnum'; import type { MeterValue } from './ocpp/MeterValues'; import type { AvailabilityType } from './ocpp/Requests'; +import type { Reservation } from './ocpp/Reservation'; export type ConnectorStatus = { availability: AvailabilityType; @@ -22,4 +23,5 @@ export type ConnectorStatus = { transactionEnergyActiveImportRegisterValue?: number; // In Wh transactionBeginMeterValue?: MeterValue; chargingProfiles?: ChargingProfile[]; + reservation?: Reservation; }; diff --git a/src/types/index.ts b/src/types/index.ts index fee7eab6..7bfa2150 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -108,6 +108,8 @@ export { type ResetRequest, type SetChargingProfileRequest, type UnlockConnectorRequest, + type OCPP16ReserveNowRequest, + type OCPP16CancelReservationRequest, } from './ocpp/1.6/Requests'; export { type ChangeAvailabilityResponse, @@ -127,6 +129,8 @@ export { type OCPP16UpdateFirmwareResponse, type SetChargingProfileResponse, type UnlockConnectorResponse, + type OCPP16ReserveNowResponse, + type OCPP16CancelReservationResponse, } from './ocpp/1.6/Responses'; export { ChargePointErrorCode } from './ocpp/ChargePointErrorCode'; export { @@ -251,3 +255,5 @@ export { WebSocketCloseEventStatusCode, WebSocketCloseEventStatusString, } from './WebSocket'; +export { ReservationFilterKey, ReservationTerminationReason } from './ocpp/1.6/Reservation'; +export { type Reservation } from './ocpp/Reservation'; diff --git a/src/types/ocpp/1.6/Requests.ts b/src/types/ocpp/1.6/Requests.ts index 60c1787f..80128a27 100644 --- a/src/types/ocpp/1.6/Requests.ts +++ b/src/types/ocpp/1.6/Requests.ts @@ -21,6 +21,8 @@ export enum OCPP16RequestCommand { DIAGNOSTICS_STATUS_NOTIFICATION = 'DiagnosticsStatusNotification', FIRMWARE_STATUS_NOTIFICATION = 'FirmwareStatusNotification', DATA_TRANSFER = 'DataTransfer', + RESERVE_NOW = 'ReserveNow', + CANCEL_RESERVATION = 'CancelReservation', } export enum OCPP16IncomingRequestCommand { @@ -39,6 +41,8 @@ export enum OCPP16IncomingRequestCommand { TRIGGER_MESSAGE = 'TriggerMessage', DATA_TRANSFER = 'DataTransfer', UPDATE_FIRMWARE = 'UpdateFirmware', + RESERVE_NOW = 'ReserveNow', + CANCEL_RESERVATION = 'CancelReservation', } export type OCPP16HeartbeatRequest = EmptyObject; @@ -183,3 +187,15 @@ export interface OCPP16DataTransferRequest extends JsonObject { messageId?: string; data?: string; } + +export interface OCPP16ReserveNowRequest extends JsonObject { + connectorId: number; + expiryDate: Date; + idTag: string; + parentIdTag?: string; + reservationId: number; +} + +export interface OCPP16CancelReservationRequest extends JsonObject { + reservationId: number; +} diff --git a/src/types/ocpp/1.6/Reservation.ts b/src/types/ocpp/1.6/Reservation.ts new file mode 100644 index 00000000..bbc3de7a --- /dev/null +++ b/src/types/ocpp/1.6/Reservation.ts @@ -0,0 +1,22 @@ +export interface OCPP16Reservation { + id: number; + connectorId: number; + expiryDate: Date; + idTag: string; + parentIdTag?: string; +} + +export enum ReservationTerminationReason { + EXPIRED = 'Expired', + TRANSACTION_STARTED = 'TransactionStarted', + CONNECTOR_STATE_CHANGED = 'ConnectorStateChanged', + RESERVATION_CANCELED = 'ReservationCanceled', +} + +export enum ReservationFilterKey { + RESERVATION_ID = 'id', + ID_TAG = 'idTag', + PARENT_ID_TAG = 'parentIdTag', + CONNECTOR_ID = 'connectorId', + EVSE_ID = 'evseId', +} diff --git a/src/types/ocpp/1.6/Responses.ts b/src/types/ocpp/1.6/Responses.ts index a6683caa..1a163aa8 100644 --- a/src/types/ocpp/1.6/Responses.ts +++ b/src/types/ocpp/1.6/Responses.ts @@ -2,7 +2,7 @@ import type { OCPP16ChargingSchedule } from './ChargingProfile'; import type { EmptyObject } from '../../EmptyObject'; import type { JsonObject } from '../../JsonType'; import type { OCPPConfigurationKey } from '../Configuration'; -import type { GenericStatus, RegistrationStatusEnumType } from '../Responses'; +import { GenericStatus, type RegistrationStatusEnumType } from '../Responses'; export interface OCPP16HeartbeatResponse extends JsonObject { currentTime: Date; @@ -109,3 +109,20 @@ export interface OCPP16DataTransferResponse extends JsonObject { status: OCPP16DataTransferStatus; data?: string; } + +export interface OCPP16CancelReservationResponse extends JsonObject { + status: GenericStatus; +} + +export enum OCPP16ReservationStatus { + ACCEPTED = 'Accepted', + FAULTED = 'Faulted', + OCCUPIED = 'Occupied', + REJECTED = 'Rejected', + UNAVAILABLE = 'Unavailable', + NOT_SUPPORTED = 'NotSupported', +} + +export interface OCPP16ReserveNowResponse extends JsonObject { + status: OCPP16ReservationStatus; +} diff --git a/src/types/ocpp/Requests.ts b/src/types/ocpp/Requests.ts index 7eef7b5b..9ca43da7 100644 --- a/src/types/ocpp/Requests.ts +++ b/src/types/ocpp/Requests.ts @@ -3,6 +3,7 @@ import type { OCPP16MeterValuesRequest } from './1.6/MeterValues'; import { OCPP16AvailabilityType, type OCPP16BootNotificationRequest, + type OCPP16CancelReservationRequest, type OCPP16DataTransferRequest, type OCPP16DiagnosticsStatusNotificationRequest, OCPP16FirmwareStatus, @@ -11,6 +12,7 @@ import { OCPP16IncomingRequestCommand, OCPP16MessageTrigger, OCPP16RequestCommand, + type OCPP16ReserveNowRequest, type OCPP16StatusNotificationRequest, } from './1.6/Requests'; import { OperationalStatusEnumType } from './2.0/Common'; @@ -101,3 +103,7 @@ export const FirmwareStatus = { export type FirmwareStatus = OCPP16FirmwareStatus; export type ResponseType = JsonType | OCPPError; + +export type ReserveNowRequest = OCPP16ReserveNowRequest; + +export type CancelReservationRequest = OCPP16CancelReservationRequest; diff --git a/src/types/ocpp/Reservation.ts b/src/types/ocpp/Reservation.ts new file mode 100644 index 00000000..b185b874 --- /dev/null +++ b/src/types/ocpp/Reservation.ts @@ -0,0 +1,3 @@ +import { type OCPP16Reservation } from './1.6/Reservation'; + +export type Reservation = OCPP16Reservation; diff --git a/src/types/ocpp/Responses.ts b/src/types/ocpp/Responses.ts index 24b8b848..8735dc8a 100644 --- a/src/types/ocpp/Responses.ts +++ b/src/types/ocpp/Responses.ts @@ -2,6 +2,7 @@ import type { OCPP16MeterValuesResponse } from './1.6/MeterValues'; import { OCPP16AvailabilityStatus, type OCPP16BootNotificationResponse, + type OCPP16CancelReservationResponse, OCPP16ChargingProfileStatus, OCPP16ClearChargingProfileStatus, OCPP16ConfigurationStatus, @@ -10,6 +11,7 @@ import { type OCPP16DiagnosticsStatusNotificationResponse, type OCPP16FirmwareStatusNotificationResponse, type OCPP16HeartbeatResponse, + OCPP16ReservationStatus, type OCPP16StatusNotificationResponse, OCPP16TriggerMessageStatus, OCPP16UnlockStatus, @@ -103,3 +105,14 @@ export const DataTransferStatus = { ...OCPP16DataTransferStatus, } as const; export type DataTransferStatus = OCPP16DataTransferStatus; + +export type ReservationStatus = OCPP16ReservationStatus; +export const ReservationStatus = { + ...OCPP16ReservationStatus, +}; + +export const CancelReservationStatus = { + ...GenericStatus, +}; + +export type CancelReservationResponse = OCPP16CancelReservationResponse; diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index a5cd8150..bb765c75 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -52,6 +52,8 @@ export class Constants { /* This is intentional */ }); + static readonly DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL = 5000; // Ms + private constructor() { // This is intentional }