From 24578c318295d517762fa075959846f230affbe0 Mon Sep 17 00:00:00 2001 From: Julian Buecher Date: Wed, 24 May 2023 23:12:18 +0200 Subject: [PATCH] build(simulator): features for reserve-now and cancel-reservation support added --- README.md | 4 +- .../abb-atg.station-template.json | 7 +- .../abb.station-template.json | 7 +- .../evlink.station-template.json | 9 +- .../keba.station-template.json | 7 +- .../schneider-imredd.station-template.json | 7 +- .../schneider.station-template.json | 7 +- .../siemens.station-template.json | 7 +- .../virtual-simple-atg.station-template.json | 7 +- .../virtual-simple.station-template.json | 7 +- .../virtual.station-template.json | 2 +- src/charging-station/ChargingStation.ts | 89 ++++++ .../ocpp/1.6/OCPP16IncomingRequestService.ts | 279 +++++++++++++++--- src/types/ChargingStationTemplate.ts | 2 + src/types/ocpp/1.6/Requests.ts | 6 +- src/types/ocpp/1.6/Reservation.ts | 7 + src/types/ocpp/1.6/Responses.ts | 18 +- src/types/ocpp/Reservation.ts | 3 + 18 files changed, 417 insertions(+), 58 deletions(-) create mode 100644 src/types/ocpp/1.6/Reservation.ts create mode 100644 src/types/ocpp/Reservation.ts diff --git a/README.md b/README.md index a3f0e4e7..9fb599ae 100644 --- a/README.md +++ b/README.md @@ -389,8 +389,8 @@ make SUBMODULES_INIT=true #### Reservation Profile -- :x: CancelReservation -- :x: ReserveNow +- :white_check_mark: CancelReservation +- :white_check_mark: ReserveNow #### 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..99a67c64 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": "true" } ] }, 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/evlink.station-template.json b/src/assets/station-templates/evlink.station-template.json index fda88bfb..05a8b3da 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", @@ -40,12 +40,17 @@ { "key": "AuthorizeRemoteTxRequests", "readonly": false, - "value": "false" + "value": "true" }, { "key": "WebSocketPingInterval", "readonly": false, "value": "60" + }, + { + "key": "ReserveConnectorZeroSupported", + "readonly": false, + "value": "true" } ] }, 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-imredd.station-template.json b/src/assets/station-templates/schneider-imredd.station-template.json index 0369d0ac..d6cc99f8 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": "true" } ] }, 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..ac075ce6 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": "true" } ] }, 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..8a0c2801 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": "true" } ] }, diff --git a/src/assets/station-templates/virtual.station-template.json b/src/assets/station-templates/virtual.station-template.json index 3980cba6..962e792f 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", diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index f47952ca..ace7aff1 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -26,6 +26,7 @@ import { type OCPPRequestService, OCPPServiceUtils, } from './ocpp'; +import { OCPPConstants } from './ocpp/OCPPConstants'; import { SharedLRUCache } from './SharedLRUCache'; import { BaseError, OCPPError } from '../exception'; import { PerformanceStatistics } from '../performance'; @@ -62,6 +63,8 @@ import { MeterValueMeasurand, type MeterValuesRequest, type MeterValuesResponse, + OCPP16SupportedFeatureProfiles, + OCPP20ConnectorStatusEnumType, OCPPVersion, type OutgoingRequest, PowerUnits, @@ -81,6 +84,7 @@ import { WebSocketCloseEventStatusCode, type WsOptions, } from '../types'; +import type { Reservation } from '../types/ocpp/Reservation'; import { ACElectricUtils, AsyncLock, @@ -132,6 +136,7 @@ export class ChargingStation { private readonly sharedLRUCache: SharedLRUCache; private webSocketPingSetInterval!: NodeJS.Timeout; private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel; + private reservations?: Reservation[]; constructor(index: number, templateFile: string) { this.started = false; @@ -888,6 +893,80 @@ export class ChargingStation { ); } + public supportsReservations(): boolean { + logger.info(`${this.logPrefix()} Check for reservation support in charging station`); + return ChargingStationConfigurationUtils.getConfigurationKey( + this, + StandardParametersKey.SupportedFeatureProfiles + ).value.includes(OCPP16SupportedFeatureProfiles.Reservation); + } + + public supportsReservationsOnConnectorId0(): boolean { + logger.info( + `Check for reservation support on connector 0 in charging station (CS): ${this.logPrefix()}` + ); + return ( + this.supportsReservations() && + ChargingStationConfigurationUtils.getConfigurationKey( + this, + OCPPConstants.OCPP_RESERVE_CONNECTOR_ZERO_SUPPORTED + ).value === 'true' + ); + } + + public addReservation(newReservation: Reservation): void { + if (Utils.isNullOrUndefined(this.reservations)) { + this.reservations = []; + } + const [exists, foundReservation] = this.doesReservationExist(newReservation.reservationId); + if (exists) { + this.replaceExistingReservation(foundReservation, newReservation); + } else { + this.reservations.push(newReservation); + } + } + + public removeReservation(existingReservationId: number): void { + const index = this.reservations.findIndex((res) => res.reservationId === existingReservationId); + this.reservations.splice(index, 1); + } + + public getReservation(reservationId: number, reservationIndex?: number): Reservation { + if (!Utils.isNullOrUndefined(reservationIndex)) { + return this.reservations[reservationIndex]; + } + return this.reservations.find((r) => r.reservationId === reservationId); + } + + public doesReservationExist( + reservationId: number, + reservation?: Reservation + ): [boolean, Reservation] { + const foundReservation = this.reservations.find( + (r) => r.reservationId === reservationId || r.reservationId === reservation.reservationId + ); + return Utils.isUndefined(foundReservation) ? [false, null] : [true, foundReservation]; + } + + public getReservationByConnectorId(connectorId: number): Reservation { + return this.reservations.find((r) => r.connectorId === connectorId); + } + + public getAvailableConnector(): Map { + for (const connectorId in this.connectors) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const connector = this.connectors[Utils.convertToInt(connectorId)]; + if ( + this.isConnectorAvailable(Utils.convertToInt(connectorId)) && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + connector.status === OCPP20ConnectorStatusEnumType.Available + ) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return connector; + } + } + } + private flushMessageBuffer(): void { if (this.messageBuffer.size > 0) { for (const message of this.messageBuffer.values()) { @@ -915,6 +994,16 @@ export class ChargingStation { return this.stationInfo.supervisionUrlOcppConfiguration ?? false; } + private replaceExistingReservation( + existingReservation: Reservation, + newReservation: Reservation + ): void { + const existingReservationIndex = this.reservations.findIndex( + (r) => r.reservationId === existingReservation.reservationId + ); + this.reservations.splice(existingReservationIndex, 1, newReservation); + } + private getSupervisionUrlOcppKey(): string { return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl; } diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 9e4ff3e0..46c12018 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved. import fs from 'node:fs'; @@ -24,6 +25,7 @@ import { type ClearChargingProfileRequest, type ClearChargingProfileResponse, type ConnectorStatus, + ConnectorStatusEnum, ErrorType, type GenericResponse, GenericStatus, @@ -84,7 +86,16 @@ import { type UnlockConnectorRequest, type UnlockConnectorResponse, } from '../../../types'; +import type { + OCPP16CancelReservationRequest, + OCPP16ReserveNowRequest, +} from '../../../types/ocpp/1.6/Requests'; +import type { + OCPP16CancelReservationResponse, + OCPP16ReserveNowResponse, +} from '../../../types/ocpp/1.6/Responses'; import { Constants, Utils, logger } from '../../../utils'; +import { OCPPConstants } from '../OCPPConstants'; import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService'; const moduleName = 'OCPP16IncomingRequestService'; @@ -799,6 +810,9 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { commandPayload: RemoteStartTransactionRequest ): Promise { const transactionConnectorId = commandPayload.connectorId; + const reserved: boolean = + chargingStation.getConnectorStatus(transactionConnectorId).status === + OCPP16ChargePointStatus.Reserved; if (chargingStation.hasConnector(transactionConnectorId) === false) { return this.notifyRemoteStartTransactionRejected( chargingStation, @@ -808,7 +822,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { } if ( chargingStation.isChargingStationAvailable() === false || - chargingStation.isConnectorAvailable(transactionConnectorId) === false + chargingStation.isConnectorAvailable(transactionConnectorId) === false || + reserved ) { return this.notifyRemoteStartTransactionRejected( chargingStation, @@ -827,36 +842,11 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { const connectorStatus = chargingStation.getConnectorStatus(transactionConnectorId); // Check if authorized if (chargingStation.getAuthorizeRemoteTxRequests() === true) { - let authorized = false; - if ( - chargingStation.getLocalAuthListEnabled() === true && - chargingStation.hasIdTags() === true && - Utils.isNotEmptyString( - chargingStation.idTagsCache - .getIdTags(ChargingStationUtils.getIdTagsFile(chargingStation.stationInfo)) - ?.find((idTag) => idTag === commandPayload.idTag) - ) - ) { - 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; - } - } else { - logger.warn( - `${chargingStation.logPrefix()} The charging station configuration expects authorize at remote start transaction but local authorization or authorize isn't enabled` - ); - } + const authorized = await this.isAuthorized( + chargingStation, + transactionConnectorId, + commandPayload.idTag + ); if (authorized === true) { // Authorization successful, start transaction if ( @@ -867,15 +857,20 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ) === true ) { connectorStatus.transactionRemoteStarted = true; + const startTransactionData: JsonType = { + connectorId: transactionConnectorId, + idTag: commandPayload.idTag, + }; + if (reserved) { + startTransactionData['reservationId'] = + chargingStation.getReservationByConnectorId(transactionConnectorId).reservationId; + } if ( ( await chargingStation.ocppRequestService.requestHandler< OCPP16StartTransactionRequest, OCPP16StartTransactionResponse - >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, { - connectorId: transactionConnectorId, - idTag: commandPayload.idTag, - }) + >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, startTransactionData) ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED ) { logger.debug(remoteStartTransactionLogMsg); @@ -899,6 +894,13 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { commandPayload.idTag ); } + if (reserved) { + await this.handleReservedRemoteStartTransaction( + chargingStation, + transactionConnectorId, + commandPayload + ); + } // No authorization check required, start transaction if ( this.setRemoteStartTransactionChargingProfile( @@ -1504,4 +1506,211 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ); } } + + private async handleRequestReserveNow( + chargingStation: ChargingStation, + commandPayload: OCPP16ReserveNowRequest + ): Promise { + const { reservationId, idTag, connectorId } = commandPayload; + let connector: Map; + let response: OCPP16ReserveNowResponse; + try { + if ( + !chargingStation.supportsReservations() && + chargingStation.isConnectorAvailable(connectorId) + ) { + return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED; + } + if (connectorId === 0 && !chargingStation.supportsReservationsOnConnectorId0()) { + return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED; + } + if (!(await this.isAuthorized(chargingStation, connectorId, commandPayload.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 (Utils.isUndefined(chargingStation.getReservation(commandPayload.reservationId))) { + response = OCPPConstants.OCPP_RESERVATION_RESPONSE_OCCUPIED; + break; + } + // eslint-disable-next-line no-fallthrough + default: + logger.info( + `${chargingStation.logPrefix()} on connector ${connectorId} is now reserved for ${ + commandPayload.idTag + }` + ); + chargingStation.getConnectorStatus(connectorId).status = ConnectorStatusEnum.Reserved; + chargingStation.addReservation({ ...commandPayload }); + await chargingStation.ocppRequestService + .requestHandler( + chargingStation, + OCPP16RequestCommand.STATUS_NOTIFICATION, + { + connectorId, + errorCode: OCPP16ChargePointErrorCode.NO_ERROR, + status: chargingStation.getConnectorStatus(connectorId).status, + }, + { + triggerMessage: true, + } + ) + .catch(Constants.EMPTY_FUNCTION); + response = OCPPConstants.OCPP_RESERVATION_RESPONSE_ACCEPTED; + break; + } + return response; + } catch (error) { + return this.handleIncomingRequestError( + chargingStation, + OCPP16IncomingRequestCommand.RESERVE_NOW, + error as Error, + { errorResponse: OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED } + ); + } + } + + private async handleRequestCancelReservation( + chargingStation: ChargingStation, + commandPayload: OCPP16CancelReservationRequest + ): Promise { + try { + const reservationId = commandPayload.reservationId; + const [exists, reservation] = chargingStation.doesReservationExist(reservationId); + if (!exists) { + logger.error( + `${chargingStation.logPrefix()} Reservation with ID ${reservationId} does not exist on charging station` + ); + return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED; + } + chargingStation.getConnectorStatus(reservation.connectorId).status = + ConnectorStatusEnum.Available; + chargingStation.removeReservation(reservation.reservationId); + await chargingStation.ocppRequestService + .requestHandler( + chargingStation, + OCPP16RequestCommand.STATUS_NOTIFICATION, + { + connectorId: reservation.connectorId, + errorCode: OCPP16ChargePointErrorCode.NO_ERROR, + status: chargingStation.getConnectorStatus(reservation.connectorId).status, + }, + { + triggerMessage: true, + } + ) + .catch(Constants.EMPTY_FUNCTION); + return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED; + } catch (error) { + return this.handleIncomingRequestError( + chargingStation, + OCPP16IncomingRequestCommand.CANCEL_RESERVATION, + error as Error, + { errorResponse: OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED } + ); + } + } + + /** + * Check for authorized access on a connector with given ConnectorId and idTag for the user + * @param {ChargingStation} chargingStation - Charging Station working on incoming request + * @param {number} ConnectorId - Identifier of the connector at the charging station + * @param {string} idTag - Identifier of the user + * @param {string} parentIdTag - Identifier for a group of idTags, which is optional + * @returns {Promise} - 'true' if user is authorized, 'false' otherwise + */ + private async isAuthorized( + chargingStation: ChargingStation, + connectorId: number, + idTag: string, + parentIdTag?: string + ): Promise { + let authorized = false; + const connectorStatus = chargingStation.getConnectorStatus(connectorId); + if ( + chargingStation.getLocalAuthListEnabled() === true && + chargingStation.hasIdTags() === true && + Utils.isNotEmptyString( + chargingStation.idTagsCache + .getIdTags(ChargingStationUtils.getIdTagsFile(chargingStation.stationInfo)) + ?.find((tag) => tag === idTag) + ) + ) { + connectorStatus.localAuthorizeIdTag = idTag; + connectorStatus.idTagLocalAuthorized = true; + authorized = true; + } else if (chargingStation.getMustAuthorizeAtRemoteStart() === true) { + connectorStatus.authorizeIdTag = idTag; + const authorizeResponse: OCPP16AuthorizeResponse = + await chargingStation.ocppRequestService.requestHandler< + OCPP16AuthorizeRequest, + OCPP16AuthorizeResponse + >(chargingStation, OCPP16RequestCommand.AUTHORIZE, { + idTag: idTag, + }); + if (authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) { + authorized = true; + } + } 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 async handleReservedRemoteStartTransaction( + chargingStation: ChargingStation, + connectorId: number, + commandPayload: RemoteStartTransactionRequest + ): Promise { + const reservation = chargingStation.getReservationByConnectorId(connectorId); + if ( + !Utils.isUndefined(reservation) && + (await this.isAuthorized(chargingStation, connectorId, commandPayload.idTag)) && + reservation.idTag === commandPayload.idTag + ) { + const remoteStartTransactionLogMsg = `${chargingStation.logPrefix()} Transaction remotely STARTED on ${ + chargingStation.stationInfo.chargingStationId + }#${connectorId.toString()} for idTag '${commandPayload.idTag}'`; + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, + connectorId, + OCPP16ChargePointStatus.Preparing + ); + if ( + this.setRemoteStartTransactionChargingProfile( + chargingStation, + connectorId, + commandPayload.chargingProfile + ) === true + ) { + chargingStation.getConnectorStatus(connectorId).transactionRemoteStarted = true; + if ( + ( + await chargingStation.ocppRequestService.requestHandler< + OCPP16StartTransactionRequest, + OCPP16StartTransactionResponse + >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, { + connectorId: connectorId, + idTag: commandPayload.idTag, + reservationId: reservation.reservationId, + }) + ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED + ) { + logger.debug(remoteStartTransactionLogMsg); + return OCPP16Constants.OCPP_RESPONSE_ACCEPTED; + } + } + } + } } diff --git a/src/types/ChargingStationTemplate.ts b/src/types/ChargingStationTemplate.ts index e3594d02..a11ac892 100644 --- a/src/types/ChargingStationTemplate.ts +++ b/src/types/ChargingStationTemplate.ts @@ -14,6 +14,7 @@ import type { MessageTrigger, RequestCommand, } from './ocpp/Requests'; +import type { Reservation } from './ocpp/Reservation'; export enum CurrentType { AC = 'AC', @@ -115,4 +116,5 @@ export type ChargingStationTemplate = { AutomaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration; Evses?: Record; Connectors?: Record; + reservation?: Reservation[]; }; diff --git a/src/types/ocpp/1.6/Requests.ts b/src/types/ocpp/1.6/Requests.ts index 786ea617..a04a3f65 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; @@ -185,7 +189,7 @@ export interface OCPP16DataTransferRequest extends JsonObject { } export interface OCPP16ReserveNowRequest { - connectorId: string; + connectorId: number; expiryDate: Date; idTag: string; parentIdTag?: string; diff --git a/src/types/ocpp/1.6/Reservation.ts b/src/types/ocpp/1.6/Reservation.ts new file mode 100644 index 00000000..7a78225d --- /dev/null +++ b/src/types/ocpp/1.6/Reservation.ts @@ -0,0 +1,7 @@ +export interface OCPP16Reservation { + connectorId: number; + expiryDate: Date; + idTag: string; + parentIdTag?: string; + reservationId: number; +} diff --git a/src/types/ocpp/1.6/Responses.ts b/src/types/ocpp/1.6/Responses.ts index 6a5ebfe4..15c76a71 100644 --- a/src/types/ocpp/1.6/Responses.ts +++ b/src/types/ocpp/1.6/Responses.ts @@ -110,23 +110,23 @@ export interface OCPP16DataTransferResponse extends JsonObject { data?: string; } -export enum OCPP16ReservationStatus { +export enum OCPP16CancelReservationStatus { ACCEPTED = 'Accepted', - FAULTED = 'Faulted', - OCCUPIED = 'Occupied', REJECTED = 'Rejected', - UNAVAILABLE = 'Unavailable', } -export interface OCPP16ReserveNowResponse { - status: OCPP16ReservationStatus; +export interface OCPP16CancelReservationResponse { + status: OCPP16CancelReservationStatus; } -export enum OCPP16CancelReservationStatus { +export enum OCPP16ReservationStatus { ACCEPTED = 'Accepted', + FAULTED = 'Faulted', + OCCUPIED = 'Occupied', REJECTED = 'Rejected', + UNAVAILABLE = 'Unavailable', } -export interface OCPP16CancelReservationResponse { - status: OCPP16CancelReservationStatus; +export interface OCPP16ReserveNowResponse { + status: OCPP16ReservationStatus; } diff --git a/src/types/ocpp/Reservation.ts b/src/types/ocpp/Reservation.ts new file mode 100644 index 00000000..dd5d8190 --- /dev/null +++ b/src/types/ocpp/Reservation.ts @@ -0,0 +1,3 @@ +import { OCPP16Reservation } from './1.6/Reservation'; + +export type Reservation = OCPP16Reservation; -- 2.34.1