Difference between revisions of "Raspberry Pi Environmental Monitoring"
Michael.mast (talk | contribs) |
Michael.mast (talk | contribs) |
||
(26 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 | ||
− | == | + | ==PM Sensor== |
− | ===Base | + | ===Base Script=== |
− | A friend sent me a Nova PM Sensor. | + | 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]) | |
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> | ||
− | * | + | *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> | ||
− | * | + | *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== | ==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 | + | #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. | + | #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 235: | Line 320: | ||
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 -e " | + | 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
Contents
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.
- 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
- An inject node with the timestamp injection using topic
select round(avg(pm25)) from aqi where date < now() - interval 24 hour;
- A mysql node with the database location and credentials.
- A change node with set to
msg.payload[0].pm25
- 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.
- ↑ https://hackernoon.com/how-to-measure-particulate-matter-with-a-raspberry-pi-75faa470ec35
- ↑ https://openschoolsolutions.org/measure-particulate-matter-with-a-raspberry-pi/
- ↑ https://pypi.org/project/paho-mqtt/#constructor-reinitialise
- ↑ http://www.steves-internet-guide.com/into-mqtt-python-client/
- ↑ https://www.w3schools.com/python/python_mysql_insert.asp
- ↑ https://flows.nodered.org/node/node-red-contrib-dht-sensor
- ↑ https://groups.google.com/forum/#!topic/node-red/bkmv6kMAFlM
- ↑ https://nodered.org/docs/getting-started/raspberrypi
- ↑ https://flows.nodered.org/node/node-red-dashboard
- ↑ https://en.wikipedia.org/wiki/Air_quality_index
- ↑ https://pimylifeup.com/raspberry-pi-kiosk/
- ↑ https://www.raspberrypi.org/forums/viewtopic.php?t=154190
- ↑ https://www.raspberrypi.org/forums/viewtopic.php?t=8298