"use strict" var events = require('events'); var util = require('util'); var Client = require('node-ssdp').Client; var xml2js = require('xml2js'); var request = require('request'); var http = require('http'); var url = require('url'); var Q = require('q'); var urn = 'urn:Belkin:service:basicevent:1'; var postbodyheader = [ '', '', ''].join('\n'); var postbodyfooter = ['', '' ].join('\n'); var getenddevs = {}; getenddevs.path = '/upnp/control/bridge1'; getenddevs.action = '"urn:Belkin:service:bridge:1#GetEndDevices"'; getenddevs.body = [ postbodyheader, '', '%s', 'PAIRED_LIST', '', postbodyfooter ].join('\n'); var getcapabilities = {}; getcapabilities.path = '/upnp/control/bridge1'; getcapabilities.action = '"urn:Belkin:service:bridge:1#GetCapabilityProfileIDList"'; getcapabilities.body = [ postbodyheader, '', '%s', '', postbodyfooter ].join('\n'); var getDevStatus = { method: 'POST', path: '/upnp/control/bridge1', action: '"urn:Belkin:service:bridge:1#GetDeviceStatus"', body: [ postbodyheader, '', '%s', '', postbodyfooter ].join('\n') }; var getSocketState = { method: 'POST', path: '/upnp/control/basicevent1', action: '"urn:Belkin:service:basicevent:1#GetBinaryState"', body: [ postbodyheader, '', '', postbodyfooter ].join('\n') } var getInsightParameters = { method: 'POST', path: '/upnp/control/insight1', action: '"urn:Belkin:service:insight:1#GetInsightParams"', body: [ postbodyheader, '', '', postbodyfooter ].join('\n') } var setdevstatus = {}; setdevstatus.path = '/upnp/control/bridge1'; setdevstatus.action = '"urn:Belkin:service:bridge:1#SetDeviceStatus"'; setdevstatus.body = [ postbodyheader, '', '', '<?xml version="1.0" encoding="UTF-8"?><DeviceStatus><IsGroupAction>%s</IsGroupAction><DeviceID available="YES">%s</DeviceID><CapabilityID>%s</CapabilityID><CapabilityValue>%s</CapabilityValue></DeviceStatus>', '', '', postbodyfooter ].join('\n'); var capabilityMap = { '10006': 'state', '10008': 'dim', '10300': 'color', '30301': 'temperature' }; var reverseCapabilityMap = { 'state': '10006', 'dim': '10008', 'color': '10300', 'temperature': '30301' }; var WeMoNG = function () { this.devices = {}; this._client; this._interval; events.EventEmitter.call(this); this.capabilityMap = capabilityMap; this.reverseCapabilityMap = reverseCapabilityMap; } function addInsightParams(insightParms, msg) { var params = insightParms.split("|"); // Whether the device is ON or OFF (1 or 0) msg.state = params[0]; // The date and time when the device was last turned on or off (as a Unix timestamp) msg.onSince = parseInt(params[1]); // How long the device was last ON for (seconds) msg.onFor = parseInt(params[2]); // How long the device has been ON today (seconds) msg.onToday = parseInt(params[3]); // How long the device has been ON total (seconds) msg.onTotal = parseInt(params[4]); // Timespan over which onTotal is relevant (seconds). Typically 2 weeks except when first started up. //msg.timespan = parseInt(params[5]); // Average power consumption (Watts) msg.averagePower = parseInt(params[6]); // Current power consumption (Watts). Conversion required because the value is delivered in milliWatts. // It is called 'power' (instead of currentPower) for backwards compatibility ... msg.power = params[7]/1000; // Energy used today (Watt-hours, or Wh) msg.energyToday = parseInt(params[8]); // Energy used in total (Wh) msg.energyTotal = parseFloat(params[9]); // The 10-th parameter is not always available if (params[10]) { // Minimum energy usage to register the insight as switched on ( milliwats, default 8000mW, configurable via WeMo App) msg.standbyLimit = parseInt(params[10]); } } util.inherits(WeMoNG, events.EventEmitter); WeMoNG.prototype.start = function start() { //console.log("searching"); var _wemo = this; _wemo.setMaxListeners(0); _wemo._client = new Client({'explicitSocketBind': true}); _wemo._client.setMaxListeners(0); _wemo._client.on('response', function (headers, statusCode, rinfo) { var location = url.parse(headers.LOCATION); var port = location.port; request.get(location.href, function(err, res, xml) { if (!err) { xml2js.parseString(xml, function(err, json) { if (!err && json && json.root) { var device = { ip: location.hostname, port: location.port }; for (var key in json.root.device[0]) { device[key] = json.root.device[0][key][0]; } if (device.deviceType == "urn:Belkin:device:bridge:1") { //console.log( device.ip + ' -' + device.deviceType); var ip = device.ip; var port = device.port; var udn = device.UDN; var postoptions = { host: ip, port: port, path: getenddevs.path, method: 'POST', headers: { 'SOAPACTION': getenddevs.action, 'Content-Type': 'text/xml; charset="utf-8"', 'Accept': '' } }; var post_request = http.request(postoptions, function(res) { var data = ""; res.setEncoding('utf8'); res.on('data', function(chunk) { data += chunk; }); res.on('end',function() { xml2js.parseString(data, function(err, result) { if(!err) { if (result["s:Envelope"]) { var list = result["s:Envelope"]["s:Body"][0]["u:GetEndDevicesResponse"][0].DeviceLists[0]; xml2js.parseString(list, function(err, result2) { if (!err) { var devinfo = result2.DeviceLists.DeviceList[0].DeviceInfos[0].DeviceInfo; for (var i=0; i', '%s', '', postbodyfooter ].join('\n'); post_request.write(util.format(body, on)); post_request.end(); } WeMoNG.prototype.getSocketStatus = function getSocketStatus(socket) { var postoptions = { host: socket.ip, port: socket.port, path: getSocketState.path, method: getSocketState.method, headers: { 'SOAPACTION': getSocketState.action, 'Content-Type': 'text/xml; charset="utf-8"', 'Accept': '' } } var def = Q.defer(); var post_request = http.request(postoptions, function(res){ var data = ""; res.setEncoding('utf8'); res.on('data', function(chunk){ data += chunk; }); res.on('end', function(){ xml2js.parseString(data, function(err, result){ if (!err && result["s:Envelope"]) { var status = result["s:Envelope"]["s:Body"][0]["u:GetBinaryStateResponse"][0]["BinaryState"][0]; status = parseInt(status); def.resolve(status); } else { def.reject(err); } }); }); }); post_request.on('error', function (e) { // console.log(e); // console.log("%j", postoptions); def.reject(e); }); post_request.on('timeout', function(){ post_request.abort(); def.reject("timeout"); }); post_request.write(getSocketState.body); post_request.end(); return def.promise; }; WeMoNG.prototype.getInsightParams = function getInsightParams(socket) { var postoptions = { host: socket.ip, port: socket.port, path: getInsightParameters.path, method: getInsightParameters.method, headers: { 'SOAPACTION': getInsightParameters.action, 'Content-Type': 'text/xml; charset="utf-8"', 'Accept': '' } } var def = Q.defer(); var post_request = http.request(postoptions, function(res){ var data = ""; res.setEncoding('utf8'); res.on('data', function(chunk){ data += chunk; }); res.on('end', function(){ xml2js.parseString(data, function(err, result){ if (!err) { var params = result["s:Envelope"]["s:Body"][0]["u:GetInsightParamsResponse"][0].InsightParams[0]; var msg = {}; addInsightParams(params, msg); def.resolve(msg); } }); }); }); post_request.on('error', function (e) { // console.log(e); // console.log("%j", postoptions); def.reject(e); }); post_request.on('timeout', function(){ post_request.abort(); def.reject("timeout"); }); post_request.write(getInsightParameters.body); post_request.end(); return def.promise; }; WeMoNG.prototype.getLightStatus = function getLightStatus(light) { var postoptions = { host: light.ip, port: light.port, path: getDevStatus.path, method: getDevStatus.method, headers: { 'SOAPACTION': getDevStatus.action, 'Content-Type': 'text/xml; charset="utf-8"', 'Accept': '' } }; var def = Q.defer(); var post_request = http.request(postoptions, function(res){ var data = ""; res.setEncoding('utf8'); res.on('data', function(chunk) { data += chunk; }); res.on('end', function() { xml2js.parseString(data, function(err, result){ if(!err) { if (result["s:Envelope"]) { var status = result["s:Envelope"]["s:Body"][0]["u:GetDeviceStatusResponse"][0].DeviceStatusList[0]; xml2js.parseString(status, function(err,result2){ if (!err) { var available = result2['DeviceStatusList']['DeviceStatus'][0]['DeviceID'][0]['$'].available === 'YES'; var state = result2['DeviceStatusList']['DeviceStatus'][0]['CapabilityValue'][0]; var capabilities = result2['DeviceStatusList']['DeviceStatus'][0]['CapabilityID'][0]; var obj = { available: available, state: state, capabilities: capabilities }; def.resolve(obj); } else { def.reject(err); // console.log("err"); } }); } } else { // console.log("err"); def.reject(err); } }); }); }); post_request.on('error', function (e) { // console.log(e); // console.log("%j", postoptions); def.reject(e); }); post_request.on('timeout', function () { // console.log("Timeout"); post_request.abort("timeout"); def.reject(); }); post_request.write(util.format(getDevStatus.body, light.id)); post_request.end(); return def.promise; } WeMoNG.prototype.setStatus = function setStatus(light, capability, value) { var postoptions = { host: light.ip, port: light.port, path: setdevstatus.path, method: 'POST', headers: { 'SOAPACTION': setdevstatus.action, 'Content-Type': 'text/xml; charset="utf-8"', 'Accept': '' } }; var post_request = http.request(postoptions, function(res) { var data = ""; res.setEncoding('utf8'); res.on('data', function(chunk) { data += chunk; }); res.on('end', function() { //console.log(data); }); }); post_request.on('error', function (e) { // console.log(e); // console.log("%j", postoptions); }); post_request.on('timeout', function () { // console.log("Timeout"); post_request.abort(); }); //console.log(util.format(setdevstatus.body, light.id, capability, value)); post_request.write(util.format(setdevstatus.body, light.type === 'light'?'NO':'YES',light.id, capability, value)); post_request.end(); } //need to promisify this so it returns WeMoNG.prototype.parseEvent = function parseEvent(evt) { var msg = {}; msg.raw = evt; var def = Q.defer(); xml2js.parseString(evt, function(err, res){ if (!err) { var prop = res['e:propertyset']['e:property'][0]; if (prop.hasOwnProperty('StatusChange')) { xml2js.parseString(prop['StatusChange'][0], function(err, res){ if (!err && res != null) { msg.id = res['StateEvent']['DeviceID'][0]['_']; msg.capability = res['StateEvent']['CapabilityId'][0]; msg.capabilityName = capabilityMap[msg.capability]; msg.value = res['StateEvent']['Value'][0]; def.resolve(msg); } else { def.reject(err); } }); } else if (prop.hasOwnProperty('BinaryState')) { msg.state = prop['BinaryState'][0]; if (msg.state.length > 1) { // Add all the insight params to the msg addInsightParams(msg.state, msg); } def.resolve(msg); } else { console.log("unhandled wemo event type \n%s", util.inspect(prop, {depth:null})); } } else { //error def.reject(err); } }); return def.promise; } // Based on https://github.com/theycallmeswift/hue.js/blob/master/lib/helpers.js // TODO: Needs to be tweaked for more accurate color representation WeMoNG.prototype.rgb2xy = function rgb2xy(red, green, blue) { var xyz; var rgb = [red / 255, green / 255, blue / 255]; for (var i = 0; i < 3; i++) { if (rgb[i] > 0.04045) { rgb[i] = Math.pow(((rgb[i] + 0.055) / 1.055), 2.4); } else { rgb[i] /= 12.92; } rgb[i] = rgb[i] * 100; } xyz = [ rgb[0] * 0.4124 + rgb[1] * 0.3576 + rgb[2] * 0.1805, rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722, rgb[0] * 0.0193 + rgb[1] * 0.1192 + rgb[2] * 0.9505 ]; return [ xyz[0] / (xyz[0] + xyz[1] + xyz[2]) * 65535, xyz[1] / (xyz[0] + xyz[1] + xyz[2]) * 65535 ]; }; //http://stackoverflow.com/questions/22894498/philips-hue-convert-xy-from-api-to-hex-or-rgb WeMoNG.prototype.xy2rgb = function xy2rgb(x,y,bri) { z = 1.0 - x - y; Y = bri / 255.0; // Brightness of lamp X = (Y / y) * x; Z = (Y / y) * z; r = X * 1.612 - Y * 0.203 - Z * 0.302; g = -X * 0.509 + Y * 1.412 + Z * 0.066; b = X * 0.026 - Y * 0.072 + Z * 0.962; r = r <= 0.0031308 ? 12.92 * r : (1.0 + 0.055) * Math.pow(r, (1.0 / 2.4)) - 0.055; g = g <= 0.0031308 ? 12.92 * g : (1.0 + 0.055) * Math.pow(g, (1.0 / 2.4)) - 0.055; b = b <= 0.0031308 ? 12.92 * b : (1.0 + 0.055) * Math.pow(b, (1.0 / 2.4)) - 0.055; maxValue = Math.max(r,g,b); r /= maxValue; g /= maxValue; b /= maxValue; r = r * 255; if (r < 0) { r = 255 }; g = g * 255; if (g < 0) { g = 255 }; b = b * 255; if (b < 0) { b = 255 }; return [r,g,b]; }; module.exports = WeMoNG;