-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathnode_red_config_complete_ex_API.txt
1 lines (1 loc) · 25 KB
/
node_red_config_complete_ex_API.txt
1
[{"id":"8dffa1ad.df32e","type":"http in","z":"200e9cc.0b8e964","name":"","url":"/clients","method":"get","swaggerDoc":"","x":217,"y":323,"wires":[["bb1246e0.068548"]]},{"id":"706fdab5.d3c3c4","type":"http in","z":"200e9cc.0b8e964","name":"","url":"/clients/:mac","method":"get","swaggerDoc":"","x":237,"y":423,"wires":[["bcc6b9f9.a3b988"]]},{"id":"57675d03.a620c4","type":"mongodb2 in","z":"200e9cc.0b8e964","service":"_ext_","configNode":"17390fba.2446c","name":"Meraki Mongodb Store (findOne)","collection":"scanningapi","operation":"findOne","x":947,"y":503,"wires":[["96bf6937.d890e8","6a46c6c7.0e81a8"]]},{"id":"54262d21.f9b4d4","type":"mongodb2 in","z":"200e9cc.0b8e964","service":"_ext_","configNode":"17390fba.2446c","name":"Meraki Mongodb Store (find.toArray)","collection":"scanningapi","operation":"find.toArray","x":817,"y":323,"wires":[["540aadd9.f60514"]]},{"id":"96bf6937.d890e8","type":"http response","z":"200e9cc.0b8e964","name":"","x":1177,"y":503,"wires":[]},{"id":"540aadd9.f60514","type":"http response","z":"200e9cc.0b8e964","name":"","x":1177,"y":323,"wires":[]},{"id":"bcc6b9f9.a3b988","type":"function","z":"200e9cc.0b8e964","name":"msg.payload = msg.req.params.mac;","func":"//Extract MAC Address\nmsg.payload = msg.req.params.mac;\nreturn msg;\n","outputs":1,"noerr":0,"x":397,"y":463,"wires":[["83d8979f.75e748"]]},{"id":"c119a2b0.facd","type":"comment","z":"200e9cc.0b8e964","name":"Logging Meraki Scanning API Data","info":"","x":287,"y":103,"wires":[]},{"id":"1e62b9ce.7f3906","type":"function","z":"200e9cc.0b8e964","name":"Prepare JSON for DB Operation:Upsert","func":"// This function updates/creates the client in the database\nvar filter = msg.payload;\nif (\"string\" == typeof filter) {\n filter = JSON.parse(filter);\n}\n\n// Add initial time seen to mongodb\nmsg.payload = [{'name':msg.payload.name},\n {$set:msg.payload,$setOnInsert:{'cfirstseen':msg.payload.clastseen}},\n {upsert:true}];\nmsg.log = msg;\nreturn msg;\n","outputs":1,"noerr":0,"x":687,"y":203,"wires":[["3970784d.2b8d58"]]},{"id":"3970784d.2b8d58","type":"mongodb2 in","z":"200e9cc.0b8e964","service":"_ext_","configNode":"17390fba.2446c","name":"Meraki Mongodb Store (findOneAndUpdate)","collection":"scanningapi","operation":"findOneAndUpdate","x":1057,"y":203,"wires":[[]]},{"id":"695989a7.5e22c8","type":"function","z":"200e9cc.0b8e964","name":"Format API Data","func":"// This Node-RED function extracts Meraki Scanning API data and prepares it for entry into Mongodb\n// If payload is empty return null\nif(msg.payload === null){\n return null;\n}\n\nvar map = msg.payload;\nclient = {}; \n\n// Localise Time to Local System Parameters and truncate\nfunction localiseTime(time) {\n time = new Date(time);\n time = time.toString();\n time = time.split(' ').slice(0, 5).join(' ');\n return time;\n}\n\n// Truncate IP field\nfunction ipClean(ip) {\n if (ip !== null) {\n ip = ip.substring(1);}\n return ip;\n}\n\n// Format Epoch Time to meet MV location request requirements\nfunction localiseEpoch(time) {\n //convert Scanning API epoch time to millisecond variant\n time = time * 1000;\n return time;\n}\n\n// Log/validate Meraki Scanning API version for debugging purposes\nif (map.version != '2.0'){\n msg.log = \"Received Scanning API post with unexpected version. Please ensure Version 2 is configured in Dashboard. Received Version: \"+map.version;\n } else {\n msg.log = \"Received Scanning API Version 2 Post.\";\n}\n\n// Normalise Wireless Client observations\nif (map.type == \"DevicesSeen\"){\n var obj = map.data.observations;\n for (var prop in obj){\n if (obj.hasOwnProperty(prop)) {\n if (!obj[prop].location){continue;}\n client.type = \"wireless\";\n client.name = obj[prop].clientMac;\n client.mac = obj[prop].clientMac;\n client.lat = obj[prop].location.lat;\n client.lng = obj[prop].location.lng;\n client.unc = obj[prop].location.unc;\n client.rssi = obj[prop].rssi;\n client.clastseen = localiseTime(obj[prop].seenTime);\n client.floors = map.data.apFloors === null ? \"\" : map.data.apFloors.join;\n client.manufacturer = obj[prop].manufacturer;\n client.os = obj[prop].os;\n client.ipv4 = ipClean(obj[prop].ipv4);\n client.ssid = obj[prop].ssid;\n client.ap = map.data.apMac;\n client.epoch = localiseEpoch(obj[prop].seenEpoch);\n client.tag = map.data.apTags;\n }\n // Return object array properties\n msg.payload = client;\n node.send(msg);\n }\n }\n\n// Normalise Bluetooth Client observations\n if (map.type == \"BluetoothDevicesSeen\"){\n var obj = map.data.observations;\n for (var prop in obj){\n if (obj.hasOwnProperty(prop)) {\n if (!obj[prop].location){continue;}\n client.type = \"bluetooth\";\n client.name = obj[prop].clientMac;\n client.mac = obj[prop].clientMac;\n client.lat = obj[prop].location.lat;\n client.lng = obj[prop].location.lng;\n client.unc = obj[prop].location.unc;\n client.rssi = obj[prop].rssi;\n client.clastseen = localiseTime(obj[prop].seenTime);\n client.floors = map.data.apFloors === null ? \"\" : map.data.apFloors.join;\n client.ap = map.data.apMac;\n client.epoch = localiseEpoch(obj[prop].seenEpoch);\n client.tag = map.data.apTags;\n }\n // Return object array properties\n msg.payload = client;\n node.send(msg);\n }\n }\n \nreturn null;","outputs":1,"noerr":0,"x":397,"y":203,"wires":[["1e62b9ce.7f3906"]]},{"id":"bb1246e0.068548","type":"function","z":"200e9cc.0b8e964","name":"find({})","func":"// Optional function to query /clients and write to payload to query mongodb present via HTML\n// default payload is null\nmsg.payload = {};\nreturn msg;","outputs":1,"noerr":0,"x":477,"y":323,"wires":[["54262d21.f9b4d4"]]},{"id":"83d8979f.75e748","type":"function","z":"200e9cc.0b8e964","name":"msg.payload = {'name':msg.payload};","func":"// Prepend key 'name' to MAC object payload\nmsg.payload = {'name':msg.payload};\nreturn msg;","outputs":1,"noerr":0,"x":637,"y":503,"wires":[["57675d03.a620c4"]]},{"id":"d1291487.cce0f8","type":"template","z":"200e9cc.0b8e964","name":"CSS","field":"payload.style","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"html, body {\n margin: 0;\n padding: 0;\n}\n\nbody {\n font-family: \"proxima-nova-1\",\"proxima-nova-2\", \"Helvetica Neue\", Helvetica, verdana, sans-serif;\n -webkit-font-smoothing: antialiased;\n}\n\n#masthead {\n height: 125px;\n width: 100%;\n position: relative;\n background: #FFFFFF;\n border-top: 4px solid #78be20;\n box-shadow: 0 2px 7px rgba(0,0,0,0.2);\n}\n\n#masthead-content {\n margin: 0 auto;\n position: relative;\n width: 80%;\n height: 100%;\n}\n\n#masthead-content img {\n float: left;\n margin: 32px;\n width: 165px;\n margin-left: 0;\n}\n\n#content {\n width: 80%;\n margin: 60px auto;\n padding: 40px;\n box-sizing: border-box;\n border-radius: 9px;\n background: #FAFAFA;\n}\n\n#mac-address {\n margin-bottom: 10px;\n}\n\n#mac-field {\n width: 30%;\n height: 35px;\n margin-bottom: 20px;\n padding-left: 13px;\n border: 1px solid #E6E6E6;\n border-radius: 2px;\n box-sizing: border-box;\n font-family: \"proxima-nova-1\",\"proxima-nova-2\", \"Helvetica Neue\", Helvetica, verdana, sans-serif;\n font-size: 16px;\n font-weight: 100;\n min-width: 136px;\n}\n\n#map-wrapper {\n width: 100%;\n height: 635px;\n}\n\n#map-canvas {\n height: 100%;\n width: 70%;\n}\n\n\nh1 {\n color: #78be20;\n font-weight: 100;\n font-size: 38px;\n margin-top: 0;\n letter-spacing: -1px;\n}\n\nh2 {\n color: #78be20;\n font-weight: 100;\n font-size: 25px;\n margin-top: 0;\n letter-spacing: -1px;\n}\n\nh3 {\n color: #78be20;\n font-weight: 75;\n font-size: 16px;\n margin-top: 0;\n letter-spacing: -1px;\n}\n\n\n#last-mac {\n color: #6B6B6B;\n width: 100%;\n font-weight: 400;\n margin-bottom: 10px;\n font-size: 14px;\n}\n\n.small {\n color: #6B6B6B;\n font-weight: 400;\n margin-bottom: 30px;\n font-size: 14px;\n}\n\n.bold {\n font-weight: 600;\n}\n\nbutton, input {\n width: 11%;\n}\n\nbutton {\n height: 35px;\n border: none;\n background: #737373;\n border-radius: 2px;\n box-sizing: border-box;\n color: white;\n font-family: \"proxima-nova-1\",\"proxima-nova-2\", \"Helvetica Neue\", Helvetica, verdana, sans-serif;\n font-weight: 200;\n font-size: 14px;\n padding: 0;\n min-width: 70px;\n}\n\nbutton:hover{\n background: #616060;\n}\n","x":737,"y":723,"wires":[["8332e8b4.0c8408"]]},{"id":"edd62576.f9d6c8","type":"template","z":"200e9cc.0b8e964","name":"JavaScript","field":"payload.script","fieldType":"msg","format":"javascript","syntax":"plain","template":"// This Node-RED template renders a client object/s and associated parameters on a Google Map\n(function ($) {\n var map, // Google map\n clientMarker, // Client marker when following a single client\n clientUncertaintyCircle, // Client circle representing client location uncertainty\n lastEvent, // Most recent scheduled polling task\n lastInfoWindowMac, // Most recent MAC displayed in a marker tooltip\n allMarkers = [], // Markers array when we are in \"View All\" context\n lastMac = \"\", // Most recent requested MAC to follow\n infoWindow = new google.maps.InfoWindow(); // Client marker tooltip\n\n var latlngbounds = new google.maps.LatLngBounds();\n\n// Removes all markers\n function clearAll() {\n clientMarker.setMap(null);\n clientUncertaintyCircle.setMap(null);\n lastInfoWindowMac = \"\";\n var m;\n while (allMarkers.length !== 0) {\n m = allMarkers.pop();\n if (infoWindow.anchor === m) {\n lastInfoWindowMac = m.mac;\n }\n m.setMap(null);\n }\n }\n\n// Plots the location and uncertainty for a single MAC address\n function track(client) {\n clearAll();\n if (client !== undefined && client.lat !== undefined && (typeof client.lat !== 'undefined')) {\n var pos = new google.maps.LatLng(client.lat, client.lng);\n console.log('track client pos '+pos);\n if (client.manufacturer !== undefined) {\n mfrStr = client.manufacturer + \" \";\n } else {\n mfrStr = \"\";\n }\n if (client.os !== undefined) {\n osStr = \" running '\" + client.os + \"'\";\n } else {\n osStr = \"\";\n }\n if (client.ssid !== undefined) {\n ssidStr = \" with SSID '\" + client.ssid + \"'\";\n } else {\n ssidStr = \"\";\n }\n if (client.floors !== undefined && client.floors !== \"\") {\n floorStr = \" at \" + client.floors + \"'\";\n } else {\n floorStr = \"\";\n }\n $('#last-mac').text(mfrStr + lastMac + osStr + ssidStr +\n \" last seen on \" + client.clastseen + floorStr +\n \" with uncertainty \" + client.unc.toFixed(1) + \" metres (reloading every 60 seconds)\");\n \n // Configure icon parameter based on type\n var iconType = \"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=W|ea4335|000000\";\n if (client.type == \"bluetooth\") {\n iconType = \"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=B|4285f4|000000\";\n }\n \n map.setCenter(pos);\n clientMarker.setMap(map);\n clientMarker.setPosition(pos);\n clientMarker.setIcon(iconType);\n \n clientUncertaintyCircle = new google.maps.Circle({\n map: map,\n center: pos,\n radius: client.unc,\n fillColor: 'Lime',\n fillOpacity: 0.25,\n strokeColor: 'Lime',\n strokeWeight: 1,\n });\n clientUncertaintyCircle.bindTo('center', clientMarker, 'position');\n map.fitBounds(clientUncertaintyCircle.getBounds());\n } else {\n $('#last-mac').text(\"Client '\" + lastMac + \"' could not be found\");\n }\n}\n\n// Looks up a single MAC address\n function lookup(mac) {\n $.getJSON('/clients/' + mac, function (response) {\n track(response);\n });\n }\n\n// Adds a marker for a single client within the \"View All\" context\n function addMarker(client) {\n var pos = new google.maps.LatLng(client.lat, client.lng);\n console.log('addMarker pos '+pos);\n \n // Configure icon parameter based on type\n var iconType = \"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=W|ea4335|000000\";\n if (client.type == \"bluetooth\") {\n iconType = \"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=B|4285f4|000000\";\n }\n \n // Create new marker\n var m = new google.maps.Marker({\n position: pos, \n map: map,\n mac: client.mac,\n icon: iconType,\n }\n );\n \n if(client.lat){\n latlngbounds.extend(pos);\n map.fitBounds(latlngbounds);\n map.setZoom(20.5);\n }\n \n google.maps.event.addListener(m, 'click', function () {\n var htmlString = '<h3>Client: '+client.name +'</h3>';\n \n for (var key in client) {\n if (client.hasOwnProperty(key)) {\n if(client[key] !== undefined){\n if(key == '_id' || key == 'name' || key == 'ap' || key == 'epoch' || key == 'tag'){continue}\n htmlString += '<p><b>'+key+'</b> : '+client[key]+'</p>';\n }\n }\n }\n \n infoWindow.setContent(\"<div>\" + htmlString + \"</div>\" + \"(<a class='client-filter' href='#' data-mac='\" +\n client.mac + \"'>Follow this client)</a>\");\n\n infoWindow.open(map, m);\n });\n if (client.mac === lastInfoWindowMac) {\n infoWindow.open(map, m);\n }\n allMarkers.push(m);\n }\n\n// Displays markers for all clients\n function trackAll(clients) {\n clearAll();\n if (clients.length === 0) {\n $('#last-mac').text(\"Found no clients (if you just started the web server, you may need to wait a few minutes to receive pushes from Meraki)\");\n } else { \n $('#last-mac').text(\"Found \" + clients.length + \" clients (reloading every 60 seconds)\"); }\n clientUncertaintyCircle.setMap(null);\n clients.forEach(addMarker);\n }\n\n// Looks up all client MAC addresses\n function lookupAll() {\n $('#last-mac').text(\"Looking up all clients...\");\n $.getJSON('/clients/', function (response) {\n trackAll(response);\n });\n }\n\n// Initiates a task timer to reload a single client MAC every 60 seconds\n function startLookup() {\n lastMac = $('#mac-field').val().trim();\n if (lastEvent !== null) { window.clearInterval(lastEvent); }\n lookup(lastMac);\n lastEvent = window.setInterval(lookup, 60000, lastMac);\n }\n\n// Initiates a task timer to reload all client MACs every 60 seconds\n function startLookupAll() {\n if (lastEvent !== null) { window.clearInterval(lastEvent); }\n lastEvent = window.setInterval(lookupAll, 60000);\n lookupAll();\n }\n\n// This is called after the DOM is loaded, so we can safely bind all the\n// listeners here.\n function initialize() {\n var center = new google.maps.LatLng(-37.8136, 144.9631);\n var mapOptions = {\n center: center,\n mapTypeId: 'hybrid' //or roadmap, satellite, hybrid, terrain\n };\n map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);\n clientMarker = new google.maps.Marker({\n position: center,\n map: null,\n });\n clientUncertaintyCircle = new google.maps.Circle({\n position: center,\n map: null\n });\n\n $('#track').click(startLookup).bind(\"enterKey\", startLookup);\n\n $('#all').click(startLookupAll);\n\n $(document).on(\"click\", \".client-filter\", function (e) {\n e.preventDefault();\n var mac = $(this).data('mac');\n $('#mac-field').val(mac);\n startLookup();\n });\n\n startLookupAll();\n }\n\n// Call the initialize function when the window loads\n $(window).load(initialize);\n}(jQuery));","x":518,"y":723,"wires":[["d1291487.cce0f8"]]},{"id":"fc2b223e.8759a","type":"http in","z":"200e9cc.0b8e964","name":"","url":"/scanningapimap","method":"get","upload":false,"swaggerDoc":"","x":247,"y":723,"wires":[["edd62576.f9d6c8"]]},{"id":"187fa1e5.33b63e","type":"http response","z":"200e9cc.0b8e964","name":"","x":1177,"y":723,"wires":[]},{"id":"8332e8b4.0c8408","type":"template","z":"200e9cc.0b8e964","name":"HTML","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<html>\n <head>\n <title>Cisco Meraki Scanning API</title>\n <meta name=\"viewport\" content=\"initial-scale=1.0, user-scalable=no\">\n <meta charset=\"utf-8\">\n <script src=\"https://maps.googleapis.com/maps/api/js?v=3.exp&key=<Google API Key>\"></script>\n <script src=\"http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.0/jquery.min.js\"></script>\n <script>{{{payload.script}}}</script>\n <style>{{{payload.style}}}</style>\n </head>\n <body>\n <div id=\"masthead\">\n <div id=\"masthead-content\">\n <img src=\"https://meraki.cisco.com/img/cisco-meraki.png\"/>\n </div>\n </div>\n <div id=\"content\">\n <h1>Cisco Meraki Scanning API <small><i><h2>integrating Cisco Meraki MV, Cisco Spark, Google Maps & Node-RED</h2></i></small></h1>\n <div id=\"mac-address\">\n <input id=\"mac-field\" type=\"text\" placeholder=\"Enter MAC address\" /> \n <button id=\"track\">Follow</button> \n <button id=\"all\">View All</button>\n <button><a href=/clients target=\"_blank\" style=\"text-decoration:none; color: inherit\">View All - JSON</a></button>\n </div>\n <div id=\"last-mac\"></div>\n <div class=\"small\"><span class=\"bold\">Clients in the wrong place?</span> Make sure your APs are placed properly in Dashboard.</div>\n <div id=\"map-wrapper\">\n <div id=\"map-canvas\"></div>\n </div>\n </div>\n </body>\n</html>","x":937,"y":723,"wires":[["187fa1e5.33b63e"]]},{"id":"ffb430a8.9dda6","type":"comment","z":"200e9cc.0b8e964","name":"Client Front-end API (View All - JSON) ","info":"","x":297,"y":283,"wires":[]},{"id":"4e30f048.c3b2b","type":"comment","z":"200e9cc.0b8e964","name":"Client Front-end Website","info":"","x":257,"y":683,"wires":[]},{"id":"f14f97a.f365c68","type":"Meraki CMX","z":"200e9cc.0b8e964","name":"Meraki API Collector","url":"/scanningapi","settings":"7baf50d4.5370a","radioType":"All","x":237,"y":143,"wires":[["695989a7.5e22c8"],[]]},{"id":"30eafae3.8db026","type":"comment","z":"200e9cc.0b8e964","name":"Client Front-end API (Follow) ","info":"","x":267,"y":383,"wires":[]},{"id":"a0bad6d2.154ec8","type":"function","z":"200e9cc.0b8e964","name":"Send Cisco Spark Message","func":"// This function parses the Meraki Scanning API data to construct\n// HTTP POST parameters for sending a Cisco Spark message to a Spark room.\n\n// Personal Access Token\nvar accessToken = \"<Cisco Spark API Key>\"; \n// Spark roomId\nvar roomId = \"<Cisco Spark Room ID>\"; \n\n// Build Cisco Spark API Call\nmsg.url = \"https://api.ciscospark.com/v1/messages\";\nmsg.headers = { \n 'content-type': 'application/json',\n 'authorization': 'Bearer ' + accessToken \n};\n\n// Construct payload for Cisco Spark Message\nmsg.payload = { \n roomId: roomId,\n markdown: msg.payload.markdown,\n files: msg.payload.files\n};\n\n// Post message payload\nmsg.method = \"post\";\n\nreturn msg;","outputs":"1","noerr":0,"x":927,"y":643,"wires":[["ee87479.e4692b8"]]},{"id":"ee87479.e4692b8","type":"http request","z":"200e9cc.0b8e964","name":"","method":"use","ret":"txt","url":"","tls":"","x":1157,"y":643,"wires":[[]]},{"id":"9e6dbe5e.74fdd","type":"function","z":"200e9cc.0b8e964","name":"Construct files and markdown properties","func":"// Construct custom Spark Message for Follow Client Tracking\n// Build custom Google PATH to create uncerntainty circle\n\n// Your Google Static Map API Key\nvar googleApi = '<Google API Key>';\n\n// Google Static Maps Path Circle Function\n// Ref: borland592:https://stackoverflow.com/questions/7316963/drawing-a-circle-google-static-maps/35660617#35660617\nfunction GMapCircle(lat,lng,rad,detail=8){\n\n // Customise your Static Map parameters \n var uri = 'https://maps.googleapis.com/maps/api/staticmap?';\n var staticMapSrc = 'center=' + lat + ',' + lng;\n staticMapSrc += '&zoom=19';\n staticMapSrc += '&scale=2';\n staticMapSrc += '&size=640x400';\n staticMapSrc += '&maptype=roadmap';\n staticMapSrc += '&key='+googleApi;\n staticMapSrc += '&format=jpg';\n staticMapSrc += '&path=color:0x29fd2f|weight:3|fillcolor:0x29fd2f33';\n\n var r = 6371;\n var pi = Math.PI;\n var _lat = (lat * pi) / 180;\n var _lng = (lng * pi) / 180;\n var d = (rad/1000) / r;\n\n var i = 0;\n for(i = 0; i <= 360; i+=detail) {\n var brng = i * pi / 180;\n\n var pLat = Math.asin(Math.sin(_lat) * Math.cos(d) + Math.cos(_lat) * Math.sin(d) * Math.cos(brng));\n var pLng = ((_lng + Math.atan2(Math.sin(brng) * Math.sin(d) * Math.cos(_lat), Math.cos(d) - Math.sin(_lat) * Math.sin(pLat))) * 180) / pi;\n pLat = (pLat * 180) / pi;\n\n staticMapSrc += \"|\" + pLat + \",\" + pLng;\n }\n\n// Add Marker to Map\nif (msg.payload.type == \"wireless\") {\n staticMapSrc += '&markers=color:0xea4335|label:W|'+lat +','+ lng;\n} else {\n staticMapSrc += '&markers=color:0x4285f4|label:B|'+lat +','+ lng;\n}\n\n// Return custom map URI\nreturn uri + encodeURI(staticMapSrc);}\n\n// Pass Scanning API parameters to GMapCircle Function\nvar staticMap = GMapCircle(msg.payload.lat,msg.payload.lng,msg.payload.unc,detail=8);\n\n// Build markdown payload\nif (msg.payload.type == \"wireless\") {\n // Render wireless markdown\n msg.payload.markdown = '**Following Wireless Client**<br>' + \n '**Name:** ' + msg.payload.name +\n '<br>**First Seen:** ' + msg.payload.cfirstseen + \n '<br>**Last Seen:** ' + msg.payload.clastseen +\n '<br>**Latitude:** ' + msg.payload.lat + \n '<br>**Longitude:** ' + msg.payload.lng +\n '<br>**IP:** ' + msg.payload.ipv4 +\n '<br>**MAC:** ' + msg.payload.mac +\n '<br>**Manufacturer:** ' + msg.payload.manufacturer + \n '<br>**OS:** ' + msg.payload.os +\n '<br>**RSSI:** ' + msg.payload.rssi + \n '<br>**SSID:** ' + msg.payload.ssid;\n } else {\n // Render bluetooth markdown string\n msg.payload.markdown = '**Following Bluetooth Client**<br>' + \n '**Name:** ' + msg.payload.name +\n '<br>**First Seen:** ' + msg.payload.cfirstseen +\n '<br>**Last Seen:** ' + msg.payload.clastseen +\n '<br>**Latitude:** ' + msg.payload.lat + \n '<br>**Longitude:** ' + msg.payload.lng + \n '<br>**MAC:** ' + msg.payload.mac + \n '<br>**RSSI:** ' + msg.payload.rssi;\n}\n\n// Build Camera String for Spark Post based on tag\nif (msg.payload.tag.indexOf('front') >= 0) {\n msg.payload.markdown = msg.payload.markdown + '<br>**Camera:** ' + 'https://<shard>.meraki.com/<network>/n/<group>/manage/nodes/new_list/<node A>?timestamp=' + msg.payload.epoch;\n}\nif (msg.payload.tag.indexOf('rear') >= 0) {\n msg.payload.markdown = msg.payload.markdown + '<br>**Camera:** ' + 'https://<shard>.meraki.com/<network>/n/<group>/manage/nodes/new_list/<node B>?timestamp=' + msg.payload.epoch;\n}\nif (msg.payload.tag.indexOf('hall') >= 0) {\n msg.payload.markdown = msg.payload.markdown + '<br>**Camera:** ' + 'https://<shard>.meraki.com/<network>/n/<group>/manage/nodes/new_list/<node C>?timestamp=' + msg.payload.epoch;\n}\n\n\n// Render files string\nmsg.payload.files = staticMap;\n\nreturn msg;","outputs":1,"noerr":0,"x":967,"y":603,"wires":[["a0bad6d2.154ec8"]]},{"id":"6a46c6c7.0e81a8","type":"change","z":"200e9cc.0b8e964","name":"Append markdown and files","rules":[{"t":"set","p":"payload.files","pt":"msg","to":"null","tot":"str"},{"t":"set","p":"payload.markdown","pt":"msg","to":"null","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":927,"y":563,"wires":[["9e6dbe5e.74fdd"]]},{"id":"99a3e9e2.301388","type":"comment","z":"200e9cc.0b8e964","name":"Follow Client Spark Feed","info":"","x":257,"y":563,"wires":[]},{"id":"17390fba.2446c","type":"mongodb2","z":"","uri":"mongodb://localhost:27017/scanningdb","name":"scanningdb","options":"","parallelism":"-1"},{"id":"7baf50d4.5370a","type":"meraki-cmx-settings","z":"","name":"scanningapi"}]