From 2f8268862300f3f2260f99003958bd1c3fa0f948 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 18 Mar 2026 17:13:49 +0100 Subject: [PATCH] refactor(ocpp): make buildRequestPayload the authoritative builder for all commands buildRequestPayload now calls the centralized builders for TRANSACTION_EVENT (via buildTransactionEvent) and STATUS_NOTIFICATION (via buildStatusNotificationRequest) in both OCPP versions, matching the existing pattern for START_TRANSACTION/STOP_TRANSACTION in 1.6. Callers pass minimal params (eventType, connectorId, status, etc.) and buildRequestPayload constructs the complete spec-compliant payload. sendTransactionEvent's offline path builds directly for queueing since the queue stores pre-built payloads sent as-is on reconnect. --- .serena/memories/code_style_conventions.md | 7 + .../ocpp/1.6/OCPP16IncomingRequestService.ts | 33 ++-- .../ocpp/1.6/OCPP16RequestService.ts | 13 +- .../ocpp/2.0/OCPP20IncomingRequestService.ts | 5 +- .../ocpp/2.0/OCPP20RequestService.ts | 35 +++- .../ocpp/2.0/OCPP20ServiceUtils.ts | 30 ++-- src/charging-station/ocpp/OCPPServiceUtils.ts | 10 +- ...uestService-RequestStopTransaction.test.ts | 28 +-- ...omingRequestService-TriggerMessage.test.ts | 6 +- ...0RequestService-StatusNotification.test.ts | 163 +++++++----------- ...CPP20ServiceUtils-TransactionEvent.test.ts | 49 +++--- 11 files changed, 193 insertions(+), 186 deletions(-) diff --git a/.serena/memories/code_style_conventions.md b/.serena/memories/code_style_conventions.md index dd359347..0c187aa0 100644 --- a/.serena/memories/code_style_conventions.md +++ b/.serena/memories/code_style_conventions.md @@ -39,6 +39,13 @@ - **Message format**: SRPC format: `[messageTypeId, messageId, action, payload]` - **Per-station state**: WeakMap-based isolation per station (not singleton properties) +### Request Architecture + +- **Single path**: `requestHandler()` → `buildRequestPayload()` → `sendMessage()` — no bypasses +- **`buildRequestPayload()`** enriches where needed (1.6: meterStart/idTag/timestamp; 2.0: CSR generation), passthrough otherwise +- **Service utils** (`buildTransactionEvent`, `buildStatusNotificationRequest`, `buildMeterValue`, etc.) build complex payloads upstream before calling `requestHandler` +- **Broadcast channel handlers** are simple passthroughs to `requestHandler` — no state management or flow duplication + ## Logging - **Winston** logger with 4 levels: `error`, `warn`, `info`, `debug` diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 23f0c402..541351f4 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -116,11 +116,7 @@ import { } from '../../../utils/index.js' import { OCPPConstants } from '../OCPPConstants.js' import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js' -import { - buildMeterValue, - buildStatusNotificationRequest, - OCPPServiceUtils, -} from '../OCPPServiceUtils.js' +import { buildMeterValue, OCPPServiceUtils } from '../OCPPServiceUtils.js' import { OCPP16Constants } from './OCPP16Constants.js' import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js' @@ -487,12 +483,11 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { .requestHandler( chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, - buildStatusNotificationRequest( - chargingStation, + { connectorId, - chargingStation.getConnectorStatus(connectorId) - ?.status as OCPP16ChargePointStatus - ) as OCPP16StatusNotificationRequest, + status: chargingStation.getConnectorStatus(connectorId) + ?.status as OCPP16ChargePointStatus, + } as unknown as OCPP16StatusNotificationRequest, { triggerMessage: true, } @@ -508,11 +503,10 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { >( chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, - buildStatusNotificationRequest( - chargingStation, - id, - connectorStatus.status as OCPP16ChargePointStatus - ) as OCPP16StatusNotificationRequest, + { + connectorId: id, + status: connectorStatus.status as OCPP16ChargePointStatus, + } as unknown as OCPP16StatusNotificationRequest, { triggerMessage: true, } @@ -529,11 +523,10 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { >( chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, - buildStatusNotificationRequest( - chargingStation, - id, - connectorStatus.status as OCPP16ChargePointStatus - ) as OCPP16StatusNotificationRequest, + { + connectorId: id, + status: connectorStatus.status as OCPP16ChargePointStatus, + } as unknown as OCPP16StatusNotificationRequest, { triggerMessage: true, } diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index d06a817c..ccfc9d20 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -5,6 +5,7 @@ import type { OCPPResponseService } from '../OCPPResponseService.js' import { OCPPError } from '../../../exception/index.js' import { + type ConnectorStatusEnum, ErrorType, type JsonObject, type JsonType, @@ -17,6 +18,7 @@ import { } from '../../../types/index.js' import { Constants, generateUUID, logger } from '../../../utils/index.js' import { OCPPRequestService } from '../OCPPRequestService.js' +import { buildStatusNotificationRequest } from '../OCPPServiceUtils.js' import { OCPP16Constants } from './OCPP16Constants.js' import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js' @@ -183,7 +185,6 @@ export class OCPP16RequestService extends OCPPRequestService { case OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION: case OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION: case OCPP16RequestCommand.METER_VALUES: - case OCPP16RequestCommand.STATUS_NOTIFICATION: return commandParams as unknown as Request case OCPP16RequestCommand.HEARTBEAT: return OCPP16Constants.OCPP_REQUEST_EMPTY as unknown as Request @@ -210,6 +211,16 @@ 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, + commandParams.status as ConnectorStatusEnum, + commandParams.evseId as number | undefined + ) as unknown as Request case OCPP16RequestCommand.STOP_TRANSACTION: chargingStation.stationInfo?.transactionDataMeterValues === true && (connectorId = chargingStation.getConnectorIdByTransactionId( diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index 19afdf32..eb30b2f2 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -144,7 +144,6 @@ import { OCPPAuthServiceFactory } from '../auth/index.js' import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js' import { buildMeterValue, - buildStatusNotificationRequest, restoreConnectorStatus, sendAndSetConnectorStatus, } from '../OCPPServiceUtils.js' @@ -3690,7 +3689,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { .requestHandler< OCPP20StatusNotificationRequest, OCPP20StatusNotificationResponse - >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, buildStatusNotificationRequest(chargingStation, connectorId, resolvedStatus, evseId) as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true }) + >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, { connectorId, evseId, status: resolvedStatus } as unknown as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true }) .catch(errorHandler) } } @@ -3710,7 +3709,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { .requestHandler< OCPP20StatusNotificationRequest, OCPP20StatusNotificationResponse - >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, buildStatusNotificationRequest(chargingStation, evse.connectorId, resolvedStatus, evse.id) as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true }) + >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, { connectorId: evse.connectorId, evseId: evse.id, status: resolvedStatus } as unknown as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true }) .catch(errorHandler) } else if (chargingStation.hasEvses) { this.triggerAllEvseStatusNotifications(chargingStation, errorHandler) diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts index bd240189..a058fdae 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -1,6 +1,12 @@ 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' @@ -16,6 +22,7 @@ import { } from '../../../types/index.js' import { generateUUID, logger } from '../../../utils/index.js' import { OCPPRequestService } from '../OCPPRequestService.js' +import { buildStatusNotificationRequest } from '../OCPPServiceUtils.js' import { generatePkcs10Csr } from './Asn1DerUtils.js' import { OCPP20Constants } from './OCPP20Constants.js' import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' @@ -157,8 +164,6 @@ export class OCPP20RequestService extends OCPPRequestService { case OCPP20RequestCommand.NOTIFY_CUSTOMER_INFORMATION: case OCPP20RequestCommand.NOTIFY_REPORT: case OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION: - case OCPP20RequestCommand.STATUS_NOTIFICATION: - case OCPP20RequestCommand.TRANSACTION_EVENT: return commandParams as unknown as Request case OCPP20RequestCommand.HEARTBEAT: return OCPP20Constants.OCPP_RESPONSE_EMPTY as unknown as Request @@ -195,6 +200,32 @@ export class OCPP20RequestService extends OCPPRequestService { return requestPayload as unknown as Request } + case OCPP20RequestCommand.STATUS_NOTIFICATION: + return buildStatusNotificationRequest( + chargingStation, + commandParams.connectorId as number, + commandParams.status as ConnectorStatusEnum, + 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 + return OCPP20ServiceUtils.buildTransactionEvent( + chargingStation, + eventType, + triggerReason, + connectorId, + transactionId, + commandParams as unknown as OCPP20TransactionEventOptions + ) as unknown as Request + } 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/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index 731a5edf..40e74903 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -745,17 +745,6 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ? this.selectTriggerReason(eventType, triggerReasonOrContext) : triggerReasonOrContext - // Build the transaction event request - const transactionEventRequest = OCPP20ServiceUtils.buildTransactionEvent( - chargingStation, - eventType, - triggerReason, - connectorId, - transactionId, - options - ) - - // OCPP 2.0.1 offline-first: Queue event if offline, send if online const connectorStatus = chargingStation.getConnectorStatus(connectorId) if (connectorStatus == null) { const errorMsg = `Cannot find connector status for connector ${connectorId.toString()}` @@ -765,7 +754,17 @@ 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). if (!chargingStation.isWebSocketConnectionOpened()) { + const transactionEventRequest = OCPP20ServiceUtils.buildTransactionEvent( + chargingStation, + eventType, + triggerReason, + connectorId, + transactionId, + options + ) logger.info( `${chargingStation.logPrefix()} ${moduleName}.sendTransactionEvent: Station offline, queueing TransactionEvent with seqNo=${transactionEventRequest.seqNo.toString()}` ) @@ -778,6 +777,7 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { return { idTokenInfo: undefined } } + // Online path: pass minimal params to requestHandler → buildRequestPayload builds logger.debug( `${chargingStation.logPrefix()} ${moduleName}.sendTransactionEvent: Sending TransactionEvent for trigger ${triggerReason}` ) @@ -785,7 +785,13 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { const response = await chargingStation.ocppRequestService.requestHandler< OCPP20TransactionEventRequest, OCPP20TransactionEventResponse - >(chargingStation, OCPP20RequestCommand.TRANSACTION_EVENT, transactionEventRequest) + >(chargingStation, OCPP20RequestCommand.TRANSACTION_EVENT, { + connectorId, + eventType, + transactionId, + triggerReason, + ...options, + } as unknown as OCPP20TransactionEventRequest) return response } catch (error) { diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index 829df23f..d5b2abcc 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -308,11 +308,11 @@ export const sendAndSetConnectorStatus = async ( await chargingStation.ocppRequestService.requestHandler< StatusNotificationRequest, StatusNotificationResponse - >( - chargingStation, - RequestCommand.STATUS_NOTIFICATION, - buildStatusNotificationRequest(chargingStation, connectorId, status, evseId) - ) + >(chargingStation, RequestCommand.STATUS_NOTIFICATION, { + connectorId, + evseId, + status, + } as unknown as StatusNotificationRequest) } connectorStatus.status = status chargingStation.emitChargingStationEvent(ChargingStationEvents.connectorStatusChanged, { 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 b3212a8b..1da0a8f7 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts @@ -369,26 +369,16 @@ await describe('F03 - Remote Stop Transaction', async () => { const args = requestHandlerMock.mock.calls[0].arguments as [ unknown, string, - OCPP20TransactionEventRequest + Record ] - const transactionEvent = args[2] - - assert.strictEqual(transactionEvent.eventType, OCPP20TransactionEventEnumType.Ended) - assert.notStrictEqual(transactionEvent.timestamp, undefined) - assert.ok(transactionEvent.timestamp instanceof Date) - assert.strictEqual(transactionEvent.triggerReason, OCPP20TriggerReasonEnumType.RemoteStop) - assert.notStrictEqual(transactionEvent.seqNo, undefined) - assert.strictEqual(typeof transactionEvent.seqNo, 'number') - - assert.notStrictEqual(transactionEvent.transactionInfo, undefined) - assert.strictEqual(transactionEvent.transactionInfo.transactionId, transactionId) - assert.strictEqual( - transactionEvent.transactionInfo.stoppedReason, - OCPP20ReasonEnumType.Remote - ) - - assert.notStrictEqual(transactionEvent.evse, undefined) - assert.strictEqual(transactionEvent.evse?.id, 2) + const minimalParams = args[2] + + assert.strictEqual(minimalParams.eventType, OCPP20TransactionEventEnumType.Ended) + assert.strictEqual(minimalParams.triggerReason, OCPP20TriggerReasonEnumType.RemoteStop) + assert.strictEqual(minimalParams.transactionId, transactionId) + assert.strictEqual(minimalParams.stoppedReason, OCPP20ReasonEnumType.Remote) + assert.notStrictEqual(minimalParams.connectorId, undefined) + assert.strictEqual(typeof minimalParams.connectorId, 'number') }) // FR: F03.FR.09 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 c7033fa9..90370fc0 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts @@ -514,8 +514,7 @@ await describe('F06 - TriggerMessage', async () => { assert.notStrictEqual(payload, undefined) assert.ok('evseId' in payload, 'Expected payload to include evseId') assert.ok('connectorId' in payload, 'Expected payload to include connectorId') - assert.ok('connectorStatus' in payload, 'Expected payload to include connectorStatus') - assert.ok('timestamp' in payload, 'Expected payload to include timestamp') + assert.ok('status' in payload, 'Expected payload to include status') assert.ok((payload.evseId as number) > 0, 'Expected evseId > 0 (EVSE 0 excluded)') assert.strictEqual(options.skipBufferingOnError, true) assert.strictEqual(options.triggerMessage, true) @@ -549,8 +548,7 @@ await describe('F06 - TriggerMessage', async () => { assert.strictEqual(command, OCPP20RequestCommand.STATUS_NOTIFICATION) assert.strictEqual(payload.evseId, 1) assert.strictEqual(payload.connectorId, 1) - assert.ok('connectorStatus' in payload) - assert.ok(payload.timestamp instanceof Date) + assert.ok('status' in payload) assert.strictEqual(options.skipBufferingOnError, true) assert.strictEqual(options.triggerMessage, true) }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts index 9f94a1b7..a19718d1 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts @@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, it } from 'node:test' import type { ChargingStation } from '../../../../src/charging-station/index.js' import { + ConnectorStatusEnum, OCPP20ConnectorStatusEnumType, OCPP20RequestCommand, type OCPP20StatusNotificationRequest, @@ -49,125 +50,100 @@ await describe('G01 - Status Notification', async () => { // FR: G01.FR.01 await it('should build StatusNotification request payload correctly with Available status', () => { - const testTimestamp = new Date('2024-01-15T10:30:00.000Z') - - const requestParams: OCPP20StatusNotificationRequest = { - connectorId: 1, - connectorStatus: OCPP20ConnectorStatusEnumType.Available, - evseId: 1, - timestamp: testTimestamp, - } - const payload = testableRequestService.buildRequestPayload( station, OCPP20RequestCommand.STATUS_NOTIFICATION, - requestParams + { + connectorId: 1, + evseId: 1, + status: ConnectorStatusEnum.Available, + } ) as OCPP20StatusNotificationRequest assert.notStrictEqual(payload, undefined) assert.strictEqual(payload.connectorId, 1) assert.strictEqual(payload.connectorStatus, OCPP20ConnectorStatusEnumType.Available) assert.strictEqual(payload.evseId, 1) - assert.strictEqual(payload.timestamp, testTimestamp) + assert.ok(payload.timestamp instanceof Date) }) // FR: G01.FR.02 await it('should build StatusNotification request payload correctly with Occupied status', () => { - const testTimestamp = new Date('2024-01-15T11:45:30.000Z') - - const requestParams: OCPP20StatusNotificationRequest = { - connectorId: 2, - connectorStatus: OCPP20ConnectorStatusEnumType.Occupied, - evseId: 2, - timestamp: testTimestamp, - } - const payload = testableRequestService.buildRequestPayload( station, OCPP20RequestCommand.STATUS_NOTIFICATION, - requestParams + { + connectorId: 2, + evseId: 2, + status: ConnectorStatusEnum.Occupied, + } ) as OCPP20StatusNotificationRequest assert.notStrictEqual(payload, undefined) assert.strictEqual(payload.connectorId, 2) assert.strictEqual(payload.connectorStatus, OCPP20ConnectorStatusEnumType.Occupied) assert.strictEqual(payload.evseId, 2) - assert.strictEqual(payload.timestamp, testTimestamp) + assert.ok(payload.timestamp instanceof Date) }) // FR: G01.FR.03 await it('should build StatusNotification request payload correctly with Faulted status', () => { - const testTimestamp = new Date('2024-01-15T12:15:45.500Z') - - const requestParams: OCPP20StatusNotificationRequest = { - connectorId: 1, - connectorStatus: OCPP20ConnectorStatusEnumType.Faulted, - evseId: 1, - timestamp: testTimestamp, - } - const payload = testableRequestService.buildRequestPayload( station, OCPP20RequestCommand.STATUS_NOTIFICATION, - requestParams + { + connectorId: 1, + evseId: 1, + status: ConnectorStatusEnum.Faulted, + } ) as OCPP20StatusNotificationRequest assert.notStrictEqual(payload, undefined) assert.strictEqual(payload.connectorId, 1) assert.strictEqual(payload.connectorStatus, OCPP20ConnectorStatusEnumType.Faulted) assert.strictEqual(payload.evseId, 1) - assert.strictEqual(payload.timestamp, testTimestamp) + assert.ok(payload.timestamp instanceof Date) }) // FR: G01.FR.04 await it('should handle all OCPP20ConnectorStatusEnumType values correctly', () => { - const testTimestamp = new Date('2024-01-15T13:00:00.000Z') - - const statusValues = [ - OCPP20ConnectorStatusEnumType.Available, - OCPP20ConnectorStatusEnumType.Faulted, - OCPP20ConnectorStatusEnumType.Occupied, - OCPP20ConnectorStatusEnumType.Reserved, - OCPP20ConnectorStatusEnumType.Unavailable, + const statusValues: [ConnectorStatusEnum, OCPP20ConnectorStatusEnumType][] = [ + [ConnectorStatusEnum.Available, OCPP20ConnectorStatusEnumType.Available], + [ConnectorStatusEnum.Faulted, OCPP20ConnectorStatusEnumType.Faulted], + [ConnectorStatusEnum.Occupied, OCPP20ConnectorStatusEnumType.Occupied], + [ConnectorStatusEnum.Reserved, OCPP20ConnectorStatusEnumType.Reserved], + [ConnectorStatusEnum.Unavailable, OCPP20ConnectorStatusEnumType.Unavailable], ] - statusValues.forEach((status, index) => { - const requestParams: OCPP20StatusNotificationRequest = { - connectorId: index + 1, - connectorStatus: status, - evseId: index + 1, - timestamp: testTimestamp, - } - + statusValues.forEach(([inputStatus, expectedConnectorStatus], index) => { const payload = testableRequestService.buildRequestPayload( station, OCPP20RequestCommand.STATUS_NOTIFICATION, - requestParams + { + connectorId: index + 1, + evseId: index + 1, + status: inputStatus, + } ) as OCPP20StatusNotificationRequest assert.notStrictEqual(payload, undefined) - assert.strictEqual(payload.connectorStatus, status) + assert.strictEqual(payload.connectorStatus, expectedConnectorStatus) assert.strictEqual(payload.connectorId, index + 1) assert.strictEqual(payload.evseId, index + 1) - assert.strictEqual(payload.timestamp, testTimestamp) + assert.ok(payload.timestamp instanceof Date) }) }) // FR: G01.FR.05 await it('should validate payload structure matches OCPP20StatusNotificationRequest interface', () => { - const testTimestamp = new Date('2024-01-15T14:30:15.123Z') - - const requestParams: OCPP20StatusNotificationRequest = { - connectorId: 3, - connectorStatus: OCPP20ConnectorStatusEnumType.Reserved, - evseId: 2, - timestamp: testTimestamp, - } - const payload = testableRequestService.buildRequestPayload( station, OCPP20RequestCommand.STATUS_NOTIFICATION, - requestParams + { + connectorId: 3, + evseId: 2, + status: ConnectorStatusEnum.Reserved, + } ) as OCPP20StatusNotificationRequest // Validate that the payload has the exact structure of OCPP20StatusNotificationRequest @@ -188,80 +164,71 @@ await describe('G01 - Status Notification', async () => { assert.strictEqual(payload.connectorId, 3) assert.strictEqual(payload.connectorStatus, OCPP20ConnectorStatusEnumType.Reserved) assert.strictEqual(payload.evseId, 2) - assert.strictEqual(payload.timestamp, testTimestamp) }) // FR: G01.FR.06 await it('should handle edge case connector and EVSE IDs correctly', () => { - const testTimestamp = new Date('2024-01-15T15:45:00.000Z') - // Test with connector ID 0 (valid in OCPP 2.0 for the charging station itself) - const requestParamsConnector0: OCPP20StatusNotificationRequest = { - connectorId: 0, - connectorStatus: OCPP20ConnectorStatusEnumType.Available, - evseId: 1, - timestamp: testTimestamp, - } - const payloadConnector0 = testableRequestService.buildRequestPayload( station, OCPP20RequestCommand.STATUS_NOTIFICATION, - requestParamsConnector0 + { + connectorId: 0, + evseId: 1, + status: ConnectorStatusEnum.Available, + } ) as OCPP20StatusNotificationRequest assert.notStrictEqual(payloadConnector0, undefined) assert.strictEqual(payloadConnector0.connectorId, 0) assert.strictEqual(payloadConnector0.connectorStatus, OCPP20ConnectorStatusEnumType.Available) assert.strictEqual(payloadConnector0.evseId, 1) - assert.strictEqual(payloadConnector0.timestamp, testTimestamp) + assert.ok(payloadConnector0.timestamp instanceof Date) // Test with EVSE ID 0 (valid in OCPP 2.0 for the charging station itself) - const requestParamsEvse0: OCPP20StatusNotificationRequest = { - connectorId: 1, - connectorStatus: OCPP20ConnectorStatusEnumType.Unavailable, - evseId: 0, - timestamp: testTimestamp, - } - const payloadEvse0 = testableRequestService.buildRequestPayload( station, OCPP20RequestCommand.STATUS_NOTIFICATION, - requestParamsEvse0 + { + connectorId: 1, + evseId: 0, + status: ConnectorStatusEnum.Unavailable, + } ) as OCPP20StatusNotificationRequest assert.notStrictEqual(payloadEvse0, undefined) assert.strictEqual(payloadEvse0.connectorId, 1) assert.strictEqual(payloadEvse0.connectorStatus, OCPP20ConnectorStatusEnumType.Unavailable) assert.strictEqual(payloadEvse0.evseId, 0) - assert.strictEqual(payloadEvse0.timestamp, testTimestamp) + assert.ok(payloadEvse0.timestamp instanceof Date) }) // FR: G01.FR.07 await it('should handle different timestamp formats correctly', () => { - const testCases = [ - new Date('2024-01-01T00:00:00.000Z'), // Start of year - new Date('2024-12-31T23:59:59.999Z'), // End of year - new Date(), // Current time - new Date('2024-06-15T12:30:45.678Z'), // Mid-year with milliseconds + // buildRequestPayload now generates its own timestamp via buildStatusNotificationRequest, + // so we verify the output always has a valid Date timestamp + const statusValues = [ + ConnectorStatusEnum.Available, + ConnectorStatusEnum.Occupied, + ConnectorStatusEnum.Faulted, + ConnectorStatusEnum.Reserved, ] - testCases.forEach((timestamp, _index) => { - const requestParams: OCPP20StatusNotificationRequest = { - connectorId: 1, - connectorStatus: OCPP20ConnectorStatusEnumType.Available, - evseId: 1, - timestamp, - } - + const beforeBuild = new Date() + statusValues.forEach(status => { const payload = testableRequestService.buildRequestPayload( station, OCPP20RequestCommand.STATUS_NOTIFICATION, - requestParams + { + connectorId: 1, + evseId: 1, + status, + } ) as OCPP20StatusNotificationRequest assert.notStrictEqual(payload, undefined) - assert.strictEqual(payload.timestamp, timestamp) assert.ok(payload.timestamp instanceof Date) + assert.ok(payload.timestamp.getTime() >= beforeBuild.getTime()) }) }) }) 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 38ddf2a5..3e5f42ea 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts @@ -2028,7 +2028,10 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) assert.strictEqual(sentRequests.length, 1) - assert.strictEqual(sentRequests[0].payload.seqNo, 0) + assert.strictEqual( + sentRequests[0].payload.eventType, + OCPP20TransactionEventEnumType.Started + ) setOnline(false) @@ -2050,8 +2053,9 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { const connector = mockStation.getConnectorStatus(connectorId) assert.strictEqual(connector?.transactionEventQueue?.length, 2) - assert.strictEqual(connector.transactionEventQueue[0].seqNo, 1) - assert.strictEqual(connector.transactionEventQueue[1].seqNo, 2) + // Online path with mock doesn't call buildTransactionEvent, so seqNo starts from 0 + assert.strictEqual(connector.transactionEventQueue[0].seqNo, 0) + assert.strictEqual(connector.transactionEventQueue[1].seqNo, 1) }) await it('should include timestamp in queued events', async () => { @@ -2228,7 +2232,11 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { connectorId, transactionId ) - assert.strictEqual(sentRequests[0].payload.seqNo, 0) + // Online path sends minimal params (no seqNo in payload) + assert.strictEqual( + sentRequests[0].payload.eventType, + OCPP20TransactionEventEnumType.Started + ) setOnline(false) @@ -2252,8 +2260,10 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockStation, connectorId) - assert.strictEqual(sentRequests[1].payload.seqNo, 1) - assert.strictEqual(sentRequests[2].payload.seqNo, 2) + // Queued events are pre-built payloads with seqNo (starts from 0 since + // the online path with mock doesn't call buildTransactionEvent) + assert.strictEqual(sentRequests[1].payload.seqNo, 0) + assert.strictEqual(sentRequests[2].payload.seqNo, 1) await OCPP20ServiceUtils.sendTransactionEvent( mockStation, @@ -2263,11 +2273,14 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { transactionId ) - assert.strictEqual(sentRequests[3].payload.seqNo, 3) + // Online path sends minimal params (no seqNo in payload) + assert.strictEqual(sentRequests[3].payload.eventType, OCPP20TransactionEventEnumType.Ended) - for (let i = 0; i < sentRequests.length; i++) { - assert.strictEqual(sentRequests[i].payload.seqNo, i) - } + // Verify seqNo continuity for queued events (indices 1 and 2) + assert.strictEqual(sentRequests[1].payload.seqNo, 0) + assert.strictEqual(sentRequests[2].payload.seqNo, 1) + // Verify total request count + assert.strictEqual(sentRequests.length, 4) }) }) @@ -2614,12 +2627,8 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { transactionId ) - // Verify EVSE info is present - assert.notStrictEqual(sentRequests[0].payload.evse, undefined) - assert.strictEqual( - (sentRequests[0].payload.evse as Record).id, - connectorId - ) + // Online path sends minimal params with connectorId (EVSE resolved by buildRequestPayload) + assert.strictEqual(sentRequests[0].payload.connectorId, connectorId) }) await it('should include transactionInfo with correct transactionId', async () => { @@ -2636,12 +2645,8 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { transactionId ) - // Verify transactionInfo contains the transaction ID - assert.notStrictEqual(sentRequests[0].payload.transactionInfo, undefined) - assert.strictEqual( - (sentRequests[0].payload.transactionInfo as Record).transactionId, - transactionId - ) + // Online path sends minimal params with transactionId at top level + assert.strictEqual(sentRequests[0].payload.transactionId, transactionId) }) }) -- 2.53.0