I recently replaced the thermostat in our new house with a Filtrete 3M-50. This thermostat has a wifi module and is available at your local Home Depot for $99. I’d been aware of it for a while but had planned on purchasing the $250 nest. The turning point for me came one even as I was going to bed. I was hot, the heat was running and I chose to turn the fan on instead of going downstairs and adjusting the thermostat. In my defense, I knew the thermostat would switch to night mode in another 45 minutes and reduce the set point but I was still wasting energy.

Comparing the Filtrete to the nest the main features you are missing out on are:

  • automatic occupancy detection
  • humidity detection
  • automatic schedule learning

Automatic scheduling is a big win for the average person but our schedule isn’t static enough for this to be much benifit to us. The location of our thermostat is about the best place for a thermostat, but not for occupancy detection so this feature isn’t missed. I’m a bit sad with the loss of humidity detection, the next model up Filtrete has it for $200 but it’s not a deal breaker.

The real win for geeks with a WIFI thermostat isn’t the iPhone or Android Apps, it’s the ability to interface with it and do geeky things. The Filtrete 3M-50 is a rebadged Radio Thermostat Company of America CT30. Radio Thermostat has the API documentation hidden on their website in the latest news section. Scroll to the bottom of the page and click the ‘I Accept’ checkbox. There is also a 3rd party wiki with API documentation and sample code.

I started with the poller.pl script available on the wiki to go from nothing to something quickly. There’s a lack of error checking in it and the non CPANed Radio::Thermostat module available on that page but it gets you up and running quickly.

After this, I wrote a simple script to tweet daily thermostat information.


use 5.010;
use strict;
use warnings;

use RRDs;
use DateTime;
use Net::Twitter;
use Mojo::UserAgent;
use List::Util 'sum';

my $runtime  = get_runtime_string();
my $averages = get_averages_string();

die unless $runtime && $averages;

my $tweet = "Yesterday $averages with $runtime.";
say $tweet;

my $consumer_key    = '';
my $consumer_secret = '';
my $token           = '';
my $token_secret    = '';

my $thermostat_url  = '';
my $rrd_path        = '/home/michael/srv/therm/poller/data/temperature.rrd';

my $nt = Net::Twitter->new(
    traits   => [qw/OAuth API::REST/],
    consumer_key        => $consumer_key,
    consumer_secret     => $consumer_secret,
    access_token        => $token,
    access_token_secret => $token_secret,


sub get_runtime_string {
    my $ua = Mojo::UserAgent->new;
    my $res = $ua->get($thermostat_url)->res;

    die "Didn't get runtimes for yesterday" unless $res;

    my $data = $res->json;
    return build_runtime_string('heat', $data->{ yesterday } )
        || build_runtime_string('cool', $data->{ yesterday } )
        || "no heat or A/C used";

sub build_runtime_string {
    my ($mode, $data)  = @_;
    my $times = $data->{ $mode . '_runtime' };
    return unless $times->{ hour } || $times->{ minute };
    return sprintf '%sing runtime of %i:%02i',
        $mode, $times->{ hour }, $times->{ minute };

sub get_averages_string {
    my $yesterday = DateTime->now()->subtract( days => 1);

    my $start_of_day = DateTime->new(
        year    => $yesterday->year,
        month   => $yesterday->month,
        day     => $yesterday->day,
        hour   => 0,
        minute => 0,
        second => 0,
        time_zone => 'America/New_York'

    my $end_of_day = $start_of_day->clone->add( hours => 24 );

    my ($start,$step,$names,$data) = RRDs::fetch (
        '-s ' . $start_of_day->epoch,
        '-e ' . $end_of_day->epoch

    if (my $error = RRDs::error) {
        die "Error reading RRD data: $error";

    my (@ext_temp, @int_temp);
    for my $row (@$data) {
        next unless grep { $_ } @$row;
        my ($outside, $inside) = @$row[2, 5];
        push @ext_temp, $outside;
        push @int_temp, $inside;

   return  sprintf "averaged %.1fF outside and %.1fF inside",
        average( \@ext_temp), average(\@int_temp);

sub average {
    my $list = shift;
    return sum(@$list) / @$list;

Tweets look like:

  • Yesterday averaged 59.5F outside and 67.9F inside with no heat or A/C used.
  • Yesterday averaged 64.2F outside and 70.1F inside with heating runtime of 0:17.
  • Yesterday averaged 58.9F outside and 69.0F inside with heating runtime of 0:31.

You can follow my house on Twitter as @mikegrbs_house.