]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
feat(ocpp2.0): implement Authorize request, fix double TransactionEvent(Started)
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 20 Mar 2026 20:07:05 +0000 (21:07 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 20 Mar 2026 20:07:05 +0000 (21:07 +0100)
OCPP20AuthAdapter.authorizeRemote() was incorrectly sending a
TransactionEvent(Started) as a proxy for authorization, causing a
duplicate Started event when ATG subsequently called
startTransactionOnConnector(). Implement the proper OCPP 2.0.1
Authorize request/response flow, harmonizing the auth+start sequence
with OCPP 1.6: Authorize first, then TransactionEvent(Started).

src/charging-station/ocpp/2.0/OCPP20RequestService.ts
src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts
src/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.ts
src/types/index.ts
src/types/ocpp/2.0/Requests.ts
src/types/ocpp/2.0/Responses.ts
tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts

index 313301de152b42046c3e982c29f355c4e459467a..8115b4a6938dc8351a837d6c6b33d2b43711eeb2 100644 (file)
@@ -149,6 +149,7 @@ export class OCPP20RequestService extends OCPPRequestService {
       `${chargingStation.logPrefix()} ${moduleName}.buildRequestPayload: Building ${commandName} payload`
     )
     switch (commandName) {
+      case OCPP20RequestCommand.AUTHORIZE:
       case OCPP20RequestCommand.BOOT_NOTIFICATION:
       case OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION:
       case OCPP20RequestCommand.GET_15118_EV_CERTIFICATE:
index 588dc07dd4c4ff287f7be06dd1d2fa5d11b65e4f..0333fd53f66405f7fe0b1095a0f02685c7db1bbf 100644 (file)
@@ -73,6 +73,7 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils {
     ]
 
   private static readonly outgoingRequestSchemaNames: readonly [OCPP20RequestCommand, string][] = [
+    [OCPP20RequestCommand.AUTHORIZE, 'Authorize'],
     [OCPP20RequestCommand.BOOT_NOTIFICATION, 'BootNotification'],
     [OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION, 'FirmwareStatusNotification'],
     [OCPP20RequestCommand.GET_15118_EV_CERTIFICATE, 'Get15118EVCertificate'],
index 22ec843fcd2c5a8975ff1ca870ed60bf3a50b58f..27ce77d6e6d34e0d8213fe501f98ab10eb22f890 100644 (file)
@@ -1,6 +1,7 @@
 import type {
   AdditionalInfoType,
-  OCPP20TransactionEventResponse,
+  OCPP20AuthorizeRequest,
+  OCPP20AuthorizeResponse,
   RequestStartStopStatusEnumType,
 } from '../../../../types/index.js'
 import type { ChargingStation } from '../../../index.js'
@@ -12,18 +13,16 @@ import type {
   UnifiedIdentifier,
 } from '../types/AuthTypes.js'
 
-import { OCPP20ServiceUtils } from '../../2.0/OCPP20ServiceUtils.js'
 import { OCPP20VariableManager } from '../../2.0/OCPP20VariableManager.js'
 import {
   GetVariableStatusEnumType,
   OCPP20AuthorizationStatusEnumType,
   OCPP20IdTokenEnumType,
   type OCPP20IdTokenType,
-  OCPP20TransactionEventEnumType,
-  OCPP20TriggerReasonEnumType,
+  OCPP20RequestCommand,
   OCPPVersion,
 } from '../../../../types/index.js'
-import { generateUUID, logger, truncateId } from '../../../../utils/index.js'
+import { logger, truncateId } from '../../../../utils/index.js'
 import {
   AuthContext,
   AuthenticationMethod,
@@ -39,9 +38,6 @@ const moduleName = 'OCPP20AuthAdapter'
  *
  * Handles authentication for OCPP 2.0/2.1 charging stations by translating
  * between unified auth types and OCPP 2.0 specific types and protocols.
- *
- * Note: OCPP 2.0 doesn't have a dedicated Authorize message. Authorization
- * happens through TransactionEvent messages and local configuration.
  */
 export class OCPP20AuthAdapter implements OCPPAuthAdapter {
   readonly ocppVersion = OCPPVersion.VERSION_20
@@ -49,10 +45,7 @@ export class OCPP20AuthAdapter implements OCPPAuthAdapter {
   constructor (private readonly chargingStation: ChargingStation) {}
 
   /**
-   * Perform remote authorization using OCPP 2.0 mechanisms
-   *
-   * Since OCPP 2.0 doesn't have Authorize, we simulate authorization
-   * by checking if we can start a transaction with the identifier
+   * Perform remote authorization using OCPP 2.0 Authorize request.
    * @param identifier - Unified identifier containing the IdToken to authorize
    * @param connectorId - EVSE/connector ID for the authorization context
    * @param transactionId - Optional existing transaction ID for ongoing transactions
@@ -67,7 +60,7 @@ export class OCPP20AuthAdapter implements OCPPAuthAdapter {
 
     try {
       logger.debug(
-        `${this.chargingStation.logPrefix()} ${moduleName}.${methodName}: Authorizing identifier ${truncateId(identifier.value)} via OCPP 2.0 TransactionEvent`
+        `${this.chargingStation.logPrefix()} ${moduleName}.${methodName}: Authorizing identifier ${truncateId(identifier.value)} via OCPP 2.0 Authorize`
       )
 
       // Check if remote authorization is configured
@@ -121,51 +114,21 @@ export class OCPP20AuthAdapter implements OCPPAuthAdapter {
           }
         }
 
-        // OCPP 2.0: Authorization through TransactionEvent
-        // According to OCPP 2.0.1 spec section G03 - Authorization
-        const tempTransactionId = transactionId != null ? transactionId.toString() : generateUUID()
-
-        // Get EVSE ID from connector
-        const evseId = connectorId // In OCPP 2.0, connector maps to EVSE
-
         logger.debug(
-          `${this.chargingStation.logPrefix()} ${moduleName}.${methodName}: Sending TransactionEvent for authorization (evseId: ${evseId.toString()}, idToken: ${idToken.idToken})`
+          `${this.chargingStation.logPrefix()} ${moduleName}.${methodName}: Sending Authorize request (idToken: ${idToken.idToken})`
         )
 
-        // Send TransactionEvent with idToken to request authorization
-        const response: OCPP20TransactionEventResponse =
-          await OCPP20ServiceUtils.sendTransactionEvent(
-            this.chargingStation,
-            OCPP20TransactionEventEnumType.Started,
-            OCPP20TriggerReasonEnumType.Authorized,
-            connectorId,
-            tempTransactionId,
-            {
-              evseId,
-              idToken,
-            }
-          )
+        // Send Authorize request
+        const response = await this.chargingStation.ocppRequestService.requestHandler<
+          OCPP20AuthorizeRequest,
+          OCPP20AuthorizeResponse
+        >(this.chargingStation, OCPP20RequestCommand.AUTHORIZE, {
+          idToken,
+        })
 
         // Extract authorization status from response
-        const authStatus = response.idTokenInfo?.status
-        const cacheExpiryDateTime = response.idTokenInfo?.cacheExpiryDateTime
-
-        if (authStatus == null) {
-          logger.warn(
-            `${this.chargingStation.logPrefix()} ${moduleName}.${methodName}: No idTokenInfo in TransactionEvent response, treating as Unknown`
-          )
-          return {
-            additionalInfo: {
-              connectorId,
-              note: 'No authorization status in response',
-              transactionId: tempTransactionId,
-            },
-            isOffline: false,
-            method: AuthenticationMethod.REMOTE_AUTHORIZATION,
-            status: AuthorizationStatus.UNKNOWN,
-            timestamp: new Date(),
-          }
-        }
+        const authStatus = response.idTokenInfo.status
+        const cacheExpiryDateTime = response.idTokenInfo.cacheExpiryDateTime
 
         // Map OCPP 2.0 authorization status to unified status
         const unifiedStatus = this.mapOCPP20AuthStatus(authStatus)
@@ -177,12 +140,11 @@ export class OCPP20AuthAdapter implements OCPPAuthAdapter {
         return {
           additionalInfo: {
             cacheExpiryDateTime,
-            chargingPriority: response.idTokenInfo?.chargingPriority,
+            chargingPriority: response.idTokenInfo.chargingPriority,
             connectorId,
             ocpp20Status: authStatus,
             tokenType: idToken.type,
             tokenValue: idToken.idToken,
-            transactionId: tempTransactionId,
           },
           isOffline: false,
           method: AuthenticationMethod.REMOTE_AUTHORIZATION,
@@ -191,7 +153,7 @@ export class OCPP20AuthAdapter implements OCPPAuthAdapter {
         }
       } catch (error) {
         logger.error(
-          `${this.chargingStation.logPrefix()} ${moduleName}.${methodName}: TransactionEvent authorization failed`,
+          `${this.chargingStation.logPrefix()} ${moduleName}.${methodName}: Authorize request failed`,
           error
         )
 
index e1ee3157d2abfa80085520199c64113d6e56222a..b8e51065603bffe53f432a59521f1a6e105757e2 100644 (file)
@@ -210,6 +210,7 @@ export {
   type OCPP20UnitOfMeasure,
 } from './ocpp/2.0/MeterValues.js'
 export {
+  type OCPP20AuthorizeRequest,
   type OCPP20BootNotificationRequest,
   type OCPP20CertificateSignedRequest,
   type OCPP20ChangeAvailabilityRequest,
@@ -245,6 +246,7 @@ export {
   type OCPP20UpdateFirmwareRequest,
 } from './ocpp/2.0/Requests.js'
 export type {
+  OCPP20AuthorizeResponse,
   OCPP20BootNotificationResponse,
   OCPP20CertificateSignedResponse,
   OCPP20ChangeAvailabilityResponse,
index e78790adc99481e0f2ef7f033b85c615989539fc..13743835003b7194ac63f3360907034ea1f45755 100644 (file)
@@ -58,6 +58,7 @@ export enum OCPP20IncomingRequestCommand {
 }
 
 export enum OCPP20RequestCommand {
+  AUTHORIZE = 'Authorize',
   BOOT_NOTIFICATION = 'BootNotification',
   FIRMWARE_STATUS_NOTIFICATION = 'FirmwareStatusNotification',
   GET_15118_EV_CERTIFICATE = 'Get15118EVCertificate',
@@ -73,6 +74,11 @@ export enum OCPP20RequestCommand {
   TRANSACTION_EVENT = 'TransactionEvent',
 }
 
+export interface OCPP20AuthorizeRequest extends JsonObject {
+  customData?: CustomDataType
+  idToken: OCPP20IdTokenType
+}
+
 export interface OCPP20BootNotificationRequest extends JsonObject {
   chargingStation: ChargingStationType
   customData?: CustomDataType
index 9dbe3b73d0035e7bb74e919f32096f0cb9e1bd44..eb71f714ab06383b77d797dd471ce9d2f203017d 100644 (file)
@@ -24,9 +24,14 @@ import type {
   UnlockStatusEnumType,
   UpdateFirmwareStatusEnumType,
 } from './Common.js'
-import type { RequestStartStopStatusEnumType } from './Transaction.js'
+import type { OCPP20IdTokenInfoType, RequestStartStopStatusEnumType } from './Transaction.js'
 import type { OCPP20GetVariableResultType, OCPP20SetVariableResultType } from './Variables.js'
 
+export interface OCPP20AuthorizeResponse extends JsonObject {
+  customData?: CustomDataType
+  idTokenInfo: OCPP20IdTokenInfoType
+}
+
 export interface OCPP20BootNotificationResponse extends JsonObject {
   currentTime: Date
   customData?: CustomDataType
index 3805b2f31695a8bd6b1ab20df3d5ea619d93dc3a..80d7345e91c439f13e714e4dab55c19777e99a21 100644 (file)
@@ -7,7 +7,6 @@ import { afterEach, beforeEach, describe, it, mock } from 'node:test'
 
 import type { ChargingStation } from '../../../../../src/charging-station/index.js'
 
-import { OCPP20ServiceUtils } from '../../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
 import { OCPP20AuthAdapter } from '../../../../../src/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.js'
 import {
   type AuthConfiguration,
@@ -223,19 +222,16 @@ await describe('OCPP20AuthAdapter', async () => {
       // Mock isRemoteAvailable to return true (avoids OCPP20VariableManager singleton issues)
       t.mock.method(adapter, 'isRemoteAvailable', () => true)
 
-      // Mock sendTransactionEvent to return accepted authorization
-      t.mock.method(
-        OCPP20ServiceUtils,
-        'sendTransactionEvent',
-        () =>
-          new Promise<Record<string, unknown>>(resolve => {
-            resolve({
-              idTokenInfo: {
-                status: OCPP20AuthorizationStatusEnumType.Accepted,
-              },
-            })
+      // Mock requestHandler to return accepted authorization
+      mockStation.ocppRequestService = {
+        requestHandler: mock.fn(() =>
+          Promise.resolve({
+            idTokenInfo: {
+              status: OCPP20AuthorizationStatusEnumType.Accepted,
+            },
           })
-      )
+        ),
+      } as unknown as ChargingStation['ocppRequestService']
 
       const identifier = createMockIdentifier(
         OCPPVersion.VERSION_20,