├── .github ├── example-columns2.jpg ├── example.png └── exampleColumnsNewLines.jpg ├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── MMM-NetworkScanner.css ├── MMM-NetworkScanner.js ├── README.md ├── exampleColumnsNewLines.jpg ├── gruntfile.js ├── node_helper.js ├── package.json ├── scripts └── arps2mm.sh └── test └── node_helper.test.js /.github/example-columns2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spitzlbergerj/MMM-NetworkScanner/3717eebe87bb6e2d4f2c33c132551711f07dd63e/.github/example-columns2.jpg -------------------------------------------------------------------------------- /.github/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spitzlbergerj/MMM-NetworkScanner/3717eebe87bb6e2d4f2c33c132551711f07dd63e/.github/example.png -------------------------------------------------------------------------------- /.github/exampleColumnsNewLines.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spitzlbergerj/MMM-NetworkScanner/3717eebe87bb6e2d4f2c33c132551711f07dd63e/.github/exampleColumnsNewLines.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esversion": 6 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | before_install: 5 | - cd ../../ && git clone https://github.com/MichMich/MagicMirror 6 | - mv ianperrin/MMM-NetworkScanner MagicMirror/modules 7 | - cd MagicMirror && npm install express 8 | - cd modules/MMM-NetworkScanner 9 | before_script: 10 | - npm install grunt-cli -g 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ian Perrin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MMM-NetworkScanner.css: -------------------------------------------------------------------------------- 1 | .MMM-NetworkScanner .device { 2 | text-align: left; 3 | } 4 | 5 | .MMM-NetworkScanner .device i { 6 | padding-left: 0; 7 | padding-right: 10px; 8 | } 9 | 10 | .MMM-NetworkScanner .date { 11 | padding-left: 30px; 12 | text-align: right; 13 | } -------------------------------------------------------------------------------- /MMM-NetworkScanner.js: -------------------------------------------------------------------------------- 1 | /* global Log, Module, moment, config */ 2 | /* Magic Mirror 3 | * Module: MMM-NetworkScanner 4 | * 5 | * By Ian Perrin http://ianperrin.com 6 | * MIT Licensed. 7 | */ 8 | 9 | //var Module, Log, moment, config, Log, moment, document; 10 | 11 | Module.register("MMM-NetworkScanner", { 12 | 13 | // Default module config. 14 | defaults: { 15 | devices: [], // an array of device objects e.g. { macAddress: "aa:bb:cc:11:22:33", name: "DEVICE-NAME", icon: "FONT-AWESOME-ICON"} 16 | network: "-l", // a Local Network IP mask to limit the mac address scan, i.e. `192.168.0.0/24`. Use `-l` for the entire localnet 17 | showUnknown: true, // shows devices found on the network even if not specified in the 'devices' option 18 | showOffline: true, // shows devices specified in the 'devices' option even when offline 19 | showLastSeen: false, // shows when the device was last seen e.g. "Device Name - last seen 5 minutes ago" 20 | keepAlive: 180, // how long (in seconds) a device should be considered 'alive' since it was last found on the network 21 | updateInterval: 20, // how often (in seconds) the module should scan the network 22 | sort: true, // sort the devices in the mirror 23 | 24 | residents: [], 25 | occupiedCMD: null, // {notification: 'TEST', payload: {action: 'occupiedCMD'}}, 26 | vacantCMD: null, // {notification: 'TEST', payload: {action: 'vacantCMD'}}, 27 | 28 | colored: false, // show devices colorcoded with color defined in devices [] // 29 | coloredSymbolOnly: false, // show symbol only in color // 30 | showLastSeenWhenOffline: false, // show last seen only when offline // 31 | 32 | debug: false, 33 | 34 | // sjj: show table as device rows or as device columns 35 | showDeviceColums: false, 36 | coloredState: false, 37 | }, 38 | 39 | // Subclass start method. 40 | start: function() { 41 | Log.info("Starting module: " + this.name); 42 | if (this.config.debug) Log.info(this.name + " config: ", this.config); 43 | 44 | // variable for if anyone is home 45 | this.occupied = true; 46 | 47 | moment.locale(config.language); 48 | 49 | this.validateDevices(); 50 | 51 | this.sendSocketNotification('CONFIG', this.config); 52 | 53 | this.scanNetwork(); 54 | }, 55 | 56 | // Subclass getStyles method. 57 | getStyles: function() { 58 | return ['MMM-NetworkScanner.css', 'font-awesome.css']; 59 | }, 60 | 61 | // Subclass getScripts method. 62 | getScripts: function() { 63 | return ["moment.js"]; 64 | }, 65 | 66 | // Subclass socketNotificationReceived method. 67 | socketNotificationReceived: function(notification, payload) { 68 | if (this.config.debug) Log.info(this.name + " received a notification: " + notification, payload); 69 | 70 | var self = this; 71 | var getKeyedObject = (objects = [], key) => objects.reduce( 72 | (acc, object) => (Object.assign(acc, { 73 | [object[key]]: object 74 | })), {} 75 | ); 76 | 77 | if (notification === 'IP_ADDRESS') { 78 | if (this.config.debug) Log.info(this.name + " IP_ADDRESS device: ", [payload.name, payload.online]); 79 | if (payload.hasOwnProperty("ipAddress")) { 80 | var device = this.config.devices.find(d => d.ipAddress === payload.ipAddress); 81 | this.updateDeviceStatus(device, payload.online); 82 | } 83 | } 84 | 85 | if (notification === 'MAC_ADDRESSES') { 86 | if (this.config.debug) Log.info(this.name + " MAC_ADDRESSES payload: ", payload); 87 | 88 | var nextState = payload.map(device => 89 | Object.assign(device, { 90 | lastSeen: moment() 91 | }) 92 | ); 93 | 94 | if (this.config.showOffline) { 95 | var networkDevicesByMac = getKeyedObject(this.networkDevices, 'macAddress'); 96 | var payloadDevicesByMac = getKeyedObject(nextState, 'macAddress'); 97 | 98 | nextState = this.config.devices.map(device => { 99 | if (device.macAddress) { 100 | var oldDeviceState = networkDevicesByMac[device.macAddress]; 101 | var payloadDeviceState = payloadDevicesByMac[device.macAddress]; 102 | var newDeviceState = payloadDeviceState || oldDeviceState || device; 103 | 104 | var sinceLastSeen = newDeviceState.lastSeen ? 105 | moment().diff(newDeviceState.lastSeen, 'seconds') : 106 | null; 107 | var isStale = (sinceLastSeen >= this.config.keepAlive); 108 | 109 | newDeviceState.online = (sinceLastSeen != null) && (!isStale); 110 | 111 | return newDeviceState; 112 | } else { 113 | return device; 114 | } 115 | }); 116 | } 117 | 118 | this.networkDevices = nextState; 119 | 120 | // Sort list by known device names, then unknown device mac addresses 121 | if (this.config.sort) { 122 | this.networkDevices.sort(function(a, b) { 123 | var stringA, stringB; 124 | stringA = (a.type != "Unknown" ? "_" + a.name + a.macAddress : a.name); 125 | stringB = (b.type != "Unknown" ? "_" + b.name + b.macAddress : b.name); 126 | 127 | return stringA.localeCompare(stringB); 128 | }); 129 | } 130 | 131 | // Send notification if user status has changed 132 | if (this.config.residents.length > 0) { 133 | var anyoneHome, command; 134 | // self = this; 135 | anyoneHome = 0; 136 | 137 | this.networkDevices.forEach(function(device) { 138 | if (self.config.residents.indexOf(device.name) >= 0) { 139 | anyoneHome = anyoneHome + device.online; 140 | } 141 | }); 142 | 143 | if (this.config.debug) Log.info("# people home: ", anyoneHome); 144 | if (this.config.debug) Log.info("Was occupied? ", this.occupied); 145 | 146 | if (anyoneHome > 0) { 147 | if (this.occupied === false) { 148 | if (this.config.debug) Log.info("Someone has come home"); 149 | if (this.config.occupiedCMD) { 150 | var occupiedCMD = self.config.occupiedCMD; 151 | this.sendNotification(occupiedCMD.notification, occupiedCMD.payload); 152 | } 153 | this.occupied = true; 154 | } 155 | } else { 156 | if (this.occupied === true) { 157 | if (this.config.debug) Log.info("Everyone has left home"); 158 | if (this.config.vacantCMD) { 159 | var vacantCMD = this.config.vacantCMD; 160 | this.sendNotification(vacantCMD.notification, vacantCMD.payload); 161 | } 162 | this.occupied = false; 163 | } 164 | } 165 | } 166 | 167 | this.updateDom(); 168 | return; 169 | 170 | } 171 | 172 | }, 173 | 174 | // Override dom generator. 175 | getDom: function() { 176 | var self = this; 177 | 178 | var wrapper = document.createElement("div"); 179 | wrapper.classList.add("small"); 180 | 181 | // Display a loading message 182 | if (!this.networkDevices) { 183 | wrapper.innerHTML = this.translate("LOADING"); 184 | return wrapper; 185 | } 186 | 187 | // Display device status 188 | var deviceTable = document.createElement("table"); 189 | deviceTable.classList.add("deviceTable", "small"); 190 | 191 | // sjj: Show devices in columns 192 | // generate header row and device state row 193 | 194 | var headerRow = document.createElement("tr"); 195 | headerRow.classList.add("headerRow", "dimmed"); 196 | var devStateRow = document.createElement("tr"); 197 | devStateRow.classList.add("devStateRow", "dimmed"); 198 | 199 | this.networkDevices.forEach(function(device) { 200 | 201 | if (device && (device.online || device.showOffline)) { 202 | 203 | // device row 204 | var deviceRow = document.createElement("tr"); 205 | var deviceOnline = (device.online ? "bright" : "dimmed"); 206 | deviceRow.classList.add("deviceRow", deviceOnline); 207 | 208 | // Icon 209 | 210 | var deviceCell = document.createElement("td"); 211 | deviceCell.classList.add("deviceCell"); 212 | var icon = document.createElement("i"); 213 | icon.classList.add("fa", "fa-fw", "fa-" + device.icon); 214 | 215 | if (self.config.colored) { 216 | icon.style.cssText = "color: " + device.color; 217 | } 218 | 219 | if (self.config.colored && !self.config.coloredSymbolOnly && device.lastSeen) { 220 | deviceCell.style.cssText = "color: " + device.color; 221 | } 222 | 223 | deviceCell.appendChild(icon); 224 | deviceCell.innerHTML += device.name; 225 | 226 | deviceRow.appendChild(deviceCell); 227 | 228 | // When last seen 229 | if ((self.config.showLastSeen && device.lastSeen && !self.config.showLastSeenWhenOffline) || 230 | (self.config.showLastSeen && !device.lastSeen && self.config.showLastSeenWhenOffline)) { 231 | var dateCell = document.createElement("td"); 232 | dateCell.classList.add("dateCell", "dimmed", "light"); 233 | if (typeof device.lastSeen !== 'undefined') { 234 | dateCell.innerHTML = device.lastSeen.fromNow(); 235 | } 236 | deviceRow.appendChild(dateCell); 237 | } 238 | 239 | // sjj: Append a new row if showDeviceColums and showInNewRow are both true 240 | 241 | if (self.config.showDeviceColums && device.showInNewRow) { 242 | // append the previously processed devices to the table 243 | deviceTable.appendChild(headerRow); 244 | deviceTable.appendChild(devStateRow); 245 | 246 | //generate new line contents 247 | headerRow = document.createElement("tr"); 248 | headerRow.classList.add("headerRow", "dimmed"); 249 | devStateRow = document.createElement("tr"); 250 | devStateRow.classList.add("devStateRow", "dimmed"); 251 | } 252 | 253 | // sjj: fill also header and devState row 254 | // header row 255 | var headerDevCell = document.createElement("td"); 256 | headerDevCell.classList.add("headerDevCell"); 257 | headerDevCell.innerHTML += device.name; 258 | 259 | headerRow.appendChild(headerDevCell); 260 | 261 | // device state row 262 | var devStateCell = document.createElement("td"); 263 | devStateCell.classList.add("devStateCell"); 264 | 265 | // color online / offline 266 | if (self.config.coloredState) { 267 | if (device.online) { 268 | icon.style.cssText = "color: " + device.colorStateOnline; 269 | } else { 270 | icon.style.cssText = "color: " + device.colorStateOffline; 271 | }; 272 | } 273 | 274 | devStateCell.appendChild(icon); 275 | 276 | devStateRow.appendChild(devStateCell); 277 | 278 | // sjj: show as Device rows or as Device columns 279 | if (!self.config.showDeviceColums) { 280 | deviceTable.appendChild(deviceRow); 281 | } 282 | 283 | } else { 284 | if (this.config.debug) Log.info(self.name + " Online, but ignoring: '" + device + "'"); 285 | } 286 | }); 287 | 288 | // sjj: show as Device rows or as Device columns 289 | if (self.config.showDeviceColums) { 290 | deviceTable.appendChild(headerRow); 291 | deviceTable.appendChild(devStateRow); 292 | } 293 | 294 | if (deviceTable.hasChildNodes()) { 295 | wrapper.appendChild(deviceTable); 296 | } else { 297 | // Display no devices online message 298 | wrapper.innerHTML = this.translate("NO DEVICES ONLINE"); 299 | } 300 | 301 | return wrapper; 302 | }, 303 | 304 | validateDevices: function() { 305 | this.config.devices.forEach(function(device) { 306 | // Add missing device attributes. 307 | if (!device.hasOwnProperty("icon")) { 308 | device.icon = "question"; 309 | } 310 | if (!device.hasOwnProperty("color")) { 311 | device.color = "#ffffff"; 312 | } 313 | if (!device.hasOwnProperty("showOffline")) { 314 | device.showOffline = true; 315 | } 316 | if (!device.hasOwnProperty("name")) { 317 | if (device.hasOwnProperty("macAddress")) { 318 | device.name = device.macAddress; 319 | } else if (device.hasOwnProperty("ipAddress")) { 320 | device.name = device.ipAddress; 321 | } else { 322 | device.name = "Unknown"; 323 | } 324 | } 325 | // sjj: coloredState 326 | if (!device.hasOwnProperty("colorStateOnline")) { 327 | device.colorStateOnline = "#ffffff"; 328 | } 329 | if (!device.hasOwnProperty("colorStateOffline")) { 330 | device.colorStateOffline = "#ffffff"; 331 | } 332 | // sjj show device in a new rox id mode is show in rows 333 | if (!device.hasOwnProperty("showInNewRow")) { 334 | device.showInNewRow = false; 335 | } 336 | }); 337 | }, 338 | 339 | scanNetwork: function() { 340 | if (this.config.debug) Log.info(this.name + " is initiating network scan"); 341 | var self = this; 342 | this.sendSocketNotification('SCAN_NETWORK'); 343 | setInterval(function() { 344 | self.sendSocketNotification('SCAN_NETWORK'); 345 | }, this.config.updateInterval * 1000); 346 | return; 347 | }, 348 | 349 | updateDeviceStatus: function(device, online) { 350 | if (device) { 351 | if (this.config.debug) Log.info(this.name + " is updating device status.", [device.name, online]); 352 | // Last Seen 353 | if (online) { 354 | device.lastSeen = moment(); 355 | } 356 | // Keep alive? 357 | var sinceLastSeen = device.lastSeen ? 358 | moment().diff(device.lastSeen, 'seconds') : 359 | null; 360 | var isStale = (sinceLastSeen >= this.config.keepAlive); 361 | device.online = (sinceLastSeen != null) && (!isStale); 362 | if (this.config.debug) Log.info(this.name + " " + device.name + " is " + (online ? "online" : "offline")); 363 | } 364 | return; 365 | } 366 | 367 | }); 368 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MMM-NetworkScanner 2 | A module for MagicMirror which determines the status of devices on the network based on their MAC address. It can also look up devices by IP addresses or hostnames. Static IP addresses work more consistently. 3 | 4 | ## Example 5 | 6 | devices as rows
7 | ![](.github/example.png) 8 | 9 | devices as columns
10 | ![](.github/example-columns2.jpg) 11 | 12 | devices as columns with newLines
13 | ![](.github/exampleColumnsNewLines.jpg) 14 | 15 | ## Installation 16 | 17 | In your terminal, install `arp-scan`: 18 | ````bash 19 | cd ~/ 20 | sudo apt-get install arp-scan 21 | ```` 22 | 23 | *Optionally*, update the vendor database used by `arp-scan`: 24 | ````bash 25 | cd /usr/share/arp-scan 26 | sudo get-iab -v -u http://standards-oui.ieee.org/iab/iab.txt 27 | sudo get-oui -v -u http://standards-oui.ieee.org/oui/oui.txt 28 | ```` 29 | 30 | Clone this repository into the MagicMirror Modules folder: 31 | ````bash 32 | cd ~/MagicMirror/modules 33 | git clone https://github.com/ianperrin/MMM-NetworkScanner.git 34 | ```` 35 | 36 | Install the dependencies (`sudo`, `ping`) in the MMM-NetworkScanner module folder: 37 | ```` 38 | cd ~/MagicMirror/modules/MMM-NetworkScanner 39 | npm install 40 | ```` 41 | 42 | Add the module to the modules array in the `config/config.js` file: 43 | ````javascript 44 | { 45 | module: 'MMM-NetworkScanner', 46 | position: 'top_left', 47 | config: { 48 | // Optional config options 49 | } 50 | }, 51 | ```` 52 | 53 | ## Config Options 54 | | **Option** | **Default** | **Description** | 55 | | --- | --- | --- | 56 | | `devices` | [] | an array of devices to be found on the network. See [Device object](#device-object) | 57 | | `network` | `-l` | `optional` a network mask to limit the scope of the network scan, i.e. `192.168.0.0/24`. If omitted, or set to `-l`, the entire network will be scanned. | 58 | | `showUnknown` | true | `optional` shows devices found on the network even if not specified in the `devices` option | 59 | | `showOffline` | true | `optional` shows devices specified in the `devices` option even when offline | 60 | | `showLastSeen` | false | `optional` shows when the device was last seen e.g. "Device Name - last seen 5 minutes ago" | 61 | | `keepAlive` | 180 | `optional` how long (in seconds) a device should be considered 'alive' since it was last found on the network | 62 | | `updateInterval` | 20 | `optional` how often (in seconds) the module should scan the network | 63 | | `sort` | `true` | `optional` sorts the devices in alphabetical order when shown in the mirror | 64 | | `residents` | [] | `optional` an array of names of the devices that should be monitored if they are online | 65 | | `occupiedCMD` | `{}` | `optional` the notification to be sent if one of the devices in the `residents` array is found online. See [Notification Example](#notification-example). | 66 | | `vacantCMD` | `{}` | `optional` the notification to be sent if **NONE** of the devices in the `residents` array is found online. See [Notification Example](#notification-example). | 67 | | `debug` | `false` | `optional` adds extended messages to the log. | 68 | | `colored` | `false` | `optional` determines whether devices are shown in the color defined in the devices section. | 69 | | `coloredSymbolOnly` | `false` | `optional` shows only the devices symbol. | 70 | | `showLastSeenWhenOffline:` | `false` | `optional` show last seen only when offline. | 71 | | `showDeviceColums:` | `false` | `optional` show devices as columns. | 72 | | `coloredState:` | `false` | `optional` determines whether devices are shown in a color defined in the devices section and controlled by the online / offline state. | 73 | 74 | 75 | 76 | #### Device Object 77 | The device object contains information about the devices to be found on the network. 78 | 79 | | **Key** | **Description** | **Example** | 80 | | --- | --- | --- | 81 | | `macAddress` | `optional` the MAC address of the device. | `aa:bb:cc:11:22:33` | 82 | | `ipAddress` | `optional` the IP address **or** host name of the device. | `192.168.0.1` or `github.com` | 83 | | `name` | `optional` the friendly name for the device. If omitted, the `macAddress` or `ipAddress` will be used. | `Phone` or `Router` | 84 | | `icon` | `optional` the symbol to show next to the device. See [Font Awesome](http://fontawesome.io/cheatsheet/) cheatsheet. If omitted, `question` will be used. | There are a huge number of icons to choose from. Here are some examples: `globe`, `server`, `desktop`, `laptop`, `mobile`, `wifi`. | 85 | | `color` | `optional` the color the device should be display with. | `#ff0000` for red | 86 | | `colorStateOnline` | `optional` the color the device should be display with when it is online. | `#ff0000` for red | 87 | | `colorStateOffline` | `optional` the color the device should be display with when it is offline. | `#ff0000` for red | 88 | | `showInNewRow` | `optional` add a line break if showDeviceColumns = true. | false for no line break | 89 | 90 | **Note** A device object should only contain either a `macAddress` *or* an `ipAddress` **NOT** both. 91 | 92 | **Note** The `coloredState` parameter overwrites the `colored` parameter if both parameters are set to true. With the parameter `coloredSymbolOnly` the status driven coloring can be limited to the icon. 93 | 94 | ##### Generating the device array 95 | The `devices` array can be generated by using `arps2mm.sh` from within the `scripts` folder. The output of the script will include all the devices found on the network, using the name of the vendor identified from the arp-scan result. Run the following script, edit the device names and icons then copy the array into the config file: 96 | 97 | ````bash 98 | cd ~/MagicMirror/modules/MMM-NetworkScanner/scripts 99 | chmod +x arps2mm.sh 100 | ./arps2mm.sh 101 | ```` 102 | 103 | **Note** Updating the vendor database is recommended before generating the device array. See the [installation instructions](#installation) for details. 104 | 105 | ### Example Config 106 | 107 | #### Simple example 108 | Scans the network (using the default `updateInterval`) and display the status of the four specified devices: 109 | ````javascript 110 | { 111 | module: "MMM-NetworkScanner", 112 | position: "top_left", 113 | header: "Who's home", 114 | config: { 115 | devices: [ 116 | { macAddress: "1a:1b:1c:1a:1b:1c", name: "Dad", icon: "male"}, 117 | { macAddress: "2a:2b:2c:2a:2b:2c", name: "Mum", icon: "female"}, 118 | { macAddress: "3a:3b:3c:3a:3b:3c", name: "Son", icon: "male"}, 119 | { macAddress: "4a:4b:4c:4a:4b:4c", name: "Daughter", icon: "female"} 120 | ], 121 | showUnknown: false, 122 | } 123 | } 124 | ```` 125 | #### example with columns 126 | Displays the specified devices as columns: 127 | ````javascript 128 | { 129 | module: "MMM-NetworkScanner", 130 | position: "top_left", 131 | header: "Geräte im Netzwerk", 132 | config: { 133 | devices: [ 134 | { 135 | ipAddress: "192.168.178.101", 136 | name: "UniFi", 137 | icon: "server", 138 | colorStateOnline: "green", 139 | colorStateOffline: "red", 140 | }, 141 | { 142 | ipAddress: "192.168.178.31", 143 | name: "QNAP1", 144 | icon: "database", 145 | colorStateOnline: "green", 146 | colorStateOffline: "red", 147 | }, 148 | { 149 | ipAddress: "192.168.178.32", 150 | name: "QNAP2", 151 | icon: "database", 152 | colorStateOnline: "green", 153 | colorStateOffline: "red", 154 | showInNewRow: true, 155 | }, 156 | { 157 | ipAddress: "192.168.178.33", 158 | name: "QNAP3", 159 | icon: "database", 160 | colorStateOnline: "green", 161 | colorStateOffline: "red", 162 | }, 163 | { 164 | ipAddress: "192.168.178.134", 165 | name: "APS", 166 | icon: "wifi", 167 | colorStateOnline: "green", 168 | colorStateOffline: "red", 169 | }, 170 | 171 | ], 172 | sort: false, 173 | showUnknown: false, 174 | showDeviceColums: true, 175 | coloredState: true, 176 | } 177 | } 178 | ```` 179 | #### Keep alive example 180 | Scan every 5 seconds and only display the specified devices whether they are online or offline. Devices will continue to be shown as online (i.e. kept alive) for 5 mins after they are last found: 181 | ````javascript 182 | { 183 | module: 'MMM-NetworkScanner', 184 | position: 'top_left', 185 | config: { 186 | devices: [ 187 | { ipAddress: "github.com", name: "Github", icon: "globe"}, 188 | { macAddress: "1a:1b:1c:1a:1b:1c", name: "Server", icon: "server"}, 189 | { macAddress: "2a:2b:2c:2a:2b:2c", name: "Desktop", icon: "desktop"}, 190 | { ipAddress: "10.1.1.10", name: "Laptop", icon: "laptop"}, 191 | { macAddress: "4a:4b:4c:4a:4b:4c", name: "Mobile", icon: "mobile"}, 192 | ], 193 | showUnknown: false, 194 | showOffline: true, 195 | keepAlive: 300, 196 | updateInterval: 5 197 | } 198 | }, 199 | ```` 200 | 201 | #### Notification example 202 | As with the previous example, this scans every 5 seconds and only display the specified devices whether they are online or offline. Devices will continue to be shown as online (i.e. kept alive) for 5 mins after they are last found on the network. 203 | 204 | In addition, the module will send a notification (`occupiedCMD`) to turn the monitor on when either `Mobile` or `Laptop` (the `residents`) are found on the network. Another notification (`vacantCMD`) will be sent when neither device is online: 205 | ````javascript 206 | { 207 | module: 'MMM-NetworkScanner', 208 | position: 'top_left', 209 | config: { 210 | devices: [ 211 | { ipAddress: "github.com", name: "Github", icon: "globe"}, 212 | { macAddress: "1a:1b:1c:1a:1b:1c", name: "Server", icon: "server"}, 213 | { macAddress: "2a:2b:2c:2a:2b:2c", name: "Desktop", icon: "desktop"}, 214 | { ipAddress: "10.1.1.10", name: "Laptop", icon: "laptop"}, 215 | { macAddress: "4a:4b:4c:4a:4b:4c", name: "Mobile", icon: "mobile"}, 216 | ], 217 | showUnknown: false, 218 | showOffline: true, 219 | keepAlive: 300, 220 | updateInterval: 5, 221 | residents: ["Mobile", "Laptop"], 222 | occupiedCMD: {notification: 'REMOTE_ACTION', payload: {action: 'MONITORON'}}, 223 | vacantCMD : {notification: 'REMOTE_ACTION', payload: {action: 'MONITOROFF'}}, 224 | 225 | } 226 | }, 227 | ```` 228 | **NOTE** The `REMOTE_ACTION` notifications (`MONITORON` and `MONITOROFF`) actions require the [MMM-Remote-Control](https://github.com/Jopyth/MMM-Remote-Control) module to be installed. 229 | 230 | ## Updating 231 | 232 | To update the module to the latest version, use your terminal to go to your MMM-NetworkScanner module folder and type the following command: 233 | 234 | ```` 235 | cd ~/MagicMirror/modules/MMM-NetworkScanner 236 | git pull 237 | npm install 238 | ```` 239 | 240 | If you haven't changed the modules, this should work without any problems. 241 | Type `git status` to see your changes, if there are any, you can reset them with `git reset --hard`. After that, git pull should be possible. 242 | -------------------------------------------------------------------------------- /exampleColumnsNewLines.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spitzlbergerj/MMM-NetworkScanner/3717eebe87bb6e2d4f2c33c132551711f07dd63e/exampleColumnsNewLines.jpg -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | nodeunit: { 4 | all: ['test/**/*.test.js'] 5 | }, 6 | jshint: { 7 | options: { 8 | jshintrc: ".jshintrc" 9 | }, 10 | all: [ 11 | "*.js", 12 | "!(node_modules)/*.js" 13 | ] 14 | }, 15 | }); 16 | 17 | grunt.loadNpmTasks('grunt-contrib-jshint'); 18 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 19 | 20 | grunt.registerTask('test', ['jshint', 'nodeunit']); 21 | }; 22 | -------------------------------------------------------------------------------- /node_helper.js: -------------------------------------------------------------------------------- 1 | /* global require, module */ 2 | /* jshint esversion: 6 */ 3 | /* Magic Mirror 4 | * Node Helper: MMM-NetworkScanner 5 | * 6 | * By Ian Perrin http://ianperrin.com 7 | * MIT Licensed. 8 | */ 9 | 10 | const NodeHelper = require("node_helper"); 11 | const ping = require("ping"); 12 | const sudo = require("sudo"); 13 | 14 | module.exports = NodeHelper.create({ 15 | 16 | start: function function_name () { 17 | this.log("Starting module: " + this.name); 18 | }, 19 | 20 | // Override socketNotificationReceived method. 21 | socketNotificationReceived: function(notification, payload) { 22 | this.log(this.name + " received " + notification); 23 | 24 | if (notification === "CONFIG") { 25 | this.config = payload; 26 | return true; 27 | } 28 | 29 | if (notification === "SCAN_NETWORK") { 30 | this.scanNetworkMAC(); 31 | this.scanNetworkIP(); 32 | return true; 33 | } 34 | 35 | }, 36 | 37 | scanNetworkMAC: function() { 38 | this.log(this.name + " is performing arp-scan"); 39 | 40 | var self = this; 41 | // Target hosts/network supplied in config or entire localnet 42 | var arpHosts = this.config.network || '-l'; 43 | var arp = sudo(['arp-scan', '-q', arpHosts]); 44 | var buffer = ''; 45 | var errstream = ''; 46 | var discoveredMacAddresses = []; 47 | var discoveredDevices = []; 48 | 49 | arp.stdout.on('data', function (data) { 50 | buffer += data; 51 | }); 52 | 53 | arp.stderr.on('data', function (data) { 54 | errstream += data; 55 | }); 56 | 57 | arp.on('error', function (err) { 58 | errstream += err; 59 | }); 60 | 61 | arp.on('close', function (code) { 62 | if (code !== 0) { 63 | self.log(self.name + " received an error running arp-scan: " + code + " - " + errstream); 64 | } else { 65 | // Parse the ARP-SCAN table response 66 | var rows = buffer.split('\n'); 67 | for (var i = 2; i < rows.length; i++) { 68 | var cells = rows[i].split('\t').filter(String); 69 | 70 | // Update device status 71 | if (cells && cells[1]) { 72 | var macAddress = cells[1].toUpperCase(); 73 | if (macAddress && discoveredMacAddresses.indexOf(macAddress) === -1) { 74 | discoveredMacAddresses.push(macAddress); 75 | var device = self.findDeviceByMacAddress(macAddress); 76 | if (device) { 77 | device.online = true; 78 | discoveredDevices.push(device); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | self.log(self.name + " arp scan addresses: ", discoveredMacAddresses); 85 | self.log(self.name + " arp scan devices: ", discoveredDevices); 86 | self.sendSocketNotification("MAC_ADDRESSES", discoveredDevices); 87 | return; 88 | }); 89 | 90 | }, 91 | 92 | scanNetworkIP: function() { 93 | if (!this.config.devices) { 94 | return; 95 | } 96 | 97 | this.log(this.name + " is performing ip address scan"); 98 | 99 | var discoveredDevices = []; 100 | var self = this; 101 | this.config.devices.forEach( function(device) { 102 | self.log(self.name + " is checking device: ", device.name); 103 | if ("ipAddress" in device) { 104 | self.log(self.name + " is pinging ", device.ipAddress); 105 | ping.sys.probe(device.ipAddress, function(isAlive) { 106 | device.online = isAlive; 107 | self.log(self.name + " ping result: ", [device.name, device.online] ); 108 | if (device.online) { 109 | discoveredDevices.push(device); 110 | } 111 | self.sendSocketNotification("IP_ADDRESS", device); 112 | }); 113 | } 114 | }); 115 | 116 | }, 117 | 118 | findDeviceByMacAddress: function (macAddress) { 119 | // Find first device with matching macAddress 120 | for (var i = 0; i < this.config.devices.length; i++) { 121 | var device = this.config.devices[i]; 122 | if (device.hasOwnProperty("macAddress")) { 123 | if (macAddress.toUpperCase() === device.macAddress.toUpperCase()){ 124 | this.log(this.name + " found device by MAC Address", device); 125 | return device; 126 | } 127 | } 128 | } 129 | // Return macAddress (if showing unknown) or null 130 | if (this.config.showUnknown) { 131 | return {macAddress: macAddress, name: macAddress, icon: "question", type: "Unknown"}; 132 | } else { 133 | return null; 134 | } 135 | }, 136 | 137 | log: function(message, object) { 138 | // Log if config is missing or in debug mode 139 | if (!this.config || this.config.debug) { 140 | if (object) { 141 | console.log(message, object); 142 | } else { 143 | console.log(message); 144 | } 145 | } 146 | }, 147 | 148 | }); 149 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MMM-NetworkScanner", 3 | "version": "0.0.5", 4 | "description": "A module for MagicMirror which determines the status of devices on the network based on their MAC address", 5 | "main": "MMM-NetworkScanner.js", 6 | "scripts": { 7 | "test": "grunt test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ianperrin/MMM-NetworkScanner.git" 12 | }, 13 | "keywords": [ 14 | "MagicMirror", 15 | "Network", 16 | "Scanner" 17 | ], 18 | "author": "Ian Perrin", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/ianperrin/MMM-NetworkScanner/issues" 22 | }, 23 | "homepage": "https://github.com/ianperrin/MMM-NetworkScanner#readme", 24 | "dependencies": { 25 | "sudo": "^1.0.3", 26 | "ping": "^0.1.10" 27 | }, 28 | "devDependencies": { 29 | "grunt": "latest", 30 | "grunt-contrib-jshint": "latest", 31 | "grunt-contrib-nodeunit": "latest" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /scripts/arps2mm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # arps2mm.sh -- A script to convert arp-scan output into MMM-NetworkScanner config file device items 3 | 4 | DARP=$(sudo arp-scan -N -l |tail -n +3 |head -n -3 |sort) 5 | DDEV="" 6 | 7 | #echo -e "DEVICE LIST:\n${DARP}\n\n" 8 | 9 | echo "devices: [" 10 | echo -e -n "$DARP\n" | while read line; do 11 | DMAC=$(echo $line | awk -F " " '{print $2}'); 12 | DNAME=$(echo $line | awk -F " " '{print $3}' | cut -d ' ' -f1 ); 13 | DIP=$(echo $line | awk -F " " '{print $1}'); 14 | DDEV=" { macAddress: \"$DMAC\", name: \"${DNAME}\", icon: \"mobile\" }, // ${DIP}\\n"; 15 | echo -e -n "$DDEV" 16 | done 17 | echo -e "],\n" 18 | 19 | exit 0 20 | -------------------------------------------------------------------------------- /test/node_helper.test.js: -------------------------------------------------------------------------------- 1 | /*global require,exports */ 2 | var Module = require("../node_helper.js"); 3 | var helper = new Module(); 4 | helper.setName("MMM-NetworkScanner"); 5 | 6 | // exampleOkTest 7 | exports.exampleOkTest = function (test) { 8 | test.expect(1); 9 | test.ok(true, "should always return true"); 10 | test.done(); 11 | }; 12 | 13 | // exampleEqualsTest 14 | exports.exampleEqualsTest = function (test) { 15 | test.expect(1); 16 | test.equals(1, 1, "should always equal 1"); 17 | test.done(); 18 | }; 19 | --------------------------------------------------------------------------------