Raspberry Pi project – swapping sensors, part 2

Now that I’ve laid out my reasons for trying a different type of sensor, let’s dig in.  We need a couple of libraries that the DHT22 sensors didn’t need, so we should go ahead and get them installed.

First, let’s install the smbus2 library. At a command prompt, type:

sudo pip install smbus2

Now, let’s install the library for the SME280 sensor. At a command prompt, type:

sudo pip install RPi.bme280

That takes care of the additional libraries we’ll need for the python script.

Now, let’s connect the sensors to the Raspberry Pi. I’m using the model 4 rev B, so I have a 40-pin GPIO header. We’re going to use six pins to connect our two sensors. We’re going to connect each sensor to it’s own 3.3v source and it’s own ground. We’ll use RPi4 pins 1 and 6 for 3.3v and ground for the upstairs sensor, and pins 17 and 9 for 3.3v and ground for the downstairs sensor. We’ll connect the two clock lines together and they will connect to RPi4 pin 5; then we’ll connect the two data lines together and they will connect to RPi4 pin 3.  Since the two sensors will have a unique address on the bus, we can connect them electrically but interrogate them separately.

We also need to make a small alteration to one of the sensors to change it’s address on the i2c bus.  In the following image, the red mark indicates the we are removing the connection between the left and center pad, and adding a connection from the center pad to the right pad.  We are only altering one of the two sensor boards in this manner.  The unaltered sensor will have bus address 0x76, and the altered sensor will have address 0x77.  The test script below is coded so that the upstairs sensor is the unaltered one, and the altered sensor is the downstairs sensor.

This sensor test script interrogates the sensors and then displays the returned values separately, followed by a string containing the data. Note that python is sensitive to indentation. I’ve added a line of code to convert the celcius temperature into fahrenheit. This line is optional. The full code for our test script follows.

#!/usr/bin/python3

import time
import smbus2
import bme280

port = 1
upstairs_addr = 0x76
downstairs_addr = 0x77
bus = smbus2.SMBus(port)

calibration_params_upstairs = bme280.load_calibration_params(bus, upstairs_addr)
calibration_params_downstairs = bme280.load_calibration_params(bus, downstairs_addr)

#exit()

while True:
    # interrogate the sensors and supply the calibration data we obtained earlier
    upstairs_data = bme280.sample(bus, upstairs_addr, calibration_params_upstairs)
    downstairs_data = bme280.sample(bus, downstairs_addr, calibration_params_downstairs)

    # the compensated data
    print(upstairs_data.id)
    print(upstairs_data.timestamp)
    print(upstairs_data.temperature)
    ftemp_upstairs = upstairs_data.temperature * 9/5.0 + 32
    print(ftemp_upstairs)
    print(upstairs_data.pressure)
    print(upstairs_data.humidity)

    # in string format
    print(upstairs_data)

    # the compensated data
    print(downstairs_data.id)
    print(downstairs_data.timestamp)
    print(downstairs_data.temperature)
    ftemp_downstairs = downstairs_data.temperature * 9/5.0 + 32
    print(ftemp_downstairs)
    print(downstairs_data.pressure)
    print(downstairs_data.humidity)

    # in string format
    print(downstairs_data)

    time.sleep(15)

Once this is working and successfully retrieving and displaying the sensor data, we’ll move on to the changes needed to our data logger script.

Raspberry Pi project – swapping sensors

I’m a little suspect of the humidity readings I get from the DHT22 sensors.  I suspect that they’re reporting higher humidity than is actually the case.  I did some research and I’m not alone in this concern.  It’s not a huge concern by any means, my primary interest is with temperature, but since I’m logging and graphing the data, why not try to make the data as accurate as possible?

That research led me to the BME280 sensor module.  It communicates through the i2c bus, using an address on the bus to differentiate between multiple devices on the same bus.  The BME280 has two available addresses depending on how a jumper on the module is configured.  Since I’m using two sensors that will work out well.

The difference that you’ll need to deal with first is that the BME280 sensors use four connections to the Pi, not three as the DHT22 sensor does.  So you’ll need an extra wire between the sensor and the Pi.

Normally you’d use a breakout board to easily handle the multiple connections needed, but because I don’t foresee adding other sensors at this point, I’m just going to solder the wires for data and clock together.  I’ll continue to use separate 3.3v and ground connections for each sensor.  If that changes, I can easily add a breakout board later.

The python script that interrogates the sensors and saves the data into the rrdtool database will require some changes.  These sensors have calibration data available, and we’ll use that to be sure the readings are as accurate as possible.  We’ll also need different libraries to use the i2c bus and to communicate with the BME280 sensors.  Both of these libraries can easily be installed using pip.

 

Raspberry Pi project – integration

Ideally, the logging / data storage script would run automatically, as would the graph image generation script.  There are multiple ways to make that happen, and some of them will vary depending on your operating system.

The simplest way is to just run the scripts a a scheduled process using cron.  If you do this, you should remove the endless loop and sleep commands from the scripts.  Since cron will execute the scripts on the schedule you choose, there is no need for the script to sleep and then wake up and run through a loop again.

If you choose to run them through cron, you’ll need to run the logging / data storage script every 5 minutes, so the minute parameter in your cron file will be “*/5”, meaning every 5 minutes.  The other timing parameters will be simply “*”.

For the graph image generation script, you would probably run it once each hour.  To run it at the top of the hour, set the minute value to “0”, and the other parameters to “*”. That means cron will run the script every time the minute is 0 (top of the hour).

Since I like the scripts to run as system services, my scripts have the loop and sleep commands in them.  On a linux operating system using systemd, you’ll create a system service for running the scripts. To do this you need root access – either switch to the root user or use sudo.  Go to the /etc/systemd/system directory, and create a file name temperature_logger.service.  Put the following code in it, adjusting the path to the file as required for your environment.

After=network.service

[Service]
ExecStart=/<path-to-your-script>/temperature_logger.py

[Install]
WantedBy=default.target

This file should be owned by root, with 644 permissions.

To activate this as a system service, type “systemctl enable temperature_logger.service”, followed by “systemctl start temperature_logger.service”. This will now start automatically when the computer is booted. To stop it from running, type “systemctl stop temperature_logger.service”. Note that it will restart if the computer is rebooted. If you don’t want it to start unless you start it, type “systemctl disable temperature_logger.service”.

Setting up the graph image generations as a system service is basically identical, except that the service file will have a different name, and the file name on the ExecStart line will be different.

It is your preference as to how you want to execute these scripts. Other OSes will offer different methods that give the same result.

To rotate the log file, we need to add a file into the /etc/logrotate.d/ directory. The name isn’t critical; I chose “mylogs” to differentiate it from the files added when other packages are installed. Presuming that you left the name of the log file as it was in the example code, here is what you’ll need to add.

/var/log/temp_humidity.log {
    su root adm
    daily
    missingok
    rotate 7
    delaycompress
    compress
    notifempty
    create 644 root adm
    sharedscripts
    postrotate
    endscript
    maxage 7
}

This will keep 7 days worth of log files. The current log file and yesterday’s file are not compressed, the other log files are compressed to save space.

I hope you’re found this little project documentation useful. You could extend it by adding another sensor, perhaps locating this one outdoors. The documentation says that the sensor can be connected to the Raspberry Pi by wires as long as 50 yards, so you have some flexibility as to where you place it.

Raspberry Pi project – pretty graphs

Now that we’re saving data into the rrd database, let’s make some graphs so we can see what happens over time to the temperature and humidity values. I chose to use php for this part of the project, as there is a php module for rrdtool that works quite well. We’ll create 5 graphs, Hourly, Daily, Weekly, Monthly, and Yearly. We can also add text to the graphs to show the maximum, average, and minimum values for the time period being displayed.

#!/usr/bin/php -q
<?php


while (true) {

    $f = fopen("/var/log/temp_humidity.log", "a+");
    fwrite($f, date('Y-m-d H:i:s')." generate graph images begin\n");

    create_graph("/var/www/html/temp/temp-hour.png", "-1h", "Woodsmor Cottage - Hourly", $f);
    create_graph("/var/www/html/temp/temp-day.png", "-1d", "Woodsmor Cottage - Daily", $f);
    create_graph("/var/www/html/temp/temp-week.png", "-1w", "Woodsmor Cottage - Weekly", $f);
    create_graph("/var/www/html/temp/temp-month.png", "-1m", "Woodsmor Cottage - Monthly", $f);
    create_graph("/var/www/html/temp/temp-year.png", "-1y", "Woodsmor Cottage - Yearly", $f);

    $output = null;
    $retval = null;
    exec('scp /var/www/html/temp/*.png xxxx@xxxxxxxxxxxx.com:/var/www/html/temp/.', $output, $retval);
    fwrite($f, date('Y-m-d H:i:s')." images copied to macbook with status $retval\n");
    //echo "Returned with output ".print_r($output, true)."\n";

    fwrite($f, date('Y-m-d H:i:s')." generate graph images complete\n");

    fclose($f);
    sleep(3600);

}

exit;

We start off in the same way as the other scripts, by telling the OS which interpreter should be used. Then we have an endless while loop to create the graph images.  The first thing we do is to log the start of the image creation process, using the same log file that the sensor data is using. Then we call a function “create_graph” that passes four parameters. The first is the full path and filename for the image, the second is time span the graph will cover, the third is the title of the graph, and the final is the handle for the log file. We call it five times, once for each graph image we want to create. Once the images have been created, I’m using SCP (SecureCoPy) to copy the images to the webserver from the pi. You might just serve them from the pi if you want. I have another computer on my local network that is running a webserver, so I decided the pi will just collect the data, save it, generate the images, and then copy them to the webserver.  At the end, we sleep for 3600 seconds (one hour), and then create the images all over again.

function create_graph($output, $start, $title, $f) {
    $date_time = date('F j, Y \a\t g:i a');
    // the : has special significance in graph variables, so it must be escaped
    $date_time = str_replace(':', '\:', $date_time);
    $options = array(
        "--width=800",
        "--height=200",
        "--imgformat=PNG",
        "--slope-mode",
        "--start", $start,
        "--title=$title",
        "--vertical-label=Temperature / Humidity",
        "DEF:utmax=/home/scripts/temp_humidity.rrd:uth_dht22:MAX",
        "DEF:uhmax=/home/scripts/temp_humidity.rrd:uhm_dht22:MAX",
        "DEF:dtmax=/home/scripts/temp_humidity.rrd:dth_dht22:MAX",
        "DEF:dhmax=/home/scripts/temp_humidity.rrd:dhm_dht22:MAX",
        "LINE1:utmax#ff0000:Upstairs Temperature",
        "LINE1:uhmax#ff9999:Upstairs Humidity",
        "LINE1:dtmax#0000ff:Downstairs Temperature",
        "LINE1:dhmax#9999ff:Downstairs Humidity",
        "COMMENT:\\n",
        "COMMENT:Generated ".$date_time
    );

    $ret = rrd_graph($output, $options);
    if (! $ret) {
        fwrite($f, date('Y-m-d H:i:s')." Graph error: ".rrd_error()."\n");
    }
}

?>

This is the function that does all of the heavy lifting. The first thing we do is to build a variable that contains the date and time in an easily-read format so that we can include the text in the graph image. That way we know when the graph was generated. We’ll use that value later.

To pass the options to the rrd_graph function, we build an array of values. We tell it the width and height (in pixels) of the image we want, and the format we want. “slope-mode” says we want a line connecting the data points. The time period is passed as a parameter, something like “-1h”. That means all data will be included starting from one hour ago. The graph title is also passed as a parameter.  There is a vertical label defined, but that is optional.

Then we define the four data fields we want to show on the graph. The first parameter in the DEF line is the variable name we will use in the options array, followed by the full path and name of the rrd file. The next parameter is the variable name used when we defined the rrd database. The fourth and final parameter is MAX, indicating that we want to graph the maximum value.

The four LINE1 parameters lay out the legend for the graph. First is the variable name we assigned in the DEF lines, followed by the rgb color values to describe the way the line color should appear. The final parameter is the label for the legend.

The output path and filename, and options array are passed to the rrd_graph function, and we get back a return code. If the graph function fails, we log the reason.

Here’s the complete script.

#!/usr/bin/php -q
<?php


while (true) {

    $f = fopen("/var/log/temp_humidity.log", "a+");
    fwrite($f, date('Y-m-d H:i:s')." generate graph images begin\n");

    create_graph("/var/www/html/temp/temp-hour.png", "-1h", "Woodsmor Cottage - Hourly", $f);
    create_graph("/var/www/html/temp/temp-day.png", "-1d", "Woodsmor Cottage - Daily", $f);
    create_graph("/var/www/html/temp/temp-week.png", "-1w", "Woodsmor Cottage - Weekly", $f);
    create_graph("/var/www/html/temp/temp-month.png", "-1m", "Woodsmor Cottage - Monthly", $f);
    create_graph("/var/www/html/temp/temp-year.png", "-1y", "Woodsmor Cottage - Yearly", $f);

    $output = null;
    $retval = null;
    exec('scp /var/www/html/temp/*.png xxxx@xxxxxxxxxxxx.com:/var/www/html/temp/.', $output, $retval);
    fwrite($f, date('Y-m-d H:i:s')." images copied to macbook with status $retval\n");
    //echo "Returned with output ".print_r($output, true)."\n";

    fwrite($f, date('Y-m-d H:i:s')." generate graph images complete\n");

    fclose($f);
    sleep(3600);

}

exit;

function create_graph($output, $start, $title, $f) {
    $date_time = date('F j, Y \a\t g:i a');
    // the : has special significance in graph variables, so it must be escaped
    $date_time = str_replace(':', '\:', $date_time);
    $options = array(
        "--width=800",
        "--height=200",
        "--imgformat=PNG",
        "--slope-mode",
        "--start", $start,
        "--title=$title",
        "--vertical-label=Temperature / Humidity",
        "DEF:ut=/home/scripts/temp_humidity.rrd:uth_dht22:MAX",
        "DEF:uh=/home/scripts/temp_humidity.rrd:uhm_dht22:MAX",
        "DEF:dt=/home/scripts/temp_humidity.rrd:dth_dht22:MAX",
        "DEF:dh=/home/scripts/temp_humidity.rrd:dhm_dht22:MAX",
        "LINE1:ut#ff0000:Upstairs Temperature",
        "LINE1:uh#ff9999:Upstairs Humidity",
        "LINE1:dt#0000ff:Downstairs Temperature",
        "LINE1:dh#9999ff:Downstairs Humidity",
        "COMMENT:\\n",
        "COMMENT:Generated ".$date_time
    );

    $ret = rrd_graph($output, $options);
    if (! $ret) {
        fwrite($f, date('Y-m-d H:i:s')." Graph error: ".rrd_error()."\n");
    }
}

?>

Here is the Daily graph image created from this script. You’ll notice that the humidity lines merge partway through the day – that’s when I implemented the correction code in the logging script. Also, note that the downstairs temperature is in centigrade, as I mentioned earlier. When this is running at the cabin, I’ll activate the line of code that converts the downstairs temperature to fahrenheit. I would expect the temperature and humidity values to track fairly closely to each other, but that’s one thing I’ll learn as it accumulates data.

Next, we need to integrate the logging script and the graphing script so that they run automatically.

Raspberry Pi project – storing the values

Now that we’ve defined the database to save our temperature and humidity values, we need a process to query the sensor and update the database. I also decided to create a log file containing the values as it’s easier to see that the process is working properly in a log file. So our little python script that we used to test the sensors will be updated to save the values in the rrd database, and also write them to a log file.

#!/usr/bin/python3

import os
import time
import Adafruit_DHT as dht
import rrdtool
import datetime

DHT_SENSOR = dht.DHT22
U_DHT_PIN = 5
D_DHT_PIN = 6

while True:

It starts off the same, except that we need to include some additional modules. I’ve added os, rrdtool, and datetime modules. The constants are defined just as before.

    try:
        f = open('/var/log/temp_humidity.log', 'a+')
    except:
        pass

This code opens the log file in append mode so that we can write to it later. It may seem odd to open the file each time through the loop, but this allows log file rotation to work properly. This script only runs every 5 minutes, so opening it within the loop is a reasonable compromise.

    # read the upstairs sensor data
    uh,ut = dht.read_retry(DHT_SENSOR, U_DHT_PIN)
    # read the downstairs sensor data
    dh,dt = dht.read_retry(DHT_SENSOR, D_DHT_PIN)

    if uh is not None and ut is not None and dh is not None and dt is not None:

Querying the sensors is the same as before, but the test for valid values is now done in a single if statement.

        # apply correction factor to DHT22 sensor temperature values
        ut = ut - 0
        dt = dt + 0
        # apply correction factor to DHT22 sensor humidity values
        uh = uh - 3.4
        dh = dh + 3.4
        # convert temp to fahrenheit
        ut = ut * 9/5.0 + 32
        #dt = dt * 9/5.0 + 32
        # log values
        f.write('{0} upstairs {1:0.1f}F {2:0.1f}% downstairs {3:0.1f}C {4:0.1f}%\r\n'.format(time.strftime('%Y-%m-%d %H:%M:%S'), ut, uh, dt, dh))
        f.flush()
        # update database
        data = 'N:' + '{0:.1f}'.format(ut) + ':' + '{0:.1f}'.format(uh) + ':' '{0:.1f}'.format(dt) + ':' + '{0:.1f}'.format(dh)
        #print(data)
        ret = rrdtool.update("%s/temp_humidity.rrd" % (os.path.dirname(os.path.abspath(__file__))),data)
        if ret:
            err = rrdtool.error()
            f.write('{0} {1}\r\n'.format(time.strftime('%Y-%m-%d %H:%M:%S'), err))
            f.flush()

And now we get to the meat of the process. I’ve added code that allows you to apply a correction factor to the values before storing them in the database. My humidity sensors were tracking 6.8% apart, and did that consistently for a couple of days. Since they’re lying on the table about 12″ apart while I test this project, and the difference was constant, I chose to reduce the upstairs value and increase the downstairs value by the same amount. The temperature sensors agreed with a about 0.2 degrees so I left the correction at zero for them – at least for now.

Then we log the values to the log file, and the flush command causes the write to happen immediately. Without it the writes are buffered and may be delayed. I prefer that the log file is current.

To update the rrd database, we build a string containing the values. The data elements are separated by colons, and the first element is “N”, which rrdtool translates to a timestamp for “right now”. The four data values are placed in the string in the same order they were defined when we created the rrd database.

The rrdtool.update command references the temp_humidity.rrd database file, and locates the full path to the file. The variable data, containing the string we constructed earlier, is the only other parameter. The return code from the update process is assigned to the variable “ret” which we then test to ensure the result is good. If not, we log the error and execute the flush, to write the error into the log file immediately.

    else:
        f.write('{0} ERROR - failed to retrieve data from temp/humidity sensor\r\n'.format(time.strftime('%Y-%m-%d %H:%M:%S')))
        f.flush()
        dateTimeObj = datetime.now()
        date_time = dateTimeObj.strftime("%B %-d, %Y at %-I:%M %p")
        port = 587  # For starttls
        smtp_server = "smtp.gmail.com"
        sender_email = "xxxx@xxxxxxxxxxxxxxxxx.com"
        receiver_email = "xxxx@xxxxxxxxx.com"
        password = "xxxxxxxxxxxx"
        message = """\
        Subject: Alert - problem reading temp/humidity sensor

        On {}, there was a problem reading the temperature/humidity sensor.

        If this is not resolved there will be gaps in the data.
        """.format(date_time)
        context = ssl.create_default_context()
        with smtplib.SMTP(smtp_server, port) as server:
            #server.ehlo()  # Can be omitted
            server.starttls(context=context)
            #server.ehlo()  # Can be omitted
            server.login(sender_email, password)
            server.sendmail(sender_email, receiver_email, message)

    f.close()
    time.sleep(300)

This code handles the condition where we do not receive valid values from querying the sensors. First we log the error condition. Then we send an email to the administrator (me) to let me know there has been a problem. Finally, we close the log file and sleep for 300 seconds (5 minutes).

Here’s the complete script. As before, be aware that python is sensitive to indentation and line spacing.

#!/usr/bin/python3

import os
import time
import Adafruit_DHT as dht
import rrdtool
import datetime

DHT_SENSOR = dht.DHT22
U_DHT_PIN = 5
D_DHT_PIN = 6

while True:
    try:
        f = open('/var/log/temp_humidity.log', 'a+')
    except:
        pass

    # read the upstairs sensor data
    uh,ut = dht.read_retry(DHT_SENSOR, U_DHT_PIN)
    # read the downstairs sensor data
    dh,dt = dht.read_retry(DHT_SENSOR, D_DHT_PIN)

    if uh is not None and ut is not None and dh is not None and dt is not None:
        # apply correction factor to DHT22 sensor temperature values
        ut = ut - 0
        dt = dt + 0
        # apply correction factor to DHT22 sensor humidity values
        uh = uh - 3.4
        dh = dh + 3.4
        # convert temp to fahrenheit
        ut = ut * 9/5.0 + 32
        #dt = dt * 9/5.0 + 32
        # log values
        f.write('{0} upstairs {1:0.1f}F {2:0.1f}% downstairs {3:0.1f}C {4:0.1f}%\r\n'.format(time.strftime('%Y-%m-%d %H:%M:%S'), ut, uh, dt, dh))
        f.flush()
        # update database
        data = 'N:' + '{0:.1f}'.format(ut) + ':' + '{0:.1f}'.format(uh) + ':' '{0:.1f}'.format(dt) + ':' + '{0:.1f}'.format(dh)
        #print(data)
        ret = rrdtool.update("%s/temp_humidity.rrd" % (os.path.dirname(os.path.abspath(__file__))),data)
        if ret:
            err = rrdtool.error()
            f.write('{0} {1}\r\n'.format(time.strftime('%Y-%m-%d %H:%M:%S'), err))
            f.flush()

    else:
        f.write('{0} ERROR - failed to retrieve data from temp/humidity sensor\r\n'.format(time.strftime('%Y-%m-%d %H:%M:%S')))
        f.flush()
        dateTimeObj = datetime.now()
        date_time = dateTimeObj.strftime("%B %-d, %Y at %-I:%M %p")
        port = 587  # For starttls
        smtp_server = "smtp.gmail.com"
        sender_email = "xxxx@xxxxxxxxxxxxxxxxx.com"
        receiver_email = "xxxx@xxxxxxxxx.com"
        password = "xxxxxxxxxxxx"
        message = """\
        Subject: Alert - problem reading temp/humidity sensor

        On {}, there was a problem reading the temperature/humidity sensor.

        If this is not resolved there will be gaps in the data.
        """.format(date_time)
        context = ssl.create_default_context()
        with smtplib.SMTP(smtp_server, port) as server:
            #server.ehlo()  # Can be omitted
            server.starttls(context=context)
            #server.ehlo()  # Can be omitted
            server.login(sender_email, password)
            server.sendmail(sender_email, receiver_email, message)

    f.close()
    time.sleep(300)

Next, the pretty part. We’ll build a script to generate graphs using the data we’ve so carefully saved.

Raspberry Pi project – defining the database

I decided to use the rrd (Round Robin Database) to hold the temperature and humidity data from the sensors. The rrd is designed to hold the data in archives that are defined when the database is created. New data will roll in, and old data will roll out, maintaining the time period data for each archive. I’ve defined daily, weekly, monthly, and yearly archives. It’s probably more than I will ever need, but it doesn’t take much space so why not.

With that said, let’s take a look at the database definition.

#!/bin/bash

The file is marked executable, and this line tells the OS what interpreter to use.

# run in the directory where the rrd file should live
rrdtool create temp_humidity.rrd \
--start "12/10/2020" \
--step 300 \

These lines give the database a name (temp_humidity.rrd), define the start date (12/10/2020), and define how often data should be expected to be loaded into the database (every 300 seconds).

DS:uth_dht22:GAUGE:1200:-10:100 \
DS:uhm_dht22:GAUGE:1200:-10:100 \
DS:dth_dht22:GAUGE:1200:-10:100 \
DS:dhm_dht22:GAUGE:1200:-10:100 \

These lines define 4 data fields, named uth_dht22, uhm_dht22, dth_dht22, and dhm_dht22. GAUGE means that the values are stored directly as provided. The 1200 value is the number of seconds the database will wait for a new value – if no data is loaded, an empty set of values will be inserted. This is so that the graphing tool, which we will get to later, will have a complete set of data to display. The final two values define the valid range of data, in this case from -10 to 100.

RRA:AVERAGE:0.5:1:288 \
RRA:AVERAGE:0.5:6:336 \
RRA:AVERAGE:0.5:24:372 \
RRA:AVERAGE:0.5:144:732 \

These lines define four archives, daily, weekly, monthly, and yearly. This set of archives will hold average data for the four data fields. 0.5 is a value used to manage consolidation of the data items – this is recommended by the author so I used the recommended value here. The next field is the number of data points that will be used to construct a consolidated data point. The final field is the number of consolidated data points that will be retained in the archive.

The final two parameters can be confusing, let’s look at them more closely.

For the first archive (daily), each data point is averaged, and 288 are retained. Since we are sampling every 300 seconds, we will get 288 samples per day.

For the second archive (weekly), we average 6 data points to a single data point, and 336 are retained. By consolidating 6 data points into 1, we will have 48 consolidated data points per day. Each week will then have 48 * 7 or 336 data points per week.

For the third archive (monthly), we average 24 data points to a single data point, and 372 are retained. By consolidating 24 data points into 1, we will have 12 consolidated data points per day. Each month will then have 12 * 31 or 372 data points per month.

For the fourth archive (yearly), we average 144 data points to a single data point, and 732 are retained. By consolidating 144 data points into 1, we will have 2 consolidated data points per day. Each year will then have 2 * 366 or 732 data points per year.

RRA:MIN:0.5:1:288 \
RRA:MIN:0.5:6:336 \
RRA:MIN:0.5:24:372 \
RRA:MIN:0.5:144:732 \
RRA:MAX:0.5:1:288 \
RRA:MAX:0.5:6:336 \
RRA:MAX:0.5:24:372 \
RRA:MAX:0.5:144:732 \

The final lines are almost duplicates of the average archives, except that they store minimum (MIN) and maximum (MAX) values.

Here is the complete script.

#!/bin/bash

# run in the directory where the rrd file should live
rrdtool create temp_humidity.rrd \
--start "12/10/2020" \
--step 300 \
DS:uth_dht22:GAUGE:1200:-10:100 \
DS:uhm_dht22:GAUGE:1200:-10:100 \
DS:dth_dht22:GAUGE:1200:-10:100 \
DS:dhm_dht22:GAUGE:1200:-10:100 \
RRA:AVERAGE:0.5:1:288 \
RRA:AVERAGE:0.5:6:336 \
RRA:AVERAGE:0.5:24:372 \
RRA:AVERAGE:0.5:144:732 \
RRA:MIN:0.5:1:288 \
RRA:MIN:0.5:6:336 \
RRA:MIN:0.5:24:372 \
RRA:MIN:0.5:144:732 \
RRA:MAX:0.5:1:288 \
RRA:MAX:0.5:6:336 \
RRA:MAX:0.5:24:372 \
RRA:MAX:0.5:144:732 \

Next, we’ll go back to python to query the sensors and store the data in the rrd database we’ve just defined.

Video security system – putting it all together

We should now have camera(s), with a dedicated IP address and a gateway IP address that isn’t actually a gateway, and an updated username/password.  The camera(s) should be connected using an ethernet cable to a POE switch, and the POE switch is connected to our router.  The computer that will run Zoneminder is connected to the router using an ethernet cable.

The ZM documentation is very good and will help you to optimize the settings for your particular camera(s) and computer system resources.  Follow the platform-specific installation steps in the User Guide to complete the initial installation.

Now let’s bring it all together.  It’s best to start with a lower resolution and frame rate as it’s less taxing to the computer running Zoneminder.  We can tweak the resolution and frame rates after we see how the system is performing.  Be aware that the number of camera(s), the size of the detection zones, and the method of detection will affect the required system resources.  Initially, we’ll use the “Monitor” configuration – this does not record video, it just allows Zoneminder to access the video streams.  We’ll enable motion detection at a later time.

I’m going to be a little vague about the exact ZM setup, as the cameras you choose and the system that runs ZM will dictate a lot of the settings you use.  The information you find in the ZM documentation and the ZM forums comes from people with a lot of knowledge and experience, and the forums are particularly helpful when you’re just getting the system up and running.

First, we need to get the cameras talking to ZM.  On the Console page, we want to add a monitor.  On the general page, you should give the monitor (camera) a name, and it is helpful for the name to be the location of the camera – something like “front door”, “garage”, “living room”, “patio door”, etc.  The Source page is most easily populated by clicking ONVIF and letting ZM locate the ONVIF devices on the network.  Select the one you want to use as monitor #1 – remember that we wrote down the IP addresses for the cameras as we set up the network information.  You’ll need to set up the framerate and the resolution for the video stream to match the camera settings, so that ZM knows what to expect.  Once this is done, the ZM console should report that the cameras is “Capturing”.    Then by clicking on the camera’s name, you should see the video stream for that camera.

Then you simply add a monitor for each camera on your network.  You can see them all at once on the Montage page.

The reason for the Raspberry Pi project

I have a cabin in southern Indiana that I don’t get to as often as I’d like.  Since it gets below freezing in the winter, I have to winterize it by turning the pump off, draining the plumbing, and putting antifreeze in the traps.  It would be nice to monitor the indoor temperature to see what actually happens when I’m not there.  Dad, being the engineer that he was, had a mechanical temperature recorder.  You know, the kind that uses a roll of paper and a moving ink pen to make a graph, like an EKG machine in the hospital.  Unfortunately it no longer works so I decided to pay homage to that by building a modern equivalent.

I picked up a pair of DHT22 temperature/humidity sensors and they will connect to the GPIO pins on the pi.  I’ll need a program to read the sensors and log the data values, and it would be great to generate a graph of the values to see the how the values change over time.

Now, a small wrinkle.  The daemon that manages the fan and power switch uses GPIO pin 4, so we’ll need to use a different one.  After powering down the pi, I plugged the “+” to 3.3v, the “-” to ground, and the data to GPIO pin 5.  A single click on the power switch and the pi fired right up.  Since I had two sensors, I connected the second one to GPIO pin 6.  The sensor on GPIO pin 5 is “upstairs” and the one on GPIO pin 6 is “downstairs.  If you accidentally reverse the 3.3v and ground connections, the pi will not boot.

Now we need a little program to test our sensor.  Python seems to be the best tool for this, so here we go. Let’s walk through the code and see how it works.

#!/usr/bin/python3

import time
import Adafruit_DHT as dht

This program has been marked as executable, so the first line tells the OS which interpreter should be used. The other lines include the python module “time” that allows us to use time functions in our script, and the ADAfruit DHT module that talks to the sensors.

DHT_SENSOR = dht.DHT22
U_DHT_PIN = 5
D_DHT_PIN = 6

These lines define constants that we will use later – this allows us to make changes to the code in one place.  We’ve defined two constants for the GPIO pins we’re using, and the sensor type we’re going to query.

while True:

This line starts an endless loop, meaning that this section of code will run until the program is terminated.

    uhm, uth = dht.read_retry(DHT_SENSOR, U_DHT_PIN)

    if uhm is not None and uth is not None:
        uth = uth * 9/5.0 + 32
        print("UPSTAIRS - Temp={0:0.1f}*F  Humidity={1:0.1f}%".format(uth, uhm))
    else:
        print("Failed to retrieve data from upstairs sensor")

This is where the magic happens. First, we query the sensor, specifically the one attached to GPIO pin 5, and place the values in the variables uth (temperature) and uhm (humidity). Then we check to be sure that we actually got values back from the sensor before doing any manipulation of the values. Then we convert the temperature from centigrade to fahrenheit.  This is optional depending on your preference. Now we print the values from the “upstairs” sensor (the one connected to GPIO pin 5). And finally, we handle the condition where the sensor does not return valid values.

    dhm, dth = dht.read_retry(DHT_SENSOR, D_DHT_PIN)

    if dhm is not None and dth is not None:
        print("DOWNSTAIRS - Temp={0:0.1f}*C  Humidity={1:0.1f}%".format(dth, dhm))
    else:
        print("Failed to retrieve data from downstairs sensor")

These lines are essentially a duplicate of the previous section, but these handle the “downstairs” sensor connected to GPIO pin 6.  You’ll notice that I did not include the line to convert centigrade to fahrenheit for this sensor.

    time.sleep(15)

This line causes the program to sleep for 15 seconds before going through the loop again.

Here’s the complete test program. Be aware that python is sensitive to indentation and line spacing.

#!/usr/bin/python3

import time
import Adafruit_DHT as dht

DHT_SENSOR = dht.DHT22
U_DHT_PIN = 5
D_DHT_PIN = 6

while True:
    uhm, uth = dht.read_retry(DHT_SENSOR, U_DHT_PIN)

    if uhm is not None and uth is not None:
        uth = uth * 9/5.0 + 32
        print("UPSTAIRS - Temp={0:0.1f}*F  Humidity={1:0.1f}%".format(uth, uhm))
    else:
        print("Failed to retrieve data from upstairs sensor")

    dhm, dth = dht.read_retry(DHT_SENSOR, D_DHT_PIN)

    if dhm is not None and dth is not None:
        print("DOWNSTAIRS - Temp={0:0.1f}*C  Humidity={1:0.1f}%".format(dth, dhm))
    else:
        print("Failed to retrieve data from downstairs sensor")

    time.sleep(15)

Next – adding a method to save the values into a database, that we can use to visualize the swings of temperature and humidity over time.

Video security system – Connections, part 2

A quick review – your router is assigning IP addresses using the range 192.168.1.100 through 192.168..1.199.  We’re going to use IP addresses that are not in that range.

Let’s say we want to use 192.168.1.51 for the first camera, and the router says that one of the cameras (it doesn’t really matter which one) is using 192.168.1.183.  In a browser address bar, type http://192.168.1.183 and press enter.  You should see a login page for your camera.  Usually the username is “admin” and the default password is usually “admin”.  The documentation that came with your camera should tell you the login default values.  Go ahead and log in.

Now we want to access the network settings, specifically the TCP/IP data.  We want to use a dedicated IP address, and we will set it to  192.168.1.51.  The netmask value should be 255.255.255.0, and you can set the DNS value to 8.8.8.8 (that’s google’s dns servers).  Next is the single most important change you will make.  We want to set the gateway IP address to a value that cannot act as a gateway to the public internet.  Usually the gateway address is the router’s IP address, something like 192.168.1.1, or maybe 192.168.1.254.  Let’s use 192.168.1.50.  That is not a valid gateway but it is a valid IP address.

Why do we want use an IP address as a gateway when it isn’t a gateway?  So that the cameras cannot communicate with the public internet, only with your local network.

I cannot over-emphasize the importance of this last step.  Failing to do this will leave the door open to a hacker gaining access to your video streams.  You do not want that to happen.

Now on the configuration page, change the camera password.  Do not leave it at “admin”.  Save your changes.  You’ll lose the connection to the camera, but that’s because you changed the IP address.  Go to http://192.168.1.51 and it should be patiently waiting for you there.  Log in using the updated password to make sure it works.  Also, as a double-check, try logging in using the default password of “admin”.  It should NOT work.

If you’ve connected multiple cameras, go through the same process with each one in turn.  The next camera will use 192.168.1.52, then .53, and so on.  Write down the IP addresses you use, and the passwords for each one.  It will make things simpler if you use the same password for all of the cameras.

When you’ve finished this, you’ll have cameras connected to your network, updated passwords, and dedicated IP addresses for each one.  The cameras will not have access to the public internet, only to your local network.

Video security project – Connections

We have a computer package to run the system, and we have cameras to supply the system with video and still images.  We will use an ethernet cable to connect to the cameras, and also to supply power to them.  What else do we need?

We need a special type of switch to obtain the data connection to the cameras and to supply power to them.  Regular switches just handle the data connection, so we’ll need a POE switch.  You can find them in various configurations, the one I chose was an 8 port model.  4 of the ports will go to the cameras, and a fifth will connect to the router.  The last connection (to the router) will allow computers on our network to access the video data.  We’ll talk about security for the video streams a bit later.

I would recommend a wired connection for the computer that will be running the system.  Wired connections are much more difficult to break into than wifi-based communication, and video streams from your home should be as private as you can possibly make them.

A bit about cameras setup with an eye towards security for your video streams.  With most IP-based cameras, once you’ve connected them to a POE switch and connected that switch to your network, the streams are available to any device on your network, AND POSSIBLY TO THE INTERNET AS WELL.  We need to fix that right now.

You can scan your network looking for the cameras using nmap, but if you’re not THAT deep into network security, your router probably has a page that displays the devices connected to your network, both wired and via wireless connections.  Just note the IP addresses that were assigned to the cameras you’ve connected – we’ll need them when we tighten up the security.

Before we continue, a bit of background.  Usually, when your router is configured you choose a range of IP addresses that can be assigned to devices – like phones, laptops connected via wifi, tablets, etc.  Looking at your router’s wireless page should tell you the range of IP addresses that are used.  I prefer to assign a dedicated IP address to each camera, and the most reliable way to do that is to select addresses that are not used for other devices.  Let’s say that your router uses IP addresses from 192.168.1.100 through 192.168..1.199 for devices that connect to your network.  We’re going to assign IP addresses for the cameras using IP addresses that are not in that range.

This getting a little long, but this is a very important discussion, so lets continue this on the next post.