wemo-client = function (opts) {
opts = opts || {};
this._port = opts.port || 0;
this._clients = {};
this._listen();
this._ssdpClient = new SSDPClient(opts.discover_opts || {});
}n/a
client = function (config) {
EventEmitter.call(this);
this.host = config.host;
this.port = config.port;
this.deviceType = config.deviceType;
this.UDN = config.UDN;
this.subscriptions = {};
this.callbackURL = config.callbackURL;
this.device = config;
this.error = undefined;
// Create map of services
config.serviceList.service.forEach(function(service) {
this[service.serviceType] = {
serviceId: service.serviceId,
controlURL: service.controlURL,
eventSubURL: service.eventSubURL
};
}, this.services = {});
// Transparently subscribe to serviceType events
// TODO: Unsubscribe from ServiceType when all listeners have been removed.
this.on('newListener', this._onListenerAdded);
}...
var Wemo = require('wemo-client');
var wemo = new Wemo();
wemo.discover(function(deviceInfo) {
console.log('Wemo Device Found: %j', deviceInfo);
// Get the client for the found device
var client = wemo.client(deviceInfo);
// Handle BinaryState events
client.on('binaryState', function(value) {
console.log('Binary State changed to: %s', value);
});
// Turn the switch on
...client = function (config) {
EventEmitter.call(this);
this.host = config.host;
this.port = config.port;
this.deviceType = config.deviceType;
this.UDN = config.UDN;
this.subscriptions = {};
this.callbackURL = config.callbackURL;
this.device = config;
this.error = undefined;
// Create map of services
config.serviceList.service.forEach(function(service) {
this[service.serviceType] = {
serviceId: service.serviceId,
controlURL: service.controlURL,
eventSubURL: service.eventSubURL
};
}, this.services = {});
// Transparently subscribe to serviceType events
// TODO: Unsubscribe from ServiceType when all listeners have been removed.
this.on('newListener', this._onListenerAdded);
}...
var Wemo = require('wemo-client');
var wemo = new Wemo();
wemo.discover(function(deviceInfo) {
console.log('Wemo Device Found: %j', deviceInfo);
// Get the client for the found device
var client = wemo.client(deviceInfo);
// Handle BinaryState events
client.on('binaryState', function(value) {
console.log('Binary State changed to: %s', value);
});
// Turn the switch on
...request = function (options, data, cb) {
if (!cb && typeof data === 'function') {
cb = data;
data = null;
}
var req = http.request(options, function(res) {
var body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
body += chunk;
});
res.on('end', function() {
if (res.statusCode === 200) {
xml2js.parseString(body, { explicitArray: false }, cb);
} else {
cb(new Error('HTTP ' + res.statusCode + ': ' + body));
}
});
res.on('error', function(err) {
debug('Error on http.request.res:', err);
cb(err);
});
});
req.on('error', function(err) {
debug('Error on http.request.req:', err);
cb(err);
});
if (data) {
req.write(data);
}
req.end();
}...
LightSwitch: 'urn:Belkin:device:lightswitch:1'
};
Wemo.prototype.load = function(setupUrl, cb) {
var self = this;
var location = url.parse(setupUrl);
WemoClient.request({
host: location.hostname,
port: location.port,
path: location.path,
method: 'GET'
}, function(err, json) {
if (!err && json) {
var device = json.root.device;
...rgb2xy = function (r, g, b) {
// Based on: https://github.com/aleroddepaz/pyhue/blob/master/src/pyhue.py
var X = (0.545053 * r) + (0.357580 * g) + (0.180423 * b);
var Y = (0.212671 * r) + (0.715160 * g) + (0.072169 * b);
var Z = (0.019334 * r) + (0.119193 * g) + (0.950227 * b);
var x = X / (X + Y + Z);
var y = Y / (X + Y + Z);
return [
Math.round(x * 65535),
Math.round(y * 65535)
];
}...
this.soapAction('urn:Belkin:service:bridge:1', 'GetDeviceStatus', {
DeviceIDs: deviceId
}, parseResponse);
};
WemoClient.prototype.setLightColor = function(deviceId, red, green, blue, cb) {
var color = WemoClient.rgb2xy(red, green, blue);
this.setDeviceStatus(deviceId, 10300, color.join(':') + ':0', cb);
};
WemoClient.prototype.setBinaryState = function(value, cb) {
this.soapAction('urn:Belkin:service:basicevent:1', 'SetBinaryState', {
BinaryState: value
}, cb);
...function EventEmitter() {
EventEmitter.init.call(this);
}n/a
_onListenerAdded = function (eventName) {
var serviceType = WemoClient.EventServices[eventName];
if (serviceType && this.services[serviceType]) {
this._subscribe(serviceType);
}
}n/a
_parseInsightParams = function (paramsStr) {
var params = paramsStr.split('|');
return {
binaryState: params[0],
instantPower: params[7],
insightParams: {
ONSince: params[1],
OnFor: params[2],
TodayONTime: params[3],
TodayConsumed: params[8] // power consumer today (mW per minute)
}
};
}...
});
};
WemoClient.prototype.getInsightParams = function(cb) {
this.soapAction('urn:Belkin:service:insight:1', 'GetInsightParams', null, function(err, data) {
if (err) return cb(err);
var params = this._parseInsightParams(data.InsightParams);
cb(null, params.binaryState, params.instantPower, params.insightParams);
}.bind(this));
};
WemoClient.prototype._parseInsightParams = function(paramsStr) {
var params = paramsStr.split('|');
..._subscribe = function (serviceType) {
if (!this.services[serviceType]) {
throw new Error('Service ' + serviceType + ' not supported by ' + this.UDN);
}
if (!this.callbackURL) {
throw new Error('Can not subscribe without callbackURL');
}
if (this.subscriptions[serviceType] && this.subscriptions[serviceType] === 'PENDING') {
debug('subscription still pending');
return;
}
var options = {
host: this.host,
port: this.port,
path: this.services[serviceType].eventSubURL,
method: 'SUBSCRIBE',
headers: {
TIMEOUT: 'Second-300'
}
};
if (!this.subscriptions[serviceType]) {
// Initial subscription
this.subscriptions[serviceType] = 'PENDING';
debug('Initial subscription - Device: %s, Service: %s', this.UDN, serviceType);
options.headers.CALLBACK = '<' + this.callbackURL + '/' + this.UDN + '>';
options.headers.NT = 'upnp:event';
} else {
// Subscription renewal
debug('Renewing subscription - Device: %s, Service: %s', this.UDN, serviceType);
options.headers.SID = this.subscriptions[serviceType];
}
var req = http.request(options, function(res) {
if (res.statusCode === 200) {
// Renew after 150 seconds
this.subscriptions[serviceType] = res.headers.sid;
setTimeout(this._subscribe.bind(this), 150 * 1000, serviceType);
} else {
// Try to recover from failed subscription after 2 seconds
debug('Subscription request failed with HTTP %s', res.statusCode);
this.subscriptions[serviceType] = null;
setTimeout(this._subscribe.bind(this), 2000, serviceType);
}
}.bind(this));
req.on('error', function(err) {
debug('Subscription error: %s - Device: %s, Service: %s', err.code, this.UDN, serviceType);
this.subscriptions[serviceType] = null;
this.error = err.code;
this.emit('error', err);
}.bind(this));
req.end();
}...
}
};
};
WemoClient.prototype._onListenerAdded = function(eventName) {
var serviceType = WemoClient.EventServices[eventName];
if (serviceType && this.services[serviceType]) {
this._subscribe(serviceType);
}
};
WemoClient.prototype._subscribe = function(serviceType) {
if (!this.services[serviceType]) {
throw new Error('Service ' + serviceType + ' not supported by ' + this.UDN);
}
...getAttributes = function (cb) {
this.soapAction('urn:Belkin:service:deviceevent:1', 'GetAttributes', null, function(err, data) {
if (err) return cb(err);
var xml = '<attributeList>' + entities.decodeXML(data.attributeList) + '</attributeList>';
xml2js.parseString(xml, { explicitArray: false }, function(err, result) {
if (err) return cb(err);
var attributes = {};
for (var key in result.attributeList.attribute) {
var attribute = result.attributeList.attribute[key];
attributes[attribute.name] = attribute.value;
}
cb(null, attributes);
});
});
}n/a
getBinaryState = function (cb) {
this.soapAction('urn:Belkin:service:basicevent:1', 'GetBinaryState', null, function(err, data) {
if (err) return cb(err);
cb(null, data.BinaryState);
});
}n/a
getDeviceStatus = function (deviceId, cb) {
var parseResponse = function(err, data) {
if (err) return cb(err);
xml2js.parseString(data.DeviceStatusList, { explicitArray: false }, function(err, result) {
if (err) return cb(err);
var deviceStatus = result['DeviceStatusList']['DeviceStatus'];
var capabilities = mapCapabilities(deviceStatus.CapabilityID, deviceStatus.CapabilityValue);
cb(null, capabilities);
});
};
this.soapAction('urn:Belkin:service:bridge:1', 'GetDeviceStatus', {
DeviceIDs: deviceId
}, parseResponse);
}n/a
getEndDevices = function (cb) {
var parseDeviceInfo = function(data) {
var device = {};
if (data.GroupID) {
// treat device group as it was a single device
device.friendlyName = data.GroupName[0];
device.deviceId = data.GroupID[0];
device.capabilities = mapCapabilities(
data.GroupCapabilityIDs[0],
data.GroupCapabilityValues[0]
);
} else {
// single device
device.friendlyName = data.FriendlyName[0];
device.deviceId = data.DeviceID[0];
device.capabilities = mapCapabilities(
data.CapabilityIDs[0],
data.CurrentState[0]
);
}
// set device type
if (device.capabilities.hasOwnProperty('10008')) {
device.deviceType = 'dimmableLight';
}
if (device.capabilities.hasOwnProperty('10300')) {
device.deviceType = 'colorLight';
}
return device;
};
var parseResponse = function(err, data) {
if (err) return cb(err);
debug('endDevices raw data', data);
var endDevices = [];
xml2js.parseString(data.DeviceLists, function(err, result) {
if (err) return cb(err);
var deviceInfos = result.DeviceLists.DeviceList[0].DeviceInfos[0].DeviceInfo;
if (deviceInfos) {
Array.prototype.push.apply(endDevices, deviceInfos.map(parseDeviceInfo));
}
if (result.DeviceLists.DeviceList[0].GroupInfos) {
var groupInfos = result.DeviceLists.DeviceList[0].GroupInfos[0].GroupInfo;
Array.prototype.push.apply(endDevices, groupInfos.map(parseDeviceInfo));
}
cb(null, endDevices);
});
};
this.soapAction('urn:Belkin:service:bridge:1', 'GetEndDevices', {
DevUDN: this.UDN,
ReqListType: 'PAIRED_LIST'
}, parseResponse);
}n/a
getInsightParams = function (cb) {
this.soapAction('urn:Belkin:service:insight:1', 'GetInsightParams', null, function(err, data) {
if (err) return cb(err);
var params = this._parseInsightParams(data.InsightParams);
cb(null, params.binaryState, params.instantPower, params.insightParams);
}.bind(this));
}n/a
handleCallback = function (body) {
var self = this;
var handler = {
BinaryState: function(data) {
self.emit('binaryState', data.substring(0, 1));
},
StatusChange: function(data) {
xml2js.parseString(data, { explicitArray: false }, function(err, xml) {
if (!err) {
self.emit('statusChange',
xml.StateEvent.DeviceID._,
xml.StateEvent.CapabilityId,
xml.StateEvent.Value
);
}
});
},
InsightParams: function(data) {
var params = this._parseInsightParams(data);
self.emit('insightParams', params.binaryState, params.instantPower, params.insightParams);
}.bind(this),
attributeList: function(data) {
var xml = '<attributeList>' + entities.decodeXML(data) + '</attributeList>';
xml2js.parseString(xml, { explicitArray: true }, function(err, result) {
if (!err) {
// In order to keep the existing event signature this
// triggers an event for every attribute changed.
result.attributeList.attribute.forEach(function(attribute) {
self.emit('attributeList',
attribute.name[0],
attribute.value[0],
attribute.prevalue[0],
attribute.ts[0]
);
});
}
});
}
};
xml2js.parseString(body, { explicitArray: false }, function(err, xml) {
if (err) throw err;
for (var prop in xml['e:propertyset']['e:property']) {
if (handler.hasOwnProperty(prop)) {
handler[prop](xml['e:propertyset']['e:property'][prop]);
} else {
debug('Unhandled Event: %s', prop);
}
}
});
}...
if ((req.method == 'NOTIFY') && this._clients[udn]) {
req.on('data', function(chunk) {
body += chunk.toString();
});
req.on('end', function() {
debug('Incoming Request for %s: %s', udn, body);
this._clients[udn].handleCallback(body);
res.writeHead(204);
res.end();
}.bind(this));
} else {
debug('Received request for unknown device: %s', udn);
res.writeHead(404);
res.end();
...setBinaryState = function (value, cb) {
this.soapAction('urn:Belkin:service:basicevent:1', 'SetBinaryState', {
BinaryState: value
}, cb);
}...
// Handle BinaryState events
client.on('binaryState', function(value) {
console.log('Binary State changed to: %s', value);
});
// Turn the switch on
client.setBinaryState(1);
});
```
## API
### Wemo
...setDeviceStatus = function (deviceId, capability, value, cb) {
var deviceStatusList = xmlbuilder.create('DeviceStatus', {
version: '1.0',
encoding: 'utf-8'
}).ele({
IsGroupAction: (deviceId.length === 10) ? 'YES' : 'NO',
DeviceID: deviceId,
CapabilityID: capability,
CapabilityValue: value
}).end();
this.soapAction('urn:Belkin:service:bridge:1', 'SetDeviceStatus', {
DeviceStatusList: {
'#text': deviceStatusList
}
}, cb);
}...
this.soapAction('urn:Belkin:service:bridge:1', 'GetDeviceStatus', {
DeviceIDs: deviceId
}, parseResponse);
};
WemoClient.prototype.setLightColor = function(deviceId, red, green, blue, cb) {
var color = WemoClient.rgb2xy(red, green, blue);
this.setDeviceStatus(deviceId, 10300, color.join(':') + ':0', cb
);
};
WemoClient.prototype.setBinaryState = function(value, cb) {
this.soapAction('urn:Belkin:service:basicevent:1', 'SetBinaryState', {
BinaryState: value
}, cb);
};
...setLightColor = function (deviceId, red, green, blue, cb) {
var color = WemoClient.rgb2xy(red, green, blue);
this.setDeviceStatus(deviceId, 10300, color.join(':') + ':0', cb);
}n/a
soapAction = function (serviceType, action, body, cb) {
cb = cb || function() {};
var xml = xmlbuilder.create('s:Envelope', {
version: '1.0',
encoding: 'utf-8',
allowEmpty: true
})
.att('xmlns:s', 'http://schemas.xmlsoap.org/soap/envelope/')
.att('s:encodingStyle', 'http://schemas.xmlsoap.org/soap/encoding/')
.ele('s:Body')
.ele('u:' + action)
.att('xmlns:u', serviceType);
var payload = (body ? xml.ele(body) : xml).end();
var options = {
host: this.host,
port: this.port,
path: this.services[serviceType].controlURL,
method: 'POST',
headers: {
'SOAPACTION': '"' + serviceType + '#' + action + '"',
'Content-Type': 'text/xml; charset="utf-8"'
}
};
WemoClient.request(options, payload, function(err, response) {
if (err) {
this.error = err.code;
this.emit('error', err);
return cb(err);
}
debug('%s Response: ', action, response);
cb(null, response && response['s:Envelope']['s:Body']['u:' + action + 'Response']);
}.bind(this));
}...
var groupInfos = result.DeviceLists.DeviceList[0].GroupInfos[0].GroupInfo;
Array.prototype.push.apply(endDevices, groupInfos.map(parseDeviceInfo));
}
cb(null, endDevices);
});
};
this.soapAction('urn:Belkin:service:bridge:1', 'GetEndDevices', {
DevUDN: this.UDN,
ReqListType: 'PAIRED_LIST'
}, parseResponse);
};
WemoClient.prototype.setDeviceStatus = function(deviceId, capability, value, cb) {
var deviceStatusList = xmlbuilder.create('DeviceStatus', {
...