Files
adam-acs/app/services/cwmp/device_action_executor.ts
T
2026-06-08 16:46:38 +02:00

418 lines
13 KiB
TypeScript

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<void> {
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<string>
): Promise<string> {
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<string>
): Promise<string> {
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<string>
): Promise<string> {
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<string>
): Promise<string> {
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, string> = {
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<string>
): Promise<string> {
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<string>
): Promise<string> {
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<string>
): Promise<string> {
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<string>
): Promise<string> {
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<string>
): Promise<string> {
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<string>
): Promise<string> {
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<string>
): Promise<string> {
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
}
}