diff --git a/lib/extension/bridge.js b/lib/extension/bridge.js index c90bc3f9..d6b50a24 100644 --- a/lib/extension/bridge.js +++ b/lib/extension/bridge.js @@ -41,11 +41,7 @@ class Bridge extends Extension { async onMQTTMessage(topic, message) { const match = topic.match(requestRegex); if (match && this.requestLookup[match[1].toLowerCase()]) { - try { - message = JSON.parse(message); - } catch (e) { - e; - } + message = utils.parseJSON(message, message); try { const response = await this.requestLookup[match[1].toLowerCase()](message); diff --git a/lib/extension/networkMap.js b/lib/extension/networkMap.js index 3831d3b3..b8750d34 100644 --- a/lib/extension/networkMap.js +++ b/lib/extension/networkMap.js @@ -13,6 +13,7 @@ class NetworkMap extends Extension { 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`; + this.topic = `${settings.get().mqtt.base_topic}/bridge/request/networkmap`; // Bind this.raw = this.raw.bind(this); @@ -33,6 +34,10 @@ class NetworkMap extends Extension { this.mqtt.subscribe(this.legacyTopic); this.mqtt.subscribe(this.legacyTopicRoutes); } + + if (settings.get().experimental.new_api) { + this.mqtt.subscribe(this.topic); + } } async onMQTTMessage(topic, message) { @@ -46,6 +51,29 @@ class NetworkMap extends Extension { this.mqtt.publish(`bridge/networkmap/${message}`, converted, {}); } } + + if (settings.get().experimental.new_api && topic === this.topic) { + try { + message = utils.parseJSON(message, message); + const type = typeof message === 'object' ? message.type : message; + if (!this.supportedFormats.hasOwnProperty(type)) { + throw new Error(`Type '${type}' not supported, allowed are: ${Object.keys(this.supportedFormats)}`); + } + + const routes = typeof message === 'object' && message.routes; + const topology = await this.networkScan(routes); + const value = this.supportedFormats[type](topology); + await this.mqtt.publish( + 'bridge/response/networkmap', + JSON.stringify(utils.getResponse(message, {routes, type, value}, null)), + ); + } catch (error) { + await this.mqtt.publish( + 'bridge/response/networkmap', + JSON.stringify(utils.getResponse(message, {}, error.message)), + ); + } + } } raw(topology) { diff --git a/lib/util/utils.js b/lib/util/utils.js index 9324c26e..397ea09b 100644 --- a/lib/util/utils.js +++ b/lib/util/utils.js @@ -142,6 +142,14 @@ function getResponse(request, data, error) { return response; } +function parseJSON(value, failedReturnValue) { + try { + return JSON.parse(value); + } catch (e) { + return failedReturnValue; + } +} + module.exports = { millisecondsToSeconds: (milliseconds) => milliseconds / 1000, secondsToMilliseconds: (seconds) => seconds * 1000, @@ -160,4 +168,5 @@ module.exports = { equalsPartial, getResponse, capitalize, + parseJSON, }; diff --git a/test/networkMap.test.js b/test/networkMap.test.js index 3751d9ab..26a148e9 100644 --- a/test/networkMap.test.js +++ b/test/networkMap.test.js @@ -26,6 +26,7 @@ describe('Networkmap', () => { data.writeDefaultConfiguration(); settings._reRead(); data.writeEmptyState(); + settings.set(['experimental', 'new_api'], true); controller = new Controller(); await controller.start(); mocksClear.forEach((m) => m.mockClear()); @@ -75,7 +76,7 @@ describe('Networkmap', () => { unsupported_router.routingTable = () => {throw new Error('failed')}; } - it('Output raw networkmap', async () => { + it('Output raw networkmap legacy api', async () => { mock(); MQTT.events.message('zigbee2mqtt/bridge/networkmap/routes', 'raw'); await flushPromises(); @@ -126,7 +127,7 @@ describe('Networkmap', () => { expect(JSON.parse(call[1])).toStrictEqual(expected); }); - it('Output graphviz networkmap', async () => { + it('Output graphviz networkmap legacy api', async () => { mock(); const device = zigbeeHerdsman.devices.bulb_color; device.lastSeen = null; @@ -163,7 +164,7 @@ describe('Networkmap', () => { } }); - it('Output plantuml networkmap', async () => { + it('Output plantuml networkmap legacy api', async () => { mock(); const device = zigbeeHerdsman.devices.bulb_color; device.lastSeen = null; @@ -178,7 +179,7 @@ describe('Networkmap', () => { expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/plantuml'); const expected = `' paste into: https://www.planttext.com/ - + @startuml card 0x0017880104e45525 [ 0x0017880104e45525 @@ -189,7 +190,7 @@ describe('Networkmap', () => { --- 9 seconds ago ] - + card 0x000b57fffec6a5b2 [ bulb --- @@ -199,7 +200,7 @@ describe('Networkmap', () => { --- 9 seconds ago ] - + card 0x000b57fffec6a5b3 [ bulb_color --- @@ -209,7 +210,7 @@ describe('Networkmap', () => { --- unknown ] - + card 0x0017880104e45521 [ button_double_key --- @@ -219,7 +220,7 @@ describe('Networkmap', () => { --- 9 seconds ago ] - + card 0x0017880104e45559 [ cc2530_router --- @@ -229,7 +230,7 @@ describe('Networkmap', () => { --- 9 seconds ago ] - + card 0x00124b00120144ae [ Coordinator --- @@ -237,13 +238,13 @@ describe('Networkmap', () => { --- 0 seconds ago ] - + 0x000b57fffec6a5b3 --> 0x00124b00120144ae: 120 0x000b57fffec6a5b2 --> 0x00124b00120144ae: 92 0x000b57fffec6a5b3 --> 0x000b57fffec6a5b2: 110 0x0017880104e45559 --> 0x000b57fffec6a5b2: 100 0x0017880104e45521 --> 0x0017880104e45559: 130 - + @enduml`; const expectedLines = expected.split('\n'); @@ -253,4 +254,57 @@ describe('Networkmap', () => { expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim()); } }); + + it('Should output raw networkmap', async () => { + mock(); + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', JSON.stringify({type: 'raw', routes: true})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + let call = MQTT.publish.mock.calls[0]; + expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + + const conv = (device) => {return {ieeeAddr: device.ieeeAddr, networkAddress: device.networkAddress}}; + + const expected = { + status: 'ok', + data: { + type: 'raw', + routes: true, + value: { + "nodes":[ + {"lastSeen": 1000,"ieeeAddr":coordinator.ieeeAddr,"friendlyName":"Coordinator","type":"Coordinator","networkAddress":0,"modelID":null,"failed":[]}, + {"lastSeen": 1000,"ieeeAddr":bulb.ieeeAddr,"friendlyName":"bulb","type":"Router","networkAddress":40369,"modelID":"TRADFRI bulb E27 WS opal 980lm","failed":[]}, + {"lastSeen": null,"ieeeAddr":bulb_color.ieeeAddr,"friendlyName":"bulb_color","type":"Router","networkAddress":40399,"modelID":"LLC020","failed":[]}, + {"lastSeen": 1000,"ieeeAddr":WXKG02LM.ieeeAddr,"friendlyName":"button_double_key","type":"EndDevice","networkAddress":6538,"modelID":"lumi.sensor_86sw2.es1"}, + {"lastSeen": 1000,"ieeeAddr":unsupported_router.ieeeAddr,"friendlyName":"0x0017880104e45525","type":"Router","networkAddress":6536,"modelID":"notSupportedModelID","manufacturerName": "Boef","failed":['lqi', 'routingTable']}, + {"lastSeen": 1000,"ieeeAddr":CC2530_ROUTER.ieeeAddr,"friendlyName":"cc2530_router","type":"Router","networkAddress":6540,"modelID":"lumi.router","failed":[]}, + ], + "links":[ + {depth: 1, linkquality: 120, routes: [], source: conv(bulb_color), target: conv(coordinator), sourceIeeeAddr: bulb_color.ieeeAddr, sourceNwkAddr: bulb_color.networkAddress, targetIeeeAddr: coordinator.ieeeAddr, lqi: 120, relationship: 2}, + {depth: 1, linkquality: 92, routes: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}], source: conv(bulb), target: conv(coordinator), sourceIeeeAddr: bulb.ieeeAddr, sourceNwkAddr: bulb.networkAddress, targetIeeeAddr: coordinator.ieeeAddr, lqi: 92, relationship: 2}, + {depth: 2, linkquality: 110, routes: [], source: conv(bulb_color), target: conv(bulb), sourceIeeeAddr: bulb_color.ieeeAddr, sourceNwkAddr: bulb_color.networkAddress, targetIeeeAddr: bulb.ieeeAddr, lqi: 110, relationship: 1}, + {depth: 2, linkquality: 100, routes: [], source: conv(CC2530_ROUTER), target: conv(bulb), sourceIeeeAddr: CC2530_ROUTER.ieeeAddr, sourceNwkAddr: CC2530_ROUTER.networkAddress, targetIeeeAddr: bulb.ieeeAddr, lqi: 100, relationship: 1}, + {depth: 2, linkquality: 130, routes: [], source: conv(WXKG02LM), target: conv(CC2530_ROUTER), sourceIeeeAddr: WXKG02LM.ieeeAddr, sourceNwkAddr: WXKG02LM.networkAddress, targetIeeeAddr: CC2530_ROUTER.ieeeAddr, lqi: 130, relationship: 1} + ] + } + } + }; + + const actual = JSON.parse(call[1]); + actual.data.value = JSON.parse(actual.data.value); + expect(actual).toStrictEqual(expected); + }); + + it('Should throw error when rquesting invalid type', async () => { + mock(); + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', 'not_existing'); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/networkmap', + JSON.stringify({"data":{},"status":"error","error":"Type 'not_existing' not supported, allowed are: raw,graphviz,plantuml"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); });