Michael Svanström

developer, photographer

Raspberry Pi thermometer - online

In part 1 of this series I hooked up a waterproof DS18B20 to my Raspberry pi. For more fun, let’s show our temperature readings to the world!

  1. The raspberry pi runs a tiny Node JS script that reads the temperature from the sensors and submits it to my web server.
  2. The readings are stored in a RethinkDB
  3. database. Another Node JS script (an Express API) returns the temperature to the web site visitors as JSON.

The Pi script:

// read all available sensors and post to API
var ds18b20 = require('ds18b20')
var request = require('request');

var url = 'API_HOST/api/temperature';

function save_temp(id, temp) {
  var postdata = {device_id: id, temperature: temp, date: new Date()};

  request({url: url,
          method: 'POST',
          json: true,
          body: postdata},
          function(err, res, body) {
                  console.log(body);
          });
}

ds18b20.sensors((err,ids) => {
	for (var i in ids) {
		var id = ids[i]
		ds18b20.temperature(id, function(err, val) {
			console.log('Temp ', id, ' ', val)
			save_temp(id, val)
		});
	}
})

The server part. Slightly based on RethinkDB’s Todo example :)

// Import express
var express = require('express');
var bodyParser = require('body-parser');
var app = express();

// Load config for RethinkDB and express
var config = {
  rethinkdb: {
      host: "localhost",
      port: 28015,
      authKey: "",
      db: "thermometer"
  },
  express: {
      port: 8183
  }
};
var r = require('rethinkdb');

app.use(bodyParser());
app.use(createConnection);

// Define main routes
app.route('/api/temperature/latest').get(getLatest);
app.route('/api/temperature').post(create);
app.route('/api/temperature/current').get(getCurrent);

// Middleware to close a connection to the database
app.use(closeConnection);

// get latest observations for all sensors
function getLatest(req, res, next) {
    r.table('observations').group('device_id').max("createdAt").run(req._rdbConn).then(function(cursor) {
        return cursor.toArray();
    }).then(function(result) {
        res.send(JSON.stringify(result));
    }).error(handleError(res))
    .finally(next);
}

// return current temperature and 24h avg/max/min
function getCurrent(req, res, next) {
  r.table('observations')
  .filter(function(doc) {
    return doc('createdAt').gt(r.now().sub(86400))
  })
  .group('device_id')
  .ungroup()
  .eqJoin('group', r.db('temperature').table('sensors')).without({right:'id'}).zip()
  .map(function(doc) { return {
    device_name: doc('device_name'),
    avg: doc('reduction')('temperature').avg().mul(10).round().div(10),
    min: doc('reduction')('temperature').min(),
    max: doc('reduction')('temperature').max(),
    cur: doc('reduction').orderBy(r.desc('createdAt')).limit(1)('temperature')(0)
  }})

.run(req._rdbConn).then(function(cursor) {
        return cursor.toArray();
    }).then(function(result) {
	res.setHeader('Content-Type', 'application/json');
        res.json({'result': result});
    }).error(handleError(res))
    .finally(next);
}

/*
 * Insert
 */
function create(req, res, next) {
    var observation = {date: req.body.date,
		device_id: req.body.device_id,
		temperature: req.body.temperature };
    observation.createdAt = r.now(); // Set the field `createdAt` to the current time
    r.table('observations').insert(observation, {returnChanges: true}).run(req._rdbConn).then(function(result) {
        if (result.inserted !== 1) {
            handleError(res, next)(new Error("Document was not inserted."));
        }
        else {
            res.send(JSON.stringify(result.changes[0].new_val));
        }
    }).error(handleError(res))
    .finally(next);
}

/*
 * Send back a 500 error
 */
function handleError(res) {
    return function(error) {
        res.send(500, {error: error.message});
    }
}

/*
 * Create a RethinkDB connection, and save it in req._rdbConn
 */
function createConnection(req, res, next) {
    r.connect(config.rethinkdb).then(function(conn) {
        req._rdbConn = conn;
        next();
    }).error(handleError(res));
}

/*
 * Close the RethinkDB connection
 */
function closeConnection(req, res, next) {
    req._rdbConn.close();
}

/*
 * Create tables/indexes then start express
 */
r.connect(config.rethinkdb, function(err, conn) {
    if (err) {
        console.log("Could not open a connection to initialize the database");
        console.log(err.message);
        process.exit(1);
    }

    r.table('observations').indexWait('createdAt').run(conn).then(function(err, result) {
        console.log("Table and index are available, starting express...");
        startExpress();
    }).error(function(err) {
        // The database/table/index was not available, create them
        r.dbCreate(config.rethinkdb.db).run(conn).finally(function() {
            return r.tableCreate('observations').run(conn)
        }).finally(function() {
            r.table('observations').indexCreate('createdAt').run(conn);
        }).finally(function(result) {
            r.table('observations').indexWait('createdAt').run(conn)
        }).then(function(result) {
            console.log("Table and index are available, starting express...");
            startExpress();
            conn.close();
        }).error(function(err) {
            if (err) {
                console.log("Could not wait for the completion of the index `observations`");
                console.log(err);
                process.exit(1);
            }
            console.log("Table and index are available, starting express...");
            startExpress();
            conn.close();
        });
    });
});

function startExpress() {
    app.listen(config.express.port);
    console.log('Listening on port '+config.express.port);
}

Making a HTTP GET request to API_HOST/api/temperature/current now hopefully returns something like this:

{"result":[{"avg":1.1,"cur":3.9,"device_name":"Outdoors","max":3.9,"min":-0.5}]}

Note! This simple example has no authentication whatsoever, anyone who knows the url of your API can insert to your database.