From 63dd1f623f59da14c0d5e3e7f5989d1b1c85849b Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 27 Feb 2026 01:16:41 +0100 Subject: [PATCH] refactor: extract EVSE/reservation helpers, reduce duplication - Add getEvseStatus(evseId) to ChargingStation - Add hasPendingReservation/hasPendingReservations to Helpers - Standardize transactionId checks to != null - Add unit tests for reservation helper functions --- src/charging-station/ChargingStation.ts | 9 ++ src/charging-station/Helpers.ts | 33 +++++++ src/charging-station/index.ts | 2 + .../ocpp/2.0/OCPP20IncomingRequestService.ts | 55 ++++-------- tests/charging-station/Helpers.test.ts | 88 +++++++++++++++++++ 5 files changed, 150 insertions(+), 37 deletions(-) diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index f5960f44..008033c9 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -539,6 +539,15 @@ export class ChargingStation extends EventEmitter { return undefined } + /** + * Returns the EVSE status for the given EVSE ID. + * @param evseId - The EVSE ID to look up + * @returns The EvseStatus if found, undefined otherwise + */ + public getEvseStatus (evseId: number): EvseStatus | undefined { + return this.evses.get(evseId) + } + public getHeartbeatInterval (): number { const HeartbeatInterval = getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) if (HeartbeatInterval != null) { diff --git a/src/charging-station/Helpers.ts b/src/charging-station/Helpers.ts index fd274e10..35a596b4 100644 --- a/src/charging-station/Helpers.ts +++ b/src/charging-station/Helpers.ts @@ -107,6 +107,39 @@ export const hasReservationExpired = (reservation: Reservation): boolean => { return isPast(reservation.expiryDate) } +/** + * Checks if a connector has a pending (non-expired) reservation. + * @param connectorStatus - The connector status to check + * @returns true if the connector has a pending reservation, false otherwise + */ +export const hasPendingReservation = (connectorStatus: ConnectorStatus): boolean => { + return connectorStatus.reservation != null && !hasReservationExpired(connectorStatus.reservation) +} + +/** + * Checks if a charging station has any pending (non-expired) reservations. + * @param chargingStation - The charging station to check + * @returns true if any connector has a pending reservation, false otherwise + */ +export const hasPendingReservations = (chargingStation: ChargingStation): boolean => { + if (chargingStation.hasEvses) { + for (const evseStatus of chargingStation.evses.values()) { + for (const connectorStatus of evseStatus.connectors.values()) { + if (hasPendingReservation(connectorStatus)) { + return true + } + } + } + } else { + for (const connectorStatus of chargingStation.connectors.values()) { + if (hasPendingReservation(connectorStatus)) { + return true + } + } + } + return false +} + export const removeExpiredReservations = async ( chargingStation: ChargingStation ): Promise => { diff --git a/src/charging-station/index.ts b/src/charging-station/index.ts index 747e42eb..b01ac1b6 100644 --- a/src/charging-station/index.ts +++ b/src/charging-station/index.ts @@ -11,6 +11,8 @@ export { getConnectorChargingProfiles, getIdTagsFile, hasFeatureProfile, + hasPendingReservation, + hasPendingReservations, hasReservationExpired, prepareChargingProfileKind, removeExpiredReservations, diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index f9d08f6f..f87b528a 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -86,7 +86,12 @@ import { validateUUID, } from '../../../utils/index.js' import { getConfigurationKey } from '../../ConfigurationKeyUtils.js' -import { getIdTagsFile, hasReservationExpired, resetConnectorStatus } from '../../Helpers.js' +import { + getIdTagsFile, + hasPendingReservation, + hasPendingReservations, + resetConnectorStatus, +} from '../../Helpers.js' import { OCPPAuthServiceFactory } from '../auth/services/OCPPAuthServiceFactory.js' import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js' import { restoreConnectorStatus, sendAndSetConnectorStatus } from '../OCPPServiceUtils.js' @@ -1328,7 +1333,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { // Check for EVSE-specific active transactions if evseId is provided let evseHasActiveTransactions = false if (evseId !== undefined && evseId > 0) { - const evse = chargingStation.evses.get(evseId) + const evse = chargingStation.getEvseStatus(evseId) if (evse != null) { evseHasActiveTransactions = this.hasEvseActiveTransactions(evse) } @@ -1414,7 +1419,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { // OnIdle reset if (evseId !== undefined) { // EVSE-specific OnIdle reset - const evse = chargingStation.evses.get(evseId) + const evse = chargingStation.getEvseStatus(evseId) if (evse != null && !this.isEvseIdle(chargingStation, evse)) { logger.info( `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: OnIdle EVSE reset scheduled for EVSE ${evseId.toString()}, waiting for idle state` @@ -1516,7 +1521,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } // Get the first connector for this EVSE - const evse = chargingStation.evses.get(evseId) + const evse = chargingStation.getEvseStatus(evseId) if (evse == null) { const errorMsg = `EVSE ${evseId.toString()} does not exist on charging station` logger.warn( @@ -1794,7 +1799,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { */ private hasEvseActiveTransactions (evse: EvseStatus): boolean { for (const connector of evse.connectors.values()) { - if (connector.transactionId !== undefined) { + if (connector.transactionId != null) { return true } } @@ -1808,7 +1813,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { */ private hasEvsePendingReservations (evse: EvseStatus): boolean { for (const connector of evse.connectors.values()) { - if (connector.reservation != null && !hasReservationExpired(connector.reservation)) { + if (hasPendingReservation(connector)) { return true } } @@ -1829,30 +1834,6 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { ) } - /** - * Checks if charging station has any non-expired reservations per OCPP 2.0.1 Errata idle definition. - * @param chargingStation - The charging station instance - * @returns true if any connector has a pending (non-expired) reservation - */ - private hasPendingReservations (chargingStation: ChargingStation): boolean { - if (chargingStation.hasEvses) { - for (const evse of chargingStation.evses.values()) { - for (const connector of evse.connectors.values()) { - if (connector.reservation != null && !hasReservationExpired(connector.reservation)) { - return true - } - } - } - } else { - for (const connector of chargingStation.connectors.values()) { - if (connector.reservation != null && !hasReservationExpired(connector.reservation)) { - return true - } - } - } - return false - } - /** * Checks if charging station is idle per OCPP 2.0.1 Errata definition. * Idle means: no active transactions, no firmware update in progress, no pending reservations. @@ -1864,7 +1845,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { return ( chargingStation.getNumberOfRunningTransactions() === 0 && !this.hasFirmwareUpdateInProgress(chargingStation) && - !this.hasPendingReservations(chargingStation) + !hasPendingReservations(chargingStation) ) } @@ -2025,7 +2006,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { // Reset EVSE - this would typically involve resetting the EVSE hardware/software // For now, we'll restore connectors to available status after a short delay setTimeout(() => { - const evse = chargingStation.evses.get(evseId) + const evse = chargingStation.getEvseStatus(evseId) if (evse) { for (const [connectorId] of evse.connectors) { const connectorStatus = chargingStation.getConnectorStatus(connectorId) @@ -2053,7 +2034,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { */ private scheduleEvseResetOnIdle (chargingStation: ChargingStation, evseId: number): void { const monitorInterval = setInterval(() => { - const evse = chargingStation.evses.get(evseId) + const evse = chargingStation.getEvseStatus(evseId) if (evse != null) { if (this.isEvseIdle(chargingStation, evse)) { clearInterval(monitorInterval) @@ -2125,7 +2106,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { evseId: number, status: OCPP20ConnectorStatusEnumType ): void { - const evse = chargingStation.evses.get(evseId) + const evse = chargingStation.getEvseStatus(evseId) if (evse) { for (const [connectorId] of evse.connectors) { sendAndSetConnectorStatus( @@ -2210,7 +2191,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { for (const [evseId, evse] of chargingStation.evses) { for (const [connectorId, connector] of evse.connectors) { - if (connector.transactionId !== undefined) { + if (connector.transactionId != null) { logger.info( `${chargingStation.logPrefix()} ${moduleName}.terminateAllTransactions: Terminating transaction ${connector.transactionId.toString()} on connector ${connectorId.toString()}` ) @@ -2248,7 +2229,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { evseId: number, reason: OCPP20ReasonEnumType ): Promise { - const evse = chargingStation.evses.get(evseId) + const evse = chargingStation.getEvseStatus(evseId) if (!evse) { logger.warn( `${chargingStation.logPrefix()} ${moduleName}.terminateEvseTransactions: EVSE ${evseId.toString()} not found` @@ -2258,7 +2239,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { const terminationPromises: Promise[] = [] for (const [connectorId, connector] of evse.connectors) { - if (connector.transactionId !== undefined) { + if (connector.transactionId != null) { logger.info( `${chargingStation.logPrefix()} ${moduleName}.terminateEvseTransactions: Terminating transaction ${connector.transactionId.toString()} on connector ${connectorId.toString()}` ) diff --git a/tests/charging-station/Helpers.test.ts b/tests/charging-station/Helpers.test.ts index 2689d1d0..26057bc6 100644 --- a/tests/charging-station/Helpers.test.ts +++ b/tests/charging-station/Helpers.test.ts @@ -13,6 +13,9 @@ import { getHashId, getMaxNumberOfEvses, getPhaseRotationValue, + hasPendingReservation, + hasPendingReservations, + hasReservationExpired, validateStationInfo, } from '../../src/charging-station/Helpers.js' import { BaseError } from '../../src/exception/index.js' @@ -24,6 +27,7 @@ import { type ConnectorStatus, ConnectorStatusEnum, OCPPVersion, + type Reservation, } from '../../src/types/index.js' import { logger } from '../../src/utils/Logger.js' import { createChargingStation, createChargingStationTemplate } from '../ChargingStationFactory.js' @@ -32,6 +36,15 @@ await describe('Helpers test suite', async () => { const baseName = 'CS-TEST' const chargingStationTemplate = createChargingStationTemplate(baseName) + // Helper to create test reservations with configurable expiry + const createTestReservation = (expired = false): Reservation => + ({ + connectorId: 1, + expiryDate: new Date(Date.now() + (expired ? -60000 : 60000)), + idTag: 'tag1', + reservationId: 1, + }) as Reservation + await it('Verify getChargingStationId()', () => { expect(getChargingStationId(1, chargingStationTemplate)).toBe(`${baseName}-00001`) }) @@ -470,4 +483,79 @@ await describe('Helpers test suite', async () => { ConnectorStatusEnum.Available ) }) + + // Tests for reservation helper functions + await it('Verify hasReservationExpired() - expired reservation', () => { + expect(hasReservationExpired(createTestReservation(true))).toBe(true) + }) + + await it('Verify hasReservationExpired() - valid reservation', () => { + expect(hasReservationExpired(createTestReservation(false))).toBe(false) + }) + + await it('Verify hasPendingReservation() - no reservation', () => { + const connectorStatus = {} as ConnectorStatus + expect(hasPendingReservation(connectorStatus)).toBe(false) + }) + + await it('Verify hasPendingReservation() - with valid reservation', () => { + const connectorStatus = { reservation: createTestReservation(false) } as ConnectorStatus + expect(hasPendingReservation(connectorStatus)).toBe(true) + }) + + await it('Verify hasPendingReservation() - with expired reservation', () => { + const connectorStatus = { reservation: createTestReservation(true) } as ConnectorStatus + expect(hasPendingReservation(connectorStatus)).toBe(false) + }) + + await it('Verify hasPendingReservations() - no reservations (without EVSEs)', () => { + const chargingStation = createChargingStation({ baseName, connectorsCount: 2 }) + expect(hasPendingReservations(chargingStation)).toBe(false) + }) + + await it('Verify hasPendingReservations() - with pending reservation (without EVSEs)', () => { + const chargingStation = createChargingStation({ baseName, connectorsCount: 2 }) + const connectorStatus = chargingStation.connectors.get(1) + if (connectorStatus != null) { + connectorStatus.reservation = createTestReservation(false) + } + expect(hasPendingReservations(chargingStation)).toBe(true) + }) + + await it('Verify hasPendingReservations() - no reservations (with EVSEs)', () => { + const chargingStation = createChargingStation({ + baseName, + connectorsCount: 2, + stationInfo: { ocppVersion: OCPPVersion.VERSION_201 }, + }) + expect(hasPendingReservations(chargingStation)).toBe(false) + }) + + await it('Verify hasPendingReservations() - with pending reservation (with EVSEs)', () => { + const chargingStation = createChargingStation({ + baseName, + connectorsCount: 2, + stationInfo: { ocppVersion: OCPPVersion.VERSION_201 }, + }) + const firstEvse = chargingStation.evses.get(1) + const firstConnector = firstEvse?.connectors.values().next().value + if (firstConnector != null) { + firstConnector.reservation = createTestReservation(false) + } + expect(hasPendingReservations(chargingStation)).toBe(true) + }) + + await it('Verify hasPendingReservations() - with expired reservation only (with EVSEs)', () => { + const chargingStation = createChargingStation({ + baseName, + connectorsCount: 2, + stationInfo: { ocppVersion: OCPPVersion.VERSION_201 }, + }) + const firstEvse = chargingStation.evses.get(1) + const firstConnector = firstEvse?.connectors.values().next().value + if (firstConnector != null) { + firstConnector.reservation = createTestReservation(true) + } + expect(hasPendingReservations(chargingStation)).toBe(false) + }) }) -- 2.53.0