type OCPPRequestService,
OCPPServiceUtils,
} from './ocpp';
+import { OCPPConstants } from './ocpp/OCPPConstants';
import { SharedLRUCache } from './SharedLRUCache';
import { BaseError, OCPPError } from '../exception';
import { PerformanceStatistics } from '../performance';
MeterValueMeasurand,
type MeterValuesRequest,
type MeterValuesResponse,
+ OCPP16SupportedFeatureProfiles,
+ OCPP20ConnectorStatusEnumType,
OCPPVersion,
type OutgoingRequest,
PowerUnits,
WebSocketCloseEventStatusCode,
type WsOptions,
} from '../types';
+import type { Reservation } from '../types/ocpp/Reservation';
import {
ACElectricUtils,
AsyncLock,
private readonly sharedLRUCache: SharedLRUCache;
private webSocketPingSetInterval!: NodeJS.Timeout;
private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel;
+ private reservations?: Reservation[];
constructor(index: number, templateFile: string) {
this.started = false;
);
}
+ 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<number, ConnectorStatus> {
+ 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()) {
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;
}
+/* eslint-disable max-len */
// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
import fs from 'node:fs';
type ClearChargingProfileRequest,
type ClearChargingProfileResponse,
type ConnectorStatus,
+ ConnectorStatusEnum,
ErrorType,
type GenericResponse,
GenericStatus,
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';
commandPayload: RemoteStartTransactionRequest
): Promise<GenericResponse> {
const transactionConnectorId = commandPayload.connectorId;
+ const reserved: boolean =
+ chargingStation.getConnectorStatus(transactionConnectorId).status ===
+ OCPP16ChargePointStatus.Reserved;
if (chargingStation.hasConnector(transactionConnectorId) === false) {
return this.notifyRemoteStartTransactionRejected(
chargingStation,
}
if (
chargingStation.isChargingStationAvailable() === false ||
- chargingStation.isConnectorAvailable(transactionConnectorId) === false
+ chargingStation.isConnectorAvailable(transactionConnectorId) === false ||
+ reserved
) {
return this.notifyRemoteStartTransactionRejected(
chargingStation,
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 (
) === 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);
commandPayload.idTag
);
}
+ if (reserved) {
+ await this.handleReservedRemoteStartTransaction(
+ chargingStation,
+ transactionConnectorId,
+ commandPayload
+ );
+ }
// No authorization check required, start transaction
if (
this.setRemoteStartTransactionChargingProfile(
);
}
}
+
+ private async handleRequestReserveNow(
+ chargingStation: ChargingStation,
+ commandPayload: OCPP16ReserveNowRequest
+ ): Promise<OCPP16ReserveNowResponse> {
+ const { reservationId, idTag, connectorId } = commandPayload;
+ let connector: Map<number, ConnectorStatus>;
+ 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<OCPP16StatusNotificationRequest, OCPP16StatusNotificationResponse>(
+ 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<OCPP16CancelReservationResponse> {
+ 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<OCPP16StatusNotificationRequest, OCPP16StatusNotificationResponse>(
+ 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<boolean>} - 'true' if user is authorized, 'false' otherwise
+ */
+ private async isAuthorized(
+ chargingStation: ChargingStation,
+ connectorId: number,
+ idTag: string,
+ parentIdTag?: string
+ ): Promise<boolean> {
+ 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<GenericResponse> {
+ 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;
+ }
+ }
+ }
+ }
}