Raspberry PI garage door opener - Part 2

Project recap

Having completed the hardware portion of this project, I now moved onto the software glue code to get it operational.

Software

The R-PI

I got a raspberry pi zero W to use for this project. I installed Raspbian Lite as I didn’t need it to be doing much other than ‘listen and interact with GPIO’. You could use a lighter distro here too, but this was one I was comfortable with and did what I need without a fuss. The only change I made after completion of this project, was to change the filesystem to ‘overlay’ or a read-only filesystem. This should save a ton of wear and tear on the R-PI SD CARD and hopefully save it if the power goes out unexpectantly. This had to be done after the programming was completed and setup though. Preferences–>Raspberry Pi Configuration–>Performance–>Overlay File System–Configure… and enable it, reboot.

SMS Gateway

I wanted my wife and family to be able to easily action the opener when needed. This meant quick and painless for them. For me it meant not spending time making a new dedicated app for the phone to do this. I also needed something a non technical person could use without obnoxious steps. Having a publically accessible web page with no authentication as a bookmark also didn’t appeal to me. Adding an extra step with basic auth to just open the garage door via a web page also wouldn’t be ease of use or go over well. So I needed to make it as easy as possible while being secure, to receive commands externally and action the response. We normally always have our phones with us already so I thought to make it SMS based.

After doing some research and looking at options, I decided to use SignalWire for the SMS gateway portion. I’ve used them before for a different project so felt comfortable in their service doing what I needed. Registering for an account and a phone number allows you to send or receive SMS with that number. Using signalwire you can enable the API, create a token and then setup Webhooks that can run when the number receives an SMS. For mine, I pointed it to a web page created just for this which authenticates the request and then sends it to my golang custom processing program to handle. Paying for the number itself and the SMS charges have been about $5 up front, and around $10 a year to maintain a balance. Extremely cost effective for what it enables! If you chose this route, you’ll need a domain name and a web server software running. I already had both, so no issues there.

Web presence handler

After receiving an SMS, Signalwire passes on details according to their API docs. My web server with a public accessible domain is already running, I just needed to write something to handle the requests recieved. For this portion I used PHP as I was already using it for other purposes on the web server and was easy to integrate. The important parts are, authenticate the request, verify it is proper, do something based on the content and then send a reply that we’ve done something.

<?php
//Server variables we expect from signal wire
$isValid = isset($_POST, $_POST['To'], $_POST['From'], $_POST['AccountSid'], $_POST['Body'], $_GET['key']);
$allowedFrom = array('+number','+number');
$allowedTo = array('+signalwire_number');

if (!$isValid || (!in_array($to, $allowedTo) || !in_array($from, $allowedFrom) ||  $sid != "<account sid>" || $key != "<assigned unique key>") {) {
   //If any of the expected server variables or blank, missing, or not correct, exit
   http_response_code(400);
   exit();
}

//[... snip ...]

$randomOpen = array('Welcome home', 'Knock knock!', 'Open Sesame!');
$randomClose = array('Closey Sesame!', 'Closing the door', 'Can\'t go home, but you can\'t stay here. We\'re closed');
$randomPause = array('Don\'t forget to renable later!', 'Alerts paused');

After that is authenticated then we do some additional processing on the text to validate and clean it up (not shown). Here’s the basics of the response to signalwire after processing. doorFunc handles sending the proper command to the golang program listening for it on the r-PI.

<?php
  switch ($verb) {
	case "status":
          echo <<<EOE
          <?xml version="1.0" encoding="UTF-8"?>
           <Response>
            <Message>$currStat and Monitoring is $currMonit</Message>
           </Response>
          EOE;
          break;

	case "open":
            $res = doorFunc("open left");
            
            if ($res >0 ) {
            	echo <<<EOE
                 <?xml version="1.0" encoding="UTF-8"?>
                  <Response>
                   <Message>${msg}
                 ERROR sending the command, try again
                 </Message>
                  </Response>
                 EOE;
           } else {
                  $msg = $randomOpen[rand(0, count($randomOpen) - 1)];
           
                   echo <<<EOE
                     <?xml version="1.0" encoding="UTF-8"?>
                      <Response>
                       <Message>${msg}</Message>
                      </Response>
                     EOE;
                  break;
          }

         default:
              $msg = $randomError[rand(0, count($randomError) - 1)];
              echo <<<EOE
               <?xml version="1.0" encoding="UTF-8"?>
                <Response>
                 <Message>${msg}.
               Valid actions are:
               pause
               unpause
               open <left or right>
               close <left or right>
               status
               </Message>
                </Response>
               EOE;

    }

The same type of logic is done for the right door and for closing both as well. This isn’t the best PHP code in the world, but it does get the job done securely enough for my current needs. I also have monitoring on the PI to tell if any of them are ‘open’ based on the sensor status, after 15 minutes or so. That way in case we forget a door being open alerts get sent and we can then close the door! The pause/unpause are for those alerts. This is handled via monit and gotify for push alerts to our phone. Monit checks the status of the web page (above) and if it parses out that the Left Door or Right Door is open for longer than two checks, it sends off an alert to the Gotify server. Gotify in turn notifies our phones of the situation. If you haven’t looked into either of these tools, I highly recommend them each. Great monitoring capabilities for Linux servers and push notifications for Android phones.

Godor program

For this portion I needed a program capable of not using a ton of memory or resources as it would be running long-term on the R-PI zero. I know python and I know python has some good capabilties with the R-PI and libraries, but I chose to use golang. I wanted to learn more about the language with a useful project and make sure it would be as lightweight as possible without writing C. I settled on a golang program to be able to do the following:

This came to be known as Godor. It’s not a difficult program and Golang made it almost trivial to do. The basics of it are thus:

func main() {

	err := rpio.Open()
	if err != nil {
		panic(fmt.Sprint("Unable to open gpio, exiting: %v", err.Error()))
	}


	defer func() {
		rpio.Close()
	}()


	startup()

	listenForConns()

}

func startup() {
	left_pin := rpio.Pin(4)
	left_pin.High()
	left_pin.Output()

	right_pin := rpio.Pin(21)
	right_pin.High()
	right_pin.Output()

	left_pin.PullUp()
	right_pin.PullUp()

	ldoor := rpio.Pin(27)
	ldoor.Input()
	ldoor.PullUp()

	rdoor := rpio.Pin(26)
	rdoor.Input()
	rdoor.PullUp()

}

func listenForConns() {
	ln, err := net.Listen("tcp",":<port>")

	if err != nil {
	    panic(err)
	}

	defer ln.Close()

	fmt.Println("Listening for connections on TCP port <port>")

	for {
		ServerConn, err := ln.Accept()
		if err != nil {
			panic(err)
		}

		go parseInput(ServerConn)

	}


}

// [snip]

	if lsStat == 0 {
		response(conn, "Opening\n")
		pin := rpio.Pin(4)
		pin.Low()
		time.Sleep(time.Second)
		pin.High()
	} else {
		fmt.Println("Attempted to OPEN left door, but it's already opened")
		response(conn, "Opened\n")

This instructs golang to use the rpio library to open connections to the header pins and begin a network listener on the defined port and run the program to parse it in a goroutine. Strictly not needed for such a simple program but I didn’t want it to hang/bottleneck resources for any reason either. The startup function sets the pins based on the GPIO numbers not the BCM physical layout. The left_pin and right_pin are for triggering the relay and thus the garage door opener to toggle. I explicitly set these to output mode as they will be sending a signal to the relay. The ldoor and rdoor are the sensor wires and we change those to an input status, as we’ll only be reading from them not triggering them. The final bit of code is from a larger function called parseinput that the goroutine feeds into. Here, I literally validate and ‘parse’ the input received from the network connection.

I also set the input pin states in the startup() function to pull up. This is so they register correctly as the open/close status for the sensors. In the code below that, I checking the current status of the left door and making sure it is actually closed before trying to trigger an open. This is because I don’t really do any other processing to remember the state of the door being opened or closed and trust the Seco-Larm sensors current reading to determine such. Once that checks out I toggle the gpio pin associated with the relay in order to action it! There are additional handling for the right door and for checking the current status of both. That’s about all there is to the golang program. Listen for incoming network connections, do something with them, and let the network caller know we did something with it.

This code, once tested and compiled, then lives on the R-PI awaiting verified instructions. I use GoReleaser to handle the versioning and making the build process very streamlined. After tagging a release ready to build, and configuring the .goreleaser.yml with the following:

builds:

- env:
  - CGO_ENABLED=0

  goos:
    - linux

  goarch:
    - arm

We push the changes to git and run goreleaser. This makes a changelog and build artifacts for releases of the newer version automatically compiled for the R-PI. Then I copy that over and start it up. I have a systemd service handling the start/stop of the golang program, set to start when the system is booted. That handles a PI restart and hopefully keeps it accessible.

[Unit]
Description=Godor - Garage Door GPIO
StartLimitIntervalSec=1
Wants=network-online.target

[Service]
Type=simple
Restart=always
RestartSec=5
User=godor
Group=godor
WorkingDirectory=/home/godor
ExecStart=/usr/bin/godor

[Install]
WantedBy=default.target

Finale

There it is! Raspberry-PI connected to physical garage door openers, while listening on the network and via SMS for instructions. SMS -> Web -> executable -> physical connections. Much more reliable than the battery operated dongles we had. In the past year of operating, only a few issues but nothing show stopping or major. The major issue currently seems to be when SMS traffic on the network is very high, the Godor SMS won’t be received and thus the physical door not opened or closed. This doesn’t happen often, but as with all network connected applications, it’s something to be wary of and build accordingly for. Mine is a ‘fail-close’ system, so in the case of power outage causing restart or no connection at all, the golang program will trigger a close and keep the garage doors closed. There are a few other failsafes in place but nothing 100% concrete when relying on this sort of setup.


Proudly written with VIM, pushed to gitea and processed with golang static site generator