]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor: extract EVSE/reservation helpers, reduce duplication
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 27 Feb 2026 00:16:41 +0000 (01:16 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 27 Feb 2026 00:16:41 +0000 (01:16 +0100)
- 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
src/charging-station/Helpers.ts
src/charging-station/index.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
tests/charging-station/Helpers.test.ts

index f5960f44995774c6548c1b593529629584cc4d86..008033c983d6205510a6e9f1c2a625b339f30918 100644 (file)
@@ -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) {
index fd274e10031f44b62ad74e9f13aa2fa68016aaf4..35a596b465678b5d6e5dad06af2635cda2bf7ab8 100644 (file)
@@ -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<void> => {
index 747e42eb735cd8e404b029b882ee366734c894a9..b01ac1b69491c5fc4c0dc00fe070c64703779a13 100644 (file)
@@ -11,6 +11,8 @@ export {
   getConnectorChargingProfiles,
   getIdTagsFile,
   hasFeatureProfile,
+  hasPendingReservation,
+  hasPendingReservations,
   hasReservationExpired,
   prepareChargingProfileKind,
   removeExpiredReservations,
index f9d08f6f7e7f0187cda428645dc2656fc9692224..f87b528a8485ce01907ff6520618eacb4b33f015 100644 (file)
@@ -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<void> {
-    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<unknown>[] = []
     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()}`
         )
index 2689d1d07387d822a4466f28097f870731eb2d2a..26057bc69c629de134c5690db22e797c3f815552 100644 (file)
@@ -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)
+  })
 })