Update WeMo node to WeMo-NG (#199)

* Replacing the original wemo node with the wemo-ng node

* Added install instructions

* Fix name of wemo out node

* Fix some jshint errors

More jshint fixes

fix jshint

last jshint fix

Fix last jshint error
This commit is contained in:
Ben Hardill 2016-04-09 17:59:36 +01:00 committed by Dave Conway-Jones
parent a8ec48553f
commit 37ba29484a
8 changed files with 1044 additions and 181 deletions

View File

@ -1,90 +0,0 @@
<!--
Copyright 2013 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script type="text/x-red" data-template-name="wemo out">
<div class="form-row">
<label for="node-input-ipaddr"><i class="fa fa-globe"></i> IP Address</label>
<input type="text" id="node-input-ipaddr" placeholder="192.168.1.100">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-tips">Expects a msg.payload with either 1/0, on/off, or true/false</div>
</script>
<script type="text/x-red" data-help-name="wemo out">
<p>Wemo output node. Expects a <code>msg.payload</code> with either 1/0, on/off or true/false.</p>
<p>It doesn't yet do any ip address discovery of the wemo devices.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('wemo out',{
category: 'advanced-output',
color:"GoldenRod",
defaults: {
ipaddr: {value:"",required:true},
name: {value:""}
},
inputs:1,
outputs:0,
icon: "light.png",
align: "right",
label: function() {
return this.name||"wemo";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
}
});
</script>
<script type="text/x-red" data-template-name="wemo in">
<div class="form-row">
<label for="node-input-ipaddr"><i class="fa fa-globe"></i> IP Address</label>
<input type="text" id="node-input-ipaddr" placeholder="192.168.1.100">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-tips">Creates a msg.payload with either 1, 0, nc or na.</div>
</script>
<script type="text/x-red" data-help-name="wemo in">
<p>Wemo input node. Creates a <code>msg.payload</code> with either 1, 0, nc (no change), or na (not available).</p>
<p>It doesn't yet do any ip address discovery of the wemo devices.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('wemo in',{
category: 'advanced-input',
color:"GoldenRod",
defaults: {
ipaddr: {value:"",required:true},
name: {value:""}
},
inputs:0,
outputs:1,
icon: "light.png",
label: function() {
return this.name||"wemo";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
}
});
</script>

View File

@ -1,60 +0,0 @@
/**
* Copyright 2013 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
"use strict";
var Wemo = require('wemo');
function WemoOut(n) {
RED.nodes.createNode(this,n);
this.ipaddr = n.ipaddr;
this.wemoSwitch = new Wemo(n.ipaddr);
var node = this;
this.on("input", function(msg) {
var state = 0;
if ( msg.payload == 1 || msg.payload === true || msg.payload == "on" ) { state = 1; }
node.wemoSwitch.setBinaryState(state, function(err, result) {
if (err) { node.warn(err); }
//else { node.log(result); }
});
});
}
RED.nodes.registerType("wemo out",WemoOut);
function WemoIn(n) {
RED.nodes.createNode(this,n);
this.ipaddr = n.ipaddr;
this.wemoSwitch = new Wemo(n.ipaddr);
this.wemoSwitch.state = 0;
var node = this;
var tick = setInterval(function() {
node.wemoSwitch.getBinaryState(function(err, result) {
if (err) { node.warn(err); }
if (parseInt(result) != node.wemoSwitch.state) {
node.wemoSwitch.state = parseInt(result);
node.send({payload:node.wemoSwitch.state,topic:"wemo/"+node.ipaddr});
}
});
}, 2000);
this.on("close", function() {
clearInterval(tick);
});
}
RED.nodes.registerType("wemo in",WemoIn);
}

View File

@ -1,7 +1,8 @@
node-red-node-wemo
==================
# node-red-contrib-nodes-wemo
A pair of <a href="http://nodered.org" target="_new">Node-RED</a> nodes to control a <a href="http://www.belkin.com/uk/Products/home-automation/c/wemo-home-automation/" target="_new">Belkin Wemo</a> set of devices.
A set of Node-RED nodes for working with Belkin WeMo devices.
These nodes use the uPnP discovery so may not discover your devices if you have a firewall enabled
Install
-------
@ -11,16 +12,71 @@ Run the following command in your Node-RED user directory - typically `~/.node-r
npm install node-red-node-wemo
Usage
-----
## Output node
It doesn't yet do any ip address discovery of the wemo devices.
The output node switches a socket, a light or group of lights on or off
### Wemo output node.
This should be backward compatible with the pervious version of this node but will benefit
from opening the config dialog and selecting the node you want.
Expects a `msg.payload` with either 1/0, on/off or true/false.
The node accecpts the following inputs
* Strings on/off
* integers 1/0
* boolean true/false
* an Object like this (lights only, coming soon)
```
{
state: 1,
dim: 255,
color: '255,255,255',
temperature: 25000
}
```
### Wemo input node.
## Input Node
Creates a `msg.payload` with either 1, 0, nc (no change), or na (not available).
The new input node is now based on uPnP notifications instead of polling. This means messages
will only be set when an actual change occurs in on the device. This means the node will not
send regular no-change messages.
The output varies depending on the type of device but examples for sockets look like this:
```
{
"raw": "<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">\n<e:property>\n<BinaryState>1</BinaryState>\n</e:property>\n</e:propertyset>\n\n\r",
"state": "1",
"sid": "uuid:e2c4586c-1dd1-11b2-8f61-b535035ae35d",
"type": "socket",
"name": "Bedroom Switch",
"id": "221448K1100085"
}
```
And a lightblub can look like this:
```
{
"raw": "<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">\n<e:property>\n<StatusChange>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;StateEvent&gt;&lt;DeviceID\navailable=&quot;YES&quot;&gt;94103EA2B27803ED&lt;/DeviceID&gt;&lt;CapabilityId&gt;10006&lt;/CapabilityId&gt;&lt;Value&gt;1&lt;/Value&gt;&lt;/StateEvent&gt;\n</StatusChange>\n</e:property>\n</e:propertyset>\n\n\r",
"id": "94103EA2B27803ED",
"capability": "10006",
"value": "1",
"sid": "uuid:e2e5739e-1dd1-11b2-943d-c238ce2bad17",
"type": "light",
"name": "Bedroom"
}
```
Insight
```
{
"raw": "<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">\n<e:property>\n<BinaryState>8|1454271649|301|834|56717|1209600|8|1010|638602|12104165</BinaryState>\n</e:property>\n</e:propertyset>\n\n\r",
"state": "8",
"power": 1.01,
"sid": "uuid:ea808ecc-1dd1-11b2-9579-8e5c117d479e",
"type": "socket",
"name": "WeMo Insight",
"id": "221450K1200F5C"
}
```

193
hardware/wemo/WeMoNG.html Normal file
View File

@ -0,0 +1,193 @@
<!--
Copyright 2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script type="text/x-red" data-template-name="wemo in">
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
<input type="text" id="node-input-topic" placeholder="Topic">
</div>
<br/>
<div class="form-row">
<label for="node-input-device"><i class="fa fa-tasks"></i> Device</label>
<input type="text" id="node-input-device" placeholder="Device">
</div>
<br/>
<!-- By convention, most nodes have a 'name' property. The following div -->
<!-- provides the necessary field. Should always be the last option -->
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="wemo in">
<p>A node that listens to event notifications from Belkin WeMo devices.</p>
<p>Supported devices include:</p>
<ul>
<li>Sockets</li>
<li>Insight Sockets</li>
<li>Light Bulbs</li>
<li>Light Groups</li>
<li>Motion Detector</li>
</ul>
<p>Sockets will generate msg.payload with values of 0/1 for off or on, all other
types will return an object like this:</p>
<pre>
{
name: 'Bedroom light',
type: 'light',
id: '94103EA2B27803ED',
capability: '1006',
value: 1
}
</pre>
<p>Current known capabilities</p>
<ul>
<li>10006 - on/off</li>
<li>10008 - brightness</li>
</ul>
</script>
<script type="text/javascript">
RED.nodes.registerType('wemo in',{
category: 'input', // the palette category
defaults: { // defines the editable properties of the node
name: {value:""}, // along with default values.
topic: {value:"wemo", required: true},
device: {value:"", type: "wemo-dev"}
},
color: "LawnGreen",
inputs:0, // set the number of inputs - only 0 or 1
outputs:1, // set the number of outputs - 0 to n
// 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";
},
labelStyle: function() { // sets the class to apply to the label
return this.name?"node_label_italic":"";
}
});
</script>
<script type="text/x-red" data-template-name="wemo out">
<div class="form-row">
<label for="node-input-device"><i class="fa fa-tasks"></i> Device</label>
<input type="text" id="node-input-device" placeholder="Device">
</div>
<br/>
<!-- By convention, most nodes have a 'name' property. The following div -->
<!-- provides the necessary field. Should always be the last option -->
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="wemo out">
<p>A node to control Belkin WeMo Devices</p>
<p>Supported devices include:</p>
<ul>
<li>Sockets</li>
<li>Insight Sockets</li>
<li>Light Bulbs</li>
</ul>
<p>Sets the device on or off based on the passed in msg.payload values of:</p>
<ul>
<li>on/off</li>
<li>1/0</li>
<li>true/false</li>
</ul>
<p>for light bulbs it also accepts an object with the following structure</p>
<pre>
{
state: 1,
dim: 255,
color: '255,255,255',
temperature: 25000
}
</pre>
</script>
<script type="text/javascript">
RED.nodes.registerType('wemo out',{
category: 'output', // the palette category
defaults: { // defines the editable properties of the node
name: {value:""}, // along with default values.
device: {type: "wemo-dev", required: true}
},
color: "LawnGreen",
inputs: 1, // set the number of inputs - only 0 or 1
// 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";
},
labelStyle: function() { // sets the class to apply to the label
return this.name?"node_label_italic":"";
}
});
</script>
<script type="text/x-red" data-template-name="wemo-dev">
<div class="form-row">
<label for="node-config-input-device"><i class="fa fa-tasks"></i> Device</label>
<select size="4" id="node-config-input-device">
</select>
</div>
<br/>
<div class="form-row">
<label for="node-config-input-type"><i class="fa fa-tasks"></i> Type</label>
<input type="text" editable="false" id="node-config-input-type" placeholder="Type"/>
<input type="hidden" editable="false" id="node-config-input-name"/>
</div>
</script>
<script type="text/javascript">
RED.nodes.registerType('wemo-dev',{
category: 'config', // the palette category
defaults: { // defines the editable properties of the node
device: {value:"", required: true}, // along with default values.
name: {value: ""}
},
label: function() { // sets the default label contents
return this.name;
},
oneditprepare: function() {
var devices;
$.getJSON('wemoNG/devices', function(data){
devices = data;
var devs = Object.keys(data);
if (devs.length !== 0) {
for (var d in devs) {
if (dev.hasOwnProperty(d)) {
$('<option/>',{
'value': devs[d],
'text': data[devs[d]].name
}).appendTo('#node-config-input-device');
console.log(data[devs[d]].name);
}
}
}
});
$('#node-config-input-device').change(function(){
var id = $( "#node-config-input-device option:selected" ).first().val();
if (devices) {
$('#node-config-input-type').val(devices[id].type);
$('#node-config-input-name').val(devices[id].name);
}
});
}
});
</script>

386
hardware/wemo/WeMoNG.js Normal file
View File

@ -0,0 +1,386 @@
/**
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var WeMoNG = require('./lib/wemo.js');
var wemo = new WeMoNG();
//this won't work as there is no way to stop it...
//but is that a problem?
var interval = setInterval(wemo.start.bind(wemo), 60000);
wemo.start();
module.exports = function(RED) {
"use strict";
var util = require('util');
var ip = require('ip');
var bodyParser = require('body-parser');
var http = require('http');
var os = require('os');
var settings = RED.settings;
var subscriptions = {};
var sub2dev = {};
var resubscribe = function() {
var subs = Object.keys(subscriptions);
for (var s in subs) {
if (subs.hasOwnProperty(s)) {
var sub = subscriptions[subs[s]];
var dev = wemo.get(subs[s]);
var reSubOptions = {
host: dev.ip,
port: dev.port,
path: dev.device.UDN.indexOf('Bridge-1_0') < 0 ? '/upnp/event/basicevent1': '/upnp/event/bridge1',
method: 'SUBSCRIBE',
headers: {
'SID': sub.sid,
'TIMEOUT': 'Second-300'
}
};
var resub_request = http.request(reSubOptions, function(res) {
//shoudl raise an error if needed
if (res.statusCode != 200) {
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 sub2dev[sub.sid];
subscribe({dev: subs[s]});
} else {
// console.log("resubscription good %s", res.statusCode);
// console.log("dev - %s", util.inspect(dev));
}
});
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 sub2dev[sub.sid];
subscribe({dev: subs[s]});
});
resub_request.end();
}
}
}
setInterval(resubscribe, 200000);
var subscribe = function(node) {
var dev = node.dev;
var device = wemo.get(dev);
if (device){
if (subscriptions[dev]) {
//exists
subscriptions[dev].count++;
} else {
//new
var ipAddr;
//device.ip
var interfaces = os.networkInterfaces();
var interfaceNames = Object.keys(interfaces);
for (var name in interfaceNames) {
if (interfaceNames.hasOwnProperty(name)) {
var addrs = interfaces[interfaceNames[name]];
for (var add in addrs) {
if (addrs[add].netmask){
//node 0.12 or better
if (!addrs[add].internal && addrs[add].family == 'IPv4') {
if (ip.isEqual(ip.mask(addrs[add].address,addrs[add].netmask),ip.mask(device.ip,addrs[add].netmask))) {
ipAddr = addrs[add].address;
break;
}
}
} else {
//node 0.10 not great but best we can do
if (!addrs[add].internal && addrs[add].family == 'IPv4') {
ipAddr = addrs[add].address;
break;
}
}
}
if (ipAddr) {
break;
}
}
}
var callback_url = 'http://' + ipAddr + ':' + settings.uiPort;
if(settings.httpAdminRoot) {
callback_url += settings.httpAdminRoot;
}
if (callback_url.lastIndexOf('/') != (callback_url.length -1)) {
callback_url += '/';
}
callback_url += 'wemoNG/notification';
console.log("Callback URL = %s",callback_url);
var subscribeOptions = {
host: device.ip,
port: device.port,
path: device.device.UDN.indexOf('Bridge-1_0') < 0 ? '/upnp/event/basicevent1': '/upnp/event/bridge1',
method: 'SUBSCRIBE',
headers: {
'CALLBACK': '<' + callback_url + '>',
'NT': 'upnp:event',
'TIMEOUT': 'Second-300'
}
};
//console.log(util.inspect(subscribeOptions));
var sub_request = http.request(subscribeOptions, function(res) {
//console.log("subscribe: %s - %s", device.name, res.statusCode);
if (res.statusCode == 200) {
subscriptions[dev] = {'count': 1, 'sid': res.headers.sid};
sub2dev[res.headers.sid] = dev;
} else {
console.log("failed to subsrcibe");
}
});
sub_request.end();
}
}
}
function unsubscribe(node) {
var dev = node.dev;
if (subscriptions[dev]) {
if (subscriptions[dev].count == 1) {
var sid = subscriptions[dev].sid;
var device = wemo.get(dev);
//need to unsubsribe properly here
var unSubOpts = {
host: device.ip,
port: device.port,
path: device.device.UDN.indexOf('Bridge-1_0') < 0 ? '/upnp/event/basicevent1': '/upnp/event/bridge1',
method: 'UNSUBSCRIBE',
headers: {
'SID': sid
}
};
//console.log(util.inspect(unSubOpts));
var unSubreq = http.request(unSubOpts, function(res){
//console.log("unsubscribe: %s \n %s", device.name, res.statusCode);
delete subscriptions[dev];
delete sub2dev[sid];
});
unSubreq.end();
} else {
subscriptions[dev].count--;
}
} else {
//shouldn't ever get here
}
}
var wemoNGConfig = function(n) {
RED.nodes.createNode(this,n);
this.device = n.device;
}
RED.nodes.registerType("wemo-dev", wemoNGConfig);
var wemoNGNode = 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"});
//console.log("Control - %j" ,this.dev);
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;
}
var on = 0;
if (typeof msg.payload === 'string') {
if (msg.payload == 'on' || msg.payload == '1' || msg.payload == 'true') {
on = 1;
} else if (msg.payload === 'toggle') {
on = 2;
}
} else if (typeof msg.payload === 'number') {
if (msg.payload >= 0 && msg.payload < 3) {
on = msg.payload;
}
} else if (typeof msg.payload === 'object') {
//object need to get complicated here
if (msg.payload.state && typeof msg.payload.state === 'number') {
if (dev.type === 'socket') {
if (msg.payload >= 0 && msg.payload < 2) {
on = msg.payload.state
}
} else if (dev.type === 'light' || dev.type === 'group') {
if (msg.payload >= 0 && msg.payload < 3) {
on = msg.payload.state;
}
}
}
} else if (typeof msg.payload === 'boolean') {
if (msg.payload) {
on = 1;
}
}
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);
}
});
}
RED.nodes.registerType("wemo out", wemoNGNode);
var wemoNGEvent = function(n) {
RED.nodes.createNode(this,n);
var node = this;
node.ipaddr = n.ipaddr;
node.device = n.device;
node.name = n.name;
node.topic = n.topic;
node.dev = RED.nodes.getNode(node.device).device;
node.status({fill:"red",shape:"dot",text:"searching"});
var onEvent = function(notification){
var d = sub2dev[notification.sid];
if (d == node.dev) {
var dd = wemo.get(node.dev);
notification.type = dd.type;
notification.name = dd.name;
if (!notification.id) {
notification.id = node.dev;
}
var msg = {
topic: node.topic ? node.topic : 'wemo',
payload: notification
};
switch (notification.type){
case 'light':
case 'group':
if (dd.id === notification.id) {
node.send(msg);
}
break;
case 'socket':
node.send(msg);
break;
default:
}
}
};
wemo.on('event', onEvent);
if (node.dev) {
//subscribe to events
if (wemo.get(node.dev)) {
node.status({fill:"green",shape:"dot",text:"found"});
subscribe(node);
} else {
wemo.on('discovered', function(d){
if (node.dev === d) {
node.status({fill:"green",shape:"dot",text:"found"});
subscribe(node);
}
});
}
} else if (node.ipaddr) {
//legacy
var devices = Object.keys(wemo.devices);
for (var d in devices) {
if (devices.hasOwnProperty(d)) {
var device = devices[d];
if (device.ip === node.ipaddr) {
node.dev = device.id;
node.status({fill:"green",shape:"circle",text:"reconfigure"});
subscribe(node);
break;
}
}
}
}
node.on('close', function(done){
//should un subscribe from events
wemo.removeListener('event', onEvent);
unsubscribe(node);
done();
});
}
RED.nodes.registerType("wemo in", wemoNGEvent)
RED.httpAdmin.get('/wemoNG/devices', function(req,res){
res.json(wemo.devices);
});
RED.httpAdmin.use('/wemoNG/notification',bodyParser.raw({type: 'text/xml'}));
RED.httpAdmin.notify('/wemoNG/notification', function(req, res){
var notification = {
'sid': req.headers.sid
};
//console.log("Incoming Event %s", req.body.toString());
wemo.parseEvent(req.body.toString()).then(function(evt){
evt.sid = notification.sid;
wemo.emit('event',evt);
});
res.send("");
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

367
hardware/wemo/lib/wemo.js Normal file
View File

@ -0,0 +1,367 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
"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 = [
'<?xml version="1.0" encoding="utf-8"?>',
'<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">',
'<s:Body>'].join('\n');
var postbodyfooter = ['</s:Body>',
'</s:Envelope>'
].join('\n');
var getenddevs = {};
getenddevs.path = '/upnp/control/bridge1';
getenddevs.action = '"urn:Belkin:service:bridge:1#GetEndDevices"';
getenddevs.body = [
postbodyheader,
'<u:GetEndDevices xmlns:u="urn:Belkin:service:bridge:1">',
'<DevUDN>%s</DevUDN>',
'<ReqListType>PAIRED_LIST</ReqListType>',
'</u:GetEndDevices>',
postbodyfooter
].join('\n');
var getcapabilities = {};
getcapabilities.path = '/upnp/control/bridge1';
getcapabilities.action = '"urn:Belkin:service:bridge:1#GetCapabilityProfileIDList"';
getcapabilities.body = [
postbodyheader,
'<u:GetCapabilityProfileIDList xmlns:u="urn:Belkin:service:bridge:1">',
'<DevUDN>%s</DevUDN>',
'</u:GetCapabilityProfileIDList>',
postbodyfooter
].join('\n');
var WeMoNG = function () {
this.devices = {};
this._client;
this._interval;
events.EventEmitter.call(this);
}
util.inherits(WeMoNG, events.EventEmitter);
WeMoNG.prototype.start = function start() {
//console.log("searching");
var _wemo = this;
_wemo._client = new Client();
_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) {
xml2js.parseString(xml, function(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];
}
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) {
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<devinfo.length; i++) {
var light = {
"ip": ip,
"port": port,
"udn": device.UDN,
"name": devinfo[i].FriendlyName[0],
"id": devinfo[i].DeviceID[0],
"capabilities": devinfo[i].CapabilityIDs[0],
"state": devinfo[i].CurrentState[0],
"type": "light",
"device": device
};
var key = device.serialNumber + "-" + light.id;
if (!_wemo.devices[key]){
_wemo.devices[key] = light;
_wemo.emit('discovered', key);
} else {
_wemo.devices[key] = light;
}
}
var groupinfo = result2.DeviceLists.DeviceList[0].GroupInfos;
if (groupinfo) {
for(var i=0; i<groupinfo.length; i++) {
var group = {
"ip": ip,
"port": port,
"udn": device.UDN,
"name": groupinfo[i].GroupInfo[0].GroupName[0],
"id": groupinfo[i].GroupInfo[0].GroupID[0],
"capabilities": groupinfo[i].GroupInfo[0].GroupCapabilityIDs[0],
"state": groupinfo[i].GroupInfo[0].GroupCapabilityValues[0],
"type": "light group",
"lights": [],
"device": device
}
for(var j=0; j<groupinfo[i].GroupInfo[0].DeviceInfos[0].DeviceInfo.length; j++) {
group.lights.push(groupinfo[i].GroupInfo[0].DeviceInfos[0].DeviceInfo[j].DeviceID[0]);
}
}
var key = device.serialNumber + "-" + group.id;
if (!_wemo.devices[key]) {
_wemo.devices[key] = group;
_wemo.emit('discovered', key);
} else {
_wemo.devices[key] = group;
}
}
}
});
}
});
});
});
post_request.write(util.format(getenddevs.body, udn));
post_request.end();
} else if (device.deviceType.indexOf('urn:Belkin:device') != -1) {
//socket
var socket = {
"ip": location.hostname,
"port": location.port,
"name": device.friendlyName,
"type": "socket",
"device": device
};
if (!_wemo.devices[device.serialNumber]) {
_wemo.devices[device.serialNumber] = socket;
_wemo.emit('discovered',device.serialNumber);
} else {
_wemo.devices[device.serialNumber] = socket;
}
} else {
//other stuff
//console.log( device.ip + ' -' + device.deviceType);
}
});
});
});
_wemo._client.search(urn);
setTimeout(function(){
//console.log("stopping");
_wemo._client._stop();
//console.log("%j", devices);
}, 10000);
}
WeMoNG.prototype.get = function get(deviceID) {
return this.devices[deviceID];
}
WeMoNG.prototype.toggleSocket = function toggleSocket(socket, on) {
var postoptions = {
host: socket.ip,
port: socket.port,
path: "/upnp/control/basicevent1",
method: 'POST',
headers: {
'SOAPACTION': '"urn:Belkin:service:basicevent:1#SetBinaryState"',
'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);
});
var body = [
postbodyheader,
'<u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">',
'<BinaryState>%s</BinaryState>',
'</u:SetBinaryState>',
postbodyfooter
].join('\n');
post_request.write(util.format(body, 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,
'<u:SetDeviceStatus xmlns:u="urn:Belkin:service:bridge:1">',
'<DeviceStatusList>',
'&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;DeviceStatus&gt;&lt;IsGroupAction&gt;NO&lt;/IsGroupAction&gt;&lt;DeviceID available=&quot;YES&quot;&gt;%s&lt;/DeviceID&gt;&lt;CapabilityID&gt;%s&lt;/CapabilityID&gt;&lt;CapabilityValue&gt;%s&lt;/CapabilityValue&gt;&lt;/DeviceStatus&gt;',
'</DeviceStatusList>',
'</u:SetDeviceStatus>',
postbodyfooter
].join('\n');
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);
});
//console.log(util.format(setdevstatus.body, light.id, capability, value));
post_request.write(util.format(setdevstatus.body, 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.value = res['StateEvent']['Value'][0];
def.resolve(msg);
}
});
} else if (prop.hasOwnProperty('BinaryState')) {
msg.state = prop['BinaryState'][0];
if (msg.state.length > 1) {
var parts = msg.state.split('|');
msg.state = parts[0];
msg.power = parts[7]/1000;
}
def.resolve(msg);
} else {
console.log("unhandled wemo event type \n%s", util.inspect(prop, {depth:null}));
}
} else {
//error
}
});
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
];
};
module.exports = WeMoNG;

View File

@ -1,24 +1,35 @@
{
"name" : "node-red-node-wemo",
"version" : "0.0.3",
"description" : "A Node-RED node to control a Belkin Wemo set of devices.",
"dependencies" : {
"wemo" : "0.2.*"
},
"repository" : {
"type":"git",
"url":"https://github.com/node-red/node-red-nodes/tree/master/hardware/wemo"
},
"license": "Apache-2.0",
"keywords": [ "node-red", "wemo" ],
"node-red" : {
"nodes" : {
"wemo": "60-wemo.js"
}
},
"author": {
"name": "Dave Conway-Jones",
"email": "ceejay@vnet.ibm.com",
"url": "http://nodered.org"
"name": "node-red-nodes-wemo",
"version": "0.1.4",
"description": "Input and Output nodes for Belkin WeMo devices",
"repository": "https://github.com/node-red/node-red-nodes/tree/master/hardware",
"main": "WeMoNG.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"node-red",
"wemo"
],
"author": {
"email": "hardillb@gmail.com",
"name": "Benjamin Hardill",
"url": "http://www.hardill.me.uk/wordpress/"
},
"license": "APACHE-2.0",
"dependencies": {
"node-ssdp": "~2.6.3",
"request": "~2.65.0",
"xml2js": "~0.4.13",
"util": "~0.10.3",
"url": "~0.11.0",
"ip": "~1.0.1",
"body-parser": "~1.14.1",
"q": "~1.4.1"
},
"node-red": {
"nodes": {
"wemo": "WeMoNG.js"
}
}
}