DHT22 WiFi (ESP8266) temperature/humidity loggers (#P7)

Guess who was busy and also did not prepare a post in advance? Yeah, lazy me :mrgreen:

Well, let’s pick up some project that I’m deploying slowly around my flat. I intend to run five of these devices, one with some light meter and maybe other outdoor sensors, the others will be indoors. I’ve been running the early prototype for quite some while now, doubling as a Blynk gateway to some LED strips in the liquor cabinet (yeah, I called it Boozy McBoozeface for a reason). Accessing lights at home from far away isn’t that important to me, so once I have time to install some more touch buttons, that one will be no longer running Blynk, probably eliminating some timing/connectivity issues.

The second device is what I initially intended to deploy. The DHT22/AM2302 sensors (1.98€ each) have been removed from their boards (I bought them without the PCB which is cheaper – they still came mounted to them). Why not make a compact design and piggyback them onto the WiFi module can?

I’ll tell you why – because it’s a bad idea in terms of accuracy. It generally works without a problem, WiFi isn’t compromised by something sitting relatively close to the PCB antenna, and the sensor also does not have any problems sitting near the 2.4GHz source. But the ESP8266 consumes power – not much, but enough to be a problem. During power-up it’s around 400mW (says the USB power meter from WHL#17), and around 120mW after that, depending on WiFi activity. It’s also not consumed entirely by the stuff inside the metal can, there’s a CH340 sitting on the other side of the board as a USB-to-serial converter, and there are some passives and transistors around that will draw another few mW as well. But all in all, there is some heat transferred to the temperature sensor, and that’s an issue:

Yup, that’s temperature data from that exact arrangement. I can interchange positions, but that doesn’t do much. I maybe have to add that the white surface in the background is the side of the freezer, which doesn’t have a traditional radiator in the back, but rather uses the left and right wall for heat exchange. So the periodic wiggle is the freezer turning on – offset by the fact that one ESP8266 unit heats the sensor from beneath and the other doesn’t. I’m actually too lazy to calculate the cycle time from that data, but that’s certainly possible.
Of course temperature offsets will screw up humidity readings as well. After all, this is relative humidity, and the ability of air carrying water vapour is strongly depending on temperature. So, surprise, surprise, humidity readings look like that (note the inversion of the graphs – obviously lower readings on the hotter device):

Well, 50% rH would be too good to be true in this close and hot summer weather anyway, especially in the kitchen…

Side note: See the spike near the date change to 20th of June? That was opening the dishwasher a few minutes after it was done. Instant +10% rH, even when it’s no longer steamy hot – never expected that. Cooking on the other hand doesn’t do that much. Of course it will if you prepare a five-course meal for your extended family, but getting a little food ready for yourself doesn’t do all that much.

So, can we just offset readings and recalculate humidity to keep the small form factor? (double-wide plot to offer some more detail)

Probably not. The 4.7K median offset (huge, if you ask me?) is enough to lift everything out of the linear region. Black-body radiation and the T⁴ factor, damn physics.

So I have to make the units into a different shape…but that’s material for another post.

What about the backend?

This is the MariaDB table that holds all data:

Currently running on the RasPi Zero that controls my SK6812 lighting under the bed – that’s always on anyway and connected to WiFi, so why not use it as a database server?

This is the ESP8266 client code

#include
#include "DHT.h"
const char* ssid = "Zeitumkehrinvarianz2G";
const char* password = "fuckyouverymuchthisisnotmyWPAkey";
const char* host = "192.168.2.96";
String getparam = "";
#define DHTPIN 5 // What digital pin we're connected to
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

unsigned long timenow;

void setup() {
Serial.begin(115200);
delay(10);
Serial.println();
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);

while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}

Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());

dht.begin();
}
void loop() {
sendSensor();
delay(9876);
delay(9876);
delay(9876);
}

void sendSensor()
{
float h = dht.readHumidity();
float t = dht.readTemperature(); // or dht.readTemperature(true) for Fahrenheit

timenow = millis();
if (isnan(h) || isnan(t)) {
Serial.println("Failed to read from DHT sensor!");
Serial.println(h);
Serial.println(t);

return;
}else{
Serial.print(timenow); Serial.print(": ");
Serial.print(h); Serial.print("% rH, ");
Serial.print(t); Serial.println("°C");
}

WiFiClient client;
getparam = "GET /adddata.php?loc=sz&hum=";
getparam += h;
getparam += "&temp=";
getparam += t;
getparam += " HTTP/1.1\r\n";
if (client.connect("192.168.2.96", 80)) {
client.print(getparam);
client.print("Host: 192.168.2.96\r\n\r\n");
client.print("User-Agent: ESP8266/Blynk\r\n");
}
}

Quite a lot of that is code from the Arduino examples. I found out the HTTP stuff myself – that was included in other examples as well. If the formatting isn’t correct, the server will not accept the requests and the data packets will be discarded. Luckily, if you write to a database on success, that’s pretty easy to figure out.

As mentioned earlier, there’s some timing issue with writing to the database, especially for the Blynk client. So this one waits three times 9.876 seconds and therefore should almost always send two data packets per minute to the server – which does work. If I do that once per minute (with a little more buffer for clocks that are way off), I get blanks in the database. Duplicates are discarded/updated, as I “only” hold one value per minute.

Which leads me to the server-side code:

<?php
if(isset($_GET['loc'])){
if(isset($_GET['hum'])){
if(isset($_GET['temp'])){
$location = $_GET['loc'];
$humidity = $_GET['hum'];
$temperature = $_GET['temp'];

echo $location;
echo $humidity;
echo $temperature;
}else{trigger_error("No temperature given", E_USER_ERROR);}
}else{trigger_error("No humidity given", E_USER_ERROR);}
}else{trigger_error("No location given", E_USER_ERROR);}

$servername = "localhost";
$username = "nope";
$password = "double-nope";
$dbname = "weather";

$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}

$t = time();
$timedb = $t - ($t % 60);
$col1 = "hum_".$location;
$col2 = "temp_".$location;

$sql = "INSERT IGNORE INTO weatherdata (time)
VALUES (FROM_UNIXTIME($timedb))";
if ($conn->query($sql) === TRUE) { } //else { echo "Error: " . $sql . "<br>" . $conn->error;}

$sql2 = "UPDATE weatherdata SET
$col1 = $humidity,
$col2 = $temperature
WHERE time = FROM_UNIXTIME($timedb)";

if ($conn->query($sql2) === TRUE) { } //else { echo "Error: " . $sql . "<br>" . $conn->error;}

$conn->close();
?>

Yup, you probably can do a lot of crap with that information – but you will need to be in my network. Aaand that’s probably not going to happen, so I’m fine with that distant possibility. Sure, you can bullet-proof the inserts, double-check the values (ranges, types), but what’s the point…I’m not up for commercial deployment of this makeshift system.

And here’s how some dandy HighCharts script will read the last 1000 data points in JSON format:

<?php
header("Access-Control-Allow-Origin: *");
header("content-type: application/json");
if(isset($_GET['loc'])){
$location = $_GET['loc'];
}else{trigger_error("No location given", E_USER_ERROR);}

$servername = "localhost";
$username = "nope";
$password = "I_said_nope?";
$dbname = "weather";

$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$plimit = 1000; // sql limit
$arr_time = array();
$arr_hum = array();
$arr_temp = array();
$colquery1 = "hum_".$location;
$colquery2 = "temp_".$location;
if ($result = $conn->query("SELECT UNIX_TIMESTAMP(time) as time, $colquery1 as hum, $colquery2 as temp FROM weather.weatherdata ORDER BY time DESC LIMIT $plimit;")) {
while($row = $result->fetch_array(MYSQLI_ASSOC)) {
$arr_time[] = $row['time']*1000;
$arr_hum[] = $row['hum'];
$arr_temp[] = $row['temp'];
}
$y = sizeof($arr_time);
echo "[";
for ($x=0; $x<($y-1); $x++){
echo "[".$arr_time[$x].",";
if(is_null($arr_hum[$x])){echo "null";}else{echo $arr_hum[$x];}
echo ",";
if(is_null($arr_temp[$x])){echo "null";}else{echo $arr_temp[$x];}
echo "],";
}
echo "[".$arr_time[$x].",";
if(is_null($arr_hum[$x])){echo "null";}else{echo $arr_hum[$x];}
echo ",";
if(is_null($arr_temp[$x])){echo "null";}else{echo $arr_temp[$x];}
echo "]]";
}

$result->close();
$conn->close();
?>

And this is the HighCharts HTML file itself that does the display:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>Highcharts data from JSON Response</title>
<style>
body{
margin-top: 30px;
margin-left:40px;
}
</style>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="http://code.highcharts.com/highcharts.js"></script>
<script type="text/javascript">

var seriesOptions = [],
seriesCounter = 0,
names = ['BAD', 'WZ', 'KUE', 'SZ', 'OUT'];
var processed_json1 = new Array();
var processed_json2 = new Array();

$.each(names, function (i, name) {
$.getJSON('http://192.168.2.96/getdata2.php?loc=' + name.toLowerCase(), function (data) {

processed_json1[i] = [];
processed_json2[i] = [];
seriesOptions[i] = {
name: name,
data: data
};

for (ii = 0; ii < data.length; ii++){
processed_json1[i].push([data[ii][0],data[ii][1]]);
processed_json2[i].push([data[ii][0],data[ii][2]]);
}

;
// As we're loading the data asynchronously, we don't know what order it will arrive. So
// we keep a counter and create the chart when all the data is loaded.
seriesCounter += 1;

if (seriesCounter === names.length) {
createChart();
}
});
});

function createChart() {

$('#container').highcharts({
chart: {
type: "spline"
},
title: {
text: "Logged temperature/humidity data"
},
xAxis: {
type: 'datetime',
dateTimeLabelFormats: {
day: '%e of %b'
},
allowDecimals: true,
title: {
text: ""
}
},

yAxis: [{
title: {
text: "Temperature [°C]"
}
}, {
title: {
text: "rel. humidity [%]"
},
opposite: true
}],
tooltip: {
crosshairs: [true,true],
xDateFormat: '%b %e %Y, %H:%M',
},
series: [{
name: 'Humidity @ Bathroom ',
data: processed_json1[0],
yAxis: 1,
tooltip: {
valueSuffix: '%'
}
},
{
name: 'Humidity @ Living Room',
data: processed_json1[1],
yAxis: 1,
tooltip: {
valueSuffix: '%'
}
},
{
name: 'Humidity @ Kitchen',
data: processed_json1[2],
yAxis: 1,
tooltip: {
valueSuffix: '%'
}
},
{
name: 'Humidity @ Bedroom',
data: processed_json1[3],
yAxis: 1,
tooltip: {
valueSuffix: '%'
}
},
{
name: 'Humidity @ Patio',
data: processed_json1[4],
yAxis: 1,
tooltip: {
valueSuffix: '%'
}
},
{
name: 'Temperature @ Bathroom ',
data: processed_json2[0],
tooltip: {
valueSuffix: '°C'
}
},
{
name: 'Temperature @ Living Room',
data: processed_json2[1],
tooltip: {
valueSuffix: '°C'
}
},
{
name: 'Temperature @ Kitchen',
data: processed_json2[2],
tooltip: {
valueSuffix: '°C'
}
},
{
name: 'Temperature @ Bedroom',
data: processed_json2[3],
tooltip: {
valueSuffix: '°C'
}
},
{
name: 'Temperature @ Patio',
data: processed_json2[4],
tooltip: {
valueSuffix: '°C'
}
}]
});
};

</script>
</head>
<body>

<div id="container" style="height: 700px"></div>

</body>
</html>

Yeah, HTML code in WordPress is a PITA.

So every client gets a unique identifier in the “GET /adddata.php?loc=” line, that’s created as a column in the database, and the HighCharts script will pull that individually for temperature and humidity. As I said, I will add a light sensor to the outdoor unit, so that’ll require some expansion in the database. But that’s also another blog post. This one is good (and large) enough for release already… ;)


3 Responses to DHT22 WiFi (ESP8266) temperature/humidity loggers (#P7)

  1. John says:

    Hi. Could you send the database structure please? So we could reproduce it

    • Bzzz says:

      Like this?
      -- phpMyAdmin SQL Dump
      -- version 4.6.6deb4
      -- https://www.phpmyadmin.net/
      --
      -- Host: localhost:3306
      -- Generation Time: Jul 12, 2018 at 08:08 PM
      -- Server version: 10.1.23-MariaDB-9+deb9u1
      -- PHP Version: 7.0.27-0+deb9u1

      SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
      SET time_zone = "+00:00";

      /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
      /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
      /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
      /*!40101 SET NAMES utf8mb4 */;

      --
      -- Database: `weather`
      --

      -- --------------------------------------------------------

      --
      -- Table structure for table `weatherdata`
      --

      CREATE TABLE `weatherdata` (
      `time` timestamp NOT NULL DEFAULT '1999-12-31 23:00:00',
      `hum_bad` decimal(3,1) DEFAULT NULL,
      `temp_bad` decimal(3,1) DEFAULT NULL,
      `hum_wz` decimal(3,1) DEFAULT NULL,
      `temp_wz` decimal(3,1) DEFAULT NULL,
      `hum_kue` decimal(3,1) DEFAULT NULL,
      `temp_kue` decimal(3,1) DEFAULT NULL,
      `hum_sz` decimal(3,1) DEFAULT NULL,
      `temp_sz` decimal(3,1) DEFAULT NULL,
      `hum_out` decimal(3,1) DEFAULT NULL,
      `temp_out` decimal(3,1) DEFAULT NULL
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

      --
      -- Indexes for dumped tables
      --

      --
      -- Indexes for table `weatherdata`
      --
      ALTER TABLE `weatherdata`
      ADD PRIMARY KEY (`time`),
      ADD UNIQUE KEY `time` (`time`);

      /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
      /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
      /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

  2. […] ESP module shown in #P7 slowly gathers dust in my kitchen, which is tolerable for a few weeks, but that’s not the […]

Leave a Reply

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

:mrgreen: 
:neutral: 
:twisted: 
:arrow: 
:shock: 
:smile: 
:???: 
:cool: 
:evil: 
:grin: 
:idea: 
:oops: 
:razz: 
:roll: 
;-) 
:cry: 
:eek: 
:lol: 
:mad: 
:sad: 
:suspect: 
:!: 
:?: 
:bye: 
:good: 
:negative: 
:scratch: 
:wacko: 
:yahoo: 
:heart: 
B-) 
:rose: 
:whistle: 
:yes: 
:cry2: 
:mail: 
:-(( 
:unsure: 
:wink: