From 4dd00cea03b44026542d2914fdd0f48afa84fb26 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sun, 12 Jul 2026 21:54:24 +0200 Subject: [PATCH] refactor(ocpp): remove as-unknown-as double-casts from request/response dispatch (#2003) Give each builder its precise input contract instead of laundering casts through phantom generics or JsonType: - buildRequestPayload drops its unsound generic and returns honest JsonType; each dispatch branch narrows with a single cast at a genuine JsonType boundary. - Builder inputs (StatusNotificationOptions, OCPP20TransactionEventOptions, SignCertificateOptions) are JsonObject subtypes; StatusNotification build validates connectorStatus via the shared isOCPP20ConnectorStatus guard. - Handler bridges (toRequestHandler/toResponseHandler) use a single cast that preserves the handler's async identity, fixing an unawaited-handler regression from a plain wrapper defeating isAsyncFunction. - CAT-C string-to-enum narrowing becomes a type guard / typed Record. Closes #1968 --- src/charging-station/ChargingStation.ts | 9 ++-- .../ChargingStationWorkerBroadcastChannel.ts | 11 ++-- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 21 ++++---- .../ocpp/1.6/OCPP16RequestService.ts | 21 ++++---- .../ocpp/1.6/OCPP16ResponseService.ts | 5 +- .../ocpp/1.6/OCPP16ServiceUtils.ts | 6 +-- .../ocpp/2.0/OCPP20IncomingRequestService.ts | 52 +++++++++++-------- .../ocpp/2.0/OCPP20RequestService.ts | 31 ++++++----- .../ocpp/2.0/OCPP20ResponseService.ts | 3 +- .../ocpp/2.0/OCPP20ServiceUtils.ts | 32 ++++++++---- .../ocpp/OCPPConnectorStatusOperations.ts | 28 +++++----- .../ocpp/OCPPIncomingRequestService.ts | 8 +-- .../ocpp/OCPPResponseService.ts | 2 +- .../ocpp/auth/types/AuthTypes.ts | 2 +- src/types/index.ts | 1 + src/types/ocpp/2.0/Transaction.ts | 2 +- src/types/ocpp/Requests.ts | 20 ++++++- ...P20ServiceUtils-StatusNotification.test.ts | 37 +++++++++++++ .../OCPPConnectorStatusOperations.test.ts | 21 +++----- 19 files changed, 188 insertions(+), 124 deletions(-) diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 95234b40..cae04dd6 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -58,7 +58,6 @@ import { type Response, StandardParametersKey, type Status, - type StatusNotificationRequest, type StopTransactionReason, SupervisionUrlDistribution, SupportedFeatureProfiles, @@ -345,7 +344,7 @@ export class ChargingStation extends EventEmitter { { connectorId: reservation.connectorId, status: ConnectorStatusEnum.Reserved, - } as unknown as StatusNotificationRequest, + }, { send: reservation.connectorId !== 0 } ) } @@ -1038,7 +1037,7 @@ export class ChargingStation extends EventEmitter { { connectorId: reservation.connectorId, status: ConnectorStatusEnum.Available, - } as unknown as StatusNotificationRequest, + }, { send: reservation.connectorId !== 0 } ) delete connectorStatus.reservation @@ -2846,7 +2845,7 @@ export class ChargingStation extends EventEmitter { connectorId, ...(evseId != null && { evseId }), status: getBootConnectorStatus(this, connectorId, connectorStatus), - } as unknown as StatusNotificationRequest) + }) } if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) { await this.ocppRequestService.requestHandler< @@ -2911,7 +2910,7 @@ export class ChargingStation extends EventEmitter { connectorId, ...(evseId != null && { evseId }), status: ConnectorStatusEnum.Unavailable, - } as unknown as StatusNotificationRequest) + }) delete connectorStatus.status } } diff --git a/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts b/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts index b3f1f751..b4aa7aa1 100644 --- a/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts +++ b/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts @@ -36,7 +36,7 @@ import { ResponseStatus, StandardParametersKey, type StartTransactionResponse, - type StatusNotificationRequest, + type StatusNotificationOptions, type StopTransactionRequest, type StopTransactionResponse, } from '../../types/index.js' @@ -471,16 +471,13 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne `${this.chargingStation.logPrefix()} ${moduleName}.handleStatusNotification: 'connectorId' field is required` ) } - const payload = requestPayload as Record - if (payload.connectorStatus == null && payload.status == null) { + const options = requestPayload as StatusNotificationOptions + if (options.connectorStatus == null && options.status == null) { throw new BaseError( `${this.chargingStation.logPrefix()} ${moduleName}.handleStatusNotification: 'connectorStatus' or 'status' field is required` ) } - await sendAndSetConnectorStatus( - this.chargingStation, - requestPayload as unknown as StatusNotificationRequest - ) + await sendAndSetConnectorStatus(this.chargingStation, options) } private async handleStopTransaction ( diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 5b124e53..208cabf0 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -85,7 +85,6 @@ import { OCPP16StandardParametersKey, type OCPP16StartTransactionRequest, type OCPP16StartTransactionResponse, - type OCPP16StatusNotificationRequest, type OCPP16StatusNotificationResponse, OCPP16StopTransactionReason, OCPP16SupportedFeatureProfiles, @@ -103,6 +102,7 @@ import { type ResetRequest, type SetChargingProfileRequest, type SetChargingProfileResponse, + type StatusNotificationOptions, type UnlockConnectorRequest, type UnlockConnectorResponse, } from '../../../types/index.js' @@ -639,14 +639,14 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService( + .requestHandler( chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, { connectorId, status: chargingStation.getConnectorStatus(connectorId) ?.status as OCPP16ChargePointStatus, - } as unknown as OCPP16StatusNotificationRequest, + }, { triggerMessage: true, } @@ -655,16 +655,13 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService( + .requestHandler( chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, { connectorId, status: connectorStatus.status as OCPP16ChargePointStatus, - } as unknown as OCPP16StatusNotificationRequest, + }, { triggerMessage: true, } @@ -945,7 +942,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService(chargingStation, commandName, commandParams) + : this.buildRequestPayload(chargingStation, commandName, commandParams) const messageId = generateUUID() logger.debug( `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Sending '${commandName}' request with message ID '${messageId}'` @@ -114,7 +114,7 @@ export class OCPP16RequestService extends OCPPRequestService { await sendAndSetConnectorStatus(chargingStation, { connectorId: (commandParams as OCPP16StartTransactionRequest).connectorId, status: OCPP16ChargePointStatus.Preparing, - } as OCPP16StatusNotificationRequest) + }) break } const response = (await this.sendMessage( @@ -158,12 +158,11 @@ export class OCPP16RequestService extends OCPPRequestService { * @param commandParams - Optional parameters provided by the caller for payload construction * @returns The fully constructed and validated request payload ready for transmission */ - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - private buildRequestPayload( + private buildRequestPayload ( chargingStation: ChargingStation, commandName: OCPP16RequestCommand, commandParams?: JsonType - ): Request { + ): JsonType { let connectorId: number | undefined let energyActiveImportRegister: number logger.debug( @@ -175,9 +174,9 @@ export class OCPP16RequestService extends OCPPRequestService { case OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION: case OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION: case OCPP16RequestCommand.METER_VALUES: - return commandParams as unknown as Request + return commandParams ?? OCPP16Constants.OCPP_REQUEST_EMPTY case OCPP16RequestCommand.HEARTBEAT: - return OCPP16Constants.OCPP_REQUEST_EMPTY as unknown as Request + return OCPP16Constants.OCPP_REQUEST_EMPTY } assertIsJsonObject( commandParams, @@ -193,7 +192,7 @@ export class OCPP16RequestService extends OCPPRequestService { return { idTag: OCPP16Constants.OCPP_DEFAULT_IDTAG, ...params, - } as unknown as Request + } case OCPP16RequestCommand.START_TRANSACTION: return { idTag: OCPP16Constants.OCPP_DEFAULT_IDTAG, @@ -215,12 +214,12 @@ export class OCPP16RequestService extends OCPPRequestService { )?.reservationId, }), ...params, - } as unknown as Request + } case OCPP16RequestCommand.STATUS_NOTIFICATION: return OCPP16ServiceUtils.buildStatusNotificationRequest({ errorCode: ChargePointErrorCode.NO_ERROR, ...params, - } as OCPP16StatusNotificationRequest) as unknown as Request + } as OCPP16StatusNotificationRequest) case OCPP16RequestCommand.STOP_TRANSACTION: ;(chargingStation.stationInfo?.transactionDataMeterValues === true || OCPP16ServiceUtils.isSigningEnabled(chargingStation)) && @@ -272,7 +271,7 @@ export class OCPP16RequestService extends OCPPRequestService { timestamp: new Date(), ...(transactionData != null && { transactionData }), ...params, - } as unknown as Request + } } default: { // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index 3a6fa384..61d5e8ee 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -26,7 +26,6 @@ import { OCPP16StandardParametersKey, type OCPP16StartTransactionRequest, type OCPP16StartTransactionResponse, - type OCPP16StatusNotificationRequest, type OCPP16StopTransactionRequest, type OCPP16StopTransactionResponse, OCPPVersion, @@ -503,7 +502,7 @@ export class OCPP16ResponseService extends OCPPResponseService { await sendAndSetConnectorStatus(chargingStation, { connectorId, status: OCPP16ChargePointStatus.Charging, - } as OCPP16StatusNotificationRequest) + }) logger.info( `${chargingStation.logPrefix()} ${moduleName}.handleResponseStartTransaction: Transaction with id ${payload.transactionId.toString()} STARTED on ${ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -589,7 +588,7 @@ export class OCPP16ResponseService extends OCPPResponseService { await sendAndSetConnectorStatus(chargingStation, { connectorId: transactionConnectorId, status: OCPP16ChargePointStatus.Finishing, - } as OCPP16StatusNotificationRequest) + }) } OCPP16ServiceUtils.stopUpdatedMeterValues(chargingStation, transactionConnectorId) if (transactionConnectorStatus != null) { diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 1b137cf0..ba7619b4 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -365,7 +365,7 @@ export class OCPP16ServiceUtils { await sendAndSetConnectorStatus(chargingStation, { connectorId, status: chargePointStatus, - } as OCPP16StatusNotificationRequest) + }) } responses.push(response) } @@ -791,7 +791,7 @@ export class OCPP16ServiceUtils { await sendAndSetConnectorStatus(chargingStation, { connectorId, status: OCPP16ChargePointStatus.Finishing, - } as OCPP16StatusNotificationRequest) + }) const stopResponse = await OCPP16ServiceUtils.stopTransactionOnConnector( chargingStation, connectorId, @@ -954,7 +954,7 @@ export class OCPP16ServiceUtils { await sendAndSetConnectorStatus(chargingStation, { connectorId, status: OCPP16ChargePointStatus.Finishing, - } as OCPP16StatusNotificationRequest) + }) } const rawTransactionId = chargingStation.getConnectorStatus(connectorId)?.transactionId const transactionId = rawTransactionId != null ? convertToInt(rawTransactionId) : undefined diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index 0d614d32..8a60e568 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -107,7 +107,6 @@ import { type OCPP20SetNetworkProfileResponse, type OCPP20SetVariablesRequest, type OCPP20SetVariablesResponse, - type OCPP20StatusNotificationRequest, type OCPP20StatusNotificationResponse, OCPP20TransactionEventEnumType, type OCPP20TriggerMessageRequest, @@ -129,6 +128,7 @@ import { ResetStatusEnumType, SetNetworkProfileStatusEnumType, SetVariableStatusEnumType, + type StatusNotificationOptions, StopTransactionReason, TriggerMessageStatusEnumType, UnlockStatusEnumType, @@ -185,7 +185,7 @@ import { } from './OCPP20CertificateManager.js' import { OCPP20CertSigningRetryManager } from './OCPP20CertSigningRetryManager.js' import { OCPP20Constants } from './OCPP20Constants.js' -import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' +import { isOCPP20ConnectorStatus, OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' import { OCPP20VariableManager } from './OCPP20VariableManager.js' import { getVariableMetadata, VARIABLE_REGISTRY } from './OCPP20VariableRegistry.js' @@ -198,6 +198,21 @@ const moduleName = 'OCPP20IncomingRequestService' // device capability declarations with no spec-defined ceiling. const MAX_ITEMS_PER_REPORT_MESSAGE = 100 as const +// V2GCertificateChain has no InstallCertificateUse counterpart, so it collapses onto V2GRootCertificate. +const getCertificateIdUseToInstallCertificateUse: Readonly< + Record +> = Object.freeze({ + [GetCertificateIdUseEnumType.CSMSRootCertificate]: + InstallCertificateUseEnumType.CSMSRootCertificate, + [GetCertificateIdUseEnumType.ManufacturerRootCertificate]: + InstallCertificateUseEnumType.ManufacturerRootCertificate, + [GetCertificateIdUseEnumType.MORootCertificate]: InstallCertificateUseEnumType.MORootCertificate, + [GetCertificateIdUseEnumType.V2GCertificateChain]: + InstallCertificateUseEnumType.V2GRootCertificate, + [GetCertificateIdUseEnumType.V2GRootCertificate]: + InstallCertificateUseEnumType.V2GRootCertificate, +}) + interface StationInfoReportField { property: 'chargePointModel' | 'chargePointSerialNumber' | 'chargePointVendor' | 'firmwareVersion' variable: OCPP20DeviceInfoVariableName @@ -1580,7 +1595,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { + }).catch((error: unknown) => { logger.error( `${chargingStation.logPrefix()} ${moduleName}.handleConnectorChangeAvailability: Error sending status notification for connector ${connectorId.toString()}:`, error @@ -2163,12 +2178,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { - if (ct === GetCertificateIdUseEnumType.V2GCertificateChain) { - return InstallCertificateUseEnumType.V2GRootCertificate - } - return ct as unknown as InstallCertificateUseEnumType - }) + const filterTypes = certificateType?.map(ct => getCertificateIdUseToInstallCertificateUse[ct]) const methodResult = chargingStation.certificateManager.getInstalledCertificates( chargingStation.stationInfo?.hashId ?? '', @@ -3185,7 +3195,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { + }).catch((error: unknown) => { logger.error( `${chargingStation.logPrefix()} ${moduleName}.sendAllConnectorsStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`, error @@ -3620,7 +3628,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { + }).catch((error: unknown) => { logger.error( `${chargingStation.logPrefix()} ${moduleName}.sendEvseStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`, error @@ -3847,7 +3855,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { + }).catch((error: unknown) => { logger.error( `${chargingStation.logPrefix()} ${moduleName}.sendRestoredAllConnectorsStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`, error @@ -3867,7 +3875,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { + }).catch((error: unknown) => { logger.error( `${chargingStation.logPrefix()} ${moduleName}.sendRestoredEvseStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`, error @@ -4247,14 +4255,14 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService( + .requestHandler( chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, { connectorId, connectorStatus: resolvedStatus, evseId, - } as unknown as OCPP20StatusNotificationRequest, + }, { skipBufferingOnError: true, triggerMessage: true } ) .catch(errorHandler) @@ -4320,14 +4328,14 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService( + .requestHandler( chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, { connectorId: evse.connectorId, connectorStatus: resolvedStatus, evseId: evse.id, - } as unknown as OCPP20StatusNotificationRequest, + }, { skipBufferingOnError: true, triggerMessage: true } ) .catch(errorHandler) diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts index 381f75b4..01ef06b6 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -14,10 +14,10 @@ import { OCPP20RequestCommand, OCPP20RequiredVariableName, type OCPP20SignCertificateRequest, - type OCPP20StatusNotificationRequest, type OCPP20TransactionEventOptions, OCPPVersion, type RequestParams, + type StatusNotificationOptions, } from '../../../types/index.js' import { generateUUID, getErrorMessage, logger } from '../../../utils/index.js' import { OCPPRequestService } from '../OCPPRequestService.js' @@ -28,6 +28,10 @@ import { buildTransactionEvent, OCPP20ServiceUtils } from './OCPP20ServiceUtils. const moduleName = 'OCPP20RequestService' +interface SignCertificateOptions extends JsonObject { + certificateType?: CertificateSigningUseEnumType +} + /** * OCPP 2.0.1 Request Service * @@ -112,7 +116,7 @@ export class OCPP20RequestService extends OCPPRequestService { const requestPayload = params?.rawPayload === true ? (commandParams as RequestType) - : this.buildRequestPayload(chargingStation, commandName, commandParams) + : this.buildRequestPayload(chargingStation, commandName, commandParams) const messageId = generateUUID() logger.debug( `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Sending '${commandName}' request with message ID '${messageId}'` @@ -139,12 +143,11 @@ export class OCPP20RequestService extends OCPPRequestService { throw new OCPPError(ErrorType.NOT_SUPPORTED, errorMsg, commandName, commandParams) } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - private buildRequestPayload( + private buildRequestPayload ( chargingStation: ChargingStation, commandName: OCPP20RequestCommand, commandParams?: JsonType - ): Request { + ): JsonType { logger.debug( `${chargingStation.logPrefix()} ${moduleName}.buildRequestPayload: Building '${commandName}' payload` ) @@ -160,9 +163,9 @@ export class OCPP20RequestService extends OCPPRequestService { case OCPP20RequestCommand.NOTIFY_CUSTOMER_INFORMATION: case OCPP20RequestCommand.NOTIFY_REPORT: case OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION: - return commandParams as unknown as Request + return commandParams ?? OCPP20Constants.OCPP_REQUEST_EMPTY case OCPP20RequestCommand.HEARTBEAT: - return OCPP20Constants.OCPP_RESPONSE_EMPTY as unknown as Request + return OCPP20Constants.OCPP_REQUEST_EMPTY case OCPP20RequestCommand.SIGN_CERTIFICATE: { let csr: string try { @@ -186,26 +189,26 @@ export class OCPP20RequestService extends OCPPRequestService { ) } - const certificateType = (commandParams as JsonObject | undefined)?.certificateType as - CertificateSigningUseEnumType | undefined + const certificateType = (commandParams as SignCertificateOptions | undefined) + ?.certificateType const requestPayload: OCPP20SignCertificateRequest = { csr, ...(certificateType != null && { certificateType }), } - return requestPayload as unknown as Request + return requestPayload } case OCPP20RequestCommand.STATUS_NOTIFICATION: return OCPP20ServiceUtils.buildStatusNotificationRequest( chargingStation, - commandParams as unknown as OCPP20StatusNotificationRequest - ) as unknown as Request + commandParams as StatusNotificationOptions + ) case OCPP20RequestCommand.TRANSACTION_EVENT: return buildTransactionEvent( chargingStation, - commandParams as unknown as OCPP20TransactionEventOptions - ) as unknown as Request + commandParams as OCPP20TransactionEventOptions + ) default: { // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). const errorMsg = `Unsupported OCPP command ${commandName as string} for payload building` diff --git a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts index 9ef4d0b0..bf4c6791 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts @@ -30,7 +30,6 @@ import { type OCPP20SecurityEventNotificationResponse, type OCPP20SignCertificateRequest, type OCPP20SignCertificateResponse, - type OCPP20StatusNotificationRequest, type OCPP20StatusNotificationResponse, OCPP20TransactionEventEnumType, type OCPP20TransactionEventRequest, @@ -444,7 +443,7 @@ export class OCPP20ResponseService extends OCPPResponseService { sendAndSetConnectorStatus(chargingStation, { connectorId, connectorStatus: ConnectorStatusEnum.Occupied, - } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => { + }).catch((error: unknown) => { logger.error( `${chargingStation.logPrefix()} ${moduleName}.handleResponseTransactionEvent: Error sending StatusNotification(Occupied):`, error diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index 1364d9fa..dafd64b8 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -10,7 +10,7 @@ import { OCPP20AuthorizationStatusEnumType, OCPP20ChargingStateEnumType, OCPP20ComponentName, - type OCPP20ConnectorStatusEnumType, + OCPP20ConnectorStatusEnumType, type OCPP20EVSEType, type OCPP20GetVariableResultType, OCPP20IdTokenEnumType, @@ -34,6 +34,7 @@ import { ReasonCodeEnumType, RequestCommand, type StartTransactionResult, + type StatusNotificationOptions, type StopTransactionReason, type StopTransactionResult, type UUIDv4, @@ -71,6 +72,11 @@ import { OCPP20VariableManager } from './OCPP20VariableManager.js' const moduleName = 'OCPP20ServiceUtils' +export const isOCPP20ConnectorStatus = ( + status: ConnectorStatusEnum +): status is OCPP20ConnectorStatusEnumType => + Object.values(OCPP20ConnectorStatusEnumType).some(value => value === status) + export interface RejectionReason { additionalInfo: string reasonCode: ReasonCodeEnumType @@ -126,17 +132,16 @@ export class OCPP20ServiceUtils { /** * @param chargingStation - Target charging station for EVSE resolution - * @param commandParams - Status notification parameters + * @param commandParams - StatusNotification input; `connectorStatus` takes precedence over `status` * @returns Formatted OCPP 2.0.1 StatusNotification request payload + * @throws {OCPPError} When the EVSE id cannot be resolved or the connector status is missing/not a valid OCPP 2.0.1 status */ public static buildStatusNotificationRequest ( chargingStation: ChargingStation, - commandParams: OCPP20StatusNotificationRequest + commandParams: StatusNotificationOptions ): OCPP20StatusNotificationRequest { - const params = commandParams as Record - const connectorId = params.connectorId as number - const connectorStatus = (params.connectorStatus ?? params.status) as ConnectorStatusEnum - const evseId = params.evseId as number | undefined + const { connectorId, evseId } = commandParams + const connectorStatus = commandParams.connectorStatus ?? commandParams.status const resolvedEvseId = evseId ?? chargingStation.getEvseIdByConnectorId(connectorId) if (resolvedEvseId === undefined) { throw new OCPPError( @@ -145,9 +150,16 @@ export class OCPP20ServiceUtils { RequestCommand.STATUS_NOTIFICATION ) } + if (connectorStatus == null || !isOCPP20ConnectorStatus(connectorStatus)) { + throw new OCPPError( + ErrorType.INTERNAL_ERROR, + `Cannot build status notification payload: invalid connector status for connector ${connectorId.toString()}`, + RequestCommand.STATUS_NOTIFICATION + ) + } return { connectorId, - connectorStatus: connectorStatus as OCPP20ConnectorStatusEnumType, + connectorStatus, evseId: resolvedEvseId, timestamp: new Date(), } satisfies OCPP20StatusNotificationRequest @@ -813,7 +825,7 @@ export class OCPP20ServiceUtils { ) const response = await chargingStation.ocppRequestService.requestHandler< - OCPP20TransactionEventRequest, + OCPP20TransactionEventOptions, OCPP20TransactionEventResponse >(chargingStation, OCPP20RequestCommand.TRANSACTION_EVENT, { connectorId, @@ -821,7 +833,7 @@ export class OCPP20ServiceUtils { transactionId, triggerReason, ...options, - } as unknown as OCPP20TransactionEventRequest) + }) return response } catch (error) { diff --git a/src/charging-station/ocpp/OCPPConnectorStatusOperations.ts b/src/charging-station/ocpp/OCPPConnectorStatusOperations.ts index 2ad010eb..68c7f80a 100644 --- a/src/charging-station/ocpp/OCPPConnectorStatusOperations.ts +++ b/src/charging-station/ocpp/OCPPConnectorStatusOperations.ts @@ -5,10 +5,9 @@ import { type ConnectorStatus, ConnectorStatusEnum, ErrorType, - type OCPP16ChargePointErrorCode, OCPPVersion, RequestCommand, - type StatusNotificationRequest, + type StatusNotificationOptions, type StatusNotificationResponse, } from '../../types/index.js' import { logger } from '../../utils/index.js' @@ -18,20 +17,18 @@ 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 commandParams - Cross-version StatusNotification input; `connectorStatus` (OCPP 2.0.1) takes precedence over `status` (OCPP 1.6) * @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, + commandParams: StatusNotificationOptions, 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 errorCode = params.errorCode as OCPP16ChargePointErrorCode | undefined + const { connectorId, errorCode } = commandParams + const status = commandParams.connectorStatus ?? commandParams.status const connectorStatus = chargingStation.getConnectorStatus(connectorId) if (connectorStatus == null) { return @@ -39,7 +36,7 @@ export const sendAndSetConnectorStatus = async ( if (options.send) { checkConnectorStatusTransition(chargingStation, connectorId, status) await chargingStation.ocppRequestService.requestHandler< - StatusNotificationRequest, + StatusNotificationOptions, StatusNotificationResponse >(chargingStation, RequestCommand.STATUS_NOTIFICATION, commandParams) } @@ -70,7 +67,7 @@ export const sendPostTransactionStatus = async ( connectorId, connectorStatus: status, status, - } as unknown as StatusNotificationRequest) + }) } /** @@ -91,19 +88,19 @@ export const restoreConnectorStatus = async ( 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 + status: ConnectorStatusEnum | undefined ): boolean => { const fromStatus = chargingStation.getConnectorStatus(connectorId)?.status let chargingStationTransitions: readonly { from?: ConnectorStatusEnum; to: ConnectorStatusEnum }[] @@ -137,7 +134,10 @@ const checkConnectorStatusTransition = ( } 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` + }' to '${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + status + }' is not allowed` ) } return transitionAllowed diff --git a/src/charging-station/ocpp/OCPPIncomingRequestService.ts b/src/charging-station/ocpp/OCPPIncomingRequestService.ts index 95104f56..55a5319a 100644 --- a/src/charging-station/ocpp/OCPPIncomingRequestService.ts +++ b/src/charging-station/ocpp/OCPPIncomingRequestService.ts @@ -291,11 +291,11 @@ export abstract class OCPPIncomingRequestService< */ protected abstract resetStationState (stationState: TStationState): void - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- bridges contravariant handler signatures into IncomingRequestHandler - protected toRequestHandler

( - handler: (chargingStation: ChargingStation, commandPayload: P) => Promise | R + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- P bridges contravariant handler payload signatures into IncomingRequestHandler + protected toRequestHandler

( + handler: (chargingStation: ChargingStation, commandPayload: P) => JsonType | Promise ): IncomingRequestHandler { - return handler as unknown as IncomingRequestHandler + return handler as IncomingRequestHandler } /** diff --git a/src/charging-station/ocpp/OCPPResponseService.ts b/src/charging-station/ocpp/OCPPResponseService.ts index 34d6c0ea..056ac1c2 100644 --- a/src/charging-station/ocpp/OCPPResponseService.ts +++ b/src/charging-station/ocpp/OCPPResponseService.ts @@ -131,7 +131,7 @@ export abstract class OCPPResponseService { requestPayload: R ) => Promise | void ): ResponseHandler { - return handler as unknown as ResponseHandler + return handler as ResponseHandler } /** diff --git a/src/charging-station/ocpp/auth/types/AuthTypes.ts b/src/charging-station/ocpp/auth/types/AuthTypes.ts index c3ae9766..8e102f3c 100644 --- a/src/charging-station/ocpp/auth/types/AuthTypes.ts +++ b/src/charging-station/ocpp/auth/types/AuthTypes.ts @@ -331,7 +331,7 @@ export const isOCPP16Type = (type: IdentifierType): boolean => { * @returns True if OCPP 2.0.1 type */ export const isOCPP20Type = (type: IdentifierType): boolean => { - return Object.values(OCPP20IdTokenEnumType).includes(type as unknown as OCPP20IdTokenEnumType) + return (Object.values(OCPP20IdTokenEnumType) as string[]).includes(type) } /** diff --git a/src/types/index.ts b/src/types/index.ts index 5dd88ef5..bfb436ee 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -394,6 +394,7 @@ export { type RequestParams, type ResponseCallback, type ResponseType, + type StatusNotificationOptions, type StatusNotificationRequest, } from './ocpp/Requests.js' export { diff --git a/src/types/ocpp/2.0/Transaction.ts b/src/types/ocpp/2.0/Transaction.ts index 1992c88d..daed85f6 100644 --- a/src/types/ocpp/2.0/Transaction.ts +++ b/src/types/ocpp/2.0/Transaction.ts @@ -271,7 +271,7 @@ export interface OCPP20MessageContentType extends JsonObject { * Optional parameters for building and sending TransactionEvent requests. * Aligned with OCPP 2.0.1 TransactionEvent.req optional fields. */ -export interface OCPP20TransactionEventOptions { +export interface OCPP20TransactionEventOptions extends JsonObject { cableMaxCurrent?: number chargingState?: OCPP20ChargingStateEnumType connectorId?: number diff --git a/src/types/ocpp/Requests.ts b/src/types/ocpp/Requests.ts index 28ddadbf..5780ee7d 100644 --- a/src/types/ocpp/Requests.ts +++ b/src/types/ocpp/Requests.ts @@ -1,8 +1,10 @@ import type { ChargingStation } from '../../charging-station/index.js' import type { OCPPError } from '../../exception/index.js' -import type { JsonType } from '../JsonType.js' +import type { JsonObject, JsonType } from '../JsonType.js' +import type { OCPP16ChargePointErrorCode } from './1.6/ChargePointErrorCode.js' import type { OCPP16MeterValuesRequest } from './1.6/MeterValues.js' import type { OCPP20MeterValuesRequest } from './2.0/MeterValues.js' +import type { ConnectorStatusEnum } from './ConnectorStatusEnum.js' import type { MessageType } from './MessageType.js' import { OCPP16DiagnosticsStatus } from './1.6/DiagnosticsStatus.js' @@ -98,6 +100,22 @@ export type MeterValuesRequest = OCPP16MeterValuesRequest | OCPP20MeterValuesReq export type ResponseCallback = (payload: JsonType, requestPayload: JsonType) => void +/** + * Cross-version (OCPP 1.6/2.0.1) input for a StatusNotification request, looser than the per-version + * wire types it is built into. `connectorStatus` (2.0.1) takes precedence over `status` (1.6). + */ +export interface StatusNotificationOptions extends JsonObject { + connectorId: number + connectorStatus?: ConnectorStatusEnum + errorCode?: OCPP16ChargePointErrorCode + evseId?: number + info?: string + status?: ConnectorStatusEnum + timestamp?: Date + vendorErrorCode?: string + vendorId?: string +} + export type StatusNotificationRequest = OCPP16StatusNotificationRequest | OCPP20StatusNotificationRequest diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-StatusNotification.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-StatusNotification.test.ts index 32dcbd3f..8311961c 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-StatusNotification.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-StatusNotification.test.ts @@ -12,9 +12,11 @@ import type { ChargingStation } from '../../../../src/charging-station/index.js' import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js' import { OCPPError } from '../../../../src/exception/index.js' import { + OCPP16ChargePointStatus, OCPP20ConnectorStatusEnumType, type OCPP20StatusNotificationRequest, OCPPVersion, + type StatusNotificationOptions, } from '../../../../src/types/index.js' import { Constants } from '../../../../src/utils/index.js' import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js' @@ -141,5 +143,40 @@ await describe('OCPP20ServiceUtils', async () => { assert.strictEqual(result.connectorStatus, OCPP20ConnectorStatusEnumType.Faulted) }) + + await it('should throw OCPPError when connectorStatus and status are both missing', () => { + const input: StatusNotificationOptions = { + connectorId: 1, + evseId: 1, + } + + assert.throws( + () => { + OCPP20ServiceUtils.buildStatusNotificationRequest(mockStation, input) + }, + (error: unknown) => { + assert.ok(error instanceof OCPPError) + return true + } + ) + }) + + await it('should throw OCPPError when connectorStatus is not a valid OCPP 2.0.1 status', () => { + const input: StatusNotificationOptions = { + connectorId: 1, + connectorStatus: OCPP16ChargePointStatus.Preparing, + evseId: 1, + } + + assert.throws( + () => { + OCPP20ServiceUtils.buildStatusNotificationRequest(mockStation, input) + }, + (error: unknown) => { + assert.ok(error instanceof OCPPError) + return true + } + ) + }) }) }) diff --git a/tests/charging-station/ocpp/OCPPConnectorStatusOperations.test.ts b/tests/charging-station/ocpp/OCPPConnectorStatusOperations.test.ts index d6cebe70..5177612f 100644 --- a/tests/charging-station/ocpp/OCPPConnectorStatusOperations.test.ts +++ b/tests/charging-station/ocpp/OCPPConnectorStatusOperations.test.ts @@ -16,12 +16,7 @@ import { restoreConnectorStatus, sendAndSetConnectorStatus, } from '../../../src/charging-station/ocpp/OCPPConnectorStatusOperations.js' -import { - ConnectorStatusEnum, - type OCPP16StatusNotificationRequest, - type OCPP20StatusNotificationRequest, - OCPPVersion, -} from '../../../src/types/index.js' +import { ConnectorStatusEnum, OCPPVersion } from '../../../src/types/index.js' import { createStationWithRequestHandler, standardCleanup, @@ -40,7 +35,7 @@ await describe('OCPPConnectorStatusOperations', async () => { await sendAndSetConnectorStatus(station, { connectorId: 1, status: ConnectorStatusEnum.Occupied, - } as unknown as OCPP16StatusNotificationRequest) + }) assert.strictEqual(requestHandler.mock.calls.length, 1) assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Occupied) @@ -52,7 +47,7 @@ await describe('OCPPConnectorStatusOperations', async () => { await sendAndSetConnectorStatus(station, { connectorId: 99, status: ConnectorStatusEnum.Occupied, - } as unknown as OCPP16StatusNotificationRequest) + }) assert.strictEqual(requestHandler.mock.calls.length, 0) }) @@ -65,7 +60,7 @@ await describe('OCPPConnectorStatusOperations', async () => { { connectorId: 1, status: ConnectorStatusEnum.Occupied, - } as unknown as OCPP16StatusNotificationRequest, + }, { send: false, } @@ -83,7 +78,7 @@ await describe('OCPPConnectorStatusOperations', async () => { await sendAndSetConnectorStatus(station, { connectorId: 1, status: ConnectorStatusEnum.Unavailable, - } as unknown as OCPP16StatusNotificationRequest) + }) assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Unavailable) }) @@ -96,7 +91,7 @@ await describe('OCPPConnectorStatusOperations', async () => { await sendAndSetConnectorStatus(station, { connectorId: 1, status: ConnectorStatusEnum.Occupied, - } as unknown as OCPP16StatusNotificationRequest) + }) assert.strictEqual(emitSpy.mock.calls.length, 1) }) @@ -110,7 +105,7 @@ await describe('OCPPConnectorStatusOperations', async () => { connectorId: 1, connectorStatus: ConnectorStatusEnum.Occupied, evseId: 1, - } as unknown as OCPP20StatusNotificationRequest) + }) assert.strictEqual(requestHandler.mock.calls.length, 1) assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Occupied) @@ -122,7 +117,7 @@ await describe('OCPPConnectorStatusOperations', async () => { await sendAndSetConnectorStatus(station, { connectorId: 1, status: ConnectorStatusEnum.Occupied, - } as unknown as OCPP16StatusNotificationRequest) + }) assert.strictEqual(requestHandler.mock.calls.length, 1) }) -- 2.53.0