Friday, December 5, 2014

Your Internet Thermometer

Photo by Bernard Gagnon
It's amazing what access to the Internet on phones has done to us. When I wake up in the morning, I look at my phone to check the outside temperature. I remember when I was a kid and we had to use antiques like thermometers stuck on the outside of our windows!

In today's comic, the frog is trying to get the body temperature reading from another frog. He fails to realize that frogs, being ectotherms, will basically have the same body temperature as that of their environment. His phone shows him (and you, the reader of the comic) the actual temperature from my town here in central Pennsylvania. Look at the comic today and you'll see today's temperature. Come back in 6 months and you'll see a warmer temperature (I hope). Whatever temperature it is here when you load the page, that's what you'll see.

Getting weather data for my comic is not difficult. I basically do the same thing that the weather app on your phone is doing - accessing a server somewhere that has been collecting data from weather stations around the country and getting the latest information for a certain location. I have used both Weather Underground and OpenWeatherMap to get data, as both have APIs that anyone can use. In the case of my comic, I wrote a simple module for Node that I use to manage the API calls.

var http = require('http');

module.exports = function(conf) {

    var currentTemp = 50;  // start with some reasonable value

    var apiKey = conf.weatherApiKey || "";
    var location = conf.location || "";

    function pullWeatherData(cb) {

        // OpenWeatherMap
        var url = "http://api.openweathermap.org/data/2.5/weather?id="
                + location + "&units=imperial&APPID=" + apiKey;

        // Weather Underground
        //var url = "http://api.wunderground.com/api/" + apiKey
        //        + "/conditions/q/" + location + ".json"; 

        http.get(url, function(res) {

            var wData = "";
            res.on("data", function(chunk) {
                wData += chunk;
            });
            res.on("end", function() {
                try {
                    var data = JSON.parse(wData);
                    cb(null, data);
                } catch (e) {
                    cb(e);
                }
            });

        }).on("error", function(e) {
            cb(e);
        });

    }

    function weatherTimer() {

        pullWeatherData(function(err, data) {
            if (err) {
                console.log(err);
            } else {
                try {

                    // OpenWeatherMap
                    if (data.main.temp) {
                        currentTemp = data.main.temp;
                    }

                    // Weather Underground
                    //if (data.current_observation.temp_f) {
                    //    currentTemp = data.current_observation.temp_f;
                    //}

                    console.log("Setting current temp to " + currentTemp);

                } catch (e) {
                    console.log("Unable to update temp");
                    console.error(e.stack);
                }
            }
            setTimeout(weatherTimer, 1800000); // call again in 30 minutes
        });

    }

    weatherTimer(); // run now, and then every 30 minutes

    return {
        temperature: function() {
            return currentTemp;
        }
    };

};

The goal of this module is to give me access to the current temperature value and update that value automatically on a timer. Temperature doesn't usually change that quickly so every 30 minutes is fine for my application.

There are really only two functions - one that calls the API to get the data and the timer that asks for new data and updates the temperature. In my code above, I show it using the OpenWeatherMap API with the Weather Underground stuff commented-out, in case you want to try that one as well.

The first function I called pullWeatherData. It simply calls http.get for whichever service you want to use and reads the response stream, in JSON format, into a string which is parsed into an object at the end. I had to make sure error conditions were handled gracefully, because when you call external APIs you sometimes get garbage back. That's the point of the try/catch around the JSON.parse call - the response might not actually be JSON (it could be a plain text HTTP 500 page, for example) and you don't want to crash your whole app. So if there is any kind of error, the callback function given to pullWeatherData is called with the error as the one and only parameter. In the case of a successful call and parse, the callback function is called with a null as the first parameter to indicate no errors and the weather data object as the second parameter. The exact format of this object will depend on which service you called.

The weatherTimer function simply calls pullWeatherData and then does something with the data before setting a timeout for calling itself again in 30 minutes. All it really does with the data is take the temperature value out of it and store it in a variable. If everything works, that variable will be automatically updated every half hour.

After defining these two functions, my module calls weatherTimer in order to get the data for the first time and start the timer for the next call. The return value is an object with a function simply called temperature which will return the current value of the temperature variable. Here's an example of how the module would be used in an app:

var express = require("express");
var app = express();

/*
 * OpenWeatherMap uses their own location ids.
 * Id 5184309 is near Reedsville, PA.
 * 
 * Weather Underground can use ZIP codes.
 * ZIP 17084 is Reedsville, PA.
 */
var weather = require("./weather")({
    weatherApiKey: "your_key_here",
    location: "5184309"
});

app.get("/", function(req, res, next) {
    res.send("weather demo");
});

app.get("/temp", function(req, res, next) {
    res.send("current temperature is: " + weather.temperature() + " deg F");
});

// ------------ start listening
var server = app.listen(3000, function() {
    console.log("listening on port %d", server.address().port);
});

Just like the comic, this sample app will tell you the temperature here in Reedsville if you point your browser at the /temp path. As you can see, it's cold in Reedsville as I write this.


If you want, you could modify this module to get different weather data or more data and store it in a more complex object. You could really just store off the entire response object and access different values each time. You might even apply this pattern to other non-weather APIs as well. Be creative!

Amphibian.com comic for 5 December 2014