zigbee2mqtt/lib/extension/networkMap.js
Koen Kanters d83085ea7f
Zigbee-herdsman (#1945)
* Update zigbee-herdsman and zigbee-shepherd-converters.

* Force Aqara S2 Lock endvices (#1764)

* Start on zigbee-herdsman controller refactor.

* More updates.

* Cleanup zapp.

* updates.

* Propagate adapter disconnected event.

* Updates.

* Initial refactor to zigbee-herdsman.

* Refactor deviceReceive to zigbee-herdsman.

* Rename

* Refactor deviceConfigure.

* Finish bridge config.

* Refactor availability.

* Active homeassistant extension and more refactors.

* Refactor groups.

* Enable soft reset.

* Activate group membership

* Start on tests.

* Enable reporting.

* Add more controller tests.

* Add more tests

* Fix linting error.

* Data en deviceReceive tests.

* Move to zigbee-herdsman-converters.

* More device publish tests.

* Cleanup dependencies.

* Bring device publish coverage to 100.

* Bring home assistant test coverage to 100.

* Device configure tests.

* Attempt to fix tests.

* Another attempt.

* Another one.

* Another one.

* Another.

* Add wait.

* Longer wait.

* Debug.

* Update dependencies.

* Another.

* Begin on availability tests.

* Improve availability tests.

* Complete deviceAvailability tests.

* Device bind tests.

* More tests.

* Begin networkmap refactors.

* start on networkmap tests.

* Network map tests.

* Add utils tests.

* Logger tests.

* Settings and logger tests.

* Ignore some stuff for coverage and add todos.

* Add remaining missing tests.

* Enforce 100% test coverage.

* Start on groups test and refactor entityPublish to resolveEntity

* Remove joinPathStorage, not used anymore as group information is stored into zigbee-herdsman database.

* Fix linting issues.

* Improve tests.

* Add groups.

* fix group membership.

* Group: log names.

* Convert MQTT message to string by default.

* Fix group name.

* Updates.

* Revert configuration.yaml.

* Add new line.

* Fixes.

* Updates.

* Fix tests.

* Ignore soft reset extension.
2019-09-09 19:48:09 +02:00

202 lines
8.1 KiB
JavaScript

const settings = require('../util/settings');
const utils = require('../util/utils');
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const logger = require('../util/logger');
class NetworkMap {
constructor(zigbee, mqtt, state, publishEntityState) {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
this.lastSeen = new Map();
// Subscribe to topic.
this.topic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
this.topicRoutes = `${settings.get().mqtt.base_topic}/bridge/networkmap/routes`;
// Bind
this.raw = this.raw.bind(this);
this.graphviz = this.graphviz.bind(this);
// Set supported formats
this.supportedFormats = {
'raw': this.raw,
'graphviz': this.graphviz,
};
}
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
if (data.device) {
this.lastSeen.set(data.device.ieeeAddr, Date.now());
}
}
onMQTTConnected() {
this.mqtt.subscribe(this.topic);
this.mqtt.subscribe(this.topicRoutes);
}
async onMQTTMessage(topic, message) {
if ((topic === this.topic || topic === this.topicRoutes) && this.supportedFormats.hasOwnProperty(message)) {
const includeRoutes = topic === this.topicRoutes;
const topology = await this.networkScan(includeRoutes);
const converted = this.supportedFormats[message](topology);
this.mqtt.publish(`bridge/networkmap/${message}`, converted, {});
}
}
raw(topology) {
return JSON.stringify(topology);
}
graphviz(topology) {
const colors = settings.get().map_options.graphviz.colors;
let text = 'digraph G {\nnode[shape=record];\n';
let style = '';
topology.nodes.forEach((device) => {
const labels = [];
// Add friendly name
labels.push(`${device.friendlyName}`);
// Add the device short network address, ieeaddr and scan note (if any)
labels.push(
`${device.ieeeAddr} (${device.networkAddress})` +
((device.failed && device.failed.length) ? `failed: ${device.failed.join(',')}` : '')
);
// Add the device model
if (device.type !== 'Coordinator') {
const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
if (mappedModel) {
labels.push(`${mappedModel.vendor} ${mappedModel.description} (${mappedModel.model})`);
} else {
// This model is not supported by zigbee-shepherd-converters, add zigbee model information
labels.push(`${device.manufacturerName} ${device.modelID}`);
}
}
// Add the device last_seen timestamp
let lastSeen = 'unknown';
const date = device.type === 'Coordinator' ? Date.now() : this.lastSeen.get(device.ieeeAddr);
if (date) {
const lastSeenAgo = `${new Date(Date.now() - date).toISOString().substr(11, 8)}s ago`;
lastSeen = utils.formatDate(date, settings.get().advanced.last_seen, lastSeenAgo);
}
labels.push(`Last seen: ${lastSeen}`);
// Shape the record according to device type
if (device.type == 'Coordinator') {
style = `style="bold, filled", fillcolor="${colors.fill.coordinator}", ` +
`fontcolor="${colors.font.coordinator}"`;
} else if (device.type == 'Router') {
style = `style="rounded, filled", fillcolor="${colors.fill.router}", ` +
`fontcolor="${colors.font.router}"`;
} else {
style = `style="rounded, dashed, filled", fillcolor="${colors.fill.enddevice}", ` +
`fontcolor="${colors.font.enddevice}"`;
}
// Add the device with its labels to the graph as a node.
text += ` "${device.ieeeAddr}" [`+style+`, label="{${labels.join('|')}}"];\n`;
/**
* 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.links.filter((e) => (e.source.ieeeAddr === device.ieeeAddr)).forEach((e) => {
const lineStyle = (device.type=='EndDevice') ? 'penwidth=1, ' :
(!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(',')})"`;
text += ` "${device.ieeeAddr}" -> "${e.target.ieeeAddr}"`;
text += ` [${lineStyle}${lineWeight}${lineLabels}]\n`;
});
});
text += '}';
return text.replace(/\0/g, '');
}
async networkScan(includeRoutes) {
logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`);
const devices = await this.zigbee.getDevices({});
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, []);
const resolved = await this.zigbee.resolveEntity(device);
try {
const result = await device.lqi();
lqis.set(device, result);
logger.debug(`LQI succeeded for '${resolved.name}'`);
} catch (error) {
failed.get(device).push('lqi');
logger.error(`Failed to execute LQI for '${resolved.name}'`);
}
if (includeRoutes) {
try {
const result = await device.routingTable();
routingTables.set(device, result);
logger.debug(`Routing table succeeded for '${resolved.name}'`);
} catch (error) {
failed.get(device).push('routingTable');
logger.error(`Failed to execute routing table for '${resolved.name}'`);
}
}
}
logger.info(`Network scan finished`);
const networkMap = {nodes: [], links: []};
// Add nodes
for (const device of devices) {
const resolved = await this.zigbee.resolveEntity(device);
networkMap.nodes.push({
ieeeAddr: device.ieeeAddr, friendlyName: resolved.name, type: device.type,
networkAddress: device.networkAddress, manufacturerName: device.manufacturerName,
modelID: device.modelID, failed: failed.get(device),
});
}
// 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: [],
};
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;
}
}
module.exports = NetworkMap;