Skip to content

Instantly share code, notes, and snippets.

@hramos
Last active November 14, 2015 15:07
Show Gist options
  • Save hramos/d595bfbafa24ff2b10f1 to your computer and use it in GitHub Desktop.
Save hramos/d595bfbafa24ff2b10f1 to your computer and use it in GitHub Desktop.
node-sonos-http-api client for Parse Cloud Code
'use strict';
// controllers/alexa-skills.controller.js
// Based on https://github.com/mattwelch/alexa-sonos/blob/master/sonos.controller.js
// Adapted to use sonos-client.js
// Reference implementation for Alexa Skills Kit service.
// https://developer.amazon.com/edw/home.html#/skills
// alexa-nodekit node module https://github.com/brutalhonesty/alexa-nodekit
var alexa = require('cloud/modules/alexa-nodekit/index.js');
// sonos-client.js https://gist.github.com/hramos/d595bfbafa24ff2b10f1
var sonosClient = require('cloud/sonos-client.js');
var _ = require("underscore");
// Use whatever fits your Sonos configuration. This controller could and should be expanded to support multiple zones
var zone = 'Living Room';
// Accept incoming Amazon Echo request.
// The Intent Request will be parsed for the Intent type and then forwarded to its proper function.
exports.index = function(req, res) {
var sessionId;
var userId;
if(req.body.request.type === 'LaunchRequest') {
alexa.launchRequest(req.body);
// TODO For now, we don't care about the session or the user id, we will refactor this later.
sessionId = alexa.sessionId;
userId = alexa.userId;
alexa.response('Welcome to Sonos. What can I play for you?', {
title: 'Sonos',
subtitle: 'Welcome to the Sonos app',
content: 'Some commands are "Play favorite xxx" or "Play playlist xxx"'
}, false, function (error, response) {
if(error) {
return res.status(500).jsonp({message: error});
}
return res.jsonp(response);
});
} else if(req.body.request.type === 'IntentRequest') {
alexa.intentRequest(req.body);
// TODO For now, we don't care about the session or the user id, we will refactor this later.
sessionId = alexa.sessionId;
userId = alexa.userId;
var intent = alexa.intentName;
var slots = alexa.slots;
if(intent === 'PlayPlaylist') {
sonosClient.playlist(zone, req.body.request.intent.slots.Playlist.value).then(function(response){
alexa.response('Playlist requested.', {
title: 'Sonos',
subtitle: 'Playlist requested',
content: 'Playlist requested.'
}, true, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
});
} else if(intent === 'ListZones') {
sonosClient.zones().then(function(zones) {
console.log(zones);
alexa.response('Zones request received. Check your logs.', {
title: 'Sonos',
subtitle: 'Zones...',
content: ''
}, true, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
});
} else if(intent === 'PlayFavorite') {
sonosClient.favorite(zone, req.body.request.intent.slots.Favorite.value).then(function(response) {
alexa.response('Playing '+req.body.request.intent.slots.Favorite.value, {
title: 'Sonos',
subtitle: 'Favorite requested: '+req.body.request.intent.slots.Favorite.value,
content: 'Favorite requested.'
}, true, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
});
} else if(intent === 'StartMusic') {
sonosClient.play(zone).then(function() {
alexa.response('Playing', {
title: 'Sonos',
subtitle: 'Music started',
content: 'Music started.'
}, true, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
}, function(error) {
return res.status(500).jsonp(error);
})
} else if(intent === 'StopMusic') {
sonosClient.pause(zone).then(function() {
alexa.response('Pausing', {
title: 'Sonos',
subtitle: 'Music stopped',
content: 'Music stopped.'
}, true, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
}, function(error) {
return res.status(500).jsonp(error);
})
} else if(intent === 'LowerVolume') {
sonosClient.volume(zone, "-15").then(function() {
alexa.response('Lowering volume', {
title: 'Sonos',
subtitle: 'Volume lowered by 15%.',
content: 'Volume lowered by 15%.'
}, true, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
});
} else if(intent === 'RaiseVolume') {
sonosClient.volume(zone, "+15").then(function() {
alexa.response('Raising volume', {
title: 'Sonos',
subtitle: 'Volume raised by 15%.',
content: 'Volume raised by 15%..'
}, true, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
});
} else if(intent === 'MuteMusic') {
sonosClient.mute(zone).then(function() {
alexa.response('Muting', {
title: 'Sonos',
subtitle: 'Music muted.',
content: 'Music muted.'
}, true, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
});
} else if(intent === 'UnmuteMusic') {
sonosClient.unmute(zone).then(function() {
alexa.response('Unmuting', {
title: 'Sonos',
subtitle: 'Unmuted',
content: 'Unmuted.'
}, true, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
});
} else if(intent === 'PlayNext') {
sonosClient.next(zone).then(function() {
alexa.response('Playing the next track in the queue.', {
title: 'Sonos',
subtitle: 'Next track.',
content: 'Next track.'
}, true, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
});
} else if(intent === 'PlayPrevious') {
sonosClient.previous(zone).then(function() {
alexa.response('Playing the previous track in the queue.', {
title: 'Sonos',
subtitle: 'Previous track.',
content: 'Previous track.'
}, true, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
});
} else if(intent === 'IdentifyMusic') {
sonosClient.state(zone).then(function(response) {
var data = response.data;
console.log(data);
var artist = data.currentTrack.artist;
var title = data.currentTrack.title;
var album = data.currentTrack.album;
var responseString = "Nothing is playing.";
var responseSubtitle = "Player may be paused or off.";
var responseContent = "";
var playerState = data.playerState;
if (playerState === "PLAYING") {
responseString = "Currently playing "
responseSubtitle = "Currently playing."
if (artist) {
responseString += artist;
responseString += " ";
responseContent += artist;
responseContent += " ";
}
if (title) {
responseString += title;
responseString += " ";
responseContent += " - ";
responseContent += title;
}
if (album) {
responseContent += " - ";
responseContent += album;
}
}
alexa.response(responseString, {
title: 'Sonos',
subtitle: responseSubtitle,
content: responseContent
}, true, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
});
} else {
alexa.response('Unknown intention, please try a different command.', {
title: 'Sonos',
subtitle: 'Unknown intention.',
content: 'Unknown intention, please try a different command.'
}, false, function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
}
} else {
alexa.sessionEndedRequest(req.body);
// TODO For now, we don't care about the session or the user id, we will refactor this later.
sessionId = alexa.sessionId;
userId = alexa.userId;
var sessionEndReason = alexa.reason;
alexa.response(function (error, response) {
if(error) {
return res.status(500).jsonp(error);
}
return res.jsonp(response);
});
}
};
// These two lines are required to initialize Express in Cloud Code.
express = require('express');
app = express();
// Global app configuration section
app.set('views', 'cloud/views'); // Specify the folder to find templates
app.set('view engine', 'ejs'); // Set the template engine
app.use(express.bodyParser()); // Middleware for reading request body
var alexaSkillsController = require('cloud/controllers/alexa-skills.controller.js');
// Invocation endpoint: https://yourapp.parseapp.com/alexaSkills
app.post('/alexaSkills', alexaSkillsController.index);
// Attach the Express app to Cloud Code.
app.listen();
<!-- Add these to your Amazon Skills service -->
{
"intents": [
{
"intent": "ListZones",
"slots": []
},
{
"intent": "PlayPlaylist",
"slots": [
{
"name": "Playlist",
"type": "LITERAL"
}
]
},
{
"intent": "PlayFavorite",
"slots": [
{
"name": "Favorite",
"type": "LITERAL"
}
]
},
{
"intent": "IdentifyMusic",
"slots": []
},
{
"intent": "StartMusic",
"slots": []
},
{
"intent": "StopMusic",
"slots": []
},
{
"intent": "LowerVolume",
"slots": []
},
{
"intent": "RaiseVolume",
"slots": []
},
{
"intent": "MuteMusic",
"slots": []
},
{
"intent": "UnmuteMusic",
"slots": []
},
{
"intent": "PlayNext",
"slots": []
},
{
"intent": "PlayPrevious",
"slots": []
}
]
}
require('cloud/app.js');
# Add these to your Amazon Skills service
ListZones list zones
StartMusic play music
StartMusic to play music
StartMusic play some music
StartMusic to play some music
StartMusic play
StopMusic stop
IdentifyMusic what's playing
IdentifyMusic what is playing
IdentifyMusic about what's playing
IdentifyMusic about what is playing
IdentifyMusic what song is this
IdentifyMusic which artist is this
IdentifyMusic who's playing
IdentifyMusic who is playing
LowerVolume lower volume
RaiseVolume raise volume
MuteMusic mute
UnmuteMusic unmute
PlayNext play next
PlayNext next
PlayPrevious play previous
PlayPrevious previous
PlayFavorite to play favorite station {favorite|Favorite}
PlayPlaylist start playlist {playlist|Playlist}
// node-sonos-http-api client for Parse Cloud Code
// Adjust these to match your node-sonos-http-api service
var sonosHost = "yourSonosHost.com";
var sonosPort = 5005;
var baseUrl = "http://" + sonosHost + ":" + sonosPort
var _ = require("underscore");
// List available zones and their status
exports.zones = function() {
var path='/zones';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
var zones = response.data;
prettyPrintZones(zones);
return Parse.Promise.as(zones);
}, function(error) {
return Parse.Promise.error('Could not play.');
});
};
// Ask zone to start playing
exports.play = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/play';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Playing.');
}, function(error) {
return Parse.Promise.error('Could not play.');
});
};
// Start playing playlist
exports.playlist = function(zone, playlist) {
var path = '/' + encodeURIComponent(zone) + '/playlist/' + encodeURIComponent(playlist);
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Playlist requested.');
}, function(error) {
return Parse.Promise.error('Could not play.');
});
};
// Play favorite
exports.favorite = function(zone, favorite) {
var path = '/' + encodeURIComponent(zone) + '/favorite/' + encodeURIComponent(favorite);
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Favorite requested.');
}, function(error) {
return Parse.Promise.error('Could not play.');
});
};
// Pause
exports.pause = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/pause';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Paused.');
}, function(error) {
return Parse.Promise.error('Could not pause.');
});
};
// Adjust volume. Value can be absolute (0 to 100) or relative (-15 or +15 percent)
exports.volume = function(zone, value) {
var path = '/' + encodeURIComponent(zone) + '/volume/' + value;
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Volume adjusted.');
}, function(error) {
return Parse.Promise.error('Could not adjust volume.');
});
};
// Same as volume, but for the entire group
exports.groupVolume = function(value) {
var path = '/groupVolume/' + value;
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Group volume adjusted.');
}, function(error) {
return Parse.Promise.error('Could not adjust group volume.');
});
};
// pause all zones
exports.pauseAll = function(timeout) {
var path = '/pauseall';
if (timeout) {
path += '/' + timeout;
}
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Paused all.');
}, function(error) {
return Parse.Promise.error('Could not pause all.');
});
};
// resume all zones
exports.resumeAll = function(timeout) {
var path = '/resumeall';
if (timeout) {
path += '/' + timeout;
}
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Resumed all.');
}, function(error) {
return Parse.Promise.error('Could not resume all.');
});
};
// clear Sonos queue
exports.clearQueue = function() {
var path = '/clearqueue';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Cleared queue.');
}, function(error) {
return Parse.Promise.error('Could not clear queue.');
});
};
// timeout in seconds, timestamp, or off
exports.sleep = function(timeout) {
var path = '/sleep/' + timeout;
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
var status = 'Sleeping';
if (timeout === 'off') {
status = 'Not sleeping.';
}
return Parse.Promise.as(status);
}, function(error) {
return Parse.Promise.error('Could not adjust sleep.');
});
};
// Skip to a track at queue index
exports.seek = function(zone, queueIndex) {
var path = '/' + encodeURIComponent(zone) + '/seek/' + queueIndex;
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Seeking.');
}, function(error) {
return Parse.Promise.error('Could not seek.');
});
};
// Seek forward or backwards within the current track
exports.trackSeek = function(zone, seconds) {
var path = '/' + encodeURIComponent(zone) + '/trackseek/' + seconds;
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Seeking.');
}, function(error) {
return Parse.Promise.error('Could not seek.');
});
};
// Mute
exports.mute = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/mute';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Volume muted.');
}, function(error) {
return Parse.Promise.error('Could not mute.');
});
};
// Unmute
exports.unmute = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/unmute';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Volume unmuted.');
}, function(error) {
return Parse.Promise.error('Could not unmute.');
});
};
// Repeat current track
exports.repeatOn = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/repeat/on';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Repeat on.');
}, function(error) {
return Parse.Promise.error('Could not adjust.');
});
};
// Repeat current track
exports.repeatOff = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/repeat/off';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Repeat off.');
}, function(error) {
return Parse.Promise.error('Could not adjust.');
});
};
// Shuffle queue
exports.shuffleOn = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/shuffle/on';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Shuffle on.');
}, function(error) {
return Parse.Promise.error('Could not adjust.');
});
};
// Shuffle queue
exports.shuffleOff = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/shuffle/off';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Shuffle off.');
}, function(error) {
return Parse.Promise.error('Could not adjust.');
});
};
// Crossfrade tracks
exports.crossfadeOn = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/crossfade/on';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Crossfade on.');
}, function(error) {
return Parse.Promise.error('Could not adjust.');
});
};
// Crossfade tracks
exports.crossfadeOff = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/crossfade/off';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Crossfade off.');
}, function(error) {
return Parse.Promise.error('Could not adjust.');
});
};
// Play next track
exports.next = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/next';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Playing next.');
}, function(error) {
return Parse.Promise.error('Could not play next.');
});
};
// Play previous track
exports.previous = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/previous';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Playing previous.');
}, function(error) {
return Parse.Promise.error('Could not play previous.');
});
};
// Get current state
exports.state = function(zone) {
var path = '/' + encodeURIComponent(zone) + '/state';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
var data = response.data;
return Parse.Promise.as(response.data);
}, function(error) {
return Parse.Promise.error('Could not unmute.');
});
};
// experimental
exports.lockVolumes = function() {
var path = '/lockvolumes';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Volumes locked.');
}, function(error) {
return Parse.Promise.error('Could not lock volumes.');
});
};
exports.unlockVolumes = function() {
var path = '/unlockvolumes';
return Parse.Cloud.httpRequest({
url: baseUrl + path
}).then(function(response){
return Parse.Promise.as('Volumes unlocked.');
}, function(error) {
return Parse.Promise.error('Could not unlock volumes.');
});
};
var prettyPrintZones = function(zones) {
var pp = "";
_.each(zones, function(zone) {
pp += 'uuid: ' + zone.uuid + '\n';
pp += 'coordinator:\n';
pp += ' uuid: ' + zone.coordinator.uuid + '\n';
pp += ' roomName: ' + zone.coordinator.roomName + '\n';
pp += 'members: \n';
_.each(zone.members, function(member) {
pp += ' uuid:' + member.uuid + '\n';
pp += ' roomName:' + member.roomName + '\n';
pp += ' playMode:\n';
pp += ' ├── shuffle:' + member.playMode.shuffle + '\n';
pp += ' ├── repeat:' + member.playMode.repeat + '\n';
pp += ' └── crossfade:' + member.playMode.crossfade + '\n';
pp += ' groupState:\n';
pp += ' ├── volume:' + member.groupState.volume + '\n';
pp += ' └── mute:' + member.groupState.mute + '\n';
pp += ' state:\n';
pp += ' ├── currentTrack:\n';
pp += ' | ├── artist:' + member.state.currentTrack.artist + '\n';
pp += ' | ├── title:' + member.state.currentTrack.title + '\n';
pp += ' | └── album:' + member.state.currentTrack.album + '\n';
pp += ' ├── nextTrack:\n';
pp += ' | ├── artist:' + member.state.nextTrack.artist + '\n';
pp += ' | ├── title:' + member.state.nextTrack.title + '\n';
pp += ' | └── album:' + member.state.nextTrack.album + '\n';
pp += ' ├── volume:' + member.state.volume + '\n';
pp += ' ├── mute:' + member.state.mute + '\n';
pp += ' ├── trackNo:' + member.state.trackNo + '\n';
pp += ' ├── elapsedTime:' + member.state.elapsedTime + '\n';
pp += ' ├── elapsedTimeFormatted:' + member.state.elapsedTimeFormatted + '\n';
pp += ' ├── zoneState:' + member.state.zoneState + '\n';
pp += ' ├── playerState:' + member.state.playerState + '\n';
pp += ' └── zonePlayMode:\n';
pp += ' ├── shuffle:' + member.state.zonePlayMode.shuffle + '\n';
pp += ' ├── repeat:' + member.state.zonePlayMode.repeat + '\n';
pp += ' └── crossfade:' + member.state.zonePlayMode.crossfade + '\n';
});
});
console.log(pp);
};
@hramos
Copy link
Author

hramos commented Aug 23, 2015

Updated with Parse Cloud Code main.js and app.js, as well as a sample of the Alexa Skills controller I am using (with non-Sonos intents removed).

@hramos
Copy link
Author

hramos commented Aug 23, 2015

Updated with sample utterances and intent schema that I am using in my SONOS Amazon Skills service.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment