diff --git a/device/WakeOnLan/controller.js b/device/WakeOnLan/controller.js new file mode 100644 index 0000000..4d007d7 --- /dev/null +++ b/device/WakeOnLan/controller.js @@ -0,0 +1,159 @@ +'use strict'; + +const debug = require('debug'); +const BluePromise = require('bluebird'); +const fs = require('fs') +const readline = require('readline') +const os = require('os'); +const net = require('net'); +const dgram = require('dgram'); + +const PORT_NUMBER = 9; +const DEFAULT_IPADDRESS = '255.255.255.255'; + +let wolMap = "wol.map"; + +/** + * Sets the path to the 'wol.map' file + */ +module.exports.setMapPath = (mapPath) => { + wolMap = mapPath; +} + +/** + * Handles button presses for WOL + */ +module.exports.onButtonPressed = (deviceid, name) => { + console.log(`WOL button pressed for ${name}`); + + const addr = parseAddr(name); + const macAddr = getMacBuffer(addr.macAddress); + + if (macAddr) { + let magic = Buffer.alloc(102, 0xff); + for (let i = 6; i < magic.length; i+=6) { + macAddr.copy(magic, i, 0); + } + + debug("Sending WOL packet"); + sendPacket(addr.ipAddress, magic) + .catch((err) => { + console.error("Error occurred writing WOL packet. ", err || err.message); + }); + + } else { + debug('MAC address was invalid and is ignored - %s', addr.macAddress); + } +} + +/** + * Returns a promise to send the magic packet to the specified IP address + * @param {any} ipAddress the ipaddress to send to + * @param {any} magic the magic packet to send + */ +function sendPacket(ipAddress, magic) { + return new BluePromise((resolve, reject) => { + const socket = dgram.createSocket(net.isIPv6(ipAddress) ? 'udp6' : 'udp4') + .on('error', (err) => { + socket.close(); + reject(err); + }) + .once('listening', () => { + socket.setBroadcast(true); + }); + + socket.send(magic, 0, magic.length, PORT_NUMBER, ipAddress, (err, res) => { + socket.close(); + if (err) { + reject(new Error("Exception writing the magic packet: " + err)); + } else { + resolve(true); + } + }); + }); +} + +/** + * Discovers devices from the wolMap file. Please note that validation of the + * address field is not + */ +module.exports.discoverWolDevices = () => { + debug('WOL discovery using %s', wolMap); + + return new BluePromise((resolve, reject) => { + const devices = []; + + readline.createInterface({ input: fs.createReadStream(wolMap) }) + .on('line', (line) => { + const parsedLine = parseLine(line); + if (parsedLine) { + var parsedAddr = parseAddr(parsedLine.addr); + const macAddr = getMacBuffer(parsedAddr.macAddress); + if (macAddr) { + debug('WOL discovery call - found %s at %s', parsedLine.name, parsedLine.addr); + devices.push({ + id: parsedLine.addr, + name: parsedLine.name, + reachable: true + }); + } else { + debug("MAC address is invalid and will be ignored: %s", parsedAddr.macAddress); + } + } + }) + .on('error', (err) => { + reject(err); + }) + .on('close', () => { + resolve(devices); + }); + }); +} + +/** + * Parses a line from the map file and returns undefined (if the line can't be parsed + * or an object containing a name and addr field. + * @param {any} line the line to parse + */ +function parseLine(line) { + if (!line.startsWith("#")) { + const idx = line.indexOf("="); + if (idx >= 0) { + return { + name : line.substring(0, idx).trim(), + addr : line.substring(idx + 1).trim().replace(/ /g, '/') + } + } + } + return undefined; +} + +/** + * Parses the string address (found in the wol.map file) to a macAddress/ipAddress + * @param {any} addr the address field to parse + */ +function parseAddr(addr) { + const idx = addr.indexOf("/"); + return { + macAddress: (idx < 0 ? addr : addr.substring(0, idx)).trim(), + ipAddress: (idx < 0 ? DEFAULT_IPADDRESS : addr.substring(idx + 1)).trim(), + }; +} + +/** + * Parses the String (hex) MAC address to a byte array of integers + * @param {any} macAddress the max address + */ +function getMacBuffer(macAddress) { + const hexArray = macAddress.split(/[-:]+/); + if (hexArray.length === 6) { + const buffer = Buffer.alloc(6); + for (let i = 0; i < 6; i++) { + buffer[i] = parseInt(hexArray[i], 16); + } + return buffer; + } + + return undefined; +} + diff --git a/device/WakeOnLan/index.js b/device/WakeOnLan/index.js new file mode 100644 index 0000000..08ab3ab --- /dev/null +++ b/device/WakeOnLan/index.js @@ -0,0 +1,55 @@ +'use strict'; + +const neeoapi = require('neeo-sdk'); + +console.log('NEEO SDK Example "WakeOnLan"'); +console.log('------------------------------------------'); + +const controller = require('./controller'); + +/** + * Uncomment the following line to set the path to the wol map file + */ +//controller.setMapPath("device/WakeOnLan/wol.map"); + +const wolDevice = neeoapi.buildDevice('WakeOnLan') + .setManufacturer('NEEO') + .addAdditionalSearchToken('WOL') + .setType('ACCESSOIRE') + .addButton({ name: 'wol', label: 'Wake Up' }) + .addButtonHander(controller.onButtonPressed) + .enableDiscovery({ + headerText: 'WOL Instructions', + description: 'Please make sure you add devices to wol.map' + }, controller.discoverWolDevices); + +function startSdkExample(brain) { + console.log('- Start server'); + neeoapi.startServer({ + brain, + port: 6336, + name: 'wake-on-lan', + devices: [wolDevice] + }) + .then(() => { + console.log('# READY, use the mobile app to search for "WakeOnLan"'); + }) + .catch((error) => { + console.error('ERROR!', error.message); + process.exit(1); + }); +} + +const brainIp = process.env.BRAINIP; +//const brainIp = '192.168.1.29'; +if (brainIp) { + console.log('- use NEEO Brain IP from env variable', brainIp); + startSdkExample(brainIp); +} else { + console.log('- discover one NEEO Brain...'); + neeoapi.discoverOneBrain() + .then((brain) => { + console.log('- Brain discovered:', brain.name); + startSdkExample(brain); + }); +} diff --git a/device/WakeOnLan/wol.map b/device/WakeOnLan/wol.map new file mode 100644 index 0000000..fbc853b --- /dev/null +++ b/device/WakeOnLan/wol.map @@ -0,0 +1,20 @@ +##################################################################################### +# This file will provide mappings between devices and their MAC address/IP addresses +# +# The format of the file is +# Device Name=MACAddress[/IPAddress] +# +# 1) (Required) The device name will appear on the NEEO remote +# 2) (Required) The MAC address to send the WOL packet to +# 3) (Optional) Slash or space followed by the device IP address +# +# If IP Address is specified, the WOL packet is sent directly to it. If not +# specified, the WOL packet will be broadcast to the network +# +# Please note that if the MACAddress appears twice (or MAC Address/IP Address combo), +# the NEEO app will complain of a duplicate when trying to add a device. +##################################################################################### + +Example=01:02:03:04:05:06 +IP Example=01:02:03:04:05:06 192.168.1.111 +IP Example2=01:02:03:04:05:06/192.168.1.112 \ No newline at end of file diff --git a/package.json b/package.json index b3192a8..458d29b 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,9 @@ "private": false, "dependencies": { "bluebird": "^3.5.0", - "neeo-sdk": "*", - "lifx-http-api": "^1.0.3" + "lifx-http-api": "^1.0.3", + "debug": "*", + "neeo-sdk": "*" }, "engines": { "node": ">=6.0.0"