switch (commandName) {
case OCPP20RequestCommand.AUTHORIZE:
case OCPP20RequestCommand.BOOT_NOTIFICATION:
+ case OCPP20RequestCommand.DATA_TRANSFER:
case OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION:
case OCPP20RequestCommand.GET_15118_EV_CERTIFICATE:
case OCPP20RequestCommand.GET_CERTIFICATE_STATUS:
OCPP20AuthorizationStatusEnumType,
type OCPP20AuthorizeResponse,
type OCPP20BootNotificationResponse,
+ type OCPP20DataTransferResponse,
type OCPP20FirmwareStatusNotificationResponse,
type OCPP20Get15118EVCertificateResponse,
type OCPP20GetCertificateStatusResponse,
OCPP20RequestCommand.BOOT_NOTIFICATION,
this.handleResponseBootNotification.bind(this) as ResponseHandler,
],
+ [
+ OCPP20RequestCommand.DATA_TRANSFER,
+ this.handleResponseDataTransfer.bind(this) as ResponseHandler,
+ ],
[
OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
this.handleResponseFirmwareStatusNotification.bind(this) as ResponseHandler,
}
}
+ private handleResponseDataTransfer (
+ chargingStation: ChargingStation,
+ payload: OCPP20DataTransferResponse
+ ): void {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleResponseDataTransfer: DataTransfer response received, status: ${payload.status}`
+ )
+ }
+
private handleResponseFirmwareStatusNotification (
chargingStation: ChargingStation,
payload: OCPP20FirmwareStatusNotificationResponse
private static readonly outgoingRequestSchemaNames: readonly [OCPP20RequestCommand, string][] = [
[OCPP20RequestCommand.AUTHORIZE, 'Authorize'],
[OCPP20RequestCommand.BOOT_NOTIFICATION, 'BootNotification'],
+ [OCPP20RequestCommand.DATA_TRANSFER, 'DataTransfer'],
[OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION, 'FirmwareStatusNotification'],
[OCPP20RequestCommand.GET_15118_EV_CERTIFICATE, 'Get15118EVCertificate'],
[OCPP20RequestCommand.GET_CERTIFICATE_STATUS, 'GetCertificateStatus'],
...params,
}
if (
- ((chargingStation.inUnknownState() || chargingStation.inPendingState()) &&
+ ((chargingStation.inUnknownState() ||
+ chargingStation.inPendingState() ||
+ chargingStation.inRejectedState()) &&
commandName === RequestCommand.BOOT_NOTIFICATION) ||
(chargingStation.stationInfo?.ocppStrictCompliance === false &&
(chargingStation.inUnknownState() || chargingStation.inPendingState())) ||
export enum OCPP20RequestCommand {
AUTHORIZE = 'Authorize',
BOOT_NOTIFICATION = 'BootNotification',
+ DATA_TRANSFER = 'DataTransfer',
FIRMWARE_STATUS_NOTIFICATION = 'FirmwareStatusNotification',
GET_15118_EV_CERTIFICATE = 'Get15118EVCertificate',
GET_CERTIFICATE_STATUS = 'GetCertificateStatus',
--- /dev/null
+/**
+ * @file Tests for OCPP20RequestService DataTransfer
+ * @description Unit tests for OCPP 2.0.1 DataTransfer outgoing command (P02)
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import {
+ createTestableRequestService,
+ type SendMessageMock,
+ type TestableOCPP20RequestService,
+} from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import {
+ type OCPP20DataTransferRequest,
+ type OCPP20DataTransferResponse,
+ OCPP20RequestCommand,
+ OCPPVersion,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('P02 - DataTransfer', async () => {
+ let station: ChargingStation
+ let sendMessageMock: SendMessageMock
+ let service: TestableOCPP20RequestService
+
+ beforeEach(() => {
+ const { station: newStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ evseConfiguration: { evsesCount: 1 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = newStation
+
+ const testable = createTestableRequestService<OCPP20DataTransferResponse>({
+ sendMessageResponse: {},
+ })
+ sendMessageMock = testable.sendMessageMock
+ service = testable.service
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should send DataTransfer with vendorId, messageId, and data', async () => {
+ const payload: OCPP20DataTransferRequest = {
+ data: 'test-payload-data',
+ messageId: 'TestMessage001',
+ vendorId: 'com.example.vendor',
+ }
+
+ await service.requestHandler(station, OCPP20RequestCommand.DATA_TRANSFER, payload)
+
+ assert.strictEqual(sendMessageMock.mock.calls.length, 1)
+
+ const sentPayload = sendMessageMock.mock.calls[0].arguments[2] as OCPP20DataTransferRequest
+ assert.strictEqual(sentPayload.vendorId, 'com.example.vendor')
+ assert.strictEqual(sentPayload.messageId, 'TestMessage001')
+ assert.strictEqual(sentPayload.data, 'test-payload-data')
+
+ const commandName = sendMessageMock.mock.calls[0].arguments[3]
+ assert.strictEqual(commandName, OCPP20RequestCommand.DATA_TRANSFER)
+ })
+
+ await it('should send DataTransfer with only required vendorId field', async () => {
+ const payload: OCPP20DataTransferRequest = {
+ vendorId: 'com.example.minimal',
+ }
+
+ await service.requestHandler(station, OCPP20RequestCommand.DATA_TRANSFER, payload)
+
+ assert.strictEqual(sendMessageMock.mock.calls.length, 1)
+
+ const sentPayload = sendMessageMock.mock.calls[0].arguments[2] as OCPP20DataTransferRequest
+ assert.strictEqual(sentPayload.vendorId, 'com.example.minimal')
+ assert.strictEqual(sentPayload.messageId, undefined)
+ assert.strictEqual(sentPayload.data, undefined)
+ })
+
+ await it('should pass through the payload as-is for DataTransfer command', async () => {
+ const payload: OCPP20DataTransferRequest = {
+ data: { nested: { key: 'value' }, numbers: [1, 2, 3] },
+ messageId: 'ComplexData',
+ vendorId: 'com.example.complex',
+ }
+
+ await service.requestHandler(station, OCPP20RequestCommand.DATA_TRANSFER, payload)
+
+ assert.strictEqual(sendMessageMock.mock.calls.length, 1)
+
+ const sentPayload = sendMessageMock.mock.calls[0].arguments[2] as OCPP20DataTransferRequest
+ assert.deepStrictEqual(sentPayload.data, {
+ nested: { key: 'value' },
+ numbers: [1, 2, 3],
+ })
+ assert.strictEqual(sentPayload.vendorId, 'com.example.complex')
+ assert.strictEqual(sentPayload.messageId, 'ComplexData')
+ })
+})
request = ocpp.v201.call.SetVariables(
set_variable_data=[
{
- "component": {"name": "ChargingStation"},
+ "component": {"name": "OCPPCommCtrlr"},
"variable": {"name": "HeartbeatInterval"},
"attribute_value": "30",
}
request_id=_random_request_id(),
report=True,
clear=False,
+ customer_identifier="test_customer_001",
)
await self._call_and_log(
request,
logPrefix,
mergeDeepRight,
once,
+ promiseWithTimeout,
queueMicrotaskErrorThrowing,
roundTo,
secureRandom,
assert.strictEqual(result, 'ABCDEFGH...')
})
})
+
+ await describe('promiseWithTimeout', async () => {
+ await it('should resolve with the promise value when it settles before timeout', async () => {
+ const result = await promiseWithTimeout(Promise.resolve(42), 1000, 'Timeout')
+ assert.strictEqual(result, 42)
+ })
+
+ await it('should reject with timeout Error when promise exceeds timeout', async t => {
+ await withMockTimers(t, ['setTimeout'], async () => {
+ const timeoutError = new Error('Operation timed out')
+ const racePromise = promiseWithTimeout(
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ new Promise<never>(() => {}),
+ 500,
+ timeoutError
+ )
+ t.mock.timers.tick(500)
+ await assert.rejects(racePromise, (error: Error) => {
+ assert.strictEqual(error, timeoutError)
+ return true
+ })
+ })
+ })
+
+ await it('should convert string timeoutError to Error on timeout', async t => {
+ await withMockTimers(t, ['setTimeout'], async () => {
+ const racePromise = promiseWithTimeout(
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ new Promise<never>(() => {}),
+ 500,
+ 'timed out'
+ )
+ t.mock.timers.tick(500)
+ await assert.rejects(racePromise, (error: Error) => {
+ assert.ok(error instanceof Error)
+ assert.strictEqual(error.message, 'timed out')
+ return true
+ })
+ })
+ })
+
+ await it('should preserve original rejection when promise rejects before timeout', async () => {
+ const originalError = new TypeError('Custom typed error')
+ await assert.rejects(
+ promiseWithTimeout(Promise.reject(originalError), 10000, 'Should not see this'),
+ (error: Error) => {
+ assert.strictEqual(error, originalError)
+ assert.ok(error instanceof TypeError)
+ return true
+ }
+ )
+ })
+ })
})