From: Jérôme Benoit Date: Fri, 20 Mar 2026 20:07:05 +0000 (+0100) Subject: feat(ocpp2.0): implement Authorize request, fix double TransactionEvent(Started) X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=c13a409bc03e6e95436af2cfabf6a4b838680810;p=e-mobility-charging-stations-simulator.git feat(ocpp2.0): implement Authorize request, fix double TransactionEvent(Started) 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). --- diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts index 313301de..8115b4a6 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -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: diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index 588dc07d..0333fd53 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -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'], diff --git a/src/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.ts b/src/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.ts index 22ec843f..27ce77d6 100644 --- a/src/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.ts +++ b/src/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.ts @@ -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 ) diff --git a/src/types/index.ts b/src/types/index.ts index e1ee3157..b8e51065 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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, diff --git a/src/types/ocpp/2.0/Requests.ts b/src/types/ocpp/2.0/Requests.ts index e78790ad..13743835 100644 --- a/src/types/ocpp/2.0/Requests.ts +++ b/src/types/ocpp/2.0/Requests.ts @@ -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 diff --git a/src/types/ocpp/2.0/Responses.ts b/src/types/ocpp/2.0/Responses.ts index 9dbe3b73..eb71f714 100644 --- a/src/types/ocpp/2.0/Responses.ts +++ b/src/types/ocpp/2.0/Responses.ts @@ -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 diff --git a/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts b/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts index 3805b2f3..80d7345e 100644 --- a/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts +++ b/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts @@ -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>(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,