diff --git a/hardware/wemo/README.md b/hardware/wemo/README.md index 3f5fe7f1..dd49ff71 100644 --- a/hardware/wemo/README.md +++ b/hardware/wemo/README.md @@ -24,7 +24,7 @@ The node accecpts the following inputs * Strings on/off * integers 1/0 * boolean true/false - * an Object like this (lights only, coming soon) + * an Object like this (lights only & color control is still work in the progress) ``` { state: 1, diff --git a/hardware/wemo/WeMoNG.html b/hardware/wemo/WeMoNG.html index f0e8e11d..b9cec3ab 100644 --- a/hardware/wemo/WeMoNG.html +++ b/hardware/wemo/WeMoNG.html @@ -29,8 +29,9 @@
  • Light Groups
  • Motion Detector
  • -

    Sockets will generate msg.payload with values of 0/1 for off or on, all other - types will return an object like this:

    +

    Sockets will generate msg.payload with values of 0/1/8 for off or on + (8 is on but at standby load for insight sockets), lightswill return an + object like this:

       {
         name: 'Bedroom light',
    @@ -44,6 +45,8 @@
       
     
     
    @@ -53,7 +56,8 @@
             defaults: {             // defines the editable properties of the node
                 name: {value:""},   //  along with default values.
                 topic: {value:"wemo", required: true},
    -            device: {value:"", type: "wemo-dev"}
    +            device: {value:"", type: "wemo-dev"},
    +            label: {value:""}
             },
             color: "LawnGreen",
             inputs:0,               // set the number of inputs - only 0 or 1
    @@ -61,10 +65,17 @@
             // set the icon (held in icons dir below where you save the node)
             icon: "belkin.png",     // saved in  icons/myicon.png
             label: function() {     // sets the default label contents
    -            return this.name||"wemo";
    +            if (this.name){
    +                return this.name;
    +            } else {
    +                return this.label||"wemo";
    +            }
             },
             labelStyle: function() { // sets the class to apply to the label
                 return this.name?"node_label_italic":"";
    +        },
    +        oneditsave: function(){
    +            this.label = $('#node-input-device option:selected').text();
             }
         });
     
    @@ -106,6 +117,7 @@
         temperature: 25000
       }
       
    +

    color control is still a work in progress, but the rest should work

    + + + + + + diff --git a/hardware/wemo/WeMoNG.js b/hardware/wemo/WeMoNG.js index 827b91ab..96ccbf7c 100644 --- a/hardware/wemo/WeMoNG.js +++ b/hardware/wemo/WeMoNG.js @@ -45,7 +45,7 @@ module.exports = function(RED) { console.log('problem with resubscription %s - %s', res.statusCode, res.statusMessage); console.log('opts - %s', util.inspect(reSubOptions)); console.log('dev - %s', util.inspect(dev)); - delete subscriptions[dev]; + delete subscriptions[subs[s]]; delete sub2dev[sub.sid]; subscribe({dev: subs[s]}); } @@ -58,7 +58,7 @@ module.exports = function(RED) { resub_request.on('error', function() { //console.log("failed to resubscribe to %s", dev.name ); //need to find a way to resubsribe - delete subscriptions[dev]; + delete subscriptions[subs[s]]; delete sub2dev[sub.sid]; subscribe({dev: subs[s]}); }); @@ -231,6 +231,7 @@ module.exports = function(RED) { } var on = 0; + var capability = '10006'; if (typeof msg.payload === 'string') { if (msg.payload == 'on' || msg.payload == '1' || msg.payload == 'true') { on = 1; @@ -238,44 +239,53 @@ module.exports = function(RED) { else if (msg.payload === 'toggle') { on = 2; } - } - else if (typeof msg.payload === 'number') { + } else if (typeof msg.payload === 'number') { if (msg.payload >= 0 && msg.payload < 3) { on = msg.payload; } - } - else if (typeof msg.payload === 'object') { + } else if (typeof msg.payload === 'object') { //object need to get complicated here - if (msg.payload.state && typeof msg.payload.state === 'number') { + if (msg.payload.hasOwnProperty('state') && typeof msg.payload.state === 'number') { if (dev.type === 'socket') { - if (msg.payload >= 0 && msg.payload < 2) { + if (msg.payload.state >= 0 && msg.payload.state < 2) { on = msg.payload.state; } } else if (dev.type === 'light' || dev.type === 'group') { - if (msg.payload >= 0 && msg.payload < 3) { - on = msg.payload.state; + // if (msg.payload.state >= 0 && msg.payload.state < 3) { + // on = msg.payload.state; + // } + var keys = Object.keys(msg.payload); + var caps = []; + var states = []; + for (var i=0; i 0) { + capability = caps.join(','); + on = states.join(','); } } } - } - else if (typeof msg.payload === 'boolean') { + } else if (typeof msg.payload === 'boolean') { if (msg.payload) { on = 1; } } - if (dev.type === 'socket') { + if (dev.type == 'socket') { //console.log("socket"); wemo.toggleSocket(dev, on); - } - else if (dev.type === 'light`') { - //console.log("light"); - wemo.setStatus(dev,'10006', on); - } - else { - console.log('group'); - wemo.setStatus(dev, '10006', on); + } else if (dev.type === 'light') { + wemo.setStatus(dev,capability, on); + } else { + //console.log('group'); + wemo.setStatus(dev, capability, on); } }); }; @@ -366,6 +376,76 @@ module.exports = function(RED) { }; RED.nodes.registerType('wemo in', wemoNGEvent); + var wemoNGLookup = function(n){ + RED.nodes.createNode(this,n); + var node = this; + node.device = n.device; + node.name = n.name; + node.dev = RED.nodes.getNode(node.device).device; + node.status({fill: 'red',shape: 'dot',text: 'searching'}); + + if (!wemo.get(node.dev)) { + wemo.on('discovered', function(d) { + if (node.dev === d) { + node.status({fill: 'green',shape: 'dot',text: 'found'}); + } + }); + } + else { + node.status({fill: 'green',shape: 'dot',text: 'found'}); + } + + node.on('input', function(msg) { + var dev = wemo.get(node.dev); + + if (!dev) { + //need to show that dev not currently found + console.log('no device found'); + return; + } + + //console.log(dev.type); + if (dev.type === 'light' || dev.type === 'group') { + //console.log("light"); + wemo.getLightStatus(dev) + .then(function(status){ + // if (status.available) { + var caps = status.capabilities.split(','); + var vals = status.state.split(','); + for (var i=0; i', '', ''].join('\n'); - var postbodyfooter = ['', '' ].join('\n'); - var getenddevs = {}; getenddevs.path = '/upnp/control/bridge1'; getenddevs.action = '"urn:Belkin:service:bridge:1#GetEndDevices"'; @@ -45,6 +44,58 @@ getcapabilities.body = [ 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 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 = {}; @@ -52,6 +103,10 @@ var WeMoNG = function () { this._interval; events.EventEmitter.call(this); + this.capabilityMap = capabilityMap; + + this.reverseCapabilityMap = reverseCapabilityMap; + } util.inherits(WeMoNG, events.EventEmitter); @@ -68,7 +123,7 @@ WeMoNG.prototype.start = function start() { request.get(location.href, function(err, res, xml) { if (!err) { xml2js.parseString(xml, function(err, json) { - if (!err) { + if (!err && json) { var device = { ip: location.hostname, port: location.port }; for (var key in json.root.device[0]) { device[key] = json.root.device[0][key][0]; @@ -248,20 +303,114 @@ WeMoNG.prototype.toggleSocket = function toggleSocket(socket, on) { post_request.end(); } -WeMoNG.prototype.setStatus = function setStatus(light, capability, value) { - 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>NO</IsGroupAction><DeviceID available="YES">%s</DeviceID><CapabilityID>%s</CapabilityID><CapabilityValue>%s</CapabilityValue></DeviceStatus>', - '', - '', - postbodyfooter - ].join('\n'); +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) { + var status = result["s:Envelope"]["s:Body"][0]["u:GetBinaryStateResponse"][0]["BinaryState"][0]; + status = parseInt(status); + def.resolve(status); + } + }); + }); + }); + + post_request.on('error', function (e) { + console.log(e); + console.log("%j", postoptions); + def.reject(); + }); + + post_request.write(getSocketState.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 { + console.log("err"); + } + }); + } + } else { + console.log("err"); + } + }); + }); + }); + + post_request.on('error', function (e) { + console.log(e); + console.log("%j", postoptions); + 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, @@ -269,7 +418,7 @@ WeMoNG.prototype.setStatus = function setStatus(light, capability, value) { method: 'POST', headers: { 'SOAPACTION': setdevstatus.action, - 'Content-Type': 'text/xml; charset="utf-8"', + 'Content-Type': 'text/xml; charset="utf-8"', 'Accept': '' } }; @@ -293,7 +442,7 @@ WeMoNG.prototype.setStatus = function setStatus(light, capability, value) { //console.log(util.format(setdevstatus.body, light.id, capability, value)); - post_request.write(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(); } @@ -312,6 +461,7 @@ WeMoNG.prototype.parseEvent = function parseEvent(evt) { 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); } @@ -364,4 +514,27 @@ WeMoNG.prototype.rgb2xy = function rgb2xy(red, green, blue) { ]; }; +//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; diff --git a/hardware/wemo/package.json b/hardware/wemo/package.json index f6632636..9ba06250 100644 --- a/hardware/wemo/package.json +++ b/hardware/wemo/package.json @@ -1,6 +1,6 @@ { "name": "node-red-node-wemo", - "version": "0.1.10", + "version": "0.1.11", "description": "Input and Output nodes for Belkin WeMo devices", "repository": "https://github.com/node-red/node-red-nodes/tree/master/hardware", "main": "WeMoNG.js", @@ -9,7 +9,8 @@ }, "keywords": [ "node-red", - "wemo" + "wemo", + "belkin" ], "author": { "email": "hardillb@gmail.com", @@ -18,7 +19,7 @@ }, "license": "APACHE-2.0", "dependencies": { - "node-ssdp": "~2.6.3", + "node-ssdp": "~2.9.1", "request": "~2.74.0", "xml2js": "~0.4.13", "util": "~0.10.3",