A Kynetx Endpoint for Radio Thermostats


Summary

The Internet of Things requires active participants--devices and services that are not just listening, but also talking. I've created a daemon that serves as a framework for linking to multiple connected devices and raising events based on their state.

Last week in a blog post about a Cosm module for KRL, I talked about logging temperature from a thermostat. I even showed the rule that does it:

rule process_temperature {
  select when thermostat new_temperature
  cosm:update("Temperature", event:attr("temperature"));
}

If you know KRL, you realize that for this rule to be called, there has to be something, somewhere, that's raising a thermostat:new_temperature event into the personal event network where this rule is installed. In the world of personal events, things that raise events are called endpoints1. Some devices or services can function as their own endpoint (Twilio, for example), but others, like the Radio Thermostat need a little more help. This post describes a simple endpoint written in Perl that serves as an endpoint for these thermometers.

Radio Thermostat CT-30

Radio Thermostat makes a Wi-Fi enabled thermostat, the CT-30, that is available from Amazon as the CT-30 or from many home improvement stores like Home Depot under the Filterete brand. As far as I can tell, the Filterete branded thermostats are identical and are 20% or so cheaper.

Not only is the CT-30 Wi-Fi enabled, but it has an API (see Advanced Technical Information on the Radio Thermostat site for details). The API is local, meaning that you talk directly to the thermostat, not a cloud service that is intermediating for the thermostat.

For example, going to the URL http://10.0.1.173/tstat returns the following JSON formatted data:

{"temp":73.50,
 "tmode":1,
 "fmode":0,
 ...
 "time":{"day":0,"hour":9,"minute":59},
 "t_type_post":0
}

You can see the current temperature, 73.50, in the temp field. The API provides ways of controlling the thermostat as well as interrogating it.

The Endpoint

Unfortunately, the CT-30's API is like a phone that never rings. You can call it, but it never calls you. That means that something else has to take the initiative. That something is the endpoint. The endpoint is a daemon that runs in the background, interrogating the CT-30 and raising relevant events as well as translating directives from KRL into API calls to the CT-30.

One of the nice things about Perl is that it's been around a long time and so there's a library for almost anything. One of the problems with Perl is that it's been around for a long time and so there are usually a dozen libraries for almost anything. I ran into that problem in looking for ways to write a Perl daemon. I picked, for its seeming simplicity, Mark Overmeer's Any::Daemon.

Since I'd like this daemon to control multiple thermostats (and maybe other things) in my home, I created a configuration file structure that allows me to define multiple devices. The daemon will fork a process for each device, running the correct code depending on the device type. Here's the current configuration file with a single device:

# should be 1 unless you know something I don't
max_children: 1
user: pjw

devices: 
  thermometer_1:  # name
    type: thermometer
    sleep_secs: 60
    host: 10.0.1.173
    eci: cb68f5a0-a787-012f-49f0-0016348af3

Each entry under devices will spawn a daemon of the correct type (in this case 'thermometer'). The host is the IP address for the thermostat and the eci is the event channel identifier for the personal event network channel that the daemon will raise event on for this device. This is important because any given device might raise events to a different personal event network that the others. For example, I envision having a separate personal event network for each HVAC zone in my house.

Just the Temperature, Ma'am!

For now, I'm just raising an event with the current temperature every 60 seconds (the sleep_secs parameter from the configuration shown above). The details of the daemon can be seen in the Github repository. Here, I'll focus on just the code that reads the temperature and raises the event to KRE.

I structured the daemon code so that each device type (see the configuration example) is associated with a Perl function that knows what to do for that kind of device. In this case, I'm treating the thermostat's thermometer independently. The daemon needs a function that takes no parameters, but, at the same time, we want to configure it for each device. The answer to this seeming dilemma is a closure. The function takes configuration parameters and returns another function that encapsulates the behavior for that specific device.

sub run_temperature_task(@) {
  my ($name, $config) = @_;
  warning "No host name for termperature sensor $name" 
    unless $config->{host};
  warning "No event channel identifier for termperature sensor $name" 
    unless $config->{eci};

  my $thermo_url = "http://$config->{host}/tstat";

  my $ua = LWP::UserAgent->new;
  my $req = HTTP::Request->new(GET => $thermo_url);

  my $event_channel = 
         Kinetic::Raise->new('thermostat',
\t \t             'new_temperature',
\t\t\t     {'eci' => $config->{eci}}
\t\t\t    );
  my $sleep_sec = $config->{sleep_secs} || 60;

  return sub {
    info "Starting temperature daemon";
    while(1) {   
      my $res = $ua->request($req);
      if ($res->is_success) {
\tmy $scalar = from_json($res->content);
\tinfo "Temperature is " . $scalar->{ 'temp' } . "\
";
\tmy $response = 
            $event_channel->raise(
             {'temperature' => $scalar->{ 'temp' }}
            );
      }
      sleep $sleep_sec;
    }
    exit 0;
  }
}

The preceding code reads parameters from the configuration file, initializes the HTTP connection to the thermostat and sets up the event channel with a specific ECI to raise a thermostat:new_temperature event. The function that's returned—and eventually run in the process that gets forked by the daemon—loops forever (while(1){...}), reading from the thermostat, raising an event with the current temperature, and then sleeping for 60 seconds before doing it all again. The event is raised using the Kinetic::Raise module I wrote for raising events from Perl.

Adding another CT-30 would be as simple as adding another device to the configuration file and restarting the daemon. Adding other kinds of devices would require writing other functions like the one above that is specific to a given device type. The daemon code provides a framework for running multiple devices of the same or different types.

An Internet of Things Requires Active Participants

By creating an endpoint for the CT-30 we've made it an active participant in the Internet of Things. Before it was attached to the Internet, but largely inert. Because it's raising events, we can write different applications that do different things with the temperature. An obvious example is to start controlling the HVAC system with an app instead of the built-in thermostat code. But we might also want to regulate the blinds or something else based on the temperature in particular parts of the house. Right now, I'm just logging it to Cosm.


1. If you're thinking that this seems backwards from how the word endpoint is used in request-response systems like HTTP, you're right. But then in an evented system the semantic responsibility of the interaction is reversed as well. So it all works out.