Airy Canary

What it is: A script that uses PurpleAir's public API to log air quality readings and show an alert when air pollution has passed a certain threshold.

How it's built: The program is a ruby script with a bash script wrapping it. The ruby script uses cURL to hit the API and then parses and logs the response. The bash wrapper controls the popup alerts.

How to use it: Search PurpleAir's docs to find the id of the nearest air sensor, then pass that id to the program on the command line or in a cron job.

THE STORY

If you don't live in the Western United States, let me bring you up to speed--in August and September of 2020 massive wildfires caused widespread destruction and blanketed the region with the most polluted air on the planet.

Here in Portland we had the worst air quality of any city in the world for 12 straight days. This photo shows the deterioration of air quality over the span of five days:

Those last two images are what it looked like out of my living room window for nearly two weeks. Everyone was confined indoors. We shoved towels into the doorjambs and taped hvac filters to the backs of box fans to create homemade air filters. We still had headaches and burning eyes and scratchy throats the whole time. We checked the air quality readings constantly.

The primary source I was using to monitor air quality was PurpleAir--they have a lot of sensors in their system, one that's just three blocks from my house. After visiting their site over and over for days I thought to myself, I wonder if they have a public API? Answer: yes. So I built Airy Canary.

At this point I should also let you know that my home office is an uninsulated garage. I love working in there but it leaves me very exposed to polluted air. So I wanted airy canary to do two things: 1) create a log of the air quality during my work hours so that I can glance at it and see over time what level of pollution I am exposing myself to, and 2) show an alert if the air quality gets above a certain threshold. This way I can monitor the safety of my work environment without having to go to PurpleAir's website every 20 minute. It saves me time and sanity.

I decided I wanted to create a ruby script to do most of the work. This is because ruby makes it so easy to work with strings and ruby's CSV library makes generating and maintaing a log simple as can be. Goal number 1 accomplished. But the most interesting piece of the design puzzle was the alert. For that I needed to write a bash script that acted as a wrapper around the ruby program.

!#/bin/bash
filepath="$( cd "$( dirname "$0" )" && pwd )/canary.rb"
sensor_id=$1
output=$(ruby $filepath $sensor_id)
if [ -n "$output" ]
then
 zenity --warning --title="Air Quality Warning" --text="$output" --width=600 --display=:0.0 2>/dev/null
fi

There's a few things going on here. First, we create a variable filepath. That snippet $( cd "$( dirname "$0" )" && pwd ) basically says, find whatever directory this script ran from (dirname "$0"), go there (cd), then tell me where you are (pwd). Seems a bit roundabout but it's the only way to be sure you are getting the absolute path of the directory. Since we will ultimately be using cron to run this program, a simple pwd will give us the path where cron is executing the file, not necessarily the path where the file actually lives.

This is important because as you can see I concatenate /canary.rb to the end of the string so that the bash wrapper can call the ruby script using the absolute path.

Next, we assign the varaible sensor_id to $1. What is $1 you ask? In Bash, arguments passed to a script are stored in special variables called Positional Parameters. So the first argument passed is stored in $1, the second is stored in $2, and so on. (You will also notice that the command dirname "$0" described above makes use of the Positional Parameter $0, which holds the actual command being passed to the shell to run the script, i.e. the name of the file being executed.)

This means that when we run the bash script, we need to pass the sensor id as an argument to the script. Using an argument to set the id makes the program much more useful--I can pass the id of the sensor closest to my house, and somebody across town can use the same script by passing a different id as an argument.

The rest of the bash wrapper script is an if statement that checks whether the ruby script outputs a warning for poor air quality ([ -n "$output" ] asks 'does this variable hold a non-empty value?'), and if so it uses Zenity to display an alert on the screen. Zenity is a standard utility for Ubuntu distributions that is used for generating dialog boxes.

My cronjob looks like this: */30 * * * * /home/dylan/src/airy-canary/canary-wrapper.sh 22903 where 22903 is the id of the sensor I want data from. If you've read my article on my take-a-break reminder you know that I use crontab to set my linux cron jobs, and that this particular cron syntax means "run every 30 minutes". It is really satisfying to know that I can work in my garage office and not have to check the air quality every 30 minutes because I know my computer is doing it for me. I hope this article inspires you to build a cron helper for yourself. Thanks for reading!