diff --git a/lib/extension/networkMap.js b/lib/extension/networkMap.js index fed7a997e..663b51f0e 100644 --- a/lib/extension/networkMap.js +++ b/lib/extension/networkMap.js @@ -58,17 +58,21 @@ class NetworkMap { let text = 'digraph G {\nnode[shape=record];\n'; let devStyle = ''; - zigbee.getDevices().forEach((device) => { + topology.nodes.forEach((device) => { const labels = []; - const friendlyDevice = settings.getDevice(device.ieeeAddr); - const friendlyName = friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr; // Add friendly name - labels.push(`${friendlyName}:${`0x${device.nwkAddr.toString(16)}`}`); + labels.push(`${device.friendlyName}`); - // Add the device type - const deviceType = utils.correctDeviceType(device); - labels.push(deviceType); + // Add the device short network address and scan note (if any) + let scanNote = ''; + if (device.scanfailed.includes('lqi')) { + scanNote += ' no lqi'; + } + if (device.scanfailed.includes('rtg')) { + scanNote += ' no routes'; + } + labels.push(`0x${device.nwkAddr.toString(16)} ${scanNote}`); // Add the device model const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); @@ -96,10 +100,10 @@ class NetworkMap { labels.push(`${device.status} (${lastSeen})`); // Shape the record according to device type - if (deviceType == 'Coordinator') { + if (device.type == 'Coordinator') { devStyle = `style="bold, filled", fillcolor="${colors.fill.coordinator}", ` + `fontcolor="${colors.font.coordinator}"`; - } else if (deviceType == 'Router') { + } else if (device.type == 'Router') { devStyle = `style="rounded, filled", fillcolor="${colors.fill.router}", ` + `fontcolor="${colors.font.router}"`; } else { @@ -111,19 +115,22 @@ class NetworkMap { text += ` "${device.ieeeAddr}" [`+devStyle+`, label="{${labels.join('|')}}"];\n`; /** - * Add an edge between the device and its parent to the graph + * Add an edge between the device and its child to the graph * NOTE: There are situations where a device is NOT in the topology, this can be e.g. * due to not responded to the lqi scan. In that case we do not add an edge for this device. */ - topology.filter((e) => (e.ieeeAddr === device.ieeeAddr) || (e.nwkAddr === device.nwkAddr)).forEach((e) => { - const lineStyle = (device.type=='EndDevice') ? 'style="dashed", ' - : (!e.routes.length) ? 'style="dotted", ' : ''; - const lineWeight = (!e.routes.length) ? `weight=0, color="${colors.line.inactive}", ` - : `weight=1, color="${colors.line.active}", `; - const textRoutes = e.routes.map((r) => `0x${r.toString(16)}`); - const lineLabels = `label="${e.lqi}\\n[${textRoutes.join(']\\n[')}]"`; - text += ` "${e.parent}" -> "${device.ieeeAddr}" [${lineStyle}${lineWeight}${lineLabels}]\n`; - }); + topology.links.filter((e) => (e.sourceIeeeAddr === device.ieeeAddr) || (e.SourceNwkAddr === device.nwkAddr)) + .forEach((e) => { + const lineStyle = (device.type=='EndDevice') ? 'style="dashed", ' + : (!e.routes.length) ? 'style="dotted", ' : ''; + const lineWeight = (!e.routes.length) ? `weight=0, color="${colors.line.inactive}", ` + : `weight=1, color="${colors.line.active}", `; + const textRoutes = e.routes.map((r) => `0x${r.toString(16)}`); + const lineLabels = (!e.routes.length) ? `label="${e.lqi}"` + : `label="${e.lqi}\\n[${textRoutes.join(']\\n[')}]"`; + text += ` "${device.ieeeAddr}" -> "${e.targetIeeeAddr}"`; + text += ` [${lineStyle}${lineWeight}${lineLabels}]\n`; + }); }); text += '}'; diff --git a/lib/zigbee.js b/lib/zigbee.js index 49eee5392..b9c3bc7f8 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -317,36 +317,63 @@ class Zigbee { // Gather the lqi and the route info into separate lists and only collate them when done. const collateMap = () => { - linkMap.sort((a, b) => (a.key > b.key) ? 1 : 0); + linkMap.sort(); logger.debug(`Link map: %j`, linkMap); logger.debug(`Route map: %j`, routeMap); - // Merge the routes into the linkMap + // Merge the routes into the linkMap by matching on 'source|target' link short addresses linkMap.forEach((link) => { routeMap.filter((e) => e.key === link.key).forEach((e) => { link.routes.push(e.destAddr); }); + link.routes.sort(function(a, b) { + return a-b; + }); delete link.key; }); - logger.debug(`Merged map: %j`, linkMap); - callback(linkMap); + const networkMap = {nodes: [], links: linkMap}; + this.getDevices().forEach((device) => { + const friendlyDevice = settings.getDevice(device.ieeeAddr); + const friendlyName = friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr; + const deviceType = utils.correctDeviceType(device); + const scanfailed = []; + if (lqiScanList.has(device.ieeeAddr)) { + scanfailed.push('lqi'); + } + if (rtgScanList.has(device.ieeeAddr)) { + scanfailed.push('rtg'); + } + networkMap.nodes.push({ieeeAddr: device.ieeeAddr, friendlyName: friendlyName, type: deviceType, + nwkAddr: device.nwkAddr, manufName: device.manufName, modelId: device.modelId, + status: device.status, scanfailed: scanfailed}); + }); + // Clear remaining devices so they don't process when/if they eventually complete + lqiScanList.clear(); + rtgScanList.clear(); + logger.debug(`Merged map: %j`, networkMap); + callback(networkMap); }; - const processLqiResponse = (error, rsp, parent) => { + const processLqiResponse = (error, rsp, targetIeeeAddr, targetNwkAddr) => { if (error) { - logger.warn(`Failed network lqi scan for device: '${parent}' with error: '${error}'`); + logger.warn(`Failed network lqi scan for device: '${targetIeeeAddr}' with error: '${error}'`); } else { - if (lqiScanList.has(parent)) { + if (lqiScanList.has(targetIeeeAddr)) { // Haven't processed this one yet if (rsp && rsp.status === 0 && rsp.neighborlqilist) { - logger.debug(`lqi scan ok for: '${parent}' with '${rsp.neighborlqilistcount}' neighbors`); + logger.debug(`lqi scan: '${targetIeeeAddr}' with '${rsp.neighborlqilistcount}' neighbors`); rsp.neighborlqilist.forEach(function(neighbor) { - const key = parent + '|' + neighbor.nwkAddr; - linkMap.push({ - key: key, parent: parent, ieeeAddr: neighbor.extAddr, nwkAddr: neighbor.nwkAddr, - lqi: neighbor.lqi, depth: neighbor.depth, routes: []}); + // only include active relationships + if (neighbor.relationship <= 3) { + // lqi is measured at receiver so link is from neighbor (source) to scanned router + const key = neighbor.nwkAddr + '|' + targetNwkAddr; + linkMap.push({ + key: key, sourceIeeeAddr: neighbor.extAddr, targetIeeeAddr: targetIeeeAddr, + sourceNwkAddr: neighbor.nwkAddr, lqi: neighbor.lqi, depth: neighbor.depth, + relationship: neighbor.relationship, routes: []}); + } }); - // Remove from list and if this was the last one return the completed network map - lqiScanList.delete(parent); + // Remove from scan list and if both lists are done return the completed network map + lqiScanList.delete(targetIeeeAddr); if (lqiScanList.size === 0 && rtgScanList === 0) { logger.info('Network scan completed'); collateMap(); @@ -355,32 +382,32 @@ class Zigbee { logger.debug(`Outstanding network rtg scans for devices: '${[...rtgScanList].join(' ')}'`); } } else { - logger.warn(`Empty network lqi scan result for: '${parent}'`); + logger.warn(`Empty network lqi scan result for: '${targetIeeeAddr}'`); } } else { - // This ieeeAddr has already been removed due to timeout so don't add to result network map - logger.warn(`Ignoring late network lqi scan result for: '${parent}'`); + // This target has already had timeout so don't add to result network map + logger.warn(`Ignoring late network lqi scan result for: '${targetIeeeAddr}'`); } } }; - const processRtgResponse = (error, rsp, parent) => { + const processRtgResponse = (error, rsp, sourceIeeeAddr, sourceNwkAddr) => { if (error) { - logger.warn(`Failed network rtg scan for device: '${parent}' with error: '${error}'`); + logger.warn(`Failed network rtg scan for device: '${sourceIeeeAddr}' with error: '${error}'`); } else { - if (rtgScanList.has(parent)) { + if (rtgScanList.has(sourceIeeeAddr)) { // Haven't processed this one yet if (rsp && rsp.status === 0 && rsp.routingtablelist) { - logger.debug(`rtg scan ok for: '${parent}' with '${rsp.routingtablelistcount}' entries`); + logger.debug(`rtg scan: '${sourceIeeeAddr}' with '${rsp.routingtablelistcount}' entries`); rsp.routingtablelist.forEach(function(route) { if (route.routeStatus === 0) { - const key = parent + '|' + route.nextHopNwkAddr; + const key = sourceNwkAddr + '|' + route.nextHopNwkAddr; routeMap.push({ key: key, destAddr: route.destNwkAddr}); } }); - // Remove from list and if this was the last one return the completed network map - rtgScanList.delete(parent); + // Remove from scan list and if both lists are done return the completed network map + rtgScanList.delete(sourceIeeeAddr); if (lqiScanList.size === 0 && rtgScanList.size === 0) { logger.info('Network scan completed'); collateMap(); @@ -389,31 +416,31 @@ class Zigbee { logger.debug(`Outstanding network rtg scans for devices: '${[...rtgScanList].join(' ')}'`); } } else { - logger.warn(`Empty network rtg scan result for: '${parent}'`); + logger.warn(`Empty network rtg scan result for: '${sourceIeeeAddr}'`); } } else { - // This ieeeAddr has already been removed due to timeout so don't add to result network map - logger.warn(`Ignoring late network rtg scan result for: '${parent}'`); + // This source has already had timeout so don't add to result network map + logger.warn(`Ignoring late network rtg scan result for: '${sourceIeeeAddr}'`); } } }; - // Queue up an lqi scan and an rtg for coordinator and each router + // Queue up an lqi scan and an rtg scan for coordinator and each router this.getScanable().forEach((dev) => { logger.debug(`Queing network scans for device: '${dev.ieeeAddr}'`); lqiScanList.add(dev.ieeeAddr); this.queue.push(dev.ieeeAddr, (queueCallback) => { this.shepherd.controller.request('ZDO', 'mgmtLqiReq', {dstaddr: dev.nwkAddr, startindex: 0}, - (error, rsp, parent) => { - processLqiResponse(error, rsp, dev.ieeeAddr); + (error, rsp, ieeeAddr, nwkAddr) => { + processLqiResponse(error, rsp, dev.ieeeAddr, dev.nwkAddr); queueCallback(error); }); }); rtgScanList.add(dev.ieeeAddr); this.queue.push(dev.ieeeAddr, (queueCallback) => { this.shepherd.controller.request('ZDO', 'mgmtRtgReq', {dstaddr: dev.nwkAddr, startindex: 0}, - (error, rsp, parent) => { - processRtgResponse(error, rsp, dev.ieeeAddr); + (error, rsp, ieeeAddr, nwkAddr) => { + processRtgResponse(error, rsp, dev.ieeeAddr, dev.nwkAddr); queueCallback(error); }); }); @@ -426,9 +453,6 @@ class Zigbee { } else { logger.warn(`Network scan timeout, skipping outstanding lqi scans for '${[...lqiScanList].join(' ')}'`); logger.warn(`Network scan timeout, skipping outstanding rtg scans for '${[...rtgScanList].join(' ')}'`); - // Clear remaining devices so they don't process when/if they eventually complete - lqiScanList.clear(); - rtgScanList.clear(); collateMap(); } }, lqiScanList.size * 1000);