import { Device } from '../../domain/device/device.js' import { DeviceParameter } from '../../domain/device/device_parameter.js' import { ParameterType } from '../../domain/shared/types.js' import { CWMPTaskQueue, ResultStatus, TaskResult } from './task_queue.js' import DeviceModel from '../../models/device.js' import logger from '@adonisjs/core/services/logger' // Parameter types (simplified from legacy provisioning domain) interface SetParameterValue { parameterName: string value: string | number | boolean type?: string } interface SetParameterAttribute { name: string notificationChange: boolean notification: number accessListChange: boolean accessList: string[] } interface SetScheduleInform { delayInSeconds: number commandKey?: string } interface DownloadParameter { commandKey: string fileType: string url: string username?: string password?: string fileSize?: number targetFileName?: string delaySeconds?: number successUrl?: string failureUrl?: string } interface UploadParameter { commandKey: string fileType: string url: string username?: string password?: string delayInSeconds?: number } interface FactoryResetParameter { commandKey?: string } interface RebootParameter { commandKey?: string } /** * Executes CWMP actions on devices via the TaskQueue * Each action wraps a callback that persists state on success */ export class DeviceActionExecutor { private taskQueue: CWMPTaskQueue constructor(taskQueue: CWMPTaskQueue) { this.taskQueue = taskQueue } private async persistDevice(device: Device): Promise { try { const snapshot = device.toSnapshot() await DeviceModel.createOrUpdate({ identifier: snapshot.identifier, deviceId: snapshot.deviceId, properties: snapshot.properties, parameters: snapshot.parameters, gpnStatus: snapshot.gpnStatus, dataModel: snapshot.dataModel, updatedAt: new Date(), }) } catch (e) { logger.error(`Failed to persist device: ${e}`) } } /** * Get parameter values from device. */ async getParameterValues( device: Device, parameterPaths: string[], callback: (result: TaskResult) => Promise ): Promise { const wrappedCallback = async (result: TaskResult) => { if (result.status === ResultStatus.Success) { try { const resp: any = result.data const list = resp?.ParameterList?.ParameterValueStruct if (Array.isArray(list) && list.length > 0) { device.updateFromValues(list) await this.persistDevice(device) } } catch (e) { logger.error(`Error updating device from parameter values: ${e}`) } } return await callback(result) } return this.taskQueue.addTask(device.deviceId, 'get_parameter_values', parameterPaths, wrappedCallback).xml } async getParameterAttributes( device: Device, parameterList: string[], callback: (result: TaskResult) => Promise ): Promise { const wrappedCallback = async (result: TaskResult) => { if (result.status === ResultStatus.Success) { try { const resp: any = result.data const list = resp?.ParameterList?.ParameterAttributeStruct if (Array.isArray(list) && list.length > 0) { for (const attr of list) { const name: string | undefined = attr?.Name if (!name) continue const notification: number | undefined = typeof attr?.Notification === 'number' ? attr.Notification : undefined const accessList: string[] | undefined = attr?.AccessList?.string const param = device.getParameter(name) if (param) { param.updateAttributes?.(notification, accessList) continue } const prop = device.getProperty(name) if (prop) { prop.updateAttributes?.(notification, accessList) } } await this.persistDevice(device) } } catch (e) { logger.error(`Error updating device from parameter attributes: ${e}`) } } return await callback(result) } return this.taskQueue.addTask(device.deviceId, 'get_parameter_attributes', parameterList, wrappedCallback).xml } async getParameterNames( device: Device, parameterName: string, callback: (result: TaskResult) => Promise ): Promise { const wrappedCallback = async (result: TaskResult) => { if (result.status === ResultStatus.Success) { try { device.organizeParameters(result.data as any, new Date()) device.setGpnStatus('supported') device.recordGpnSuccess() device.lastGpnSentDate = new Date() await this.persistDevice(device) } catch (e) { logger.error(`Error organizing parameters: ${e}`) } } else { // Inspect fault code to distinguish "unsupported" from "transient error" const faultCode = result.data?.FaultCode ?? result.data?.faultCode ?? result.data?.Detail?.Fault?.FaultCode ?? result.data?.detail?.Fault?.FaultCode const code = typeof faultCode === 'string' ? parseInt(faultCode, 10) : faultCode const GPN_UNSUPPORTED_CODES = [9000, 9001, 9003, 9004, 9005] const GPN_TRANSIENT_CODES = [9002] if (GPN_UNSUPPORTED_CODES.includes(code)) { device.setGpnStatus('unsupported') device.recordGpnFailure() await this.persistDevice(device) } else if (GPN_TRANSIENT_CODES.includes(code)) { // Transient error: log warning, do not mark unsupported, allow retry this session logger.warn({ code, deviceId: device.deviceId?.serialNumber }, 'GPN transient error (9002) — will not mark unsupported') } // Network timeout or other codes: do not mark unsupported, skip GPN for this session only } return await callback(result) } return this.taskQueue.addTask(device.deviceId, 'get_parameter_names', parameterName, wrappedCallback).xml } async applySetParameterValues( device: Device, parameters: SetParameterValue[], callback: (result: boolean) => Promise ): Promise { const existingParameters = device.getParameters() const inferType = (val: any): string => { const t = typeof val switch (t) { case 'boolean': return 'xsd:boolean' case 'number': return Number.isInteger(val) ? 'xsd:int' : 'xsd:string' default: return 'xsd:string' } } const cwmpTypeMap: Record = { string: 'xsd:string', unsignedInt: 'xsd:unsignedInt', int: 'xsd:int', boolean: 'xsd:boolean', dateTime: 'xsd:dateTime', } const pvStructs = parameters.map((p) => { const existing = existingParameters.find((ep) => ep.name === p.parameterName) // Priority: 1) explicit type from config, 2) known device type, 3) inferred from JS value let xsdType: string if (p.type && cwmpTypeMap[p.type]) { xsdType = cwmpTypeMap[p.type] } else if (existing?.type && existing.type !== ParameterType.UNKNOWN) { xsdType = existing.type // already in xsd: format (e.g. 'xsd:string') } else { xsdType = inferType(p.value) } return { Name: p.parameterName, Value: { _: p.value, $: { Type: xsdType } }, } }) const parameterKey = `SetPValues_${device.deviceId.serialNumber}_${Date.now()}` return this.taskQueue.addTask( device.deviceId, 'set_parameter_values', { parameterValueList: pvStructs, parameterKey }, async (result: TaskResult) => { if (result.status === ResultStatus.Success) { device.updateFromValues(pvStructs as any) await this.persistDevice(device) return await callback(true) } return await callback(false) } ).xml } async applySetParameterAttributes( device: Device, parameters: SetParameterAttribute[], callback: (result: boolean) => Promise ): Promise { const attrs = parameters.map((p) => ({ name: p.name, notificationChange: p.notificationChange ?? false, notification: p.notification ?? 0, accessListChange: p.accessListChange ?? false, accessList: (p.accessList as any)?.string || [], })) return this.taskQueue.addTask( device.deviceId, 'set_parameter_attributes', attrs, async (result: TaskResult) => { if (result.status === ResultStatus.Success) { return await callback(true) } return await callback(false) } ).xml } async applyScheduleInform( device: Device, parameter: SetScheduleInform, callback: (result: boolean) => Promise ): Promise { const commandKey = parameter.commandKey || `ScheduleInform_${device.deviceId.serialNumber}_${Date.now()}` return this.taskQueue.addTask( device.deviceId, 'schedule_inform', { delayInSeconds: parameter.delayInSeconds, commandKey }, async (result: TaskResult) => { return await callback(result.status === ResultStatus.Success) } ).xml } async applyFactoryReset( _device: Device, _parameter: FactoryResetParameter, callback: (result: boolean) => Promise ): Promise { return this.taskQueue.addTask( _device.deviceId, 'factory_reset', {}, async (result: TaskResult) => { return await callback(result.status === ResultStatus.Success) } ).xml } async applyReboot( device: Device, parameter: RebootParameter, callback: (result: boolean) => Promise ): Promise { return this.taskQueue.addTask( device.deviceId, 'reboot', { commandKey: parameter.commandKey || '' }, async (result: TaskResult) => { return await callback(result.status === ResultStatus.Success) } ).xml } async applyDownload( device: Device, parameter: DownloadParameter, callback: (result: boolean) => Promise ): Promise { return this.taskQueue.addTask( device.deviceId, 'download', { commandKey: parameter.commandKey, fileType: parameter.fileType, url: parameter.url, username: parameter.username, password: parameter.password, fileSize: parameter.fileSize, targetFileName: parameter.targetFileName, delaySeconds: parameter.delaySeconds, successUrl: parameter.successUrl, failureUrl: parameter.failureUrl, }, async (result: TaskResult) => { return await callback(result.status === ResultStatus.Success) } ).xml } async applyUpload( device: Device, parameter: UploadParameter, callback: (result: boolean) => Promise ): Promise { return this.taskQueue.addTask( device.deviceId, 'upload', { commandKey: parameter.commandKey, fileType: parameter.fileType, url: parameter.url, username: parameter.username, password: parameter.password, delaySeconds: parameter.delayInSeconds, }, async (result: TaskResult) => { return await callback(result.status === ResultStatus.Success) } ).xml } async insertProvisioningCode( device: Device, callback: (result: boolean) => Promise ): Promise { const allParams = device.getParameters() const allProps = device.getProperties() const knownPC = [...allParams, ...allProps].find((p: any) => /(^|\.)ProvisioningCode$/.test(p?.name)) // Use the detected data model stored on the device; fall back to scanning known params let provisioningPath: string if (knownPC) { provisioningPath = knownPC.name } else if (device.dataModel === 'tr-181') { provisioningPath = 'Device.DeviceInfo.ProvisioningCode' } else if (device.dataModel === 'tr-098') { provisioningPath = 'InternetGatewayDevice.DeviceInfo.ProvisioningCode' } else { // Legacy fallback: scan stored params const hasDeviceTree = [...allParams, ...allProps].some((p: any) => typeof p?.name === 'string' && p.name.startsWith('Device.')) provisioningPath = hasDeviceTree ? 'Device.DeviceInfo.ProvisioningCode' : 'InternetGatewayDevice.DeviceInfo.ProvisioningCode' } const parameterKey = `SetPValues_${device.deviceId.serialNumber}_${Date.now()}` const pvStructs = [{ Name: provisioningPath, Value: { _: device.tmpPC, $: { Type: 'xsd:string' } } }] return this.taskQueue.addTask( device.deviceId, 'set_parameter_values', { parameterValueList: pvStructs, parameterKey }, async (result: TaskResult) => { if (result.status === ResultStatus.Success) { // ProvisioningCode must always be stored as a writable parameter. // If it was previously classified as a read-only property (wrong), remove it first. device.removeProperty(provisioningPath) device.setParameter(new DeviceParameter(provisioningPath, device.tmpPC, ParameterType.STRING)) await this.persistDevice(device) device.tmpPC = '' return await callback(true) } return await callback(false) } ).xml } }