thing-it-device-sonos
Version:
[thing-it-node] Device Plugin for Sonos players.
705 lines (620 loc) • 21.6 kB
JavaScript
module.exports = {
metadata: {
family: "sonos",
plugin: "sonos",
label: "Sonos",
tangible: true,
discoverable: true,
state: [{
id: "currentTrack",
label: "Current Track",
type: {
id: "string"
}
}, {
id: "currentState",
label: "Current State",
type: {
id: "string"
}
}, {
id: "artist",
label: "Artist",
type: {
id: "string"
}
}, {
id: "album",
label: "Album",
type: {
id: "string"
}
}, {
id: "albumArtURI",
label: "Album Art URI",
type: {
id: "string"
}
}, {
id: "volume",
label: "Volume",
type: {
id: "integer"
}
}, {
id: "muted",
label: "Muted",
type: {
id: "boolean"
}
}],
actorTypes: [],
sensorTypes: [],
services: [{
id: "play",
label: "Play"
}, {
id: "pause",
label: "Pause"
}, {
id: "stopPlayback",
label: "Stop"
}, {
id: "mute",
label: "Mute"
}, {
id: "next",
label: "Next"
}, {
id: "previous",
label: "Previous"
}, {
id: "changeVolume",
label: "Change Volume"
}],
configuration: [{
id: "simulated",
label: "Simulated",
type: {
id: "boolean"
}
}, {
id: "host",
label: "Host",
type: {
id: "string"
}
}, {
id: "name",
label: "Name",
type: {
id: "string"
}
}, {
id: "updateInterval",
label: "Update Interval",
type: {
id: "integer"
},
defaultValue: 5000
}]
},
create: function (device) {
return new Sonos();
},
discovery: function (options) {
var discovery = new SonosDiscovery();
discovery.options = options;
return discovery;
}
};
var q = require('q');
var xml2js = require('sonos/node_modules/xml2js');
var SonosLibrary;
var transportModes = {
stopped: "STOPPED",
playing: "PLAYING",
paused: "PAUSED_PLAYBACK",
transitioning: "TRANSITIONING",
noMediaPresent: "NO_MEDIA_PRESENT"
};
function SonosDiscovery() {
/**
*
* @param options
*/
SonosDiscovery.prototype.start = function () {
if (this.node.isSimulated()) {
} else {
if (!SonosLibrary) {
SonosLibrary = require("sonos");
}
this.logInfo("Scanning for Sonos hosts started.");
var sonosSearch = SonosLibrary.search();
sonosSearch.on('DeviceAvailable', function (sonos) {
sonos.deviceDescription(function (err, output) {
var deferred = q.defer();
if (err != null) {
this.logInfo("ERROR - " + JSON.stringify(err));
}
else {
this.logInfo("Auto discovered Sonos host " + sonos.host + " with name " + output.roomName
+ " and friendly name " + output.friendlyName + ".");
var sonosSpeaker = new Sonos();
sonosSpeaker.id = "sonos" + output.roomName.replace(/\W/g, '');
sonosSpeaker.label = "Sonos " + output.roomName;
sonosSpeaker.uuid = output.friendlyName;
sonosSpeaker.configuration = {
host: sonos.host,
name: output.roomName,
};
this.advertiseDevice(sonosSpeaker);
}
deferred.resolve();
return deferred.promise;
}.bind(this));
}.bind(this));
}
};
/**
*
* @param options
*/
SonosDiscovery.prototype.stop = function () {
this.logDebug("Sonos discovery prototype stop called. Doing nothing.");
};
}
/**
*
*/
function Sonos() {
/**
*
*/
Sonos.prototype.start = function () {
var deferred = q.defer();
this.state = {
currentTrack: null,
currentState: null,
volume: 0,
muted: false,
artist: null,
album: null,
albumArtURI: null
};
this.simulationIntervals = [];
this.intervals = [];
this.logDebug("Sonos state: " + JSON.stringify(this.state));
if (this.sonos) {
this.logInfo("Unexpected Condition: Sonos already exists in prototype start.");//@TODO remove
this.connect();
deferred.resolve();
}
else {
if (!this.isSimulated()) {
this.logInfo("Starting up Sonos.");
this.started = true;
if (!SonosLibrary) {
SonosLibrary = require("sonos");
}
this.configuration.updateInterval =
((this.configuration.updateInterval === undefined || 1000 > this.configuration.updateInterval)
? 1000
: this.configuration.updateInterval);
this.scan();
deferred.resolve();
} else {
this.logInfo("Starting up simulated Sonos.");
deferred.resolve();
this.initiateSimulation();
}
}
return deferred.promise;
};
Sonos.prototype.stop = function () {
this.started = false;
this.logInfo("Stopping Sonos Device " + this.configuration.name + ".");
for (var interval in this.intervals) {
clearInterval(interval);
}
for (var interval in this.simulationIntervals) {
clearInterval(interval);
}
this.logDebug("Un-registering for zone events.");
if (this.listener && this.sids && this.sids.length > 0) {
var sid;
for (var i in this.sids) {
sid = this.sids[i];
this.logDebug("Un-registering sid.", sid);
try {
this.listener.removeService(sid, function (error, data) {
if (error){
this.logError("Error unregistering event.", error);
} else {
this.logDebug("Successfully unregistered.");
}
}.bind(this));
} catch (e) {
this.logError("Error unregistering event.", e);
}
}
}
}
/**
*
*/
Sonos.prototype.scan = function () {
this.logInfo("Scanning for Sonos Host " + this.configuration.name + " started.");
var sonosSearch = SonosLibrary.search();
sonosSearch.on('DeviceAvailable', function (sonos) {
var deferred = q.defer();
this.logDebug("Found Sonos " + sonos.host);
sonos.deviceDescription(function (err, output) {
var deferred = q.defer();
if (err != null) {
this.logInfo("ERROR - " + JSON.stringify(err));
}
else {
if (output.roomName === this.configuration.name) {
this.logInfo("Found matching host with name " + output.roomName + " and friendly name "
+ output.friendlyName);
this.sonos = sonos;
this.description = output;
this.registerEvents();
}
else {
this.logDebug("Ignoring host " + sonos.host + " with room name " + output.roomName
+ " and friendly name " + output.friendlyName);
}
}
deferred.resolve();
return deferred.promise;
}.bind(this));
deferred.resolve();
return deferred.promise;
}.bind(this));
};
/**
*
*/
Sonos.prototype.registerEvents = function () {
this.logDebug("Registering for zone events.");
var Listener = require('sonos/lib/events/listener');
this.listener = new Listener(this.sonos);
this.sids = [];
this.logDebug("Initiated the listener.");
this.listener.listen(function (err) {
if (err) throw err;
/**
* TODO: Add services for:
* - /MediaServer/ConnectionManager/Event
* UPnP standard connection manager service for the media server.
* - /MediaRenderer/ConnectionManager/Event
* UPnP standard connection manager service for the media renderer.
* - /MediaRenderer/Queue/Event
* Sonos queue service, for functions relating to queue management, saving
* queues etc.
* - /MediaRenderer/GroupRenderingControl/Control
* Sonos group rendering control service, for functions relating to
* group volume etc.
* - /ZoneGroupTopology/Event
* Notification about zone changes
*/
// Register for play, pause, etc.
this.listener.addService('/MediaRenderer/AVTransport/Event', function (error, sid) {
if (error) {
this.logError("Error subscribing to event: " + JSON.stringify(error));
}
else {
this.logDebug('Successfully subscribed, with subscription id', sid);
this.sids.push(sid);
}
}.bind(this));
// register for playback rendering, eg bass, treble, volume and EQ.
this.listener.addService('/MediaRenderer/RenderingControl/Event', function (error, sid) {
if (error) {
this.logError("Error: " + JSON.stringify(error));
} else {
this.logDebug('Successfully subscribed, with subscription id', sid);
this.sids.push(sid);
}
}.bind(this));
this.listener.on('serviceEvent', function (endpoint, sid, data) {
this.logDebug('Received event from', endpoint, '(' + sid + ').');
xml2js.parseString(data.LastChange, function (err, event) {
try {
if ("/MediaRenderer/AVTransport/Event" == endpoint) {
try {
this.state.currentState = event.Event.InstanceID[0].TransportState[0].$.val;
this.logInfo("Notified about current play mode.", this.state.currentState);
this.publishStateChange();
} catch (e) {
this.logDebug("No transport state.", endpoint);
}
var currentTrackMetaDataXML;
try {
currentTrackMetaDataXML = event.Event.InstanceID[0].CurrentTrackMetaData[0].$.val;
} catch (e) {
this.logDebug("No track meta data.", endpoint);
}
if (currentTrackMetaDataXML) {
xml2js.parseString(currentTrackMetaDataXML, function (err, trackMetaData) {
if (!err) {
try {
this.state.currentTrack = trackMetaData["DIDL-Lite"].item[0]["dc:title"][0];
this.state.artist = trackMetaData["DIDL-Lite"].item[0]["dc:creator"][0];
this.state.album = trackMetaData["DIDL-Lite"].item[0]["upnp:album"][0];
this.state.albumArtURI = trackMetaData["DIDL-Lite"].item[0]["upnp:albumArtURI"][0];
} catch (e) {
this.logError("Error reading track meta data.", e);
}
this.logInfo("Notified about current track.", this.state.currentTrack,
this.state.artist, this.state.album);
this.logDebug("Album Art URI", this.state.albumArtURI);
this.publishStateChange();
}
else {
this.logError("Error parsing event from", endpoint, '(' + sid + ').', e);
}
}.bind(this));
}
} else if ("/MediaRenderer/RenderingControl/Event" == endpoint) {
try {
this.state.muted = (event.Event.InstanceID[0].Mute[0].$.val == 1) ? true : false;
this.logInfo("Muted", this.state.muted);
} catch (e) {
this.logDebug("No mute state.", endpoint);
}
try {
this.state.volume = parseInt(event.Event.InstanceID[0].Volume[0].$.val);
this.logInfo("Volume", this.state.volume);
} catch (e) {
this.logDebug("No volume state.", endpoint);
}
this.publishStateChange();
}
} catch (e) {
this.logError("Could not handle event from", endpoint, '(' + sid + ').');
this.logError(e);
}
}.bind(this));
}.bind(this));
}.bind(this));
this.logDebug("Done registering events.");
}
/**
*
*/
Sonos.prototype.setState = function (state) {
this.state = state;
this.publishStateChange();
};
/**
*
*/
Sonos.prototype.getState = function () {
return this.state;
};
/**
*
*
*/
Sonos.prototype.play = function () {
this.logDebug("Sonos play called");
if (!this.isSimulated()) {
this.sonos.play(function (err, data) {
// no need to do anything, really.
}.bind(this));
}
else {
this.state.currentState = transportModes.playing;
}
this.publishStateChange();
};
/**
*
*
*/
Sonos.prototype.pause = function () {
if (!this.isSimulated()) {
this.sonos.pause(function (err, data) {
// no need to do anything, really.
}.bind(this));
}
else {
this.state.currentState = transportModes.paused;
}
this.publishStateChange();
};
/**
*
*
*/
Sonos.prototype.stopPlayback = function () {
if (!this.isSimulated()) {
this.sonos.stop(function (err, data) {
// no need to do anything, really.
}.bind(this));
}
else {
this.state.currentState = transportModes.stopped;
}
this.publishStateChange();
};
/**
*
*
*/
Sonos.prototype.next = function () {
if (!this.isSimulated()) {
this.sonos.next(function (err, data) {
// no need to do anything, really.
}.bind(this));
this.publishStateChange();
}
else {
this.simulateNextSong();
}
};
/**
*
*
*/
Sonos.prototype.previous = function () {
if (!this.isSimulated()) {
this.sonos.previous(function (err, data) {
// no need to do anything
}.bind(this));
this.publishStateChange();
}
else {
this.simulatePreviousSong()
}
};
/**
*
*
*/
Sonos.prototype.mute = function () {
this.logDebug("Sonos mute called");
if (!this.isSimulated()) {
this.sonos.getMuted(function (err, muted) {
var muteOpposite = !muted;
this.logDebug("Setting mute to " + muteOpposite);
this.sonos.setMuted(muteOpposite, function (err, data) {
});
}.bind(this))
}
else {
this.state.muted = !this.state.muted;
}
this.publishStateChange();
};
/**
*
*
*/
Sonos.prototype.isPlaying = function () {
return ((this.state.currentState == "playing") || (this.state.currentState == "transitioning"));
}
/**
*
*
*/
Sonos.prototype.isPaused = function () {
return (this.state.currentState == transportModes.paused);
}
/**
*
*
*/
Sonos.prototype.isStopped = function () {
return (this.state.currentState == transportModes.stopped);
}
/**
*
*
*/
Sonos.prototype.isMuted = function () {
return this.state.muted;
}
/**
*
*
*/
Sonos.prototype.changeVolume = function (parameters) {
this.logDebug("Sonos changeVolume called");
this.setVolume(parameters.level);
};
Sonos.prototype.setVolume = function (volume) {
this.logDebug("Sonos setVolume called");
this.state.volume = volume;
if (!this.isSimulated()) {
this.sonos.setVolume(this.state.volume, function (err, data) {
// no need to do anything, really.
}.bind(this));
}
this.publishStateChange();
}
/**
*
*
*/
Sonos.prototype.initiateSimulation = function () {
this.state = {
currentTrack: null,
currentState: "playing",
volume: 23,
muted: false,
artist: null,
album: null,
albumArtURI: null
};
this.simulationData = {
songNr: 0,
songs: [{
currentTrack: "My Other Love",
artist: "Pretty Lights",
album: "Filling Up The City Skies",
albumArtURI: "http://cont-sv5-1.pandora.com/images/public/amz/8/0/1/6/900006108_500W_500H.jpg"
}, {
currentTrack: "Rainy Streets",
artist: "Blue In Green",
album: "The Break Of Dawn",
albumArtURI: "http://cont-sjl-1.pandora.com/images/public/amz/8/0/1/0/885007210108_500W_500H.jpg"
}, {
currentTrack: "Ike's Mood I",
artist: "Visioneers",
album: "Dirty Old Hip-Hop",
albumArtURI: "http://cont-2.p-cdn.com/images/public/amz/2/2/1/7/730003107122_500W_500H.jpg"
}]
};
this.simulateSong(0);
// Simulating song changes every 15 seconds, but only if playing
this.simulationIntervals.push(setInterval(function () {
if (this.isPlaying()) {
this.simulateNextSong();
}
}.bind(this), 15000));
}
/**
*
*
*/
Sonos.prototype.simulatePreviousSong = function () {
this.logDebug("Simulating previous song.");
if (this.simulationData.songNr == 0) {
this.simulationData.songNr = (this.simulationData.songs.length - 1);
}
else {
this.simulationData.songNr--;
}
this.simulateSong(this.simulationData.songNr);
}
/**
*
*
*/
Sonos.prototype.simulateNextSong = function () {
this.logDebug("Simulating next song.");
if (this.simulationData.songNr < (this.simulationData.songs.length - 1)) {
this.simulationData.songNr++;
}
else {
this.simulationData.songNr = 0;
}
this.simulateSong(this.simulationData.songNr);
}
/**
*
*
*/
Sonos.prototype.simulateSong = function (index) {
this.logDebug("Simulating song change to ", this.simulationData.songs[index]);
this.state.currentTrack = this.simulationData.songs[index].currentTrack;
this.state.artist = this.simulationData.songs[index].artist;
this.state.album = this.simulationData.songs[index].album;
this.state.albumArtURI = this.simulationData.songs[index].albumArtURI;
this.publishStateChange();
}
}