fix: eslint consistent-type-imports and eqeqeq violations
- Convert value imports used only as types to 'import type' across all modified files (cwmp_message_builder, task_queue, device_action_executor, main_state_machine, immediate_command_state_machine, configuration_evaluation_state, cwmp_controller, cwmp_middleware, device.ts, soap.ts, soap_envelope.ts, tr069_types.ts) - Replace != null with !== null && !== undefined in main_state_machine and cwmp_controller
This commit is contained in:
1 parent
ab45275a89
commit
9cceb139b7
13 files changed
+518
-158
No files matched your search
@@ -61,7 +61,12 @@ export default class CwmpController {
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
{ sessionId, remoteKey, messageType: message?.type ?? 'none', cwmpId: envelope?.header?.id },
|
||||
{
|
||||
sessionId,
|
||||
remoteKey,
|
||||
messageType: message?.type ?? 'none',
|
||||
cwmpId: envelope?.header?.id,
|
||||
},
|
||||
'CWMP message routing'
|
||||
)
|
||||
|
||||
@@ -76,7 +81,10 @@ export default class CwmpController {
|
||||
request.header('x-forwarded-for') || ''
|
||||
)
|
||||
ctx.cwmpDeviceId = hashId
|
||||
logger.debug({ sessionId, hashId, responseLength: xml.length }, 'CWMP sending InformResponse')
|
||||
logger.debug(
|
||||
{ sessionId, hashId, responseLength: xml.length },
|
||||
'CWMP sending InformResponse'
|
||||
)
|
||||
return this.sendSoap(response, xml)
|
||||
}
|
||||
|
||||
@@ -140,10 +148,17 @@ export default class CwmpController {
|
||||
let device = await this.loadDevice(deviceId)
|
||||
if (!device) {
|
||||
device = new Device(deviceId)
|
||||
logger.info({ deviceId: deviceId.toIdentifier() }, '[handleInform] Device not found in DB — creating new')
|
||||
logger.info(
|
||||
{ deviceId: deviceId.toIdentifier() },
|
||||
'[handleInform] Device not found in DB — creating new'
|
||||
)
|
||||
} else {
|
||||
logger.info(
|
||||
{ deviceId: deviceId.toIdentifier(), gpnStatus: device.gpnStatus, circuitBreaker: device.gpnCircuitBreaker },
|
||||
{
|
||||
deviceId: deviceId.toIdentifier(),
|
||||
gpnStatus: device.gpnStatus,
|
||||
circuitBreaker: device.gpnCircuitBreaker,
|
||||
},
|
||||
'[handleInform] Device loaded from DB'
|
||||
)
|
||||
}
|
||||
@@ -197,7 +212,7 @@ export default class CwmpController {
|
||||
: []
|
||||
const events: string[] = eventStructArray
|
||||
.map((e) => e?.EventCode)
|
||||
.filter((c) => c != null)
|
||||
.filter((c) => c !== null && c !== undefined)
|
||||
.map((c) => String(c))
|
||||
|
||||
logger.info({ events }, 'CWMP Inform events extracted')
|
||||
@@ -245,11 +260,17 @@ export default class CwmpController {
|
||||
{ identifier: hashId },
|
||||
{ $set: { references, updatedAt: new Date() } }
|
||||
).catch(() => {})
|
||||
logger.debug({ hashId, providers: references.map((r) => r.provider) }, '[References] Resolved')
|
||||
logger.debug(
|
||||
{ hashId, providers: references.map((r) => r.provider) },
|
||||
'[References] Resolved'
|
||||
)
|
||||
}
|
||||
} catch (_e: unknown) {
|
||||
const err = _e as Error
|
||||
logger.error({ error: err?.message, stack: err?.stack }, '[References] Error during resolution')
|
||||
logger.error(
|
||||
{ error: err?.message, stack: err?.stack },
|
||||
'[References] Error during resolution'
|
||||
)
|
||||
}
|
||||
|
||||
return { xml: CWMPMessageBuilder.informResponse(cwmpId), hashId }
|
||||
@@ -290,10 +311,14 @@ export default class CwmpController {
|
||||
)
|
||||
|
||||
record.properties?.forEach((p: { name: string; value: unknown; type: string }) =>
|
||||
device.setProperty(new DeviceProperty(p.name, p.value, (p.type as ParameterType) ?? ParameterType.UNKNOWN))
|
||||
device.setProperty(
|
||||
new DeviceProperty(p.name, p.value, (p.type as ParameterType) ?? ParameterType.UNKNOWN)
|
||||
)
|
||||
)
|
||||
record.parameters?.forEach((p: { name: string; value: unknown; type: string }) =>
|
||||
device.setParameter(new DeviceParameter(p.name, p.value, (p.type as ParameterType) ?? ParameterType.UNKNOWN))
|
||||
device.setParameter(
|
||||
new DeviceParameter(p.name, p.value, (p.type as ParameterType) ?? ParameterType.UNKNOWN)
|
||||
)
|
||||
)
|
||||
device.setGpnStatus(record.gpnStatus ?? 'unknown')
|
||||
device.setDataModel(record.dataModel ?? 'unknown')
|
||||
|
||||
+28
-10
@@ -1,5 +1,10 @@
|
||||
import { DeviceId } from './device_id.js'
|
||||
import { DeviceParameter, DeviceProperty, ParameterSnapshot, PropertySnapshot } from './device_parameter.js'
|
||||
import { type DeviceId } from './device_id.js'
|
||||
import {
|
||||
DeviceParameter,
|
||||
DeviceProperty,
|
||||
type ParameterSnapshot,
|
||||
type PropertySnapshot,
|
||||
} from './device_parameter.js'
|
||||
import { ParameterType } from '../shared/types.js'
|
||||
import { createHash } from 'node:crypto'
|
||||
import type { ParameterValueStruct, ParameterInfoStruct } from '../../protocol/tr069_types.js'
|
||||
@@ -271,12 +276,9 @@ export class Device {
|
||||
* - *.ParameterKey
|
||||
*/
|
||||
public reclassifyInformWritables(paramValues: ParameterValueStruct[]): void {
|
||||
const WRITABLE_PATTERNS = [
|
||||
/(^|\.)ProvisioningCode$/,
|
||||
/(^|\.)ParameterKey$/,
|
||||
]
|
||||
const WRITABLE_PATTERNS = [/(^|\.)ProvisioningCode$/, /(^|\.)ParameterKey$/]
|
||||
for (const pv of paramValues || []) {
|
||||
const name = typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null
|
||||
const name = typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null
|
||||
if (!name) continue
|
||||
if (!WRITABLE_PATTERNS.some((re) => re.test(name))) continue
|
||||
// Only act if it ended up as a property (wrong classification)
|
||||
@@ -357,7 +359,7 @@ export class Device {
|
||||
const hasTextNode = valueObj && Object.prototype.hasOwnProperty.call(valueObj, '_')
|
||||
const valueObjRaw = valueObj as unknown as Record<string, unknown> | undefined
|
||||
const hasTypeAttr = valueObjRaw && (valueObjRaw['$xsi:type'] || valueObj?.$?.Type)
|
||||
const val = hasTextNode ? valueObj?._ : (hasTypeAttr ? '' : undefined)
|
||||
const val = hasTextNode ? valueObj?._ : hasTypeAttr ? '' : undefined
|
||||
const typeStr = (valueObjRaw?.['$xsi:type'] as string | undefined) || valueObj?.$?.Type
|
||||
const mappedType = this.mapType(typeStr)
|
||||
|
||||
@@ -370,7 +372,15 @@ export class Device {
|
||||
const snap = existing.toSnapshot()
|
||||
this._parameters.set(
|
||||
name,
|
||||
new DeviceParameter(name, snap.value, mappedType, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList)
|
||||
new DeviceParameter(
|
||||
name,
|
||||
snap.value,
|
||||
mappedType,
|
||||
snap.createdAt,
|
||||
snap.updatedAt,
|
||||
snap.notification,
|
||||
snap.accessList
|
||||
)
|
||||
)
|
||||
}
|
||||
this.updateParameter(name, val)
|
||||
@@ -380,7 +390,15 @@ export class Device {
|
||||
const snap = existing.toSnapshot()
|
||||
this._properties.set(
|
||||
name,
|
||||
new DeviceProperty(name, snap.value, mappedType, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList)
|
||||
new DeviceProperty(
|
||||
name,
|
||||
snap.value,
|
||||
mappedType,
|
||||
snap.createdAt,
|
||||
snap.updatedAt,
|
||||
snap.notification,
|
||||
snap.accessList
|
||||
)
|
||||
)
|
||||
}
|
||||
this.updateProperty(name, val)
|
||||
|
||||
@@ -82,7 +82,10 @@ export default class CwmpMiddleware {
|
||||
cwmpVersion: envelope.cwmpVersion,
|
||||
cwmpId: envelope.header?.id,
|
||||
messageType: message?.type ?? 'unknown',
|
||||
messageKeys: message?.data && typeof message.data === 'object' ? Object.keys(message.data as object) : [],
|
||||
messageKeys:
|
||||
message?.data && typeof message.data === 'object'
|
||||
? Object.keys(message.data as object)
|
||||
: [],
|
||||
},
|
||||
'CWMP XML parsed'
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseModel } from '#database/base_model'
|
||||
import { ConfigurationVersion } from '../domain/configuration/configuration_version.js'
|
||||
import { type ConfigurationVersion } from '../domain/configuration/configuration_version.js'
|
||||
import { Sort } from 'mongodb'
|
||||
|
||||
export default class ConfigurationVersionModel extends BaseModel {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
Cwmp_1_0,
|
||||
Download,
|
||||
FactoryReset,
|
||||
FaultCodeType,
|
||||
type FaultCodeType,
|
||||
FaultDetail,
|
||||
FaultStruct,
|
||||
Fault,
|
||||
@@ -26,10 +26,15 @@ import {
|
||||
CwmpID,
|
||||
NAMESPACES,
|
||||
SoapEnvelope,
|
||||
SoapEnvelopeAttribute,
|
||||
type SoapEnvelopeAttribute,
|
||||
SoapEnvelopeHeader,
|
||||
} from '../soap_envelope.js'
|
||||
import type { DownloadTaskPayload, UploadTaskPayload, SetParameterValuesTaskPayload, SetParameterAttributesTaskPayload } from '../tr069_types.js'
|
||||
import type {
|
||||
DownloadTaskPayload,
|
||||
UploadTaskPayload,
|
||||
SetParameterValuesTaskPayload,
|
||||
SetParameterAttributesTaskPayload,
|
||||
} from '../tr069_types.js'
|
||||
|
||||
function generateCwmpID(): string {
|
||||
return randomBytes(4).toString('hex')
|
||||
@@ -54,7 +59,11 @@ export class CWMPMessageBuilder {
|
||||
return enve.getXml()
|
||||
}
|
||||
|
||||
static getParameterNames(cwmpId: string | undefined, parameterPath: string, nextLevel: boolean = true): string {
|
||||
static getParameterNames(
|
||||
cwmpId: string | undefined,
|
||||
parameterPath: string,
|
||||
nextLevel: boolean = true
|
||||
): string {
|
||||
const enve = makeEnvelope(cwmpId)
|
||||
enve.Body.GetParameterNames = new GetParameterNames({
|
||||
ParameterPath: parameterPath,
|
||||
@@ -89,10 +98,13 @@ export class CWMPMessageBuilder {
|
||||
return enve.getXml()
|
||||
}
|
||||
|
||||
static setParameterValuesFromPayload(cwmpId: string | undefined, payload: SetParameterValuesTaskPayload): string {
|
||||
static setParameterValuesFromPayload(
|
||||
cwmpId: string | undefined,
|
||||
payload: SetParameterValuesTaskPayload
|
||||
): string {
|
||||
const params = (payload.parameterValueList || []).map((p) => ({
|
||||
name: p.Name,
|
||||
value: String(p.Value?._ ?? ''),
|
||||
value: String(p.Value?._ ?? ''),
|
||||
type: p.Value?.$?.Type || 'xsd:string',
|
||||
}))
|
||||
return CWMPMessageBuilder.setParameterValues(cwmpId, params)
|
||||
@@ -140,7 +152,11 @@ export class CWMPMessageBuilder {
|
||||
return enve.getXml()
|
||||
}
|
||||
|
||||
static scheduleInform(cwmpId: string | undefined, delaySeconds: number, commandKey?: string): string {
|
||||
static scheduleInform(
|
||||
cwmpId: string | undefined,
|
||||
delaySeconds: number,
|
||||
commandKey?: string
|
||||
): string {
|
||||
const enve = makeEnvelope(cwmpId)
|
||||
enve.Body.ScheduleInform = new ScheduleInform({
|
||||
DelaySeconds: delaySeconds,
|
||||
@@ -151,7 +167,9 @@ export class CWMPMessageBuilder {
|
||||
|
||||
static getParameterAttributes(cwmpId: string | undefined, parameterNames: string[]): string {
|
||||
const enve = makeEnvelope(cwmpId)
|
||||
enve.Body.GetParameterAttributes = new GetParameterAttributes({ ParameterNames: parameterNames })
|
||||
enve.Body.GetParameterAttributes = new GetParameterAttributes({
|
||||
ParameterNames: parameterNames,
|
||||
})
|
||||
return enve.getXml()
|
||||
}
|
||||
|
||||
@@ -185,7 +203,9 @@ export class CWMPMessageBuilder {
|
||||
const fault = new Fault()
|
||||
fault.Faultcode = 'Server'
|
||||
fault.Faultstring = reason
|
||||
fault.Detail = new FaultDetail({ Fault: new FaultStruct({ FaultCode: faultCode, FaultString: reason }) })
|
||||
fault.Detail = new FaultDetail({
|
||||
Fault: new FaultStruct({ FaultCode: faultCode, FaultString: reason }),
|
||||
})
|
||||
return fault.getXml()
|
||||
}
|
||||
|
||||
|
||||
@@ -53,9 +53,7 @@ export function parseSoapXml(xml: string): SoapEnvelope {
|
||||
const idNode = header['cwmp:ID'] || header['cwmp:Id']
|
||||
if (idNode) {
|
||||
soapHeader.id =
|
||||
typeof idNode === 'string'
|
||||
? idNode
|
||||
: (idNode as { _?: string })?._ || String(idNode)
|
||||
typeof idNode === 'string' ? idNode : (idNode as { _?: string })?._ || String(idNode)
|
||||
}
|
||||
|
||||
return { header: soapHeader, body, cwmpVersion }
|
||||
|
||||
@@ -119,11 +119,15 @@ export class SoapEnvelopeHeader {
|
||||
constructor(props: Record<string, unknown> = {}) {
|
||||
this.ID = new CwmpID(props?.ID as any)
|
||||
this.NoMoreRequests = (props?.NoMoreRequests as number) || 0
|
||||
this.SessionTimeout = props?.SessionTimeout ? new SessionTimeout(props.SessionTimeout) : undefined
|
||||
this.SessionTimeout = props?.SessionTimeout
|
||||
? new SessionTimeout(props.SessionTimeout)
|
||||
: undefined
|
||||
this.SupportedCWMPVersions = props?.SupportedCWMPVersions
|
||||
? new SupportedCWMPVersions(props.SupportedCWMPVersions)
|
||||
: undefined
|
||||
this.UseCWMPVersion = props?.UseCWMPVersion ? new UseCWMPVersion(props.UseCWMPVersion) : undefined
|
||||
this.UseCWMPVersion = props?.UseCWMPVersion
|
||||
? new UseCWMPVersion(props.UseCWMPVersion)
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,16 +139,28 @@ export class SoapEnvelope {
|
||||
|
||||
constructor(props: Record<string, unknown> = {}) {
|
||||
this.version = (props?.version as '1.0' | '1.1' | '1.2' | '1.3' | '1.4') || '1.0'
|
||||
this.$ = props?.$ ? new SoapEnvelopeAttribute(props.$ as any) : new SoapEnvelopeAttribute(NAMESPACES[this.version])
|
||||
this.$ = props?.$
|
||||
? new SoapEnvelopeAttribute(props.$ as any)
|
||||
: new SoapEnvelopeAttribute(NAMESPACES[this.version])
|
||||
this.Header = new SoapEnvelopeHeader((props.Header as Record<string, unknown>) || {})
|
||||
|
||||
const bodyProps = (props.Body as Record<string, unknown>) || {}
|
||||
switch (this.version) {
|
||||
case '1.4': this.Body = new Cwmp_1_4(bodyProps); break
|
||||
case '1.3': this.Body = new Cwmp_1_3(bodyProps); break
|
||||
case '1.2': this.Body = new Cwmp_1_2(bodyProps); break
|
||||
case '1.1': this.Body = new Cwmp_1_1(bodyProps); break
|
||||
default: this.Body = new Cwmp_1_0(bodyProps); break
|
||||
case '1.4':
|
||||
this.Body = new Cwmp_1_4(bodyProps)
|
||||
break
|
||||
case '1.3':
|
||||
this.Body = new Cwmp_1_3(bodyProps)
|
||||
break
|
||||
case '1.2':
|
||||
this.Body = new Cwmp_1_2(bodyProps)
|
||||
break
|
||||
case '1.1':
|
||||
this.Body = new Cwmp_1_1(bodyProps)
|
||||
break
|
||||
default:
|
||||
this.Body = new Cwmp_1_0(bodyProps)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,15 +185,15 @@ export interface RebootTaskPayload {
|
||||
}
|
||||
|
||||
export type CWMPTaskPayload =
|
||||
| string // get_parameter_names
|
||||
| string[] // get_parameter_values / get_parameter_attributes
|
||||
| string // get_parameter_names
|
||||
| string[] // get_parameter_values / get_parameter_attributes
|
||||
| SetParameterValuesTaskPayload
|
||||
| SetParameterAttributesTaskPayload[]
|
||||
| ScheduleInformTaskPayload
|
||||
| DownloadTaskPayload
|
||||
| UploadTaskPayload
|
||||
| RebootTaskPayload
|
||||
| Record<string, never> // factory_reset
|
||||
| Record<string, never> // factory_reset
|
||||
|
||||
/** Données de résultat retournées dans TaskResult.data selon le type de tâche */
|
||||
export type CWMPTaskResultData =
|
||||
|
||||
@@ -3,7 +3,10 @@ import type { EngineResult } from '../configuration_engine.js'
|
||||
import type { DeviceStateResult, DeviceContext } from './main_state_machine.js'
|
||||
import { PartialOperationalState } from './main_state_machine.js'
|
||||
import DeviceModel from '#models/device'
|
||||
import type { GetParameterNamesResponse, GetParameterValuesResponse } from '../../protocol/tr069_types.js'
|
||||
import type {
|
||||
GetParameterNamesResponse,
|
||||
GetParameterValuesResponse,
|
||||
} from '../../protocol/tr069_types.js'
|
||||
import logger from '@adonisjs/core/services/logger'
|
||||
import { ComplianceStatus } from '../../domain/configuration/compliance.js'
|
||||
import { buildProvisioningCode } from '../../utils/provisioning_code.js'
|
||||
@@ -55,7 +58,8 @@ export class ConfigurationEvaluationState {
|
||||
{
|
||||
'compliance.status': ComplianceStatus.IN_PROGRESS,
|
||||
'compliance.lastEvaluatedAt': new Date(),
|
||||
'compliance.configurationIdentifier': this.engineResult.configuration?.identifier || null,
|
||||
'compliance.configurationIdentifier':
|
||||
this.engineResult.configuration?.identifier || null,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -72,7 +76,10 @@ export class ConfigurationEvaluationState {
|
||||
|
||||
return this.processNextAction(context)
|
||||
} catch (error: any) {
|
||||
logger.error({ identifier: context.device.identifier, error: error.message }, '[ConfigEval] Evaluation failed')
|
||||
logger.error(
|
||||
{ identifier: context.device.identifier, error: error.message },
|
||||
'[ConfigEval] Evaluation failed'
|
||||
)
|
||||
|
||||
await DeviceModel.updateOne(
|
||||
{ identifier: context.device.identifier },
|
||||
@@ -83,11 +90,13 @@ export class ConfigurationEvaluationState {
|
||||
}
|
||||
).catch(() => {})
|
||||
|
||||
return { nextState: new PartialOperationalState(context.device, {
|
||||
reason: 'configuration_evaluation_failed',
|
||||
blockingState: 'configuration-evaluation',
|
||||
error: error.message,
|
||||
}) }
|
||||
return {
|
||||
nextState: new PartialOperationalState(context.device, {
|
||||
reason: 'configuration_evaluation_failed',
|
||||
blockingState: 'configuration-evaluation',
|
||||
error: error.message,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,17 +110,27 @@ export class ConfigurationEvaluationState {
|
||||
const leafNames: string[] = infoList
|
||||
.map((info) => {
|
||||
const n = info?.Name
|
||||
return typeof n === 'string' ? n : (n && typeof (n as { _?: string })._ === 'string' ? (n as { _: string })._ : null)
|
||||
return typeof n === 'string'
|
||||
? n
|
||||
: n && typeof (n as { _?: string })._ === 'string'
|
||||
? (n as { _: string })._
|
||||
: null
|
||||
})
|
||||
.filter((name): name is string => !!name && !name.endsWith('.'))
|
||||
if (leafNames.length > 0) {
|
||||
this.pendingValuesQueue.push(...leafNames)
|
||||
}
|
||||
logger.debug({ identifier: context.device.identifier, leafCount: leafNames.length }, '[ConfigEval] Inventory GPN response processed inline')
|
||||
logger.debug(
|
||||
{ identifier: context.device.identifier, leafCount: leafNames.length },
|
||||
'[ConfigEval] Inventory GPN response processed inline'
|
||||
)
|
||||
} else if (context.message && !context.message.success) {
|
||||
// GPN failed during inventory — record failure
|
||||
context.device.recordGpnFailure()
|
||||
logger.debug({ identifier: context.device.identifier }, '[ConfigEval] Inventory GPN failed — skipping remaining inventory')
|
||||
logger.debug(
|
||||
{ identifier: context.device.identifier },
|
||||
'[ConfigEval] Inventory GPN failed — skipping remaining inventory'
|
||||
)
|
||||
this.inventoryQueue = []
|
||||
}
|
||||
} else if (this.awaitingPhase === 'gpv_response') {
|
||||
@@ -122,19 +141,30 @@ export class ConfigurationEvaluationState {
|
||||
if (list.length > 0) {
|
||||
const normalized = list.map((pv) => ({
|
||||
...pv,
|
||||
Name: typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || pv?.Name,
|
||||
Name:
|
||||
typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || pv?.Name,
|
||||
}))
|
||||
context.device.updateFromValues(normalized)
|
||||
logger.debug({ identifier: context.device.identifier, count: normalized.length }, '[ConfigEval] Inventory GPV values saved to device')
|
||||
logger.debug(
|
||||
{ identifier: context.device.identifier, count: normalized.length },
|
||||
'[ConfigEval] Inventory GPV values saved to device'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Inventory phase 1: GetParameterNames per subtree path
|
||||
// Skip if device does not support GPN or circuit breaker is active
|
||||
if (this.inventoryQueue.length > 0 && context.device.gpnStatus !== 'unsupported' && context.device.isGpnAllowed()) {
|
||||
if (
|
||||
this.inventoryQueue.length > 0 &&
|
||||
context.device.gpnStatus !== 'unsupported' &&
|
||||
context.device.isGpnAllowed()
|
||||
) {
|
||||
const path = this.inventoryQueue.shift()!
|
||||
logger.debug({ identifier: context.device.identifier, path }, '[ConfigEval] Inventory GetParameterNames')
|
||||
logger.debug(
|
||||
{ identifier: context.device.identifier, path },
|
||||
'[ConfigEval] Inventory GetParameterNames'
|
||||
)
|
||||
const response = await context.services.deviceAction.getParameterNames(
|
||||
context.device,
|
||||
path,
|
||||
@@ -144,14 +174,20 @@ export class ConfigurationEvaluationState {
|
||||
return { response }
|
||||
} else if (this.inventoryQueue.length > 0) {
|
||||
// GPN unsupported or circuit breaker active — skip inventory
|
||||
logger.debug({ identifier: context.device.identifier, skipped: this.inventoryQueue.length }, '[ConfigEval] Skipping inventory (GPN unsupported or circuit breaker active)')
|
||||
logger.debug(
|
||||
{ identifier: context.device.identifier, skipped: this.inventoryQueue.length },
|
||||
'[ConfigEval] Skipping inventory (GPN unsupported or circuit breaker active)'
|
||||
)
|
||||
this.inventoryQueue = []
|
||||
}
|
||||
|
||||
// 2. Inventory phase 2: GetParameterValues for discovered leaf names
|
||||
if (this.pendingValuesQueue.length > 0) {
|
||||
const leaves = this.pendingValuesQueue.splice(0)
|
||||
logger.debug({ identifier: context.device.identifier, count: leaves.length }, '[ConfigEval] Inventory GetParameterValues')
|
||||
logger.debug(
|
||||
{ identifier: context.device.identifier, count: leaves.length },
|
||||
'[ConfigEval] Inventory GetParameterValues'
|
||||
)
|
||||
const response = await context.services.deviceAction.getParameterValues(
|
||||
context.device,
|
||||
leaves,
|
||||
@@ -164,7 +200,10 @@ export class ConfigurationEvaluationState {
|
||||
// 3. All parameter diffs in one SetParameterValues RPC
|
||||
if (this.paramDiffs.length > 0) {
|
||||
const diffs = this.paramDiffs.splice(0)
|
||||
logger.debug({ identifier: context.device.identifier, count: diffs.length }, '[ConfigEval] Applying parameter diffs')
|
||||
logger.debug(
|
||||
{ identifier: context.device.identifier, count: diffs.length },
|
||||
'[ConfigEval] Applying parameter diffs'
|
||||
)
|
||||
const params = diffs.map((d) => ({ parameterName: d.name, value: d.value, type: d.type }))
|
||||
const response = await context.services.deviceAction.applySetParameterValues(
|
||||
context.device,
|
||||
@@ -177,10 +216,19 @@ export class ConfigurationEvaluationState {
|
||||
// 4. Firmware upgrade (if needed and within schedule)
|
||||
if (this.engineResult!.firmwareMigrationPath.length > 0 && !this.engineResult!.skipFirmware) {
|
||||
logger.debug(
|
||||
{ identifier: context.device.identifier, steps: this.engineResult!.firmwareMigrationPath.length },
|
||||
{
|
||||
identifier: context.device.identifier,
|
||||
steps: this.engineResult!.firmwareMigrationPath.length,
|
||||
},
|
||||
'[ConfigEval] Transitioning to firmware upgrade'
|
||||
)
|
||||
return { nextState: new FirmwareUpgradeState(this.engineResult!.firmwareMigrationPath, 0, this.engineResult!) }
|
||||
return {
|
||||
nextState: new FirmwareUpgradeState(
|
||||
this.engineResult!.firmwareMigrationPath,
|
||||
0,
|
||||
this.engineResult!
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// 5. All done
|
||||
@@ -214,7 +262,11 @@ export class FirmwareUpgradeState {
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
{ identifier: context.device.identifier, step: this.currentStepIndex + 1, total: this.migrationSteps.length },
|
||||
{
|
||||
identifier: context.device.identifier,
|
||||
step: this.currentStepIndex + 1,
|
||||
total: this.migrationSteps.length,
|
||||
},
|
||||
'[FirmwareUpgrade] Sending Download RPC'
|
||||
)
|
||||
|
||||
@@ -233,7 +285,10 @@ export class FirmwareUpgradeState {
|
||||
|
||||
return { response }
|
||||
} catch (error: any) {
|
||||
logger.error({ identifier: context.device.identifier, error: error.message }, '[FirmwareUpgrade] Download RPC failed')
|
||||
logger.error(
|
||||
{ identifier: context.device.identifier, error: error.message },
|
||||
'[FirmwareUpgrade] Download RPC failed'
|
||||
)
|
||||
|
||||
await DeviceModel.updateOne(
|
||||
{ identifier: context.device.identifier },
|
||||
@@ -279,7 +334,8 @@ export class ComplianceUpdateState {
|
||||
}
|
||||
|
||||
if (this.engineResult.configuration) {
|
||||
updateData['compliance.configurationIdentifier'] = this.engineResult.configuration.identifier
|
||||
updateData['compliance.configurationIdentifier'] =
|
||||
this.engineResult.configuration.identifier
|
||||
}
|
||||
|
||||
// Always build and write provisioning code to device (functional for "once", informative for "maintain")
|
||||
@@ -292,18 +348,32 @@ export class ComplianceUpdateState {
|
||||
const currentPCValue = currentPC?.value ?? ''
|
||||
|
||||
if (currentPCValue === provCode) {
|
||||
logger.debug({ identifier: context.device.identifier, provCode }, '[ComplianceUpdate] Provisioning code already matches — skipping SPV')
|
||||
logger.debug(
|
||||
{ identifier: context.device.identifier, provCode },
|
||||
'[ComplianceUpdate] Provisioning code already matches — skipping SPV'
|
||||
)
|
||||
updateData.provisioningCode = provCode
|
||||
await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(() => {})
|
||||
await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(
|
||||
() => {}
|
||||
)
|
||||
return { nextState: new ExecutingCommandsState(true) }
|
||||
}
|
||||
|
||||
updateData.provisioningCode = provCode
|
||||
|
||||
await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(() => {})
|
||||
await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(
|
||||
() => {}
|
||||
)
|
||||
|
||||
context.device.tmpPC = provCode
|
||||
logger.debug({ identifier: context.device.identifier, provCode, policy: this.engineResult.configuration.policy }, '[ComplianceUpdate] Writing provisioning code')
|
||||
logger.debug(
|
||||
{
|
||||
identifier: context.device.identifier,
|
||||
provCode,
|
||||
policy: this.engineResult.configuration.policy,
|
||||
},
|
||||
'[ComplianceUpdate] Writing provisioning code'
|
||||
)
|
||||
|
||||
const response = await context.services.deviceAction.insertProvisioningCode(
|
||||
context.device,
|
||||
@@ -312,8 +382,13 @@ export class ComplianceUpdateState {
|
||||
return { response }
|
||||
}
|
||||
|
||||
await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(() => {})
|
||||
logger.debug({ identifier: context.device.identifier }, '[ComplianceUpdate] Compliance finalized (no configuration)')
|
||||
await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(
|
||||
() => {}
|
||||
)
|
||||
logger.debug(
|
||||
{ identifier: context.device.identifier },
|
||||
'[ComplianceUpdate] Compliance finalized (no configuration)'
|
||||
)
|
||||
return { nextState: new ExecutingCommandsState(true) }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Device } from '../../domain/device/device.js'
|
||||
import { type 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 { type CWMPTaskQueue, ResultStatus, type TaskResult } from './task_queue.js'
|
||||
import DeviceModel from '../../models/device.js'
|
||||
import logger from '@adonisjs/core/services/logger'
|
||||
import type {
|
||||
@@ -116,7 +116,12 @@ export class DeviceActionExecutor {
|
||||
return await callback(result)
|
||||
}
|
||||
|
||||
return this.taskQueue.addTask(device.deviceId, 'get_parameter_values', parameterPaths, wrappedCallback).xml
|
||||
return this.taskQueue.addTask(
|
||||
device.deviceId,
|
||||
'get_parameter_values',
|
||||
parameterPaths,
|
||||
wrappedCallback
|
||||
).xml
|
||||
}
|
||||
|
||||
async getParameterAttributes(
|
||||
@@ -128,11 +133,14 @@ export class DeviceActionExecutor {
|
||||
if (result.status === ResultStatus.Success) {
|
||||
try {
|
||||
const resp = result.data as GetParameterAttributesResponse
|
||||
const list: ParameterAttributeStruct[] = resp?.ParameterList?.ParameterAttributeStruct ?? []
|
||||
const list: ParameterAttributeStruct[] =
|
||||
resp?.ParameterList?.ParameterAttributeStruct ?? []
|
||||
if (list.length > 0) {
|
||||
for (const attr of list) {
|
||||
const name: string | undefined =
|
||||
typeof attr?.Name === 'string' ? attr.Name : (attr?.Name as { _?: string } | undefined)?._
|
||||
typeof attr?.Name === 'string'
|
||||
? attr.Name
|
||||
: (attr?.Name as { _?: string } | undefined)?._
|
||||
if (!name) continue
|
||||
const notification: number | undefined =
|
||||
typeof attr?.Notification === 'number' ? attr.Notification : undefined
|
||||
@@ -157,7 +165,12 @@ export class DeviceActionExecutor {
|
||||
return await callback(result)
|
||||
}
|
||||
|
||||
return this.taskQueue.addTask(device.deviceId, 'get_parameter_attributes', parameterList, wrappedCallback).xml
|
||||
return this.taskQueue.addTask(
|
||||
device.deviceId,
|
||||
'get_parameter_attributes',
|
||||
parameterList,
|
||||
wrappedCallback
|
||||
).xml
|
||||
}
|
||||
|
||||
async getParameterNames(
|
||||
@@ -180,13 +193,18 @@ export class DeviceActionExecutor {
|
||||
}
|
||||
} else {
|
||||
// Inspect fault code to distinguish "unsupported" from "transient error"
|
||||
const faultData = result.data as { FaultCode?: number | string; faultCode?: number | string; Detail?: { Fault?: { FaultCode?: number | string } }; detail?: { Fault?: { FaultCode?: number | string } } }
|
||||
const faultData = result.data as {
|
||||
FaultCode?: number | string
|
||||
faultCode?: number | string
|
||||
Detail?: { Fault?: { FaultCode?: number | string } }
|
||||
detail?: { Fault?: { FaultCode?: number | string } }
|
||||
}
|
||||
const faultCode =
|
||||
faultData?.FaultCode ??
|
||||
faultData?.faultCode ??
|
||||
faultData?.Detail?.Fault?.FaultCode ??
|
||||
faultData?.detail?.Fault?.FaultCode
|
||||
const code = typeof faultCode === 'string' ? parseInt(faultCode, 10) : faultCode
|
||||
const code = typeof faultCode === 'string' ? Number.parseInt(faultCode, 10) : faultCode
|
||||
const GPN_UNSUPPORTED_CODES = [9000, 9001, 9003, 9004, 9005]
|
||||
const GPN_TRANSIENT_CODES = [9002]
|
||||
|
||||
@@ -195,13 +213,21 @@ export class DeviceActionExecutor {
|
||||
device.recordGpnFailure()
|
||||
await this.persistDevice(device)
|
||||
} else if (code !== undefined && GPN_TRANSIENT_CODES.includes(code)) {
|
||||
logger.warn({ code, deviceId: device.deviceId?.serialNumber }, 'GPN transient error (9002) — will not mark unsupported')
|
||||
logger.warn(
|
||||
{ code, deviceId: device.deviceId?.serialNumber },
|
||||
'GPN transient error (9002) — will not mark unsupported'
|
||||
)
|
||||
}
|
||||
}
|
||||
return await callback(result)
|
||||
}
|
||||
|
||||
return this.taskQueue.addTask(device.deviceId, 'get_parameter_names', parameterName, wrappedCallback).xml
|
||||
return this.taskQueue.addTask(
|
||||
device.deviceId,
|
||||
'get_parameter_names',
|
||||
parameterName,
|
||||
wrappedCallback
|
||||
).xml
|
||||
}
|
||||
|
||||
async applySetParameterValues(
|
||||
@@ -214,9 +240,12 @@ export class DeviceActionExecutor {
|
||||
const inferType = (val: string | number | boolean): 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'
|
||||
case 'boolean':
|
||||
return 'xsd:boolean'
|
||||
case 'number':
|
||||
return Number.isInteger(val) ? 'xsd:int' : 'xsd:string'
|
||||
default:
|
||||
return 'xsd:string'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +321,8 @@ export class DeviceActionExecutor {
|
||||
parameter: SetScheduleInform,
|
||||
callback: (result: boolean) => Promise<string>
|
||||
): Promise<string> {
|
||||
const commandKey = parameter.commandKey || `ScheduleInform_${device.deviceId.serialNumber}_${Date.now()}`
|
||||
const commandKey =
|
||||
parameter.commandKey || `ScheduleInform_${device.deviceId.serialNumber}_${Date.now()}`
|
||||
return this.taskQueue.addTask(
|
||||
device.deviceId,
|
||||
'schedule_inform',
|
||||
@@ -407,7 +437,9 @@ export class DeviceActionExecutor {
|
||||
}
|
||||
|
||||
const parameterKey = `SetPValues_${device.deviceId.serialNumber}_${Date.now()}`
|
||||
const pvStructs = [{ Name: provisioningPath, Value: { _: device.tmpPC as string, $: { Type: 'xsd:string' } } }]
|
||||
const pvStructs = [
|
||||
{ Name: provisioningPath, Value: { _: device.tmpPC as string, $: { Type: 'xsd:string' } } },
|
||||
]
|
||||
|
||||
return this.taskQueue.addTask(
|
||||
device.deviceId,
|
||||
@@ -416,7 +448,9 @@ export class DeviceActionExecutor {
|
||||
async (result: TaskResult) => {
|
||||
if (result.status === ResultStatus.Success) {
|
||||
device.removeProperty(provisioningPath)
|
||||
device.setParameter(new DeviceParameter(provisioningPath, device.tmpPC, ParameterType.STRING))
|
||||
device.setParameter(
|
||||
new DeviceParameter(provisioningPath, device.tmpPC, ParameterType.STRING)
|
||||
)
|
||||
await this.persistDevice(device)
|
||||
device.tmpPC = ''
|
||||
return await callback(true)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Device } from '../../domain/device/device.js'
|
||||
import { CWMPTaskQueue } from './task_queue.js'
|
||||
import { type Device } from '../../domain/device/device.js'
|
||||
import { type CWMPTaskQueue } from './task_queue.js'
|
||||
import { DeviceActionExecutor } from './device_action_executor.js'
|
||||
import { ExecutingCommandsState, DeviceContext, DeviceStateResult } from './main_state_machine.js'
|
||||
import {
|
||||
ExecutingCommandsState,
|
||||
type DeviceContext,
|
||||
type DeviceStateResult,
|
||||
} from './main_state_machine.js'
|
||||
import type { CWMPTaskResultData } from '../../protocol/tr069_types.js'
|
||||
import logger from '@adonisjs/core/services/logger'
|
||||
|
||||
@@ -27,7 +31,9 @@ export class ImmediateCommandStateMachine {
|
||||
}
|
||||
|
||||
async start(): Promise<string> {
|
||||
logger.info(`Starting immediate-command state machine for device ${this.context.device.deviceId?.serialNumber}`)
|
||||
logger.info(
|
||||
`Starting immediate-command state machine for device ${this.context.device.deviceId?.serialNumber}`
|
||||
)
|
||||
return this.execute()
|
||||
}
|
||||
|
||||
@@ -41,7 +47,11 @@ export class ImmediateCommandStateMachine {
|
||||
}
|
||||
|
||||
isCompleted(): boolean {
|
||||
return this.currentState.name === 'completed' || this.currentState.name === 'operational' || this.currentState.name === 'partial_operational'
|
||||
return (
|
||||
this.currentState.name === 'completed' ||
|
||||
this.currentState.name === 'operational' ||
|
||||
this.currentState.name === 'partial_operational'
|
||||
)
|
||||
}
|
||||
|
||||
private async execute(): Promise<string> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Device } from '../../domain/device/device.js'
|
||||
import { CWMPTaskQueue } from './task_queue.js'
|
||||
import { type Device } from '../../domain/device/device.js'
|
||||
import { type CWMPTaskQueue } from './task_queue.js'
|
||||
import { DeviceActionExecutor } from './device_action_executor.js'
|
||||
import { getDeviceHashIdentifier } from '../../utils/device_utils.js'
|
||||
import { CommandStatus, CommandType } from '../../domain/commands/command.js'
|
||||
@@ -21,7 +21,7 @@ import type {
|
||||
UploadParameter,
|
||||
RebootParameter,
|
||||
} from './device_action_executor.js'
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { type ObjectId } from 'mongodb'
|
||||
|
||||
// ─── Interfaces ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -67,13 +67,15 @@ class IdleState extends DeviceState {
|
||||
if (device.gpnStatus === 'unknown') {
|
||||
// Never classified → all tmpParams are unclassified
|
||||
for (const pv of tmpParams) {
|
||||
const name = typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null
|
||||
const name =
|
||||
typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null
|
||||
if (name) unclassified.push(name)
|
||||
}
|
||||
} else {
|
||||
// Already classified before → only params NOT in _parameters AND NOT in _properties are new
|
||||
for (const pv of tmpParams) {
|
||||
const name = typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null
|
||||
const name =
|
||||
typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null
|
||||
if (!name) continue
|
||||
if (!device.getParameter(name) && !device.getProperty(name)) {
|
||||
unclassified.push(name)
|
||||
@@ -109,7 +111,11 @@ class IdleState extends DeviceState {
|
||||
// Case 1: GPN unsupported or circuit breaker open → classify unclassified via catalog
|
||||
if (device.gpnStatus === 'unsupported' || !device.isGpnAllowed()) {
|
||||
logger.info(
|
||||
{ gpnStatus: device.gpnStatus, unclassifiedCount: unclassified.length, deviceId: device.identifier },
|
||||
{
|
||||
gpnStatus: device.gpnStatus,
|
||||
unclassifiedCount: unclassified.length,
|
||||
deviceId: device.identifier,
|
||||
},
|
||||
'[IdleState] GPN unavailable — classifying unclassified params via catalog'
|
||||
)
|
||||
reclassifyDeviceFromCatalog(device)
|
||||
@@ -120,7 +126,12 @@ class IdleState extends DeviceState {
|
||||
// Case 3: GPN unknown → send GPN to test support + classify
|
||||
const paths = Array.from(parentPaths).sort()
|
||||
logger.info(
|
||||
{ paths, unclassifiedCount: unclassified.length, gpnStatus: device.gpnStatus, deviceId: device.identifier },
|
||||
{
|
||||
paths,
|
||||
unclassifiedCount: unclassified.length,
|
||||
gpnStatus: device.gpnStatus,
|
||||
deviceId: device.identifier,
|
||||
},
|
||||
'[IdleState] Classifying unclassified params via GPN'
|
||||
)
|
||||
return { nextState: new DiscoveringParametersState(paths), response: '' }
|
||||
@@ -179,7 +190,11 @@ class DiscoveringParametersState extends DeviceState {
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
logger.info(
|
||||
{ hashId, gpnStatus: context.device.gpnStatus, circuitBreaker: context.device.gpnCircuitBreaker },
|
||||
{
|
||||
hashId,
|
||||
gpnStatus: context.device.gpnStatus,
|
||||
circuitBreaker: context.device.gpnCircuitBreaker,
|
||||
},
|
||||
'[DiscoveringParameters] Persisted GPN failure + catalog classification to DB'
|
||||
)
|
||||
} catch (e) {
|
||||
@@ -197,11 +212,17 @@ class DiscoveringParametersState extends DeviceState {
|
||||
context.device.organizeParameters(infoList, new Date())
|
||||
context.device.setGpnStatus('supported')
|
||||
logger.info(
|
||||
{ deviceId: context.device.identifier, paramCount: context.device.getParameters().length },
|
||||
{
|
||||
deviceId: context.device.identifier,
|
||||
paramCount: context.device.getParameters().length,
|
||||
},
|
||||
'[DiscoveringParameters] GPN response processed'
|
||||
)
|
||||
} catch (e: unknown) {
|
||||
logger.error({ error: (e as Error)?.message }, '[DiscoveringParameters] Error processing GPN response')
|
||||
logger.error(
|
||||
{ error: (e as Error)?.message },
|
||||
'[DiscoveringParameters] Error processing GPN response'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +237,9 @@ class DiscoveringParametersState extends DeviceState {
|
||||
private async processNext(context: DeviceContext): Promise<string> {
|
||||
if (this.parameterQueue.length === 0) return ''
|
||||
const current = this.parameterQueue.shift()!
|
||||
return context.services.deviceAction.getParameterNames(context.device, current, () => Promise.resolve(''))
|
||||
return context.services.deviceAction.getParameterNames(context.device, current, () =>
|
||||
Promise.resolve('')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,14 +300,21 @@ class CriticalReadsState extends DeviceState {
|
||||
// Normalize Name fields: XMLParser may return {_: "path", "$xsi:type": "..."} when Name has attributes
|
||||
const normalized = list.map((pv) => ({
|
||||
...pv,
|
||||
Name: typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || pv?.Name,
|
||||
Name:
|
||||
typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || pv?.Name,
|
||||
}))
|
||||
context.device.setTmpParamValues(normalized)
|
||||
context.device.updateFromValues(normalized)
|
||||
logger.info({ count: normalized.length, names: normalized.map((p) => p.Name) }, '[CriticalReads] GPV response values processed')
|
||||
logger.info(
|
||||
{ count: normalized.length, names: normalized.map((p) => p.Name) },
|
||||
'[CriticalReads] GPV response values processed'
|
||||
)
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
logger.error({ error: (e as Error)?.message }, '[CriticalReads] Error processing GPV response values')
|
||||
logger.error(
|
||||
{ error: (e as Error)?.message },
|
||||
'[CriticalReads] Error processing GPV response values'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,15 +346,33 @@ export class ExecutingCommandsState extends DeviceState {
|
||||
})
|
||||
|
||||
const collection = CommandModel.getCollection()
|
||||
const iWaiting = await collection.find({ deviceIdentifier: hashId, mode: ModeType.I, status: CommandStatus.WAITING }).sort({ priority: 1 }).toArray()
|
||||
const iPending = await collection.find({ deviceIdentifier: hashId, mode: ModeType.I, status: CommandStatus.PENDING }).sort({ priority: 1 }).toArray()
|
||||
const iWaiting = await collection
|
||||
.find({ deviceIdentifier: hashId, mode: ModeType.I, status: CommandStatus.WAITING })
|
||||
.sort({ priority: 1 })
|
||||
.toArray()
|
||||
const iPending = await collection
|
||||
.find({ deviceIdentifier: hashId, mode: ModeType.I, status: CommandStatus.PENDING })
|
||||
.sort({ priority: 1 })
|
||||
.toArray()
|
||||
const niPending = this.includeNI
|
||||
? await collection.find({ deviceIdentifier: hashId, mode: ModeType.NI, status: CommandStatus.PENDING }).sort({ priority: 1 }).toArray()
|
||||
? await collection
|
||||
.find({ deviceIdentifier: hashId, mode: ModeType.NI, status: CommandStatus.PENDING })
|
||||
.sort({ priority: 1 })
|
||||
.toArray()
|
||||
: []
|
||||
|
||||
this.commandsQueue = [...iWaiting, ...iPending, ...niPending]
|
||||
this.started = true
|
||||
logger.debug({ hashId, iWaiting: iWaiting.length, iPending: iPending.length, niPending: niPending.length, includeNI: this.includeNI }, '[ExecutingCommands] Loaded commands queue')
|
||||
logger.debug(
|
||||
{
|
||||
hashId,
|
||||
iWaiting: iWaiting.length,
|
||||
iPending: iPending.length,
|
||||
niPending: niPending.length,
|
||||
includeNI: this.includeNI,
|
||||
},
|
||||
'[ExecutingCommands] Loaded commands queue'
|
||||
)
|
||||
|
||||
if (this.commandsQueue.length === 0) {
|
||||
if (this.includeNI) {
|
||||
@@ -347,11 +395,17 @@ export class ExecutingCommandsState extends DeviceState {
|
||||
|
||||
// Optimistic update for SPV
|
||||
if (this.currentCommandRecord.action === CommandType.SET_PARAMETER_VALUES) {
|
||||
const rawList = Array.isArray(this.currentCommandRecord.parameters) ? this.currentCommandRecord.parameters as Array<{ name?: string; value?: unknown }> : []
|
||||
const rawList = Array.isArray(this.currentCommandRecord.parameters)
|
||||
? (this.currentCommandRecord.parameters as Array<{ name?: string; value?: unknown }>)
|
||||
: []
|
||||
const pvStructs: ParameterValueStruct[] = rawList.map((p) => ({
|
||||
'@class': '.ParameterValueStruct',
|
||||
Name: p?.name ?? '',
|
||||
Value: { '@class': '.ValueObject', _: String(p?.value ?? ''), $: { '@class': '.ValueObjectAttribute', Type: 'xsd:string' } },
|
||||
'Name': p?.name ?? '',
|
||||
'Value': {
|
||||
'@class': '.ValueObject',
|
||||
'_': String(p?.value ?? ''),
|
||||
'$': { '@class': '.ValueObjectAttribute', 'Type': 'xsd:string' },
|
||||
},
|
||||
}))
|
||||
if (pvStructs.length > 0) {
|
||||
context.device.updateFromValues(pvStructs)
|
||||
@@ -398,7 +452,11 @@ export class ExecutingCommandsState extends DeviceState {
|
||||
commandRecord.status = CommandStatus.SENT
|
||||
commandRecord.executedAt = now
|
||||
try {
|
||||
await CommandModel.updateCommand(commandRecord._id as ObjectId, { status: CommandStatus.SENT, executedAt: now, updatedAt: now })
|
||||
await CommandModel.updateCommand(commandRecord._id as ObjectId, {
|
||||
status: CommandStatus.SENT,
|
||||
executedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
} catch (err) {
|
||||
logger.error(`Failed to mark command as SENT: ${err}`)
|
||||
}
|
||||
@@ -410,73 +468,144 @@ export class ExecutingCommandsState extends DeviceState {
|
||||
|
||||
const action = commandRecord.action as CommandType
|
||||
// params is typed as unknown from MongoDB — we cast to a flexible record for access
|
||||
const params = commandRecord.parameters as Record<string, unknown> | string | unknown[] | undefined
|
||||
const params = commandRecord.parameters as
|
||||
| Record<string, unknown>
|
||||
| string
|
||||
| unknown[]
|
||||
| undefined
|
||||
|
||||
// Map CommandType to CWMP request
|
||||
switch (action) {
|
||||
case CommandType.GET_PARAMETER_VALUES: {
|
||||
const p = params as Record<string, unknown> | string[] | undefined
|
||||
const names: string[] = Array.isArray(p) ? (p as string[]) : ((p as Record<string, unknown>)?.names as string[] || (p as Record<string, unknown>)?.paths as string[] || (p as Record<string, unknown>)?.parameterNames as string[] || [])
|
||||
return context.services.deviceAction.getParameterValues(context.device, names, () => Promise.resolve(''))
|
||||
const names: string[] = Array.isArray(p)
|
||||
? (p as string[])
|
||||
: ((p as Record<string, unknown>)?.names as string[]) ||
|
||||
((p as Record<string, unknown>)?.paths as string[]) ||
|
||||
((p as Record<string, unknown>)?.parameterNames as string[]) ||
|
||||
[]
|
||||
return context.services.deviceAction.getParameterValues(context.device, names, () =>
|
||||
Promise.resolve('')
|
||||
)
|
||||
}
|
||||
case CommandType.GET_PARAMETER_NAMES: {
|
||||
const p = params as Record<string, unknown> | string | undefined
|
||||
const name: string = typeof p === 'string' ? p : ((p as Record<string, unknown>)?.name as string || (p as Record<string, unknown>)?.path as string || (p as Record<string, unknown>)?.parameterPath as string || '')
|
||||
return context.services.deviceAction.getParameterNames(context.device, name, () => Promise.resolve(''))
|
||||
const name: string =
|
||||
typeof p === 'string'
|
||||
? p
|
||||
: ((p as Record<string, unknown>)?.name as string) ||
|
||||
((p as Record<string, unknown>)?.path as string) ||
|
||||
((p as Record<string, unknown>)?.parameterPath as string) ||
|
||||
''
|
||||
return context.services.deviceAction.getParameterNames(context.device, name, () =>
|
||||
Promise.resolve('')
|
||||
)
|
||||
}
|
||||
case CommandType.GET_PARAMETER_ATTRIBUTES: {
|
||||
const p = params as Record<string, unknown> | string[] | undefined
|
||||
const names: string[] = Array.isArray(p) ? (p as string[]) : ((p as Record<string, unknown>)?.names as string[] || (p as Record<string, unknown>)?.paths as string[] || (p as Record<string, unknown>)?.parameterNames as string[] || [])
|
||||
return context.services.deviceAction.getParameterAttributes(context.device, names, () => Promise.resolve(''))
|
||||
const names: string[] = Array.isArray(p)
|
||||
? (p as string[])
|
||||
: ((p as Record<string, unknown>)?.names as string[]) ||
|
||||
((p as Record<string, unknown>)?.paths as string[]) ||
|
||||
((p as Record<string, unknown>)?.parameterNames as string[]) ||
|
||||
[]
|
||||
return context.services.deviceAction.getParameterAttributes(context.device, names, () =>
|
||||
Promise.resolve('')
|
||||
)
|
||||
}
|
||||
case CommandType.SET_PARAMETER_VALUES: {
|
||||
const p = params as Record<string, unknown> | Array<{ parameterName?: string; name?: string; value: unknown; type?: string }> | undefined
|
||||
const rawParams: Array<{ parameterName?: string; name?: string; value: unknown; type?: string }> = Array.isArray(p)
|
||||
const p = params as
|
||||
| Record<string, unknown>
|
||||
| Array<{ parameterName?: string; name?: string; value: unknown; type?: string }>
|
||||
| undefined
|
||||
const rawParams: Array<{
|
||||
parameterName?: string
|
||||
name?: string
|
||||
value: unknown
|
||||
type?: string
|
||||
}> = Array.isArray(p)
|
||||
? p
|
||||
: ((p as Record<string, unknown>)?.parameterValues as typeof rawParams || (p as Record<string, unknown>)?.parameters as typeof rawParams || [])
|
||||
: ((p as Record<string, unknown>)?.parameterValues as typeof rawParams) ||
|
||||
((p as Record<string, unknown>)?.parameters as typeof rawParams) ||
|
||||
[]
|
||||
const spvParams = rawParams.map((pv) => ({
|
||||
parameterName: pv.parameterName ?? pv.name ?? '',
|
||||
value: pv.value as string | number | boolean,
|
||||
type: pv.type,
|
||||
}))
|
||||
return context.services.deviceAction.applySetParameterValues(context.device, spvParams, () => Promise.resolve(''))
|
||||
return context.services.deviceAction.applySetParameterValues(
|
||||
context.device,
|
||||
spvParams,
|
||||
() => Promise.resolve('')
|
||||
)
|
||||
}
|
||||
case CommandType.SET_PARAMETER_ATTRIBUTES: {
|
||||
const p = params as Record<string, unknown> | unknown[] | undefined
|
||||
const spaParams = Array.isArray(p) ? p : ((p as Record<string, unknown>)?.attributes as unknown[] || (p as Record<string, unknown>)?.parameterAttributes as unknown[] || [])
|
||||
return context.services.deviceAction.applySetParameterAttributes(context.device, spaParams as SetParameterAttribute[], () => Promise.resolve(''))
|
||||
const spaParams = Array.isArray(p)
|
||||
? p
|
||||
: ((p as Record<string, unknown>)?.attributes as unknown[]) ||
|
||||
((p as Record<string, unknown>)?.parameterAttributes as unknown[]) ||
|
||||
[]
|
||||
return context.services.deviceAction.applySetParameterAttributes(
|
||||
context.device,
|
||||
spaParams as SetParameterAttribute[],
|
||||
() => Promise.resolve('')
|
||||
)
|
||||
}
|
||||
case CommandType.DOWNLOAD: {
|
||||
const p = (params as Record<string, unknown>) || {}
|
||||
return context.services.deviceAction.applyDownload(context.device, p as unknown as DownloadParameter, () => Promise.resolve(''))
|
||||
return context.services.deviceAction.applyDownload(
|
||||
context.device,
|
||||
p as unknown as DownloadParameter,
|
||||
() => Promise.resolve('')
|
||||
)
|
||||
}
|
||||
case CommandType.UPLOAD: {
|
||||
const p = (params as Record<string, unknown>) || {}
|
||||
return context.services.deviceAction.applyUpload(context.device, p as unknown as UploadParameter, () => Promise.resolve(''))
|
||||
return context.services.deviceAction.applyUpload(
|
||||
context.device,
|
||||
p as unknown as UploadParameter,
|
||||
() => Promise.resolve('')
|
||||
)
|
||||
}
|
||||
case CommandType.REBOOT: {
|
||||
const p = (params as Record<string, unknown>) || {}
|
||||
return context.services.deviceAction.applyReboot(context.device, p as RebootParameter, () => Promise.resolve(''))
|
||||
return context.services.deviceAction.applyReboot(context.device, p as RebootParameter, () =>
|
||||
Promise.resolve('')
|
||||
)
|
||||
}
|
||||
case CommandType.FACTORY_RESET: {
|
||||
return context.services.deviceAction.applyFactoryReset(context.device, {}, () => Promise.resolve(''))
|
||||
return context.services.deviceAction.applyFactoryReset(context.device, {}, () =>
|
||||
Promise.resolve('')
|
||||
)
|
||||
}
|
||||
case CommandType.SCHEDULE_INFORM: {
|
||||
const p = (params as Record<string, unknown>) || {}
|
||||
return context.services.deviceAction.applyScheduleInform(
|
||||
context.device,
|
||||
{ delayInSeconds: (p?.delayInSeconds as number) || 300, commandKey: p?.commandKey as string | undefined },
|
||||
{
|
||||
delayInSeconds: (p?.delayInSeconds as number) || 300,
|
||||
commandKey: p?.commandKey as string | undefined,
|
||||
},
|
||||
() => Promise.resolve('')
|
||||
)
|
||||
}
|
||||
case CommandType.REFRESH: {
|
||||
// Refresh = GetParameterValues on all known leaf parameters that have non-empty values
|
||||
const allNames = [
|
||||
...context.device.getParameters().filter((p) => p.value != null && p.value !== '').map((p) => p.name),
|
||||
...context.device.getProperties().filter((p) => p.value != null && p.value !== '').map((p) => p.name),
|
||||
...context.device
|
||||
.getParameters()
|
||||
.filter((p) => p.value !== null && p.value !== undefined && p.value !== '')
|
||||
.map((p) => p.name),
|
||||
...context.device
|
||||
.getProperties()
|
||||
.filter((p) => p.value !== null && p.value !== undefined && p.value !== '')
|
||||
.map((p) => p.name),
|
||||
].filter((name) => !!name && !name.endsWith('.'))
|
||||
if (allNames.length > 0) {
|
||||
return context.services.deviceAction.getParameterValues(context.device, allNames, () => Promise.resolve(''))
|
||||
return context.services.deviceAction.getParameterValues(context.device, allNames, () =>
|
||||
Promise.resolve('')
|
||||
)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -484,7 +613,10 @@ export class ExecutingCommandsState extends DeviceState {
|
||||
logger.error(`Unknown command type: ${action}`)
|
||||
// Mark as failed
|
||||
commandRecord.status = CommandStatus.FAILED
|
||||
await CommandModel.updateCommand(commandRecord._id as ObjectId, { status: CommandStatus.FAILED, updatedAt: new Date() })
|
||||
await CommandModel.updateCommand(commandRecord._id as ObjectId, {
|
||||
status: CommandStatus.FAILED,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
this.currentCommandRecord = null
|
||||
if (this.commandsQueue.length > 0) {
|
||||
return this.processNextCommand(context)
|
||||
@@ -526,7 +658,10 @@ export class OperationalState extends DeviceState {
|
||||
logger.error(`Failed to persist device on completion: ${e}`)
|
||||
}
|
||||
|
||||
logger.info({ deviceId: device.identifier, state: 'operational' }, '[StateMachine] Session completed successfully')
|
||||
logger.info(
|
||||
{ deviceId: device.identifier, state: 'operational' },
|
||||
'[StateMachine] Session completed successfully'
|
||||
)
|
||||
|
||||
return { isComplete: true, response: '' }
|
||||
}
|
||||
@@ -540,7 +675,10 @@ export class PartialOperationalState extends DeviceState {
|
||||
name = 'partial_operational'
|
||||
private failureContext: { reason: string; blockingState?: string; error?: any }
|
||||
|
||||
constructor(_device: Device, failureContext: { reason: string; blockingState?: string; error?: any }) {
|
||||
constructor(
|
||||
_device: Device,
|
||||
failureContext: { reason: string; blockingState?: string; error?: any }
|
||||
) {
|
||||
super()
|
||||
this.failureContext = failureContext
|
||||
}
|
||||
@@ -570,12 +708,15 @@ export class PartialOperationalState extends DeviceState {
|
||||
logger.error(`Failed to persist device on partial completion: ${e}`)
|
||||
}
|
||||
|
||||
logger.error({
|
||||
deviceId: device.identifier,
|
||||
reason: this.failureContext.reason,
|
||||
blockingState: this.failureContext.blockingState,
|
||||
state: 'partial_operational',
|
||||
}, '[StateMachine] Session completed with degraded status')
|
||||
logger.error(
|
||||
{
|
||||
deviceId: device.identifier,
|
||||
reason: this.failureContext.reason,
|
||||
blockingState: this.failureContext.blockingState,
|
||||
state: 'partial_operational',
|
||||
},
|
||||
'[StateMachine] Session completed with degraded status'
|
||||
)
|
||||
|
||||
return { isComplete: true, response: '' }
|
||||
}
|
||||
@@ -610,7 +751,9 @@ export class MainStateMachine {
|
||||
}
|
||||
|
||||
isCompleted(): boolean {
|
||||
return this.currentState.name === 'operational' || this.currentState.name === 'partial_operational'
|
||||
return (
|
||||
this.currentState.name === 'operational' || this.currentState.name === 'partial_operational'
|
||||
)
|
||||
}
|
||||
|
||||
getCurrentStateName(): string {
|
||||
@@ -623,11 +766,17 @@ export class MainStateMachine {
|
||||
|
||||
while (transitionCount++ < MAX_STATE_TRANSITIONS) {
|
||||
try {
|
||||
logger.debug({ state: this.currentState.name, iteration: transitionCount }, '[StateMachine] Executing state')
|
||||
logger.debug(
|
||||
{ state: this.currentState.name, iteration: transitionCount },
|
||||
'[StateMachine] Executing state'
|
||||
)
|
||||
const result = await this.currentState.handle(this.context)
|
||||
|
||||
if (result.nextState) {
|
||||
logger.debug({ from: this.currentState.name, to: result.nextState.name }, '[StateMachine] State transition')
|
||||
logger.debug(
|
||||
{ from: this.currentState.name, to: result.nextState.name },
|
||||
'[StateMachine] State transition'
|
||||
)
|
||||
this.currentState = result.nextState
|
||||
}
|
||||
|
||||
@@ -637,7 +786,10 @@ export class MainStateMachine {
|
||||
}
|
||||
|
||||
if (result.response && result.response !== '') {
|
||||
logger.debug({ state: this.currentState.name, responseLength: result.response.length }, '[StateMachine] Sending response to CPE')
|
||||
logger.debug(
|
||||
{ state: this.currentState.name, responseLength: result.response.length },
|
||||
'[StateMachine] Sending response to CPE'
|
||||
)
|
||||
return result.response
|
||||
}
|
||||
|
||||
@@ -658,11 +810,14 @@ export class MainStateMachine {
|
||||
}
|
||||
|
||||
// Phase 9: Explicit error handling when max transitions exceeded
|
||||
logger.error({
|
||||
deviceId: this.context.device.identifier,
|
||||
lastState: this.currentState.name,
|
||||
sessionId: this.context.session,
|
||||
}, `State machine stuck in ${this.currentState.name} after ${MAX_STATE_TRANSITIONS} transitions`)
|
||||
logger.error(
|
||||
{
|
||||
deviceId: this.context.device.identifier,
|
||||
lastState: this.currentState.name,
|
||||
sessionId: this.context.session,
|
||||
},
|
||||
`State machine stuck in ${this.currentState.name} after ${MAX_STATE_TRANSITIONS} transitions`
|
||||
)
|
||||
|
||||
this.currentState = new PartialOperationalState(this.context.device, {
|
||||
reason: 'max_transitions_exceeded',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DeviceId } from '../../domain/device/device_id.js'
|
||||
import { type DeviceId } from '../../domain/device/device_id.js'
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { CWMPMessageBuilder } from '../../protocol/common/cwmp_message_builder.js'
|
||||
import logger from '@adonisjs/core/services/logger'
|
||||
@@ -168,7 +168,9 @@ export class CWMPTaskQueue extends EventEmitter {
|
||||
/**
|
||||
* Retrieve and mark a pending task as SENT for a device
|
||||
*/
|
||||
getTaskForDevice(deviceId: DeviceId): { id: string; task: CWMPTaskType; props: CWMPTaskPayload } | undefined {
|
||||
getTaskForDevice(
|
||||
deviceId: DeviceId
|
||||
): { id: string; task: CWMPTaskType; props: CWMPTaskPayload } | undefined {
|
||||
const deviceKey = JSON.stringify(deviceId)
|
||||
const queue = this.deviceQueues.get(deviceKey)
|
||||
if (!queue || queue.length === 0) return undefined
|
||||
@@ -210,7 +212,11 @@ export class CWMPTaskQueue extends EventEmitter {
|
||||
this.emit('taskFailed', task)
|
||||
}
|
||||
|
||||
async executeTaskCallback(taskId: string, status: ResultStatus, data: CWMPTaskResultData): Promise<string> {
|
||||
async executeTaskCallback(
|
||||
taskId: string,
|
||||
status: ResultStatus,
|
||||
data: CWMPTaskResultData
|
||||
): Promise<string> {
|
||||
const callback = this.taskCallbacks.get(taskId)
|
||||
if (callback) {
|
||||
const result = new TaskResult()
|
||||
|
||||
Reference in new issue
Block a user