2018-08-28 12:55:00 -07:00
|
|
|
const settings = require('../util/settings');
|
2019-06-04 10:23:58 -07:00
|
|
|
const utils = require('../util/utils');
|
2019-09-09 10:48:09 -07:00
|
|
|
const logger = require('../util/logger');
|
2020-04-11 09:10:56 -07:00
|
|
|
const Extension = require('./extension');
|
2018-08-28 12:55:00 -07:00
|
|
|
|
2020-04-11 11:58:22 -07:00
|
|
|
/**
|
|
|
|
* This extension creates a network map
|
|
|
|
*/
|
2020-04-11 09:10:56 -07:00
|
|
|
class NetworkMap extends Extension {
|
2020-01-09 13:47:19 -07:00
|
|
|
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
|
|
|
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
2018-08-28 12:55:00 -07:00
|
|
|
|
2020-04-11 11:45:50 -07:00
|
|
|
this.legacyApi = settings.get().advanced.legacy_api;
|
|
|
|
this.legacyTopic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
|
|
|
|
this.legacyTopicRoutes = `${settings.get().mqtt.base_topic}/bridge/networkmap/routes`;
|
2018-08-28 12:55:00 -07:00
|
|
|
|
2019-06-17 12:28:27 -07:00
|
|
|
// Bind
|
|
|
|
this.raw = this.raw.bind(this);
|
|
|
|
this.graphviz = this.graphviz.bind(this);
|
|
|
|
|
2018-08-28 12:55:00 -07:00
|
|
|
// Set supported formats
|
|
|
|
this.supportedFormats = {
|
|
|
|
'raw': this.raw,
|
2018-08-28 13:07:57 -07:00
|
|
|
'graphviz': this.graphviz,
|
2018-08-28 12:55:00 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
onMQTTConnected() {
|
2020-04-11 11:45:50 -07:00
|
|
|
/* istanbul ignore else */
|
|
|
|
if (this.legacyApi) {
|
|
|
|
this.mqtt.subscribe(this.legacyTopic);
|
|
|
|
this.mqtt.subscribe(this.legacyTopicRoutes);
|
|
|
|
}
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
async onMQTTMessage(topic, message) {
|
2020-04-11 11:45:50 -07:00
|
|
|
/* istanbul ignore else */
|
|
|
|
if (this.legacyApi) {
|
|
|
|
if ((topic === this.legacyTopic || topic === this.legacyTopicRoutes) &&
|
|
|
|
this.supportedFormats.hasOwnProperty(message)) {
|
|
|
|
const includeRoutes = topic === this.legacyTopicRoutes;
|
|
|
|
const topology = await this.networkScan(includeRoutes);
|
|
|
|
const converted = this.supportedFormats[message](topology);
|
|
|
|
this.mqtt.publish(`bridge/networkmap/${message}`, converted, {});
|
|
|
|
}
|
2018-08-28 12:55:00 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
raw(topology) {
|
2018-08-28 12:55:00 -07:00
|
|
|
return JSON.stringify(topology);
|
|
|
|
}
|
2018-08-28 13:07:57 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
graphviz(topology) {
|
2019-06-26 10:30:38 -07:00
|
|
|
const colors = settings.get().map_options.graphviz.colors;
|
|
|
|
|
2018-10-07 12:46:54 -07:00
|
|
|
let text = 'digraph G {\nnode[shape=record];\n';
|
2019-09-09 10:48:09 -07:00
|
|
|
let style = '';
|
2018-10-07 12:46:54 -07:00
|
|
|
|
2020-05-30 09:27:25 -07:00
|
|
|
topology.nodes.forEach((node) => {
|
2018-10-07 12:46:54 -07:00
|
|
|
const labels = [];
|
|
|
|
|
|
|
|
// Add friendly name
|
2020-05-30 09:27:25 -07:00
|
|
|
labels.push(`${node.friendlyName}`);
|
2018-10-07 12:46:54 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
// Add the device short network address, ieeaddr and scan note (if any)
|
|
|
|
labels.push(
|
2020-05-30 09:27:25 -07:00
|
|
|
`${node.ieeeAddr} (${node.networkAddress})` +
|
|
|
|
((node.failed && node.failed.length) ? `failed: ${node.failed.join(',')}` : ''),
|
2019-09-09 10:48:09 -07:00
|
|
|
);
|
2018-10-07 12:46:54 -07:00
|
|
|
|
|
|
|
// Add the device model
|
2020-05-30 09:27:25 -07:00
|
|
|
if (node.type !== 'Coordinator') {
|
|
|
|
const definition = this.zigbee.resolveEntity(node.ieeeAddr).definition;
|
2020-04-11 11:45:50 -07:00
|
|
|
if (definition) {
|
|
|
|
labels.push(`${definition.vendor} ${definition.description} (${definition.model})`);
|
2019-09-09 10:48:09 -07:00
|
|
|
} else {
|
2019-11-09 12:38:47 -07:00
|
|
|
// This model is not supported by zigbee-herdsman-converters, add zigbee model information
|
2020-05-30 09:27:25 -07:00
|
|
|
labels.push(`${node.manufacturerName} ${node.modelID}`);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
2018-10-07 12:46:54 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
// Add the device last_seen timestamp
|
2019-06-17 12:28:27 -07:00
|
|
|
let lastSeen = 'unknown';
|
2020-05-30 09:27:25 -07:00
|
|
|
const date = node.type === 'Coordinator' ? Date.now() : node.lastSeen;
|
2019-09-09 10:48:09 -07:00
|
|
|
if (date) {
|
2019-11-06 12:51:31 -07:00
|
|
|
lastSeen = utils.formatDate(date, 'ISO_8601_local');
|
2019-06-17 12:28:27 -07:00
|
|
|
}
|
2019-09-09 10:48:09 -07:00
|
|
|
|
2019-11-06 11:49:03 -07:00
|
|
|
labels.push(lastSeen);
|
2018-10-07 12:46:54 -07:00
|
|
|
|
2019-01-22 12:07:38 -07:00
|
|
|
// Shape the record according to device type
|
2020-05-30 09:27:25 -07:00
|
|
|
if (node.type == 'Coordinator') {
|
2019-09-09 10:48:09 -07:00
|
|
|
style = `style="bold, filled", fillcolor="${colors.fill.coordinator}", ` +
|
2019-06-26 10:30:38 -07:00
|
|
|
`fontcolor="${colors.font.coordinator}"`;
|
2020-05-30 09:27:25 -07:00
|
|
|
} else if (node.type == 'Router') {
|
2019-09-09 10:48:09 -07:00
|
|
|
style = `style="rounded, filled", fillcolor="${colors.fill.router}", ` +
|
2019-06-26 10:30:38 -07:00
|
|
|
`fontcolor="${colors.font.router}"`;
|
2019-01-22 12:07:38 -07:00
|
|
|
} else {
|
2019-09-09 10:48:09 -07:00
|
|
|
style = `style="rounded, dashed, filled", fillcolor="${colors.fill.enddevice}", ` +
|
2019-09-09 10:04:07 -07:00
|
|
|
`fontcolor="${colors.font.enddevice}"`;
|
2019-01-22 12:07:38 -07:00
|
|
|
}
|
|
|
|
|
2018-10-07 12:46:54 -07:00
|
|
|
// Add the device with its labels to the graph as a node.
|
2020-05-30 09:27:25 -07:00
|
|
|
text += ` "${node.ieeeAddr}" [`+style+`, label="{${labels.join('|')}}"];\n`;
|
2018-10-07 12:46:54 -07:00
|
|
|
|
|
|
|
/**
|
2019-07-01 07:10:50 -07:00
|
|
|
* Add an edge between the device and its child to the graph
|
2018-10-07 12:46:54 -07:00
|
|
|
* 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.
|
|
|
|
*/
|
2020-05-30 09:27:25 -07:00
|
|
|
topology.links.filter((e) => (e.source.ieeeAddr === node.ieeeAddr)).forEach((e) => {
|
|
|
|
const lineStyle = (node.type=='EndDevice') ? 'penwidth=1, ' :
|
2019-09-09 10:48:09 -07:00
|
|
|
(!e.routes.length) ? 'penwidth=0.5, ' : 'penwidth=2, ';
|
|
|
|
const lineWeight = (!e.routes.length) ? `weight=0, color="${colors.line.inactive}", ` :
|
|
|
|
`weight=1, color="${colors.line.active}", `;
|
|
|
|
const textRoutes = e.routes.map((r) => r.destinationAddress);
|
|
|
|
const lineLabels = (!e.routes.length) ? `label="${e.linkquality}"` :
|
|
|
|
`label="${e.linkquality} (routes: ${textRoutes.join(',')})"`;
|
2020-05-30 09:27:25 -07:00
|
|
|
text += ` "${node.ieeeAddr}" -> "${e.target.ieeeAddr}"`;
|
2019-09-09 10:48:09 -07:00
|
|
|
text += ` [${lineStyle}${lineWeight}${lineLabels}]\n`;
|
|
|
|
});
|
2018-08-28 13:07:57 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
text += '}';
|
|
|
|
|
2018-10-07 12:46:54 -07:00
|
|
|
return text.replace(/\0/g, '');
|
2018-08-28 13:07:57 -07:00
|
|
|
}
|
2019-09-09 10:48:09 -07:00
|
|
|
|
|
|
|
async networkScan(includeRoutes) {
|
|
|
|
logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`);
|
2020-04-11 08:11:09 -07:00
|
|
|
const devices = this.zigbee.getDevices().filter((d) => d.type !== 'GreenPower');
|
2019-09-09 10:48:09 -07:00
|
|
|
const lqis = new Map();
|
|
|
|
const routingTables = new Map();
|
|
|
|
const failed = new Map();
|
|
|
|
|
|
|
|
for (const device of devices.filter((d) => d.type != 'EndDevice')) {
|
|
|
|
failed.set(device, []);
|
2020-04-11 11:45:50 -07:00
|
|
|
const resolvedEntity = this.zigbee.resolveEntity(device);
|
2019-09-09 10:48:09 -07:00
|
|
|
try {
|
|
|
|
const result = await device.lqi();
|
|
|
|
lqis.set(device, result);
|
2020-04-11 11:45:50 -07:00
|
|
|
logger.debug(`LQI succeeded for '${resolvedEntity.name}'`);
|
2019-09-09 10:48:09 -07:00
|
|
|
} catch (error) {
|
|
|
|
failed.get(device).push('lqi');
|
2020-04-11 11:45:50 -07:00
|
|
|
logger.error(`Failed to execute LQI for '${resolvedEntity.name}'`);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (includeRoutes) {
|
|
|
|
try {
|
|
|
|
const result = await device.routingTable();
|
|
|
|
routingTables.set(device, result);
|
2020-04-11 11:45:50 -07:00
|
|
|
logger.debug(`Routing table succeeded for '${resolvedEntity.name}'`);
|
2019-09-09 10:48:09 -07:00
|
|
|
} catch (error) {
|
|
|
|
failed.get(device).push('routingTable');
|
2020-04-11 11:45:50 -07:00
|
|
|
logger.error(`Failed to execute routing table for '${resolvedEntity.name}'`);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.info(`Network scan finished`);
|
|
|
|
|
|
|
|
const networkMap = {nodes: [], links: []};
|
|
|
|
// Add nodes
|
|
|
|
for (const device of devices) {
|
2020-04-11 11:45:50 -07:00
|
|
|
const resolvedEntity = this.zigbee.resolveEntity(device);
|
2019-09-09 10:48:09 -07:00
|
|
|
networkMap.nodes.push({
|
2020-04-11 11:45:50 -07:00
|
|
|
ieeeAddr: device.ieeeAddr, friendlyName: resolvedEntity.name, type: device.type,
|
2019-09-09 10:48:09 -07:00
|
|
|
networkAddress: device.networkAddress, manufacturerName: device.manufacturerName,
|
2019-09-12 13:48:23 -07:00
|
|
|
modelID: device.modelID, failed: failed.get(device), lastSeen: device.lastSeen,
|
2019-09-09 10:48:09 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add links
|
|
|
|
lqis.forEach((lqi, device) => {
|
|
|
|
for (const neighbor of lqi.neighbors) {
|
|
|
|
if (neighbor.relationship > 3) {
|
|
|
|
// Relationship is not active, skip it
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const link = {
|
|
|
|
source: {ieeeAddr: neighbor.ieeeAddr, networkAddress: neighbor.networkAddress},
|
|
|
|
target: {ieeeAddr: device.ieeeAddr, networkAddress: device.networkAddress},
|
|
|
|
linkquality: neighbor.linkquality, depth: neighbor.depth, routes: [],
|
2019-09-17 09:19:42 -07:00
|
|
|
// DEPRECATED:
|
|
|
|
sourceIeeeAddr: neighbor.ieeeAddr, targetIeeeAddr: device.ieeeAddr,
|
|
|
|
sourceNwkAddr: neighbor.networkAddress, lqi: neighbor.linkquality,
|
|
|
|
relationship: neighbor.relationship,
|
2019-09-09 10:48:09 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
const routingTable = routingTables.get(device);
|
|
|
|
if (routingTable) {
|
|
|
|
link.routes = routingTable.table
|
|
|
|
.filter((t) => t.status === 'ACTIVE' && t.nextHop === neighbor.networkAddress);
|
|
|
|
}
|
|
|
|
|
|
|
|
networkMap.links.push(link);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return networkMap;
|
|
|
|
}
|
2018-08-28 12:55:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = NetworkMap;
|