From a2ea520b0c49f97e438569d0406076cf224be022 Mon Sep 17 00:00:00 2001 From: Kosta Date: Sun, 14 Jun 2020 15:48:50 +0200 Subject: [PATCH] add support for plantuml based network graphs (#3742) Add support for [PlantUML](http://plantuml.com/guide) based network graphs, which can be pasted into [this online editor](https://www.planttext.com/). The test configuration used for unit testing for example produces this output: ![output.svg](https://www.planttext.com/api/plantuml/svg/j5LTRzem57tVhxYFGSFbEbyG7gOoOBHggrMCQPsg2Zc9KwmwSPeufTjVsyD-qlx20dICZuJni4PHPDdtd7lkyRNpwyTFU7bESyqoNSJopP-PW6KXu3CYRNSwID7cjkEO3jok8_ONrqk4a0DXOBYEvJOWOaBoi0loKJGpLMYHt4-OBtabpNl58qYK_bIagSeq3wzKoENve3AOUcqaICjtWhIBs4NwTZbOHzojH_iLe--qeoZYWSiStzoes2-aNrlZ3igmqNAzHcNu-SMm1vDnR_1XV0wNmy6I68QcsngQRV0w2BA8UTA4KCJnmf4cp6T2SyXJ7kYiY9kWKYPV9esIWJgsCw9cTRh_w8QruyOqK59bntbYBJtnQZovdWafqXpE2WuZ1KQRsYwwU7rM7Lua3ucQ9qTLiDLzTAi2hLKo3LLHUZnzuD-EQs2wRE1EjR0RmLGieFV8CWOhPYYPOIuoBdcUmvn92VbSw606nfURYn6QbrUliN6RcXrle-mWC1qBfuiZn-ltRWTUbcnZjSn-aMiLGyYVHC1pC0RYQmDk-_r55bXbkJDcMgVTLYlLJ-f4995ghLGGd-Ky9D_5lSyv2PJAGf4mhJB2af2im8HIizK0OyLuClxuUJ8SlufZfkqllaXjc_4Dn_f_TTNxjGqsQipQKqZntmehpX8XxohITg4S7RgMwc2UUKooRR8o6SL3FLCBmtfOURhM6hVOIfY5-zQSyYHS_BT-0m00__y30000) The generated script looks like this: ``` ' 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 ``` Co-authored-by: Konstantin Baumann --- lib/extension/networkMap.js | 60 +++++++++++++++++++++++ test/networkMap.test.js | 97 +++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) 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; + }); });