EP01 – Garage Automation using the Raspberry Pi & Twilio SDK for SMS.

In this episode, I shared the details of the Garage Automation project I posted on Instagram on May 5th, 2018.  You can find the link to that post below.



I created this project because my wife and I kept forgetting to close the garage doors at night. Although we live in a good neighborhood, the garage door opened is like an open invitation for someone driving by to break into the house. That’s when I decided to fix that problem by using technology to hack my garage to make it smarter.

The goal of this project was to add the following functionalities:

  • Automatically close the garage door if it’s past x time, e.g. Midnight
  • Notify me via SMS when the garage door stays open past the set threshold
  • Allow me to communicate with the garage via text messages (SMS)
  • Send me a photo of the garage door to make it easier to confirm that the door is open or closed
  • Allow me to open or close the garage remotely via text message (SMS)

Parts used in this project

Total Cost

The total cost for this project was about $50 USD

The Result

Now the garage communicates with me via text messages (SMS). I can reply back with a text to Open, Close or to check Status. If the garage stays open past 11:00 pm, it will notify me via text message, if I don’t reply back within 5 minutes, it will automatically close the door for me.  Finally, I can request a photo from the inside of the garage to visually confirm that the door is open or closed.

Photos

Source Code

You can find the rest of the source codes on my Github page


"""Python script to monitor and control garage doors via SMS and a Raspberry Pi."""
"""Created by: Eddie Espinal - May 3, 2018 -  http://instagram.com/4hackrr"""

import os
import datetime
import time
import httplib
import email.utils
import RPi.GPIO as GPIO

from time import sleep, strftime
from twilio.rest import Client
from twilio.base.exceptions import TwilioRestException
from dotenv import load_dotenv
from enum import Enum
from dateutil import parser
import picamera
from picamera import Color
from fractions import Fraction
import pyimgur

# Setup Environment Variables
dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(dotenv_path)

# Setup GPIO
IRSENSORPIN = 17
RELAYPIN = 18

GPIO.setmode(GPIO.BCM)    
GPIO.setup(IRSENSORPIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(RELAYPIN, GPIO.OUT)
GPIO.output(RELAYPIN, GPIO.HIGH)

alarmTriggerTime = "11:00 PM"
notificationDelayInSeconds = 300 # five minutes in seconds
GARAGE_OPEN_CLOSE_DELAY = 5 # five seconds

IMG_WIDTH = 640
IMG_HEIGHT = 480
IMAGE_PATH = "/home/pi/GarageAutomation/image.jpg"

# initialize imgur 
# imgur client setup
CLIENT_ID = os.getenv("IMGUR_CLIENT_ID")
imgur = pyimgur.Imgur(CLIENT_ID)

class DoorStatus(Enum):
    OPEN = 0 
    CLOSED = 1 
    UNKNOWN = 2

class GarageAutomation():
    def __init__(self):
        self.client = Client(os.getenv("TWILIO_ACCOUNT_SID"), os.getenv("TWILIO_AUTH_TOKEN"))
        self.toNumber = os.getenv("TO_NUMBER")
        self.fromNumber = os.getenv("TWILIO_NUMBER")
        self.doorStatus = DoorStatus.UNKNOWN
        self.lastSentNoticationTime = -1

        # Send a SMS when the system starts. This will be used when automaticaly restarting the Pi to make sure is working
        self.sendSystemStartedNotification()

    def sendSystemStartedNotification(self):
        self.sendNotificationsMessage("Garage Automation Started")

    def reset(self):
        self.lastSentNoticationTime = -1
        self.doorStatus = DoorStatus.UNKNOWN

    def captureSendImage(self):
        dateString = datetime.datetime.now().strftime("%m-%d-%Y %-I:%M:%S %p")
        with picamera.PiCamera() as camera:
            camera.annotate_text = dateString
            self.configureCamera(camera)
            camera.capture(IMAGE_PATH)
            
        uploaded_image = imgur.upload_image(IMAGE_PATH, title=dateString)

        doorStatusString = "CLOSED"
        if self.doorStatus == DoorStatus.OPEN:
            doorStatusString = "OPEN"

        self.sendNotificationsMessage("The garage door is currently {}".format(doorStatusString), uploaded_image.link)

    def configureCamera(self, camera):
        camera.annotate_foreground = Color('white')
        camera.annotate_background = Color('black')
        camera.resolution = (IMG_WIDTH, IMG_HEIGHT)
        if self.doorStatus == DoorStatus.CLOSED:
            camera.contrast = 100
            camera.brightness = 80
            camera.framerate = Fraction(1, 6)
            camera.iso = 800
            camera.exposure_mode = 'night'
            camera.shutter_speed = 4000000
            time.sleep(5)

    def sendImageViaSMS(self):
        dateString = datetime.datetime.now().strftime("%m-%d-%Y %-I:%M:%S %p")
        with picamera.PiCamera() as camera:
            camera.annotate_text = dateString
            self.configureCamera(camera)
            camera.capture(IMAGE_PATH)
        uploaded_image = imgur.upload_image(IMAGE_PATH, title=dateString)
        self.sendNotificationsMessage(dateString, uploaded_image.link)

    def getDoorStatus(self):
        if (GPIO.input(IRSENSORPIN) == 0):
            self.doorStatus = DoorStatus.OPEN
        if (GPIO.input(IRSENSORPIN) == 1):
            self.doorStatus = DoorStatus.CLOSED
        return self.doorStatus

    def openCloseDoor(self, doorStatus):
        print "Received command to {} the door".format("OPEN" if doorStatus == DoorStatus.OPEN else "CLOSE")
        
        if doorStatus == DoorStatus.OPEN:
            self.logStatus("OPEN")
        else:
            self.logStatus("CLOSE")
        
        GPIO.output(RELAYPIN, GPIO.LOW)
        time.sleep(1)
        GPIO.output(RELAYPIN, GPIO.HIGH)

    def sendNotificationsMessage(self, messageBody, media_url=None):
        try: 
            if media_url is not None:
                self.client.messages.create(from_=self.fromNumber, to=self.toNumber, body=messageBody, media_url=media_url)
            else:
                self.client.messages.create(from_=self.fromNumber, to=self.toNumber, body=messageBody)
        except TwilioRestException as e: 
            print(e)

    def checkIfGarageDoorIsOpenedPastTriggerTime(self):
        now = datetime.datetime.now().strftime("%I:%M %p")
        dt = datetime.datetime.now().timetuple()
        alarmTriggerTimeObject = parser.parse(alarmTriggerTime) # returns 23:00 in 24hr format

        if now == alarmTriggerTime and self.doorStatus == DoorStatus.OPEN:
            if self.lastSentNoticationTime < 0 :
                print("Warning, Garage Doors are Opened Past Trigger Time")
                self.lastSentNoticationTime = time.time()
                self.sendNotificationsMessage("Warning, The garage door is opened, reply `Close` to automatically close it")
                self.logStatus("OPEN")

        # If the notification was already sent and the garage still opened, let's wait 5 minutes before closing the garage automatically.
        if (dt.tm_hour == alarmTriggerTimeObject.hour and dt.tm_min > alarmTriggerTimeObject.minute) and (time.time() - self.lastSentNoticationTime >= notificationDelayInSeconds) and self.doorStatus == DoorStatus.OPEN:
            print("Automatically closing the garage")
            self.logStatus("Automatically closing the garage")
            self.openCloseDoor(DoorStatus.CLOSED)
            self.sendNotificationsMessage("The garage door was automatically closed at {}".format(now))
        elif dt.tm_hour > alarmTriggerTimeObject.hour:
            print("Resetting stored flags")
            self.reset()
       

    def listenForSMSCommand(self):
        try:
            messages = self.client.messages.list(to=self.fromNumber,from_=self.toNumber,date_sent=datetime.datetime.utcnow())

            for message in messages:
                print "Message Body: {} - Date: {}".format(message.body.lower(), message.date_sent)
                if message.status == 'received':
                # select only recently sent messages were time now less time sent is very small
                # (removing an amount from datetime.utcnow() allows the message to be retreived for on a few seconds.
                    date_sent = message.date_sent.strftime('%a, %d %b %Y %H:%M:%S+0000')
                    if (time.mktime(datetime.datetime.utcnow().timetuple())-21602) < email.utils.mktime_tz(email.utils.parsedate_tz(date_sent)):

                        if message.body.lower() == 'close':
                            # call the close garage command here
                            self.openCloseDoor(DoorStatus.CLOSED)
                            self.sendNotificationsMessage("Executed - Garage Close Command")
                            time.sleep(GARAGE_OPEN_CLOSE_DELAY)
                            self.sendImageViaSMS()

                        if message.body.lower() == 'open':
                            # call the open garage command here
                            self.openCloseDoor(DoorStatus.OPEN)
                            self.sendNotificationsMessage("Executed - Garage Open Command")
                            time.sleep(GARAGE_OPEN_CLOSE_DELAY)
                            self.sendImageViaSMS()

                        if message.body.lower() == 'status':
                            # call the garage status command here
                            doorStatusString = "CLOSED"
                            if self.doorStatus == DoorStatus.OPEN:
                                doorStatusString = "OPEN"

                            self.sendNotificationsMessage("The garage door is currently {}".format(doorStatusString))

                        if message.body.lower() == 'photo':
                            self.sendNotificationsMessage("Requesting photo, please wait...")
                            # take a photo and send it via SMS
                            self.captureSendImage()
                        
                        if message.body.lower() == 'reboot':
                            self.sendNotificationsMessage("Executed - Reboot Command")
                            os.system('sudo shutdown -r now')

                        if message.body.lower() == 'shutdown':
                            self.sendNotificationsMessage("Executed - Shutdown Command")
                            os.system('sudo shutdown -h now')

                        time.sleep(5)

        except TwilioRestException as e:
            print(e)
            pass
        except:
            print("Unexpected error")
            pass

    def logStatus(self, garageStatus):
        with open("garage_status_log.csv", "a") as log:
            log.write("{0},{1}\n".format(datetime.datetime.now().strftime("%m-%d-%Y %-I:%M:%S %p"),str(garageStatus)))
            sleep(1)

    def run(self):
        try:
            while (True):
                self.getDoorStatus()
                self.checkIfGarageDoorIsOpenedPastTriggerTime()
                self.listenForSMSCommand()
        finally:
            GPIO.cleanup() # ensures a clean exit



if __name__ == "__main__":
    try:
        print("Starting Garage Automation System")
        garageAutomation = GarageAutomation()
        garageAutomation.run()
    # End program cleanly with keyboard  
    except KeyboardInterrupt:  
        print "Quit"