Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ This module exports 3 items:
s.currentTrack(console.log);

// sonos.Services - wrappers arounds all UPNP services provided by sonsos
// These aren't used internally by the module at all but may be usefull
// These aren't used internally by the module at all but may be useful
// for more complex projects.

###var Sonos = new sonos.Sonos(host, port)###
Expand Down
40 changes: 40 additions & 0 deletions examples/logicalDeviceVolume.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
var Sonos = require('../');
var keypress = require('keypress');

Sonos.LogicalDevice.search(function(err, groups) {
console.log(err, groups);
});

var dev = new Sonos.LogicalDevice([
{ host: '172.17.106.196', port: 1400 },
{ host: '172.17.107.66', port: 1400 }
]);

dev.initialize(function() {
console.log('use up / down keys to change volume');

keypress(process.stdin);

process.stdin.on('keypress', function (ch, key) {
if (key.name === 'down') {

dev.getVolume(function(vol) {
dev.setVolume(vol - 5);
});

} else if (key.name === 'up') {

dev.getVolume(function(vol) {
dev.setVolume(vol + 5);
});

}
if (key && key.ctrl && key.name === 'c') {
process.exit();
}
});

process.stdin.setRawMode(true);
process.stdin.resume();

});
8 changes: 8 additions & 0 deletions examples/volumeWatcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
var Sonos = require('../index').Sonos;

var device = new Sonos(process.env.SONOS_HOST || '192.168.2.11');
device.initialize(function() {
device.on('volumeChange', function(volume) {
console.log(volume);
});
});
85 changes: 66 additions & 19 deletions lib/events/listener.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
var async = require('async');
var request = require('request');
var http = require('http');
var ip = require('ip');
Expand All @@ -6,11 +7,16 @@ var util = require('util');
var _ = require('underscore');
var events = require('events');

var Listener = function(device) {
/**
* Listener "Class"
* @param {Sonos} device Corresponding Sonos device
*/
function Listener(device) {
this.device = device;
this.parser = new xml2js.Parser();
this.services = {};
};
this.serviceEndpoints = {};
}

util.inherits(Listener, events.EventEmitter);

Expand All @@ -37,6 +43,40 @@ Listener.prototype._startInternalServer = function(callback) {

};

/**
* Add event handler to the endpoint
* @param {String} serviceEndpoint Endpoint to subscribe to
* @param {Function} handler handler to call for events
* @param {Function} callback {err, sonosServiceId}
*/
Listener.prototype.addHandler = function(serviceEndpoint, handler, callback) {

this.on('serviceEvent', function(endpoint, sid, data) {
if (endpoint === serviceEndpoint) {
handler(data);
}
});

if (!this.serviceEndpoints[serviceEndpoint])
this._addService(serviceEndpoint, callback);
};

/**
* Remove all handlers from endpoint
* @param {Function} callback {err, results}
*/
Listener.prototype.removeAllHandlers = function(callback) {

this.removeAllListeners('serviceEvent');

async.parallel(Object.keys(this.services).map(function(sid) {
return function(cb) {
this._removeService(sid, cb);
}.bind(this);
}.bind(this)), callback);

};

Listener.prototype._messageHandler = function(req, res) {

if (req.method.toUpperCase() === 'NOTIFY' && req.url.toLowerCase() === '/notify') {
Expand All @@ -61,9 +101,11 @@ Listener.prototype._messageHandler = function(req, res) {
}
};

Listener.prototype.addService = function(serviceEndpoint, callback) {
Listener.prototype._addService = function(serviceEndpoint, callback) {
if (!this.server) {
throw 'Service endpoints can only be added after listen() is called';
callback(new Error('Service endpoints can only be added after listen() is finished'));
} else if (this.serviceEndpoints[serviceEndpoint]) {
callback(new Error('Service endpoint already added (' + serviceEndpoint + ')'));
} else {

var opt = {
Expand All @@ -78,35 +120,31 @@ Listener.prototype.addService = function(serviceEndpoint, callback) {

request(opt, function(err, response) {
if (err || response.statusCode !== 200) {
console.log(response.message || response.statusCode);
callback(err || response.statusMessage);
} else {
callback(null, response.headers.sid);
var sid = response.headers.sid;

this.services[response.headers.sid] = {
this.services[sid] = {
endpoint: serviceEndpoint,
data: {}
};

this.serviceEndpoints[serviceEndpoint] = sid;

if (callback)
callback(null, sid);
}
}.bind(this));

}
};

Listener.prototype.listen = function(callback) {
Listener.prototype._removeService = function(sid, callback) {

if (!this.server) {
this._startInternalServer(callback);
} else {
throw 'Service listener is already listening';
}
};

Listener.prototype.removeService = function(sid, callback) {
if (!this.server) {
throw 'Service endpoints can only be modified after listen() is called';
callback(new Error('Service endpoints can only be modified after listen() is called'));
} else if (!this.services[sid]) {
throw 'Service with sid ' + sid + ' is not registered';
callback(new Error('Service with sid ' + sid + ' is not registered'));
} else {

var opt = {
Expand All @@ -125,8 +163,17 @@ Listener.prototype.removeService = function(sid, callback) {
callback(null, true);
}
});
}

};


Listener.prototype.listen = function(callback) {
if (!this.server) {
this._startInternalServer(callback);
} else {
callback(new Error('Service listener is already listening'));
}
};

module.exports = Listener;
module.exports = Listener;
29 changes: 29 additions & 0 deletions lib/events/volumeListener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
var SERVICE_ENDPOINT = '/MediaRenderer/RenderingControl/Event';

var initVolumeListener = function(baseListener, callback) {

var initialized = false;

baseListener.addHandler(SERVICE_ENDPOINT, function(data) {

// wait for initial data before callback
if (!initialized) {
initialized = true;
callback(null);
}

baseListener.parser.parseString(data.LastChange, function(err, result) {

if (!result.Event.InstanceID[0].Volume) return; // non-volume related change to rendering

baseListener.device.state.volume = parseInt(result.Event.InstanceID[0].Volume[0].$.val);
baseListener.device.emit('volumeChange', baseListener.device.state.volume);
});

}, function(err) {
if (err) callback(err);
});

};

module.exports = initVolumeListener;
126 changes: 126 additions & 0 deletions lib/logicalDevice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
var async = require('async'),
Sonos = require('./sonos').Sonos,
sonosSearch = require('./sonos').search,
util = require('util'),
url = require('url');

function LogicalDevice(devices, coordinator, groupId) {
if (devices.length === 0) {
throw new Error('Logical device must be initialized with at least one device (' + devices.length + ' given)');
}

var coordinatorDevice = coordinator || devices[0];

Sonos.call(this, coordinatorDevice.host, coordinatorDevice.port);
var sonosDevices = devices.map(function(device) {
return new Sonos(device.host, device.post);
});

this.devices = sonosDevices;
this.groupId = groupId;
}

util.inherits(LogicalDevice, Sonos);

LogicalDevice.prototype.initialize = function(cb) {
async.forEach(this.devices, function(device, done) {
device.initialize(done);
}, cb);
};

LogicalDevice.prototype.destroy = function(cb) {
async.forEach(this.devices, function(device, done) {
device.destroy(done);
}, cb);
};

LogicalDevice.prototype.setVolume = function(volume, cb) {
this.getVolume(function(oldVolume) {

var diff = volume - oldVolume;

async.forEach(this.devices, function(device, done) {
var oldDeviceVolume = device.state.volume;
var newDeviceVolume = oldDeviceVolume + diff;

newDeviceVolume = Math.max(newDeviceVolume, 0);
newDeviceVolume = Math.min(newDeviceVolume, 100);

device.setVolume(newDeviceVolume, done);

}, cb);

}.bind(this));
};

LogicalDevice.prototype.getVolume = function(cb) {

var sum = 0;

this.devices.forEach(function(device) {
sum += device.state.volume || 0;
});

cb(sum / this.devices.length);
};

/**
* Create a Search Instance (emits 'DeviceAvailable' with a found Logical Sonos Component)
* @param {Function} Optional 'DeviceAvailable' listener (sonos)
* @return {Search/EventEmitter Instance}
*/
var search = function(callback) {
var search = sonosSearch();

search.once('DeviceAvailable', function(device) {
device.getTopology(function(err, topology) {
if (err) callback(err);
else {

var devices = topology.zones;
var groups = {};

// bucket devices by groupid
devices.forEach(function(device) {

if (device.coordinator === 'false' || device.name === 'BRIDGE' || device.name === 'BOOST') return; // devices to ignore in search

if (!groups[device.group]) groups[device.group] = { members: [] };

var parsedLocation = url.parse(device.location);
var sonosDevice = new Sonos(parsedLocation.hostname, parsedLocation.port);

if (device.coordinator === 'true') groups[device.group].coordinator = sonosDevice;
groups[device.group].members.push(sonosDevice);

});

// initialze all of the logical devices brefore callback
var logicalDevices = Object.keys(groups).map(function(groupId) {
var group = groups[groupId];
return new LogicalDevice(group.members, group.coordinator, groupId);
});

async.forEach(logicalDevices, function(device, done) {
device.initialize(function(err) {
if (err) done(err);
else {
done(null);
}
});
}, function(err) {
if (err) callback(err);
else {
callback(null, logicalDevices);
}
});
}

});
});

return search;
};

module.exports = LogicalDevice;
module.exports.search = search;
Loading