+ public async stopTransactionOnConnector(
+ connectorId: number,
+ reason?: StopTransactionReason,
+ ): Promise<StopTransactionResponse> {
+ const transactionId = this.getConnectorStatus(connectorId)?.transactionId;
+ if (
+ this.stationInfo?.beginEndMeterValues === true &&
+ this.stationInfo?.ocppStrictCompliance === true &&
+ this.stationInfo?.outOfOrderEndMeterValues === false
+ ) {
+ // FIXME: Implement OCPP version agnostic helpers
+ const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
+ this,
+ connectorId,
+ this.getEnergyActiveImportRegisterByTransactionId(transactionId!),
+ );
+ await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
+ this,
+ RequestCommand.METER_VALUES,
+ {
+ connectorId,
+ transactionId,
+ meterValue: [transactionEndMeterValue],
+ },
+ );
+ }
+ return this.ocppRequestService.requestHandler<StopTransactionRequest, StopTransactionResponse>(
+ this,
+ RequestCommand.STOP_TRANSACTION,
+ {
+ transactionId,
+ meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId!, true),
+ ...(isNullOrUndefined(reason) && { reason }),
+ },
+ );
+ }
+
+ public getReserveConnectorZeroSupported(): boolean {
+ return convertToBoolean(
+ getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value,
+ );
+ }
+
+ public async addReservation(reservation: Reservation): Promise<void> {
+ const reservationFound = this.getReservationBy('reservationId', reservation.reservationId);
+ if (!isUndefined(reservationFound)) {
+ await this.removeReservation(
+ reservationFound!,
+ ReservationTerminationReason.REPLACE_EXISTING,
+ );
+ }
+ this.getConnectorStatus(reservation.connectorId)!.reservation = reservation;
+ await OCPPServiceUtils.sendAndSetConnectorStatus(
+ this,
+ reservation.connectorId,
+ ConnectorStatusEnum.Reserved,
+ undefined,
+ { send: reservation.connectorId !== 0 },
+ );
+ }
+
+ public async removeReservation(
+ reservation: Reservation,
+ reason: ReservationTerminationReason,
+ ): Promise<void> {
+ const connector = this.getConnectorStatus(reservation.connectorId)!;
+ switch (reason) {
+ case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
+ case ReservationTerminationReason.TRANSACTION_STARTED:
+ delete connector.reservation;
+ break;
+ case ReservationTerminationReason.RESERVATION_CANCELED:
+ case ReservationTerminationReason.REPLACE_EXISTING:
+ case ReservationTerminationReason.EXPIRED:
+ await OCPPServiceUtils.sendAndSetConnectorStatus(
+ this,
+ reservation.connectorId,
+ ConnectorStatusEnum.Available,
+ undefined,
+ { send: reservation.connectorId !== 0 },
+ );
+ delete connector.reservation;
+ break;
+ default:
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ throw new BaseError(`Unknown reservation termination reason '${reason}'`);
+ }
+ }
+
+ public getReservationBy(
+ filterKey: ReservationKey,
+ value: number | string,
+ ): Reservation | undefined {
+ if (this.hasEvses) {
+ for (const evseStatus of this.evses.values()) {
+ for (const connectorStatus of evseStatus.connectors.values()) {
+ if (connectorStatus?.reservation?.[filterKey] === value) {
+ return connectorStatus.reservation;
+ }
+ }
+ }
+ } else {
+ for (const connectorStatus of this.connectors.values()) {
+ if (connectorStatus?.reservation?.[filterKey] === value) {
+ return connectorStatus.reservation;
+ }
+ }
+ }
+ }
+
+ public isConnectorReservable(
+ reservationId: number,
+ idTag?: string,
+ connectorId?: number,
+ ): boolean {
+ const reservation = this.getReservationBy('reservationId', reservationId);
+ const reservationExists = !isUndefined(reservation) && !hasReservationExpired(reservation!);
+ if (arguments.length === 1) {
+ return !reservationExists;
+ } else if (arguments.length > 1) {
+ const userReservation = !isUndefined(idTag)
+ ? this.getReservationBy('idTag', idTag!)
+ : undefined;
+ const userReservationExists =
+ !isUndefined(userReservation) && !hasReservationExpired(userReservation!);
+ const notConnectorZero = isUndefined(connectorId) ? true : connectorId! > 0;
+ const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
+ return (
+ !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
+ );
+ }
+ return false;
+ }
+
+ private startReservationExpirationSetInterval(customInterval?: number): void {
+ const interval = customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_INTERVAL;
+ if (interval > 0) {
+ logger.info(
+ `${this.logPrefix()} Reservation expiration date checks started every ${formatDurationMilliSeconds(
+ interval,
+ )}`,
+ );
+ this.reservationExpirationSetInterval = setInterval((): void => {
+ removeExpiredReservations(this).catch(Constants.EMPTY_FUNCTION);
+ }, interval);
+ }
+ }
+
+ private stopReservationExpirationSetInterval(): void {
+ if (!isNullOrUndefined(this.reservationExpirationSetInterval)) {
+ clearInterval(this.reservationExpirationSetInterval);