From: Jérôme Benoit Date: Sun, 15 Mar 2026 22:57:20 +0000 (+0100) Subject: refactor(ocpp): extract responseHandler and incomingRequestHandler into base classes X-Git-Tag: ocpp-server@v3.1.0~3 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=ef86be68d6b8ed032921d85b37315629ad6cf0f2;p=e-mobility-charging-stations-simulator.git refactor(ocpp): extract responseHandler and incomingRequestHandler into base classes 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. --- diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 5d74632c..55264ac1 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -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 + protected payloadValidatorFunctions: Map> - 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([ + this.incomingRequestHandlers = new Map([ [ 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( + public override stop (chargingStation: ChargingStation): void { + /* no-op for OCPP 1.6 */ + } + + protected isIncomingRequestCommandSupported ( chargingStation: ChargingStation, - messageId: string, - commandName: OCPP16IncomingRequestCommand, - commandPayload: ReqType - ): Promise { - 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 ( diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index c4303c68..64b4c30d 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -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 > + protected readonly bootNotificationRequestCommand = OCPP16RequestCommand.BOOT_NOTIFICATION + protected readonly csmsName = 'central system' + protected payloadValidatorFunctions: Map> - private readonly responseHandlers: Map + + protected readonly responseHandlers: Map public constructor () { super(OCPPVersion.VERSION_16) - this.responseHandlers = new Map([ + this.responseHandlers = new Map([ [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( + protected isRequestCommandSupported ( chargingStation: ChargingStation, - commandName: OCPP16RequestCommand, - payload: ResType, - requestPayload: ReqType - ): Promise { - 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 ( diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index a19f143e..c4c25456 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -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 + protected payloadValidatorFunctions: Map> - private readonly incomingRequestHandlers: Map< - OCPP20IncomingRequestCommand, - IncomingRequestHandler - > + protected readonly pendingStateBlockedCommands: IncomingRequestCommand[] = [ + OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION, + OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION, + ] private readonly stationStates = new WeakMap() public constructor () { super(OCPPVersion.VERSION_201) - this.incomingRequestHandlers = new Map([ + this.incomingRequestHandlers = new Map([ [ 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( - chargingStation: ChargingStation, - messageId: string, - commandName: OCPP20IncomingRequestCommand, - commandPayload: ReqType - ): Promise { - 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 diff --git a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts index 134163a8..60611914 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts @@ -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 > + protected readonly bootNotificationRequestCommand = OCPP20RequestCommand.BOOT_NOTIFICATION + protected readonly csmsName = 'CSMS' + protected payloadValidatorFunctions: Map> - private readonly responseHandlers: Map + + protected readonly responseHandlers: Map public constructor () { super(OCPPVersion.VERSION_201) - this.responseHandlers = new Map([ + this.responseHandlers = new Map([ [ 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( + protected isRequestCommandSupported ( chargingStation: ChargingStation, - commandName: OCPP20RequestCommand, - payload: ResType, - requestPayload: ReqType - ): Promise { - 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 ( diff --git a/src/charging-station/ocpp/OCPPIncomingRequestService.ts b/src/charging-station/ocpp/OCPPIncomingRequestService.ts index c73a03ce..40449ef7 100644 --- a/src/charging-station/ocpp/OCPPIncomingRequestService.ts +++ b/src/charging-station/ocpp/OCPPIncomingRequestService.ts @@ -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 > + 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( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters + public async incomingRequestHandler( chargingStation: ChargingStation, messageId: string, commandName: IncomingRequestCommand, commandPayload: ReqType - ): Promise + ): Promise { + 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 diff --git a/src/charging-station/ocpp/OCPPResponseService.ts b/src/charging-station/ocpp/OCPPResponseService.ts index baab610f..5e6966c4 100644 --- a/src/charging-station/ocpp/OCPPResponseService.ts +++ b/src/charging-station/ocpp/OCPPResponseService.ts @@ -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> + protected abstract readonly responseHandlers: Map 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( + public async responseHandler( chargingStation: ChargingStation, commandName: RequestCommand, payload: ResType, requestPayload: ReqType - ): Promise + ): Promise { + 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