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:
Kevin L'Hereec committed 2026-07-20 17:14:12 +02:00
1 parent ab45275a89
commit 9cceb139b7
13 files changed
+518 -158

No files matched your search

+34 -9
View File
@@ -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
View File
@@ -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)
+4 -1
View File
@@ -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 -1
View File
@@ -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 {
+29 -9
View File
@@ -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()
}
+1 -3
View File
@@ -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 }
+24 -8
View File
@@ -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
}
}
+3 -3
View File
@@ -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) }
}
+50 -16
View File
@@ -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> {
+216 -61
View File
@@ -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',
+9 -3
View File
@@ -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()