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 = new Map() private _properties: Map = 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[] }