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,
+ OCPP16AuthorizationStatus,
+ type OCPP16AuthorizeRequest,
+ type OCPP16AuthorizeResponse,
+ OCPP16RequestCommand,
+ OCPP16SupportedFeatureProfiles,
OCPPVersion,
type OutgoingRequest,
PowerUnits,
WebSocketCloseEventStatusCode,
type WsOptions,
} from '../types';
+import { ReservationTerminationReason } from '../types/ocpp/1.6/Reservation';
+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[];
+ private reservationExpiryDateSetInterval?: NodeJS.Timeout;
constructor(index: number, templateFile: string) {
this.started = false;
if (this.getEnableStatistics() === true) {
this.performanceStatistics?.start();
}
+ if (this.supportsReservations()) {
+ this.startReservationExpiryDateSetInterval();
+ }
this.openWSConnection();
// Monitor charging station template file
this.templateFileWatcher = FileUtils.watchJsonFile(
);
}
+ 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(
+ ` ${this.logPrefix()} Check for reservation support on connector 0 in charging station (CS)`
+ );
+ return (
+ this.supportsReservations() &&
+ ChargingStationConfigurationUtils.getConfigurationKey(
+ this,
+ OCPPConstants.OCPP_RESERVE_CONNECTOR_ZERO_SUPPORTED
+ ).value === 'true'
+ );
+ }
+
+ public async addReservation(reservation: Reservation): Promise<void> {
+ if (Utils.isNullOrUndefined(this.reservations)) {
+ this.reservations = [];
+ }
+ const [exists, reservationFound] = this.doesReservationExists(reservation);
+ if (exists) {
+ await this.removeReservation(reservationFound);
+ }
+ this.reservations.push(reservation);
+ if (reservation.connectorId === 0) {
+ return;
+ }
+ this.getConnectorStatus(reservation.connectorId).status = ConnectorStatusEnum.Reserved;
+ 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<void> {
+ const sameReservation = (r: Reservation) => r.id === reservation.id;
+ const index = this.reservations?.findIndex(sameReservation);
+ this.reservations.splice(index, 1);
+ switch (reason) {
+ case ReservationTerminationReason.TRANSACTION_STARTED:
+ // No action needed
+ break;
+ case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
+ // No action needed
+ break;
+ default: // ReservationTerminationReason.EXPIRED, ReservationTerminationReason.CANCELED
+ this.getConnectorStatus(reservation.connectorId).status = ConnectorStatusEnum.Available;
+ await this.ocppRequestService.requestHandler<
+ StatusNotificationRequest,
+ StatusNotificationResponse
+ >(
+ this,
+ RequestCommand.STATUS_NOTIFICATION,
+ OCPPServiceUtils.buildStatusNotificationRequest(
+ this,
+ reservation.connectorId,
+ ConnectorStatusEnum.Available
+ )
+ );
+ break;
+ }
+ }
+
+ public getReservationById(id: number): Reservation {
+ return this.reservations?.find((reservation) => reservation.id === id);
+ }
+
+ public getReservationByIdTag(id: string): Reservation {
+ return this.reservations?.find((reservation) => reservation.idTag === id);
+ }
+
+ public getReservationByConnectorId(id: number): Reservation {
+ return this.reservations?.find((reservation) => reservation.connectorId === id);
+ }
+
+ public doesReservationExists(reservation: Partial<Reservation>): [boolean, Reservation] {
+ const sameReservation = (r: Reservation) => r.id === reservation.id;
+ const foundReservation = this.reservations?.find(sameReservation);
+ return Utils.isUndefined(foundReservation) ? [false, null] : [true, foundReservation];
+ }
+
+ public async isAuthorized(
+ connectorId: number,
+ idTag: string,
+ parentIdTag?: string
+ ): Promise<boolean> {
+ let authorized = false;
+ const connectorStatus = this.getConnectorStatus(connectorId);
+ if (
+ this.getLocalAuthListEnabled() === true &&
+ this.hasIdTags() === true &&
+ Utils.isNotEmptyString(
+ this.idTagsCache
+ .getIdTags(ChargingStationUtils.getIdTagsFile(this.stationInfo))
+ ?.find((tag) => tag === idTag)
+ )
+ ) {
+ connectorStatus.localAuthorizeIdTag = idTag;
+ connectorStatus.idTagLocalAuthorized = true;
+ authorized = true;
+ } else if (this.getMustAuthorizeAtRemoteStart() === true) {
+ connectorStatus.authorizeIdTag = idTag;
+ const authorizeResponse: OCPP16AuthorizeResponse =
+ await this.ocppRequestService.requestHandler<
+ OCPP16AuthorizeRequest,
+ OCPP16AuthorizeResponse
+ >(this, OCPP16RequestCommand.AUTHORIZE, {
+ idTag: idTag,
+ });
+ if (authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
+ authorized = true;
+ }
+ } else {
+ logger.warn(
+ `${this.logPrefix()} The charging station configuration expects authorize at
+ remote start transaction but local authorization or authorize isn't enabled`
+ );
+ }
+ return authorized;
+ }
+
+ 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<void> => {
+ if (!Utils.isNullOrUndefined(this.reservations) && !Utils.isEmptyArray(this.reservations)) {
+ for (const reservation of this.reservations) {
+ if (reservation.expiryDate.toString() < new Date().toISOString()) {
+ await this.removeReservation(reservation);
+ logger.info(
+ `${this.logPrefix()} Reservation with ID ${
+ reservation.id
+ } reached expiration date and was removed from CS`
+ );
+ }
+ }
+ }
+ }, interval);
+ }
+
+ public restartReservationExpiryDateSetInterval(): void {
+ this.stopReservationExpiryDateSetInterval();
+ this.startReservationExpiryDateSetInterval();
+ }
+
+ public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean {
+ const reservation = this.getReservationByConnectorId(connectorId);
+ return Utils.isUndefined(reservation) || reservation.idTag !== idTag;
+ }
+
+ public isConnectorReservable(
+ reservationId: number,
+ connectorId?: number,
+ idTag?: string
+ ): boolean {
+ const [alreadyExists] = this.doesReservationExists({ id: reservationId });
+ if (alreadyExists) {
+ return alreadyExists;
+ }
+ const userReservedAlready = Utils.isUndefined(this.getReservationByIdTag(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;
+ this.connectors.forEach((connector, id) => {
+ if (id === 0) {
+ return;
+ }
+ if (connector.status === ConnectorStatusEnum.Available) {
+ reservableConnectors++;
+ }
+ });
+ return reservableConnectors - this.getNumberOfReservationsOnConnectorZero();
+ }
+
+ private getNumberOfReservationsOnConnectorZero(): number {
+ const reservations = this.reservations?.filter((reservation) => reservation.connectorId === 0);
+ return Utils.isNullOrUndefined(reservations) ? 0 : reservations.length;
+ }
+
private flushMessageBuffer(): void {
if (this.messageBuffer.size > 0) {
for (const message of this.messageBuffer.values()) {
return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
}
+ private stopReservationExpiryDateSetInterval(): void {
+ if (this.reservationExpiryDateSetInterval) {
+ clearInterval(this.reservationExpiryDateSetInterval);
+ }
+ }
+
private getSupervisionUrlOcppKey(): string {
return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
}