From 91f9621473de2f50db59e35beea2e169acfa511c Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Mon, 30 Mar 2026 23:45:32 +0200 Subject: [PATCH] refactor(ocpp): dissolve OCPPServiceUtils class and eliminate dynamic imports Dissolve the OCPPServiceUtils class into standalone exported functions, removing the class hierarchy (extends) from OCPP16/20ServiceUtils. Extract 6 version-dispatching operations into OCPPServiceOperations.ts with static top-level imports, eliminating all 11 dynamic await import() calls that were used to work around circular dependencies. The dependency graph is now a clean DAG: OCPPServiceUtils.ts (pure utilities, no version knowledge of subclasses) OCPP16/20ServiceUtils.ts (version-specific, import utils directly) OCPPServiceOperations.ts (dispatchers, static imports of both) Cross-component imports now go through barrel files per project conventions. JSDoc harmonized across all modified files. --- .../AutomaticTransactionGenerator.ts | 7 +- src/charging-station/ChargingStation.ts | 3 +- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 30 +- .../ocpp/1.6/OCPP16RequestService.ts | 6 +- .../ocpp/1.6/OCPP16ResponseService.ts | 29 +- .../ocpp/1.6/OCPP16ServiceUtils.ts | 107 ++- .../ocpp/2.0/OCPP20IncomingRequestService.ts | 28 +- .../ocpp/2.0/OCPP20RequestService.ts | 10 +- .../ocpp/2.0/OCPP20ResponseService.ts | 30 +- .../ocpp/2.0/OCPP20ServiceUtils.ts | 131 +++- .../ocpp/OCPPServiceOperations.ts | 246 +++++++ src/charging-station/ocpp/OCPPServiceUtils.ts | 680 +++++++----------- src/charging-station/ocpp/auth/cache/index.ts | 1 - .../ocpp/auth/factories/index.ts | 1 - src/charging-station/ocpp/auth/index.ts | 3 +- src/charging-station/ocpp/auth/utils/index.ts | 8 - src/charging-station/ocpp/index.ts | 6 + .../ocpp/1.6/OCPP16ServiceUtils.test.ts | 33 +- ...CPP20ServiceUtils-TransactionEvent.test.ts | 6 +- .../OCPPServiceUtils-StopTransaction.test.ts | 4 +- .../ocpp/OCPPServiceUtils-pure.test.ts | 8 +- .../ocpp/OCPPServiceUtils-validation.test.ts | 42 +- 22 files changed, 876 insertions(+), 543 deletions(-) create mode 100644 src/charging-station/ocpp/OCPPServiceOperations.ts delete mode 100644 src/charging-station/ocpp/auth/cache/index.ts delete mode 100644 src/charging-station/ocpp/auth/factories/index.ts delete mode 100644 src/charging-station/ocpp/auth/utils/index.ts diff --git a/src/charging-station/AutomaticTransactionGenerator.ts b/src/charging-station/AutomaticTransactionGenerator.ts index 654e7583..0d630156 100644 --- a/src/charging-station/AutomaticTransactionGenerator.ts +++ b/src/charging-station/AutomaticTransactionGenerator.ts @@ -28,8 +28,11 @@ import { } from '../utils/index.js' import { checkChargingStationState } from './Helpers.js' import { IdTagsCache } from './IdTagsCache.js' -import { isIdTagAuthorized } from './ocpp/index.js' -import { startTransactionOnConnector, stopTransactionOnConnector } from './ocpp/OCPPServiceUtils.js' +import { + isIdTagAuthorized, + startTransactionOnConnector, + stopTransactionOnConnector, +} from './ocpp/index.js' export class AutomaticTransactionGenerator { private static readonly instances: Map = new Map< diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 8f96d7b3..4dab126a 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -146,6 +146,7 @@ import { } from './Helpers.js' import { IdTagsCache } from './IdTagsCache.js' import { + flushQueuedTransactionMessages, OCPP16IncomingRequestService, OCPP16RequestService, OCPP16ResponseService, @@ -156,8 +157,8 @@ import { type OCPPIncomingRequestService, type OCPPRequestService, sendAndSetConnectorStatus, + stopRunningTransactions, } from './ocpp/index.js' -import { flushQueuedTransactionMessages, stopRunningTransactions } from './ocpp/OCPPServiceUtils.js' import { SharedLRUCache } from './SharedLRUCache.js' export class ChargingStation extends EventEmitter { diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index bbb9840e..02a33d53 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -119,7 +119,14 @@ import { AuthContext } from '../auth/index.js' import { isIdTagAuthorized } from '../IdTagAuthorization.js' import { OCPPConstants } from '../OCPPConstants.js' import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js' -import { buildMeterValue, sendAndSetConnectorStatus } from '../OCPPServiceUtils.js' +import { + buildMeterValue, + createPayloadValidatorMap, + isConnectorIdValid, + isIncomingRequestCommandSupported, + isMessageTriggerSupported, + sendAndSetConnectorStatus, +} from '../OCPPServiceUtils.js' import { OCPP16Constants } from './OCPP16Constants.js' import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js' @@ -172,6 +179,9 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION, ] + /** + * Constructs an OCPP 1.6 Incoming Request Service with request handlers, validators, and event listeners. + */ public constructor () { super(OCPPVersion.VERSION_16) this.incomingRequestHandlers = new Map([ @@ -244,7 +254,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { this.toRequestHandler(this.handleRequestUpdateFirmware.bind(this)), ], ]) - this.payloadValidatorFunctions = OCPP16ServiceUtils.createPayloadValidatorMap( + this.payloadValidatorFunctions = createPayloadValidatorMap( OCPP16ServiceUtils.createIncomingRequestPayloadConfigs(), OCPP16ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajv @@ -556,15 +566,25 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ) } + /** + * Stops the incoming request service for the given charging station. + * @param chargingStation - Target charging station + */ public override stop (chargingStation: ChargingStation): void { /* no-op for OCPP 1.6 */ } + /** + * Checks whether the given incoming request command is supported by the charging station. + * @param chargingStation - Target charging station + * @param commandName - Incoming request command to check + * @returns Whether the command is supported + */ protected isIncomingRequestCommandSupported ( chargingStation: ChargingStation, commandName: IncomingRequestCommand ): boolean { - return OCPP16ServiceUtils.isIncomingRequestCommandSupported( + return isIncomingRequestCommandSupported( chargingStation, commandName as OCPP16IncomingRequestCommand ) @@ -1494,13 +1514,13 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { OCPP16SupportedFeatureProfiles.RemoteTrigger, OCPP16IncomingRequestCommand.TRIGGER_MESSAGE ) || - !OCPP16ServiceUtils.isMessageTriggerSupported(chargingStation, requestedMessage) + !isMessageTriggerSupported(chargingStation, requestedMessage) ) { return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED } if ( connectorId != null && - !OCPP16ServiceUtils.isConnectorIdValid( + !isConnectorIdValid( chargingStation, OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, connectorId diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index f57d7d62..63891195 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -21,6 +21,8 @@ import { OCPPRequestService } from '../OCPPRequestService.js' import { buildStatusNotificationRequest, buildTransactionEndMeterValue, + createPayloadValidatorMap, + isRequestCommandSupported, sendAndSetConnectorStatus, } from '../OCPPServiceUtils.js' import { OCPP16Constants } from './OCPP16Constants.js' @@ -59,7 +61,7 @@ export class OCPP16RequestService extends OCPPRequestService { */ public constructor (ocppResponseService: OCPPResponseService) { super(OCPPVersion.VERSION_16, ocppResponseService) - this.payloadValidatorFunctions = OCPP16ServiceUtils.createPayloadValidatorMap( + this.payloadValidatorFunctions = createPayloadValidatorMap( OCPP16ServiceUtils.createRequestPayloadConfigs(), OCPP16ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajv @@ -98,7 +100,7 @@ export class OCPP16RequestService extends OCPPRequestService { logger.debug( `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Processing '${commandName}' request` ) - if (OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) { + if (isRequestCommandSupported(chargingStation, commandName)) { try { logger.debug( `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Building request payload for '${commandName}'` diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index 2b8eb41c..32cc33db 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -39,6 +39,8 @@ import { Constants, convertToInt, logger, truncateId } from '../../../utils/inde import { OCPPResponseService } from '../OCPPResponseService.js' import { buildTransactionEndMeterValue, + createPayloadValidatorMap, + isRequestCommandSupported, restoreConnectorStatus, sendAndSetConnectorStatus, } from '../OCPPServiceUtils.js' @@ -94,6 +96,9 @@ export class OCPP16ResponseService extends OCPPResponseService { protected readonly responseHandlers: Map + /** + * Constructs an OCPP 1.6 Response Service instance with response handlers and validators. + */ public constructor () { super(OCPPVersion.VERSION_16) this.responseHandlers = new Map([ @@ -126,27 +131,29 @@ export class OCPP16ResponseService extends OCPPResponseService { this.toResponseHandler(this.handleResponseStopTransaction.bind(this)), ], ]) - this.payloadValidatorFunctions = OCPP16ServiceUtils.createPayloadValidatorMap( + this.payloadValidatorFunctions = createPayloadValidatorMap( OCPP16ServiceUtils.createResponsePayloadConfigs(), OCPP16ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajv ) - this.incomingRequestResponsePayloadValidateFunctions = - OCPP16ServiceUtils.createPayloadValidatorMap( - OCPP16ServiceUtils.createIncomingRequestResponsePayloadConfigs(), - OCPP16ServiceUtils.createPayloadOptions(moduleName, 'constructor'), - this.ajvIncomingRequest - ) + this.incomingRequestResponsePayloadValidateFunctions = createPayloadValidatorMap( + OCPP16ServiceUtils.createIncomingRequestResponsePayloadConfigs(), + OCPP16ServiceUtils.createPayloadOptions(moduleName, 'constructor'), + this.ajvIncomingRequest + ) } + /** + * Checks whether the given request command is supported by the charging station. + * @param chargingStation - Target charging station + * @param commandName - Request command to check + * @returns Whether the command is supported + */ protected isRequestCommandSupported ( chargingStation: ChargingStation, commandName: RequestCommand ): boolean { - return OCPP16ServiceUtils.isRequestCommandSupported( - chargingStation, - commandName as OCPP16RequestCommand - ) + return isRequestCommandSupported(chargingStation, commandName as OCPP16RequestCommand) } private handleResponseAuthorize ( diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 7807a54a..026ba05e 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -57,14 +57,16 @@ import { buildSampledValue, buildTransactionEndMeterValue, getSampledValueTemplate, - OCPPServiceUtils, + PayloadValidatorConfig, + PayloadValidatorOptions, sendAndSetConnectorStatus, } from '../OCPPServiceUtils.js' import { OCPP16Constants } from './OCPP16Constants.js' const moduleName = 'OCPP16ServiceUtils' -export class OCPP16ServiceUtils extends OCPPServiceUtils { +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class OCPP16ServiceUtils { private static readonly incomingRequestSchemaNames: readonly [ OCPP16IncomingRequestCommand, string @@ -101,6 +103,13 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { [OCPP16RequestCommand.STOP_TRANSACTION, 'StopTransaction'], ] + /** + * Builds a meter value for the beginning of a transaction. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier + * @param meterStart - Initial meter reading in Wh + * @returns Meter value with the transaction begin context + */ public static buildTransactionBeginMeterValue ( chargingStation: ChargingStation, connectorId: number, @@ -124,6 +133,12 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return meterValue } + /** + * Builds an array of transaction data meter values from begin and end values. + * @param transactionBeginMeterValue - Meter value at transaction start + * @param transactionEndMeterValue - Meter value at transaction end + * @returns Array containing the begin and end meter values + */ public static buildTransactionDataMeterValues ( transactionBeginMeterValue: OCPP16MeterValue, transactionEndMeterValue: OCPP16MeterValue @@ -134,6 +149,14 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return meterValues } + /** + * Changes the availability of connectors and updates their status. + * @param chargingStation - Target charging station + * @param connectorIds - Array of connector identifiers to update + * @param chargePointStatus - New charge point status to set + * @param availabilityType - Operative or inoperative availability type + * @returns Accepted or scheduled availability change response + */ public static changeAvailability = async ( chargingStation: ChargingStation, connectorIds: number[], @@ -166,6 +189,13 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED } + /** + * Checks whether a feature profile is enabled on the charging station. + * @param chargingStation - Target charging station + * @param featureProfile - Feature profile to check + * @param command - OCPP command requiring the feature profile + * @returns Whether the feature profile is enabled + */ public static checkFeatureProfile ( chargingStation: ChargingStation, featureProfile: OCPP16SupportedFeatureProfiles, @@ -182,6 +212,13 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return true } + /** + * Clears charging profiles matching the given criteria from the profiles array. + * @param chargingStation - Target charging station + * @param commandPayload - Clear charging profile request with filter criteria + * @param chargingProfiles - Array of charging profiles to filter + * @returns Whether any charging profiles were cleared + */ public static clearChargingProfiles = ( chargingStation: ChargingStation, commandPayload: OCPP16ClearChargingProfileRequest, @@ -223,6 +260,13 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return clearedCP } + /** + * Composes a composite charging schedule from higher and lower priority schedules. + * @param chargingScheduleHigher - Higher priority charging schedule + * @param chargingScheduleLower - Lower priority charging schedule + * @param compositeInterval - Time interval for the composite schedule + * @returns Composed charging schedule or undefined if both inputs are null + */ public static composeChargingSchedules = ( chargingScheduleHigher: OCPP16ChargingSchedule | undefined, chargingScheduleLower: OCPP16ChargingSchedule | undefined, @@ -435,7 +479,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ][] => OCPP16ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [ command, - OCPP16ServiceUtils.PayloadValidatorConfig(`${schemaBase}.json`), + PayloadValidatorConfig(`${schemaBase}.json`), ]) /** @@ -448,7 +492,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ][] => OCPP16ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [ command, - OCPP16ServiceUtils.PayloadValidatorConfig(`${schemaBase}Response.json`), + PayloadValidatorConfig(`${schemaBase}Response.json`), ]) /** @@ -458,7 +502,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { * @returns Factory options object for OCPP 1.6 validators */ public static createPayloadOptions = (moduleName: string, methodName: string) => - OCPP16ServiceUtils.PayloadValidatorOptions( + PayloadValidatorOptions( OCPPVersion.VERSION_16, 'assets/json-schemas/ocpp/1.6', moduleName, @@ -475,7 +519,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ][] => OCPP16ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [ command, - OCPP16ServiceUtils.PayloadValidatorConfig(`${schemaBase}.json`), + PayloadValidatorConfig(`${schemaBase}.json`), ]) /** @@ -488,9 +532,16 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ][] => OCPP16ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [ command, - OCPP16ServiceUtils.PayloadValidatorConfig(`${schemaBase}Response.json`), + PayloadValidatorConfig(`${schemaBase}Response.json`), ]) + /** + * Checks whether a connector or the charging station has a valid reservation for the given idTag. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier to check + * @param idTag - RFID tag to match against the reservation + * @returns Whether a valid reservation exists for the idTag + */ public static hasReservation = ( chargingStation: ChargingStation, connectorId: number, @@ -518,6 +569,11 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return false } + /** + * Determines whether a configuration key should be visible in GetConfiguration responses. + * @param key - Configuration key to check + * @returns Whether the key is visible + */ public static isConfigurationKeyVisible (key: ConfigurationKey): boolean { if (key.visible == null) { return true @@ -525,6 +581,12 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return key.visible } + /** + * Stops a transaction remotely on the given connector. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier with the active transaction + * @returns Accepted or rejected generic response + */ public static remoteStopTransaction = async ( chargingStation: ChargingStation, connectorId: number @@ -544,6 +606,12 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return OCPP16Constants.OCPP_RESPONSE_REJECTED } + /** + * Sets or replaces a charging profile on a connector. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier to set the profile on + * @param cp - Charging profile to set + */ public static setChargingProfile ( chargingStation: ChargingStation, connectorId: number, @@ -589,6 +657,13 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp) } + /** + * Sends a StartTransaction request to the central system for the given connector. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier to start the transaction on + * @param idTag - Optional RFID tag for the transaction + * @returns Start transaction response from the central system + */ public static async startTransactionOnConnector ( chargingStation: ChargingStation, connectorId: number, @@ -603,6 +678,12 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { }) } + /** + * Starts periodic meter value updates for an active transaction on a connector. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier with the active transaction + * @param interval - Meter value sample interval in milliseconds + */ public static startUpdatedMeterValues ( chargingStation: ChargingStation, connectorId: number, @@ -649,6 +730,13 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { }, clampToSafeTimerValue(interval)) } + /** + * Sends a StopTransaction request to the central system for the given connector. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier with the active transaction + * @param reason - Optional stop transaction reason + * @returns Stop transaction response from the central system + */ public static async stopTransactionOnConnector ( chargingStation: ChargingStation, connectorId: number, @@ -688,6 +776,11 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { }) } + /** + * Stops periodic meter value updates for a connector. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier to stop updates for + */ public static stopUpdatedMeterValues ( 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 fe02790e..aff6b6e0 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -150,6 +150,8 @@ import { import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js' import { buildMeterValue, + createPayloadValidatorMap, + isIncomingRequestCommandSupported, restoreConnectorStatus, sendAndSetConnectorStatus, } from '../OCPPServiceUtils.js' @@ -312,7 +314,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { this.toRequestHandler(this.handleRequestUpdateFirmware.bind(this)), ], ]) - this.payloadValidatorFunctions = OCPP20ServiceUtils.createPayloadValidatorMap( + this.payloadValidatorFunctions = createPayloadValidatorMap( OCPP20ServiceUtils.createIncomingRequestPayloadConfigs(), OCPP20ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajv @@ -573,6 +575,12 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { ) } + /** + * Handle OCPP 2.0.1 GetVariables request from the CSMS. + * @param chargingStation - Target charging station + * @param commandPayload - GetVariables request payload + * @returns GetVariables response with variable results + */ public handleRequestGetVariables ( chargingStation: ChargingStation, commandPayload: OCPP20GetVariablesRequest @@ -641,6 +649,12 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { return getVariablesResponse } + /** + * Handle OCPP 2.0.1 SetVariables request from the CSMS. + * @param chargingStation - Target charging station + * @param commandPayload - SetVariables request payload + * @returns SetVariables response with variable results + */ public handleRequestSetVariables ( chargingStation: ChargingStation, commandPayload: OCPP20SetVariablesRequest @@ -707,6 +721,10 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { return setVariablesResponse } + /** + * Stop the incoming request service and clean up per-station state. + * @param chargingStation - Target charging station to stop + */ public override stop (chargingStation: ChargingStation): void { const stationState = this.stationsState.get(chargingStation) if (stationState != null) { @@ -759,11 +777,17 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } + /** + * Check whether an incoming request command is supported by the charging station. + * @param chargingStation - Target charging station + * @param commandName - Incoming request command to check + * @returns Whether the command is supported + */ protected isIncomingRequestCommandSupported ( chargingStation: ChargingStation, commandName: IncomingRequestCommand ): boolean { - return OCPP20ServiceUtils.isIncomingRequestCommandSupported( + return isIncomingRequestCommandSupported( chargingStation, commandName as OCPP20IncomingRequestCommand ) diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts index 0e5b8261..f5435a7b 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -18,7 +18,11 @@ import { } from '../../../types/index.js' import { generateUUID, logger } from '../../../utils/index.js' import { OCPPRequestService } from '../OCPPRequestService.js' -import { buildStatusNotificationRequest } from '../OCPPServiceUtils.js' +import { + buildStatusNotificationRequest, + createPayloadValidatorMap, + isRequestCommandSupported, +} from '../OCPPServiceUtils.js' import { generatePkcs10Csr } from './Asn1DerUtils.js' import { OCPP20Constants } from './OCPP20Constants.js' import { buildTransactionEvent, OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' @@ -60,7 +64,7 @@ export class OCPP20RequestService extends OCPPRequestService { */ public constructor (ocppResponseService: OCPPResponseService) { super(OCPPVersion.VERSION_201, ocppResponseService) - this.payloadValidatorFunctions = OCPP20ServiceUtils.createPayloadValidatorMap( + this.payloadValidatorFunctions = createPayloadValidatorMap( OCPP20ServiceUtils.createRequestPayloadConfigs(), OCPP20ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajv @@ -101,7 +105,7 @@ export class OCPP20RequestService extends OCPPRequestService { logger.debug( `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Processing '${commandName}' request` ) - if (OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) { + if (isRequestCommandSupported(chargingStation, commandName)) { try { logger.debug( `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Building request payload for '${commandName}'` diff --git a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts index 48f6f8df..1470cae6 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts @@ -40,7 +40,11 @@ import { import { convertToDate, logger } from '../../../utils/index.js' import { mapOCPP20TokenType, OCPPAuthServiceFactory } from '../auth/index.js' import { OCPPResponseService } from '../OCPPResponseService.js' -import { sendAndSetConnectorStatus } from '../OCPPServiceUtils.js' +import { + createPayloadValidatorMap, + isRequestCommandSupported, + sendAndSetConnectorStatus, +} from '../OCPPServiceUtils.js' import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' const moduleName = 'OCPP20ResponseService' @@ -164,27 +168,29 @@ export class OCPP20ResponseService extends OCPPResponseService { this.toResponseHandler(this.handleResponseTransactionEvent.bind(this)), ], ]) - this.payloadValidatorFunctions = OCPP20ServiceUtils.createPayloadValidatorMap( + this.payloadValidatorFunctions = createPayloadValidatorMap( OCPP20ServiceUtils.createResponsePayloadConfigs(), OCPP20ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajv ) - this.incomingRequestResponsePayloadValidateFunctions = - OCPP20ServiceUtils.createPayloadValidatorMap( - OCPP20ServiceUtils.createIncomingRequestResponsePayloadConfigs(), - OCPP20ServiceUtils.createPayloadOptions(moduleName, 'constructor'), - this.ajvIncomingRequest - ) + this.incomingRequestResponsePayloadValidateFunctions = createPayloadValidatorMap( + OCPP20ServiceUtils.createIncomingRequestResponsePayloadConfigs(), + OCPP20ServiceUtils.createPayloadOptions(moduleName, 'constructor'), + this.ajvIncomingRequest + ) } + /** + * Check whether a request command is supported by the charging station. + * @param chargingStation - Target charging station + * @param commandName - Request command to check + * @returns Whether the command is supported + */ protected isRequestCommandSupported ( chargingStation: ChargingStation, commandName: RequestCommand ): boolean { - return OCPP20ServiceUtils.isRequestCommandSupported( - chargingStation, - commandName as OCPP20RequestCommand - ) + return isRequestCommandSupported(chargingStation, commandName as OCPP20RequestCommand) } private handleResponseAuthorize ( diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index 34bbab14..4f875bd6 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -41,7 +41,8 @@ import { import { buildConfigKey, getConfigurationKey } from '../../ConfigurationKeyUtils.js' import { buildMeterValue, - OCPPServiceUtils, + PayloadValidatorConfig, + PayloadValidatorOptions, sendAndSetConnectorStatus, } from '../OCPPServiceUtils.js' import { OCPP20VariableManager } from './OCPP20VariableManager.js' @@ -53,7 +54,8 @@ export interface RejectionReason { reasonCode: ReasonCodeEnumType } -export class OCPP20ServiceUtils extends OCPPServiceUtils { +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class OCPP20ServiceUtils { private static readonly incomingRequestSchemaNames: readonly [ OCPP20IncomingRequestCommand, string @@ -98,6 +100,12 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { [OCPP20RequestCommand.TRANSACTION_EVENT, 'TransactionEvent'], ] + /** + * Build meter values for the start of a transaction. + * @param chargingStation - Target charging station + * @param transactionId - Transaction identifier + * @returns Array of OCPP 2.0 meter values at transaction begin + */ static buildTransactionStartedMeterValues ( chargingStation: ChargingStation, transactionId: number | string @@ -123,6 +131,12 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { } } + /** + * Clean up connector state after a transaction has ended. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier + * @param connectorStatus - Connector status to reset + */ public static async cleanupEndedTransaction ( chargingStation: ChargingStation, connectorId: number, @@ -147,7 +161,7 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ][] => OCPP20ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [ command, - OCPP20ServiceUtils.PayloadValidatorConfig(`${schemaBase}Request.json`), + PayloadValidatorConfig(`${schemaBase}Request.json`), ]) /** @@ -160,7 +174,7 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ][] => OCPP20ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [ command, - OCPP20ServiceUtils.PayloadValidatorConfig(`${schemaBase}Response.json`), + PayloadValidatorConfig(`${schemaBase}Response.json`), ]) /** @@ -170,7 +184,7 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { * @returns Factory options object for OCPP 2.0 validators */ public static createPayloadOptions = (moduleName: string, methodName: string) => - OCPP20ServiceUtils.PayloadValidatorOptions( + PayloadValidatorOptions( OCPPVersion.VERSION_201, 'assets/json-schemas/ocpp/2.0', moduleName, @@ -187,7 +201,7 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ][] => OCPP20ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [ command, - OCPP20ServiceUtils.PayloadValidatorConfig(`${schemaBase}Request.json`), + PayloadValidatorConfig(`${schemaBase}Request.json`), ]) /** @@ -200,9 +214,23 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ][] => OCPP20ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [ command, - OCPP20ServiceUtils.PayloadValidatorConfig(`${schemaBase}Response.json`), + PayloadValidatorConfig(`${schemaBase}Response.json`), ]) + /** + * Enforce ItemsPerMessage and BytesPerMessage limits on request data. + * @param chargingStation - Charging station providing log prefix + * @param chargingStation.logPrefix - Log prefix function + * @param moduleName - Module name for logging context + * @param context - Method name for logging context + * @param data - Array of variable data items to validate + * @param itemsLimit - Maximum allowed items per message (0 = unlimited) + * @param bytesLimit - Maximum allowed bytes per message (0 = unlimited) + * @param buildRejected - Factory function to build rejection results + * @param logger - Logger instance for debug output + * @param logger.debug - Debug logging function + * @returns Object indicating whether data was rejected and the rejection results + */ public static enforceMessageLimits< T extends { attributeType?: unknown; component: unknown; variable: unknown }, R @@ -246,6 +274,20 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { return { rejected: false, results: [] } } + /** + * Enforce BytesPerMessage limit after results have been computed. + * @param chargingStation - Charging station providing log prefix + * @param chargingStation.logPrefix - Log prefix function + * @param moduleName - Module name for logging context + * @param context - Method name for logging context + * @param originalData - Original variable data items + * @param currentResults - Computed results to check against byte limit + * @param bytesLimit - Maximum allowed bytes per message (0 = unlimited) + * @param buildRejected - Factory function to build rejection results + * @param logger - Logger instance for debug output + * @param logger.debug - Debug logging function + * @returns Original results if within limit, or rejection results if exceeded + */ public static enforcePostCalculationBytesLimit< T extends { attributeType?: unknown; component: unknown; variable: unknown }, R @@ -284,6 +326,11 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { return currentResults } + /** + * Retrieve the AlignedDataCtrlr interval in milliseconds. + * @param chargingStation - Target charging station + * @returns Aligned data interval in milliseconds + */ public static getAlignedDataInterval (chargingStation: ChargingStation): number { return OCPP20ServiceUtils.readVariableAsIntervalMs( chargingStation, @@ -293,6 +340,11 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ) } + /** + * Retrieve the SampledDataCtrlr TxEndedInterval in milliseconds. + * @param chargingStation - Target charging station + * @returns Transaction ended meter values interval in milliseconds + */ public static getTxEndedInterval (chargingStation: ChargingStation): number { return OCPP20ServiceUtils.readVariableAsIntervalMs( chargingStation, @@ -302,6 +354,11 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ) } + /** + * Retrieve the SampledDataCtrlr TxUpdatedInterval in milliseconds. + * @param chargingStation - Target charging station + * @returns Transaction updated meter values interval in milliseconds + */ public static getTxUpdatedInterval (chargingStation: ChargingStation): number { return OCPP20ServiceUtils.readVariableAsIntervalMs( chargingStation, @@ -354,6 +411,13 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { return { bytesLimit, itemsLimit } } + /** + * Deauthorize an active transaction per OCPP 2.0.1 E05 requirements. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier with the active transaction + * @param evseId - Optional EVSE identifier + * @returns Promise resolving to the TransactionEvent response + */ public static async requestDeauthorizeTransaction ( chargingStation: ChargingStation, connectorId: number, @@ -431,6 +495,15 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ) } + /** + * Stop an active transaction by sending a TransactionEvent(Ended). + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier with the active transaction + * @param evseId - Optional EVSE identifier + * @param triggerReason - Trigger reason for the stop event + * @param stoppedReason - Reason the transaction was stopped + * @returns Promise resolving to the TransactionEvent response + */ public static async requestStopTransaction ( chargingStation: ChargingStation, connectorId: number, @@ -476,6 +549,11 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { } } + /** + * Send queued TransactionEvent requests accumulated while offline. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier whose queue to drain + */ public static async sendQueuedTransactionEvents ( chargingStation: ChargingStation, connectorId: number @@ -523,6 +601,16 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { } } + /** + * Send a TransactionEvent request to the CSMS, or queue it if offline. + * @param chargingStation - Target charging station + * @param eventType - Transaction event type (Started, Updated, Ended) + * @param triggerReason - Reason that triggered the event + * @param connectorId - Connector identifier + * @param transactionId - Transaction identifier + * @param options - Additional transaction event options + * @returns Promise resolving to the TransactionEvent response + */ public static async sendTransactionEvent ( chargingStation: ChargingStation, eventType: OCPP20TransactionEventEnumType, @@ -590,6 +678,12 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { } } + /** + * Start periodic collection of TxEnded meter values for a connector. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier + * @param interval - Collection interval in milliseconds + */ public static startEndedMeterValues ( chargingStation: ChargingStation, connectorId: number, @@ -629,6 +723,12 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ) } + /** + * Start periodic TransactionEvent(Updated) with meter values for a connector. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier + * @param interval - Sending interval in milliseconds + */ public static startUpdatedMeterValues ( chargingStation: ChargingStation, connectorId: number, @@ -712,6 +812,13 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ) } + /** + * Stop all active transactions on the charging station or a specific EVSE. + * @param chargingStation - Target charging station + * @param triggerReason - Trigger reason for stop events + * @param stoppedReason - Reason the transactions were stopped + * @param evseId - Optional EVSE identifier to limit scope + */ public static async stopAllTransactions ( chargingStation: ChargingStation, triggerReason: OCPP20TriggerReasonEnumType = OCPP20TriggerReasonEnumType.RemoteStop, @@ -770,6 +877,11 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { } } + /** + * Stop periodic TxEnded meter value collection for a connector. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier + */ public static stopEndedMeterValues (chargingStation: ChargingStation, connectorId: number): void { const connectorStatus = chargingStation.getConnectorStatus(connectorId) if (connectorStatus?.transactionEndedMeterValuesSetInterval != null) { @@ -781,6 +893,11 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { } } + /** + * Stop periodic TransactionEvent(Updated) sending for a connector. + * @param chargingStation - Target charging station + * @param connectorId - Connector identifier + */ public static stopUpdatedMeterValues ( chargingStation: ChargingStation, connectorId: number diff --git a/src/charging-station/ocpp/OCPPServiceOperations.ts b/src/charging-station/ocpp/OCPPServiceOperations.ts new file mode 100644 index 00000000..074d5e86 --- /dev/null +++ b/src/charging-station/ocpp/OCPPServiceOperations.ts @@ -0,0 +1,246 @@ +import type { StopTransactionReason } from '../../types/index.js' + +import { type ChargingStation } from '../../charging-station/index.js' +import { OCPPError } from '../../exception/index.js' +import { + AuthorizationStatus, + ErrorType, + OCPP20AuthorizationStatusEnumType, + OCPP20IdTokenEnumType, + OCPP20TransactionEventEnumType, + OCPP20TriggerReasonEnumType, + OCPPVersion, + type StartTransactionResult, + type StopTransactionResult, +} from '../../types/index.js' +import { generateUUID, logger } from '../../utils/index.js' +import { OCPP16ServiceUtils } from './1.6/OCPP16ServiceUtils.js' +import { OCPP20ServiceUtils } from './2.0/OCPP20ServiceUtils.js' +import { mapStopReasonToOCPP20 } from './OCPPServiceUtils.js' + +/** + * Starts a transaction on a specific connector using the appropriate OCPP version handler. + * @param chargingStation - Target charging station + * @param connectorId - Connector ID to start the transaction on + * @param idTag - Optional RFID tag for authorization + * @returns Result indicating whether the transaction was accepted + */ +export const startTransactionOnConnector = async ( + chargingStation: ChargingStation, + connectorId: number, + idTag?: string +): Promise => { + switch (chargingStation.stationInfo?.ocppVersion) { + case OCPPVersion.VERSION_16: { + const response = await OCPP16ServiceUtils.startTransactionOnConnector( + chargingStation, + connectorId, + idTag + ) + return { accepted: response.idTagInfo.status === AuthorizationStatus.ACCEPTED } + } + case OCPPVersion.VERSION_20: + case OCPPVersion.VERSION_201: { + const connectorStatus = chargingStation.getConnectorStatus(connectorId) + let transactionId = connectorStatus?.transactionId as string | undefined + if (transactionId == null) { + transactionId = generateUUID() + if (connectorStatus != null) { + connectorStatus.transactionId = transactionId + } + OCPP20ServiceUtils.resetTransactionSequenceNumber(chargingStation, connectorId) + } + const startedMeterValues = OCPP20ServiceUtils.buildTransactionStartedMeterValues( + chargingStation, + transactionId + ) + const response = await OCPP20ServiceUtils.sendTransactionEvent( + chargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transactionId, + { + idToken: + idTag != null ? { idToken: idTag, type: OCPP20IdTokenEnumType.ISO14443 } : undefined, + ...(startedMeterValues.length > 0 && { meterValue: startedMeterValues }), + } + ) + return { + accepted: + response.idTokenInfo == null || + response.idTokenInfo.status === OCPP20AuthorizationStatusEnumType.Accepted, + } + } + default: + throw new OCPPError( + ErrorType.INTERNAL_ERROR, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `startTransactionOnConnector: unsupported OCPP version ${chargingStation.stationInfo?.ocppVersion}` + ) + } +} + +/** + * Stops a transaction on a specific connector using the appropriate OCPP version handler. + * @param chargingStation - Target charging station + * @param connectorId - Connector ID to stop the transaction on + * @param reason - Optional reason for stopping the transaction + * @returns Result indicating whether the stop was accepted + */ +export const stopTransactionOnConnector = async ( + chargingStation: ChargingStation, + connectorId: number, + reason?: StopTransactionReason +): Promise => { + switch (chargingStation.stationInfo?.ocppVersion) { + case OCPPVersion.VERSION_16: { + const response = await OCPP16ServiceUtils.stopTransactionOnConnector( + chargingStation, + connectorId, + reason + ) + return { accepted: response.idTagInfo?.status === AuthorizationStatus.ACCEPTED } + } + case OCPPVersion.VERSION_20: + case OCPPVersion.VERSION_201: { + const evseId = chargingStation.getEvseIdByConnectorId(connectorId) + if (evseId == null) { + logger.warn( + `${chargingStation.logPrefix()} stopTransactionOnConnector: cannot resolve EVSE ID for connector ${connectorId.toString()}, skipping` + ) + return { accepted: false } + } + const { stoppedReason, triggerReason } = mapStopReasonToOCPP20(reason) + const response = await OCPP20ServiceUtils.requestStopTransaction( + chargingStation, + connectorId, + evseId, + triggerReason, + stoppedReason + ) + return { + accepted: + response.idTokenInfo == null || + response.idTokenInfo.status === OCPP20AuthorizationStatusEnumType.Accepted, + } + } + default: + throw new OCPPError( + ErrorType.INTERNAL_ERROR, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `stopTransactionOnConnector: unsupported OCPP version ${chargingStation.stationInfo?.ocppVersion}` + ) + } +} + +/** + * Stops all running transactions on all connectors of a charging station. + * @param chargingStation - Target charging station + * @param reason - Optional reason for stopping the transactions + */ +export const stopRunningTransactions = async ( + chargingStation: ChargingStation, + reason?: StopTransactionReason +): Promise => { + switch (chargingStation.stationInfo?.ocppVersion) { + case OCPPVersion.VERSION_16: { + for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors(true)) { + if (connectorStatus.transactionStarted === true) { + await OCPP16ServiceUtils.stopTransactionOnConnector(chargingStation, connectorId, reason) + } + } + break + } + case OCPPVersion.VERSION_20: + case OCPPVersion.VERSION_201: { + const { stoppedReason, triggerReason } = mapStopReasonToOCPP20(reason) + await OCPP20ServiceUtils.stopAllTransactions(chargingStation, triggerReason, stoppedReason) + break + } + default: + logger.warn( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${chargingStation.logPrefix()} stopRunningTransactions: unsupported OCPP version ${chargingStation.stationInfo?.ocppVersion}, no transactions stopped` + ) + } +} + +/** + * Starts periodic meter value updates for a connector during an active transaction. + * @param chargingStation - Target charging station + * @param connectorId - Connector ID to start meter value updates for + * @param interval - Meter value sampling interval in milliseconds + */ +export const startUpdatedMeterValues = ( + chargingStation: ChargingStation, + connectorId: number, + interval: number +): void => { + switch (chargingStation.stationInfo?.ocppVersion) { + case OCPPVersion.VERSION_16: + OCPP16ServiceUtils.startUpdatedMeterValues(chargingStation, connectorId, interval) + break + case OCPPVersion.VERSION_20: + case OCPPVersion.VERSION_201: + OCPP20ServiceUtils.startUpdatedMeterValues(chargingStation, connectorId, interval) + break + default: + logger.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${chargingStation.logPrefix()} startUpdatedMeterValues: unsupported OCPP version ${chargingStation.stationInfo?.ocppVersion}` + ) + } +} + +/** + * Stops periodic meter value updates for a connector. + * @param chargingStation - Target charging station + * @param connectorId - Connector ID to stop meter value updates for + */ +export const stopUpdatedMeterValues = ( + chargingStation: ChargingStation, + connectorId: number +): void => { + switch (chargingStation.stationInfo?.ocppVersion) { + case OCPPVersion.VERSION_16: + OCPP16ServiceUtils.stopUpdatedMeterValues(chargingStation, connectorId) + break + case OCPPVersion.VERSION_20: + case OCPPVersion.VERSION_201: + OCPP20ServiceUtils.stopUpdatedMeterValues(chargingStation, connectorId) + break + default: + break + } +} + +/** + * Flushes queued transaction event messages for all connectors on an OCPP 2.0 charging station. + * @param chargingStation - Target charging station + */ +export const flushQueuedTransactionMessages = async ( + chargingStation: ChargingStation +): Promise => { + switch (chargingStation.stationInfo?.ocppVersion) { + case OCPPVersion.VERSION_16: + break + case OCPPVersion.VERSION_20: + case OCPPVersion.VERSION_201: + for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors()) { + if ((connectorStatus.transactionEventQueue?.length ?? 0) > 0) { + await OCPP20ServiceUtils.sendQueuedTransactionEvents(chargingStation, connectorId).catch( + (error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} flushQueuedTransactionMessages: Error flushing queued TransactionEvents:`, + error + ) + } + ) + } + } + break + default: + break + } +} diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index 5c3857fc..37ac2898 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -10,7 +10,6 @@ import type { StopTransactionReason } from '../../types/index.js' import { type ChargingStation, getConfigurationKey } from '../../charging-station/index.js' import { BaseError, OCPPError } from '../../exception/index.js' import { - AuthorizationStatus, ChargePointErrorCode, ChargingStationEvents, type ConfigurationKeyType, @@ -34,24 +33,19 @@ import { type OCPP16SampledValue, type OCPP16StatusNotificationRequest, OCPP16StopTransactionReason, - OCPP20AuthorizationStatusEnumType, type OCPP20ConnectorStatusEnumType, - OCPP20IdTokenEnumType, type OCPP20MeterValue, OCPP20ReasonEnumType, type OCPP20SampledValue, type OCPP20StatusNotificationRequest, - OCPP20TransactionEventEnumType, OCPP20TriggerReasonEnumType, OCPPVersion, RequestCommand, type SampledValue, type SampledValueTemplate, StandardParametersKey, - type StartTransactionResult, type StatusNotificationRequest, type StatusNotificationResponse, - type StopTransactionResult, } from '../../types/index.js' import { ACElectricUtils, @@ -59,7 +53,6 @@ import { convertToFloat, convertToInt, DCElectricUtils, - generateUUID, getRandomFloatFluctuatedRounded, getRandomFloatRounded, handleFileException, @@ -92,6 +85,12 @@ interface SingleValueMeasurandData { value: number } +/** + * Builds a StatusNotification request payload for the appropriate OCPP version. + * @param chargingStation - Target charging station + * @param commandParams - Status notification parameters including connector ID and status + * @returns Formatted StatusNotification request payload + */ export const buildStatusNotificationRequest = ( chargingStation: ChargingStation, commandParams: StatusNotificationRequest @@ -136,6 +135,13 @@ export const buildStatusNotificationRequest = ( } } +/** + * Sends a StatusNotification request and updates the connector status locally. + * @param chargingStation - Target charging station + * @param commandParams - Status notification parameters including connector ID and status + * @param options - Optional settings to control whether the request is actually sent + * @param options.send - Whether to actually send the status notification + */ export const sendAndSetConnectorStatus = async ( chargingStation: ChargingStation, commandParams: StatusNotificationRequest, @@ -163,6 +169,12 @@ export const sendAndSetConnectorStatus = async ( }) } +/** + * Restores a connector status to Reserved or Available based on its current state. + * @param chargingStation - Target charging station + * @param connectorId - Connector ID to restore + * @param connectorStatus - Current connector status to evaluate + */ export const restoreConnectorStatus = async ( chargingStation: ChargingStation, connectorId: number, @@ -184,6 +196,11 @@ export const restoreConnectorStatus = async ( } } +/** + * Maps an OCPP 1.6 or generic stop transaction reason to OCPP 2.0 stopped and trigger reasons. + * @param reason - Stop transaction reason to map + * @returns Object containing the OCPP 2.0 stoppedReason and triggerReason + */ export const mapStopReasonToOCPP20 = ( reason?: StopTransactionReason ): { @@ -252,216 +269,6 @@ export const mapStopReasonToOCPP20 = ( } } -export const startTransactionOnConnector = async ( - chargingStation: ChargingStation, - connectorId: number, - idTag?: string -): Promise => { - switch (chargingStation.stationInfo?.ocppVersion) { - case OCPPVersion.VERSION_16: { - const { OCPP16ServiceUtils } = await import('./1.6/OCPP16ServiceUtils.js') - const response = await OCPP16ServiceUtils.startTransactionOnConnector( - chargingStation, - connectorId, - idTag - ) - return { accepted: response.idTagInfo.status === AuthorizationStatus.ACCEPTED } - } - case OCPPVersion.VERSION_20: - case OCPPVersion.VERSION_201: { - const { OCPP20ServiceUtils } = await import('./2.0/OCPP20ServiceUtils.js') - const connectorStatus = chargingStation.getConnectorStatus(connectorId) - let transactionId = connectorStatus?.transactionId as string | undefined - if (transactionId == null) { - transactionId = generateUUID() - if (connectorStatus != null) { - connectorStatus.transactionId = transactionId - } - OCPP20ServiceUtils.resetTransactionSequenceNumber(chargingStation, connectorId) - } - const startedMeterValues = OCPP20ServiceUtils.buildTransactionStartedMeterValues( - chargingStation, - transactionId - ) - const response = await OCPP20ServiceUtils.sendTransactionEvent( - chargingStation, - OCPP20TransactionEventEnumType.Started, - OCPP20TriggerReasonEnumType.Authorized, - connectorId, - transactionId, - { - idToken: - idTag != null ? { idToken: idTag, type: OCPP20IdTokenEnumType.ISO14443 } : undefined, - ...(startedMeterValues.length > 0 && { meterValue: startedMeterValues }), - } - ) - return { - accepted: - response.idTokenInfo == null || - response.idTokenInfo.status === OCPP20AuthorizationStatusEnumType.Accepted, - } - } - default: - throw new OCPPError( - ErrorType.INTERNAL_ERROR, - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `startTransactionOnConnector: unsupported OCPP version ${chargingStation.stationInfo?.ocppVersion}` - ) - } -} - -export const stopTransactionOnConnector = async ( - chargingStation: ChargingStation, - connectorId: number, - reason?: StopTransactionReason -): Promise => { - switch (chargingStation.stationInfo?.ocppVersion) { - case OCPPVersion.VERSION_16: { - const { OCPP16ServiceUtils } = await import('./1.6/OCPP16ServiceUtils.js') - const response = await OCPP16ServiceUtils.stopTransactionOnConnector( - chargingStation, - connectorId, - reason - ) - return { accepted: response.idTagInfo?.status === AuthorizationStatus.ACCEPTED } - } - case OCPPVersion.VERSION_20: - case OCPPVersion.VERSION_201: { - const { OCPP20ServiceUtils } = await import('./2.0/OCPP20ServiceUtils.js') - const evseId = chargingStation.getEvseIdByConnectorId(connectorId) - if (evseId == null) { - logger.warn( - `${chargingStation.logPrefix()} stopTransactionOnConnector: cannot resolve EVSE ID for connector ${connectorId.toString()}, skipping` - ) - return { accepted: false } - } - const { stoppedReason, triggerReason } = mapStopReasonToOCPP20(reason) - const response = await OCPP20ServiceUtils.requestStopTransaction( - chargingStation, - connectorId, - evseId, - triggerReason, - stoppedReason - ) - return { - accepted: - response.idTokenInfo == null || - response.idTokenInfo.status === OCPP20AuthorizationStatusEnumType.Accepted, - } - } - default: - throw new OCPPError( - ErrorType.INTERNAL_ERROR, - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `stopTransactionOnConnector: unsupported OCPP version ${chargingStation.stationInfo?.ocppVersion}` - ) - } -} - -export const stopRunningTransactions = async ( - chargingStation: ChargingStation, - reason?: StopTransactionReason -): Promise => { - switch (chargingStation.stationInfo?.ocppVersion) { - case OCPPVersion.VERSION_16: { - const { OCPP16ServiceUtils } = await import('./1.6/OCPP16ServiceUtils.js') - // Sequential — OCPP 1.6 behavior - for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors(true)) { - if (connectorStatus.transactionStarted === true) { - await OCPP16ServiceUtils.stopTransactionOnConnector(chargingStation, connectorId, reason) - } - } - break - } - case OCPPVersion.VERSION_20: - case OCPPVersion.VERSION_201: { - const { OCPP20ServiceUtils } = await import('./2.0/OCPP20ServiceUtils.js') - const { stoppedReason, triggerReason } = mapStopReasonToOCPP20(reason) - await OCPP20ServiceUtils.stopAllTransactions(chargingStation, triggerReason, stoppedReason) - break - } - default: - logger.warn( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${chargingStation.logPrefix()} stopRunningTransactions: unsupported OCPP version ${chargingStation.stationInfo?.ocppVersion}, no transactions stopped` - ) - } -} - -export const startUpdatedMeterValues = async ( - chargingStation: ChargingStation, - connectorId: number, - interval: number -): Promise => { - switch (chargingStation.stationInfo?.ocppVersion) { - case OCPPVersion.VERSION_16: { - const { OCPP16ServiceUtils } = await import('./1.6/OCPP16ServiceUtils.js') - OCPP16ServiceUtils.startUpdatedMeterValues(chargingStation, connectorId, interval) - break - } - case OCPPVersion.VERSION_20: - case OCPPVersion.VERSION_201: { - const { OCPP20ServiceUtils } = await import('./2.0/OCPP20ServiceUtils.js') - OCPP20ServiceUtils.startUpdatedMeterValues(chargingStation, connectorId, interval) - break - } - default: - logger.error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${chargingStation.logPrefix()} OCPPServiceUtils.startUpdatedMeterValues: unsupported OCPP version ${chargingStation.stationInfo?.ocppVersion}` - ) - } -} - -export const stopUpdatedMeterValues = async ( - chargingStation: ChargingStation, - connectorId: number -): Promise => { - switch (chargingStation.stationInfo?.ocppVersion) { - case OCPPVersion.VERSION_16: { - const { OCPP16ServiceUtils } = await import('./1.6/OCPP16ServiceUtils.js') - OCPP16ServiceUtils.stopUpdatedMeterValues(chargingStation, connectorId) - break - } - case OCPPVersion.VERSION_20: - case OCPPVersion.VERSION_201: { - const { OCPP20ServiceUtils } = await import('./2.0/OCPP20ServiceUtils.js') - OCPP20ServiceUtils.stopUpdatedMeterValues(chargingStation, connectorId) - break - } - default: - break - } -} - -export const flushQueuedTransactionMessages = async ( - chargingStation: ChargingStation -): Promise => { - switch (chargingStation.stationInfo?.ocppVersion) { - case OCPPVersion.VERSION_16: - break - case OCPPVersion.VERSION_20: - case OCPPVersion.VERSION_201: { - const { OCPP20ServiceUtils } = await import('./2.0/OCPP20ServiceUtils.js') - for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors()) { - if ((connectorStatus.transactionEventQueue?.length ?? 0) > 0) { - await OCPP20ServiceUtils.sendQueuedTransactionEvents(chargingStation, connectorId).catch( - (error: unknown) => { - logger.error( - `${chargingStation.logPrefix()} OCPPServiceUtils.flushQueuedTransactionMessages: Error flushing queued TransactionEvents:`, - error - ) - } - ) - } - } - break - } - default: - break - } -} - const checkConnectorStatusTransition = ( chargingStation: ChargingStation, connectorId: number, @@ -522,6 +329,11 @@ const checkConnectorStatusTransition = ( return transitionAllowed } +/** + * Converts Ajv validation errors to the corresponding OCPP error type. + * @param errors - Array of Ajv validation error objects + * @returns OCPP ErrorType corresponding to the validation failure + */ export const ajvErrorsToErrorType = (errors: ErrorObject[] | null | undefined): ErrorType => { if (isNotEmptyArray(errors)) { for (const error of errors) { @@ -540,6 +352,10 @@ export const ajvErrorsToErrorType = (errors: ErrorObject[] | null | undefined): return ErrorType.FORMAT_VIOLATION } +/** + * Recursively converts Date values to ISO 8601 strings within a JSON-compatible object. + * @param object - Object whose Date properties will be converted in-place + */ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters export const convertDateToISOString = (object: T): void => { for (const [key, value] of Object.entries(object as Record)) { @@ -1367,11 +1183,25 @@ const buildCurrentMeasurandValue = ( } } +/** + * Builds an empty MeterValue with no sampled values and the current timestamp. + * @returns Empty MeterValue object + */ export const buildEmptyMeterValue = (): MeterValue => ({ sampledValue: [], timestamp: new Date(), }) +/** + * Builds a complete MeterValue with all configured measurands for a transaction. + * @param chargingStation - Target charging station + * @param transactionId - Active transaction identifier + * @param interval - Meter value sampling interval in milliseconds + * @param measurandsKey - Configuration key for the sampled measurands list + * @param context - Meter value reading context + * @param debug - Enable debug logging for measurand validation + * @returns Populated MeterValue object + */ export const buildMeterValue = ( chargingStation: ChargingStation, transactionId: number | string | undefined, @@ -1795,6 +1625,13 @@ export const buildMeterValue = ( } } +/** + * Builds a MeterValue for the end of a transaction with the final energy register value. + * @param chargingStation - Target charging station + * @param connectorId - Connector ID associated with the transaction + * @param meterStop - Final meter reading in Wh at transaction end + * @returns MeterValue containing the transaction end energy reading + */ export const buildTransactionEndMeterValue = ( chargingStation: ChargingStation, connectorId: number, @@ -1900,6 +1737,16 @@ const isMeasurandSupported = (measurand: MeterValueMeasurand): boolean => { return supportedMeasurands.includes(measurand as string) } +/** + * Retrieves the sampled value template matching the given measurand and phase from configuration. + * @param chargingStation - Target charging station + * @param connectorId - Connector ID to look up templates for + * @param measurandsKey - Configuration key containing the list of sampled measurands + * @param measurand - Meter value measurand to match + * @param evseId - Optional EVSE ID for OCPP 2.0 template lookup + * @param phase - Optional phase to match in the template + * @returns Matching sampled value template, or undefined if not found + */ export const getSampledValueTemplate = ( chargingStation: ChargingStation, connectorId: number, @@ -2210,228 +2057,213 @@ const getMeasurandDefaultUnit = ( } /** - * Utility class providing core OCPP (Open Charge Point Protocol) service functionality - * and common operations across all OCPP versions and protocol implementations. - * - * This class serves as the foundation for OCPP protocol handling, providing: - * - JSON schema-based payload validation using AJV (Another JSON Schema Validator) - * - Common OCPP operations like connector status management and transaction handling - * - Utility functions for meter value processing and ID tag authorization - * - Shared functionality between OCPP 1.6 and OCPP 2.0+ implementations - * - * Key Features: - * - **Schema Validation**: Centralized JSON schema loading and validation functions - * - **Protocol Agnostic**: Provides utilities that work across OCPP versions - * - **Transaction Management**: Utilities for transaction lifecycle and meter values - * - **Status Management**: Connector and charging station status operations - * - **Static Interface**: All functionality exposed as static methods for easy access - * - * Usage Pattern: - * This class is typically used by other OCPP service classes (incoming request services, - * response services) to perform common operations and validation. It acts as a shared - * utility layer that prevents code duplication across OCPP version-specific implementations. - * @see {@link parseJsonSchemaFile} Core JSON schema parsing functionality - * @see {@link validateIncomingRequestPayload} Payload validation methods in service classes - * @see {@link validateResponsePayload} Payload validation methods in service classes + * 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 */ +export function createPayloadValidatorMap ( + configs: [Command, { schemaPath: string }][], + options: { + methodName: string + moduleName: string + ocppVersion: OCPPVersion + schemaDir: string + }, + ajvInstance: Ajv +): Map> { + return new Map>( + configs.map(([command, config]) => { + const fullSchemaPath = `${options.schemaDir}/${config.schemaPath}` + const schema = parseJsonSchemaFile( + fullSchemaPath, + options.ocppVersion, + options.moduleName, + options.methodName + ) + return [command, ajvInstance.compile(schema)] + }) + ) +} -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class OCPPServiceUtils { - protected constructor () { - // This is intentional - } - - /** - * 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( - configs: [Command, { schemaPath: string }][], - options: { - methodName: string - moduleName: string - ocppVersion: OCPPVersion - schemaDir: string - }, - ajvInstance: Ajv - ): Map> { - return new Map>( - configs.map(([command, config]) => { - const fullSchemaPath = `${options.schemaDir}/${config.schemaPath}` - const schema = OCPPServiceUtils.parseJsonSchemaFile( - fullSchemaPath, - options.ocppVersion, - options.moduleName, - options.methodName - ) - return [command, ajvInstance.compile(schema)] - }) +/** + * @param chargingStation - Target charging station + * @param ocppCommand - OCPP command triggering the validation + * @param connectorId - Connector ID to validate + * @returns Whether the connector ID is valid (>= 0) + */ +export function isConnectorIdValid ( + chargingStation: ChargingStation, + ocppCommand: IncomingRequestCommand, + connectorId: number +): boolean { + if (connectorId < 0) { + logger.error( + `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId.toString()}` ) + return false } + return true +} - public static isConnectorIdValid ( - chargingStation: ChargingStation, - ocppCommand: IncomingRequestCommand, - connectorId: number - ): boolean { - if (connectorId < 0) { - logger.error( - `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId.toString()}` - ) - return false - } +/** + * @param chargingStation - Target charging station + * @param command - Incoming request command to check + * @returns Whether the command is supported by the station configuration + */ +export function isIncomingRequestCommandSupported ( + chargingStation: ChargingStation, + command: IncomingRequestCommand +): boolean { + const isIncomingRequestCommand = + Object.values(IncomingRequestCommand).includes(command) + if ( + isIncomingRequestCommand && + chargingStation.stationInfo?.commandsSupport?.incomingCommands == null + ) { return true + } else if ( + isIncomingRequestCommand && + chargingStation.stationInfo?.commandsSupport?.incomingCommands[command] != null + ) { + return chargingStation.stationInfo.commandsSupport.incomingCommands[command] } + logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`) + return false +} - public static isIncomingRequestCommandSupported ( - chargingStation: ChargingStation, - command: IncomingRequestCommand - ): boolean { - const isIncomingRequestCommand = - Object.values(IncomingRequestCommand).includes(command) - if ( - isIncomingRequestCommand && - chargingStation.stationInfo?.commandsSupport?.incomingCommands == null - ) { - return true - } else if ( - isIncomingRequestCommand && - chargingStation.stationInfo?.commandsSupport?.incomingCommands[command] != null - ) { - return chargingStation.stationInfo.commandsSupport.incomingCommands[command] - } - logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`) - return false +/** + * @param chargingStation - Target charging station + * @param messageTrigger - Message trigger to check + * @returns Whether the trigger is supported by the station configuration + */ +export function isMessageTriggerSupported ( + chargingStation: ChargingStation, + messageTrigger: MessageTrigger +): boolean { + const isMessageTrigger = (Object.values(MessageTrigger) as MessageTrigger[]).includes( + messageTrigger + ) + if (isMessageTrigger && chargingStation.stationInfo?.messageTriggerSupport == null) { + return true + } else if ( + isMessageTrigger && + chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger] != null + ) { + return chargingStation.stationInfo.messageTriggerSupport[messageTrigger] } + logger.error( + `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'` + ) + return false +} - public static isMessageTriggerSupported ( - chargingStation: ChargingStation, - messageTrigger: MessageTrigger - ): boolean { - const isMessageTrigger = (Object.values(MessageTrigger) as MessageTrigger[]).includes( - messageTrigger - ) - if (isMessageTrigger && chargingStation.stationInfo?.messageTriggerSupport == null) { - return true - } else if ( - isMessageTrigger && - chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger] != null - ) { - return chargingStation.stationInfo.messageTriggerSupport[messageTrigger] - } - logger.error( - `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'` - ) - return false +/** + * @param chargingStation - Target charging station + * @param command - Outgoing request command to check + * @returns Whether the command is supported by the station configuration + */ +export function isRequestCommandSupported ( + chargingStation: ChargingStation, + command: RequestCommand +): boolean { + const isRequestCommand = Object.values(RequestCommand).includes(command) + if (isRequestCommand && chargingStation.stationInfo?.commandsSupport?.outgoingCommands == null) { + return true + } else if ( + isRequestCommand && + chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command] != null + ) { + return chargingStation.stationInfo.commandsSupport.outgoingCommands[command] } + logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`) + return false +} - public static isRequestCommandSupported ( - chargingStation: ChargingStation, - command: RequestCommand - ): boolean { - const isRequestCommand = Object.values(RequestCommand).includes(command) - if ( - isRequestCommand && - chargingStation.stationInfo?.commandsSupport?.outgoingCommands == null - ) { - return true - } else if ( - isRequestCommand && - chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command] != null - ) { - return chargingStation.stationInfo.commandsSupport.outgoingCommands[command] - } - logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`) - return false - } +/** + * Configuration for a single payload validator. + * @param schemaPath - Path to the JSON schema file + * @returns Configuration object for payload validator creation + */ +export const PayloadValidatorConfig = (schemaPath: string) => + ({ + schemaPath, + }) as const - /** - * 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) => - ({ - schemaPath, - }) 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 - * @returns Options object for payload validator creation - */ - public static readonly PayloadValidatorOptions = ( - ocppVersion: OCPPVersion, - schemaDir: string, - moduleName: string, - methodName: string - ) => - ({ - methodName, - moduleName, - ocppVersion, - schemaDir, - }) as const - - /** - * Parses and loads a JSON schema file for OCPP payload validation. - * Handles file reading and JSON parsing 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 - * @throws {NodeJS.ErrnoException} If the schema file cannot be read or parsed - */ - protected static parseJsonSchemaFile( - relativePath: string, - ocppVersion: OCPPVersion, - moduleName?: string, - methodName?: string - ): JSONSchemaType { - const baseDir = dirname(fileURLToPath(import.meta.url)) - // Primary: resolve from file directory (production esbuild bundle) - const primaryPath = join(baseDir, relativePath) +/** + * 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 + */ +export const PayloadValidatorOptions = ( + ocppVersion: OCPPVersion, + schemaDir: string, + moduleName: string, + methodName: string +) => + ({ + methodName, + moduleName, + ocppVersion, + schemaDir, + }) as const + +/** + * Parses and loads a JSON schema file for OCPP payload validation. + * Handles file reading and JSON parsing 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 + * @throws {NodeJS.ErrnoException} If the schema file cannot be read or parsed + */ +export function parseJsonSchemaFile ( + relativePath: string, + ocppVersion: OCPPVersion, + moduleName?: string, + methodName?: string +): JSONSchemaType { + const baseDir = dirname(fileURLToPath(import.meta.url)) + // Primary: resolve from file directory (production esbuild bundle) + const primaryPath = join(baseDir, relativePath) + try { + return JSON.parse(readFileSync(primaryPath, 'utf8')) as JSONSchemaType + } catch (primaryError) { + // Fallback: resolve from source root (development/test with tsx) + const fallbackPath = join(baseDir, '..', '..', relativePath) try { - return JSON.parse(readFileSync(primaryPath, 'utf8')) as JSONSchemaType - } catch (primaryError) { - // Fallback: resolve from source root (development/test with tsx) - const fallbackPath = join(baseDir, '..', '..', relativePath) - try { - return JSON.parse(readFileSync(fallbackPath, 'utf8')) as JSONSchemaType - } catch { - handleFileException( - primaryPath, - FileType.JsonSchema, - primaryError as NodeJS.ErrnoException, - OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName) - ) - // handleFileException throws by default; this satisfies the compiler - throw primaryError - } + return JSON.parse(readFileSync(fallbackPath, 'utf8')) as JSONSchemaType + } catch { + handleFileException( + primaryPath, + FileType.JsonSchema, + primaryError as NodeJS.ErrnoException, + ocppServiceUtilsLogPrefix(ocppVersion, moduleName, methodName) + ) + // handleFileException throws by default; this satisfies the compiler + throw primaryError } } +} - private static readonly logPrefix = ( - ocppVersion: OCPPVersion, - moduleName?: string, - methodName?: string - ): string => { - const logMsg = - isNotEmptyString(moduleName) && isNotEmptyString(methodName) - ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:` - : ` OCPP ${ocppVersion} |` - return logPrefix(logMsg) - } +const ocppServiceUtilsLogPrefix = ( + ocppVersion: OCPPVersion, + moduleName?: string, + methodName?: string +): string => { + const logMsg = + isNotEmptyString(moduleName) && isNotEmptyString(methodName) + ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:` + : ` OCPP ${ocppVersion} |` + return logPrefix(logMsg) } diff --git a/src/charging-station/ocpp/auth/cache/index.ts b/src/charging-station/ocpp/auth/cache/index.ts deleted file mode 100644 index 5161b803..00000000 --- a/src/charging-station/ocpp/auth/cache/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { InMemoryAuthCache } from './InMemoryAuthCache.js' diff --git a/src/charging-station/ocpp/auth/factories/index.ts b/src/charging-station/ocpp/auth/factories/index.ts deleted file mode 100644 index 2df7e0bf..00000000 --- a/src/charging-station/ocpp/auth/factories/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AuthComponentFactory } from './AuthComponentFactory.js' diff --git a/src/charging-station/ocpp/auth/index.ts b/src/charging-station/ocpp/auth/index.ts index b6aa93ee..14cb45c7 100644 --- a/src/charging-station/ocpp/auth/index.ts +++ b/src/charging-station/ocpp/auth/index.ts @@ -80,4 +80,5 @@ export { // Utils // ============================================================================ -export * from './utils/index.js' +export { AuthValidators } from './utils/AuthValidators.js' +export { AuthConfigValidator } from './utils/ConfigValidator.js' diff --git a/src/charging-station/ocpp/auth/utils/index.ts b/src/charging-station/ocpp/auth/utils/index.ts deleted file mode 100644 index 5af98ac2..00000000 --- a/src/charging-station/ocpp/auth/utils/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Authentication utilities module - * - * Provides validation and helper functions for authentication operations - */ - -export { AuthValidators } from './AuthValidators.js' -export { AuthConfigValidator } from './ConfigValidator.js' diff --git a/src/charging-station/ocpp/index.ts b/src/charging-station/ocpp/index.ts index c1d71766..ed86ff63 100644 --- a/src/charging-station/ocpp/index.ts +++ b/src/charging-station/ocpp/index.ts @@ -11,6 +11,12 @@ export { OCPPAuthServiceFactory } from './auth/index.js' export { isIdTagAuthorized } from './IdTagAuthorization.js' export { OCPPIncomingRequestService } from './OCPPIncomingRequestService.js' export { OCPPRequestService } from './OCPPRequestService.js' +export { + flushQueuedTransactionMessages, + startTransactionOnConnector, + stopRunningTransactions, + stopTransactionOnConnector, +} from './OCPPServiceOperations.js' export { buildMeterValue, buildStatusNotificationRequest, diff --git a/tests/charging-station/ocpp/1.6/OCPP16ServiceUtils.test.ts b/tests/charging-station/ocpp/1.6/OCPP16ServiceUtils.test.ts index 6036e547..40cb28d4 100644 --- a/tests/charging-station/ocpp/1.6/OCPP16ServiceUtils.test.ts +++ b/tests/charging-station/ocpp/1.6/OCPP16ServiceUtils.test.ts @@ -10,7 +10,11 @@ import assert from 'node:assert/strict' import { afterEach, describe, it } from 'node:test' import { OCPP16ServiceUtils } from '../../../../src/charging-station/ocpp/1.6/OCPP16ServiceUtils.js' -import { buildTransactionEndMeterValue } from '../../../../src/charging-station/ocpp/OCPPServiceUtils.js' +import { + buildTransactionEndMeterValue, + isIncomingRequestCommandSupported, + isRequestCommandSupported, +} from '../../../../src/charging-station/ocpp/OCPPServiceUtils.js' import { type OCPP16ChargingProfile, OCPP16ChargingProfileKindType, @@ -568,10 +572,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => { }) // Act - const result = OCPP16ServiceUtils.isRequestCommandSupported( - station, - OCPP16RequestCommand.HEARTBEAT - ) + const result = isRequestCommandSupported(station, OCPP16RequestCommand.HEARTBEAT) // Assert assert.strictEqual(result, true) @@ -590,10 +591,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => { }, }) - const result = OCPP16ServiceUtils.isRequestCommandSupported( - station, - OCPP16RequestCommand.HEARTBEAT - ) + const result = isRequestCommandSupported(station, OCPP16RequestCommand.HEARTBEAT) assert.strictEqual(result, true) }) @@ -611,10 +609,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => { }, }) - const result = OCPP16ServiceUtils.isRequestCommandSupported( - station, - OCPP16RequestCommand.HEARTBEAT - ) + const result = isRequestCommandSupported(station, OCPP16RequestCommand.HEARTBEAT) assert.strictEqual(result, false) }) @@ -629,10 +624,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => { stationInfo: { commandsSupport: undefined }, }) - const result = OCPP16ServiceUtils.isIncomingRequestCommandSupported( - station, - OCPP16IncomingRequestCommand.RESET - ) + const result = isIncomingRequestCommandSupported(station, OCPP16IncomingRequestCommand.RESET) assert.strictEqual(result, true) }) @@ -649,10 +641,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => { }, }) - const result = OCPP16ServiceUtils.isIncomingRequestCommandSupported( - station, - OCPP16IncomingRequestCommand.RESET - ) + const result = isIncomingRequestCommandSupported(station, OCPP16IncomingRequestCommand.RESET) assert.strictEqual(result, true) }) @@ -669,7 +658,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => { }, }) - const result = OCPP16ServiceUtils.isIncomingRequestCommandSupported( + const result = isIncomingRequestCommandSupported( station, OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION ) diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts index e5f95c39..95c0a558 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts @@ -23,7 +23,7 @@ import { OCPP20ServiceUtils, } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js' import { OCPP20VariableManager } from '../../../../src/charging-station/ocpp/2.0/OCPP20VariableManager.js' -import { startUpdatedMeterValues } from '../../../../src/charging-station/ocpp/OCPPServiceUtils.js' +import { startUpdatedMeterValues } from '../../../../src/charging-station/ocpp/OCPPServiceOperations.js' import { OCPPError } from '../../../../src/exception/index.js' import { AttributeEnumType, @@ -2010,7 +2010,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { await describe('startUpdatedMeterValues', async () => { await it('should not start OCPP 2.0 timer for OCPP 1.6 stations via dispatch', async t => { - await withMockTimers(t, ['setInterval'], async () => { + await withMockTimers(t, ['setInterval'], () => { const { station: ocpp16Station } = createMockChargingStation({ baseName: TEST_CHARGING_STATION_BASE_NAME, connectorsCount: 1, @@ -2019,7 +2019,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { }, }) - await startUpdatedMeterValues(ocpp16Station, 1, 60000) + startUpdatedMeterValues(ocpp16Station, 1, 60000) const connectorStatus = ocpp16Station.getConnectorStatus(1) assert.strictEqual(connectorStatus?.transactionUpdatedMeterValuesSetInterval, undefined) diff --git a/tests/charging-station/ocpp/OCPPServiceUtils-StopTransaction.test.ts b/tests/charging-station/ocpp/OCPPServiceUtils-StopTransaction.test.ts index ffa3dc92..73a6f368 100644 --- a/tests/charging-station/ocpp/OCPPServiceUtils-StopTransaction.test.ts +++ b/tests/charging-station/ocpp/OCPPServiceUtils-StopTransaction.test.ts @@ -12,11 +12,11 @@ import type { MockChargingStationOptions } from '../helpers/StationHelpers.js' import { flushQueuedTransactionMessages, - mapStopReasonToOCPP20, startTransactionOnConnector, stopRunningTransactions, stopTransactionOnConnector, -} from '../../../src/charging-station/ocpp/OCPPServiceUtils.js' +} from '../../../src/charging-station/ocpp/OCPPServiceOperations.js' +import { mapStopReasonToOCPP20 } from '../../../src/charging-station/ocpp/OCPPServiceUtils.js' import { type OCPP20TransactionEventRequest, OCPPVersion, diff --git a/tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts b/tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts index dbb50344..236284dd 100644 --- a/tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts +++ b/tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts @@ -18,7 +18,7 @@ import type { ChargingStation } from '../../../src/charging-station/index.js' import { ajvErrorsToErrorType, convertDateToISOString, - OCPPServiceUtils, + isConnectorIdValid, } from '../../../src/charging-station/ocpp/OCPPServiceUtils.js' import { ErrorType, IncomingRequestCommand, type JsonType } from '../../../src/types/index.js' import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js' @@ -136,7 +136,7 @@ await describe('OCPPServiceUtils — pure functions', async () => { await describe('OCPPServiceUtils.isConnectorIdValid', async () => { await it('should return true for connector ID greater than zero', () => { - const result = OCPPServiceUtils.isConnectorIdValid( + const result = isConnectorIdValid( makeStationMock(), IncomingRequestCommand.REMOTE_START_TRANSACTION, 1 @@ -145,7 +145,7 @@ await describe('OCPPServiceUtils — pure functions', async () => { }) await it('should return true for connector ID zero', () => { - const result = OCPPServiceUtils.isConnectorIdValid( + const result = isConnectorIdValid( makeStationMock(), IncomingRequestCommand.REMOTE_START_TRANSACTION, 0 @@ -154,7 +154,7 @@ await describe('OCPPServiceUtils — pure functions', async () => { }) await it('should return false for negative connector ID', () => { - const result = OCPPServiceUtils.isConnectorIdValid( + const result = isConnectorIdValid( makeStationMock(), IncomingRequestCommand.REMOTE_START_TRANSACTION, -1 diff --git a/tests/charging-station/ocpp/OCPPServiceUtils-validation.test.ts b/tests/charging-station/ocpp/OCPPServiceUtils-validation.test.ts index c2c15570..af26b0d0 100644 --- a/tests/charging-station/ocpp/OCPPServiceUtils-validation.test.ts +++ b/tests/charging-station/ocpp/OCPPServiceUtils-validation.test.ts @@ -14,7 +14,11 @@ import { afterEach, describe, it } from 'node:test' import type { ChargingStation } from '../../../src/charging-station/index.js' -import { OCPPServiceUtils } from '../../../src/charging-station/ocpp/OCPPServiceUtils.js' +import { + isIncomingRequestCommandSupported, + isMessageTriggerSupported, + isRequestCommandSupported, +} from '../../../src/charging-station/ocpp/OCPPServiceUtils.js' import { IncomingRequestCommand, MessageTrigger, RequestCommand } from '../../../src/types/index.js' import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js' @@ -45,10 +49,7 @@ await describe('OCPPServiceUtils — command/trigger validation', async () => { }, }) - const result = OCPPServiceUtils.isIncomingRequestCommandSupported( - station, - IncomingRequestCommand.RESET - ) + const result = isIncomingRequestCommandSupported(station, IncomingRequestCommand.RESET) assert.strictEqual(result, true) }) @@ -62,10 +63,7 @@ await describe('OCPPServiceUtils — command/trigger validation', async () => { }, }) - const result = OCPPServiceUtils.isIncomingRequestCommandSupported( - station, - IncomingRequestCommand.RESET - ) + const result = isIncomingRequestCommandSupported(station, IncomingRequestCommand.RESET) assert.strictEqual(result, false) }) @@ -73,10 +71,7 @@ await describe('OCPPServiceUtils — command/trigger validation', async () => { await it('should return true when commandsSupport is undefined', () => { const station = makeStationMock({}) - const result = OCPPServiceUtils.isIncomingRequestCommandSupported( - station, - IncomingRequestCommand.RESET - ) + const result = isIncomingRequestCommandSupported(station, IncomingRequestCommand.RESET) assert.strictEqual(result, true) }) @@ -86,10 +81,7 @@ await describe('OCPPServiceUtils — command/trigger validation', async () => { commandsSupport: {}, }) - const result = OCPPServiceUtils.isIncomingRequestCommandSupported( - station, - IncomingRequestCommand.RESET - ) + const result = isIncomingRequestCommandSupported(station, IncomingRequestCommand.RESET) assert.strictEqual(result, true) }) @@ -105,7 +97,7 @@ await describe('OCPPServiceUtils — command/trigger validation', async () => { }, }) - const result = OCPPServiceUtils.isRequestCommandSupported(station, RequestCommand.HEARTBEAT) + const result = isRequestCommandSupported(station, RequestCommand.HEARTBEAT) assert.strictEqual(result, true) }) @@ -119,7 +111,7 @@ await describe('OCPPServiceUtils — command/trigger validation', async () => { }, }) - const result = OCPPServiceUtils.isRequestCommandSupported(station, RequestCommand.HEARTBEAT) + const result = isRequestCommandSupported(station, RequestCommand.HEARTBEAT) assert.strictEqual(result, false) }) @@ -127,7 +119,7 @@ await describe('OCPPServiceUtils — command/trigger validation', async () => { await it('should return true when commandsSupport is undefined', () => { const station = makeStationMock({}) - const result = OCPPServiceUtils.isRequestCommandSupported(station, RequestCommand.HEARTBEAT) + const result = isRequestCommandSupported(station, RequestCommand.HEARTBEAT) assert.strictEqual(result, true) }) @@ -137,7 +129,7 @@ await describe('OCPPServiceUtils — command/trigger validation', async () => { commandsSupport: {}, }) - const result = OCPPServiceUtils.isRequestCommandSupported(station, RequestCommand.HEARTBEAT) + const result = isRequestCommandSupported(station, RequestCommand.HEARTBEAT) assert.strictEqual(result, true) }) @@ -151,7 +143,7 @@ await describe('OCPPServiceUtils — command/trigger validation', async () => { }, }) - const result = OCPPServiceUtils.isMessageTriggerSupported(station, MessageTrigger.Heartbeat) + const result = isMessageTriggerSupported(station, MessageTrigger.Heartbeat) assert.strictEqual(result, true) }) @@ -163,7 +155,7 @@ await describe('OCPPServiceUtils — command/trigger validation', async () => { }, }) - const result = OCPPServiceUtils.isMessageTriggerSupported(station, MessageTrigger.Heartbeat) + const result = isMessageTriggerSupported(station, MessageTrigger.Heartbeat) assert.strictEqual(result, false) }) @@ -171,7 +163,7 @@ await describe('OCPPServiceUtils — command/trigger validation', async () => { await it('should return true when messageTriggerSupport is undefined', () => { const station = makeStationMock({}) - const result = OCPPServiceUtils.isMessageTriggerSupported(station, MessageTrigger.Heartbeat) + const result = isMessageTriggerSupported(station, MessageTrigger.Heartbeat) assert.strictEqual(result, true) }) @@ -181,7 +173,7 @@ await describe('OCPPServiceUtils — command/trigger validation', async () => { messageTriggerSupport: null, }) - const result = OCPPServiceUtils.isMessageTriggerSupported(station, MessageTrigger.Heartbeat) + const result = isMessageTriggerSupported(station, MessageTrigger.Heartbeat) assert.strictEqual(result, true) }) -- 2.43.0