Overly Complex Doorbell

I wanted to add some kind of doorbell indication to my office, and was almost ready to start buying parts for wired doorbell sensor like this one on hackaday.com , but I wasn’t sure how well it would work with the Beagle Bone Black that’s running my digitemp sensor network in the garage. I’d have to run about 20’ of wire over to the sensor from the Beagle and that was bound to effect the reliability.

So I came up with a ‘better’ solution that doesn’t require stringing any more wires. Just a bit of duct-taped software. I’m running Strix with a number of security cameras, including one on the front door right over the doorbell. Strix uses Motion to handle detection of movement and creation of images that Strix turns into movies. Ends up Motion has a nice feature called area_detect that can be used to run a script if movement is detected in one or more of the 9 sections of an image. This is much easier than using on_motion_detected because I don’t need to limit it to one part of the image. 3 of the 9 sections work just fine for eliminating areas that I don’t care about for a doorbell feature.

So I added this to my front door Motion config file:

# Run a script when motion is detected near the door
area_detect "589"
# Pass the width, height, and pixels changed of the motion area
on_area_detected "/home/strix/bin/doorbell %i %J %D"

And wrote a simple python3 script to log the event and make a POST to my desktop:

#!/usr/bin/python3

import argparse
import time
import urllib.request

URL="http://desktop.local:8000/dingdong"

def setup_argparse():
    parser = argparse.ArgumentParser(description="Virtual Motion Doorbell")
    parser.add_argument("width", type=int, help="Width of motion area")
    parser.add_argument("height", type=int, help="Height of motion area")
    parser.add_argument("pixels", type=int, help="Number of changed pixels")

    return parser.parse_args()

def main():
    opts = setup_argparse()

    with open("/var/tmp/doorbell.log", "a") as f:
        f.write("%s: W=%s H=%s P=%s\n" % (time.ctime(), opts.width, opts.height, opts.pixels))

        # Ring the 'doorbell' on ohop
        urllib.request.urlopen(URL, b"")

if __name__=='__main__':
    main()

I’m passing in the size of the movement area, and the number of pixels changed so that if I need to tweak things in the future I can.

On my desktop there is a totally insecure little service running that listens for the POST and runs a script. I wrote this in Go:

package main

import (
        "flag"
        "fmt"
        "log"
        "net/http"
        "os/exec"
        "time"
)

/* commandline flags */
type cmdlineArgs struct {
        Port   int    // Port to bind to
        Script string // Script to run when POST to /dingdong is received
        Debug  bool   // Log debugging information
}

/* commandline defaults */
var cmdline = cmdlineArgs{
        Port:   8889,
        Script: "/bin/false",
        Debug:  false,
}

/* parseArgs handles parsing the cmdline args and setting values in the global cmdline struct */
func parseArgs() {
        flag.IntVar(&cmdline.Port, "port", cmdline.Port, "Port to bind to")
        flag.StringVar(&cmdline.Script, "script", cmdline.Script, "Script to execute when POST is received")
        flag.BoolVar(&cmdline.Debug, "debug", cmdline.Debug, "Log debugging information")

        flag.Parse()
}

func main() {
        parseArgs()

        fmt.Printf("Listening for doorbell on port %d\n", cmdline.Port)
        fmt.Printf("Running script: %s\n", cmdline.Script)

        http.HandleFunc("/dingdong", func(w http.ResponseWriter, r *http.Request) {

                // Is it a POST?
                if r.Method != "POST" {
                        http.Error(w, "", http.StatusMethodNotAllowed)
                        return
                }
                if cmdline.Debug {
                        fmt.Printf("%s: DING DONG!\n", time.Now().Local())
                }

                // Run the script and get the result code
                cmd := exec.Command(cmdline.Script)
                err := cmd.Run()
                if err != nil {
                        http.Error(w, err.Error(), http.StatusServiceUnavailable)
                } else {
                        fmt.Fprintf(w, "OK\n")
                }
        })

        log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", cmdline.Port), nil))
}

Pass it the port to listen on, and the script to run. It’s no more complicated than the examples in the Go net/http documentation.

The final piece is the script that opens the Strix camera page in a new tab of my running browser.

#!/bin/sh
DISPLAY=:0
if xwininfo -d $DISPLAY -root -children | grep -i "Strix"; then
    exit 0
fi
exec firefox --new-tab http://cameras.brianlane.com

This looks at the list of window titles and checks to see if the Strix tab is currently selected (it will only be listed if selected) to prevent a stackup of open tabs when I’m away from my desk. If it isn’t already open/selected it opens a new tab with the Strix page to get my attention.

It’s way more complex than a wired doorbell, and I expect it to fail in interesting ways, but it was easy to throw together this morning and seems to be working ok for now. So I’d call it ‘Good Enough(tm)’ for the moment.