Cisco Meraki CMX: Example Google Map flow

Overview

This example flow demonstrates the power of the Cisco Meraki CMX API. By using a CMX node to capture location analytics data, this information can be used to place detected WiFi clients on a Google map. Any additional data captured for that client is then placed in an info window for the respective client.

Complete write-up

http://www.InternetOfLEGO.com

Cisco Meraki CMX docs

https://documentation.meraki.com/MR/Monitoring_and_Reporting/CMX_Analytics http://developers.meraki.com/tagged/Location

Pre-req

MongoDB https://docs.mongodb.com/manual/installation/

MongoDB2 node http://flows.nodered.org/node/node-red-contrib-mongodb2

Meraki CMX node http://flows.nodered.org/node/node-red-contrib-meraki-cmx

Usage

Website

http://yourserver:1880/cmxapimap

All Client Details

http://yourserver:1880/clients

Specific Client

http://yourserver:1880/client:mac

Written by Cory Guynn 2016

MIT License

[{"id":"6e0a9b11.29b1f4","type":"http in","z":"57bbd386.23f07c","name":"","url":"/clients","method":"get","swaggerDoc":"","x":113,"y":520,"wires":[["fa4e3dba.7eb65"]]},{"id":"7bbac9d3.0df198","type":"http in","z":"57bbd386.23f07c","name":"","url":"/clients/:mac","method":"get","swaggerDoc":"","x":133,"y":600,"wires":[["cda8c0f2.b0d8a"]]},{"id":"75ecbb4b.7cd6c4","type":"mongodb2 in","z":"57bbd386.23f07c","service":"_ext_","configNode":"1778d567.52c97b","name":"","collection":"cmxmapapi","operation":"findOne","x":393,"y":720,"wires":[["b88d8b9f.2e8a38","f587bf83.fbe5d"]]},{"id":"9fea066d.3f1438","type":"mongodb2 in","z":"57bbd386.23f07c","service":"_ext_","configNode":"1778d567.52c97b","name":"","collection":"cmxmapapi","operation":"find.toArray","x":503,"y":520,"wires":[["4865e022.4e32e","2366f708.a8e978"]]},{"id":"811330d1.8391d","type":"debug","z":"57bbd386.23f07c","name":"/clients/:mac","active":false,"console":"false","complete":"payload","x":713,"y":600,"wires":[]},{"id":"b88d8b9f.2e8a38","type":"debug","z":"57bbd386.23f07c","name":"/clients/:mac - mongodb","active":false,"console":"false","complete":"payload","x":673,"y":760,"wires":[]},{"id":"f587bf83.fbe5d","type":"http response","z":"57bbd386.23f07c","name":"","x":733,"y":720,"wires":[]},{"id":"4865e022.4e32e","type":"http response","z":"57bbd386.23f07c","name":"","x":733,"y":520,"wires":[]},{"id":"cda8c0f2.b0d8a","type":"function","z":"57bbd386.23f07c","name":"msg.payload=msg.req.params.mac;","func":"//extract mac\nmsg.payload=msg.req.params.mac;\nreturn msg;\n","outputs":1,"noerr":0,"x":433,"y":600,"wires":[["811330d1.8391d","fba88a4d.26bc88"]]},{"id":"125874f2.0c425b","type":"comment","z":"57bbd386.23f07c","name":"Log data","info":"","x":143,"y":240,"wires":[]},{"id":"3174c879.299a28","type":"function","z":"57bbd386.23f07c","name":"build operation parameters: filter, update","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\nmsg.payload = [\n    {'name':msg.payload.name},\n    msg.payload,\n    {upsert:true}\n];\n\nreturn msg;\n","outputs":1,"noerr":0,"x":423,"y":300,"wires":[["32ec6dd3.bff9d2"]]},{"id":"32ec6dd3.bff9d2","type":"mongodb2 in","z":"57bbd386.23f07c","service":"_ext_","configNode":"1778d567.52c97b","name":"","collection":"cmxmapapi","operation":"findOneAndUpdate","x":413,"y":340,"wires":[["73db14fa.89f71c"]]},{"id":"73db14fa.89f71c","type":"debug","z":"57bbd386.23f07c","name":"mongdb insert/update","active":true,"console":"false","complete":"payload","x":683,"y":340,"wires":[]},{"id":"1f738c4a.b4b534","type":"function","z":"57bbd386.23f07c","name":"Format Client","func":"// This function extracts the raw CMX data to create a consistent DB entry\nif(!msg.payload){\n    return null;\n}\n\nmap = msg.payload;\nclient = {}; //reset payload object for clarity\n\nif (map['version'] != '2.0'){\n    msg.log = \"got post with unexpected version: #{map['version']}\";\n    return msg;\n}else{\n    msg.log = \"working with correct version\";\n}\nif (map['type'] != 'DevicesSeen'){\nmsg.log = \"got post for event that we're not interested in: #{map['type']}\";\nreturn msg;\n}\n\nvar o = map['data']['observations'];\nconsole.log('map.data.apMac = '+map.data['apMac']);\n   for (var c in o){\n    if (o.hasOwnProperty(c)) {\n        //console.log(\"Key is \" + c + \", value is \" + o[c].location.lat);\n        if (!o[c]['location']){continue}\n        client.name = o[c]['clientMac'];\n        client.mac = o[c]['clientMac'];\n        client.lat = o[c]['location']['lat'];\n        client.lng = o[c]['location']['lng'];\n        client.unc = o[c]['location']['unc'];\n        client.seenString = o[c]['seenTime'];\n        client.seenEpoch = o[c]['seenEpoch'];\n        client.floors = map['data']['apFloors'] === null ? \"\" : map['data']['apFloors'].join;\n        client.manufacturer = o[c]['manufacturer'];\n        client.os = o[c]['os'];\n        client.ssid = o[c]['ssid'];\n        client.ap = map['data']['apMac'];\n        msg.log = \"AP #{map['data']['apMac']} on #{map['data']['apFloors']}: #{c}\";\n        if (client.seenEpoch===null || client.seenEpoch === 0){continue}//  # This probe is useless, so ignore it\n        \n    }\n    msg.payload = client;\n    node.send(msg);\n   }\n\n   \nreturn msg;","outputs":1,"noerr":0,"x":343,"y":240,"wires":[["f3de897b.807bb8","78973a36.c8af24","3174c879.299a28"]]},{"id":"f3de897b.807bb8","type":"debug","z":"57bbd386.23f07c","name":"format client","active":true,"console":"false","complete":"payload","x":713,"y":280,"wires":[]},{"id":"c8efb60f.be42c8","type":"debug","z":"57bbd386.23f07c","name":"/cmx3 Raw Data","active":true,"console":"false","complete":"payload","x":693,"y":400,"wires":[]},{"id":"fa4e3dba.7eb65","type":"function","z":"57bbd386.23f07c","name":"find({})","func":"// Create search for all clients\nmsg.payload = {};\nreturn msg;","outputs":1,"noerr":0,"x":293,"y":520,"wires":[["9fea066d.3f1438"]]},{"id":"fba88a4d.26bc88","type":"function","z":"57bbd386.23f07c","name":"msg.payload = {'name':msg.payload};","func":"msg.payload = {'name':msg.payload};\nreturn msg;","outputs":1,"noerr":0,"x":433,"y":660,"wires":[["75ecbb4b.7cd6c4"]]},{"id":"46765b46.cb4eb4","type":"function","z":"57bbd386.23f07c","name":"CMX Sample Data","func":"msg.payload = {\n    \"version\": \"2.0\",\n    \"secret\": \"supersecret\",\n    \"type\": \"DevicesSeen\",\n    \"data\": {\n        \"apMac\": \"00:18:0a:13:dd:b0\",\n        \"apFloors\": [],\n        \"apTags\": [ \"\", \"home\", \"\" ],\n        \"observations\": [ { \n            \"ipv4\": \"/192.168.0.15\",\n            \"location\": {\n                \"lat\": 51.5355157,\n                \"lng\": -0.06990350000000944,\n                \"unc\": 1.1185886512767726,\n                \"x\": [], \"y\": [] \n                \n            },\n            \"seenTime\": \"2016-07-29T13:17:10Z\",\n            \"ssid\": \".interwebs\",\n            \"os\": \"Debian-based Linux\",\n            \"clientMac\": \"CC:CC:CC:CC:CC:CC\",\n            \"seenEpoch\": 1469798230,\n            \"rssi\": 48,\n            \"ipv6\": null,\n            \"manufacturer\": \"Edimax Technology\" \n            }, \n            { \n            \"ipv4\": null,\n            \"location\": {\n                \"lat\": 51.5133157,\n                \"lng\": -0.06890350000000944,\n                \"unc\": 49, \"x\": [],\n                \"y\": []\n                },\n            \"seenTime\": \"2016-07-29T13:17:07Z\",\n            \"ssid\": \"Hotspot-123\",\n            \"os\": \"fancyOS\",\n            \"clientMac\": \"DD:DD:DD:DD:DD:DD\",\n            \"seenEpoch\": 1469798227,\n            \"rssi\": 9,\n            \"ipv6\": null,\n            \"manufacturer\": \"Samsung(THAILAND)\"\n            }\n        ]\n    }\n};\nreturn msg;","outputs":1,"noerr":0,"x":353,"y":180,"wires":[["1f738c4a.b4b534","ae8df78c.63cfb8"]]},{"id":"3e4473d6.daf05c","type":"inject","z":"57bbd386.23f07c","name":"Sample Client C D","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":133,"y":180,"wires":[["46765b46.cb4eb4"]]},{"id":"ae8df78c.63cfb8","type":"debug","z":"57bbd386.23f07c","name":"CMX Sample Raw Data","active":true,"console":"false","complete":"payload","x":673,"y":180,"wires":[]},{"id":"78973a36.c8af24","type":"debug","z":"57bbd386.23f07c","name":"format client. info","active":true,"console":"false","complete":"info","x":693,"y":240,"wires":[]},{"id":"fc1137ad.b82148","type":"mongodb2 in","z":"57bbd386.23f07c","service":"_ext_","configNode":"1778d567.52c97b","name":"","collection":"cmxmapapi","operation":"find.toArray","x":403,"y":960,"wires":[["6855ebd7.893d94"]]},{"id":"b3304ba1.893f98","type":"inject","z":"57bbd386.23f07c","name":"List all clients","topic":"","payload":"{}","payloadType":"json","repeat":"","crontab":"","once":false,"x":113,"y":960,"wires":[["fc1137ad.b82148"]]},{"id":"6855ebd7.893d94","type":"debug","z":"57bbd386.23f07c","name":"mongo data","active":true,"console":"false","complete":"true","x":713,"y":960,"wires":[]},{"id":"f831817c.3396c","type":"template","z":"57bbd386.23f07c","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: 700px;\n}\n\n#map-canvas {\n  height: 70%;\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\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":453,"y":860,"wires":[["bc79c796.5d4748"]]},{"id":"1f95ea1d.28f096","type":"template","z":"57bbd386.23f07c","name":"JavaScript","field":"payload.script","fieldType":"msg","format":"javascript","syntax":"plain","template":"(function ($) {\n  var map,                                      // This is the Google map\n    clientMarker,                               // The current marker when we are following a single client\n    clientUncertaintyCircle,                    // The circle describing that client's location uncertainty\n    lastEvent,                                  // The last scheduled polling task\n    lastInfoWindowMac,                          // The last Mac displayed in a marker tooltip\n    allMarkers = [],                            // The markers when we are in \"View All\" mode\n    lastMac = \"\",                               // The last requested MAC to follow\n    infoWindow = new google.maps.InfoWindow();  // The marker tooltip\n    /*\n    ,\n    markerImage = new google.maps.MarkerImage('blue_circle.png',\n      new google.maps.Size(15, 15),\n      new google.maps.Point(0, 0),\n      new google.maps.Point(4.5, 4.5)\n    );\n    */\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.seenString + floorStr +\n        \" with uncertainty \" + client.unc.toFixed(1) + \" meters (reloading every 20 seconds)\");\n      map.setCenter(pos);\n      clientMarker.setMap(map);\n      clientMarker.setPosition(pos);\n      clientUncertaintyCircle = new google.maps.Circle({\n        map: map,\n        center: pos,\n        radius: client.unc,\n        fillColor: 'RoyalBlue',\n        fillOpacity: 0.25,\n        strokeColor: 'RoyalBlue',\n        strokeWeight: 1\n      });\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\" perspective\n  function addMarker(client) {\n    var pos = new google.maps.LatLng(client.lat, client.lng);\n    console.log('addMarker pos '+pos);\n    var m = new google.maps.Marker({\n      position: pos,\n      map: map,\n      mac: client.mac,\n      //icon: markerImage\n    });\n    \n    if(client.lat){\n        latlngbounds.extend(pos);\n        map.fitBounds(latlngbounds);\n    }\n    google.maps.event.addListener(m, 'click', function () {\n        //build info\n        var htmlString = '<h2>Client:  '+client.name +'</h2>';\n        \n        for (var key in client) {\n            if (client.hasOwnProperty(key)) {\n                if(client[key] !== undefined){\n                    if(key == '_id' || key == 'name'){continue}\n                    htmlString += '<p>'+key+' : '+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.setContent(\"<div>\" + client.mac + \"</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 { $('#last-mac').text(\"Found \" + clients.length + \" clients (reloading every 20 seconds)\"); }\n    clientUncertaintyCircle.setMap(null);\n    clients.forEach(addMarker);\n  }\n\n  // Looks up all 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  // Begins a task timer to reload a single MAC every 20 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, 20000, lastMac);\n  }\n\n  // Begins a task timer to reload all MACs every 20 seconds\n  function startLookupAll() {\n    if (lastEvent !== null) { window.clearInterval(lastEvent); }\n    lastEvent = window.setInterval(lookupAll, 20000);\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.7705, -122.3870);\n    var mapOptions = {\n      zoom: 20,\n      center: center\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      //icon: markerImage\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":293,"y":860,"wires":[["f831817c.3396c"]]},{"id":"727f1b7.1164ce4","type":"http in","z":"57bbd386.23f07c","name":"","url":"/cmxapimap","method":"get","swaggerDoc":"","x":123,"y":860,"wires":[["1f95ea1d.28f096"]]},{"id":"1c56d65d.8f6e4a","type":"http response","z":"57bbd386.23f07c","name":"","x":733,"y":860,"wires":[]},{"id":"bc79c796.5d4748","type":"template","z":"57bbd386.23f07c","name":"HTML","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<html>\n  <head>\n    <title>CMX push API demo app with Node-RED</title>\n    <meta name=\"viewport\" content=\"initial-scale=1.0, user-scalable=no\">\n    <meta charset=\"utf-8\">\n    <script>TypekitConfig={kitId:\"hum1oye\",scriptTimeout:1.5e3},function(){var a=document.getElementsByTagName(\"html\")[0];a.className+=\" wf-loading\";var b=setTimeout(function(){a.className=a.className.replace(/(\\s|^)wf-loading(\\s|$)/g,\"\"),a.className+=\" wf-inactive\"},TypekitConfig.scriptTimeout),c=document.createElement(\"script\");c.src=\"//use.typekit.com/\"+TypekitConfig.kitId+\".js\",c.type=\"text/javascript\",c.async=\"true\",c.onload=c.onreadystatechange=function(){var a=this.readyState;if(!a||a==\"complete\"||a==\"loaded\"){clearTimeout(b);try{Typekit.load(TypekitConfig)}catch(c){}}};var d=document.getElementsByTagName(\"script\")[0];d.parentNode.insertBefore(c,d)}()</script>\n    <script src=\"https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false&key=AIzaSyCWFVfLzjGaepofBse9sHFF-S-mtqVjzLA\"></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>CMX API Demo with Node-RED</h1>\n      <div id=\"mac-address\">\n        <input id=\"mac-field\" type=\"text\" placeholder=\"Enter MAC address\" />&nbsp;\n        <button id=\"track\">Follow</button>&nbsp;\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":593,"y":860,"wires":[["1c56d65d.8f6e4a"]]},{"id":"2366f708.a8e978","type":"debug","z":"57bbd386.23f07c","name":"find({})","active":false,"console":"false","complete":"payload","x":733,"y":560,"wires":[]},{"id":"edbfeb05.5fd318","type":"comment","z":"57bbd386.23f07c","name":"Client Front-end API","info":"","x":113,"y":480,"wires":[]},{"id":"5a9c5368.00a1fc","type":"comment","z":"57bbd386.23f07c","name":"Receive CMX Data","info":"","x":113,"y":100,"wires":[]},{"id":"8dbc1e3f.60813","type":"comment","z":"57bbd386.23f07c","name":"Utilities","info":"","x":73,"y":920,"wires":[]},{"id":"5b280428.ebbaac","type":"function","z":"57bbd386.23f07c","name":"CMX Sample Data","func":"msg.payload = {\n    \"version\": \"2.0\",\n    \"secret\": \"supersecret\",\n    \"type\": \"DevicesSeen\",\n    \"data\": {\n        \"apMac\": \"00:18:0a:13:dd:b0\",\n        \"apFloors\": [],\n        \"apTags\": [ \"\", \"home\", \"\" ],\n        \"observations\": [ { \n            \"ipv4\": \"/192.168.0.15\",\n            \"location\": {\n                \"lat\": 51.5155157,\n                \"lng\": -0.06590350000000944,\n                \"unc\": 1.2185886512767726,\n                \"x\": [], \"y\": [] \n                \n            },\n            \"seenTime\": \"2016-07-29T13:17:10Z\",\n            \"ssid\": \".interwebs\",\n            \"os\": \"Debian-based Linux\",\n            \"clientMac\": \"AA:AA:AA:AA:AA:AA\",\n            \"seenEpoch\": 1469795230,\n            \"rssi\": 48,\n            \"ipv6\": null,\n            \"manufacturer\": \"Edimax Technology\" \n            }, \n            { \n            \"ipv4\": null,\n            \"location\": {\n                \"lat\": 51.5215157,\n                \"lng\": -0.069905350000000944,\n                \"unc\": 49, \"x\": [],\n                \"y\": []\n                },\n            \"seenTime\": \"2016-07-29T13:17:07Z\",\n            \"ssid\": null,\n            \"os\": null,\n            \"clientMac\": \"BB:BB:BB:BB:BB:BB\",\n            \"seenEpoch\": 1469598227,\n            \"rssi\": 9,\n            \"ipv6\": null,\n            \"manufacturer\": \"Samsung(THAILAND)\"\n            }\n        ]\n    }\n};\nreturn msg;","outputs":1,"noerr":0,"x":353,"y":140,"wires":[["1f738c4a.b4b534"]]},{"id":"e96b881.c855178","type":"inject","z":"57bbd386.23f07c","name":"Sample Client A B","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":133,"y":140,"wires":[["5b280428.ebbaac"]]},{"id":"60e0a268.34049c","type":"comment","z":"57bbd386.23f07c","name":"Client Front-end Site","info":"","x":113,"y":820,"wires":[]},{"id":"12547a8d.69f035","type":"comment","z":"57bbd386.23f07c","name":"README","info":"#Cisco Meraki CMX API - Demo Map\nThis Flow is an example of how to use the Cisco Meraki CMX\nJSON feed to store clients into a database and track them\non a Google map.\n\nThere are three major components\n- CMX Receier\n- Front-end API\n- Front-end Website (Google MAP)\n\n\n##CMX Receier -\nThe CMX Receiver utilizes the Node-RED CMX node to collect\nJSON data from a Cisco Meraki network. This feed is generally\nupdated within 2 minutes. There are two Sample Clients feeds\nthat can be used to test the flow.\n\nOnce the data has been received, the JSON is parsed and\ncommitted to a MongoDB. Please ensure your MongoDB is running\nfor this flow to work properly.\n\n\n##Front-end API - \nThere are two [GET] HTTP routes that provide access to the \ncollected client data. These will be used by the front-end \nwebsite to pull the client information for all or specific\nclients. Fun Fact: You can also use these routes with Postman\nor a standard browser to pull the data directly.\n\n##Front-end Website - \nThis will provide the webpage to view the Google map and \ntrack clients.\nThe website can be viewed at\n`http://yourserver:1880/cmxapimap`\n\n#Setup\n- Configure a Cisco Meraki network to post the CMX JSON to\nyour listening URL. Example: `http://yourserver:1880/cmx`\n\n- Install and configure MongoDB. Then update the MongoDB2 \nnodes within your flow to match the appropriate settings.\nExample: `mongodb://localhost:27017/test`\n\n- Insert Sample Client information by pressing the blue\nbuttons for each. Note, this will place the clients in London\nby default. Remove these clients if you do not want to \nconfuse your map centering\n\nMore information can be found on the Meraki Developers portal\nhttp://developers.meraki.com/tagged/Location\n\n\nThis flow was created by \nCory Guynn\nSystems Engineer\nCisco Meraki \n2016\n\nFor other fun IoT projects\nhttp://www.InternetOfLEGO.com\n\nMIT License. \n\n","x":393,"y":20,"wires":[]},{"id":"d8243cbc.591be","type":"inject","z":"57bbd386.23f07c","name":"DELETE ALL DATA","topic":"","payload":"{}","payloadType":"json","repeat":"","crontab":"","once":false,"x":133,"y":1005,"wires":[["e4d82358.ae36d"]]},{"id":"e4d82358.ae36d","type":"mongodb2 in","z":"57bbd386.23f07c","service":"_ext_","configNode":"1778d567.52c97b","name":"","collection":"cmxmapapi","operation":"remove","x":393,"y":1005,"wires":[["1a9d4613.003b3a"]]},{"id":"1a9d4613.003b3a","type":"debug","z":"57bbd386.23f07c","name":"delete mongo data","active":true,"console":"false","complete":"true","x":733,"y":1005,"wires":[]},{"id":"ec4504e8.921c48","type":"Meraki CMX","z":"57bbd386.23f07c","name":"","url":"/cmx","settings":"","x":71.5,"y":399,"wires":[["1f738c4a.b4b534","c8efb60f.be42c8"]]},{"id":"1778d567.52c97b","type":"mongodb2","z":"57bbd386.23f07c","uri":"mongodb://localhost:27017/test","name":"test","options":"","parallelism":"-1"}]
dexterlabora

Flow Info

created 1 year, 5 months ago
updated 1 year, 3 months ago

Node Types

Core
  • comment (x6)
  • debug (x10)
  • function (x7)
  • http in (x3)
  • http response (x3)
  • inject (x4)
  • template (x3)
Other

Tags

  • cisco
  • meraki
  • cmx
  • wifi
  • map
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option