How to Control your Nest Thermometers with Alexa on the Amazon Echo with AWS Lambda, the Nest REST API, and Alexa Skills SDK
- 17 minutes read - 3491 wordsGet ready for a long, nerdy post with lots of coding and even more configuration. Don’t worry, I did it in a few nights, so anyone can.
Pretty much every night, my wife asks, is the Nest still on? It’s freezing in here. Due to our setup, and hopefully not sedentary lifestyle, the downstairs Nest often things we aren’t home when we’re on the couch, and goes into a balmy 55 degree away mode. I don’t like to pull my phone out just to check, so since we use our Echo to control music and get news on the couch, I figured controlling the Nest would be a logical project. I also am totally enamored with the Echo since getting it (actually I may have seen a prototype version with no plastic that looked like a movie bomb with colorful wires everywhere a few years ago). I think much more so than Siri or the voice part of Google Now, Alexa has found a great niche for voice control and people seem to respond much better to it. I wanted to find a project to learn more about the Alexa SDK and what it could do, and the fact that I could also learn more about Amazon Web Services Lambda, recently launched in 2015, and the Nest REST API were great bonuses.
Lambda is a really cool service where you can essentially run code without having to set up a server in Node.js or Java. Since I write Java pretty consistently in my daily work, I wanted to learn more about Node.js. With Lambda, you can just type your code right into the browser and run it, upload a zip, or trigger it from a variety of devices, including the Echo and now the new IoT SDK from AWS. This might be the next AWS service I explore though I did a little experimenting with it by sending messages to the browser with websockets.
You can find all of the code referenced here on my github at https://github.com/Tylopoda/alexa-nest. Feel free to clone or fork to your delight. You’ll need to make a few configuration changes which I’ll detail below.
We’ll be setting up Alexa to have two main functions. “Status” or “What’s the temperature” will result in Alexa telling us the current temperature and temperature setting on all thermostats connected to the account. You can customize these prompts or responses as you like. “Set upstairs to 65” will have Alexa set the thermostat named “upstairs” to 65 degrees. You can do all of this through If This Then That, but first, it is less customizable and won’t let you get the current temp, and second, this is way more fun.
There are a few limitations to the Alexa Skills SDK. First, rather than being a direct single phrase interaction, you first need to invoke your skill with a phrase. This will be “Alexa, ask X” or “Alex, tell X” where X is whatever you want. Alexa will then tell you the options, and you have to give another phrase to trigger your desired “intent”. You can have as many phrases map to an intent as you want, and as many intents as you want, but you need to configure every combination. For mine, I decided to only respond to these exact phrases. Alexa will parse numeric values easily, but for variable names or other values, you need to enumerate every value. For the termostats names for example, you need to list out each one in the configuration. For our setup, this is “upstairs” and “downstairs”.
Setting up an AWS Lambda Function
I’m going to assume you have an AWS account. If not, you can create a free one. For the usage of this, you probably won’t exceed the free tier. Lambda is a cool technology that allows you to run a “serverless” function in several languages. You get billed for the time your code is actually running, but the free tier allows you to run a pretty hefty amount of invocations, and these skills functions are pretty lightweight.
In the AWS console for your account, navigate to the Lambda section, and create a new Lambda function. The name isn’t super important, but give it something you’ll remember what it means. You can select a blueprint for the function, I suggest using the Alexa skills one as it will give you some built in modules and libraries and preconfigure much of the function. Here you can select the runtime language, for this, we’ll use Node.js. Leave the rest of the settings to the defaults.
You’ll now get a code tab where you can play with the sample app. You can even send test messages to your function to mimic Alexa intents. The “Alexa - start session” mimics invoking the skill with “Alexa, tell X” and “Alexa - my favorite color” send a test intent using the favorite color intent. Feel free to play around with these to make sure things are configured right. We’ll go deeper into the code and testing shortly.
The inline code editor here is nice and makes rapid test and iteration easy, but unfortunately we won’t be able to use it much. We’ll be using the Alexa js module which simplifies our code, but means we need to zip up our code file and the library, and upload it after each change.
The Alexa SDK also provides several sample skills for playing around with. I started with the HelloWorld one and pulled in some additional functionality from the others. If you want to play around first, uploading the HelloWorld one is a good place to start. I’ll wait.
Configuring an Alexa Skill
In order to create and test your skill, you’ll need to register for an Amazon developer account on developer.amazon.com. It’s best to do so with the same account you have tied to your actual Echo, otherwise, you won’t be able to test your skill on the device.
Once you sign in, go to the Alexa tab where you can create a new “Alexa Skills Kit”. Here you’ll get the app id you need for your code. You should also set the name to whatever you want. The invocation name is important because this is what you’ll say to Alexa to trigger the skill. Last on this page, set the Endpoint to Lambda Function and for the value, give the arn which you’ll find at the top right of the AWS Lambda page for your function.
The next page configures the interaction model you’ll use with the skill. Intent schema defines the intents Alexa can interact with in your skill. You can find this configuration as well as the next in the github package, or copy the below.
{
“intents”: [
{
“intent”: “SetTempIntent”,
“slots”: [
{
“name”: “temperature”,
“type”: “AMAZON.NUMBER”
},
{
“name”: “thermostat”,
“type”: “LIST_OF_THERMOSTATS”
}
]
},
{
“intent”: “StatusIntent”
},
{
“intent”: “AMAZON.HelpIntent”
}
]
}
This defines three intents. The AMAZON.HelpIntent isn’t extremely interesting. It’s the one that responds if you ask for help. Status intent is the one we’ll use to get the current temperature data. It takes no parameters. SetTempIntent is the intent for setting the temperature on a given device. The slots here define the two parameters Alexa will pass in, temperature, which is a simple number value, and thermostat, which must be a value found in the definition of LIST_OF_THERMOSTATS. We’ll configure this list next.
The next field allows you to specify the custom slot types, in our case, the thermostat names. My setup has just an upstairs and downstairs thermostat, with these names. Set these to match the names you have configured your thermostat(s) with.
downstairs
upstairs
Last is the list of “utterances” that Alexa will interact with the skill and intents with. You can get very creative here, but I decided to just have a few options.
StatusIntent status
StatusIntent what is the temperature
SetTempIntent set {thermostat} to {temperature}
This defines which intent to invoke for each utterance and defines the variables or “slots” to populate.
On to the last page (unless you eventually want to certify the skill as a public skill, though it wil likely never be certified since the names are hardcoded) where you can test your skill.
Since we haven’t set up the skill yet, there’s not much useful here yet, but you can play with the voice playback, and test sending utterances to your function. It’s useful to try “status” and “set upstairs to 55” or something similar and send these, just to get the JSON structure for these which makes testing your function much easier.
{
“session”: {
“sessionId”: “SessionId.4427d4a9-fad0-4172-94cb-5390e2132c78”,
“application”: {
“applicationId”: “amzn1.echo-sdk-ams.app.{your app id}}”
},
“user”: {
“userId”: “amzn1.account.{your account id}}”
},
“new”: true
},
“request”: {
“type”: “IntentRequest”,
“requestId”: “EdwRequestId.2b03bc5c-5f4f-4b68-8d55-02b83f413dd5”,
“timestamp”: 1449023058883,
“intent”: {
“name”: “SetTempIntent”,
“slots”: {
“thermostat”: {
“name”: “thermostat”,
“value”: “downstairs”
},
“temperature”: {
“name”: “temperature”,
“value”: “65”
}
}
}
}
}
Nest API
On to the last configuration step before you can start coding. This one is more fun though because you can essentially set up a command line controller for your nest.
Start by setting up a developer account on https://developer.nest.com/. Go to https://developer.nest.com/products and create a new “product”. You’ll need permissions configured for “thermostat read & write”. I added away state as well in case I want to add some control for the away state at some point. Leave the rest of the defaults for now as you don’t need to publish this to begin working with it.
After configuring, you’ll get several important values in your “product”. ProductId is needed to configure the API. Secret is needed for getting authentication tokens. Authorization url is needed to allow users to sign in. Access Token Url is used to retrieve an access token, needed to invoke the API for the user.
Since we’re only creating this app to interact with a single user (you) nest devices, this will be greatly simplified.
In your browser, access the auth url first. This is the url you’d normally vend to users to authenticate and give permissions. After signing in and allowing permissions, you’ll get a 8 digit pin. You need this to get the access token needed for calling the API.
Now, using the command line, we’ll call several of the APIs, starting with getting the access token.
curl -X POST “https://api.home.nest.com/oauth2/access_token” -d “code={PIN}” -d “client_id={client_id}” -d “client_secret={client_secret}” -d “grant_type=authorization_code”
You’ll need to replace the pin from above, as well as the client_id and client_secret from your “product” configuration page. This gives us a response containing the access code. This is pretty much all we need now, but let’s have some fun with the API first.
curl -v -L -H “Authorization: Bearer {token}” -X GET “https://developer-api.nest.com/devices”
Put your access token in above and you’ll get back a JSON blob with a list of all of your nest devices.
curl -v -L -H “Authorization: Bearer {token}” -X GET “https://developer-api.nest.com/devices/thermostats”
This gives back just the thermostat devices. Note the device_id for one of them. You can also see the current (ambient) temperature as well as the set temperature.
curl -v -L -H “Authorization: Bearer {token}” -X PUT –data ’{“target_temperature_f”:65}’ “https://developer-api.nest.com/devices/thermostats/{device_id}”
Set the token and device_id, and this will allow you to set a new temperature for the thermostat. All too easy!
Creating the Alexa Skill Code
If you just want to get this running as fast as possible, add your Alexa application id and Nest Access Token to the index.js file, then take the two js files in the src directory, zip them, and upload them to your Lambda function. With some luck, it’ll just work.
This is a good opportunity to rant a bit about debugging. The Lambda function does give stack traces for exceptions and errors, but the line number info doesn’t seem to work super well. I found it easier to install Node.js on my machine and run the index.js file in node. It won’t do anything, but if you have a syntax error, it will give a much better error that helps find it.
The main file we need to worry about now that we’re configured is index.js. Let’s walk through it function by function. I added a ton of debugging code to help get this working and left it in. The actual code is considerably lighter. The biggest chunk is probably the redirect logic I had to add to handle 307 redirect responses from the Nest API.
var APP_ID = “”; //replace with “amzn1.echo-sdk-ams.app.[your-unique-value-here]”;
var NEST_TOKEN = “”; //replace with auth token from Nest
APP_ID should be your Alexa Skills Application Id from developer.amazon.com
NEST_TOKEN is your access token for the Nest API.
var AlexaSkill = require(’./AlexaSkill’);
var https = require(‘https’);
var url = require(‘url’);
These bring in some necessary dependencies. Alexa skill is a library module (js prototype) for simplifying the interaction model. https is a Node.JS model for making https requests. We’ll use it extensively for calling the Nest API. Url is used for constructing and parsing urls.
var NestSkill = function () {
AlexaSkill.call(this, APP_ID);
};
// Extend AlexaSkill
NestSkill.prototype = Object.create(AlexaSkill.prototype);
NestSkill.prototype.constructor = NestSkill;
This just sets up our skill as a child the AlexaSkill prototype so we can use it’s methods.
NestSkill.prototype.eventHandlers.onSessionStarted = function (sessionStartedRequest, session) {
console.log(“NestSkill onSessionStarted requestId: ” + sessionStartedRequest.requestId
+ “, sessionId: ” + session.sessionId);
// any initialization logic goes here
};
Here we define the session start event. You can initialize things here, or just log startup data here.
NestSkill.prototype.eventHandlers.onLaunch = function (launchRequest, session, response) {
console.log(“NestSkill onLaunch requestId: ” + launchRequest.requestId + “, sessionId: ” + session.sessionId);
var speechOutput = “Say Status, or Set Temperature Upstairs to 65”;
var repromptText = “Say Status, or Set Temperature Upstairs to 65”;
response.ask(speechOutput, repromptText);
};
Here we define the onLaunch event. This is triggered when the skill is invoked. All we can really do here is respond with output. speechOutput is spoken by Alexa. repromptText is used for the card within the Alexa app.
NestSkill.prototype.eventHandlers.onSessionEnded = function (sessionEndedRequest, session) {
console.log(“NestSkill onSessionEnded requestId: ” + sessionEndedRequest.requestId
+ “, sessionId: ” + session.sessionId);
// any cleanup logic goes here
};
This defines the onSessionended event, called when the skill is exited. We don’t do anything other than log here.
NestSkill.prototype.intentHandlers = {
// register custom intent handlers
“StatusIntent”: function (intent, session, response) {
getNestFromServer(function(body) {
console.log(“Status onResponse from nest: ” + body);
var bod = JSON.parse(body);
var responseString = “”;
for (i in bod) {
console.log(i);
var val = bod[i];
console.log(val.name);
console.log(“name”, val.name);
console.log(“target temp”, val.target_temperature_f);
console.log(“current temp”, val.ambient_temperature_f);
responseString += “The temperature ” + val.name + “ is ” + val.ambient_temperature_f + “. The temperature is set to ” + val.target_temperature_f + “. ”;
}
response.tellWithCard(responseString, “Greeter”, responseString);
});
},
“SetTempIntent”: function (intent, session, response) {
var temperatureSlot = intent.slots.temperature;
var temperature = 0;
if (temperatureSlot && temperatureSlot.value) {
temperature = temperatureSlot.value;
}
var thermostatSlot = intent.slots.thermostat;
var thermostat = “”;
if (thermostatSlot && thermostatSlot.value) {
thermostat = thermostatSlot.value;
}
setNestTemperatureFromServer(thermostat, function(body) {
console.log(“SetTemp onResponse from nest: ” + body);
setNestTemperatureOnDeviceFromServer(body.device_id, temperature, function(body) {
console.log(“SetTempDevice onResponse from nest: ” + body);
response.tellWithCard(“Set Temperature ” + thermostat + “ to ” + temperature + “.”, “Greeter”, “Set temperature ” + thermostat + “to ” + temperature + “.”);
});
});
},
“AMAZON.HelpIntent”: function (intent, session, response) {
response.ask(“You can ask me status, or set the temperature on a thermostat”, “You can ask me status, or set the temperature on a thermostat”);
}
};
This function defines each of the intents, and is the largest one, so we’ll break it apart. We’re defining a map struct here with the intent name, corresponding to those we configured in the Alexa skill, and the function invoked for each.
For the StatusIntent, we call the Nest API to get the devices and temperature data, then parse the response for these values and build up a response string for Alexa to say. We pass the same string as the third parameter which is displayed in the card shown in the Alexa companion app.
For the SetTempIntent, we first parse the two slots we need values from, temperature and thermostat name. Then we call the Nest API to set the temperature on this thermostat. On success, we construct a response in a similar way.
Last, the AMAZON.HelpIntent just gives a reponse telling the user what the options for prompts are.
function doRequest(options, eventCallback, requestNo, data) {
console.log(“calling ”, options.path);
if(requestNo > 5) {
console.log(“too many redirects”);
return;
}
var req = https.request(options, function(res) {
var body = “;
var redirect = false;
console.log(“statusCode: ”, res.statusCode);
console.log(“headers: ”, res.headers);
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers && res.headers.location) {
var location = res.headers.location;
console.log(‘redirect’, location);
redirect = true;
var redirectURI = url.parse(location);
console.log(‘redirect URI’, redirectURI);
options = {
hostname: redirectURI.hostname,
port: redirectURI.port,
path: redirectURI.pathname,
method: ‘GET’,
headers: {‘Authorization’ : ‘Bearer ’ + NEST_TOKEN}
};
doRequest(options, eventCallback, requestNo + 1);
} else if (res.statusCode === 401) {
redirect = true;
var authHeader = req._auth.onResponse(res);
if (authHeader) {
req.setHeader(‘authorization’, authHeader);
var location = res.headers.location;
console.log(‘redirect’, location);
var redirectURI = new URI(location);
console.log(‘redirect URI’, redirectURI);
options = {
hostname: redirectURI.hostname,
port: redirectURI.port,
path: redirectURI.pathname,
method: ‘GET’,
headers: {‘Authorization’ : ‘Bearer ’ + NEST_TOKEN}
};
doRequest(options, eventCallback, requestNo + 1);
}
}
res.on(‘data’, function(d) {
body += d;
});
res.on(’end’, function () {
if(body && !redirect) {
eventCallback(body);
} else {
console.log(‘redirectng so not done’);
}
});
});
if(data) {
req.write(data);
}
req.end();
req.on(’error’, function(e) {
console.error(e);
});
}
This method that I am not at all proud of in terms of structure, but am proud of for functionality, performs the actual REST API calls. It’s a recursive function that calls itself if the response has a redirect status code (300-399). It won’t allow more than 5 redirects as the http spec says that it is the responsibility of clients to not redirect too many times. I won’t go through it line by line as it’s mostly tedious https request and response code. Essentially we call the configured service and get a response chunk by chunk. Not all http is delivered in a single response body, so we build up the response object chunk by chunk. If the status code is a redirect, we grab the url in the location header and make another call to that url with the same path and options. We’re also passing the Nest Access token in the Authentication header.
function getNestFromServer(eventCallback) {
var options = {
hostname: ‘developer-api.nest.com’,
port: 443,
path: ’/devices/thermostats/’,
method: ‘GET’,
headers: {‘Authorization’ : ‘Bearer ’ + NEST_TOKEN}
};
doRequest(options, eventCallback, 0);
}
This function is used to get the list of thermostats from Nest. We pass in an event callback which is called with the response data when the API responds.
function setNestTemperatureFromServer(thermostat, eventCallback) {
var options = {
hostname: ‘developer-api.nest.com’,
port: 443,
path: ’/devices/thermostats/’,
method: ‘GET’,
headers: {‘Authorization’ : ‘Bearer ’ + NEST_TOKEN}
};
doRequest(options, function(body) {
console.log(“SetTemp device list onResponse from nest: ” + body);
var bod = JSON.parse(body);
for (i in bod) {
console.log(i);
var val = bod[i];
console.log(val.name);
console.log(“name”, val.name);
console.log(“thermostat”, thermostat);
if(val.name.toUpperCase() === thermostat.toUpperCase()) {
console.log(“matched”, thermostat);
eventCallback(val);
}
}
}, 0);
}
This is pretty similar, but has the logic to find the specified thermostat. This function gets the list of thermostats, but calls the specified callback function with the matching thermostat. Therefore if you ask Alexa to set the temperature “upstairs”, this will find the device details for the “upstairs” thermostat.
function setNestTemperatureOnDeviceFromServer(thermostat, temperature, eventCallback) {
var options = {
hostname: ‘developer-api.nest.com’,
port: 443,
path: ’/devices/thermostats/’ + thermostat,
method: ‘PUT’,
headers: {‘Authorization’ : ‘Bearer ’ + NEST_TOKEN}
};
doRequest(options, function(body) {
console.log(“SetTemp on device onResponse from nest: ” + body);
eventCallback(body);
}, 0, ’{“target_temperature_f”:’ + temperature + ’}’);
}
Lastly, this function calls the Nest API to actually set the temperature. This is using the PUT method with the temperature data. Here we’re passing a JSON string with the data passed to the API, target_temperare_f and the desired temperature.
// Create the handler that responds to the Alexa Request.
exports.handler = function (event, context) {
// Create an instance of the NestSkill skill.
var nestSkill = new NestSkill();
nestSkill.execute(event, context);
};
This last function is just the initialization of the Skill.
Phew, we made it through the code. Try not to judge too much, I just wanted to get it working.
Now that we have the code complete, we need to push this to our Lambda function. Zip the AlexaSkill.js and index.js and upload this zip to your function. You can now test in the browser with the two JSON invocation samples you got earlier. If you are successful, you’ll get back a response with what Alexa would speak and the card details. You should then be able to actually run this on your Echo!
See, I promised it wouldn’t be too hard.
For reference:
Nest API: https://developer.nest.com/documentation/cloud/rest-guide
Alexa Skills SDK and Lambda Setup: https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/testing-an-alexa-skill