Bash Garage Door Monitor on Raspberry Pi
This post describes the bash script that I use to enable my Raspberry Pi to Email updates on the state (open/closed) of each of my garage doors.
This is a nice use of the bash 'trap' statement used to handle events, such as the garage door opening or closing. Note that 'trap' is a bash shell function, not a linux command. Although it may be fun to type in 'man trap' - you'll not get any help. Use 'man bash' to get help on the trap function.
Firstly, we have the statement at the top of the file that says to use /bin/bash. If you miss this out, things tend to work OK while developing the script, but you'll run into issues once you try and automate things, as you could end up with running /bin/sh rather than bash, causing errors.
#!/bin/bash
Next we define the pin names. This is just the name for each garage door, or more specifically who uses the door and which GPIO pin is connected to the door. (I use a Magnetic Reed Switch for each GPIO). For this i'm using pins 23, 24 and 25.
pinname[23]="Milly"
pinname[24]="Molly"
pinname[25]="Mndy"
Next we set a flag, which is used to check if the tasks has kicked off, it really has no functional value
touch /tmp/alarmon
The childproc variable is set to NONE. Childproc is used to track the child process that is generated when a door opens.
childproc=NONE
waitforit is a function that is used handle a garage door that's been left open too long (10 minutes). If the garage remains open too long, then an Email is sent. waitforit is the child process that we track with childproc.
waitforit() {
pin=$1
zone=$2
trap 'exit' SIGTRAP
sleep 600 #wait ten minutes
#if we get here - then we have not been killed, so garage must be open.
#we could add another check, but we will trust the system
echo "zone${zone}(${pinname[pin]}): $(date): $status"|mail -s "Garage Still Open:${pinname[pin]}:$status" me@me.me
#set trap so that it email out a "closed" and then exits.
trap 'echo "zone${zone}(${pinname[pin]}): $(date): $status"|mail -s "Garage Garage Closed:${pinname[pin]}" me@me.me;exit' SIGTRAP
#wait forever
while true
do
sleep 60
done
}
To break down the above, 'pin' is used to state which door is open. 'Zone' is not really used. (This is due to code reuse from my Alarm bash, it doesn't offer any real value here.)
The "trap 'exit' SIGTRAP" is used to handle signals. If the process received the SIGTRAP signal, then the 'exit' statement is performed, meaning that we exit the process and no Email is generated.
We then wait 10 minutes (sleep 600), once that's complete, then we Email me@me.me (may want to change the Email) with a notification that the "Garage Still Open". Note that from the look of this function alone, it may look like we'll always send an Email, but that's not the case, but you'll have to look at the 'zone' function below to find out why (and how).
The '${pinname[pin]}' is using the pin variable to index into the pinname[] array, hence we end up with "Molly" rather than "1" in the Email.
Once we've emailed, then we set a replacement SIGTRAP trap. This time rather than just exiting when we receive the SIGTRAP, we send an email confirming that the garage is closed and then exit. (Note that this email line has been cleaned up from the previous email line - no useless zone information here)
We then go into an infinite loop waiting for the end of the world, or more specifically a SIGTRAP signal.
The zone function is the main monitoring function. This monitors a pin (garage door) and when the pin changes state it wakes up and works out what to do.
zone()
{
pin=$1
zone=$2
For the above, we're setting the pin and zone based on parameters passed in when the function is invoked. Note that zone isn't really used, it's just a hangover from code reuse.
We then save the process ID. This is for monitoring/debugging. It's not used by this script.
echo $! >>/tmp/monitor.pids
Next we set the pin we're monitoring to a known state (up). We do this to ensure that it's set the way we want it set. As we're forcing the pin to GND when the garage door is closed this means that it will go high/up when the garage door opens. (because we've connected the loop from a 3.3v pin to the GND pin, this will be more obvious once I've posted the pinout)
Note that the gpio utility that we're using is from http://wiringpi.com/. It appears that this utility is now pre-installed with the Raspberry Pi Debian and the creator can go back to sitting on a wall, but if you're running an older version, this is where you'd get the download.
/usr/local/bin/gpio mode $pin up
I put a sleep in, just to ensure that signals are stable before we begin monitoring the pin/garage door. Then we capture the current state of the door. (Appears a slight defect here in that if the garage door is open when you kick off the script then you will not get an email notification that the door is open.)
sleep 1
#current state of door
current=$( /usr/local/bin/gpio read $pin )
We now start the main loop waiting for exciting things to happen.
while true
do
We use the gpio utility to "wfi" - Wait for interrupt. Which really means that we're waiting for the pin to change state (from 0 to 1 or up to down or down to up, etc...). The script will halt execution at this point waiting for the pin to change state.
/usr/local/bin/gpio wfi $pin both
Once the script has passed the above, something must have happened - a door is opening or has closed. Whenever a pin changes state, there is a chance of a bouncing signal from logical 0 to 1 and back again over a short period of time. We could have some elaborate debounce check, but it's sometimes easier to just wait, as it's not life threatening.
sleep 1
Now we obtain the current state of the pin. If this is different from what we initially recorded, then we use the new state to take action
state=$( /usr/local/bin/gpio read $pin )
if [[ $state != $current ]]
then
date=$( date )
If the state is "1", which means "open" (as we were pulling the pin up), then we kick off the 'waitforit' function as a child process, passing in the pin and zone. We save the child process number in childproc.
if [[ "$state" == "1" ]]
then
status="open"
#kick off timer
waitforit $pin $zone &
childproc=$!
If the state is not "1" (ie, "0"), which means that the garage door is closed, then if we already have a child process, we'll kill ii off by sending a SIGTRAP signal to it. As the 'waitforit' function has a receiver for this signal (trap) then it will handle the SIGTRAP based on the last trap statement.
else
status="closed"
if [[ "$childproc" != "NONE" ]]
then
kill -SIGTRAP $childproc
childproc=NONE
fi
fi
Next we generate some logs. These are used to generate reports. (Email summary, etc...)
echo "zone${zone}(${pinname[pin]}): $date: $status" >>/tmp/activity.log
echo "zone${zone}(${pinname[pin]}): $date: $status" >>/tmp/motion.log
We then create a log that can be more easily used to generate a chart.
S=$(( $(date +"(10#%H * 60) + 10#%M") ))
echo $S,$zone,$state >>/tmp/plot.log
We also have a flag that's used to immediately send an Email if there's no one home. Useful if you're away and want to know if the garage door opens.
someonehome=$( ls -l /mnt/stuff/home/*.home|wc -l )
if [[ -f /tmp/alarmon ]] && [[ $someonehome = 0 ]]
then
echo "zone${zone}(${pinname[pin]}): $date: $status"|mail -s "Garage:${pinname[pin]}:$status" me@me.me
fi
The last part of the zone function is to update the current state of the garage door.
current=$state
fi
done
}
The last part of the script starts the monitoring (the 'zone' function) for each garage door. Each door will have its own process. The first line invokes zone passing the pin number 23 and zone 1.
As each of the instances is going to run in the background (its own process), this script will exit when you run it, but the processes will remain running in the background
#Note: may want to test the level low, before driving high
zone 23 1 &
zone 24 2 &
zone 25 3 &
This is a nice use of the bash 'trap' statement used to handle events, such as the garage door opening or closing. Note that 'trap' is a bash shell function, not a linux command. Although it may be fun to type in 'man trap' - you'll not get any help. Use 'man bash' to get help on the trap function.
Firstly, we have the statement at the top of the file that says to use /bin/bash. If you miss this out, things tend to work OK while developing the script, but you'll run into issues once you try and automate things, as you could end up with running /bin/sh rather than bash, causing errors.
#!/bin/bash
Next we define the pin names. This is just the name for each garage door, or more specifically who uses the door and which GPIO pin is connected to the door. (I use a Magnetic Reed Switch for each GPIO). For this i'm using pins 23, 24 and 25.
pinname[23]="Milly"
pinname[24]="Molly"
pinname[25]="Mndy"
Next we set a flag, which is used to check if the tasks has kicked off, it really has no functional value
touch /tmp/alarmon
The childproc variable is set to NONE. Childproc is used to track the child process that is generated when a door opens.
childproc=NONE
waitforit is a function that is used handle a garage door that's been left open too long (10 minutes). If the garage remains open too long, then an Email is sent. waitforit is the child process that we track with childproc.
waitforit() {
pin=$1
zone=$2
trap 'exit' SIGTRAP
sleep 600 #wait ten minutes
#if we get here - then we have not been killed, so garage must be open.
#we could add another check, but we will trust the system
echo "zone${zone}(${pinname[pin]}): $(date): $status"|mail -s "Garage Still Open:${pinname[pin]}:$status" me@me.me
#set trap so that it email out a "closed" and then exits.
trap 'echo "zone${zone}(${pinname[pin]}): $(date): $status"|mail -s "Garage Garage Closed:${pinname[pin]}" me@me.me;exit' SIGTRAP
#wait forever
while true
do
sleep 60
done
}
To break down the above, 'pin' is used to state which door is open. 'Zone' is not really used. (This is due to code reuse from my Alarm bash, it doesn't offer any real value here.)
The "trap 'exit' SIGTRAP" is used to handle signals. If the process received the SIGTRAP signal, then the 'exit' statement is performed, meaning that we exit the process and no Email is generated.
We then wait 10 minutes (sleep 600), once that's complete, then we Email me@me.me (may want to change the Email) with a notification that the "Garage Still Open". Note that from the look of this function alone, it may look like we'll always send an Email, but that's not the case, but you'll have to look at the 'zone' function below to find out why (and how).
The '${pinname[pin]}' is using the pin variable to index into the pinname[] array, hence we end up with "Molly" rather than "1" in the Email.
Once we've emailed, then we set a replacement SIGTRAP trap. This time rather than just exiting when we receive the SIGTRAP, we send an email confirming that the garage is closed and then exit. (Note that this email line has been cleaned up from the previous email line - no useless zone information here)
We then go into an infinite loop waiting for the end of the world, or more specifically a SIGTRAP signal.
The zone function is the main monitoring function. This monitors a pin (garage door) and when the pin changes state it wakes up and works out what to do.
zone()
{
pin=$1
zone=$2
For the above, we're setting the pin and zone based on parameters passed in when the function is invoked. Note that zone isn't really used, it's just a hangover from code reuse.
We then save the process ID. This is for monitoring/debugging. It's not used by this script.
echo $! >>/tmp/monitor.pids
Next we set the pin we're monitoring to a known state (up). We do this to ensure that it's set the way we want it set. As we're forcing the pin to GND when the garage door is closed this means that it will go high/up when the garage door opens. (because we've connected the loop from a 3.3v pin to the GND pin, this will be more obvious once I've posted the pinout)
Note that the gpio utility that we're using is from http://wiringpi.com/. It appears that this utility is now pre-installed with the Raspberry Pi Debian and the creator can go back to sitting on a wall, but if you're running an older version, this is where you'd get the download.
/usr/local/bin/gpio mode $pin up
I put a sleep in, just to ensure that signals are stable before we begin monitoring the pin/garage door. Then we capture the current state of the door. (Appears a slight defect here in that if the garage door is open when you kick off the script then you will not get an email notification that the door is open.)
sleep 1
#current state of door
current=$( /usr/local/bin/gpio read $pin )
We now start the main loop waiting for exciting things to happen.
while true
do
We use the gpio utility to "wfi" - Wait for interrupt. Which really means that we're waiting for the pin to change state (from 0 to 1 or up to down or down to up, etc...). The script will halt execution at this point waiting for the pin to change state.
/usr/local/bin/gpio wfi $pin both
Once the script has passed the above, something must have happened - a door is opening or has closed. Whenever a pin changes state, there is a chance of a bouncing signal from logical 0 to 1 and back again over a short period of time. We could have some elaborate debounce check, but it's sometimes easier to just wait, as it's not life threatening.
sleep 1
Now we obtain the current state of the pin. If this is different from what we initially recorded, then we use the new state to take action
state=$( /usr/local/bin/gpio read $pin )
if [[ $state != $current ]]
then
date=$( date )
If the state is "1", which means "open" (as we were pulling the pin up), then we kick off the 'waitforit' function as a child process, passing in the pin and zone. We save the child process number in childproc.
if [[ "$state" == "1" ]]
then
status="open"
#kick off timer
waitforit $pin $zone &
childproc=$!
If the state is not "1" (ie, "0"), which means that the garage door is closed, then if we already have a child process, we'll kill ii off by sending a SIGTRAP signal to it. As the 'waitforit' function has a receiver for this signal (trap) then it will handle the SIGTRAP based on the last trap statement.
else
status="closed"
if [[ "$childproc" != "NONE" ]]
then
kill -SIGTRAP $childproc
childproc=NONE
fi
fi
Next we generate some logs. These are used to generate reports. (Email summary, etc...)
echo "zone${zone}(${pinname[pin]}): $date: $status" >>/tmp/activity.log
echo "zone${zone}(${pinname[pin]}): $date: $status" >>/tmp/motion.log
We then create a log that can be more easily used to generate a chart.
S=$(( $(date +"(10#%H * 60) + 10#%M") ))
echo $S,$zone,$state >>/tmp/plot.log
We also have a flag that's used to immediately send an Email if there's no one home. Useful if you're away and want to know if the garage door opens.
someonehome=$( ls -l /mnt/stuff/home/*.home|wc -l )
if [[ -f /tmp/alarmon ]] && [[ $someonehome = 0 ]]
then
echo "zone${zone}(${pinname[pin]}): $date: $status"|mail -s "Garage:${pinname[pin]}:$status" me@me.me
fi
The last part of the zone function is to update the current state of the garage door.
current=$state
fi
done
}
The last part of the script starts the monitoring (the 'zone' function) for each garage door. Each door will have its own process. The first line invokes zone passing the pin number 23 and zone 1.
As each of the instances is going to run in the background (its own process), this script will exit when you run it, but the processes will remain running in the background
#Note: may want to test the level low, before driving high
zone 23 1 &
zone 24 2 &
zone 25 3 &
Comments
Post a Comment