From 5c93607d1f1553e17a80205253bdea564eb12c90 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 29 Oct 2025 23:25:09 +0100 Subject: [PATCH] test(ocpp20): add GetVariables list defaults coverage and update registry enumerations --- .../ocpp/2.0/OCPP20VariableRegistry.ts | 51 +++++++++++---- ...comingRequestService-GetBaseReport.test.ts | 3 +- ...ncomingRequestService-GetVariables.test.ts | 64 +++++++++++++++++++ .../ocpp/2.0/OCPP20VariableManager.test.ts | 50 +++++++++++++-- 4 files changed, 147 insertions(+), 21 deletions(-) diff --git a/src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts b/src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts index 7bc7aa2a..80f95db7 100644 --- a/src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts +++ b/src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts @@ -229,9 +229,16 @@ export const VARIABLE_REGISTRY: Record = { )]: { component: OCPP20ComponentName.ClockCtrlr as string, dataType: DataEnumType.SequenceList, - defaultValue: 'NTP,GPS,RTC', + defaultValue: 'NTP,GPS,RealTimeClock,Heartbeat', description: 'Ordered list of clock sources by preference.', - enumeration: ['NTP', 'GPS', 'RTC', 'Manual'], + enumeration: [ + 'Heartbeat', + 'NTP', + 'GPS', + 'RealTimeClock', + 'MobileNetwork', + 'RadioTimeTransmitter', + ], mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], @@ -444,10 +451,10 @@ export const VARIABLE_REGISTRY: Record = { )]: { component: OCPP20ComponentName.OCPPCommCtrlr as string, dataType: DataEnumType.MemberList, - defaultValue: 'HTTP', + defaultValue: 'HTTPS,FTPS,SFTP', description: 'Supported file transfer protocols.', - enumeration: ['HTTP', 'HTTPS', 'FTP', 'SFTP'], - mutability: MutabilityEnumType.ReadWrite, + enumeration: ['HTTP', 'HTTPS', 'FTP', 'FTPS', 'SFTP'], + mutability: MutabilityEnumType.ReadOnly, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], variable: OCPP20RequiredVariableName.FileTransferProtocols as string, @@ -514,8 +521,9 @@ export const VARIABLE_REGISTRY: Record = { )]: { component: OCPP20ComponentName.OCPPCommCtrlr as string, dataType: DataEnumType.SequenceList, - defaultValue: '', + defaultValue: '1,2,3', description: 'Comma separated ordered list of network profile priorities.', + enumeration: ['1', '2', '3'], mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], @@ -583,7 +591,7 @@ export const VARIABLE_REGISTRY: Record = { variable: OCPP20RequiredVariableName.UnlockOnEVSideDisconnect as string, }, - // SampledDataCtrlr variables (simulation measurands) + // SampledDataCtrlr variables [buildRegistryKey(OCPP20ComponentName.SampledDataCtrlr as string, 'Current.Import')]: { component: OCPP20ComponentName.SampledDataCtrlr as string, dataType: DataEnumType.decimal, @@ -637,7 +645,8 @@ export const VARIABLE_REGISTRY: Record = { )]: { component: OCPP20ComponentName.SampledDataCtrlr as string, dataType: DataEnumType.MemberList, - defaultValue: 'Energy.Active.Import.Register,Current.Import', + // Default includes cumulative energy and interval energy plus voltage for billing context + defaultValue: 'Energy.Active.Import.Register,Energy.Active.Import.Interval,Voltage', description: 'Measurands sampled at transaction end.', enumeration: [ 'Energy.Active.Import.Register', @@ -668,16 +677,25 @@ export const VARIABLE_REGISTRY: Record = { )]: { component: OCPP20ComponentName.SampledDataCtrlr as string, dataType: DataEnumType.MemberList, - defaultValue: 'Energy.Active.Import.Register,Power.Active.Import', + defaultValue: 'Energy.Active.Import.Register,Power.Active.Import,Voltage', description: 'Measurands sampled at transaction start.', enumeration: [ 'Energy.Active.Import.Register', + 'Energy.Active.Import.Interval', 'Energy.Active.Export.Register', 'Power.Active.Import', 'Power.Active.Export', + 'Power.Reactive.Import', + 'Power.Reactive.Export', + 'Power.Offered', 'Current.Import', + 'Current.Export', 'Voltage', + 'Frequency', + 'Temperature', 'SoC', + 'RPM', + 'Power.Factor', ], mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, @@ -709,16 +727,25 @@ export const VARIABLE_REGISTRY: Record = { )]: { component: OCPP20ComponentName.SampledDataCtrlr as string, dataType: DataEnumType.MemberList, - defaultValue: 'Energy.Active.Import.Register', + defaultValue: 'Energy.Active.Import.Register,Current.Import,Voltage', description: 'Measurands included in periodic updates.', enumeration: [ 'Energy.Active.Import.Register', + 'Energy.Active.Import.Interval', 'Energy.Active.Export.Register', 'Power.Active.Import', 'Power.Active.Export', + 'Power.Reactive.Import', + 'Power.Reactive.Export', + 'Power.Offered', 'Current.Import', + 'Current.Export', 'Voltage', + 'Frequency', + 'Temperature', 'SoC', + 'RPM', + 'Power.Factor', ], mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, @@ -844,10 +871,8 @@ export const VARIABLE_REGISTRY: Record = { )]: { component: OCPP20ComponentName.TxCtrlr as string, dataType: DataEnumType.MemberList, - // Spec-aligned default (exclude EnergyTransfer & DataSigned due to measurement skew and signed data optionality) defaultValue: 'Authorized,EVConnected', description: 'Trigger conditions for starting a transaction.', - // Spec-aligned enumeration per errata: Authorized, EVConnected, PowerPathClosed, EnergyTransfer, ParkingBayOccupancy plus DataSigned (start only) enumeration: [ 'Authorized', 'EVConnected', @@ -865,10 +890,8 @@ export const VARIABLE_REGISTRY: Record = { { component: OCPP20ComponentName.TxCtrlr as string, dataType: DataEnumType.MemberList, - // Spec-aligned default stop triggers (exclude Authorized by default to avoid id re-presentation auto-stop) defaultValue: 'EVConnected,PowerPathClosed', description: 'Trigger conditions for ending a transaction.', - // Spec-aligned enumeration per errata: Authorized, EVConnected, PowerPathClosed, EnergyTransfer, ParkingBayOccupancy (DataSigned excluded as invalid stop point) enumeration: [ 'Authorized', 'EVConnected', diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetBaseReport.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetBaseReport.test.ts index 2e05f3b2..f69f8ce4 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetBaseReport.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetBaseReport.test.ts @@ -280,7 +280,8 @@ await describe('B08 - Get Base Report', async () => { // Choose TimeSource (SequenceList) and construct an artificially long ordered list value > 10 chars const variableManager = OCPP20VariableManager.getInstance() - const longValue = 'NTP,GPS,RTC,Manual' + // Use members exceeding 10 chars total; exclude removed RTC/Manual. + const longValue = 'Heartbeat,NTP,GPS,RealTimeClock,MobileNetwork,RadioTimeTransmitter' // Set Actual (SequenceList). Should accept full value internally. const setResult: OCPP20SetVariableResultType[] = variableManager.setVariables( mockChargingStation, diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetVariables.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetVariables.test.ts index 3ae17add..3429c5c0 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetVariables.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetVariables.test.ts @@ -365,6 +365,70 @@ void describe('B06 - Get Variables', () => { expect(result.attributeValue).toBe('30') }) + void it('Should retrieve list/sequence defaults for FileTransferProtocols, TimeSource, NetworkConfigurationPriority', () => { + const request: OCPP20GetVariablesRequest = { + getVariableData: [ + { + component: { name: OCPP20ComponentName.OCPPCommCtrlr }, + variable: { name: OCPP20RequiredVariableName.FileTransferProtocols }, + }, + { + component: { name: OCPP20ComponentName.ClockCtrlr }, + variable: { name: OCPP20RequiredVariableName.TimeSource }, + }, + { + component: { name: OCPP20ComponentName.OCPPCommCtrlr }, + variable: { name: OCPP20RequiredVariableName.NetworkConfigurationPriority }, + }, + ], + } + const response = incomingRequestService.handleRequestGetVariables(mockChargingStation, request) + expect(response.getVariableResult).toHaveLength(3) + const fileTransfer = response.getVariableResult[0] + expect(fileTransfer.attributeStatus).toBe(GetVariableStatusEnumType.Accepted) + expect(fileTransfer.attributeValue).toBe('HTTPS,FTPS,SFTP') + const timeSource = response.getVariableResult[1] + expect(timeSource.attributeStatus).toBe(GetVariableStatusEnumType.Accepted) + expect(timeSource.attributeValue).toBe('NTP,GPS,RealTimeClock,Heartbeat') + const netConfigPriority = response.getVariableResult[2] + expect(netConfigPriority.attributeStatus).toBe(GetVariableStatusEnumType.Accepted) + expect(netConfigPriority.attributeValue).toBe('1,2,3') + }) + + void it('Should retrieve list defaults for TxStartedMeasurands, TxEndedMeasurands, TxUpdatedMeasurands', () => { + const request: OCPP20GetVariablesRequest = { + getVariableData: [ + { + component: { name: OCPP20ComponentName.SampledDataCtrlr }, + variable: { name: OCPP20RequiredVariableName.TxStartedMeasurands }, + }, + { + component: { name: OCPP20ComponentName.SampledDataCtrlr }, + variable: { name: OCPP20RequiredVariableName.TxEndedMeasurands }, + }, + { + component: { name: OCPP20ComponentName.SampledDataCtrlr }, + variable: { name: OCPP20RequiredVariableName.TxUpdatedMeasurands }, + }, + ], + } + const response = incomingRequestService.handleRequestGetVariables(mockChargingStation, request) + expect(response.getVariableResult).toHaveLength(3) + const txStarted = response.getVariableResult[0] + expect(txStarted.attributeStatus).toBe(GetVariableStatusEnumType.Accepted) + expect(txStarted.attributeValue).toBe( + 'Energy.Active.Import.Register,Power.Active.Import,Voltage' + ) + const txEnded = response.getVariableResult[1] + expect(txEnded.attributeStatus).toBe(GetVariableStatusEnumType.Accepted) + expect(txEnded.attributeValue).toBe( + 'Energy.Active.Import.Register,Energy.Active.Import.Interval,Voltage' + ) + const txUpdated = response.getVariableResult[2] + expect(txUpdated.attributeStatus).toBe(GetVariableStatusEnumType.Accepted) + expect(txUpdated.attributeValue).toBe('Energy.Active.Import.Register,Current.Import,Voltage') + }) + // FR: B06.FR.13 void it('Should reject Target attribute for NetworkConfigurationPriority', () => { const request: OCPP20GetVariablesRequest = { diff --git a/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts b/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts index 3c9d8e79..034878c2 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts @@ -1189,15 +1189,15 @@ await describe('OCPP20VariableManager test suite', async () => { await describe('List validation tests', async () => { const manager = OCPP20VariableManager.getInstance() - await it('Should accept valid updates to list/sequence list variables', () => { - const validUpdates: OCPP20SetVariableDataType[] = [ + await it('Should accept valid updates to writable list/sequence list variables and reject read-only', () => { + const updateAttempts: OCPP20SetVariableDataType[] = [ { - attributeValue: 'HTTP,HTTPS', + attributeValue: 'HTTP,HTTPS', // FileTransferProtocols now ReadOnly -> expect rejection component: { name: OCPP20ComponentName.OCPPCommCtrlr }, variable: { name: OCPP20RequiredVariableName.FileTransferProtocols }, }, { - attributeValue: 'GPS,NTP,RTC', // reorder TimeSource + attributeValue: 'Heartbeat,NTP,GPS', // valid TimeSource reorder (RTC & Manual removed, RealTimeClock optional) component: { name: OCPP20ComponentName.ClockCtrlr }, variable: { name: OCPP20RequiredVariableName.TimeSource }, }, @@ -1228,12 +1228,50 @@ await describe('OCPP20VariableManager test suite', async () => { variable: { name: OCPP20RequiredVariableName.TxUpdatedMeasurands }, }, ] - const results = manager.setVariables(mockChargingStation, validUpdates) - for (const r of results) { + const results = manager.setVariables(mockChargingStation, updateAttempts) + // First (FileTransferProtocols) should be rejected (ReadOnly); others accepted + expect(results[0].attributeStatus).toBe(SetVariableStatusEnumType.Rejected) + expect(results[0].attributeStatusInfo?.reasonCode).toBe(ReasonCodeEnumType.ReadOnly) + for (const r of results.slice(1)) { expect(r.attributeStatus).toBe(SetVariableStatusEnumType.Accepted) } }) + await it('Should retrieve FileTransferProtocols default including FTPS (ReadOnly)', () => { + const getRes = manager.getVariables(mockChargingStation, [ + { + component: { name: OCPP20ComponentName.OCPPCommCtrlr }, + variable: { name: OCPP20RequiredVariableName.FileTransferProtocols }, + }, + ])[0] + expect(getRes.attributeStatus).toBe(GetVariableStatusEnumType.Accepted) + expect(getRes.attributeValue).toBe('HTTPS,FTPS,SFTP') + }) + + await it('Should reject removed TimeSource members RTC and Manual', () => { + const res = manager.setVariables(mockChargingStation, [ + { + attributeValue: 'NTP,GPS,RTC,Manual', // RTC & Manual no longer valid + component: { name: OCPP20ComponentName.ClockCtrlr }, + variable: { name: OCPP20RequiredVariableName.TimeSource }, + }, + ])[0] + expect(res.attributeStatus).toBe(SetVariableStatusEnumType.Rejected) + expect(res.attributeStatusInfo?.reasonCode).toBe(ReasonCodeEnumType.InvalidValue) + expect(res.attributeStatusInfo?.additionalInfo).toContain('Member not in enumeration') + }) + + await it('Should accept extended TimeSource including RealTimeClock and MobileNetwork', () => { + const res = manager.setVariables(mockChargingStation, [ + { + attributeValue: 'MobileNetwork,Heartbeat,NTP,GPS,RealTimeClock', + component: { name: OCPP20ComponentName.ClockCtrlr }, + variable: { name: OCPP20RequiredVariableName.TimeSource }, + }, + ])[0] + expect(res.attributeStatus).toBe(SetVariableStatusEnumType.Accepted) + }) + await it('Should reject invalid list formats and members', () => { interface ListVar { component: OCPP20ComponentName -- 2.43.0