From 0d54c7212b5448e2bb749b4ac029ec528fd8a5b0 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 18 Mar 2026 17:59:56 +0100 Subject: [PATCH] refactor(ocpp): add rawPayload bypass, resolve minimal params in buildRequestPayload, fix test types Add rawPayload option to RequestParams for offline queue replay to explicitly bypass buildRequestPayload instead of heuristic detection. Make buildRequestPayload resolve missing TransactionEvent fields from station context (connectorId from evse, transactionId generation, triggerReason defaults) so the broadcast channel passthrough works. Remove pre-built payload guard from OCPP 1.6 STATUS_NOTIFICATION. Replace Record downgrades in tests with proper OCPP types (Partial, RequestParams). --- .../ocpp/1.6/OCPP16RequestService.ts | 12 ++---- .../ocpp/2.0/OCPP20RequestService.ts | 43 +++++++++++-------- .../ocpp/2.0/OCPP20ServiceUtils.ts | 9 ++-- src/types/ocpp/Requests.ts | 1 + ...uestService-RequestStopTransaction.test.ts | 2 +- ...omingRequestService-TriggerMessage.test.ts | 15 ++++--- ...CPP20ServiceUtils-TransactionEvent.test.ts | 5 ++- 7 files changed, 49 insertions(+), 38 deletions(-) diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index ccfc9d20..9ac47a5e 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -99,11 +99,10 @@ export class OCPP16RequestService extends OCPPRequestService { logger.debug( `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Building request payload for '${commandName}'` ) - const requestPayload = this.buildRequestPayload( - chargingStation, - commandName, - commandParams - ) + const requestPayload = + params?.rawPayload === true + ? (commandParams as RequestType) + : this.buildRequestPayload(chargingStation, commandName, commandParams) const messageId = generateUUID() logger.debug( `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Sending '${commandName}' request with message ID '${messageId}'` @@ -212,9 +211,6 @@ export class OCPP16RequestService extends OCPPRequestService { ...commandParams, } as unknown as Request case OCPP16RequestCommand.STATUS_NOTIFICATION: - if (commandParams.errorCode != null) { - return commandParams as unknown as Request - } return buildStatusNotificationRequest( chargingStation, commandParams.connectorId as number, diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts index a058fdae..40d5b8d3 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -1,22 +1,20 @@ import type { ValidateFunction } from 'ajv' import type { ChargingStation } from '../../../charging-station/index.js' -import type { - ConnectorStatusEnum, - OCPP20TransactionEventEnumType, - OCPP20TriggerReasonEnumType, -} from '../../../types/index.js' import type { OCPP20TransactionEventOptions } from '../../../types/ocpp/2.0/Transaction.js' import type { OCPPResponseService } from '../OCPPResponseService.js' import { OCPPError } from '../../../exception/index.js' import { type CertificateSigningUseEnumType, + type ConnectorStatusEnum, ErrorType, type JsonObject, type JsonType, OCPP20RequestCommand, type OCPP20SignCertificateRequest, + OCPP20TransactionEventEnumType, + OCPP20TriggerReasonEnumType, OCPPVersion, type RequestParams, } from '../../../types/index.js' @@ -110,11 +108,10 @@ export class OCPP20RequestService extends OCPPRequestService { logger.debug( `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Building request payload for '${commandName}'` ) - const requestPayload = this.buildRequestPayload( - chargingStation, - commandName, - commandParams - ) + const requestPayload = + params?.rawPayload === true + ? (commandParams as RequestType) + : this.buildRequestPayload(chargingStation, commandName, commandParams) const messageId = generateUUID() logger.debug( `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Sending '${commandName}' request with message ID '${messageId}'` @@ -208,15 +205,25 @@ export class OCPP20RequestService extends OCPPRequestService { commandParams.evseId as number | undefined ) as unknown as Request case OCPP20RequestCommand.TRANSACTION_EVENT: { - // Pre-built payloads (e.g., from offline queue) already have transactionInfo; - // pass them through as-is to avoid double-building. - if (commandParams.transactionInfo != null) { - return commandParams as unknown as Request - } const eventType = commandParams.eventType as OCPP20TransactionEventEnumType - const triggerReason = commandParams.triggerReason as OCPP20TriggerReasonEnumType - const connectorId = commandParams.connectorId as number - const transactionId = commandParams.transactionId as string + const triggerReason: OCPP20TriggerReasonEnumType = + commandParams.triggerReason != null + ? (commandParams.triggerReason as OCPP20TriggerReasonEnumType) + : eventType === OCPP20TransactionEventEnumType.Ended + ? OCPP20TriggerReasonEnumType.RemoteStop + : OCPP20TriggerReasonEnumType.Authorized + const evse = commandParams.evse as undefined | { connectorId?: number; id?: number } + const connectorId: number = + commandParams.connectorId != null + ? (commandParams.connectorId as number) + : (evse?.connectorId ?? evse?.id ?? 1) + const transactionId: string = + commandParams.transactionId != null + ? (commandParams.transactionId as string) + : eventType === OCPP20TransactionEventEnumType.Ended + ? (chargingStation.getConnectorStatus(connectorId)?.transactionId?.toString() ?? + generateUUID()) + : generateUUID() return OCPP20ServiceUtils.buildTransactionEvent( chargingStation, eventType, diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index 40e74903..968ec58a 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -703,7 +703,9 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { await chargingStation.ocppRequestService.requestHandler< OCPP20TransactionEventRequest, OCPP20TransactionEventResponse - >(chargingStation, OCPP20RequestCommand.TRANSACTION_EVENT, queuedEvent.request) + >(chargingStation, OCPP20RequestCommand.TRANSACTION_EVENT, queuedEvent.request, { + rawPayload: true, + }) } catch (error) { logger.error( `${chargingStation.logPrefix()} ${moduleName}.sendQueuedTransactionEvents: Failed to send queued TransactionEvent with seqNo=${queuedEvent.seqNo.toString()}:`, @@ -754,8 +756,7 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { throw new OCPPError(ErrorType.PROPERTY_CONSTRAINT_VIOLATION, errorMsg) } - // Offline path: build payload directly for queueing (queue stores pre-built requests - // that are sent as-is on reconnect, bypassing buildRequestPayload). + // Offline: build and queue pre-built payload (sent as-is via rawPayload on reconnect) if (!chargingStation.isWebSocketConnectionOpened()) { const transactionEventRequest = OCPP20ServiceUtils.buildTransactionEvent( chargingStation, @@ -777,7 +778,7 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { return { idTokenInfo: undefined } } - // Online path: pass minimal params to requestHandler → buildRequestPayload builds + // Online: minimal params → requestHandler → buildRequestPayload logger.debug( `${chargingStation.logPrefix()} ${moduleName}.sendTransactionEvent: Sending TransactionEvent for trigger ${triggerReason}` ) diff --git a/src/types/ocpp/Requests.ts b/src/types/ocpp/Requests.ts index 9ed57117..566514c4 100644 --- a/src/types/ocpp/Requests.ts +++ b/src/types/ocpp/Requests.ts @@ -75,6 +75,7 @@ export const RequestCommand = { export type RequestCommand = OCPP16RequestCommand | OCPP20RequestCommand export interface RequestParams { + rawPayload?: boolean skipBufferingOnError?: boolean throwError?: boolean triggerMessage?: boolean diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts index 1da0a8f7..2d5b0a8b 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts @@ -369,7 +369,7 @@ await describe('F03 - Remote Stop Transaction', async () => { const args = requestHandlerMock.mock.calls[0].arguments as [ unknown, string, - Record + Partial ] const minimalParams = args[2] diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts index 90370fc0..334cf166 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts @@ -7,8 +7,10 @@ import assert from 'node:assert/strict' import { afterEach, beforeEach, describe, it, mock } from 'node:test' import type { + OCPP20StatusNotificationRequest, OCPP20TriggerMessageRequest, OCPP20TriggerMessageResponse, + RequestParams, } from '../../../../src/types/index.js' import type { MockChargingStation } from '../../ChargingStationTestUtils.js' @@ -506,8 +508,8 @@ await describe('F06 - TriggerMessage', async () => { const args = call.arguments as [ unknown, string, - Record, - Record + Partial, + RequestParams ] const [, command, payload, options] = args assert.strictEqual(command, OCPP20RequestCommand.STATUS_NOTIFICATION) @@ -515,7 +517,10 @@ await describe('F06 - TriggerMessage', async () => { assert.ok('evseId' in payload, 'Expected payload to include evseId') assert.ok('connectorId' in payload, 'Expected payload to include connectorId') assert.ok('status' in payload, 'Expected payload to include status') - assert.ok((payload.evseId as number) > 0, 'Expected evseId > 0 (EVSE 0 excluded)') + assert.ok( + payload.evseId != null && payload.evseId > 0, + 'Expected evseId > 0 (EVSE 0 excluded)' + ) assert.strictEqual(options.skipBufferingOnError, true) assert.strictEqual(options.triggerMessage, true) } @@ -541,8 +546,8 @@ await describe('F06 - TriggerMessage', async () => { const args = requestHandlerMock.mock.calls[0].arguments as [ unknown, string, - Record, - Record + Partial, + RequestParams ] const [, command, payload, options] = args assert.strictEqual(command, OCPP20RequestCommand.STATUS_NOTIFICATION) 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 3e5f42ea..dd1d4ac5 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts @@ -30,6 +30,7 @@ import { type OCPP20IdTokenType, OCPP20ReasonEnumType, type OCPP20TransactionContext, + type OCPP20TransactionType, } from '../../../../src/types/ocpp/2.0/Transaction.js' import { Constants, generateUUID } from '../../../../src/utils/index.js' import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js' @@ -2363,7 +2364,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { assert.strictEqual(sentRequests.length, 1) assert.strictEqual( - (sentRequests[0].payload.transactionInfo as Record).transactionId, + (sentRequests[0].payload.transactionInfo as OCPP20TransactionType).transactionId, transactionId1 ) @@ -2374,7 +2375,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { assert.strictEqual(sentRequests.length, 2) assert.strictEqual( - (sentRequests[1].payload.transactionInfo as Record).transactionId, + (sentRequests[1].payload.transactionInfo as OCPP20TransactionType).transactionId, transactionId2 ) }) -- 2.43.0