What’s involved?

AM2302 is a cheap, but precise temperature and humidity sensor. You can get one from eBay for as little as £5. It has temperature range of -40 to 80 degrees C with 1 degrees C accuracy, and 0-100% humidity with 2-5% accuracy. According to the DHT11, DHT22 and AM2302 Sensors overview. The only difference between DHT22 and AM2302 is AM2302 has a 5.1K pullup resistor.

Connect AM2302 to RPi4

Raspberry Pi GPIO Pinout is useful to identify the pins.

I chose to connect:

    • -> pin 17 (3v3 power)
  • data -> pin 37 (GPIO 26)
    • -> pin 39 (GND)

Install AM2302 driver

The Adafruit_Python_DHT is a simple driver written in C and Python. Although deprecated, it is still good for my need. The more up-to-date driver is the Adafruit_CircuitPython_DHT package, but this one requires Adafruit CircuitPython. Simple is the best. Since I am using Manjaro Arm, it has added benefit of Arch User Repository. Luckily, there is AUR: python-adafruit_dht.

The AUR PKGBUILD is written for armv6h and armv7h. However, as we know, the source code is written in C and Python, which should complie to aarch64 target just fine.

yay python-adafruit_dht

Test AM2302 data reading

Once AM2302 is connected and AM2302 driver is installed, I can begin the testing. As we can see from the source code, the driver needs read & write access to /dev/gpiomem. As a temporary solution, I granted myself RW permission to this device:

sudo setfacl -m u:$USER:rw /dev/gpiomem

Next, I cloned the AM2302 driver repo. It contains example code. The one I am interested in is examples/AdafruitDHT.py. Simply type in the terminal:

./examples/AdafruitDHT.py 2302 26

It should print current temperature and humidity.

Build a database

I plan to take a measurement every 5 seconds, so that this is more than the sampling period (2 seconds) requires for AM2302. This is also frequent enough for me to display a temperature curve throughout the day. The only problem is the huge data it is going to produce. I don’t want to write to the un-reliable micro-SD card in the RPi4. Fortunately, I have a NAS!

MariaDB is a fork of MySQL with some extensions that you only get if you pay for MySQL. PostgreSQL is more advanced, but it suffers some performance issues.

Data produced by RPi4:

  • timestamp
  • temperature
  • humidity

I need to build a table the has 3 coloums, with timstamp as the primary key.

CREATE DATABASE bel_env;
USE bel_env;
CREATE TABLE rpi4_am2302_measurements (
    measurement_time TIMESTAMP PRIMARY KEY,
    temperature FLOAT not null,
    humidity FLOAT not null
);

CREATE USER 'USER'@'ip' IDENTIFIED BY 'password';
GRANT ALL ON bel_env.* to 'USER'@'ip' IDENTIFIED BY 'password' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EXIT;

The RPi4 does not have a Real Time Clock, it requires time update every time it boots up. This leaves a period where timestamp cannot be accurately created. Fortunately, the TIMESTAMP data type in MariaDB will default to CURRENT_TIMESTAMP when value is not supplied.

Change bind address of MariaDB server from 127.0.0.1 to 0.0.0.0.

I read the How to install Mariadb on Ubuntu 20.04, and How to enable Remote access to your MariaDB/MySQL database

Store temperature measurements in the database

measure.py

#!/usr/bin/python
import time
import getpass
import Adafruit_DHT
from mysql.connector import connect, Error

sensor = Adafruit_DHT.AM2302
pin = 26

try:
    with connect(
            host="ip",
            user="USER",
            password=getpass.getpass(),
            database="bel_env"
            ) as connection:
        with connection.cursor() as cursor:
            while True:
                # Try to grab a sensor reading.  Use the read_retry method which will retry up
                # to 15 times to get a sensor reading (waiting 2 seconds between each retry).
                humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)
                cursor.execute(
                        "INSERT INTO rpi4_am2302_measurements(temperature, humidity) VALUES (%(temp)s, %(hum)s)",
                        {
                            'temp': "{:.1f}".format(temperature),
                            'hum': "{:.1f}".format(humidity)
                            }
                        )
                connection.commit()
                time.sleep(4)
except Error as e:
    print(e)

temp-mon.service

[Unit]
Description = Measure temperature and upload to DB
Requires = network-online.target
After = network-online.target

[Service]
User = temp-mon
DynamicUser = yes
StandardInput = file:/opt/rpi4-temp/db-password
ExecStartPre = !/usr/bin/setfacl -m u:temp-mon:rw /dev/gpiomem
ExecStart = /opt/rpi4-temp/measure.py
ExecStopPost = !/usr/bin/setfacl -x u:temp-mon:rw /dev/gpiomem

[Install]
WantedBy = multi-user.target

Visualise data

See Tutorial: Simple line graph with v4

get-temp.php

<?php
$username = "user";
$password = "pw";
$host = "IP";
$database = "bel_env";

$urlQueries = array();
parse_str($_SERVER['QUERY_STRING'], $urlQueries);

$startTime = htmlspecialchars($urlQueries['start_time']);
if (array_key_exists('end_time', $urlQueries)) {
  if (!empty($urlQueries['end_time'])) {
    $endTime = htmlspecialchars($urlQueries['end_time']);
  }
  else {
    $endTime = date("Y-m-d\TH:i:s");
  }
}
else {
  $endTime = date("Y-m-d\TH:i:s");
}

$dbLink = new mysqli($host, $username, $password, $database);

$queryString = sprintf("
SELECT * FROM rpi4_am2302_measurements
WHERE measurement_time >= '%s' AND measurement_time <= '%s';
",
  $dbLink->real_escape_string($startTime),
  $dbLink->real_escape_string($endTime)
);

$res = $dbLink->query($queryString);

$data = array();

for ($i=0; $i < $res->num_rows; $i++) {
  $data[] = $res->fetch_assoc();
}

echo json_encode($data);

$dbLink->close();
?>

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="https://d3js.org/d3.v4.min.js"></script>
	<style> /* set the CSS */
		.temp-line {
			fill: none;
			stroke: coral;
			stroke-width: 2px;
		}
		.hum-line {
			fill: none;
			stroke: blue;
			stroke-width: 2px;
		}
	</style>
</head>
<body>
  <p>
  start time: <input type="datetime-local" id="startTime">
  </p>
  <p>
  end time: <input type="datetime-local" id="endTime">
  </p>
  <button onclick="submit_datetime()">Submit datetime</button>
  <div id="temp-graph"></div>
<script>
	var margin = {top: 20, right: 20, bottom: 30, left: 50},
			width = 960 - margin.left - margin.right,
			height = 500 - margin.top - margin.bottom;

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

  var x = d3.scaleTime().range([0, width]);
  var temp = d3.scaleLinear().range([height, 0]);
  var hum = d3.scaleLinear().range([height, 0]);

  var tempLine = d3.line()
    .x(function(d) {return x(d.measurement_time);})
    .y(function(d) {return temp(d.temperature);});
  var humLine = d3.line()
    .x(function(d) {return x(d.measurement_time);})
    .y(function(d) {return hum(d.humidity);});

  const svg = d3.select("#temp-graph").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 + ")");
  function submit_datetime() {
    const startTime = document.getElementById("startTime").value;
    const endTime = document.getElementById("endTime").value;
    const stQStr = "start_time="+startTime;
    const etQStr = "end_time="+endTime;
    d3.json("get-temp.php?"+stQStr+"&"+etQStr, function(error, data) {
        if (error) throw error;
        data.forEach(function(d) {
            d.measurement_time = parseTime(d.measurement_time);
        });
        x.domain(d3.extent(data, function(d) {return d.measurement_time; }));
        temp.domain([d3.min(data, function(d) {return d.temperature;}), d3.max(data, function(d) {return d.temperature;})]);
        hum.domain([d3.min(data, function(d) {return d.humidity;}), d3.max(data, function(d) {return d.humidity;})]);

        svg.append("path")
        .data([data])
        .attr("class", "temp-line")
        .attr("d", tempLine);

        svg.append("path")
        .data([data])
        .attr("class", "hum-line")
        .attr("d", humLine);
        
        svg.append("g")
        .attr("transform", "translate(0,"+height+")")
        .call(d3.axisBottom(x));

        svg.append("g")
        .call(d3.axisLeft(temp));

        svg.append("g")
        .call(d3.axisRight(hum));
    });
  }
</script>
</body>