import { Device } from '../../domain/device/device.js' import { ParameterType } from '../../domain/shared/types.js' import { CWMPTaskQueue, ResultStatus, TaskResult } from './task_queue.js' import { reclassifyDeviceFromCatalog } from '../parameter_writability.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) } 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) } 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') await this.persistDevice(device) } catch (e) { logger.error(`Error organizing parameters: ${e}`) } } else { // Inspecter le fault code pour distinguer "non supporté" de "erreur transitoire" 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 if (code === 9000 || code === 9001) { device.setGpnStatus('unsupported') // Reclassify parameters using TR-181 catalog writability reclassifyDeviceFromCatalog(device) await this.persistDevice(device) } // Autres erreurs (réseau, timeout, 9002+) → gpnStatus inchangé } return await callback(result) } return this.taskQueue.addTask(device.deviceId, 'get_parameter_names', parameterName, wrappedCallback) } 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) } ) } 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) } ) } 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) } ) } 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) } ) } 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) } ) } 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) } ) } 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) } ) } 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) { device.updateFromValues(pvStructs as any) await this.persistDevice(device) device.tmpPC = '' return await callback(true) } return await callback(false) } ) } }