From 44285bdcda3072ed41815ac5d0714725ddb51784 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Mon, 10 Nov 2025 20:33:52 +0100 Subject: [PATCH] refactor: align more with specs MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 14 +- .../ocpp/1.6/OCPP16RequestService.ts | 72 +- .../ocpp/1.6/OCPP16ResponseService.ts | 14 +- .../ocpp/1.6/OCPP16ServiceUtils.ts | 46 +- .../ocpp/2.0/OCPP20IncomingRequestService.ts | 1138 +++++++++++++---- .../ocpp/2.0/OCPP20RequestService.ts | 59 +- .../ocpp/2.0/OCPP20ResponseService.ts | 11 +- .../ocpp/2.0/OCPP20ServiceUtils.ts | 46 +- src/charging-station/ocpp/OCPPServiceUtils.ts | 68 +- ...estService-RequestStartTransaction.test.ts | 2 +- 10 files changed, 1081 insertions(+), 389 deletions(-) diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index f924231e..6aeff5f6 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -160,9 +160,6 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { > public constructor () { - // if (new.target.name === moduleName) { - // throw new TypeError(`Cannot construct ${new.target.name} instances directly`) - // } super(OCPPVersion.VERSION_16) this.incomingRequestHandlers = new Map([ [ @@ -453,7 +450,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { commandPayload, undefined, 2 - )} while the charging station is in pending state on the central server`, + )} while the charging station is in pending state on the central system`, commandName, commandPayload ) @@ -506,7 +503,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { commandPayload, undefined, 2 - )} while the charging station is not registered on the central server`, + )} while the charging station is not registered on the central system`, commandName, commandPayload ) @@ -830,7 +827,6 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { end: addSeconds(currentDate, duration), start: currentDate, } - // FIXME: add and handle charging station charging profiles const chargingProfiles: OCPP16ChargingProfile[] = getConnectorChargingProfiles( chargingStation, connectorId @@ -1160,12 +1156,12 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ): GenericResponse { const { transactionId } = commandPayload if (chargingStation.getConnectorIdByTransactionId(transactionId) != null) { - logger.debug( + logger.info( `${chargingStation.logPrefix()} ${moduleName}.handleRequestRemoteStopTransaction: Remote stop transaction ACCEPTED for transactionId '${transactionId.toString()}'` ) return OCPP16Constants.OCPP_RESPONSE_ACCEPTED } - logger.debug( + logger.warn( `${chargingStation.logPrefix()} ${moduleName}.handleRequestRemoteStopTransaction: Remote stop transaction REJECTED for transactionId '${transactionId.toString()}'` ) return OCPP16Constants.OCPP_RESPONSE_REJECTED @@ -1269,7 +1265,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { .reset(`${type}Reset` as OCPP16StopTransactionReason) .catch(Constants.EMPTY_FUNCTION) logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: ${type} reset command received, simulating it. The station will be back online in ${formatDurationMilliSeconds( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: ${type} reset request received, simulating it. The station will be back online in ${formatDurationMilliSeconds( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion chargingStation.stationInfo!.resetTime! )}` diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index e2a0417e..0f2b766b 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -23,22 +23,66 @@ import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js' const moduleName = 'OCPP16RequestService' +/** + * OCPP 1.6 Request Service + * + * Handles outgoing OCPP 1.6 requests from the charging station to the central system. + * This service is responsible for: + * - Building and validating request payloads according to OCPP 1.6 specification + * - Managing request-response cycles with proper error handling + * - Ensuring message integrity through JSON schema validation + * - Providing type-safe interfaces for all supported OCPP 1.6 commands + * + * Key architectural components: + * - Payload validation using AJV schema validators + * - Standardized logging with charging station context + * - Comprehensive error handling with OCPP-specific error types + * - Integration with the broader OCPP service architecture + * OCPPRequestService - Base class providing common OCPP functionality + */ export class OCPP16RequestService extends OCPPRequestService { protected payloadValidatorFunctions: Map> + /** + * Constructs an OCPP 1.6 Request Service instance + * + * Initializes the service with OCPP 1.6-specific configurations including: + * - JSON schema validators for all supported OCPP 1.6 request commands + * - Response service integration for handling command responses + * - AJV validation setup with proper error handling + * @param ocppResponseService - The response service instance for handling responses + */ public constructor (ocppResponseService: OCPPResponseService) { - // if (new.target.name === moduleName) { - // throw new TypeError(`Cannot construct ${new.target.name} instances directly`) - // } super(OCPPVersion.VERSION_16, ocppResponseService) this.payloadValidatorFunctions = OCPP16ServiceUtils.createPayloadValidatorMap( OCPP16ServiceUtils.createRequestPayloadConfigs(), - OCPP16ServiceUtils.createRequestFactoryOptions(moduleName, 'constructor'), + OCPP16ServiceUtils.createRequestPayloadOptions(moduleName, 'constructor'), this.ajv ) this.buildRequestPayload = this.buildRequestPayload.bind(this) } + /** + * Handles OCPP 1.6 request processing with full validation and error handling + * + * This method serves as the main entry point for all outgoing OCPP 1.6 requests. + * It performs the following operations: + * - Validates that the requested command is supported by the charging station + * - Builds and validates the request payload according to OCPP 1.6 schemas + * - Sends the request to the central system with proper error handling + * - Processes responses with comprehensive logging and error recovery + * + * The method ensures type safety through generic type parameters while maintaining + * backward compatibility with the OCPP 1.6 specification. + * @template RequestType - The expected type of the request parameters + * @template ResponseType - The expected type of the response from the central system + * @param chargingStation - The charging station instance making the request + * @param commandName - The OCPP 1.6 command to execute (e.g., 'StartTransaction', 'StopTransaction') + * @param commandParams - Optional parameters specific to the command being executed + * @param params - Optional request parameters for controlling request behavior + * @returns Promise resolving to the typed response from the central system + * @throws {OCPPError} When the command is not supported or validation fails + */ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters public async requestHandler( chargingStation: ChargingStation, @@ -49,7 +93,6 @@ export class OCPP16RequestService extends OCPPRequestService { logger.debug( `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Processing '${commandName}' request` ) - // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc. if (OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) { try { logger.debug( @@ -99,6 +142,25 @@ export class OCPP16RequestService extends OCPPRequestService { throw new OCPPError(ErrorType.NOT_SUPPORTED, errorMsg, commandName, commandParams) } + /** + * Builds OCPP 1.6 request payloads with command-specific logic and validation + * + * This private method handles the construction of request payloads for various OCPP 1.6 commands. + * It implements command-specific business logic including: + * - Connector ID determination and validation + * - Energy meter readings for transaction-related commands + * - Transaction data aggregation (when enabled) + * - IdTag extraction from charging station context + * - Automatic timestamp generation for time-sensitive operations + * + * The method ensures that all required fields are populated according to OCPP 1.6 specification + * requirements while handling optional parameters and station-specific configurations. + * @template Request - The expected type of the constructed request payload + * @param chargingStation - The charging station instance containing context and configuration + * @param commandName - The OCPP 1.6 command being processed (e.g., 'StartTransaction', 'StopTransaction') + * @param commandParams - Optional parameters provided by the caller for payload construction + * @returns The fully constructed and validated request payload ready for transmission + */ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters private buildRequestPayload( chargingStation: ChargingStation, diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index 78bd2e2c..b19ced7f 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -22,7 +22,6 @@ import { type OCPP16BootNotificationResponse, OCPP16ChargePointStatus, OCPP16IncomingRequestCommand, - type OCPP16MeterValue, type OCPP16MeterValuesRequest, type OCPP16MeterValuesResponse, OCPP16RequestCommand, @@ -86,9 +85,6 @@ export class OCPP16ResponseService extends OCPPResponseService { private readonly responseHandlers: Map public constructor () { - // if (new.target.name === moduleName) { - // throw new TypeError(`Cannot construct ${new.target.name} instances directly`) - // } super(OCPPVersion.VERSION_16) this.responseHandlers = new Map([ [OCPP16RequestCommand.AUTHORIZE, this.handleResponseAuthorize.bind(this) as ResponseHandler], @@ -119,13 +115,13 @@ export class OCPP16ResponseService extends OCPPResponseService { ]) this.payloadValidatorFunctions = OCPP16ServiceUtils.createPayloadValidatorMap( OCPP16ServiceUtils.createResponsePayloadConfigs(), - OCPP16ServiceUtils.createResponseFactoryOptions(moduleName, 'constructor'), + OCPP16ServiceUtils.createResponsePayloadOptions(moduleName, 'constructor'), this.ajv ) this.incomingRequestResponsePayloadValidateFunctions = OCPP16ServiceUtils.createPayloadValidatorMap( OCPP16ServiceUtils.createIncomingRequestResponsePayloadConfigs(), - OCPP16ServiceUtils.createIncomingRequestResponseFactoryOptions(moduleName, 'constructor'), + OCPP16ServiceUtils.createIncomingRequestResponsePayloadOptions(moduleName, 'constructor'), this.ajvIncomingRequest ) this.validatePayload = this.validatePayload.bind(this) @@ -197,7 +193,7 @@ export class OCPP16ResponseService extends OCPPResponseService { payload, undefined, 2 - )} while the charging station is not registered on the central server`, + )} while the charging station is not registered on the central system`, commandName, payload ) @@ -304,7 +300,7 @@ export class OCPP16ResponseService extends OCPPResponseService { } const logMsg = `${chargingStation.logPrefix()} ${moduleName}.handleResponseBootNotification: Charging station in '${ payload.status - }' state on the central server` + }' state on the central system` payload.status === RegistrationStatusEnumType.REJECTED ? logger.warn(logMsg) : logger.info(logMsg) @@ -557,7 +553,7 @@ export class OCPP16ResponseService extends OCPPResponseService { chargingStation, transactionConnectorId, requestPayload.meterStop - ) as OCPP16MeterValue, + ), ], transactionId: requestPayload.transactionId, })) diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index a97598d8..1f9f1a22 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -467,23 +467,6 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { methodName ) - /** - * Factory options for OCPP 1.6 Incoming Request Response Service - * @param moduleName - Name of the OCPP module - * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 1.6 incoming request response validators - */ - public static createIncomingRequestResponseFactoryOptions = ( - moduleName: string, - methodName: string - ) => - OCPP16ServiceUtils.PayloadValidatorOptions( - OCPPVersion.VERSION_16, - 'assets/json-schemas/ocpp/1.6', - moduleName, - methodName - ) - /** * OCPP 1.6 Incoming Request Response Service validator configurations * @returns Array of validator configuration tuples @@ -563,12 +546,15 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ] /** - * Factory options for OCPP 1.6 Request Service + * Factory options for OCPP 1.6 Incoming Request Response Service * @param moduleName - Name of the OCPP module * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 1.6 validators + * @returns Factory options object for OCPP 1.6 incoming request response validators */ - public static createRequestFactoryOptions = (moduleName: string, methodName: string) => + public static createIncomingRequestResponsePayloadOptions = ( + moduleName: string, + methodName: string + ) => OCPP16ServiceUtils.PayloadValidatorOptions( OCPPVersion.VERSION_16, 'assets/json-schemas/ocpp/1.6', @@ -621,12 +607,12 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ] /** - * Factory options for OCPP 1.6 Response Service + * Factory options for OCPP 1.6 Request Service * @param moduleName - Name of the OCPP module * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 1.6 response validators + * @returns Factory options object for OCPP 1.6 validators */ - public static createResponseFactoryOptions = (moduleName: string, methodName: string) => + public static createRequestPayloadOptions = (moduleName: string, methodName: string) => OCPP16ServiceUtils.PayloadValidatorOptions( OCPPVersion.VERSION_16, 'assets/json-schemas/ocpp/1.6', @@ -684,6 +670,20 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ], ] + /** + * Factory options for OCPP 1.6 Response Service + * @param moduleName - Name of the OCPP module + * @param methodName - Name of the method/command + * @returns Factory options object for OCPP 1.6 response validators + */ + public static createResponsePayloadOptions = (moduleName: string, methodName: string) => + OCPP16ServiceUtils.PayloadValidatorOptions( + OCPPVersion.VERSION_16, + 'assets/json-schemas/ocpp/1.6', + moduleName, + methodName + ) + public static hasReservation = ( chargingStation: ChargingStation, connectorId: number, diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index 4c4051e8..4f7412b5 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -5,6 +5,7 @@ import type { ValidateFunction } from 'ajv' import type { ChargingStation } from '../../../charging-station/index.js' import type { OCPP20ChargingProfileType, + OCPP20ChargingScheduleType, OCPP20IdTokenType, } from '../../../types/ocpp/2.0/Transaction.js' @@ -50,6 +51,12 @@ import { SetVariableStatusEnumType, StopTransactionReason, } from '../../../types/index.js' +import { + OCPP20ChargingProfileKindEnumType, + OCPP20ChargingProfilePurposeEnumType, + OCPP20ChargingRateUnitEnumType, + OCPP20ReasonEnumType, +} from '../../../types/ocpp/2.0/Transaction.js' import { StandardParametersKey } from '../../../types/ocpp/Configuration.js' import { convertToIntOrNaN, @@ -59,7 +66,7 @@ import { validateUUID, } from '../../../utils/index.js' import { getConfigurationKey } from '../../ConfigurationKeyUtils.js' -import { resetConnectorStatus } from '../../Helpers.js' +import { getIdTagsFile, resetConnectorStatus } from '../../Helpers.js' import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js' import { restoreConnectorStatus, sendAndSetConnectorStatus } from '../OCPPServiceUtils.js' import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' @@ -108,7 +115,7 @@ const moduleName = 'OCPP20IncomingRequestService' * 4. Business logic executed with variable model integration * 5. Response payload validated and sent back to CSMS * @see {@link validatePayload} Request payload validation method - * @see {@link handleRequestRequestStartTransaction} Example OCPP 2.0+ request handler + * @see {@link handleRequestStartTransaction} Example OCPP 2.0+ request handler * @see {@link OCPP20VariableManager} Variable management integration */ @@ -123,9 +130,6 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { private readonly reportDataCache: Map public constructor () { - // if (new.target.name === moduleName) { - // throw new TypeError(`Cannot construct ${new.target.name} instances directly`) - // } super(OCPPVersion.VERSION_201) this.reportDataCache = new Map() this.incomingRequestHandlers = new Map([ @@ -143,11 +147,11 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { ], [ OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION, - this.handleRequestRequestStartTransaction.bind(this) as unknown as IncomingRequestHandler, + this.handleRequestStartTransaction.bind(this) as unknown as IncomingRequestHandler, ], [ OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION, - this.handleRequestRequestStopTransaction.bind(this) as unknown as IncomingRequestHandler, + this.handleRequestStopTransaction.bind(this) as unknown as IncomingRequestHandler, ], [ OCPP20IncomingRequestCommand.RESET, @@ -385,7 +389,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { commandPayload, undefined, 2 - )} while the charging station is in pending state on the central server`, + )} while the charging station is in pending state on the CSMS`, commandName, commandPayload ) @@ -437,7 +441,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { commandPayload, undefined, 2 - )} while the charging station is not registered on the central server`, + )} while the charging station is not registered on the CSMS`, commandName, commandPayload ) @@ -874,6 +878,223 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } + /** + * Handles OCPP 2.0 Reset request from central system with enhanced EVSE-specific support + * Initiates station or EVSE reset based on request parameters and transaction states + * @param chargingStation - The charging station instance processing the request + * @param commandPayload - Reset request payload with type and optional EVSE ID + * @returns Promise resolving to ResetResponse indicating operation status + */ + + private async handleRequestReset ( + chargingStation: ChargingStation, + commandPayload: OCPP20ResetRequest + ): Promise { + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Reset request received with type ${commandPayload.type}${commandPayload.evseId !== undefined ? ` for EVSE ${commandPayload.evseId.toString()}` : ''}` + ) + + const { evseId, type } = commandPayload + + if (evseId !== undefined && evseId > 0) { + // Check if the charging station supports EVSE-specific reset + if (!chargingStation.hasEvses) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Charging station does not support EVSE-specific reset` + ) + return { + status: ResetStatusEnumType.Rejected, + statusInfo: { + additionalInfo: 'Charging station does not support resetting individual EVSE', + reasonCode: ReasonCodeEnumType.UnsupportedRequest, + }, + } + } + + // Check if the EVSE exists + const evseExists = chargingStation.evses.has(evseId) + if (!evseExists) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: EVSE ${evseId.toString()} not found, rejecting reset request` + ) + return { + status: ResetStatusEnumType.Rejected, + statusInfo: { + additionalInfo: `EVSE ${evseId.toString()} does not exist on charging station`, + reasonCode: ReasonCodeEnumType.UnknownEvse, + }, + } + } + } + + // Check for active transactions + const hasActiveTransactions = chargingStation.getNumberOfRunningTransactions() > 0 + + // Check for EVSE-specific active transactions if evseId is provided + let hasEvseActiveTransactions = false + if (evseId !== undefined && evseId > 0) { + // Check if there are active transactions on the specific EVSE + const evse = chargingStation.evses.get(evseId) + if (evse) { + for (const [, connector] of evse.connectors) { + if (connector.transactionId !== undefined) { + hasEvseActiveTransactions = true + break + } + } + } + } + + try { + if (type === ResetEnumType.Immediate) { + if (evseId !== undefined) { + // EVSE-specific immediate reset + if (hasEvseActiveTransactions) { + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Immediate EVSE reset with active transaction, will terminate transaction and reset EVSE ${evseId.toString()}` + ) + + // Implement EVSE-specific transaction termination + await this.terminateEvseTransactions( + chargingStation, + evseId, + OCPP20ReasonEnumType.ImmediateReset + ) + this.scheduleEvseReset(chargingStation, evseId, true) + + return { + status: ResetStatusEnumType.Accepted, + } + } else { + // Reset EVSE immediately + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Immediate EVSE reset without active transactions for EVSE ${evseId.toString()}` + ) + + this.scheduleEvseReset(chargingStation, evseId, false) + + return { + status: ResetStatusEnumType.Accepted, + } + } + } else { + // Charging station immediate reset + if (hasActiveTransactions) { + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Immediate reset with active transactions, will terminate transactions and reset` + ) + + // Implement proper transaction termination with TransactionEventRequest + await this.terminateAllTransactions( + chargingStation, + OCPP20ReasonEnumType.ImmediateReset + ) + chargingStation.reset(StopTransactionReason.REMOTE).catch((error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Error during immediate reset:`, + error + ) + }) + + return { + status: ResetStatusEnumType.Accepted, + } + } else { + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Immediate reset without active transactions` + ) + + // Send StatusNotification(Unavailable) for all connectors + this.sendAllConnectorsStatusNotifications( + chargingStation, + OCPP20ConnectorStatusEnumType.Unavailable + ) + chargingStation.reset(StopTransactionReason.REMOTE).catch((error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Error during immediate reset:`, + error + ) + }) + + return { + status: ResetStatusEnumType.Accepted, + } + } + } + } else { + // OnIdle reset + if (evseId !== undefined) { + // EVSE-specific OnIdle reset + if (hasEvseActiveTransactions) { + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: OnIdle EVSE reset scheduled for EVSE ${evseId.toString()}, waiting for transaction completion` + ) + + // Monitor EVSE for transaction completion and schedule reset when idle + this.scheduleEvseResetOnIdle(chargingStation, evseId) + + return { + status: ResetStatusEnumType.Scheduled, + } + } else { + // No active transactions on EVSE, reset immediately + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: OnIdle EVSE reset without active transactions for EVSE ${evseId.toString()}` + ) + + this.scheduleEvseReset(chargingStation, evseId, false) + + return { + status: ResetStatusEnumType.Accepted, + } + } + } else { + // Charging station OnIdle reset + if (hasActiveTransactions) { + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: OnIdle reset scheduled, waiting for transaction completion` + ) + + this.scheduleResetOnIdle(chargingStation) + + return { + status: ResetStatusEnumType.Scheduled, + } + } else { + // No active transactions, reset immediately + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: OnIdle reset without active transactions, resetting immediately` + ) + + chargingStation.reset(StopTransactionReason.REMOTE).catch((error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Error during OnIdle reset:`, + error + ) + }) + + return { + status: ResetStatusEnumType.Accepted, + } + } + } + } + } catch (error) { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Error handling reset request:`, + error + ) + + return { + status: ResetStatusEnumType.Rejected, + statusInfo: { + additionalInfo: 'Internal error occurred while processing reset request', + reasonCode: ReasonCodeEnumType.InternalError, + }, + } + } + } + /** * Handles OCPP 2.0 RequestStartTransaction request from central system * Initiates charging transaction on specified EVSE with enhanced authorization @@ -881,20 +1102,20 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { * @param commandPayload - RequestStartTransaction request payload with EVSE, ID token and profiles * @returns Promise resolving to RequestStartTransactionResponse with status and transaction details */ - private async handleRequestRequestStartTransaction ( + private async handleRequestStartTransaction ( chargingStation: ChargingStation, commandPayload: OCPP20RequestStartTransactionRequest ): Promise { const { chargingProfile, evseId, groupIdToken, idToken, remoteStartId } = commandPayload logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Remote start transaction request received on EVSE ${evseId?.toString() ?? 'undefined'} with idToken ${idToken.idToken} and remoteStartId ${remoteStartId.toString()}` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Remote start transaction request received on EVSE ${evseId?.toString() ?? 'undefined'} with idToken ${idToken.idToken} and remoteStartId ${remoteStartId.toString()}` ) // Validate that EVSE ID is provided if (evseId == null) { const errorMsg = 'EVSE ID is required for RequestStartTransaction' logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: ${errorMsg}` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: ${errorMsg}` ) throw new OCPPError( ErrorType.PROPERTY_CONSTRAINT_VIOLATION, @@ -909,7 +1130,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { if (evse == null) { const errorMsg = `EVSE ${evseId.toString()} does not exist on charging station` logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: ${errorMsg}` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: ${errorMsg}` ) throw new OCPPError( ErrorType.PROPERTY_CONSTRAINT_VIOLATION, @@ -925,7 +1146,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { if (connectorStatus == null || connectorId == null) { const errorMsg = `Connector ${connectorId?.toString() ?? 'undefined'} status is undefined` logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: ${errorMsg}` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: ${errorMsg}` ) throw new OCPPError( ErrorType.INTERNAL_ERROR, @@ -938,7 +1159,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { // Check if connector is available for a new transaction if (connectorStatus.transactionStarted === true) { logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Connector ${connectorId.toString()} already has an active transaction` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Connector ${connectorId.toString()} already has an active transaction` ) return { status: RequestStartStopStatusEnumType.Rejected, @@ -949,10 +1170,10 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { // Authorize idToken let isAuthorized = false try { - isAuthorized = await this.isIdTokenAuthorized(chargingStation, idToken) + isAuthorized = this.isIdTokenAuthorized(chargingStation, idToken) } catch (error) { logger.error( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Authorization error for ${idToken.idToken}:`, + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Authorization error for ${idToken.idToken}:`, error ) return { @@ -963,7 +1184,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { if (!isAuthorized) { logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: IdToken ${idToken.idToken} is not authorized` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: IdToken ${idToken.idToken} is not authorized` ) return { status: RequestStartStopStatusEnumType.Rejected, @@ -975,10 +1196,10 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { if (groupIdToken != null) { let isGroupAuthorized = false try { - isGroupAuthorized = await this.isIdTokenAuthorized(chargingStation, groupIdToken) + isGroupAuthorized = this.isIdTokenAuthorized(chargingStation, groupIdToken) } catch (error) { logger.error( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Group authorization error for ${groupIdToken.idToken}:`, + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Group authorization error for ${groupIdToken.idToken}:`, error ) return { @@ -989,7 +1210,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { if (!isGroupAuthorized) { logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: GroupIdToken ${groupIdToken.idToken} is not authorized` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: GroupIdToken ${groupIdToken.idToken} is not authorized` ) return { status: RequestStartStopStatusEnumType.Rejected, @@ -1005,7 +1226,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { isValidProfile = this.validateChargingProfile(chargingStation, chargingProfile, evseId) } catch (error) { logger.error( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Charging profile validation error:`, + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Charging profile validation error:`, error ) return { @@ -1016,7 +1237,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { if (!isValidProfile) { logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Invalid charging profile` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Invalid charging profile` ) return { status: RequestStartStopStatusEnumType.Rejected, @@ -1030,7 +1251,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { try { // Set connector transaction state logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Setting transaction state for connector ${connectorId.toString()}, transaction ID: ${transactionId}` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Setting transaction state for connector ${connectorId.toString()}, transaction ID: ${transactionId}` ) connectorStatus.transactionStarted = true connectorStatus.transactionId = transactionId @@ -1039,12 +1260,12 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { connectorStatus.transactionEnergyActiveImportRegisterValue = 0 connectorStatus.remoteStartId = remoteStartId logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Transaction state set successfully for connector ${connectorId.toString()}` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Transaction state set successfully for connector ${connectorId.toString()}` ) // Update connector status to Occupied logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Updating connector ${connectorId.toString()} status to Occupied` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Updating connector ${connectorId.toString()} status to Occupied` ) await sendAndSetConnectorStatus( chargingStation, @@ -1057,14 +1278,13 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { if (chargingProfile != null) { connectorStatus.chargingProfiles ??= [] connectorStatus.chargingProfiles.push(chargingProfile) - // TODO: Implement charging profile storage logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Charging profile stored for transaction ${transactionId} (TODO: implement profile storage)` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Charging profile stored for transaction ${transactionId}` ) } logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Remote start transaction ACCEPTED on #${connectorId.toString()} for idToken '${idToken.idToken}'` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Remote start transaction ACCEPTED on #${connectorId.toString()} for idToken '${idToken.idToken}'` ) return { @@ -1074,7 +1294,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } catch (error) { await this.resetConnectorOnStartTransactionError(chargingStation, connectorId, evseId) logger.error( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Error starting transaction:`, + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Error starting transaction:`, error ) return { @@ -1084,19 +1304,19 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } - private async handleRequestRequestStopTransaction ( + private async handleRequestStopTransaction ( chargingStation: ChargingStation, commandPayload: OCPP20RequestStopTransactionRequest ): Promise { const { transactionId } = commandPayload logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStopTransaction: Remote stop transaction request received for transaction ID ${transactionId}` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStopTransaction: Remote stop transaction request received for transaction ID ${transactionId}` ) if (!validateUUID(transactionId)) { logger.warn( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStopTransaction: Invalid transaction ID format (expected UUID): ${transactionId}` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStopTransaction: Invalid transaction ID format (expected UUID): ${transactionId}` ) return { status: RequestStartStopStatusEnumType.Rejected, @@ -1106,7 +1326,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { const evseId = chargingStation.getEvseIdByTransactionId(transactionId) if (evseId == null) { logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStopTransaction: Transaction ID ${transactionId} does not exist on any EVSE` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStopTransaction: Transaction ID ${transactionId} does not exist on any EVSE` ) return { status: RequestStartStopStatusEnumType.Rejected, @@ -1116,7 +1336,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { const connectorId = chargingStation.getConnectorIdByTransactionId(transactionId) if (connectorId == null) { logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStopTransaction: Transaction ID ${transactionId} does not exist on any connector` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStopTransaction: Transaction ID ${transactionId} does not exist on any connector` ) return { status: RequestStartStopStatusEnumType.Rejected, @@ -1132,7 +1352,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { if (stopResponse.status === GenericStatus.Accepted) { logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStopTransaction: Remote stop transaction ACCEPTED for transactionId '${transactionId}'` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStopTransaction: Remote stop transaction ACCEPTED for transactionId '${transactionId}'` ) return { status: RequestStartStopStatusEnumType.Accepted, @@ -1140,14 +1360,14 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStopTransaction: Remote stop transaction REJECTED for transactionId '${transactionId}'` + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStopTransaction: Remote stop transaction REJECTED for transactionId '${transactionId}'` ) return { status: RequestStartStopStatusEnumType.Rejected, } } catch (error) { logger.error( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStopTransaction: Error occurred during remote stop transaction for transaction ID ${transactionId} on connector ${connectorId.toString()}:`, + `${chargingStation.logPrefix()} ${moduleName}.handleRequestStopTransaction: Error occurred during remote stop transaction for transaction ID ${transactionId} on connector ${connectorId.toString()}:`, error ) return { @@ -1156,223 +1376,104 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } - private handleRequestReset ( + // Helper methods for RequestStartTransaction + private isIdTokenAuthorized ( chargingStation: ChargingStation, - commandPayload: OCPP20ResetRequest - ): OCPP20ResetResponse { + idToken: OCPP20IdTokenType + ): boolean { + /** + * OCPP 2.0 Authorization Logic Implementation + * + * OCPP 2.0 handles authorization differently from 1.6: + * 1. Check if authorization is required (LocalAuthorizeOffline, AuthorizeRemoteStart variables) + * 2. Local authorization list validation if enabled + * 3. For OCPP 2.0, there's no explicit AuthorizeRequest - authorization is validated + * through configuration variables and local auth lists + * 4. Remote validation through TransactionEvent if needed + */ + logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Reset request received with type ${commandPayload.type}${commandPayload.evseId !== undefined ? ` for EVSE ${commandPayload.evseId.toString()}` : ''}` + `${chargingStation.logPrefix()} ${moduleName}.isIdTokenAuthorized: Validating idToken ${idToken.idToken} of type ${idToken.type}` ) - const { evseId, type } = commandPayload + try { + // Check if local authorization is disabled and remote authorization is also disabled + const localAuthListEnabled = chargingStation.getLocalAuthListEnabled() + const remoteAuthorizationEnabled = chargingStation.stationInfo?.remoteAuthorization ?? true - if (evseId !== undefined && evseId > 0) { - // Check if the charging station supports EVSE-specific reset - if (!chargingStation.hasEvses) { + if (!localAuthListEnabled && !remoteAuthorizationEnabled) { logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Charging station does not support EVSE-specific reset` + `${chargingStation.logPrefix()} ${moduleName}.isIdTokenAuthorized: Both local and remote authorization are disabled. Allowing access but this may indicate misconfiguration.` ) - return { - status: ResetStatusEnumType.Rejected, - statusInfo: { - additionalInfo: 'Charging station does not support resetting individual EVSE', - reasonCode: ReasonCodeEnumType.UnsupportedRequest, - }, - } + return true } - // Check if the EVSE exists - const evseExists = chargingStation.evses.has(evseId) - if (!evseExists) { - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: EVSE ${evseId.toString()} not found, rejecting reset request` - ) - return { - status: ResetStatusEnumType.Rejected, - statusInfo: { - additionalInfo: `EVSE ${evseId.toString()} does not exist on charging station`, - reasonCode: ReasonCodeEnumType.UnknownEvse, - }, + // 1. Check local authorization list first (if enabled) + if (localAuthListEnabled) { + const isLocalAuthorized = this.isIdTokenLocalAuthorized(chargingStation, idToken.idToken) + if (isLocalAuthorized) { + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.isIdTokenAuthorized: IdToken ${idToken.idToken} authorized via local auth list` + ) + return true } + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.isIdTokenAuthorized: IdToken ${idToken.idToken} not found in local auth list` + ) } - } - - // Check for active transactions - const hasActiveTransactions = chargingStation.getNumberOfRunningTransactions() > 0 - // Check for EVSE-specific active transactions if evseId is provided - let hasEvseActiveTransactions = false - if (evseId !== undefined && evseId > 0) { - // Check if there are active transactions on the specific EVSE - const evse = chargingStation.evses.get(evseId) - if (evse) { - for (const [, connector] of evse.connectors) { - if (connector.transactionId !== undefined) { - hasEvseActiveTransactions = true - break - } - } + // 2. For OCPP 2.0, if we can't authorize locally and remote auth is enabled, + // we should validate through TransactionEvent mechanism or return false + // In OCPP 2.0, there's no explicit remote authorize - it's handled during transaction events + if (remoteAuthorizationEnabled) { + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.isIdTokenAuthorized: Remote authorization enabled but no explicit remote auth mechanism in OCPP 2.0 - deferring to transaction event validation` + ) + // In OCPP 2.0, remote authorization happens during TransactionEvent processing + // For now, we'll allow the transaction to proceed and let the CSMS validate during TransactionEvent + return true } - } - try { - if (type === ResetEnumType.Immediate) { - if (evseId !== undefined) { - // EVSE-specific immediate reset - if (hasEvseActiveTransactions) { - logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Immediate EVSE reset with active transaction, will terminate transaction and reset EVSE ${evseId.toString()}` - ) - - // TODO: Implement EVSE-specific transaction termination - // For now, accept and schedule the reset - this.scheduleEvseReset(chargingStation, evseId, true) - - return { - status: ResetStatusEnumType.Accepted, - } - } else { - // Reset EVSE immediately - logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Immediate EVSE reset without active transactions for EVSE ${evseId.toString()}` - ) - - this.scheduleEvseReset(chargingStation, evseId, false) - - return { - status: ResetStatusEnumType.Accepted, - } - } - } else { - // Charging station immediate reset - if (hasActiveTransactions) { - logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Immediate reset with active transactions, will terminate transactions and reset` - ) - - // TODO: Implement proper transaction termination with TransactionEventRequest - // For now, reset immediately and let the reset handle transaction cleanup - chargingStation.reset(StopTransactionReason.REMOTE).catch((error: unknown) => { - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Error during immediate reset:`, - error - ) - }) - - return { - status: ResetStatusEnumType.Accepted, - } - } else { - logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Immediate reset without active transactions` - ) - - // TODO: Send StatusNotification(Unavailable) for all connectors - chargingStation.reset(StopTransactionReason.REMOTE).catch((error: unknown) => { - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Error during immediate reset:`, - error - ) - }) - - return { - status: ResetStatusEnumType.Accepted, - } - } - } - } else { - // OnIdle reset - if (evseId !== undefined) { - // EVSE-specific OnIdle reset - if (hasEvseActiveTransactions) { - logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: OnIdle EVSE reset scheduled for EVSE ${evseId.toString()}, waiting for transaction completion` - ) - - // TODO: Implement proper monitoring of EVSE transaction completion - this.scheduleEvseResetOnIdle(chargingStation, evseId) - - return { - status: ResetStatusEnumType.Scheduled, - } - } else { - // No active transactions on EVSE, reset immediately - logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: OnIdle EVSE reset without active transactions for EVSE ${evseId.toString()}` - ) - - this.scheduleEvseReset(chargingStation, evseId, false) - - return { - status: ResetStatusEnumType.Accepted, - } - } - } else { - // Charging station OnIdle reset - if (hasActiveTransactions) { - logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: OnIdle reset scheduled, waiting for transaction completion` - ) - - this.scheduleResetOnIdle(chargingStation) - - return { - status: ResetStatusEnumType.Scheduled, - } - } else { - // No active transactions, reset immediately - logger.info( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: OnIdle reset without active transactions, resetting immediately` - ) - - chargingStation.reset(StopTransactionReason.REMOTE).catch((error: unknown) => { - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Error during OnIdle reset:`, - error - ) - }) - - return { - status: ResetStatusEnumType.Accepted, - } - } - } - } + // 3. If we reach here, authorization failed + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.isIdTokenAuthorized: IdToken ${idToken.idToken} authorization failed - not found in local list and remote auth not configured` + ) + return false } catch (error) { logger.error( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: Error handling reset request:`, + `${chargingStation.logPrefix()} ${moduleName}.isIdTokenAuthorized: Error during authorization validation for ${idToken.idToken}:`, error ) - - return { - status: ResetStatusEnumType.Rejected, - statusInfo: { - additionalInfo: 'Internal error occurred while processing reset request', - reasonCode: ReasonCodeEnumType.InternalError, - }, - } + // Fail securely - deny access on authorization errors + return false } } - // Helper methods for RequestStartTransaction - private async isIdTokenAuthorized ( + /** + * Check if idToken is authorized in local authorization list + * @param chargingStation - The charging station instance + * @param idTokenString - The ID token string to validate + * @returns true if authorized locally, false otherwise + */ + private isIdTokenLocalAuthorized ( chargingStation: ChargingStation, - idToken: OCPP20IdTokenType - ): Promise { - // TODO: Implement proper authorization logic - // This should check: - // 1. Local authorization list if enabled - // 2. Remote authorization via AuthorizeRequest if needed - // 3. Cache for known tokens - // 4. Return false if authorization fails - - logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.isIdTokenAuthorized: Validating idToken ${idToken.idToken} of type ${idToken.type}` - ) - - // For now, return true to allow development/testing - // TODO: Implement actual async authorization logic - return await Promise.resolve(true) + idTokenString: string + ): boolean { + try { + return ( + chargingStation.hasIdTags() && + chargingStation.idTagsCache + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + .getIdTags(getIdTagsFile(chargingStation.stationInfo!)!) + ?.includes(idTokenString) === true + ) + } catch (error) { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.isIdTokenLocalAuthorized: Error checking local authorization for ${idTokenString}:`, + error + ) + return false + } } /** @@ -1392,39 +1493,61 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { await restoreConnectorStatus(chargingStation, connectorId, connectorStatus) } + /** + * Schedules EVSE reset with optional transaction termination + * @param chargingStation - The charging station instance + * @param evseId - The EVSE identifier to reset + * @param hasActiveTransactions - Whether there are active transactions to handle + */ private scheduleEvseReset ( chargingStation: ChargingStation, evseId: number, - terminateTransactions: boolean + hasActiveTransactions: boolean ): void { - logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.scheduleEvseReset: Scheduling EVSE ${evseId.toString()} reset${terminateTransactions ? ' with transaction termination' : ''}` + // Send status notification for unavailable EVSE + this.sendEvseStatusNotifications( + chargingStation, + evseId, + OCPP20ConnectorStatusEnumType.Unavailable ) - setTimeout( - () => { - // TODO: Implement actual EVSE-specific reset logic - // This should: - // 1. Send StatusNotification(Unavailable) for EVSE connectors (B11.FR.08) - // 2. Terminate active transactions if needed - // 3. Reset EVSE state - // 4. Restore EVSE to appropriate state after reset - - logger.info( - `${chargingStation.logPrefix()} ${moduleName}.scheduleEvseReset: EVSE ${evseId.toString()} reset executed` - ) - }, - terminateTransactions ? 1000 : 100 - ) // Small delay for immediate execution + // Schedule the actual EVSE reset + setImmediate(() => { + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.scheduleEvseReset: Executing EVSE ${evseId.toString()} reset${hasActiveTransactions ? ' after transaction termination' : ''}` + ) + // 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) + if (evse) { + for (const [connectorId] of evse.connectors) { + const connectorStatus = chargingStation.getConnectorStatus(connectorId) + restoreConnectorStatus(chargingStation, connectorId, connectorStatus).catch( + (error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.scheduleEvseReset: Error restoring connector ${connectorId.toString()} status:`, + error + ) + } + ) + } + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.scheduleEvseReset: EVSE ${evseId.toString()} reset completed` + ) + } + }, 1000) + }) } + /** + * Schedules EVSE reset on idle (when no active transactions) + * @param chargingStation - The charging station instance + * @param evseId - The EVSE identifier to reset + */ private scheduleEvseResetOnIdle (chargingStation: ChargingStation, evseId: number): void { - logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.scheduleEvseResetOnIdle: Monitoring EVSE ${evseId.toString()} for transaction completion` - ) - - // TODO: Implement proper monitoring logic - const checkInterval = setInterval(() => { + // Monitor for transaction completion and reset when idle + const monitorInterval = setInterval(() => { const evse = chargingStation.evses.get(evseId) if (evse) { let hasActiveTransactions = false @@ -1436,25 +1559,32 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } if (!hasActiveTransactions) { - clearInterval(checkInterval) + clearInterval(monitorInterval) + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.scheduleEvseResetOnIdle: EVSE ${evseId.toString()} is now idle, executing reset` + ) this.scheduleEvseReset(chargingStation, evseId, false) } + } else { + clearInterval(monitorInterval) } }, 5000) // Check every 5 seconds } + /** + * Schedules charging station reset on idle (when no active transactions) + * @param chargingStation - The charging station instance + */ private scheduleResetOnIdle (chargingStation: ChargingStation): void { - logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.scheduleResetOnIdle: Monitoring charging station for transaction completion` - ) - - // TODO: Implement proper monitoring logic - const checkInterval = setInterval(() => { + // Monitor for transaction completion and reset when idle + const monitorInterval = setInterval(() => { const hasActiveTransactions = chargingStation.getNumberOfRunningTransactions() > 0 if (!hasActiveTransactions) { - clearInterval(checkInterval) - // TODO: Use OCPP2 stop transaction reason when implemented + clearInterval(monitorInterval) + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.scheduleResetOnIdle: Charging station is now idle, executing reset` + ) chargingStation.reset(StopTransactionReason.REMOTE).catch((error: unknown) => { logger.error( `${chargingStation.logPrefix()} ${moduleName}.scheduleResetOnIdle: Error during scheduled reset:`, @@ -1465,6 +1595,59 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { }, 5000) // Check every 5 seconds } + /** + * Sends status notifications for all connectors on the charging station + * @param chargingStation - The charging station instance + * @param status - The connector status to send + */ + private sendAllConnectorsStatusNotifications ( + chargingStation: ChargingStation, + status: OCPP20ConnectorStatusEnumType + ): void { + for (const [, evse] of chargingStation.evses) { + for (const [connectorId] of evse.connectors) { + sendAndSetConnectorStatus( + chargingStation, + connectorId, + status as ConnectorStatusEnum + ).catch((error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.sendAllConnectorsStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`, + error + ) + }) + } + } + } + + /** + * Sends status notifications for all connectors on the specified EVSE + * @param chargingStation - The charging station instance + * @param evseId - The EVSE identifier + * @param status - The connector status to send + */ + private sendEvseStatusNotifications ( + chargingStation: ChargingStation, + evseId: number, + status: OCPP20ConnectorStatusEnumType + ): void { + const evse = chargingStation.evses.get(evseId) + if (evse) { + for (const [connectorId] of evse.connectors) { + sendAndSetConnectorStatus( + chargingStation, + connectorId, + status as ConnectorStatusEnum + ).catch((error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.sendEvseStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`, + error + ) + }) + } + } + } + private async sendNotifyReportRequest ( chargingStation: ChargingStation, request: OCPP20GetBaseReportRequest, @@ -1520,23 +1703,430 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { this.reportDataCache.delete(requestId) } + /** + * Terminates all active transactions on the charging station using OCPP 2.0 TransactionEventRequest + * @param chargingStation - The charging station instance + * @param reason - The reason for transaction termination + */ + private async terminateAllTransactions ( + chargingStation: ChargingStation, + reason: OCPP20ReasonEnumType + ): Promise { + const terminationPromises: Promise[] = [] + + for (const [evseId, evse] of chargingStation.evses) { + for (const [connectorId, connector] of evse.connectors) { + if (connector.transactionId !== undefined) { + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.terminateAllTransactions: Terminating transaction ${connector.transactionId.toString()} on connector ${connectorId.toString()}` + ) + // Use the proper OCPP 2.0 transaction termination method + terminationPromises.push( + OCPP20ServiceUtils.requestStopTransaction(chargingStation, connectorId, evseId).catch( + (error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.terminateAllTransactions: Error terminating transaction on connector ${connectorId.toString()}:`, + error + ) + } + ) + ) + } + } + } + + if (terminationPromises.length > 0) { + await Promise.all(terminationPromises) + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.terminateAllTransactions: All transactions terminated on charging station` + ) + } + } + + /** + * Terminates all active transactions on the specified EVSE using OCPP 2.0 TransactionEventRequest + * @param chargingStation - The charging station instance + * @param evseId - The EVSE identifier to terminate transactions on + * @param reason - The reason for transaction termination + */ + private async terminateEvseTransactions ( + chargingStation: ChargingStation, + evseId: number, + reason: OCPP20ReasonEnumType + ): Promise { + const evse = chargingStation.evses.get(evseId) + if (!evse) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.terminateEvseTransactions: EVSE ${evseId.toString()} not found` + ) + return + } + + const terminationPromises: Promise[] = [] + for (const [connectorId, connector] of evse.connectors) { + if (connector.transactionId !== undefined) { + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.terminateEvseTransactions: Terminating transaction ${connector.transactionId.toString()} on connector ${connectorId.toString()}` + ) + // Use the proper OCPP 2.0 transaction termination method + terminationPromises.push( + OCPP20ServiceUtils.requestStopTransaction(chargingStation, connectorId, evseId).catch( + (error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.terminateEvseTransactions: Error terminating transaction on connector ${connectorId.toString()}:`, + error + ) + } + ) + ) + } + } + + if (terminationPromises.length > 0) { + await Promise.all(terminationPromises) + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.terminateEvseTransactions: All transactions terminated on EVSE ${evseId.toString()}` + ) + } + } + private validateChargingProfile ( chargingStation: ChargingStation, chargingProfile: OCPP20ChargingProfileType, evseId: number ): boolean { - // TODO: Implement proper charging profile validation - // This should validate: - // 1. Profile structure and required fields - // 2. Schedule periods and limits - // 3. Compatibility with EVSE capabilities - // 4. Time constraints and validity - logger.debug( `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: Validating charging profile ${chargingProfile.id.toString()} for EVSE ${evseId.toString()}` ) - // For now, return true to allow development/testing + // Basic validation - check required fields + if (!chargingProfile.id || !chargingProfile.stackLevel) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: Invalid charging profile - missing required fields` + ) + return false + } + + // Validate stack level range (OCPP 2.0 spec: 0-9) + if (chargingProfile.stackLevel < 0 || chargingProfile.stackLevel > 9) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: Invalid stack level ${chargingProfile.stackLevel.toString()}, must be 0-9` + ) + return false + } + + // Validate charging profile ID is positive + if (chargingProfile.id <= 0) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: Invalid charging profile ID ${chargingProfile.id.toString()}, must be positive` + ) + return false + } + + // Validate EVSE compatibility + if (!chargingStation.hasEvses && evseId > 0) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: EVSE ${evseId.toString()} not supported by this charging station` + ) + return false + } + + if (chargingStation.hasEvses && evseId > chargingStation.getNumberOfEvses()) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: EVSE ${evseId.toString()} exceeds available EVSEs (${chargingStation.getNumberOfEvses().toString()})` + ) + return false + } + + // Validate charging schedules array is not empty + if (chargingProfile.chargingSchedule.length === 0) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: Charging profile must contain at least one charging schedule` + ) + return false + } + + // Time constraints validation + const now = new Date() + if (chargingProfile.validFrom && chargingProfile.validTo) { + if (chargingProfile.validFrom >= chargingProfile.validTo) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: validFrom must be before validTo` + ) + return false + } + } + + if (chargingProfile.validTo && chargingProfile.validTo <= now) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: Charging profile already expired` + ) + return false + } + + // Validate recurrency kind compatibility with profile kind + if ( + chargingProfile.recurrencyKind && + chargingProfile.chargingProfileKind !== OCPP20ChargingProfileKindEnumType.Recurring + ) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: recurrencyKind only valid for Recurring profile kind` + ) + return false + } + + if ( + chargingProfile.chargingProfileKind === OCPP20ChargingProfileKindEnumType.Recurring && + !chargingProfile.recurrencyKind + ) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: Recurring profile kind requires recurrencyKind` + ) + return false + } + + // Validate each charging schedule + for (const [scheduleIndex, schedule] of chargingProfile.chargingSchedule.entries()) { + if ( + !this.validateChargingSchedule( + chargingStation, + schedule, + scheduleIndex, + chargingProfile, + evseId + ) + ) { + return false + } + } + + // Profile purpose specific validations + if (!this.validateChargingProfilePurpose(chargingStation, chargingProfile, evseId)) { + return false + } + + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: Charging profile ${chargingProfile.id.toString()} validation passed` + ) + return true + } + + /** + * Validates charging profile purpose-specific business rules + * @param chargingStation - The charging station instance + * @param chargingProfile - The charging profile to validate + * @param evseId - EVSE identifier + * @returns True if purpose validation passes, false otherwise + */ + private validateChargingProfilePurpose ( + chargingStation: ChargingStation, + chargingProfile: OCPP20ChargingProfileType, + evseId: number + ): boolean { + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfilePurpose: Validating purpose-specific rules for profile ${chargingProfile.id.toString()} with purpose ${chargingProfile.chargingProfilePurpose}` + ) + + switch (chargingProfile.chargingProfilePurpose) { + case OCPP20ChargingProfilePurposeEnumType.ChargingStationExternalConstraints: + // ChargingStationExternalConstraints must apply to EVSE 0 (entire station) + if (evseId !== 0) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfilePurpose: ChargingStationExternalConstraints must apply to EVSE 0, got EVSE ${evseId.toString()}` + ) + return false + } + break + + case OCPP20ChargingProfilePurposeEnumType.ChargingStationMaxProfile: + // ChargingStationMaxProfile must apply to EVSE 0 (entire station) + if (evseId !== 0) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfilePurpose: ChargingStationMaxProfile must apply to EVSE 0, got EVSE ${evseId.toString()}` + ) + return false + } + break + + case OCPP20ChargingProfilePurposeEnumType.TxDefaultProfile: + // TxDefaultProfile can apply to EVSE 0 or specific EVSE + // No additional constraints beyond general EVSE validation + break + + case OCPP20ChargingProfilePurposeEnumType.TxProfile: + // TxProfile must apply to a specific EVSE (not 0) + if (evseId === 0) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfilePurpose: TxProfile cannot apply to EVSE 0, must target specific EVSE` + ) + return false + } + + // TxProfile should have a transactionId when used with active transaction + if (!chargingProfile.transactionId) { + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfilePurpose: TxProfile without transactionId - may be for future use` + ) + } + break + + default: + logger.warn( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfilePurpose: Unknown charging profile purpose: ${chargingProfile.chargingProfilePurpose}` + ) + return false + } + + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfilePurpose: Purpose validation passed for profile ${chargingProfile.id.toString()}` + ) + return true + } + + /** + * Validates an individual charging schedule within a charging profile + * @param chargingStation - The charging station instance + * @param schedule - The charging schedule to validate + * @param scheduleIndex - Index of the schedule in the profile's schedule array + * @param chargingProfile - The parent charging profile + * @param evseId - EVSE identifier + * @returns True if schedule is valid, false otherwise + */ + private validateChargingSchedule ( + chargingStation: ChargingStation, + schedule: OCPP20ChargingScheduleType, + scheduleIndex: number, + chargingProfile: OCPP20ChargingProfileType, + evseId: number + ): boolean { + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Validating schedule ${scheduleIndex.toString()} (ID: ${schedule.id.toString()}) in profile ${chargingProfile.id.toString()}` + ) + + // Validate schedule ID is positive + if (schedule.id <= 0) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Invalid schedule ID ${schedule.id.toString()}, must be positive` + ) + return false + } + + // Validate charging schedule periods array is not empty + if (schedule.chargingSchedulePeriod.length === 0) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Schedule must contain at least one charging schedule period` + ) + return false + } + + // Validate charging rate unit is valid (type system ensures it exists) + if (!Object.values(OCPP20ChargingRateUnitEnumType).includes(schedule.chargingRateUnit)) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Invalid charging rate unit: ${schedule.chargingRateUnit}` + ) + return false + } + + // Validate duration constraints + if (schedule.duration !== undefined && schedule.duration <= 0) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Schedule duration must be positive if specified` + ) + return false + } + + // Validate minimum charging rate if specified + if (schedule.minChargingRate !== undefined && schedule.minChargingRate < 0) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Minimum charging rate cannot be negative` + ) + return false + } + + // Validate start schedule time constraints + if ( + schedule.startSchedule && + chargingProfile.validFrom && + schedule.startSchedule < chargingProfile.validFrom + ) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Schedule start time cannot be before profile validFrom` + ) + return false + } + + if ( + schedule.startSchedule && + chargingProfile.validTo && + schedule.startSchedule >= chargingProfile.validTo + ) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Schedule start time must be before profile validTo` + ) + return false + } + + // Validate charging schedule periods + let previousStartPeriod = -1 + for (const [periodIndex, period] of schedule.chargingSchedulePeriod.entries()) { + // Validate start period is non-negative and increasing + if (period.startPeriod < 0) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Period ${periodIndex.toString()} start time cannot be negative` + ) + return false + } + + if (period.startPeriod <= previousStartPeriod) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Period ${periodIndex.toString()} start time must be greater than previous period` + ) + return false + } + previousStartPeriod = period.startPeriod + + // Validate charging limit is positive + if (period.limit <= 0) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Period ${periodIndex.toString()} charging limit must be positive` + ) + return false + } + + // Validate minimum charging rate constraint + if (schedule.minChargingRate !== undefined && period.limit < schedule.minChargingRate) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Period ${periodIndex.toString()} limit cannot be below minimum charging rate` + ) + return false + } + + // Validate number of phases constraints + if (period.numberPhases !== undefined) { + if (period.numberPhases < 1 || period.numberPhases > 3) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Period ${periodIndex.toString()} number of phases must be 1-3` + ) + return false + } + + // If phaseToUse is specified, validate it's within the number of phases + if ( + period.phaseToUse !== undefined && + (period.phaseToUse < 1 || period.phaseToUse > period.numberPhases) + ) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Period ${periodIndex.toString()} phaseToUse must be between 1 and numberPhases` + ) + return false + } + } + } + + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Schedule ${scheduleIndex.toString()} validation passed` + ) return true } diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts index 155b4c78..6201fdf1 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -21,22 +21,72 @@ import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' const moduleName = 'OCPP20RequestService' +/** + * OCPP 2.0.1 Request Service + * + * Handles outgoing OCPP 2.0.1 requests from the charging station to the charging station management system (CSMS). + * This service is responsible for: + * - Building and validating request payloads according to OCPP 2.0.1 specification + * - Managing request-response cycles with enhanced error handling and status reporting + * - Ensuring message integrity through comprehensive JSON schema validation + * - Providing type-safe interfaces for all supported OCPP 2.0.1 commands + * - Supporting advanced OCPP 2.0.1 features like variables, components, and enhanced transaction management + * + * Key architectural improvements over OCPP 1.6: + * - Enhanced variable and component model support + * - Improved transaction lifecycle management with UUIDs + * - Advanced authorization capabilities with IdTokens + * - Comprehensive EVSE and connector state management + * - Extended security and certificate handling + * OCPPRequestService - Base class providing common OCPP functionality + */ export class OCPP20RequestService extends OCPPRequestService { protected payloadValidatorFunctions: Map> + /** + * Constructs an OCPP 2.0.1 Request Service instance + * + * Initializes the service with OCPP 2.0.1-specific configurations including: + * - JSON schema validators for all supported OCPP 2.0.1 request commands + * - Enhanced payload validation with stricter type checking + * - Response service integration for comprehensive response handling + * - AJV validation setup optimized for OCPP 2.0.1's expanded message set + * - Support for advanced OCPP 2.0.1 features like variable management and enhanced security + * @param ocppResponseService - The response service instance for handling OCPP 2.0.1 responses + */ public constructor (ocppResponseService: OCPPResponseService) { - // if (new.target.name === moduleName) { - // throw new TypeError(`Cannot construct ${new.target.name} instances directly`) - // } super(OCPPVersion.VERSION_201, ocppResponseService) this.payloadValidatorFunctions = OCPP20ServiceUtils.createPayloadValidatorMap( OCPP20ServiceUtils.createRequestPayloadConfigs(), - OCPP20ServiceUtils.createRequestFactoryOptions(moduleName, 'constructor'), + OCPP20ServiceUtils.createRequestPayloadOptions(moduleName, 'constructor'), this.ajv ) this.buildRequestPayload = this.buildRequestPayload.bind(this) } + /** + * Handles OCPP 2.0.1 request processing with enhanced validation and comprehensive error handling + * + * This method serves as the main entry point for all outgoing OCPP 2.0.1 requests to the CSMS. + * It performs advanced operations including: + * - Validates that the requested command is supported by the charging station configuration + * - Builds and validates request payloads according to strict OCPP 2.0.1 schemas + * - Handles OCPP 2.0.1-specific features like component/variable management and enhanced security + * - Sends requests with comprehensive error handling and detailed logging + * - Processes responses with full support for OCPP 2.0.1's enhanced status reporting + * - Manages advanced OCPP 2.0.1 concepts like EVSE management and transaction UUIDs + * + * The method ensures full compliance with OCPP 2.0.1 specification while providing + * enhanced type safety and detailed error reporting for debugging and monitoring. + * @template RequestType - The expected type of the request parameters + * @template ResponseType - The expected type of the response from the CSMS + * @param chargingStation - The charging station instance making the request + * @param commandName - The OCPP 2.0.1 command to execute (e.g., 'Authorize', 'TransactionEvent') + * @param commandParams - Optional parameters specific to the command being executed + * @param params - Optional request parameters for controlling request behavior + * @returns Promise resolving to the typed response from the CSMS + * @throws {OCPPError} When the command is not supported, validation fails, or CSMS returns an error + */ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters public async requestHandler( chargingStation: ChargingStation, @@ -47,7 +97,6 @@ export class OCPP20RequestService extends OCPPRequestService { logger.debug( `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Processing '${commandName}' request` ) - // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc. if (OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) { try { logger.debug( diff --git a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts index b2bb8434..6adcf378 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts @@ -77,9 +77,6 @@ export class OCPP20ResponseService extends OCPPResponseService { private readonly responseHandlers: Map public constructor () { - // if (new.target.name === moduleName) { - // throw new TypeError(`Cannot construct ${new.target.name} instances directly`) - // } super(OCPPVersion.VERSION_201) this.responseHandlers = new Map([ [ @@ -98,13 +95,13 @@ export class OCPP20ResponseService extends OCPPResponseService { ]) this.payloadValidatorFunctions = OCPP20ServiceUtils.createPayloadValidatorMap( OCPP20ServiceUtils.createResponsePayloadConfigs(), - OCPP20ServiceUtils.createResponseFactoryOptions(moduleName, 'constructor'), + OCPP20ServiceUtils.createResponsePayloadOptions(moduleName, 'constructor'), this.ajv ) this.incomingRequestResponsePayloadValidateFunctions = OCPP20ServiceUtils.createPayloadValidatorMap( OCPP20ServiceUtils.createIncomingRequestResponsePayloadConfigs(), - OCPP20ServiceUtils.createIncomingRequestResponseFactoryOptions(moduleName, 'constructor'), + OCPP20ServiceUtils.createIncomingRequestResponsePayloadOptions(moduleName, 'constructor'), this.ajvIncomingRequest ) this.validatePayload = this.validatePayload.bind(this) @@ -176,7 +173,7 @@ export class OCPP20ResponseService extends OCPPResponseService { payload, undefined, 2 - )} while the charging station is not registered on the central server`, + )} while the charging station is not registered on the CSMS`, commandName, payload ) @@ -220,7 +217,7 @@ export class OCPP20ResponseService extends OCPPResponseService { } const logMsg = `${chargingStation.logPrefix()} ${moduleName}.handleResponseBootNotification: Charging station in '${ payload.status - }' state on the central server` + }' state on the CSMS` payload.status === RegistrationStatusEnumType.REJECTED ? logger.warn(logMsg) : logger.info(logMsg) diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index 884983ec..d2b8eb7b 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -73,23 +73,6 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { methodName ) - /** - * Factory options for OCPP 2.0 Incoming Request Response Service - * @param moduleName - Name of the OCPP module - * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 2.0 incoming request response validators - */ - public static createIncomingRequestResponseFactoryOptions = ( - moduleName: string, - methodName: string - ) => - OCPP20ServiceUtils.PayloadValidatorOptions( - OCPPVersion.VERSION_201, - 'assets/json-schemas/ocpp/2.0', - moduleName, - methodName - ) - /** * Configuration for OCPP 2.0 Incoming Request Response validators * @returns Array of validator configuration tuples @@ -117,12 +100,15 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ] /** - * Factory options for OCPP 2.0 Request Service + * Factory options for OCPP 2.0 Incoming Request Response Service * @param moduleName - Name of the OCPP module * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 2.0 validators + * @returns Factory options object for OCPP 2.0 incoming request response validators */ - public static createRequestFactoryOptions = (moduleName: string, methodName: string) => + public static createIncomingRequestResponsePayloadOptions = ( + moduleName: string, + methodName: string + ) => OCPP20ServiceUtils.PayloadValidatorOptions( OCPPVersion.VERSION_201, 'assets/json-schemas/ocpp/2.0', @@ -157,12 +143,12 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ] /** - * Factory options for OCPP 2.0 Response Service + * Factory options for OCPP 2.0 Request Service * @param moduleName - Name of the OCPP module * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 2.0 response validators + * @returns Factory options object for OCPP 2.0 validators */ - public static createResponseFactoryOptions = (moduleName: string, methodName: string) => + public static createRequestPayloadOptions = (moduleName: string, methodName: string) => OCPP20ServiceUtils.PayloadValidatorOptions( OCPPVersion.VERSION_201, 'assets/json-schemas/ocpp/2.0', @@ -196,6 +182,20 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ], ] + /** + * Factory options for OCPP 2.0 Response Service + * @param moduleName - Name of the OCPP module + * @param methodName - Name of the method/command + * @returns Factory options object for OCPP 2.0 response validators + */ + public static createResponsePayloadOptions = (moduleName: string, methodName: string) => + OCPP20ServiceUtils.PayloadValidatorOptions( + OCPPVersion.VERSION_201, + 'assets/json-schemas/ocpp/2.0', + moduleName, + methodName + ) + public static enforceMessageLimits< T extends { attributeType?: unknown; component: unknown; variable: unknown } >( diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index c83377c3..8cc07e40 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -70,6 +70,8 @@ import { OCPP16Constants } from './1.6/OCPP16Constants.js' import { OCPP20Constants } from './2.0/OCPP20Constants.js' import { OCPPConstants } from './OCPPConstants.js' +const moduleName = 'OCPPServiceUtils' + type Ajv = _Ajv.default // eslint-disable-next-line @typescript-eslint/no-redeclare const Ajv = _Ajv.default @@ -383,7 +385,7 @@ const validateSocMeasurandValue = ( debug ) { logger.error( - `${chargingStation.logPrefix()} MeterValues measurand ${ + `${chargingStation.logPrefix()} ${moduleName}.validateSocMeasurandValue: MeterValues measurand ${ sampledValue.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER // eslint-disable-next-line @typescript-eslint/restrict-template-expressions }: connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${socMinimumValue.toString()}/${sampledValue.value.toString()}/${socMaximumValue.toString()}` @@ -605,7 +607,7 @@ const validateEnergyMeasurandValue = ( if (energyValue > maxValue || energyValue < minValue || debug) { const connector = chargingStation.getConnectorStatus(connectorId) logger.error( - `${chargingStation.logPrefix()} MeterValues measurand ${ + `${chargingStation.logPrefix()} ${moduleName}.validateEnergyMeasurandValue: MeterValues measurand ${ sampledValue.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER // eslint-disable-next-line @typescript-eslint/restrict-template-expressions }: connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${minValue.toString()}/${energyValue.toString()}/${maxValue.toString()}, duration: ${interval.toString()}ms` @@ -826,7 +828,7 @@ const validatePowerMeasurandValue = ( debug ) { logger.error( - `${chargingStation.logPrefix()} MeterValues measurand ${ + `${chargingStation.logPrefix()} ${moduleName}.validatePowerMeasurandValue: MeterValues measurand ${ sampledValue.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER // eslint-disable-next-line @typescript-eslint/restrict-template-expressions }: connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumPower.toString()}/${sampledValue.value.toString()}/${connectorMaximumPower.toString()}` @@ -849,7 +851,7 @@ const validateCurrentMeasurandValue = ( debug ) { logger.error( - `${chargingStation.logPrefix()} MeterValues measurand ${ + `${chargingStation.logPrefix()} ${moduleName}.validateCurrentMeasurandValue: MeterValues measurand ${ sampledValue.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER // eslint-disable-next-line @typescript-eslint/restrict-template-expressions }: connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumAmperage.toString()}/${sampledValue.value.toString()}/${connectorMaximumAmperage.toString()}` @@ -872,7 +874,7 @@ const validateCurrentMeasurandPhaseValue = ( debug ) { logger.error( - `${chargingStation.logPrefix()} MeterValues measurand ${ + `${chargingStation.logPrefix()} ${moduleName}.validateCurrentMeasurandPhaseValue: MeterValues measurand ${ sampledValue.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER }: phase ${ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -1592,12 +1594,12 @@ const getSampledValueTemplate = ( } /** - * Builds a sampled value object according to the specified OCPP version - * @param ocppVersion - The OCPP version to use for formatting the sampled value - * @param sampledValueTemplate - Template containing measurement configuration and metadata - * @param value - The measured numeric value to be included in the sampled value - * @param context - Optional context specifying when the measurement was taken (e.g., Sample.Periodic) - * @param phase - Optional phase information for multi-phase electrical measurements + * Builds a sampled value object according to the specified OCPP version. + * @param ocppVersion The OCPP version to use for formatting the sampled value + * @param sampledValueTemplate Template containing measurement configuration and metadata + * @param value The measured numeric value to be included in the sampled value + * @param context Optional context specifying when the measurement was taken (e.g., Sample.Periodic) + * @param phase Optional phase information for multi-phase electrical measurements * @returns A sampled value object formatted according to the specified OCPP version */ function buildSampledValue ( @@ -1806,15 +1808,15 @@ export class OCPPServiceUtils { } /** - * Creates a Map of compiled OCPP payload validators from configurations - * Reduces code duplication across OCPP services - * @param configs - Array of tuples containing command and validator configuration - * @param options - Factory options including OCPP version, schema directory, etc. - * @param options.ocppVersion - The OCPP version for schema validation - * @param options.schemaDir - Directory path containing JSON schemas - * @param options.moduleName - Name of the module for logging - * @param options.methodName - Name of the method for logging - * @param ajvInstance - Configured Ajv instance for validation + * Creates a Map of compiled OCPP payload validators from configurations. + * Reduces code duplication across OCPP services. + * @param configs Array of tuples containing command and validator configuration + * @param options Factory options including OCPP version, schema directory, etc. + * @param options.ocppVersion The OCPP version for schema validation + * @param options.schemaDir Directory path containing JSON schemas + * @param options.moduleName Name of the module for logging + * @param options.methodName Name of the method for logging + * @param ajvInstance Configured Ajv instance for validation * @returns Map of commands to their compiled validation functions */ public static createPayloadValidatorMap( @@ -1916,8 +1918,8 @@ export class OCPPServiceUtils { } /** - * Configuration for a single payload validator - * @param schemaPath - Path to the JSON schema file + * Configuration for a single payload validator. + * @param schemaPath Path to the JSON schema file * @returns Configuration object for payload validator creation */ public static readonly PayloadValidatorConfig = (schemaPath: string) => @@ -1926,11 +1928,11 @@ export class OCPPServiceUtils { }) as const /** - * Options for payload validator creation - * @param ocppVersion - The OCPP version - * @param schemaDir - Directory containing JSON schemas - * @param moduleName - Name of the OCPP module - * @param methodName - Name of the method/command + * Options for payload validator creation. + * @param ocppVersion The OCPP version + * @param schemaDir Directory containing JSON schemas + * @param moduleName Name of the OCPP module + * @param methodName Name of the method/command * @returns Options object for payload validator creation */ public static readonly PayloadValidatorOptions = ( @@ -1947,12 +1949,12 @@ export class OCPPServiceUtils { }) as const /** - * Parses and loads a JSON schema file for OCPP payload validation - * Handles file reading, JSON parsing, and error recovery for schema validation - * @param relativePath - Path to the schema file relative to the OCPP utils directory - * @param ocppVersion - The OCPP version for error logging context - * @param moduleName - Optional module name for error logging - * @param methodName - Optional method name for error logging + * Parses and loads a JSON schema file for OCPP payload validation. + * Handles file reading, JSON parsing, and error recovery for schema validation. + * @param relativePath Path to the schema file relative to the OCPP utils directory + * @param ocppVersion The OCPP version for error logging context + * @param moduleName Optional module name for error logging + * @param methodName Optional method name for error logging * @returns Parsed JSON schema object or empty object if parsing fails */ protected static parseJsonSchemaFile( diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts index e95e1a86..17e34a75 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts @@ -124,7 +124,7 @@ await describe('E01 - Remote Start Transaction', async () => { mockChargingStation, invalidEvseRequest ) - ).rejects.toThrow('EVSE 999 not found on charging station') + ).rejects.toThrow('EVSE 999 does not exist on charging station') }) await it('Should reject RequestStartTransaction when connector is already occupied', async () => { -- 2.43.0