'use strict' const HOST_FIELDS = ['HostName', 'IPAddress', 'PhysAddress', 'Active', 'AddressSource', 'LeaseTimeRemaining', 'Layer1Interface'] const hostParams = ['Device.Hosts.HostNumberOfEntries'] for (let i = 1; i <= 20; i++) for (const f of HOST_FIELDS) hostParams.push(`Device.Hosts.Host.${i}.${f}`) const multiApParams = [] for (let d = 1; d <= 3; d++) { for (let r = 1; r <= 2; r++) { for (let a = 1; a <= 4; a++) { multiApParams.push(`Device.WiFi.MultiAP.APDevice.${d}.Radio.${r}.AP.${a}.AssociatedDeviceNumberOfEntries`) for (let c = 1; c <= 8; c++) { const base = `Device.WiFi.MultiAP.APDevice.${d}.Radio.${r}.AP.${a}.AssociatedDevice.${c}` multiApParams.push(`${base}.MACAddress`, `${base}.SignalStrength`, `${base}.X_TP_NegotiationSpeed`) } } } const base = `Device.WiFi.MultiAP.APDevice.${d}` multiApParams.push(`${base}.X_TP_HostName`, `${base}.MACAddress`, `${base}.X_TP_Active`, `${base}.Radio.1.OperatingFrequencyBand`, `${base}.Radio.2.OperatingFrequencyBand`) } module.exports = function createHostsHandler ({ nbiRequest, json, deviceCache, cacheHosts, getCachedHosts }) { return async function handleHosts (res, deviceId) { const encId = encodeURIComponent(deviceId) try { await nbiRequest(`/devices/${encId}/tasks?connection_request&timeout=15000`, 'POST', { name: 'getParameterValues', parameterNames: hostParams }) } catch {} try { await nbiRequest(`/devices/${encId}/tasks?timeout=10000`, 'POST', { name: 'getParameterValues', parameterNames: multiApParams }) } catch {} const result = await nbiRequest(`/devices/?query=${encodeURIComponent(JSON.stringify({ _id: deviceId }))}&projection=Device.Hosts,Device.WiFi.MultiAP`) const devices = Array.isArray(result.data) ? result.data : [] if (!devices.length) return json(res, 404, { error: 'Device not found' }) const dd = devices[0] const hostTree = dd.Device?.Hosts?.Host || {} const hostCount = dd.Device?.Hosts?.HostNumberOfEntries?._value || 0 // Build client MAC -> node mapping from MultiAP const clientNodeMap = new Map(), meshNodes = new Map() const apDevices = dd.Device?.WiFi?.MultiAP?.APDevice || {} for (const dk of Object.keys(apDevices)) { if (dk.startsWith('_')) continue const dev = apDevices[dk] const nodeMac = dev.MACAddress?._value || '', nodeName = dev.X_TP_HostName?._value || '' if (nodeMac) meshNodes.set(nodeMac.toUpperCase(), { name: nodeName, ip: dev.X_TP_IPAddress?._value || '' }) for (const rk of Object.keys(dev.Radio || {})) { if (rk.startsWith('_')) continue const radio = dev.Radio[rk] if (!radio) continue const freqBand = radio.OperatingFrequencyBand?._value || '' const band = freqBand.includes('5') ? '5GHz' : freqBand.includes('2.4') ? '2.4GHz' : freqBand || '' for (const ak of Object.keys(radio.AP || {})) { if (ak.startsWith('_')) continue for (const ck of Object.keys(radio.AP[ak]?.AssociatedDevice || {})) { if (ck.startsWith('_')) continue const client = radio.AP[ak].AssociatedDevice[ck] const cMac = client?.MACAddress?._value if (!cMac) continue clientNodeMap.set(cMac.toUpperCase(), { nodeName, nodeMac: nodeMac.toUpperCase(), band, signal: client.SignalStrength?._value != null ? Number(client.SignalStrength._value) : null, speed: client.X_TP_NegotiationSpeed?._value != null ? String(client.X_TP_NegotiationSpeed._value) : '', }) } } } } const hosts = [] for (const k of Object.keys(hostTree)) { if (k.startsWith('_')) continue const h = hostTree[k] if (!h?._object === undefined) continue const gv = key => h[key]?._value !== undefined ? h[key]._value : null const name = gv('HostName'), ip = gv('IPAddress'), mac = gv('PhysAddress') if (!name && !ip && !mac) continue const isMeshNode = mac && meshNodes.has(mac.toUpperCase()) const nodeInfo = mac ? clientNodeMap.get(mac.toUpperCase()) : null const connType = isMeshNode ? 'mesh-node' : (!nodeInfo && !gv('Active')) ? 'unknown' : 'wifi' hosts.push({ id: k, name: name || '', ip: ip || '', mac: mac || '', active: gv('Active') === true, addressSource: gv('AddressSource') || '', leaseRemaining: gv('LeaseTimeRemaining') != null ? Number(gv('LeaseTimeRemaining')) : null, connType, band: nodeInfo?.band || '', signal: nodeInfo?.signal ?? null, linkRate: nodeInfo?.speed || '', attachedNode: nodeInfo?.nodeName || '', attachedMac: nodeInfo?.nodeMac || '', isMeshNode, }) } hosts.sort((a, b) => { if (a.isMeshNode !== b.isMeshNode) return a.isMeshNode ? 1 : -1 if (a.active !== b.active) return a.active ? -1 : 1 return (a.name || a.ip).localeCompare(b.name || b.ip) }) const hostsResult = { total: hostCount, hosts } for (const [serial, entry] of deviceCache) { if (entry.summary?._id === deviceId) { cacheHosts(serial, hostsResult); break } } return json(res, 200, hostsResult) } }