Difference between revisions of "Raspberry Pi Environmental Monitoring"

From Michael's Information Zone
Jump to navigation Jump to search
 
(28 intermediate revisions by the same user not shown)
Line 1: Line 1:
 
==Purpose==
 
==Purpose==
 
To monitor air temperature, humidity, and air quality.
 
To monitor air temperature, humidity, and air quality.
 +
<br>
 +
<br>
 +
Initially this was put together to run node-red and mysql on a single pi. This worked ok until the database reached ~200MB and the flash drive I was using started to choke. I also noticed the power draw for the pi, monitor, flash drive, and sensors exceeded the supply.
 +
===Work in progress===
 +
*<s>Move database off pi to central home server.</s>
 +
*Move node-red to central server.
 +
*Convert pi to pull from sensors using python instead of node-red.
 +
*Send data to central server using MQTT instead of MySQL
  
==Air Quality==
+
==PM Sensor==
===Base Config for Sensor===
+
===Base Script===
A friend sent me a Nova PM Sensor. I do not know python yet, but looking over the script that is readily available<ref>https://hackernoon.com/how-to-measure-particulate-matter-with-a-raspberry-pi-75faa470ec35</ref><ref>https://openschoolsolutions.org/measure-particulate-matter-with-a-raspberry-pi/</ref> I should be able to modify this to work for my needs.
+
A friend sent me a Nova PM Sensor. Found a script that is readily available<ref>https://hackernoon.com/how-to-measure-particulate-matter-with-a-raspberry-pi-75faa470ec35</ref><ref>https://openschoolsolutions.org/measure-particulate-matter-with-a-raspberry-pi/</ref>.
 +
*<b>NOTE</b> : This appears to run in python2. When attempting to run with python3 I received indentation errors. Should be easy to fix, but I am lazy and this is not a critical system.
 
<pre>
 
<pre>
 +
sudo apt install git-core python-serial python-enum python-pip
 
mkdir -p /home/pi/build/air && cd /home/pi/build/air
 
mkdir -p /home/pi/build/air && cd /home/pi/build/air
 
sudo apt install git-core python-serial python-enum mariadb-server python-mysql.connector
 
sudo apt install git-core python-serial python-enum mariadb-server python-mysql.connector
Line 21: Line 31:
 
PM2.5:  0.7 , PM10:  0.7
 
PM2.5:  0.7 , PM10:  0.7
 
</pre>
 
</pre>
===Configure for writing to database===
+
*Now remove the print command and instead create variables for pm2.5 and pm10
 +
<pre>
 +
              #print("PM2.5: ", values[0], ", PM10: ", values[1])
 +
      pm25=values[0]
 +
      pm10=values[1]
 +
</pre>
 +
 
 +
===Send values to MQTT===
 +
*Work in progress.
 +
<ref>https://pypi.org/project/paho-mqtt/#constructor-reinitialise</ref><ref>http://www.steves-internet-guide.com/into-mqtt-python-client/</ref>
 +
*Install python modules for mqtt
 +
<pre>
 +
pip install paho-mqtt
 +
</pre>
 +
*Update the aqi.py script to imnport the module, and configure as seen below.
 +
<b>NOTE : </b> I am using authentication but not encryption in this example.
 +
<pre>
 +
import paho.mqtt.client as mqtt
 +
 
 +
def mqttpub(pm1,pm2):
 +
    broker_address="xxx.xxx.xxx.xxx"
 +
    username="<username on broker>"
 +
    password="<password on broker>"
 +
    mqttc=mqtt.Client()
 +
    mqttc.username_pw_set(username, password)
 +
    mq = "0"
 +
    while mq == "0":
 +
        try:
 +
            mqttc.connect(broker_address)
 +
        except:
 +
            print("Failed MQTT")
 +
            time.sleep(5)
 +
            mq="0"
 +
        else:
 +
            mq="1"
 +
            mqttc.publish("air/livingroom/pm25",pm1)
 +
            mqttc.publish("air/livingroom/pm10",pm2)
 +
 
 +
...
 +
              mqttpub(pm25,pm10)
 +
</pre>
 +
 
 +
===Configure for writing to database (LEGACY)===
 +
*<b>NOTE</b> : This is really not necessary except for historical data purposes, and running on the pi is a bit much.
 
However, I want the history written to a database. Configure mariadb anyway you want, if you have credentials keep them on hand.
 
However, I want the history written to a database. Configure mariadb anyway you want, if you have credentials keep them on hand.
 
<pre>
 
<pre>
Line 46: Line 99:
 
Now we edit the script to work with mysql<ref>https://www.w3schools.com/python/python_mysql_insert.asp</ref><br>
 
Now we edit the script to work with mysql<ref>https://www.w3schools.com/python/python_mysql_insert.asp</ref><br>
 
Please note that of this writing I the script searches the entire database for 0 values and deletes them. I need to go back and update to simply not write 0 values in the first place.
 
Please note that of this writing I the script searches the entire database for 0 values and deletes them. I need to go back and update to simply not write 0 values in the first place.
 +
*Edit downloaded script with the following
 +
<pre>
 +
import mysql.connector
 +
 +
mydb = mysql.connector.connect(
 +
  host="localhost",
 +
  user="air",
 +
  database="air"
 +
)
 +
...
 +
              mycursor.execute('INSERT INTO aqi (pm25, pm10, date) VALUES (%s, %s, %s)' % (pm25,pm10,'now()'))
 +
              mydb.commit()
 +
              mycursor.execute("delete from aqi where pm10 = 0 and pm25 = 0")
 +
...
 +
              mycursor.execute("delete from aqi where date < (now() - interval 1 year)")
 +
</pre>
 
===Full Modified Script===
 
===Full Modified Script===
 
<pre>
 
<pre>
Line 54: Line 123:
 
from __future__ import print_function
 
from __future__ import print_function
 
import serial, struct, sys, time, json, subprocess,  mysql.connector
 
import serial, struct, sys, time, json, subprocess,  mysql.connector
 +
 +
time.sleep(120)
  
 
mydb = mysql.connector.connect(
 
mydb = mysql.connector.connect(
Line 165: Line 236:
 
             values = cmd_query_data();
 
             values = cmd_query_data();
 
             if values is not None and len(values) == 2:
 
             if values is not None and len(values) == 2:
              print("PM2.5: ", values[0], ", PM10: ", values[1])
+
#              print("PM2.5: ", values[0], ", PM10: ", values[1])
 
      pm25=values[0]
 
      pm25=values[0]
 
      pm10=values[1]
 
      pm10=values[1]
Line 179: Line 250:
 
         mycursor.execute("delete from aqi where date < (now() - interval 1 year)")
 
         mycursor.execute("delete from aqi where date < (now() - interval 1 year)")
 
</pre>
 
</pre>
 +
Configure to run at boot by adding the following to your crontab
 +
<pre>
 +
@reboot /home/pi/build/air/air.py
 +
</pre>
 +
 
==Tempurature / Humidity==
 
==Tempurature / Humidity==
 +
===Using Python to pull from Sensor===
 +
Place holder
 +
===Using Node-Red to pull from Sensor===
 +
NOTE : See https://michaelwiki.geekgalaxy.com/index.php/Raspberry_Pi_Environmental_Monitoring#Graph_Display
 +
<br>
 +
<br>
 
For this one I decided to use node-red to pull the values directly. It supports raspberry pi GPIO and the DHT-11 sensor I have on hand<ref>https://flows.nodered.org/node/node-red-contrib-dht-sensor</ref>.
 
For this one I decided to use node-red to pull the values directly. It supports raspberry pi GPIO and the DHT-11 sensor I have on hand<ref>https://flows.nodered.org/node/node-red-contrib-dht-sensor</ref>.
 
*Install the node-red-contrib-dht-sensor packages (I used the web gui)
 
*Install the node-red-contrib-dht-sensor packages (I used the web gui)
Line 185: Line 267:
 
sudo apt install wiringpi
 
sudo apt install wiringpi
 
</pre>
 
</pre>
*After adding the rpi-dht11 node to the flow, I gave it the following values
+
*Add inject module with as timestamp. I chose to inject every 5 seconds.
 +
*Add the rpi-dht11 node to the flow, I gave it the following values
 
**Sensor : DHT11
 
**Sensor : DHT11
 
**Pin numbering : WiringPi (rev.1)
 
**Pin numbering : WiringPi (rev.1)
 
**Pin number : 7
 
**Pin number : 7
 +
*The dht11 or 22 node will break out to a function and change module.
 
*This breaks out to a function node that changes C to F <ref>https://groups.google.com/forum/#!topic/node-red/bkmv6kMAFlM</ref>
 
*This breaks out to a function node that changes C to F <ref>https://groups.google.com/forum/#!topic/node-red/bkmv6kMAFlM</ref>
 
<pre>
 
<pre>
Line 197: Line 281:
 
return msg;
 
return msg;
 
</pre>
 
</pre>
*We also send the output of the rpi-dht11 to a set node to change the payload from msg.humidity.
+
*Now use a change module to change msg.payload to msg.humidity.
*Then add two gauges to represent temperature and humidity.
+
*Both the function and change modules connect to MQTT out modules that connect to my MQTT server.
  
 
==Graph Display==
 
==Graph Display==
Line 212: Line 296:
 
===Configure PM 2.5 Flow===
 
===Configure PM 2.5 Flow===
 
My flow consists of
 
My flow consists of
#An inject node with the timestamp injection using topic <pre>select pm25 from aqi order by date desc limit 1;</pre>
+
#An inject node with the timestamp injection using topic <pre>select round(avg(pm25)) from aqi where date < now() - interval 24 hour;</pre>
 
#A mysql node with the database location and credentials.
 
#A mysql node with the database location and credentials.
 
#A change node with set to <pre>msg.payload[0].pm25</pre>
 
#A change node with set to <pre>msg.payload[0].pm25</pre>
#A gauge node set for values between 0.5 and 2.
+
#A gauge node set for values between 0.1 and 30<ref>https://en.wikipedia.org/wiki/Air_quality_index</ref>.
 
<br>
 
<br>
 
<br>
 
<br>
 +
 
==Configure Pi for Kiosk Mode==
 
==Configure Pi for Kiosk Mode==
 
<ref>https://pimylifeup.com/raspberry-pi-kiosk/</ref>Please note that this is not the most secure configuration. In my case the terminal is located in my house, on a segregated wifi network and broadcast domain, and the information contained is air quality metrics so impact of compromise is low.
 
<ref>https://pimylifeup.com/raspberry-pi-kiosk/</ref>Please note that this is not the most secure configuration. In my case the terminal is located in my house, on a segregated wifi network and broadcast domain, and the information contained is air quality metrics so impact of compromise is low.
Line 231: Line 316:
 
x11-utils x11-xkb-utils x11-xserver-utils xterm lightdm openbox
 
x11-utils x11-xkb-utils x11-xserver-utils xterm lightdm openbox
 
</pre>
 
</pre>
After running the previous the system now logs me into openbox. At this point we want openbox to start chrome in kiosk mode.
+
After running the previous the system now logs me into openbox. At this point we want openbox to start chrome in kiosk mode.<ref>https://www.raspberrypi.org/forums/viewtopic.php?t=8298</ref>
 
<pre>
 
<pre>
 
sudo apt install chromium-browser
 
sudo apt install chromium-browser
 
mkdir -p ~/.config/openbox && cp /etc/xdg/openbox/* ~/.config/openbox
 
mkdir -p ~/.config/openbox && cp /etc/xdg/openbox/* ~/.config/openbox
echo 'chromium-browser --kiosk http://127.0.0.1:1880/ui' >> .config/openbox/autostart
+
echo -e "xset s noblank\nxset s off\n\xset -dpms\nunclutter &\nchromium-browser --kiosk http://127.0.0.1:1880/ui" >> ~/.config/openbox/autostart
 
sudo reboot -h
 
sudo reboot -h
 
</pre>
 
</pre>
 
Now you should be booted into openbox with chromium running in kiosk mode, loaded with the node red graphs.
 
Now you should be booted into openbox with chromium running in kiosk mode, loaded with the node red graphs.

Latest revision as of 09:36, 19 May 2020

Purpose

To monitor air temperature, humidity, and air quality.

Initially this was put together to run node-red and mysql on a single pi. This worked ok until the database reached ~200MB and the flash drive I was using started to choke. I also noticed the power draw for the pi, monitor, flash drive, and sensors exceeded the supply.

Work in progress

  • Move database off pi to central home server.
  • Move node-red to central server.
  • Convert pi to pull from sensors using python instead of node-red.
  • Send data to central server using MQTT instead of MySQL

PM Sensor

Base Script

A friend sent me a Nova PM Sensor. Found a script that is readily available[1][2].

  • NOTE : This appears to run in python2. When attempting to run with python3 I received indentation errors. Should be easy to fix, but I am lazy and this is not a critical system.
sudo apt install git-core python-serial python-enum python-pip
mkdir -p /home/pi/build/air && cd /home/pi/build/air
sudo apt install git-core python-serial python-enum mariadb-server python-mysql.connector
wget -O aqi.py https://raw.githubusercontent.com/zefanja/aqi/master/python/aqi.py
echo [] > aqi.json
sed -i 's|/var/www/html/aqi.json|/home/pi/build/air/aqi.json|' aqi.py

At this point we can run the script, see output on the screen, and see the history written to the json file.

./aqi.py 
Y: 18, M: 1, D: 18, ID: 0xbc2d, CRC=OK
PM2.5:  0.0 , PM10:  0.0
PM2.5:  0.7 , PM10:  0.7
PM2.5:  0.6 , PM10:  0.6
PM2.5:  0.7 , PM10:  0.7
  • Now remove the print command and instead create variables for pm2.5 and pm10
              #print("PM2.5: ", values[0], ", PM10: ", values[1])
	      pm25=values[0]
	      pm10=values[1]

Send values to MQTT

  • Work in progress.

[3][4]

  • Install python modules for mqtt
pip install paho-mqtt
  • Update the aqi.py script to imnport the module, and configure as seen below.

NOTE : I am using authentication but not encryption in this example.

import paho.mqtt.client as mqtt

def mqttpub(pm1,pm2):
    broker_address="xxx.xxx.xxx.xxx"
    username="<username on broker>"
    password="<password on broker>"
    mqttc=mqtt.Client()
    mqttc.username_pw_set(username, password)
    mq = "0"
    while mq == "0":
        try:
            mqttc.connect(broker_address)
        except:
            print("Failed MQTT")
            time.sleep(5)
            mq="0"
        else:
            mq="1"
            mqttc.publish("air/livingroom/pm25",pm1)
            mqttc.publish("air/livingroom/pm10",pm2)

...
              mqttpub(pm25,pm10)

Configure for writing to database (LEGACY)

  • NOTE : This is really not necessary except for historical data purposes, and running on the pi is a bit much.

However, I want the history written to a database. Configure mariadb anyway you want, if you have credentials keep them on hand.

create database air;
create user air;
grant all on air.* to 'air';
use air;
create table aqi (pm25 FLOAT, pm10 FLOAT, date DATETIME);
select * from aqi;
Empty set (0.00 sec)

insert into aqi values (2.1, 2.3, now());
Query OK, 1 row affected (0.02 sec)

select * from aqi;
+------+------+---------------------+
| pm25 | pm10 | date                |
+------+------+---------------------+
|  2.1 |  2.3 | 2019-12-20 13:32:18 |
+------+------+---------------------+
1 row in set (0.00 sec)

Now we edit the script to work with mysql[5]
Please note that of this writing I the script searches the entire database for 0 values and deletes them. I need to go back and update to simply not write 0 values in the first place.

  • Edit downloaded script with the following
import mysql.connector

mydb = mysql.connector.connect(
  host="localhost",
  user="air",
  database="air"
)
...
              mycursor.execute('INSERT INTO aqi (pm25, pm10, date) VALUES (%s, %s, %s)' % (pm25,pm10,'now()'))
              mydb.commit()
              mycursor.execute("delete from aqi where pm10 = 0 and pm25 = 0")
...
              mycursor.execute("delete from aqi where date < (now() - interval 1 year)")

Full Modified Script

#!/usr/bin/python -u
# coding=utf-8
# "DATASHEET": http://cl.ly/ekot
# https://gist.github.com/kadamski/92653913a53baf9dd1a8
from __future__ import print_function
import serial, struct, sys, time, json, subprocess,  mysql.connector

time.sleep(120)

mydb = mysql.connector.connect(
  host="localhost",
  user="air",
  database="air"
)

mycursor = mydb.cursor()

DEBUG = 0
CMD_MODE = 2
CMD_QUERY_DATA = 4
CMD_DEVICE_ID = 5
CMD_SLEEP = 6
CMD_FIRMWARE = 7
CMD_WORKING_PERIOD = 8
MODE_ACTIVE = 0
MODE_QUERY = 1
PERIOD_CONTINUOUS = 0

ser = serial.Serial()
ser.port = "/dev/ttyUSB0"
ser.baudrate = 9600

ser.open()
ser.flushInput()

byte, data = 0, ""

def dump(d, prefix=''):
    print(prefix + ' '.join(x.encode('hex') for x in d))

def construct_command(cmd, data=[]):
    assert len(data) <= 12
    data += [0,]*(12-len(data))
    checksum = (sum(data)+cmd-2)%256
    ret = "\xaa\xb4" + chr(cmd)
    ret += ''.join(chr(x) for x in data)
    ret += "\xff\xff" + chr(checksum) + "\xab"

    if DEBUG:
        dump(ret, '> ')
    return ret

def process_data(d):
    r = struct.unpack('<HHxxBB', d[2:])
    pm25 = r[0]/10.0
    pm10 = r[1]/10.0
    checksum = sum(ord(v) for v in d[2:8])%256
    return [pm25, pm10]
    #print("PM 2.5: {} μg/m^3  PM 10: {} μg/m^3 CRC={}".format(pm25, pm10, "OK" if (checksum==r[2] and r[3]==0xab) else "NOK"))

def process_version(d):
    r = struct.unpack('<BBBHBB', d[3:])
    checksum = sum(ord(v) for v in d[2:8])%256
    print("Y: {}, M: {}, D: {}, ID: {}, CRC={}".format(r[0], r[1], r[2], hex(r[3]), "OK" if (checksum==r[4] and r[5]==0xab) else "NOK"))

def read_response():
    byte = 0
    while byte != "\xaa":
        byte = ser.read(size=1)

    d = ser.read(size=9)

    if DEBUG:
        dump(d, '< ')
    return byte + d

def cmd_set_mode(mode=MODE_QUERY):
    ser.write(construct_command(CMD_MODE, [0x1, mode]))
    read_response()

def cmd_query_data():
    ser.write(construct_command(CMD_QUERY_DATA))
    d = read_response()
    values = []
    if d[1] == "\xc0":
        values = process_data(d)
    return values

def cmd_set_sleep(sleep):
    mode = 0 if sleep else 1
    ser.write(construct_command(CMD_SLEEP, [0x1, mode]))
    read_response()

def cmd_set_working_period(period):
    ser.write(construct_command(CMD_WORKING_PERIOD, [0x1, period]))
    read_response()

def cmd_firmware_ver():
    ser.write(construct_command(CMD_FIRMWARE))
    d = read_response()
    process_version(d)

def cmd_set_id(id):
    id_h = (id>>8) % 256
    id_l = id % 256
    ser.write(construct_command(CMD_DEVICE_ID, [0]*10+[id_l, id_h]))
    read_response()


if __name__ == "__main__":
    cmd_set_sleep(0)
    cmd_firmware_ver()
    cmd_set_working_period(PERIOD_CONTINUOUS)
    cmd_set_mode(MODE_QUERY);
    while True:
        cmd_set_sleep(0)
        for t in range(15):
            values = cmd_query_data();
            if values is not None and len(values) == 2:
#              print("PM2.5: ", values[0], ", PM10: ", values[1])
	      pm25=values[0]
	      pm10=values[1]
              mycursor.execute('INSERT INTO aqi (pm25, pm10, date) VALUES (%s, %s, %s)' % (pm25,pm10,'now()'))
              mydb.commit()
              mycursor.execute("delete from aqi where pm10 = 0 and pm25 = 0")
              time.sleep(2)


        print("Going to sleep for 1 min...")
        cmd_set_sleep(1)
        time.sleep(60)
        mycursor.execute("delete from aqi where date < (now() - interval 1 year)")

Configure to run at boot by adding the following to your crontab

@reboot /home/pi/build/air/air.py

Tempurature / Humidity

Using Python to pull from Sensor

Place holder

Using Node-Red to pull from Sensor

NOTE : See https://michaelwiki.geekgalaxy.com/index.php/Raspberry_Pi_Environmental_Monitoring#Graph_Display

For this one I decided to use node-red to pull the values directly. It supports raspberry pi GPIO and the DHT-11 sensor I have on hand[6].

  • Install the node-red-contrib-dht-sensor packages (I used the web gui)
sudo apt install wiringpi
  • Add inject module with as timestamp. I chose to inject every 5 seconds.
  • Add the rpi-dht11 node to the flow, I gave it the following values
    • Sensor : DHT11
    • Pin numbering : WiringPi (rev.1)
    • Pin number : 7
  • The dht11 or 22 node will break out to a function and change module.
  • This breaks out to a function node that changes C to F [7]
var tempc = msg.payload;
tempf = tempc * 9/5 + 32;
tempf = Math.round(tempf * 10) / 10;
msg.payload = tempf;
return msg;
  • Now use a change module to change msg.payload to msg.humidity.
  • Both the function and change modules connect to MQTT out modules that connect to my MQTT server.

Graph Display

The same pointed me to node-red for the display. What I really wanted was a nerdy ncurses display, but node-red should suffice until I learn ncurses.

Install Node-Red

[8] When running the install script, make sure the pi settings are applied.

bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)
sudo systemctl enable --now nodered

Install the dashboard components[9]

Configure PM 2.5 Flow

My flow consists of

  1. An inject node with the timestamp injection using topic
    select round(avg(pm25)) from aqi where date < now() - interval 24 hour;
  2. A mysql node with the database location and credentials.
  3. A change node with set to
    msg.payload[0].pm25
  4. A gauge node set for values between 0.1 and 30[10].



Configure Pi for Kiosk Mode

[11]Please note that this is not the most secure configuration. In my case the terminal is located in my house, on a segregated wifi network and broadcast domain, and the information contained is air quality metrics so impact of compromise is low.

sudo apt-get install lightdm
sudo raspi-config

At this point go to boot options, desktop /CLI, then select the desktop autologin. Close the tool and reboot.

  • Note : I ran into a login loop where I was unable to auto-login and manually logging in did not work. I found a forum[12] mentioning to install a set of x11 packages as well as openbox. I went ahead with this as I used open box years ago and found it very light weight and useful for such tasks.
sudo apt-get install xserver-xorg-core xserver-xorg-input-all \
xserver-xorg-video-fbdev libx11-6 x11-common \
x11-utils x11-xkb-utils x11-xserver-utils xterm lightdm openbox

After running the previous the system now logs me into openbox. At this point we want openbox to start chrome in kiosk mode.[13]

sudo apt install chromium-browser
mkdir -p ~/.config/openbox && cp /etc/xdg/openbox/* ~/.config/openbox
echo -e "xset s noblank\nxset s off\n\xset -dpms\nunclutter &\nchromium-browser --kiosk http://127.0.0.1:1880/ui" >> ~/.config/openbox/autostart
sudo reboot -h

Now you should be booted into openbox with chromium running in kiosk mode, loaded with the node red graphs.