diff --git a/lib/extension/networkMap.js b/lib/extension/networkMap.js index e88b5191..914f6c99 100644 --- a/lib/extension/networkMap.js +++ b/lib/extension/networkMap.js @@ -17,11 +17,13 @@ class NetworkMap extends Extension { // Bind this.raw = this.raw.bind(this); this.graphviz = this.graphviz.bind(this); + this.plantuml = this.plantuml.bind(this); // Set supported formats this.supportedFormats = { 'raw': this.raw, 'graphviz': this.graphviz, + 'plantuml': this.plantuml, }; } @@ -126,6 +128,64 @@ class NetworkMap extends Extension { return text.replace(/\0/g, ''); } + plantuml(topology) { + const text = []; + + text.push(`' paste into: https://www.planttext.com/`); + text.push(``); + text.push('@startuml'); + + topology.nodes.sort((a, b) => a.friendlyName.localeCompare(b.friendlyName)).forEach((node) => { + // Add friendly name + text.push(`card ${node.ieeeAddr} [`); + text.push(`${node.friendlyName}`); + text.push(`---`); + + // Add the device short network address, ieeaddr and scan note (if any) + text.push( + `${node.ieeeAddr} (${node.networkAddress})` + + ((node.failed && node.failed.length) ? ` failed: ${node.failed.join(',')}` : ''), + ); + + // Add the device model + if (node.type !== 'Coordinator') { + text.push(`---`); + const definition = this.zigbee.resolveEntity(node.ieeeAddr).definition; + if (definition) { + text.push(`${definition.vendor} ${definition.description} (${definition.model})`); + } else { + // This model is not supported by zigbee-herdsman-converters, add zigbee model information + text.push(`${node.manufacturerName} ${node.modelID}`); + } + } + + // Add the device last_seen timestamp + let lastSeen = 'unknown'; + const date = node.type === 'Coordinator' ? Date.now() : node.lastSeen; + if (date) { + lastSeen = utils.formatDate(date, 'ISO_8601_local'); + } + text.push(`---`); + text.push(lastSeen); + text.push(`]`); + text.push(``); + }); + + /** + * Add edges between the devices + * 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.forEach((link) => { + text.push(`${link.sourceIeeeAddr} --> ${link.targetIeeeAddr}: ${link.lqi}`); + }); + text.push(''); + + text.push(`@enduml`); + + return text.join(`\n`); + } + async networkScan(includeRoutes) { logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`); const devices = this.zigbee.getDevices().filter((d) => d.type !== 'GreenPower'); diff --git a/test/networkMap.test.js b/test/networkMap.test.js index 2963e0b4..1354b1a5 100644 --- a/test/networkMap.test.js +++ b/test/networkMap.test.js @@ -168,4 +168,101 @@ describe('Networkmap', () => { Date.prototype.getTimezoneOffset = getTimezoneOffset; Date.prototype.getHours = getHours; }); + + it('Output plantuml networkmap', async () => { + const getTimezoneOffset = Date.prototype.getTimezoneOffset; + const getHours = Date.prototype.getHours; + Date.prototype.getTimezoneOffset = () => -60; + Date.prototype.getHours = () => 1; + mock(); + const device = zigbeeHerdsman.devices.bulb_color; + device.lastSeen = null; + const endpoint = device.getEndpoint(1); + const data = {modelID: 'test'} + const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + MQTT.events.message('zigbee2mqtt/bridge/networkmap/routes', 'plantuml'); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + let call = MQTT.publish.mock.calls[0]; + expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/plantuml'); + + const expected = `' paste into: https://www.planttext.com/ + + @startuml + card 0x0017880104e45525 [ + 0x0017880104e45525 + --- + 0x0017880104e45525 (6536) failed: lqi,routingTable + --- + Boef notSupportedModelID + --- + 1970-01-01T01:00:01+01:00 + ] + + card 0x000b57fffec6a5b2 [ + bulb + --- + 0x000b57fffec6a5b2 (40369) + --- + IKEA TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12) + --- + 1970-01-01T01:00:01+01:00 + ] + + card 0x000b57fffec6a5b3 [ + bulb_color + --- + 0x000b57fffec6a5b3 (40399) + --- + Philips Hue Go (7146060PH) + --- + unknown + ] + + card 0x0017880104e45521 [ + button_double_key + --- + 0x0017880104e45521 (6538) + --- + Xiaomi Aqara double key wireless wall switch (WXKG02LM) + --- + 1970-01-01T01:00:01+01:00 + ] + + card 0x0017880104e45559 [ + cc2530_router + --- + 0x0017880104e45559 (6540) + --- + Custom devices (DiY) [CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/) (CC2530.ROUTER) + --- + 1970-01-01T01:00:01+01:00 + ] + + card 0x00124b00120144ae [ + Coordinator + --- + 0x00124b00120144ae (0) + --- + 1970-01-01T01:00:10+01:00 + ] + + 0x000b57fffec6a5b3 --> 0x00124b00120144ae: 120 + 0x000b57fffec6a5b2 --> 0x00124b00120144ae: 92 + 0x000b57fffec6a5b3 --> 0x000b57fffec6a5b2: 110 + 0x0017880104e45559 --> 0x000b57fffec6a5b2: 100 + 0x0017880104e45521 --> 0x0017880104e45559: 130 + + @enduml`; + + const expectedLines = expected.split('\n'); + const actualLines = call[1].split('\n'); + + for (let i = 0; i < expectedLines.length; i++) { + expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim()); + } + Date.prototype.getTimezoneOffset = getTimezoneOffset; + Date.prototype.getHours = getHours; + }); });