From ec7e66555d189763f511b7a948c324eb2d18ff2a Mon Sep 17 00:00:00 2001 From: Kevin L'Hereec Date: Mon, 8 Jun 2026 16:54:37 +0200 Subject: [PATCH] fix --- app/controllers/cwmp_controller.ts | 4 ++ app/domain/device/device.ts | 27 +++++++++++ app/services/cwmp/main_state_machine.ts | 9 ++-- tests/unit/domain/device.spec.ts | 64 +++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/app/controllers/cwmp_controller.ts b/app/controllers/cwmp_controller.ts index bbc560b..b82ae0e 100644 --- a/app/controllers/cwmp_controller.ts +++ b/app/controllers/cwmp_controller.ts @@ -131,6 +131,10 @@ export default class CwmpController { const paramValues = informData?.ParameterList?.ParameterValueStruct || [] if (paramValues.length > 0) { device.updateFromValues(paramValues) + // Some writable parameters arrive in the Inform before GPN has classified them. + // updateFromValues falls back to storing unknowns as read-only properties. + // Reclassify known-writable Inform parameters as parameters (writable). + device.reclassifyInformWritables(paramValues) } device.setTmpParamValues(paramValues) diff --git a/app/domain/device/device.ts b/app/domain/device/device.ts index 267b158..a7b32c3 100644 --- a/app/domain/device/device.ts +++ b/app/domain/device/device.ts @@ -255,6 +255,33 @@ export class Device { this._properties.delete(name) } + /** + * Parameters that arrive in the Inform payload are writable by definition + * (the CPE reports them because the ACS is supposed to manage them). + * updateFromValues() defaults unknowns to properties (read-only), so we + * promote the known-writable Inform params back to parameters here. + * + * Known writable Inform parameters (TR-098 and TR-181): + * - *.ProvisioningCode + * - *.ParameterKey + */ + public reclassifyInformWritables(paramValues: any[]): void { + const WRITABLE_PATTERNS = [ + /(^|\.)ProvisioningCode$/, + /(^|\.)ParameterKey$/, + ] + for (const pv of paramValues || []) { + const name = typeof pv?.Name === 'string' ? pv.Name : pv?.Name?._ || null + if (!name) continue + if (!WRITABLE_PATTERNS.some((re) => re.test(name))) continue + // Only act if it ended up as a property (wrong classification) + if (!this._properties.has(name)) continue + const prop = this._properties.get(name)! + this._properties.delete(name) + this._parameters.set(name, new DeviceParameter(name, prop.value, prop.type)) + } + } + public getConnectionRequestURL(): URL | null { for (const property of this._properties.values()) { const url = property.getConnectionRequestURL() diff --git a/app/services/cwmp/main_state_machine.ts b/app/services/cwmp/main_state_machine.ts index 6ce1628..bb49b95 100644 --- a/app/services/cwmp/main_state_machine.ts +++ b/app/services/cwmp/main_state_machine.ts @@ -59,9 +59,10 @@ class IdleState extends DeviceState { '[IdleState] GPN allowed — proceeding to discovery' ) - // Discover mandatory subtrees via GPN. - // DeviceInfo is always included so that HardwareVersion, SoftwareVersion, - // ModelName, etc. are classified (writable vs read-only) on every session. + // GPN is supported: discover mandatory subtrees to classify parameters. + // DeviceInfo. is always included so HardwareVersion, SoftwareVersion, ModelName etc. + // are classified (writable vs read-only) from the device itself. + // If GPN is not supported, the catalog handles classification (reclassifyDeviceFromCatalog). const dataModel = device.dataModel if (dataModel === 'tr-181') { return { @@ -80,7 +81,7 @@ class IdleState extends DeviceState { response: '', } } - // Unknown data model — skip GPN test, go to CriticalReadsState + // Unknown data model — skip GPN, go to CriticalReadsState return { nextState: new CriticalReadsState(), response: '' } } } diff --git a/tests/unit/domain/device.spec.ts b/tests/unit/domain/device.spec.ts index 990c504..454143e 100644 --- a/tests/unit/domain/device.spec.ts +++ b/tests/unit/domain/device.spec.ts @@ -226,3 +226,67 @@ describe('Device', () => { expect(d.getParameter('Device.X')!.value).toBe('new') }) }) + +describe('Device.reclassifyInformWritables', () => { + it('promotes ProvisioningCode from property to parameter', () => { + const d = makeDevice() + // Simulate what updateFromValues does on Inform: stores as property (unknown writability) + d.setProperty(new DeviceProperty('InternetGatewayDevice.DeviceInfo.ProvisioningCode', 'abc')) + + d.reclassifyInformWritables([ + { Name: 'InternetGatewayDevice.DeviceInfo.ProvisioningCode', Value: { _: 'abc' } }, + ]) + + expect(d.getParameter('InternetGatewayDevice.DeviceInfo.ProvisioningCode')).not.toBeNull() + expect(d.getProperty('InternetGatewayDevice.DeviceInfo.ProvisioningCode')).toBeNull() + }) + + it('promotes ParameterKey from property to parameter', () => { + const d = makeDevice() + d.setProperty(new DeviceProperty('InternetGatewayDevice.ManagementServer.ParameterKey', '')) + + d.reclassifyInformWritables([ + { Name: 'InternetGatewayDevice.ManagementServer.ParameterKey', Value: { _: '' } }, + ]) + + expect(d.getParameter('InternetGatewayDevice.ManagementServer.ParameterKey')).not.toBeNull() + expect(d.getProperty('InternetGatewayDevice.ManagementServer.ParameterKey')).toBeNull() + }) + + it('does not touch params that are already parameters', () => { + const d = makeDevice() + d.setParameter(new DeviceParameter('Device.DeviceInfo.ProvisioningCode', 'existing')) + + d.reclassifyInformWritables([ + { Name: 'Device.DeviceInfo.ProvisioningCode', Value: { _: 'existing' } }, + ]) + + expect(d.getParameter('Device.DeviceInfo.ProvisioningCode')!.value).toBe('existing') + expect(d.getProperty('Device.DeviceInfo.ProvisioningCode')).toBeNull() + }) + + it('does not reclassify unknown names (e.g. SoftwareVersion stays property)', () => { + const d = makeDevice() + d.setProperty(new DeviceProperty('InternetGatewayDevice.DeviceInfo.SoftwareVersion', 'v1')) + + d.reclassifyInformWritables([ + { Name: 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', Value: { _: 'v1' } }, + ]) + + // SoftwareVersion is NOT in the writable list — should stay as property + expect(d.getProperty('InternetGatewayDevice.DeviceInfo.SoftwareVersion')).not.toBeNull() + expect(d.getParameter('InternetGatewayDevice.DeviceInfo.SoftwareVersion')).toBeNull() + }) + + it('handles Name as object node (XMLParser artefact)', () => { + const d = makeDevice() + d.setProperty(new DeviceProperty('Device.DeviceInfo.ProvisioningCode', 'code')) + + d.reclassifyInformWritables([ + { Name: { _: 'Device.DeviceInfo.ProvisioningCode' }, Value: { _: 'code' } }, + ]) + + expect(d.getParameter('Device.DeviceInfo.ProvisioningCode')).not.toBeNull() + expect(d.getProperty('Device.DeviceInfo.ProvisioningCode')).toBeNull() + }) +})