Dynamic Network Map
Dynamic Network Map
This map is created via an object and pings all the devices once, every 30 seconds
Requirements:
- Linux based OS due to use of ping parameters and grep use.
- Dashboard nodes (node-red-dashboard)
The flow currently has 2 types of information that can be displayed when a node is clicked in the network map:
- ping
- tasmota
With "ping" it will try to ping and display the rtt/packet loss
With "tasmota" it will use a http request node to execute the status 0 and return the ssid/version/rssi. You can click the ip address to open the tasmota ui in a new tab. Note: if you use user/password for tasmota device this will not work. Modify the flow to your liking.
Template nodes are used to format the output per type.
The switch node can be used to add/remove others
Detail
It creates a web location: /networkapi that shows the topology and is read by the network map in the ui-template node
The map is refreshed every 30 seconds, can be changed in the ui-template node
[{"id":"90831ff5.70c858","type":"http in","z":"674d3c11.099964","name":"","url":"/networkapi","method":"get","upload":false,"swaggerDoc":"","x":327,"y":192,"wires":[["9d0f8230.226f88"]],"l":false},{"id":"e82032cc.66ced8","type":"http response","z":"674d3c11.099964","name":"","statusCode":"","headers":{},"x":423,"y":192,"wires":[],"l":false},{"id":"9d0f8230.226f88","type":"change","z":"674d3c11.099964","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"network","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":375,"y":192,"wires":[["e82032cc.66ced8"]],"l":false},{"id":"62016808.314be8","type":"function","z":"674d3c11.099964","name":"","func":"m = msg.payload\nglobal.set('network',m)\n\nreturn {payload:global.get('network')}","outputs":1,"noerr":0,"x":159,"y":192,"wires":[["b5f63d1d.57a9b8"]],"l":false},{"id":"b5f63d1d.57a9b8","type":"debug","z":"674d3c11.099964","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":207,"y":192,"wires":[],"l":false},{"id":"6058a657.1e87d8","type":"change","z":"674d3c11.099964","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"network","tot":"global"},{"t":"set","p":"payload","pt":"msg","to":"**.ip","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":159,"y":312,"wires":[["2c92f82c.ae7c3"]],"l":false},{"id":"2c92f82c.ae7c3","type":"function","z":"674d3c11.099964","name":"","func":"m = msg.payload\n\nfor(x=0;x<m.length;x++){\nnode.send({payload:m[x],ip:m[x]}) \n \n}\n","outputs":1,"noerr":0,"x":215,"y":312,"wires":[["36739bd4.08a97c"]],"l":false},{"id":"36739bd4.08a97c","type":"exec","z":"674d3c11.099964","command":"ping -c1","addpay":true,"append":"| grep -i \"100%\"","useSpawn":"false","timer":"","oldrc":false,"name":"","x":279,"y":312,"wires":[["6eb947a3.51da88"],[],[]],"l":false},{"id":"6eb947a3.51da88","type":"function","z":"674d3c11.099964","name":"","func":"n = global.get('network')\nip = msg.ip\nstate = (msg.payload===\"\")?true:false\n\nreturn {network:n,ipaddress:ip,state:state}","outputs":1,"noerr":0,"x":351,"y":312,"wires":[["55ea9ebe.c981b"]],"l":false},{"id":"55ea9ebe.c981b","type":"change","z":"674d3c11.099964","name":"","rules":[{"t":"set","p":"network","pt":"global","to":"\t$globalContext('network') ~>| **.properties[$contains(ip,$$.ipaddress)] |{\"reachable\":$boolean($$.state)} |","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":423,"y":312,"wires":[[]],"l":false},{"id":"4351f652.050938","type":"comment","z":"674d3c11.099964","name":"Update Network Status","info":"","x":172,"y":264,"wires":[]},{"id":"88de01e0.6d774","type":"comment","z":"674d3c11.099964","name":"Network - JSON API","info":"","x":402,"y":144,"wires":[]},{"id":"c32250dd.48ba3","type":"comment","z":"674d3c11.099964","name":"Set Topology","info":"","x":142,"y":144,"wires":[]},{"id":"51a55089.d0c8e8","type":"inject","z":"674d3c11.099964","name":"","topic":"","payload":"","payloadType":"date","repeat":"30","crontab":"","once":true,"onceDelay":0.1,"x":111,"y":312,"wires":[["6058a657.1e87d8"]],"l":false},{"id":"71c08d4e.d425fc","type":"ui_template","z":"674d3c11.099964","group":"948b2f03.477ee","name":"","order":1,"width":0,"height":0,"format":"<style>\n.nr-dashboard-template .ng-scope ._md .visible,.nr-dashboard-theme .ui-card-panel{background:transparent!important}\n\n.device{font-weight:bold}\ncircle {\n\tfill: #090;\n\tstroke: #090;\n\tstroke-width: 1.5px;\n}\ncircle.unreachable {\n\tfill: #f00;\n\tstroke: #f00;\n\tstroke-width: 1.5px;\n}\ncircle.reachable {\n\tfill: #090;\n\tstroke: #090;\n\tstroke-width: 1.5px;\n}\n.node,\ntext {\n\tfont: 10px 'Helvetica Neue';\n}\n.link {\n\tfill: none;\n\tstroke: #888;\n\tstroke-width: 1px;\n\tstroke-opacity: 1;\n}\n.reachable {\n\tfill: none;\n\tstroke: #090;\n}\n.unreachable {\n\tfill: none;\n\tstroke: #f30;\n}\n\n#chart {\n\twidth: 800px;\n\tmargin: 20px auto;\n\n}\n#chart path{\n fill:none;\n}\n#info{position:absolute;bottom:8px;}\n#info p{\n padding:8px;\n font-size:11px;\n}\n#info a{text-decoration:none;color:#f90}\n.weight{font-weight:bold}\n</style>\n<script type=\"text/javascript\">\nfunction renderNetwork() {\n $(\"body.nr-dashboard-theme .md-content .md-card,.nr-dashboard-theme .md-content .md-card,.nr-dashboard-theme .ui-card-panel\").css(\"background-color\",\"transparent!important\")\n\tvar w = 600,\n\t\th = 600;\n\n\tvar cluster = d3.layout.cluster().size([h, w - 200]);\n\n\tvar diagonal = d3.svg.diagonal().projection(function (d) {\n\t\treturn [d.y, d.x];\n\t});\n\n\tvar vis = d3\n\t\t.select('#chart')\n\t\t.append('svg:svg')\n\t\t.attr('width', w)\n\t\t.attr('height', h)\n\t\t.append('svg:g')\n\t\t.attr('transform', 'translate(70, 0)');\n\n\td3.json('../networkapi', function (json) {\n\t\tvar nodes = cluster.nodes(json);\n\n\t\tvar link = vis\n\t\t\t.selectAll('path.link')\n\t\t\t.data(cluster.links(nodes))\n\t\t\t.enter()\n\t\t\t.append('svg:path')\n\n\t\t\t.attr('class', 'link')\n\t\t\t.attr('d', diagonal);\n\n\t\tvar node = vis\n\t\t\t.selectAll('g.node')\n\t\t\t.data(nodes)\n\t\t\t.enter()\n\t\t\t.append('svg:g')\n\n\t\t\t.attr('transform', function (d) {\n\t\t\t\treturn 'translate(' + d.y + ',' + d.x + ')';\n\t\t\t});\n\n\t\tnode.append('svg:circle').attr('r', 4.5);\n\t\n\t\tnode.append('svg:text')\n\t\t\t.attr('dx', function (d) {\n\t\t\t\treturn d.children ? -8 : 8;\n\t\t\t})\n\n\t\t\t.attr('dy', 3)\n\t\t\t.attr('text-anchor', function (d) {\n\t\t\t\treturn d.children ? 'end' : 'start';\n\t\t\t})\n\t\t\t.attr('stroke-opacity', 0)\n\t\t\t.attr('fill', '#fff')\n\t\t\t.text(function (d) {\n\t\t\t\treturn d.name;\n\t\t\t})\n\t\t\t.attr('cursor', 'pointer')\n\t\t\t.on('mouseover', mouseover)\n\t\t\t.on('mouseout', mouseout)\n\t\t\t.on('click', info);\n\n\t\tvis.selectAll('circle')\n\t\t\t.filter(function (d) {\n\t\t\t\t//console.log(d.properties.reachable);\n\t\t\t\treturn d;\n\t\t\t})\n\t\t\t.attr('class', function (d, i) {\n\t\t\t//\tconsole.log(d, i);\n\t\t\t\tif (d.properties.reachable) {\n\t\t\t\t\treturn ' reachable';\n\t\t\t\t} else {\n\t\t\t\t\treturn ' unreachable';\n\t\t\t\t}\n\t\t\t});\n\t});\n}\n\nfunction updateNetwork() {\n console.log('update')\n\td3.select('#chart svg').remove();\n\n\trenderNetwork();\n}\nfunction mouseover(d, i) {\n\td3.select(this).attr({\n\t\tfill: 'orange',\n\t});\n}\nfunction mouseout(d, i) {\n\td3.select(this).attr({\n\t\tfill: '#fff',\n\t});\n}\nfunction info(d, i) {\n if(d.properties.type===\"ping\"){\n $('#info').html(\"<p>Pinging...</p>\") \n }\n else{\n $('#info').html(\"<p>Loading...</p>\")\n }\n\n\tsc.send({payload:{name:d.name,ip:d.properties.ip,type:d.properties.type}});\n}\nrenderNetwork()\n\nvar sc = scope;\n\nint = setInterval(function(){\n updateNetwork()\n},30000)\n</script>\n<script>\n(function(scope) {\n scope.$watch('msg', function(msg) {\n if (msg.update) {\n // Do something when msg arrives\n updateNetwork()\n // $(\"#my_\"+scope.$id).html(msg.payload);\n }\n if (msg.info){\n $(\"#info\").hide().html(\"\").html(msg.info).fadeIn()\n }\n });\n})(scope);\n</script>\n<div id=\"chart\"></div>\n<div id=\"info\"></div>\n \n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":255,"y":480,"wires":[["3ad6a50b.144c0a"]],"l":false},{"id":"ebb36c06.8fb5a","type":"comment","z":"674d3c11.099964","name":"Render Network Map","info":"","x":172,"y":384,"wires":[]},{"id":"41f7dc80.724e64","type":"inject","z":"674d3c11.099964","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":111,"y":456,"wires":[["afb1db1b.fa126"]],"l":false},{"id":"fc9ae982.d0555","type":"function","z":"674d3c11.099964","name":"","func":"return {update:true}","outputs":1,"noerr":0,"x":207,"y":456,"wires":[["71c08d4e.d425fc"]],"l":false},{"id":"3ad6a50b.144c0a","type":"switch","z":"674d3c11.099964","name":"","property":"payload.type","propertyType":"msg","rules":[{"t":"eq","v":"tasmota","vt":"str"},{"t":"eq","v":"ping","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":303,"y":480,"wires":[["cd498e21.cfd81"],["43c44754.63804"]],"l":false},{"id":"cd498e21.cfd81","type":"function","z":"674d3c11.099964","name":"","func":"u = \"http://\"\nm = msg.payload\n\nurl = \"http://\"+m.ip+\"/cm?cmnd=status%200\"\n\n\nreturn {url:url,node:m.name,ip:m.ip}","outputs":1,"noerr":0,"x":351,"y":456,"wires":[["a6b24bf8.904898"]],"l":false},{"id":"a6b24bf8.904898","type":"http request","z":"674d3c11.099964","name":"","method":"GET","ret":"obj","paytoqs":false,"url":"","tls":"","persist":false,"proxy":"","authType":"","x":399,"y":456,"wires":[["db490bc9.521ca8"]],"l":false},{"id":"db490bc9.521ca8","type":"template","z":"674d3c11.099964","name":"Tasmota","field":"info","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<p>\n<span class='weight'>{{node}}</span><br/>\n<span class='weight'>IP</span> <a href=\"http://{{ip}}\" target=\"_blank\">{{ip}}</a>\n<span class='weight'>Uptime</span> {{payload.StatusSTS.Uptime}} \n<span class='weight'>Version</span> {{payload.StatusFWR.Version}} \n<span class='weight'>SSId</span> {{payload.StatusSTS.Wifi.SSId}} \n<span class='weight'>RSSI</span> {{payload.StatusSTS.Wifi.RSSI}} \n</p>","output":"str","x":492,"y":456,"wires":[["4d477f4c.f0314"]]},{"id":"4d477f4c.f0314","type":"link out","z":"674d3c11.099964","name":"tasmota template out","links":["46559f3.46bf56"],"x":615,"y":480,"wires":[]},{"id":"46559f3.46bf56","type":"link in","z":"674d3c11.099964","name":"","links":["4d477f4c.f0314"],"x":207,"y":504,"wires":[["71c08d4e.d425fc"]]},{"id":"43c44754.63804","type":"function","z":"674d3c11.099964","name":"","func":"m = msg.payload\ncmd = \"ping -c4 \"+msg.payload.ip +\" | tail -n2\"\n\nreturn {payload:cmd,node:m.name,ip:m.ip}","outputs":1,"noerr":0,"x":351,"y":504,"wires":[["6ba88c05.e45f14"]],"l":false},{"id":"6ba88c05.e45f14","type":"exec","z":"674d3c11.099964","command":"","addpay":true,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"","x":399,"y":504,"wires":[["a9061c41.5ef538"],[],[]],"l":false},{"id":"a9061c41.5ef538","type":"template","z":"674d3c11.099964","name":"Ping","field":"info","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<p>\n<span class='weight'>{{node}} ({{ip}})</span><br/>\n{{payload}}\n</p>","output":"str","x":482,"y":504,"wires":[["4d477f4c.f0314"]]},{"id":"84782896.673eb8","type":"comment","z":"674d3c11.099964","name":"Help - see Info tab","info":"### Set Topology in inject node\n\nFormat the network topology as (can have as many depths/connections as you like):\n\n```\n{\n \"name\": \"Internet\",\n \"properties\": {\n \"type\": \"ping\",\n \"reachable\": false,\n \"ip\": \"google.com\"\n },\n \"children\": [\n {\n \"name\": \"Internet Router\",\n \"properties\": {\n \"type\": \"ping\",\n \"reachable\": false,\n \"ip\": \"192.168.1.1\"\n },\n \"children\": [\n {\n \"name\": \"Switch 1\",\n \"properties\": {\n \"type\": \"ping\",\n \"reachable\": false,\n \"ip\": \"192.168.1.10\"\n },\n \"children\": [\n {\n \"name\": \"Switch 2\",\n \"properties\": {\n \"type\": \"ping\",\n \"reachable\": false,\n \"ip\": \"192.168.1.20\"\n },\n \"children\": [\n {\n \"name\": \"Computer 1\",\n \"properties\": {\n \"type\": \"ping\",\n \"reachable\": false,\n \"ip\": \"192.168.1.100\"\n }\n },\n {\n \"name\": \"Computer 2\",\n \"properties\": {\n \"type\": \"ping\",\n \"reachable\": false,\n \"ip\": \"192.168.1.101\"\n }\n }\n ]\n },\n {\n \"name\": \"Hue Bridge\",\n \"properties\": {\n \"type\": \"ping\",\n \"ip\": \"192.168.1.30\",\n \"reachable\": false\n }\n },\n {\n \"name\": \"TV\",\n \"properties\": {\n \"type\": \"ping\",\n \"ip\": \"192.168.1.40\",\n \"reachable\": false\n }\n },\n {\n \"name\": \"NAS\",\n \"properties\": {\n \"type\": \"ping\",\n \"reachable\": false,\n \"ip\": \"192.168.1.200\"\n }\n }\n ]\n },\n {\n \"name\": \"Access Point\",\n \"properties\": {\n \"type\": \"ping\",\n \"reachable\": false,\n \"ip\": \"192.168.1.50\"\n },\n \"children\": [\n {\n \"name\": \"Wireless device 1\",\n \"properties\": {\n \"type\": \"tasmota\",\n \"ip\": \"192.168.1.61\",\n \"reachable\": false\n }\n },\n {\n \"name\": \"Wireless device 2\",\n \"properties\": {\n \"type\": \"tasmota\",\n \"ip\": \"192.168.1.63\",\n \"reachable\": false\n }\n }\n ]\n }\n ]\n }\n ]\n}\n```\n\n### Types\n\nThe flow currently has 2 types of information that can be displayed when a node is clicked in the network map:\n\n- ping\n- tasmota\n\nWith \"ping\" it will try to ping and display the rtt/packet loss\n\nWith \"tasmota\" it will use a http request node to execute the `status 0` and return the ssid/version/rssi. You can click the ip address to open the tasmota ui in a new tab\n\nTemplate nodes are used to format the output per type.\n\nThe switch node can be used to add/remove others\n\n#### Detail\n\nIt creates a web location: /networkapi that shows the topology and is read by the network map in the ui-template node\n\nThe map is refreshed every 30 seconds, can be changed in the ui-template node","x":162,"y":72,"wires":[]},{"id":"29604fa0.ef3b98","type":"inject","z":"674d3c11.099964","name":"","topic":"","payload":"{\"name\":\"Internet\",\"properties\":{\"type\":\"ping\",\"reachable\":false,\"ip\":\"google.com\"},\"children\":[{\"name\":\"Internet Router\",\"properties\":{\"type\":\"ping\",\"reachable\":false,\"ip\":\"192.168.1.1\"},\"children\":[{\"name\":\"Switch 1\",\"properties\":{\"type\":\"ping\",\"reachable\":false,\"ip\":\"192.168.1.10\"},\"children\":[{\"name\":\"Switch 2\",\"properties\":{\"type\":\"ping\",\"reachable\":false,\"ip\":\"192.168.1.20\"},\"children\":[{\"name\":\"Computer 1\",\"properties\":{\"type\":\"ping\",\"reachable\":false,\"ip\":\"192.168.1.100\"}},{\"name\":\"Computer 2\",\"properties\":{\"type\":\"ping\",\"reachable\":false,\"ip\":\"192.168.1.101\"}}]},{\"name\":\"Hue Bridge\",\"properties\":{\"type\":\"ping\",\"ip\":\"192.168.1.30\",\"reachable\":false}},{\"name\":\"TV\",\"properties\":{\"type\":\"ping\",\"ip\":\"192.168.1.40\",\"reachable\":false}},{\"name\":\"NAS\",\"properties\":{\"type\":\"ping\",\"reachable\":false,\"ip\":\"192.168.1.200\"}}]},{\"name\":\"Access Point\",\"properties\":{\"type\":\"ping\",\"reachable\":false,\"ip\":\"192.168.1.50\"},\"children\":[{\"name\":\"Wireless device 1\",\"properties\":{\"type\":\"tasmota\",\"ip\":\"192.168.1.61\",\"reachable\":false}},{\"name\":\"Wireless device 2\",\"properties\":{\"type\":\"tasmota\",\"ip\":\"192.168.1.63\",\"reachable\":false}}]}]}]}","payloadType":"json","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":111,"y":192,"wires":[["62016808.314be8"]],"l":false},{"id":"afb1db1b.fa126","type":"delay","z":"674d3c11.099964","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":159,"y":456,"wires":[["fc9ae982.d0555"]],"l":false},{"id":"948b2f03.477ee","type":"ui_group","z":"","name":"Network","tab":"458d5bcd.691d94","order":1,"disp":false,"width":17,"collapse":false},{"id":"458d5bcd.691d94","type":"ui_tab","z":"","name":"Network","icon":"dashboard","order":4,"disabled":false,"hidden":false}]