Arduino Indoor Outdoor Temperatures

There has been many times that I’ve left the house and noticed the difference in temperature between inside and outside my home. I thought a fun project would be to combine an Arduino reading the inside temperature and the outside temperature.

I had a couple ideas on how to do this. The first involved two Arduinos, one inside and one outside, both using TMP36 sensors to monitor the temperatures. The second was a single Arduino inside that recorded the temperature using the sensor and then using the Open Weather Map API to gain the outside temperature.

Both ideas had some pros and cons but I went for option two. It may not be as accurate as having an outside sensor but I think the data reported by the API is good enough that I could see the difference between inside and outside.

The code for this is available in a GitHub repo.

Parts used

Arduino Uno WiFi

TMP36 Sensor

Jumper wires

You’ll also need

A web and database server running PHP and MySQL. In the code used this is an internal server that does not accept requests from outside of the network. If you are going to use a server open to traffic outside of your network you will need to add extra security measures to ensure that unauthorised requests cannot add data.

Setup

Database

Create a database and then run the SQL that is in the data.sql file. This will create a table that stores the indoor and outdoor temperatures as well as a unique id and a created at date and time.

Data receive page

The first part of the script includes the Composer autoload file, the settings file and then uses Guzzle’s HTTP client that will be used to send a get request to Open Weather Map API.

require __DIR__ . '/vendor/autoload.php';

require './settings.inc';

use GuzzleHttp\Client;

There is then the basic security check for the key parameter in the query string.

if (isset($_GET['key']) && $_GET['key'] === BASIC_KEY) {

We create a connection to the database or output the error message and exit if it was not successful.

  try {
    $db = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME, DB_USER, DB_PASS);
  } catch (PDOException $e) {
    echo $e->getMessage();
    exit;
  }

The next section of code creates a new Guzzle HTTP client with the base uri set to the Open Weather Map API.

We then set the query string parameters to send and perform a GET request.

The response body is first converted to a string and then JSON decoded for easier use with PHP. This decoded response is a PHP object.

We now access the temperature returned by the API call and use this as our outdoor temperature reading.

$client = new GuzzleHttp\Client(['base_uri' => 'api.openweathermap.org/']);

  $params = [
    'query' => [
      'id' => CITY_ID,
      'appid' => API_KEY,
      'units' => 'metric',
    ],
  ];

  $request = $client->request('GET', 'data/2.5/weather', $params);

  $response = json_decode((string)$request->getBody());

  $outdoorTemperature = $response->main->temp;

The last part of the logic inserts a row into the database. To ensure that the temperature recordings are in the correct format we cast them as floats and user the number_format function to round to two decimal places.

  $data = [
    'indoor_temperature' => number_format((float)$_GET['temp'], 2),
    'outdoor_temperature' => $outdoorTemperature,
  ];

  $sql = "INSERT INTO data (indoor_temperature, outdoor_temperature) VALUES (:indoor_temperature, :outdoor_temperature)";

  $stmt= $db->prepare($sql);

  $stmt->execute($data);

After inserting the row into the database we return a 201 success response code to show that the row has been created.

  // Return a 201 created response.
  header('HTTP/1.1 201 Unauthorized');
  exit;

The last part in the else statement returns a not authorised response header if the query string does not contain the key parameter or if the key parameter is incorrect.

  header('HTTP/1.1 401 Unauthorized');
  exit;

Arduino sketch

The first portion of code includes the library for the Arduino Uno WiFi, this allows us to use the Ciao library for sending HTTP requests. We then declare some constants that will be used when sending the request. You will need to replace the SERVER_ADDRESS and KEY to match those used on your web server for receiving the data. Finally we declare our TMP36 sensor input pin to zero.

#include <UnoWiFiDevEd.h>

#define CONNECTOR "rest"
#define SERVER_ADDRESS "YOUR_SERVER_ADDRESS"
#define METHOD "GET"
#define KEY "YOUR_KEY"

int temperaturePin = 0;

Inside the setup function we initialise the Ciao library ready for use.

void setup() {
  Ciao.begin();
}

Inside our loop function we first calculate a five minute interval as this is how often we want to be taking a recording. We then set the last sampled time to zero so we get a recording on the first time in the loop.

  const unsigned long fiveMinutes = 5 * 60 * 1000UL;
  static unsigned long lastSampleTime = 0 - fiveMinutes;

Next we get the number of milliseconds that have passed and check if five minutes has passed since the last recording. If it has then we add five minutes to our last sampled time ready for the next recording.

unsigned long now = millis();
  if (now - lastSampleTime >= fiveMinutes)
  {
    lastSampleTime += fiveMinutes;

Our next few lines of code take the recording from the sensor and convert it to degrees celsius.

    // Getting the voltage temperature reading from the sensor.
    int temperatureReading = analogRead(temperaturePin);
  
    // Converting that temperature reading to voltage, for 3.3v arduino use 3.3.
    float voltage = temperatureReading * 5.0;
    voltage /= 1024.0;
  
    // Temperature in degrees celsius.
    float temperatureC = (voltage - 0.5) * 100;  // Converting from 10 mv per degree wit 500 mV offset to degrees ((voltage - 500mV) times 100).

The last part of code builds up a URI that we send to the server using the Ciao library.

    // Build the GET request URI
    String uri = "/data.php?key=";
    uri += String(KEY);
    uri += "&temp=";
    uri += String(temperatureC);

    // Send the data to the webserver.
    CiaoData data = Ciao.write(CONNECTOR, SERVER_ADDRESS, uri);

Index page and chart

The last piece of code to look at is the index.php file for the web server. This draws a line chart using the indoor and outdoor temperatures.

We first include the settings file and then make a database connection.

require './settings.inc';

// Try to connect to the database.
try {
  $db = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME, DB_USER, DB_PASS);
} catch (PDOException $e) {
  echo $e->getMessage();
  exit;
}

The SQL query gets all of the recordings in the past 24 hours and orders them oldest to newest. We then add all of these results to an array that we will use later to output as JSON.

$query = $db->query("SELECT * FROM data WHERE created_at >= now() - INTERVAL 1 DAY ORDER BY created_at ASC;");

while($row = $query->fetch( PDO::FETCH_ASSOC )){
  $recordings[] = $row;
}

In the head section we set some basic styling for the chart.

  <style>

    body {
      font-family: sans-serif;
      color: #444;
    }

    .line {
      fill: none;
      stroke-width: 3;
    }

    .line__indoor {
      stroke: #ffab00;
    }

    .line__outdoor {
      stroke: #34e823;
    }

    .axis path,
    .axis line {
      fill: none;
      stroke: #000;
      shape-rendering: crispEdges;
    }

    .axis text {
      font-size: 10px;
    }

  </style>

The body has no content, just two script tags. The first tag includes the D3 library.

<script src="https://d3js.org/d3.v5.min.js"></script>

The second script tag has the logic for creating the chart.

We first create a variable to hold the JSON data that we created earlier.

    var recordings = <?php echo json_encode($recordings); ?>;

Then we set some variables for margins, width and height and append an SVG element to the body that will contain the chart.

    // Set variables for margins, width and height.
    var margin = {top: 50, right: 50, bottom: 50, left: 50},
        width = window.innerWidth - margin.left - margin.right,
        height = window.innerHeight - margin.top - margin.bottom;

    // Create an svg element and append it to the body element.
    var svg = d3.select('body').append("svg")
        .attr("width",  width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

The next section does some setup of a time parse in the format returned from our database and then an array of all of the temperatures so we can get the min and max values.

    var timeConv = d3.timeParse("%Y-%m-%d %H:%M:%S");

    var temperatureRange = [];

    // Create an array of all the temperatures so we can get the min and max values.
    recordings.map(function(recording) {
        temperatureRange.push(recording.indoor_temperature);
        temperatureRange.push(recording.outdoor_temperature);
    });

We then create scales for our X and Y axes using time for the X axis and a linear scale for the temperatures on the Y axis.

For the yScale domain I’ve subtracted 4 from the minimum and added 4 to the maximum so the chart had a bit of breathing room above and below the lines. We then append the axis to the chart.

    var xScale = d3.scaleTime().range([0,width]);
    var yScale = d3.scaleLinear().rangeRound([height, 0]);

    xScale.domain(d3.extent(recordings, function(d){
        return timeConv(d.created_at);
    }));

    yScale.domain([parseFloat(d3.min(temperatureRange)) - 4.00, parseFloat(d3.max(temperatureRange)) + 4]);

    // Create the axis.
    var yaxis = d3.axisLeft().scale(yScale);
    var xaxis = d3.axisBottom().scale(xScale);

    svg.append("g")
        .attr("class", "axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xaxis);

    svg.append("g")
        .attr("class", "axis")
        .call(yaxis);

The last part of the code adds the two lines to the chart and gives them slightly different classes so we can colour the lines differently.

    // Create the indoor line.
    var indoorLine = d3.line()
        .x(function(d) {
            return xScale(timeConv(d.created_at));
        })
        .y(function(d) {
            return yScale(d.indoor_temperature);
        })
        .curve(d3.curveMonotoneX);

    svg.append("path")
        .data([recordings])
        .attr("class", "line line__indoor")
        .attr("d", indoorLine);

    // Create the outdoor line.
    var outdoorLine = d3.line()
        .x(function(d) {
            return xScale(timeConv(d.created_at));
        })
        .y(function(d) {
            return yScale(d.outdoor_temperature);
        })
        .curve(d3.curveMonotoneX);

    svg.append("path")
        .data([recordings])
        .attr("class", "line line__outdoor")
        .attr("d", outdoorLine);

Improving

There are a couple of things I’d like to do to improve this.

  1. Security. To allow the data to be sent to a public facing server some more security steps than a basic key should be used.
  2. Allow different timeframes on the chart, e.g. past X hours, days or weeks. For this I’d need to group the data as showing data for every 5 minutes of an hour over 7 days would clutter a chart.

Leave a Reply

Your email address will not be published. Required fields are marked *