]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(ocpp): add rawPayload bypass, resolve minimal params in buildRequestPayload...
authorJérôme Benoit <jerome.benoit@sap.com>
Wed, 18 Mar 2026 16:59:56 +0000 (17:59 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Wed, 18 Mar 2026 17:05:13 +0000 (18:05 +0100)
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<string, unknown> downgrades in tests with proper OCPP
types (Partial<OCPP20TransactionEventRequest>, RequestParams).

src/charging-station/ocpp/1.6/OCPP16RequestService.ts
src/charging-station/ocpp/2.0/OCPP20RequestService.ts
src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts
src/types/ocpp/Requests.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts

index ccfc9d208eff4bfb7d78e111f3477a783b9617c7..9ac47a5e5c4461e9caf2d32fe9ad2f042b931958 100644 (file)
@@ -99,11 +99,10 @@ export class OCPP16RequestService extends OCPPRequestService {
         logger.debug(
           `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Building request payload for '${commandName}'`
         )
-        const requestPayload = this.buildRequestPayload<RequestType>(
-          chargingStation,
-          commandName,
-          commandParams
-        )
+        const requestPayload =
+          params?.rawPayload === true
+            ? (commandParams as RequestType)
+            : this.buildRequestPayload<RequestType>(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,
index a058fdaee2457e2027dae9669ef2fb0849cd03cd..40d5b8d31f9da116d469c0e1eef5cc258cc86be8 100644 (file)
@@ -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<RequestType>(
-          chargingStation,
-          commandName,
-          commandParams
-        )
+        const requestPayload =
+          params?.rawPayload === true
+            ? (commandParams as RequestType)
+            : this.buildRequestPayload<RequestType>(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,
index 40e7490398faeab0d807bc774d95c161e193cece..968ec58a6311fce8665e65b0cc7260b3cfbea7bf 100644 (file)
@@ -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}`
       )
index 9ed571177f09631d01b7c7f0a3921308c85a5ba6..566514c4bd7c825228be5021df6f1993a0108e52 100644 (file)
@@ -75,6 +75,7 @@ export const RequestCommand = {
 export type RequestCommand = OCPP16RequestCommand | OCPP20RequestCommand
 
 export interface RequestParams {
+  rawPayload?: boolean
   skipBufferingOnError?: boolean
   throwError?: boolean
   triggerMessage?: boolean
index 1da0a8f70e977f58cc6385386b5488eff483fec9..2d5b0a8b7a2788ac761a7efb69ef0940860263c4 100644 (file)
@@ -369,7 +369,7 @@ await describe('F03 - Remote Stop Transaction', async () => {
       const args = requestHandlerMock.mock.calls[0].arguments as [
         unknown,
         string,
-        Record<string, unknown>
+        Partial<OCPP20TransactionEventRequest>
       ]
       const minimalParams = args[2]
 
index 90370fc0d5b151cb64f64364f57fe1cf652d39f5..334cf166d8676cf044a9f4836c9f43306378aa89 100644 (file)
@@ -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<string, unknown>,
-          Record<string, unknown>
+          Partial<OCPP20StatusNotificationRequest>,
+          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<string, unknown>,
-        Record<string, unknown>
+        Partial<OCPP20StatusNotificationRequest>,
+        RequestParams
       ]
       const [, command, payload, options] = args
       assert.strictEqual(command, OCPP20RequestCommand.STATUS_NOTIFICATION)
index 3e5f42eaf55187012eabc8266bc288d46a81b74f..dd1d4ac54227e2cb6326be09343956dafef43700 100644 (file)
@@ -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<string, unknown>).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<string, unknown>).transactionId,
+          (sentRequests[1].payload.transactionInfo as OCPP20TransactionType).transactionId,
           transactionId2
         )
       })