Files
adam-acs/app/domain/device/device.ts
T
Kevin L'HereecandCopilot 6082a0e610 feat: TR-069 per-device SOAP trace logging + log viewer UI
- Remove SOAP from Docker stdout; route to per-device trace files only
- Fix trace service paths using process.cwd() (/app/data/, /app/logs/tr069/)
- Fix Docker volume permissions: dedicated /app/data & /app/logs/tr069 dirs
- Add GET /api/v1/tr069-trace/:id/log endpoint (tail N lines)
- Rewrite Tr069Trace.vue: clickable rows, dark log viewer, 3s auto-refresh
- Add simulator-ui-env.js to public/ (remove from config/)
- TR-181/InternetGatewayDevice dataModel auto-detection
- Parameter writability config with W/R fallback default
- Fix device_action_executor for GetParameterNames detection flow

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 14:33:44 +02:00

288 lines
8.4 KiB
TypeScript

import { DeviceId } from './device_id.js'
import { DeviceParameter, DeviceProperty, ParameterSnapshot, PropertySnapshot } from './device_parameter.js'
import { ParameterType } from '../shared/types.js'
import { createHash } from 'node:crypto'
export type GpnStatus = 'supported' | 'unsupported' | 'unknown'
/** TR-098: InternetGatewayDevice.* root — TR-181: Device.* root */
export type DataModel = 'tr-181' | 'tr-098' | 'unknown'
/** Detect which TR data model a device uses from a ParameterValueStruct list */
export function detectDataModelFromParams(paramValues: any[]): DataModel {
for (const pv of paramValues ?? []) {
const name: unknown = pv?.Name
if (typeof name !== 'string') continue
if (name.startsWith('Device.')) return 'tr-181'
if (name.startsWith('InternetGatewayDevice.')) return 'tr-098'
}
return 'unknown'
}
export enum OnboardingStatus {
PENDING = 'PENDING',
DISCOVERING = 'DISCOVERING',
PROVISIONING = 'PROVISIONING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
}
export class Device {
public tmpParamValues: any[] = []
public tmpPC: any = null
private _parameters: Map<string, DeviceParameter> = new Map()
private _properties: Map<string, DeviceProperty> = new Map()
private _session: any = null
private _lastSeen: Date = new Date()
private _onboardingStatus: OnboardingStatus = OnboardingStatus.PENDING
private _gpnStatus: GpnStatus = 'unknown'
private _dataModel: DataModel = 'unknown'
constructor(
public readonly deviceId: DeviceId,
public readonly createdAt: Date = new Date()
) {}
public get session(): any {
return this._session
}
public get lastSeen(): Date {
return this._lastSeen
}
public get onboardingStatus(): OnboardingStatus {
return this._onboardingStatus
}
public get gpnStatus(): GpnStatus {
return this._gpnStatus
}
public setGpnStatus(status: GpnStatus): void {
this._gpnStatus = status
}
public get dataModel(): DataModel {
return this._dataModel
}
public setDataModel(model: DataModel): void {
this._dataModel = model
}
public get identifier(): string {
const raw = `${this.deviceId.manufacturer}-${this.deviceId.oui}-${this.deviceId.productClass}-${this.deviceId.serialNumber}`
return createHash('sha256').update(raw).digest('hex').substring(0, 24)
}
public attachSession(session: any): void {
this._session = session
this._lastSeen = new Date()
}
public detachSession(): void {
if (this._session) {
this._session = null
}
}
public isOnline(): boolean {
return this._session !== null
}
public updateOnboardingStatus(status: OnboardingStatus): void {
this._onboardingStatus = status
this._lastSeen = new Date()
}
public setParameter(parameter: DeviceParameter): void {
this._parameters.set(parameter.name, parameter)
}
public getParameter(name: string): DeviceParameter | null {
return this._parameters.get(name) || null
}
public getParameters(): DeviceParameter[] {
return Array.from(this._parameters.values())
}
public updateParameter(name: string, value: any): void {
const parameter = this._parameters.get(name)
if (parameter) {
parameter.updateValue(value)
}
}
public setProperty(property: DeviceProperty): void {
this._properties.set(property.name, property)
}
public getProperty(name: string): DeviceProperty | null {
return this._properties.get(name) || null
}
public getProperties(): DeviceProperty[] {
return Array.from(this._properties.values())
}
public updateProperty(name: string, value: any): void {
const property = this._properties.get(name)
if (property) {
property.updateValue(value)
}
}
public getConnectionRequestURL(): URL | null {
for (const property of this._properties.values()) {
const url = property.getConnectionRequestURL()
if (url) return url
}
return null
}
public canExecuteCommands(): boolean {
return this.isOnline() && this._onboardingStatus === OnboardingStatus.COMPLETED
}
public setTmpParamValues(values: any[]): void {
this.tmpParamValues = values
}
public getTmpParamValues(): any[] {
return this.tmpParamValues
}
public setTmpPC(value: any): void {
this.tmpPC = value
}
public getTmpPC(): any {
return this.tmpPC
}
public clearTmpValues(): void {
this.tmpParamValues = []
this.tmpPC = null
}
public organizeParameters(response: any, timestamp: Date): void {
const infoList = response?.ParameterList?.ParameterInfoStruct || []
for (const info of infoList) {
const name = info?.Name
if (!name) continue
// Skip object paths (ending with dot) - only store leaf parameters
if (name.endsWith('.')) continue
const isWritable = !!info?.Writable
if (this._parameters.has(name) || this._properties.has(name)) {
if (isWritable && this._properties.has(name)) {
const prop = this._properties.get(name)!
this._properties.delete(name)
this.setParameter(new DeviceParameter(name, prop.value, prop.type))
} else if (!isWritable && this._parameters.has(name)) {
const param = this._parameters.get(name)!
this._parameters.delete(name)
this.setProperty(new DeviceProperty(name, param.value, param.type))
}
continue
}
if (isWritable) {
this.setParameter(new DeviceParameter(name, null, ParameterType.UNKNOWN))
} else {
this.setProperty(new DeviceProperty(name, null, ParameterType.UNKNOWN))
}
}
this._lastSeen = timestamp
}
public updateFromValues(paramValues: any[]): void {
for (const pv of paramValues || []) {
const name = pv?.Name
const valueObj = pv?.Value
const hasTextNode = valueObj && Object.prototype.hasOwnProperty.call(valueObj as any, '_')
const hasTypeAttr = valueObj && (valueObj['$xsi:type'] || valueObj?.$?.Type)
const val = hasTextNode ? valueObj._ : (hasTypeAttr ? '' : undefined)
const typeStr = valueObj?.['$xsi:type'] || valueObj?.$?.Type
const mappedType = this.mapType(typeStr)
if (!name) continue
if (val === undefined && !hasTypeAttr) continue
if (this._parameters.has(name)) {
const existing = this._parameters.get(name)!
if (mappedType && existing.type === ParameterType.UNKNOWN) {
const snap = existing.toSnapshot()
this._parameters.set(
name,
new DeviceParameter(name, snap.value, mappedType, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList)
)
}
this.updateParameter(name, val)
} else if (this._properties.has(name)) {
const existing = this._properties.get(name)!
if (mappedType && existing.type === ParameterType.UNKNOWN) {
const snap = existing.toSnapshot()
this._properties.set(
name,
new DeviceProperty(name, snap.value, mappedType, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList)
)
}
this.updateProperty(name, val)
} else {
this.setProperty(new DeviceProperty(name, val ?? null, mappedType || ParameterType.UNKNOWN))
}
}
this._lastSeen = new Date()
}
private mapType(typeStr: string | undefined): ParameterType | undefined {
switch (typeStr) {
case 'xsd:string':
return ParameterType.STRING
case 'xsd:int':
return ParameterType.INT
case 'xsd:unsignedInt':
return ParameterType.UNSIGNED_INT
case 'xsd:boolean':
return ParameterType.BOOLEAN
default:
return undefined
}
}
public toSnapshot(): DeviceSnapshot {
return {
deviceId: this.deviceId,
identifier: this.identifier,
createdAt: this.createdAt,
lastSeen: this._lastSeen,
onboardingStatus: this._onboardingStatus,
isOnline: this.isOnline(),
gpnStatus: this._gpnStatus,
dataModel: this._dataModel,
parameters: this.getParameters().map((p) => p.toSnapshot()),
properties: this.getProperties().map((p) => p.toSnapshot()),
}
}
public updateLastSeen(): void {
this._lastSeen = new Date()
}
}
export interface DeviceSnapshot {
deviceId: DeviceId
identifier: string
createdAt: Date
lastSeen: Date
onboardingStatus: OnboardingStatus
isOnline: boolean
gpnStatus: GpnStatus
dataModel: DataModel
parameters: ParameterSnapshot[]
properties: PropertySnapshot[]
}