]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(ocpp): extract responseHandler and incomingRequestHandler into base classes
authorJérôme Benoit <jerome.benoit@sap.com>
Sun, 15 Mar 2026 22:57:20 +0000 (23:57 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Sun, 15 Mar 2026 22:57:20 +0000 (23:57 +0100)
Both methods were 95% identical across OCPP 1.6 and 2.0 subclasses
(~160 lines of pure duplication). Move the shared logic into the base
classes, parameterized by 3 abstract properties each subclass provides:
- bootNotificationRequestCommand / pendingStateBlockedCommands
- csmsName ('central system' vs 'CSMS')
- isRequestCommandSupported / isIncomingRequestCommandSupported

Zero behavior change — pure refactoring validated by 1853 passing tests.

src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/charging-station/ocpp/1.6/OCPP16ResponseService.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20ResponseService.ts
src/charging-station/ocpp/OCPPIncomingRequestService.ts
src/charging-station/ocpp/OCPPResponseService.ts

index 5d74632c3c5b965ee85d4a90ceb31bed17f42320..55264ac1a67676878cddc01731ef33de78981c36 100644 (file)
@@ -41,6 +41,7 @@ import {
   type GetConfigurationResponse,
   type GetDiagnosticsRequest,
   type GetDiagnosticsResponse,
+  type IncomingRequestCommand,
   type IncomingRequestHandler,
   type JsonType,
   type LogConfiguration,
@@ -107,7 +108,6 @@ import {
   convertToInt,
   formatDurationMilliSeconds,
   handleIncomingRequestError,
-  isAsyncFunction,
   isEmpty,
   isNotEmptyArray,
   isNotEmptyString,
@@ -157,16 +157,20 @@ const moduleName = 'OCPP16IncomingRequestService'
  */
 
 export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
+  protected readonly csmsName = 'central system'
+
+  protected readonly incomingRequestHandlers: Map<IncomingRequestCommand, IncomingRequestHandler>
+
   protected payloadValidatorFunctions: Map<OCPP16IncomingRequestCommand, ValidateFunction<JsonType>>
 
-  private readonly incomingRequestHandlers: Map<
-    OCPP16IncomingRequestCommand,
-    IncomingRequestHandler
-  >
+  protected readonly pendingStateBlockedCommands: IncomingRequestCommand[] = [
+    OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
+    OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
+  ]
 
   public constructor () {
     super(OCPPVersion.VERSION_16)
-    this.incomingRequestHandlers = new Map<OCPP16IncomingRequestCommand, IncomingRequestHandler>([
+    this.incomingRequestHandlers = new Map<IncomingRequestCommand, IncomingRequestHandler>([
       [
         OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
         this.handleRequestCancelReservation.bind(this) as unknown as IncomingRequestHandler,
@@ -527,99 +531,18 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     )
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  public async incomingRequestHandler<ReqType extends JsonType, ResType extends JsonType>(
+  public override stop (chargingStation: ChargingStation): void {
+    /* no-op for OCPP 1.6 */
+  }
+
+  protected isIncomingRequestCommandSupported (
     chargingStation: ChargingStation,
-    messageId: string,
-    commandName: OCPP16IncomingRequestCommand,
-    commandPayload: ReqType
-  ): Promise<void> {
-    let response: ResType
-    if (
-      chargingStation.stationInfo?.ocppStrictCompliance === true &&
-      chargingStation.inPendingState() &&
-      (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION ||
-        commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION)
-    ) {
-      throw new OCPPError(
-        ErrorType.SECURITY_ERROR,
-        `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
-          commandPayload,
-          undefined,
-          2
-        )} while the charging station is in pending state on the central system`,
-        commandName,
-        commandPayload
-      )
-    }
-    if (
-      chargingStation.inAcceptedState() ||
-      chargingStation.inPendingState() ||
-      (chargingStation.stationInfo?.ocppStrictCompliance === false &&
-        chargingStation.inUnknownState())
-    ) {
-      if (
-        this.incomingRequestHandlers.has(commandName) &&
-        OCPP16ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName)
-      ) {
-        try {
-          this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload)
-          // Call the method to build the response
-          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          const incomingRequestHandler = this.incomingRequestHandlers.get(commandName)!
-          if (isAsyncFunction(incomingRequestHandler)) {
-            response = (await incomingRequestHandler(chargingStation, commandPayload)) as ResType
-          } else {
-            response = incomingRequestHandler(chargingStation, commandPayload) as ResType
-          }
-        } catch (error) {
-          // Log
-          logger.error(
-            `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`,
-            error
-          )
-          throw error
-        }
-      } else {
-        // Throw exception
-        throw new OCPPError(
-          ErrorType.NOT_IMPLEMENTED,
-          `${commandName} is not implemented to handle request PDU ${JSON.stringify(
-            commandPayload,
-            undefined,
-            2
-          )}`,
-          commandName,
-          commandPayload
-        )
-      }
-    } else {
-      throw new OCPPError(
-        ErrorType.SECURITY_ERROR,
-        `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
-          commandPayload,
-          undefined,
-          2
-        )} while the charging station is not registered on the central system`,
-        commandName,
-        commandPayload
-      )
-    }
-    // Send the built response
-    await chargingStation.ocppRequestService.sendResponse(
+    commandName: IncomingRequestCommand
+  ): boolean {
+    return OCPP16ServiceUtils.isIncomingRequestCommandSupported(
       chargingStation,
-      messageId,
-      response,
-      commandName
+      commandName as OCPP16IncomingRequestCommand
     )
-    // Emit command name event to allow delayed handling only if there are listeners
-    if (this.listenerCount(commandName) > 0) {
-      this.emit(commandName, chargingStation, commandPayload, response)
-    }
-  }
-
-  public override stop (chargingStation: ChargingStation): void {
-    /* no-op for OCPP 1.6 */
   }
 
   private composeCompositeSchedule (
index c4303c68c386408efcfb818efc2fd6c3cd6f5abf..64b4c30d02e7973db67c130c36d4b433372e6c2e 100644 (file)
@@ -9,10 +9,8 @@ import {
   hasReservationExpired,
   resetConnectorStatus,
 } from '../../../charging-station/index.js'
-import { OCPPError } from '../../../exception/index.js'
 import {
   ChargingStationEvents,
-  ErrorType,
   type JsonType,
   OCPP16AuthorizationStatus,
   type OCPP16AuthorizeRequest,
@@ -31,10 +29,11 @@ import {
   type OCPP16StopTransactionResponse,
   OCPPVersion,
   RegistrationStatusEnumType,
+  type RequestCommand,
   ReservationTerminationReason,
   type ResponseHandler,
 } from '../../../types/index.js'
-import { Constants, convertToInt, isAsyncFunction, logger } from '../../../utils/index.js'
+import { Constants, convertToInt, logger } from '../../../utils/index.js'
 import { OCPPResponseService } from '../OCPPResponseService.js'
 import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js'
 
@@ -80,12 +79,16 @@ export class OCPP16ResponseService extends OCPPResponseService {
     ValidateFunction<JsonType>
   >
 
+  protected readonly bootNotificationRequestCommand = OCPP16RequestCommand.BOOT_NOTIFICATION
+  protected readonly csmsName = 'central system'
+
   protected payloadValidatorFunctions: Map<OCPP16RequestCommand, ValidateFunction<JsonType>>
-  private readonly responseHandlers: Map<OCPP16RequestCommand, ResponseHandler>
+
+  protected readonly responseHandlers: Map<RequestCommand, ResponseHandler>
 
   public constructor () {
     super(OCPPVersion.VERSION_16)
-    this.responseHandlers = new Map<OCPP16RequestCommand, ResponseHandler>([
+    this.responseHandlers = new Map<RequestCommand, ResponseHandler>([
       [OCPP16RequestCommand.AUTHORIZE, this.handleResponseAuthorize.bind(this) as ResponseHandler],
       [
         OCPP16RequestCommand.BOOT_NOTIFICATION,
@@ -125,77 +128,14 @@ export class OCPP16ResponseService extends OCPPResponseService {
       )
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  public async responseHandler<ReqType extends JsonType, ResType extends JsonType>(
+  protected isRequestCommandSupported (
     chargingStation: ChargingStation,
-    commandName: OCPP16RequestCommand,
-    payload: ResType,
-    requestPayload: ReqType
-  ): Promise<void> {
-    if (
-      chargingStation.inAcceptedState() ||
-      ((chargingStation.inUnknownState() || chargingStation.inPendingState()) &&
-        commandName === OCPP16RequestCommand.BOOT_NOTIFICATION) ||
-      (chargingStation.stationInfo?.ocppStrictCompliance === false &&
-        (chargingStation.inUnknownState() || chargingStation.inPendingState()))
-    ) {
-      if (
-        this.responseHandlers.has(commandName) &&
-        OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName)
-      ) {
-        try {
-          this.validateResponsePayload(chargingStation, commandName, payload)
-          logger.debug(
-            `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handling '${commandName}' response`
-          )
-          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          const responseHandler = this.responseHandlers.get(commandName)!
-          if (isAsyncFunction(responseHandler)) {
-            await responseHandler(chargingStation, payload, requestPayload)
-          } else {
-            ;(
-              responseHandler as (
-                chargingStation: ChargingStation,
-                payload: JsonType,
-                requestPayload?: JsonType
-              ) => void
-            )(chargingStation, payload, requestPayload)
-          }
-          logger.debug(
-            `${chargingStation.logPrefix()} ${moduleName}.responseHandler: '${commandName}' response processed successfully`
-          )
-        } catch (error) {
-          logger.error(
-            `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle '${commandName}' response error:`,
-            error
-          )
-          throw error
-        }
-      } else {
-        // Throw exception
-        throw new OCPPError(
-          ErrorType.NOT_IMPLEMENTED,
-          `${commandName} is not implemented to handle response PDU ${JSON.stringify(
-            payload,
-            undefined,
-            2
-          )}`,
-          commandName,
-          payload
-        )
-      }
-    } else {
-      throw new OCPPError(
-        ErrorType.SECURITY_ERROR,
-        `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
-          payload,
-          undefined,
-          2
-        )} while the charging station is not registered on the central system`,
-        commandName,
-        payload
-      )
-    }
+    commandName: RequestCommand
+  ): boolean {
+    return OCPP16ServiceUtils.isRequestCommandSupported(
+      chargingStation,
+      commandName as OCPP16RequestCommand
+    )
   }
 
   private handleResponseAuthorize (
index a19f143e2a7ecf3aff4af446ef10bf785357efa8..c4c25456491cd5ce07e0512af80bc27b02aa5172 100644 (file)
@@ -32,6 +32,7 @@ import {
   GetCertificateIdUseEnumType,
   GetInstalledCertificateStatusEnumType,
   GetVariableStatusEnumType,
+  type IncomingRequestCommand,
   type IncomingRequestHandler,
   InstallCertificateStatusEnumType,
   InstallCertificateUseEnumType,
@@ -126,14 +127,7 @@ import {
   OCPP20ChargingRateUnitEnumType,
   OCPP20ReasonEnumType,
 } from '../../../types/ocpp/2.0/Transaction.js'
-import {
-  Constants,
-  generateUUID,
-  isAsyncFunction,
-  logger,
-  sleep,
-  validateUUID,
-} from '../../../utils/index.js'
+import { Constants, generateUUID, logger, sleep, validateUUID } from '../../../utils/index.js'
 import { getConfigurationKey } from '../../ConfigurationKeyUtils.js'
 import {
   getIdTagsFile,
@@ -165,18 +159,22 @@ interface OCPP20PerStationState {
 }
 
 export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
+  protected readonly csmsName = 'CSMS'
+
+  protected readonly incomingRequestHandlers: Map<IncomingRequestCommand, IncomingRequestHandler>
+
   protected payloadValidatorFunctions: Map<OCPP20IncomingRequestCommand, ValidateFunction<JsonType>>
 
-  private readonly incomingRequestHandlers: Map<
-    OCPP20IncomingRequestCommand,
-    IncomingRequestHandler
-  >
+  protected readonly pendingStateBlockedCommands: IncomingRequestCommand[] = [
+    OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
+    OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION,
+  ]
 
   private readonly stationStates = new WeakMap<ChargingStation, OCPP20PerStationState>()
 
   public constructor () {
     super(OCPPVersion.VERSION_201)
-    this.incomingRequestHandlers = new Map<OCPP20IncomingRequestCommand, IncomingRequestHandler>([
+    this.incomingRequestHandlers = new Map<IncomingRequestCommand, IncomingRequestHandler>([
       [
         OCPP20IncomingRequestCommand.CERTIFICATE_SIGNED,
         this.toHandler(this.handleRequestCertificateSigned.bind(this)),
@@ -570,96 +568,6 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
     return setVariablesResponse
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  public async incomingRequestHandler<ReqType extends JsonType, ResType extends JsonType>(
-    chargingStation: ChargingStation,
-    messageId: string,
-    commandName: OCPP20IncomingRequestCommand,
-    commandPayload: ReqType
-  ): Promise<void> {
-    let response: ResType
-    if (
-      chargingStation.stationInfo?.ocppStrictCompliance === true &&
-      chargingStation.inPendingState() &&
-      (commandName === OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION ||
-        commandName === OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION)
-    ) {
-      throw new OCPPError(
-        ErrorType.SECURITY_ERROR,
-        `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
-          commandPayload,
-          undefined,
-          2
-        )} while the charging station is in pending state on the CSMS`,
-        commandName,
-        commandPayload
-      )
-    }
-    if (
-      chargingStation.inAcceptedState() ||
-      chargingStation.inPendingState() ||
-      (chargingStation.stationInfo?.ocppStrictCompliance === false &&
-        chargingStation.inUnknownState())
-    ) {
-      if (
-        this.incomingRequestHandlers.has(commandName) &&
-        OCPP20ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName)
-      ) {
-        try {
-          this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload)
-          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          const incomingRequestHandler = this.incomingRequestHandlers.get(commandName)!
-          if (isAsyncFunction(incomingRequestHandler)) {
-            response = (await incomingRequestHandler(chargingStation, commandPayload)) as ResType
-          } else {
-            response = incomingRequestHandler(chargingStation, commandPayload) as ResType
-          }
-        } catch (error) {
-          // Log
-          logger.error(
-            `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`,
-            error
-          )
-          throw error
-        }
-      } else {
-        // Throw exception
-        throw new OCPPError(
-          ErrorType.NOT_IMPLEMENTED,
-          `${commandName} is not implemented to handle request PDU ${JSON.stringify(
-            commandPayload,
-            undefined,
-            2
-          )}`,
-          commandName,
-          commandPayload
-        )
-      }
-    } else {
-      throw new OCPPError(
-        ErrorType.SECURITY_ERROR,
-        `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
-          commandPayload,
-          undefined,
-          2
-        )} while the charging station is not registered on the CSMS`,
-        commandName,
-        commandPayload
-      )
-    }
-    // Send the built response
-    await chargingStation.ocppRequestService.sendResponse(
-      chargingStation,
-      messageId,
-      response,
-      commandName
-    )
-    // Emit command name event to allow delayed handling only if there are listeners
-    if (this.listenerCount(commandName) > 0) {
-      this.emit(commandName, chargingStation, commandPayload, response)
-    }
-  }
-
   public override stop (chargingStation: ChargingStation): void {
     const ss = this.stationStates.get(chargingStation)
     if (ss != null) {
@@ -714,6 +622,16 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
     }
   }
 
+  protected isIncomingRequestCommandSupported (
+    chargingStation: ChargingStation,
+    commandName: IncomingRequestCommand
+  ): boolean {
+    return OCPP20ServiceUtils.isIncomingRequestCommandSupported(
+      chargingStation,
+      commandName as OCPP20IncomingRequestCommand
+    )
+  }
+
   private buildReportData (
     chargingStation: ChargingStation,
     reportBase: ReportBaseEnumType
index 134163a8797aaa320bf197b1c55bd9af507b12f3..60611914596d498902ea693afd9d81a34e832125 100644 (file)
@@ -1,10 +1,8 @@
 import type { ValidateFunction } from 'ajv'
 
 import { addConfigurationKey, type ChargingStation } from '../../../charging-station/index.js'
-import { OCPPError } from '../../../exception/index.js'
 import {
   ChargingStationEvents,
-  ErrorType,
   type JsonType,
   type OCPP20BootNotificationResponse,
   type OCPP20FirmwareStatusNotificationResponse,
@@ -22,10 +20,11 @@ import {
   type OCPP20TransactionEventResponse,
   OCPPVersion,
   RegistrationStatusEnumType,
+  type RequestCommand,
   type ResponseHandler,
 } from '../../../types/index.js'
 import { OCPP20AuthorizationStatusEnumType } from '../../../types/ocpp/2.0/Transaction.js'
-import { isAsyncFunction, logger } from '../../../utils/index.js'
+import { logger } from '../../../utils/index.js'
 import { OCPPResponseService } from '../OCPPResponseService.js'
 import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js'
 
@@ -79,12 +78,16 @@ export class OCPP20ResponseService extends OCPPResponseService {
     ValidateFunction<JsonType>
   >
 
+  protected readonly bootNotificationRequestCommand = OCPP20RequestCommand.BOOT_NOTIFICATION
+  protected readonly csmsName = 'CSMS'
+
   protected payloadValidatorFunctions: Map<OCPP20RequestCommand, ValidateFunction<JsonType>>
-  private readonly responseHandlers: Map<OCPP20RequestCommand, ResponseHandler>
+
+  protected readonly responseHandlers: Map<RequestCommand, ResponseHandler>
 
   public constructor () {
     super(OCPPVersion.VERSION_201)
-    this.responseHandlers = new Map<OCPP20RequestCommand, ResponseHandler>([
+    this.responseHandlers = new Map<RequestCommand, ResponseHandler>([
       [
         OCPP20RequestCommand.BOOT_NOTIFICATION,
         this.handleResponseBootNotification.bind(this) as ResponseHandler,
@@ -136,77 +139,14 @@ export class OCPP20ResponseService extends OCPPResponseService {
       )
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  public async responseHandler<ReqType extends JsonType, ResType extends JsonType>(
+  protected isRequestCommandSupported (
     chargingStation: ChargingStation,
-    commandName: OCPP20RequestCommand,
-    payload: ResType,
-    requestPayload: ReqType
-  ): Promise<void> {
-    if (
-      chargingStation.inAcceptedState() ||
-      ((chargingStation.inUnknownState() || chargingStation.inPendingState()) &&
-        commandName === OCPP20RequestCommand.BOOT_NOTIFICATION) ||
-      (chargingStation.stationInfo?.ocppStrictCompliance === false &&
-        (chargingStation.inUnknownState() || chargingStation.inPendingState()))
-    ) {
-      if (
-        this.responseHandlers.has(commandName) &&
-        OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName)
-      ) {
-        try {
-          this.validateResponsePayload(chargingStation, commandName, payload)
-          logger.debug(
-            `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handling '${commandName}' response`
-          )
-          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          const responseHandler = this.responseHandlers.get(commandName)!
-          if (isAsyncFunction(responseHandler)) {
-            await responseHandler(chargingStation, payload, requestPayload)
-          } else {
-            ;(
-              responseHandler as (
-                chargingStation: ChargingStation,
-                payload: JsonType,
-                requestPayload?: JsonType
-              ) => void
-            )(chargingStation, payload, requestPayload)
-          }
-          logger.debug(
-            `${chargingStation.logPrefix()} ${moduleName}.responseHandler: '${commandName}' response processed successfully`
-          )
-        } catch (error) {
-          logger.error(
-            `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle '${commandName}' response error:`,
-            error
-          )
-          throw error
-        }
-      } else {
-        // Throw exception
-        throw new OCPPError(
-          ErrorType.NOT_IMPLEMENTED,
-          `${commandName} is not implemented to handle response PDU ${JSON.stringify(
-            payload,
-            undefined,
-            2
-          )}`,
-          commandName,
-          payload
-        )
-      }
-    } else {
-      throw new OCPPError(
-        ErrorType.SECURITY_ERROR,
-        `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
-          payload,
-          undefined,
-          2
-        )} while the charging station is not registered on the CSMS`,
-        commandName,
-        payload
-      )
-    }
+    commandName: RequestCommand
+  ): boolean {
+    return OCPP20ServiceUtils.isRequestCommandSupported(
+      chargingStation,
+      commandName as OCPP20RequestCommand
+    )
   }
 
   private handleResponseBootNotification (
index c73a03ce91395ee3c8dfd21ee531d1d2c08faa60..40449ef7f87345a140bd02fa53f165b344961946 100644 (file)
@@ -2,11 +2,16 @@ import _Ajv, { type ValidateFunction } from 'ajv'
 import _ajvFormats from 'ajv-formats'
 import { EventEmitter } from 'node:events'
 
-import type { IncomingRequestCommand, JsonType, OCPPVersion } from '../../types/index.js'
-
 import { type ChargingStation } from '../../charging-station/index.js'
 import { OCPPError } from '../../exception/index.js'
-import { logger } from '../../utils/index.js'
+import {
+  ErrorType,
+  type IncomingRequestCommand,
+  type IncomingRequestHandler,
+  type JsonType,
+  type OCPPVersion,
+} from '../../types/index.js'
+import { isAsyncFunction, logger } from '../../utils/index.js'
 import { ajvErrorsToErrorType } from './OCPPServiceUtils.js'
 
 type Ajv = _Ajv.default
@@ -23,11 +28,18 @@ export abstract class OCPPIncomingRequestService extends EventEmitter {
   >()
 
   protected readonly ajv: Ajv
+  protected abstract readonly csmsName: string
+  protected abstract readonly incomingRequestHandlers: Map<
+    IncomingRequestCommand,
+    IncomingRequestHandler
+  >
+
   protected abstract payloadValidatorFunctions: Map<
     IncomingRequestCommand,
     ValidateFunction<JsonType>
   >
 
+  protected abstract readonly pendingStateBlockedCommands: IncomingRequestCommand[]
   private readonly version: OCPPVersion
 
   protected constructor (version: OCPPVersion) {
@@ -49,15 +61,101 @@ export abstract class OCPPIncomingRequestService extends EventEmitter {
     return OCPPIncomingRequestService.instances.get(this) as T
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unnecessary-type-parameters
-  public abstract incomingRequestHandler<ReqType extends JsonType, ResType extends JsonType>(
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+  public async incomingRequestHandler<ReqType extends JsonType, ResType extends JsonType>(
     chargingStation: ChargingStation,
     messageId: string,
     commandName: IncomingRequestCommand,
     commandPayload: ReqType
-  ): Promise<void>
+  ): Promise<void> {
+    let response: ResType
+    if (
+      chargingStation.stationInfo?.ocppStrictCompliance === true &&
+      chargingStation.inPendingState() &&
+      this.pendingStateBlockedCommands.includes(commandName)
+    ) {
+      throw new OCPPError(
+        ErrorType.SECURITY_ERROR,
+        `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
+          commandPayload,
+          undefined,
+          2
+        )} while the charging station is in pending state on the ${this.csmsName}`,
+        commandName,
+        commandPayload
+      )
+    }
+    if (
+      chargingStation.inAcceptedState() ||
+      chargingStation.inPendingState() ||
+      (chargingStation.stationInfo?.ocppStrictCompliance === false &&
+        chargingStation.inUnknownState())
+    ) {
+      if (
+        this.incomingRequestHandlers.has(commandName) &&
+        this.isIncomingRequestCommandSupported(chargingStation, commandName)
+      ) {
+        try {
+          this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload)
+          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          const incomingRequestHandler = this.incomingRequestHandlers.get(commandName)!
+          if (isAsyncFunction(incomingRequestHandler)) {
+            response = (await incomingRequestHandler(chargingStation, commandPayload)) as ResType
+          } else {
+            response = incomingRequestHandler(chargingStation, commandPayload) as ResType
+          }
+        } catch (error) {
+          // Log
+          logger.error(
+            `${chargingStation.logPrefix()} ${this.constructor.name}.incomingRequestHandler: Handle incoming request error:`,
+            error
+          )
+          throw error
+        }
+      } else {
+        // Throw exception
+        throw new OCPPError(
+          ErrorType.NOT_IMPLEMENTED,
+          `${commandName} is not implemented to handle request PDU ${JSON.stringify(
+            commandPayload,
+            undefined,
+            2
+          )}`,
+          commandName,
+          commandPayload
+        )
+      }
+    } else {
+      throw new OCPPError(
+        ErrorType.SECURITY_ERROR,
+        `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
+          commandPayload,
+          undefined,
+          2
+        )} while the charging station is not registered on the ${this.csmsName}`,
+        commandName,
+        commandPayload
+      )
+    }
+    // Send the built response
+    await chargingStation.ocppRequestService.sendResponse(
+      chargingStation,
+      messageId,
+      response,
+      commandName
+    )
+    // Emit command name event to allow delayed handling only if there are listeners
+    if (this.listenerCount(commandName) > 0) {
+      this.emit(commandName, chargingStation, commandPayload, response)
+    }
+  }
 
   public abstract stop (chargingStation: ChargingStation): void
+
+  protected abstract isIncomingRequestCommandSupported (
+    chargingStation: ChargingStation,
+    commandName: IncomingRequestCommand
+  ): boolean
   /**
    * Validates incoming request payload against JSON schema
    * @param chargingStation - The charging station instance processing the request
index baab610ff72b95152b983169e7b689b5a7d6ee7e..5e6966c449d059d731ed10888878cee02d2cdee4 100644 (file)
@@ -2,15 +2,17 @@ import _Ajv, { type ValidateFunction } from 'ajv'
 import _ajvFormats from 'ajv-formats'
 
 import type { ChargingStation } from '../../charging-station/index.js'
-import type {
-  IncomingRequestCommand,
-  JsonType,
-  OCPPVersion,
-  RequestCommand,
-} from '../../types/index.js'
 
 import { OCPPError } from '../../exception/index.js'
-import { Constants, logger } from '../../utils/index.js'
+import {
+  ErrorType,
+  type IncomingRequestCommand,
+  type JsonType,
+  type OCPPVersion,
+  type RequestCommand,
+  type ResponseHandler,
+} from '../../types/index.js'
+import { Constants, isAsyncFunction, logger } from '../../utils/index.js'
 import { ajvErrorsToErrorType } from './OCPPServiceUtils.js'
 
 type Ajv = _Ajv.default
@@ -29,8 +31,11 @@ export abstract class OCPPResponseService {
 
   protected readonly ajv: Ajv
   protected readonly ajvIncomingRequest: Ajv
+  protected abstract readonly bootNotificationRequestCommand: RequestCommand
+  protected abstract readonly csmsName: string
   protected emptyResponseHandler = Constants.EMPTY_FUNCTION
   protected abstract payloadValidatorFunctions: Map<RequestCommand, ValidateFunction<JsonType>>
+  protected abstract readonly responseHandlers: Map<RequestCommand, ResponseHandler>
   private readonly version: OCPPVersion
 
   protected constructor (version: OCPPVersion) {
@@ -57,12 +62,82 @@ export abstract class OCPPResponseService {
   }
 
   // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  public abstract responseHandler<ReqType extends JsonType, ResType extends JsonType>(
+  public async responseHandler<ReqType extends JsonType, ResType extends JsonType>(
     chargingStation: ChargingStation,
     commandName: RequestCommand,
     payload: ResType,
     requestPayload: ReqType
-  ): Promise<void>
+  ): Promise<void> {
+    if (
+      chargingStation.inAcceptedState() ||
+      ((chargingStation.inUnknownState() || chargingStation.inPendingState()) &&
+        commandName === this.bootNotificationRequestCommand) ||
+      (chargingStation.stationInfo?.ocppStrictCompliance === false &&
+        (chargingStation.inUnknownState() || chargingStation.inPendingState()))
+    ) {
+      if (
+        this.responseHandlers.has(commandName) &&
+        this.isRequestCommandSupported(chargingStation, commandName)
+      ) {
+        try {
+          this.validateResponsePayload(chargingStation, commandName, payload)
+          logger.debug(
+            `${chargingStation.logPrefix()} ${this.constructor.name}.responseHandler: Handling '${commandName}' response`
+          )
+          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          const responseHandler = this.responseHandlers.get(commandName)!
+          if (isAsyncFunction(responseHandler)) {
+            await responseHandler(chargingStation, payload, requestPayload)
+          } else {
+            ;(
+              responseHandler as (
+                chargingStation: ChargingStation,
+                payload: JsonType,
+                requestPayload?: JsonType
+              ) => void
+            )(chargingStation, payload, requestPayload)
+          }
+          logger.debug(
+            `${chargingStation.logPrefix()} ${this.constructor.name}.responseHandler: '${commandName}' response processed successfully`
+          )
+        } catch (error) {
+          logger.error(
+            `${chargingStation.logPrefix()} ${this.constructor.name}.responseHandler: Handle '${commandName}' response error:`,
+            error
+          )
+          throw error
+        }
+      } else {
+        // Throw exception
+        throw new OCPPError(
+          ErrorType.NOT_IMPLEMENTED,
+          `${commandName} is not implemented to handle response PDU ${JSON.stringify(
+            payload,
+            undefined,
+            2
+          )}`,
+          commandName,
+          payload
+        )
+      }
+    } else {
+      throw new OCPPError(
+        ErrorType.SECURITY_ERROR,
+        `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
+          payload,
+          undefined,
+          2
+        )} while the charging station is not registered on the ${this.csmsName}`,
+        commandName,
+        payload
+      )
+    }
+  }
+
+  protected abstract isRequestCommandSupported (
+    chargingStation: ChargingStation,
+    commandName: RequestCommand
+  ): boolean
 
   /**
    * Validates incoming response payload against JSON schema