nest_cooling-low-resI think the Nest Thermostat is beautiful. Just a lovely piece of hardware that solves a problem nobody knew they had. (Nest did the same with their new “Protect” smoke alarm, btw.) Unfortunately, the Nest was released just a couple weeks after I dropped a fair bit of money on a couple WiFi thermostats from Radio Thermostat. These are nice and all, but they just aren't “shiny” like Nests are. And just as importantly to me, they lack Nest's reporting features. So I figured I'd build some of those features for myself.

I've been wanting to write a command-line node program for a while, and I've been wanting to try JSforce for just as long. So why not write a node app to get information from my thermostats using their publicly-available API and send that info to Salesforce, where I can do all kinds of neat things? That's just what I'm going to do, so let's get started.

Architecture

Before we dive in, let's go over the architecture. As opposed to the Nest, whose API is served up via their servers (which in turn communicate with the local thermostats via a private API), the radstat thermostats are individually addressable: You can GET and POST straight to them, getting current statistics, and even modifying their programs. With that in mind, we'll be building out this infrastructure:

rtstatforce Architecture

I've written an npm package called “rtstat” which can be installed via the regular “npm install” flow. (No need to do that yet, though, as it'll automatically be installed later.) Rtstat implements a large subset of the radio thermostat API. At the time of this post, only the read portion is implemented, as that's all I needed for my project. I plan on implementing the write parts as well, so programming the thermostats can be accomplished via node.

A node script, which I call rtstatforce.js, running on a headless Mac Mini in my case, grabs information from whatever thermostats it can find, formats that information in a JSON package, and sends it up to a custom Apex REST endpoint via the very nice JSforce node package. On Salesforce, the data is saved as a Thermostat__c record (only once per thermostat, uniquely identified by its UUID), and as a new Reading__c record, which will contain things like current indoor and outdoor temperature.

Setup

To get this all set up, we need to prepare a couple things: a new Connected App on Salesforce, and a developer account at Forecast.io (which we use to grab current conditions.) Our Connected App will allow JSforce to authenticate and connect via OAuth 2, so we never have to do messy things like store usernames and passwords. Create your Connected App as usual, and configure it to look like this:

RTerm App

Ignore the Web, Mobile, and Canvas App settings on the setup page, and tick the “Enable OAuth Setting” checkbox to fill in the callback URL (http://localhost:8085) and your selected OAuth scopes (to match those in the image above). Once you're done, click save, and you'll be redirected to a page that looks like the image above, containing your new consumer id and consumer secret. You'll need those, along with the callback URL, a bit later.

Next, go to the Forecast.io developer site, and create an account. This'll get you an API key for their service. We'll need this key later, as well.

Finally, we'll need two new Salesforce objects. First, create a new Thermostat__c object, with the following field configuration:

Thermostat

Then create a Reading__c object:

Reading

I guess I should mention that you'll need nodejs (and npm) to run the code we're going to write. It's pretty straightforward to get set up, but hit me up on twitter if you need any guidance.

Code

All of the code for this project can be found on github, so grab it and follow along.

The most interesting part of the project is actually the authorization flow via JSforce. We're going to use the Connected App we built, above, to handle all the Salesforce authorization. JSforce can implement any number of auth flows, but we're going to use the OAuth2 flow. Since we're a command line app, and not a browser, client-side app, though, we're going to have to think a little outside the box. What we're going to do is fire up a browser from the command line, and send the user to Salesforce's authorization page (which JSforce nicely provides for us). Then we're going to build a temporary server to listen on the callback Url we provided when we described the Connected App. Check it out:

// Open up the user's browser and send them to the auth page for their SF instance (jsforce nicely provides this URL for us)
open(oauth2.getAuthorizationUrl({ scope : 'api id visualforce refresh_token' }));

// At the same time, set up a server to get the callback from SFDC....
var oAuthServer=http.createServer(function (req, res) {
	res.writeHead(200, { 'Content-Type': 'text/plain' });
	res.end("");

// Grab the query string....
	var url_parts = url.parse(req.url, true);
	var query = url_parts.query;
// Now turn around and call back out to SFDC with our temporary token....
	var conn = new jsforce.Connection({ oauth2 : oauth2 });
	conn.authorize(query.code, function(err, userInfo) {
		if (err) { return console.error(err); }
// And save the results so we can easily log in in the future
		localStorage.setItem('accessToken', conn.accessToken);
		localStorage.setItem('refreshToken', conn.refreshToken);
		localStorage.setItem('instanceUrl', conn.instanceUrl);
		console.log("IAuthorization complete. rtstatforce is ready to use\n");
	});
	oAuthServer.close();
	req.connection.end();
	req.connection.destroy();
}).listen(8085, "127.0.0.1");

Recall that we set the callback Url to http://localhost:8085. You can see in the code above that we've set up a server to listen on 127.0.0.1 (localhost) port 8085. It'll extract the “code” url parameter (which Salesforce provides during the callback), and turn right back around and exchange it (again via a Url that JSforce gives us for free) for a permanent access and refresh token. We save these to the local filesystem.

On a side note, I've saved all of the necessary data via a package called “localStorage” which emulates the browser localStorage interface. THIS IS A BAD IDEA if you were using this code for real, important data. I chose this route because it was simple, and the point of this article isn't secure data storage techniques. Don't do this if you plan to implement for real. Use a database, or some other form of secure data storage.

Once everything is authorized (a one-time-only thing), we can use the rtstat package to talk with the thermostats. An interface on rtstat, called “findThermostats()” returns a promise that'll eventually resolve to an object with thermostat uuids as keys and tstat objects as values. These tstat objects are what we use to communicate directly with the radio thermostat.

The “findThermostat” method is pretty cool, though, and worth digging in to a bit. It uses a variation of “Simple Service Discovery Protocol” (ssdp) to ask thermostats to identify via the multicast channel. (This is all described in the thermostat spec linked above.) Our code broadcasts the discovery message on this channel, then turns around and listens for a couple seconds for any responses:

var Discover=function() {
  var addresses=[];
  var socket = dgram.createSocket('udp4')

  var deferred = Q.defer();

  socket.on('error', function (err) {
    deferred.reject(new Error(err));
  })

  socket.on('message', function onMessage(msg, rinfo) {
	 addresses.push(rinfo.address);
  })

  socket.on('listening', function onListening() {
// When we're ready to start listening, let's add ourselves to the multicast "channel", so we can
// hear the response our thermos might send out
    socket.addMembership('239.255.255.250')
    socket.setMulticastTTL(1)
  	setTimeout(function(){
  		socket.close();
  		deferred.resolve(addresses);
  	},2000);
  })

// Send out a message to the multicast ip address... Anyone out there will respond and our listeners above
// will hear
  var message = new Buffer('TYPE: WM-DISCOVER\r\nVERSION: 1.0\r\n\r\nservices: com.marvell.wm.system*\r\n\r\n')
  socket.send(message, 0, message.length, 1900, '239.255.255.250', function (err, bytes) {
  })

  return deferred.promise;
}

At the very bottom there is our broadcast. The “message” is a custom string that the thermostats listen for (and, I found out after many hours of multicast futility, what makes all this “not quite” ssdp.) We send that message out, and the listeners we put in place above collect the response, extract the thermostat's ip address, and returns an array of those addresses. The “findThermostats” interface, which calls this Discover method, then packages the ip addresses into our uuid/tstat object.

The tstat object has several interfaces which implement specific parts of the thermostat API. For example, tstat.sys() gives basic information about the thermostat (like ip address, uuid, etc), while tstat.tstat() gives current information, like temperature set points, current temperature, etc. All of these interfaces return promises, which eventually resolve to objects containing the returned information.

Our rtstatforce.js script calls the needed tstat methods, packages the data up into a master object (along with data from forecast.io, for weather), and POSTs that object to a custom Apex REST endpoint, using JSforce:

// Make a big ole object to send up
var postData={tsys: tsys, tname: tname, ttstat: ttstat, tttemp: tttemp, weather: weatherData};

// And POST to our Apex REST endpoint
conn.apex.post("/Thermostat/", postData, function(err, res) {
  if (err) { return console.error(err); }
  console.log("response: ", res);
});

Our REST endpoint is a pretty simple one: it unpackages the JSON, and looks at the thermostat fields, and decides whether this is a new thermostat, or an existing one (based on the uuid). If it's new, it creates a new Thermostat__c record. The endpoint then looks at the current conditions fields, and makes a new Reading__c record, specifying our Thermostat__c record as the parent.

Running the Script

After cloning the github repo, cd into the cloned directory, and execute “npm install” to get all the dependencies.

Next, create a new Apex class called “RESTThermo.cls” in Salesforce, and copy the contents of the same-named file into it. Compile the code up to Salesforce. You can safely delete the .cls file from the node directory, as it's obviously not needed anymore.

Before running the script, you'll want to change the local directory we're using for localStorage. In the “rstatforce.js”, “jsfOAuth.js”, and “clientSetup.js” files, look for the path currently set to “/Users/matt/.rtstatforce” and change it to something that works for your operating system and user.

To execute the script, simply type “node rtstatforce.js” at the command line. The first time we run it, it'll collect information from us, like our consumer id and secret from our Connected App, as well as our forecast.io API key. Then, it exits. Yup, you read that right, the program exits here, which highlights a shortcoming of node for command-line apps. Its asynchronous nature means that waiting from prompted input from the user doesn't block, and the script will keep on executing whatever is next while the prompt is active. In our case, this would mean that the script would try to grab thermostats and callout to Salesforce, all before we entered our first piece of information at the command line.

We get around this by exiting the program after invoking the prompt code. This doesn't provide a first-class user experience, and if the setup flow were more than a one time thing, I'd look in to something like calling the script from itself after the user successfully enters their information, but in this case, I let it be.

Anyway, after entering our required information and exiting, we restart (again with “node rtstatforce.js”), and wait for it to pop open our browser to start the authorization flow. Logging in to the correct Salesforce org, we grant permission to rtstatforce. Then, after saving the access and refresh tokens, our script just”¦. stops again.

You know the drill, fire it up again (“node rtstatforce.js”) and this time, it'll silently find any thermostats, and push their information up to Salesforce. See for yourself: there should be a new Thermostat__c record and Reading__c child in your org.

Wrapping Up

So now we've got a script that'll silently grab thermostat data and send it up to Salesforce. I've got mine running via a cron job every 5 minutes, accumulating a lot of data about my house's environment. I'm going to let things set and simmer here for a couple weeks while I build up some data, and I'll come back with a post about some of the neat things we might be able to do with that data.

A couple notes:

  • I found the discovery process to be a bit sketchy. 90% of the time, the discovery flow correctly found both of my thermostats, but the other 10%, it would miss one or both of them. I might suggest assigning your thermostats permanent ip addresses, and modifying the rtstatforce.js script to create tstat object manually (via new Tstat(ipAddress), a flow which can be seen in the index.js file).
  • There are a couple different variations of the Radio Thermostat out there, and word is the API is pretty loosely implemented in each one. If you have problems with anything, get in touch with me, and we'll see if we can figure out if your thermostat might be the culprit (because it's certainly not my code!)