From a5376f40c7ed130804cf6bd1b99183474d7599b9 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 1 Apr 2026 17:13:47 +0200 Subject: [PATCH] refactor(ocpp): extract connector status operations into dedicated module Move sendAndSetConnectorStatus, restoreConnectorStatus, and checkConnectorStatusTransition into OCPPConnectorStatusOperations.ts to enforce the operations/utils semantic separation without creating circular dependencies. The new module depends only on Constants (leaf modules), not on version-specific ServiceUtils. --- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 2 +- .../ocpp/1.6/OCPP16RequestService.ts | 7 +- .../ocpp/1.6/OCPP16ResponseService.ts | 7 +- .../ocpp/1.6/OCPP16ServiceUtils.ts | 2 +- .../ocpp/2.0/OCPP20IncomingRequestService.ts | 6 +- .../ocpp/2.0/OCPP20ResponseService.ts | 7 +- .../ocpp/2.0/OCPP20ServiceUtils.ts | 2 +- .../ocpp/OCPPConnectorStatusOperations.ts | 119 ++++++++++++++++++ src/charging-station/ocpp/OCPPServiceUtils.ts | 110 ---------------- src/charging-station/ocpp/index.ts | 6 +- ... => OCPPConnectorStatusOperations.test.ts} | 6 +- 11 files changed, 141 insertions(+), 133 deletions(-) create mode 100644 src/charging-station/ocpp/OCPPConnectorStatusOperations.ts rename tests/charging-station/ocpp/{OCPPServiceUtils-connectorStatus.test.ts => OCPPConnectorStatusOperations.test.ts} (97%) diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index c387da4b..2c5eb356 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -116,6 +116,7 @@ import { truncateId, } from '../../../utils/index.js' import { AuthContext } from '../auth/index.js' +import { sendAndSetConnectorStatus } from '../OCPPConnectorStatusOperations.js' import { OCPPConstants } from '../OCPPConstants.js' import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js' import { isIdTagAuthorized } from '../OCPPServiceOperations.js' @@ -125,7 +126,6 @@ import { isConnectorIdValid, isIncomingRequestCommandSupported, isMessageTriggerSupported, - sendAndSetConnectorStatus, } from '../OCPPServiceUtils.js' import { OCPP16Constants } from './OCPP16Constants.js' import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js' diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index 5b850139..eeb4d938 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -17,12 +17,9 @@ import { type RequestParams, } from '../../../types/index.js' import { generateUUID, logger } from '../../../utils/index.js' +import { sendAndSetConnectorStatus } from '../OCPPConnectorStatusOperations.js' import { OCPPRequestService } from '../OCPPRequestService.js' -import { - createPayloadValidatorMap, - isRequestCommandSupported, - sendAndSetConnectorStatus, -} from '../OCPPServiceUtils.js' +import { createPayloadValidatorMap, isRequestCommandSupported } from '../OCPPServiceUtils.js' import { OCPP16Constants } from './OCPP16Constants.js' import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js' diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index 9ecc9d29..b60ce5e9 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -35,13 +35,12 @@ import { type ResponseHandler, } from '../../../types/index.js' import { Constants, convertToInt, logger, truncateId } from '../../../utils/index.js' -import { OCPPResponseService } from '../OCPPResponseService.js' import { - createPayloadValidatorMap, - isRequestCommandSupported, restoreConnectorStatus, sendAndSetConnectorStatus, -} from '../OCPPServiceUtils.js' +} from '../OCPPConnectorStatusOperations.js' +import { OCPPResponseService } from '../OCPPResponseService.js' +import { createPayloadValidatorMap, isRequestCommandSupported } from '../OCPPServiceUtils.js' import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js' const moduleName = 'OCPP16ResponseService' diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index b7840fdb..9894752b 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -59,6 +59,7 @@ import { truncateId, } from '../../../utils/index.js' import { mapOCPP16Status, OCPPAuthServiceFactory } from '../auth/index.js' +import { sendAndSetConnectorStatus } from '../OCPPConnectorStatusOperations.js' import { buildEmptyMeterValue, buildMeterValue, @@ -66,7 +67,6 @@ import { createPayloadConfigs, getSampledValueTemplate, PayloadValidatorOptions, - sendAndSetConnectorStatus, } from '../OCPPServiceUtils.js' import { OCPP16Constants } from './OCPP16Constants.js' diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index 90df5d0c..14cc212b 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -147,13 +147,15 @@ import { mapOCPP20TokenType, OCPPAuthServiceFactory, } from '../auth/index.js' +import { + restoreConnectorStatus, + sendAndSetConnectorStatus, +} from '../OCPPConnectorStatusOperations.js' import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js' import { buildMeterValue, createPayloadValidatorMap, isIncomingRequestCommandSupported, - restoreConnectorStatus, - sendAndSetConnectorStatus, } from '../OCPPServiceUtils.js' import { type GetInstalledCertificatesResult, diff --git a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts index 4e3c92df..cbe58fa2 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts @@ -41,12 +41,9 @@ import { type ResponseHandler, } from '../../../types/index.js' import { convertToDate, logger } from '../../../utils/index.js' +import { sendAndSetConnectorStatus } from '../OCPPConnectorStatusOperations.js' import { OCPPResponseService } from '../OCPPResponseService.js' -import { - createPayloadValidatorMap, - isRequestCommandSupported, - sendAndSetConnectorStatus, -} from '../OCPPServiceUtils.js' +import { createPayloadValidatorMap, isRequestCommandSupported } from '../OCPPServiceUtils.js' import { OCPP20IncomingRequestService } from './OCPP20IncomingRequestService.js' import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' const moduleName = 'OCPP20ResponseService' diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index fd1e0b2f..940d1c73 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -52,11 +52,11 @@ import { mapOCPP20TokenType, OCPPAuthServiceFactory, } from '../auth/index.js' +import { sendAndSetConnectorStatus } from '../OCPPConnectorStatusOperations.js' import { buildMeterValue, createPayloadConfigs, PayloadValidatorOptions, - sendAndSetConnectorStatus, } from '../OCPPServiceUtils.js' import { OCPP20VariableManager } from './OCPP20VariableManager.js' diff --git a/src/charging-station/ocpp/OCPPConnectorStatusOperations.ts b/src/charging-station/ocpp/OCPPConnectorStatusOperations.ts new file mode 100644 index 00000000..d07e2809 --- /dev/null +++ b/src/charging-station/ocpp/OCPPConnectorStatusOperations.ts @@ -0,0 +1,119 @@ +import { type ChargingStation } from '../../charging-station/index.js' +import { OCPPError } from '../../exception/index.js' +import { + ChargingStationEvents, + type ConnectorStatus, + ConnectorStatusEnum, + ErrorType, + OCPPVersion, + RequestCommand, + type StatusNotificationRequest, + type StatusNotificationResponse, +} from '../../types/index.js' +import { logger } from '../../utils/index.js' +import { OCPP16Constants } from './1.6/OCPP16Constants.js' +import { OCPP20Constants } from './2.0/OCPP20Constants.js' + +/** + * 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, + options?: { send: boolean } +): Promise => { + options = { send: true, ...options } + const params = commandParams as Record + const connectorId = params.connectorId as number + const status = (params.connectorStatus ?? params.status) as ConnectorStatusEnum + const connectorStatus = chargingStation.getConnectorStatus(connectorId) + if (connectorStatus == null) { + return + } + if (options.send) { + checkConnectorStatusTransition(chargingStation, connectorId, status) + await chargingStation.ocppRequestService.requestHandler< + StatusNotificationRequest, + StatusNotificationResponse + >(chargingStation, RequestCommand.STATUS_NOTIFICATION, commandParams) + } + connectorStatus.status = status + chargingStation.emitChargingStationEvent(ChargingStationEvents.connectorStatusChanged, { + connectorId, + ...connectorStatus, + }) +} + +/** + * 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, + connectorStatus: ConnectorStatus | undefined +): Promise => { + if ( + connectorStatus?.reservation != null && + connectorStatus.status !== ConnectorStatusEnum.Reserved + ) { + await sendAndSetConnectorStatus(chargingStation, { + connectorId, + status: ConnectorStatusEnum.Reserved, + } as unknown as StatusNotificationRequest) + } else if (connectorStatus?.status !== ConnectorStatusEnum.Available) { + await sendAndSetConnectorStatus(chargingStation, { + connectorId, + status: ConnectorStatusEnum.Available, + } as unknown as StatusNotificationRequest) + } +} + +const checkConnectorStatusTransition = ( + chargingStation: ChargingStation, + connectorId: number, + status: ConnectorStatusEnum +): boolean => { + const fromStatus = chargingStation.getConnectorStatus(connectorId)?.status + let chargingStationTransitions: readonly { from?: ConnectorStatusEnum; to: ConnectorStatusEnum }[] + let connectorTransitions: readonly { from?: ConnectorStatusEnum; to: ConnectorStatusEnum }[] + switch (chargingStation.stationInfo?.ocppVersion) { + case OCPPVersion.VERSION_16: + chargingStationTransitions = OCPP16Constants.ChargePointStatusChargingStationTransitions + connectorTransitions = OCPP16Constants.ChargePointStatusConnectorTransitions + break + case OCPPVersion.VERSION_20: + case OCPPVersion.VERSION_201: + chargingStationTransitions = OCPP20Constants.ChargingStationStatusTransitions + connectorTransitions = OCPP20Constants.ConnectorStatusTransitions + break + default: + throw new OCPPError( + ErrorType.INTERNAL_ERROR, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`, + RequestCommand.STATUS_NOTIFICATION + ) + } + const transitions = connectorId === 0 ? chargingStationTransitions : connectorTransitions + const transitionAllowed = transitions.some( + transition => transition.from === fromStatus && transition.to === status + ) + if (!transitionAllowed) { + logger.warn( + `${chargingStation.logPrefix()} OCPP ${ + chargingStation.stationInfo.ocppVersion + } connector id ${connectorId.toString()} status transition from '${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + chargingStation.getConnectorStatus(connectorId)?.status + }' to '${status}' is not allowed` + ) + } + return transitionAllowed +} diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index b9996f4a..495e8445 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -11,10 +11,8 @@ import type { StopTransactionReason } from '../../types/index.js' import { type ChargingStation, getConfigurationKey } from '../../charging-station/index.js' import { BaseError, OCPPError } from '../../exception/index.js' import { - ChargingStationEvents, type ConfigurationKeyType, type ConnectorStatus, - ConnectorStatusEnum, CurrentType, ErrorType, FileType, @@ -41,8 +39,6 @@ import { type SampledValue, type SampledValueTemplate, StandardParametersKey, - type StatusNotificationRequest, - type StatusNotificationResponse, } from '../../types/index.js' import { ACElectricUtils, @@ -61,8 +57,6 @@ import { min, roundTo, } from '../../utils/index.js' -import { OCPP16Constants } from './1.6/OCPP16Constants.js' -import { OCPP20Constants } from './2.0/OCPP20Constants.js' import { OCPPConstants } from './OCPPConstants.js' const moduleName = 'OCPPServiceUtils' @@ -92,67 +86,6 @@ interface SingleValueMeasurandData { value: number } -/** - * 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, - options?: { send: boolean } -): Promise => { - options = { send: true, ...options } - const params = commandParams as Record - const connectorId = params.connectorId as number - const status = (params.connectorStatus ?? params.status) as ConnectorStatusEnum - const connectorStatus = chargingStation.getConnectorStatus(connectorId) - if (connectorStatus == null) { - return - } - if (options.send) { - checkConnectorStatusTransition(chargingStation, connectorId, status) - await chargingStation.ocppRequestService.requestHandler< - StatusNotificationRequest, - StatusNotificationResponse - >(chargingStation, RequestCommand.STATUS_NOTIFICATION, commandParams) - } - connectorStatus.status = status - chargingStation.emitChargingStationEvent(ChargingStationEvents.connectorStatusChanged, { - connectorId, - ...connectorStatus, - }) -} - -/** - * 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, - connectorStatus: ConnectorStatus | undefined -): Promise => { - if ( - connectorStatus?.reservation != null && - connectorStatus.status !== ConnectorStatusEnum.Reserved - ) { - await sendAndSetConnectorStatus(chargingStation, { - connectorId, - status: ConnectorStatusEnum.Reserved, - } as unknown as StatusNotificationRequest) - } else if (connectorStatus?.status !== ConnectorStatusEnum.Available) { - await sendAndSetConnectorStatus(chargingStation, { - connectorId, - status: ConnectorStatusEnum.Available, - } as unknown as StatusNotificationRequest) - } -} - /** * 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 @@ -226,49 +159,6 @@ export const mapStopReasonToOCPP20 = ( } } -const checkConnectorStatusTransition = ( - chargingStation: ChargingStation, - connectorId: number, - status: ConnectorStatusEnum -): boolean => { - const fromStatus = chargingStation.getConnectorStatus(connectorId)?.status - let chargingStationTransitions: readonly { from?: ConnectorStatusEnum; to: ConnectorStatusEnum }[] - let connectorTransitions: readonly { from?: ConnectorStatusEnum; to: ConnectorStatusEnum }[] - switch (chargingStation.stationInfo?.ocppVersion) { - case OCPPVersion.VERSION_16: - chargingStationTransitions = OCPP16Constants.ChargePointStatusChargingStationTransitions - connectorTransitions = OCPP16Constants.ChargePointStatusConnectorTransitions - break - case OCPPVersion.VERSION_20: - case OCPPVersion.VERSION_201: - chargingStationTransitions = OCPP20Constants.ChargingStationStatusTransitions - connectorTransitions = OCPP20Constants.ConnectorStatusTransitions - break - default: - throw new OCPPError( - ErrorType.INTERNAL_ERROR, - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`, - RequestCommand.STATUS_NOTIFICATION - ) - } - const transitions = connectorId === 0 ? chargingStationTransitions : connectorTransitions - const transitionAllowed = transitions.some( - transition => transition.from === fromStatus && transition.to === status - ) - if (!transitionAllowed) { - logger.warn( - `${chargingStation.logPrefix()} OCPP ${ - chargingStation.stationInfo.ocppVersion - } connector id ${connectorId.toString()} status transition from '${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - chargingStation.getConnectorStatus(connectorId)?.status - }' to '${status}' is not allowed` - ) - } - return transitionAllowed -} - /** * Converts Ajv validation errors to the corresponding OCPP error type. * @param errors - Array of Ajv validation error objects diff --git a/src/charging-station/ocpp/index.ts b/src/charging-station/ocpp/index.ts index 4d084d96..d570f06e 100644 --- a/src/charging-station/ocpp/index.ts +++ b/src/charging-station/ocpp/index.ts @@ -8,6 +8,10 @@ export { OCPP20ResponseService } from './2.0/OCPP20ResponseService.js' export { buildTransactionEvent, OCPP20ServiceUtils } from './2.0/OCPP20ServiceUtils.js' export { OCPP20VariableManager } from './2.0/OCPP20VariableManager.js' export { OCPPAuthServiceFactory } from './auth/index.js' +export { + restoreConnectorStatus, + sendAndSetConnectorStatus, +} from './OCPPConnectorStatusOperations.js' export { OCPPConstants } from './OCPPConstants.js' export { OCPPIncomingRequestService } from './OCPPIncomingRequestService.js' export { OCPPRequestService } from './OCPPRequestService.js' @@ -20,4 +24,4 @@ export { stopRunningTransactions, stopTransactionOnConnector, } from './OCPPServiceOperations.js' -export { buildMeterValue, sendAndSetConnectorStatus } from './OCPPServiceUtils.js' +export { buildMeterValue } from './OCPPServiceUtils.js' diff --git a/tests/charging-station/ocpp/OCPPServiceUtils-connectorStatus.test.ts b/tests/charging-station/ocpp/OCPPConnectorStatusOperations.test.ts similarity index 97% rename from tests/charging-station/ocpp/OCPPServiceUtils-connectorStatus.test.ts rename to tests/charging-station/ocpp/OCPPConnectorStatusOperations.test.ts index 7a6f0802..3aad3422 100644 --- a/tests/charging-station/ocpp/OCPPServiceUtils-connectorStatus.test.ts +++ b/tests/charging-station/ocpp/OCPPConnectorStatusOperations.test.ts @@ -1,5 +1,5 @@ /** - * @file Tests for OCPPServiceUtils connector status management + * @file Tests for OCPPConnectorStatusOperations * @description Verifies sendAndSetConnectorStatus and restoreConnectorStatus functions * * Covers: @@ -17,7 +17,7 @@ import type { MockChargingStationOptions } from '../helpers/StationHelpers.js' import { restoreConnectorStatus, sendAndSetConnectorStatus, -} from '../../../src/charging-station/ocpp/OCPPServiceUtils.js' +} from '../../../src/charging-station/ocpp/OCPPConnectorStatusOperations.js' import { ConnectorStatusEnum, type OCPP16StatusNotificationRequest, @@ -44,7 +44,7 @@ function createStationWithRequestHandler (opts?: Partial { +await describe('OCPPConnectorStatusOperations', async () => { afterEach(() => { standardCleanup() }) -- 2.53.0