├── .gitignore ├── LICENSE ├── README.md ├── WiringDiagram.png ├── autostart_systemd.sh ├── config.yaml ├── garageqtpi@pi.service ├── lib ├── __init__.py ├── eventhook.py └── garage.py ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | RPi/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jerrod Lankford 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is GarageQTPi 2 | 3 | GarageQTPi is an implementation that provides methods to communicate with a Raspberry Pi garage door opener via the MQTT protocol. 4 | Although it is designed to work out of the box with a Home Assistant cover component it can also be used as the basis for any Raspberry Pi garage project. 5 | 6 | ## Motivation 7 | 8 | Home Assistant has integration for raspberry pi garage door openers but only if the instance of Home Assistant is running on the raspberry pi. If your raspberry pi is soley a garage door opener like mine 9 | then you need to use an MQTT cover component to interface with the pi. 10 | 11 | ## Hardware 12 | 13 | 1. Raspberry pi 3 14 | * [Canakit with everything ~$75](https://www.amazon.com/CanaKit-Raspberry-Complete-Starter-Kit/dp/B01C6Q2GSY) 15 | * [Canakit with PS/case ~$50](https://www.amazon.com/CanaKit-Raspberry-Clear-Power-Supply/dp/B01C6EQNNK) 16 | 2. Relay 17 | * [Sainsmart 2-channel](https://www.amazon.com/gp/product/B0057OC6D8) 18 | 3. Magnetic switches 19 | * [Magnetic Switches](https://www.amazon.com/gp/product/B0009SUF08) 20 | 4. Additional wires/wire nuts. 21 | * 14 gauge solid copper wire for garage motor wiring 22 | * 20-22 gauge copper wire for magnetic switch wiring 23 | * jumper wiries for GPIO pins 24 | 5. Mounting Hardware. 25 | * See installation section for mounting ideas. 26 | 27 | Total cost: ~75-$100. Cheaper if you already have some raspberry pi parts 28 | 29 | ## Wiring/Installation 30 | 31 | ![alt text](WiringDiagram.png) 32 | 33 | Copyright (c) 2013 andrewshilliday 34 | 35 | Note: The switches I linked have 3 terminals (COM, NO, NC). You should wire up COM to GND and NO to the GPIO. 36 | 37 | **Important: The above diagram is outdated, pin 21 may actually be pin 27. Consult your raspberry pi's pin diagram** 38 | 39 | ### Relay wiring 40 | **IMPORTANT: You shoud always consult with a manual before wiring** 41 | 42 | It's impossible to write a generic guide as all garage door motors are not equal. I will instead explain what I did as a reference that you can use. 43 | 44 | The basic idea is to wire it in parallel with the button on the wall. 45 | The code is essentially mimicking a button press by switching the relay on and off quickly. In my case the two leftmost wires (red/white) are connected to the button on the wall. 46 | The two rightmost white wires are for the collision detection sensors. So I removed the two leftmost wires, wirenutted 3 solid 14 gauge wires together (the button wire, my relay wire, and then one wire to go to the garage door opener) two times for each of the two wires. 47 | 48 | 49 | 50 | ### Magnetic switch wiring 51 | I ran the magnetic switch wires along the same path as the sensor wires, stapled them to the wall, and stuck the magnetic switches to the door and wall as close as I could get them. As noted above wire up the COM (common) to the GND pin and the NO (normally open) to the GPIO pin. 52 | 53 | 54 | 55 | Notice mine aren't exactly on the same plane but I was monitoring the gpio pins in the code to make sure they were close enough to complete the circuit before I attached them. So far the included 3M sticky tape is holding up but time will tell. 56 | ### Mounting 57 | I've seen a lot of people mounting the pi/relay onto plywood and mounting that to the ceiling. I wasn't really keen on that so what I did was drill four small holes into the top of my pi and found screws and nylon spacers at lowes. I attached the 58 | relay to the top of the pi case. 59 | 60 | 61 | 62 | 63 | The pi case included with the Canakit has mounting holes on the back, so I used small bolts that sit flush into the mounting holes, and then large washers and attached the case to the garage door mount. 64 | The lid to the case comes off easily so once it was mounted I ran zip ties around the lid and secured it. I also squirted locktite around all the screw threads to keep the vibration of the garage door from shaking any screws loose. 65 | So far this has proved to be relatively stable. 66 | 67 | 68 | 69 | 70 | ## Software 71 | 72 | ### Prereqs 73 | * Raspberry pi 3 running rasbian jessie 74 | * Python 2.7.x 75 | * pip (python 2 pip) 76 | 77 | ### Installation 78 | 1. `git clone https://github.com/Jerrkawz/GarageQTPi.git` 79 | 2. `pip install -r requirements.txt` 80 | 3. edit the configuration.yaml to set up mqtt (See below) 81 | 4. `python main.py` 82 | 5. To start the server on boot run `sudo bash autostart_systemd.sh` 83 | 84 | ## MQTT setup 85 | I won't try to butcher an mqtt setup guide but will instead link you to some other resources: 86 | 87 | HomeAssistant MQTT Setup: https://home-assistant.io/components/mqtt/ 88 | 89 | Bruh Automation: https://www.youtube.com/watch?v=AsDHEDbyLfg 90 | 91 | ## Home Assistant component setup 92 | Either follow the cover setup or enable mqtt discovery 93 | HomeAssistant MQTT Cover: https://home-assistant.io/components/cover.mqtt/ 94 | HomeAssistant MQTT Discovery: https://home-assistant.io/docs/mqtt/discovery/ 95 | 96 | Screenshot: 97 | 98 | ![Home assistant ui][1] 99 | 100 | ## API Reference 101 | 102 | The server works with the Home Assisant MQTT Cover component out of the box but if you want to write your own MQTT client you need to adhere to the following API: 103 | 104 | Publish one of the following UPPER CASE strings to the command_topic in your config: 105 | 106 | `OPEN | CLOSE | STOP` 107 | 108 | Subscribe to the state_topic in your config and you will recieve one of these lower case strings when the state pin changes: 109 | 110 | `open | closed` 111 | 112 | Thats it! 113 | 114 | ## Sample Configuration 115 | 116 | config.yaml: 117 | ``` 118 | mqtt: 119 | host: m10.cloudmqtt.com 120 | port: * 121 | user: * 122 | password: * 123 | doors: 124 | - 125 | id: 'left' 126 | relay: 23 127 | state: 17 128 | state_topic: "home-assistant/cover/left" 129 | command_topic: "home-assistant/cover/left/set" 130 | - 131 | id: 'right' 132 | relay: 24 133 | state: 27 134 | state_topic: "home-assistant/cover/right" 135 | command_topic: "home-assistant/cover/right/set" 136 | ``` 137 | 138 | ### Optional configuration 139 | There are five optional configuration parameters. 140 | Two of the option parameters are for mqtt. One is to enable discovery by HomeAssistant. The second one changes the discovery prefix for HomeAssitant. 141 | ``` 142 | mqtt: 143 | host: m10.cloudmqtt.com 144 | port: * 145 | user: * 146 | password: * 147 | discovery: true 148 | discovery_prefix: 'homeassistant' 149 | ``` 150 | 151 | The discovery parameter defaults to false and should be set to true to enable discovery by HomeAssistant. If set to true, the door state_topic and command_topic parameters are not necessary and are ignored. 152 | The discovery_prefix parameter defaults to 'homeassistant' and shouldn't be changed unless changed in HomeAssistant 153 | 154 | The other three of the option parameters are for the doors. One to give the door a name for discovery. The second one to flip the state pin of the magnetic switch in the invent of a different wiring schema. The third one to filp the relay logic. This is a per door configuration option like: 155 | ``` 156 | doors: 157 | - 158 | id: 'left' 159 | name: 'Left Garage Door' 160 | relay: 23 161 | state: 17 162 | state_mode: normally_closed 163 | invert_relay: true 164 | state_topic: "home-assistant/cover/left" 165 | command_topic: "home-assistant/cover/left/set" 166 | ``` 167 | 168 | The name parameter defaults to the unsanitized id parameter 169 | The state_mode parameter defaults to 'normally_open' and isn't necessary unless you want to change it to 'normally_closed' 170 | The invert_relay parameter defaults to false and isn't necessary unless you want to set the relay pin to be powered by default 171 | 172 | ## Contributors 173 | 174 | I wrote the code myself but as far as hardware/wiring and motivation goes I was heavily insipired by Andrew Shilliday. 175 | As you can tell I borrowed some images from him. If you find my guide hard to read, need a web gui, or just want a second reference definitely check out his repo: https://github.com/andrewshilliday/garage-door-controller 176 | 177 | [1]: http://imgur.com/obgvgKJ.png 178 | -------------------------------------------------------------------------------- /WiringDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerrod-lankford/GarageQTPi/644ab878956c3cbb4f6a5b98569f78a5a4b44ff2/WiringDiagram.png -------------------------------------------------------------------------------- /autostart_systemd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cp garageqtpi@pi.service /etc/systemd/system/garageqtpi@${SUDO_USER:-${USER}}.service 3 | sed -i "s?/home/pi/GarageQTPi?`pwd`?" /etc/systemd/system/garageqtpi@${SUDO_USER:-${USER}}.service 4 | systemctl --system daemon-reload 5 | systemctl enable garageqtpi@${SUDO_USER:-${USER}}.service 6 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | mqtt: 2 | host: 3 | port: 4 | user: 5 | password: 6 | # discovery: true #defaults to false, uncomment to enable home-assistant discovery 7 | # discovery_prefix: homeassistant #change to match with setting of home-assistant 8 | doors: 9 | - 10 | id: 11 | # name: #defaults to an unsanitized version of the id paramater 12 | relay: 13 | state: 14 | # state_mode: normally_closed #defaults to normally open, uncomment to switch 15 | # invert_relay: true #defaults to false, uncomment to turn relay pin on by default 16 | state_topic: "home-assistant/cover" 17 | command_topic: "home-assistant/cover/set" 18 | 19 | -------------------------------------------------------------------------------- /garageqtpi@pi.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=GarageQTPi 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | Restart=always 8 | RestartSec=3 9 | User=%i 10 | ExecStart=/usr/bin/python -u /home/pi/GarageQTPi/main.py 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerrod-lankford/GarageQTPi/644ab878956c3cbb4f6a5b98569f78a5a4b44ff2/lib/__init__.py -------------------------------------------------------------------------------- /lib/eventhook.py: -------------------------------------------------------------------------------- 1 | # Credit to https://gist.github.com/smoak/1045874 2 | class EventHook(object): 3 | def __init__(self): 4 | self.__handlers = [] 5 | 6 | def addHandler(self, handler): 7 | self.__handlers.append(handler) 8 | 9 | def removeHandler(self, handler): 10 | self.__handlers.remove(handler) 11 | 12 | def fire(self, *args, **kwargs): 13 | for handler in self.__handlers: 14 | handler(*args, **kwargs) 15 | 16 | def clearObjectHandlers(self, inObject): 17 | for theHandler in self.__handlers: 18 | if theHandler.im_self == inObject: 19 | self.removeHandler(theHandler) 20 | -------------------------------------------------------------------------------- /lib/garage.py: -------------------------------------------------------------------------------- 1 | import time 2 | import RPi.GPIO as GPIO 3 | from eventhook import EventHook 4 | 5 | 6 | SHORT_WAIT = .2 #S (200ms) 7 | """ 8 | The purpose of this class is to map the idea of a garage door to the pinouts on 9 | the raspberrypi. It provides methods to control the garage door and also provides 10 | and event hook to notify you of the state change. It also doesn't maintain any 11 | state internally but rather relies directly on reading the pin. 12 | """ 13 | class GarageDoor(object): 14 | 15 | def __init__(self, config): 16 | 17 | # Config 18 | self.relay_pin = config['relay'] 19 | self.state_pin = config['state'] 20 | self.id = config['id'] 21 | self.mode = int(config.get('state_mode') == 'normally_closed') 22 | self.invert_relay = bool(config.get('invert_relay')) 23 | 24 | # Setup 25 | self._state = None 26 | self.onStateChange = EventHook() 27 | 28 | # Set relay pin to output, state pin to input, and add a change listener to the state pin 29 | GPIO.setwarnings(False) 30 | GPIO.setmode(GPIO.BCM) 31 | GPIO.setup(self.relay_pin, GPIO.OUT) 32 | GPIO.setup(self.state_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) 33 | GPIO.add_event_detect(self.state_pin, GPIO.BOTH, callback=self.__stateChanged, bouncetime=300) 34 | 35 | 36 | # Set default relay state to false (off) 37 | GPIO.output(self.relay_pin, self.invert_relay) 38 | 39 | # Release rpi resources 40 | def __del__(self): 41 | GPIO.cleanup() 42 | 43 | # These methods all just mimick the button press, they dont differ other than that 44 | # but for api sake I'll create three methods. Also later we may want to react to state 45 | # changes or do things differently depending on the intended action 46 | 47 | def open(self): 48 | if self.state == 'closed': 49 | self.__press() 50 | 51 | def close(self): 52 | if self.state == 'open': 53 | self.__press() 54 | 55 | def stop(self): 56 | self.__press() 57 | 58 | # State is a read only property that actually gets its value from the pin 59 | @property 60 | def state(self): 61 | # Read the mode from the config. Then compare the mode to the current state. IE. If the circuit is normally closed and the state is 1 then the circuit is closed. 62 | # and vice versa for normally open 63 | state = GPIO.input(self.state_pin) 64 | if state == self.mode: 65 | return 'closed' 66 | else: 67 | return 'open' 68 | 69 | # Mimick a button press by switching the GPIO pin on and off quickly 70 | def __press(self): 71 | GPIO.output(self.relay_pin, not self.invert_relay) 72 | time.sleep(SHORT_WAIT) 73 | GPIO.output(self.relay_pin, self.invert_relay) 74 | 75 | 76 | # Provide an event for when the state pin changes 77 | def __stateChanged(self, channel): 78 | if channel == self.state_pin: 79 | # Had some issues getting an accurate value so we are going to wait for a short timeout 80 | # after a statechange and then grab the state 81 | time.sleep(SHORT_WAIT) 82 | self.onStateChange.fire(self.state) 83 | 84 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import binascii 3 | import yaml 4 | import paho.mqtt.client as mqtt 5 | import re 6 | 7 | from lib.garage import GarageDoor 8 | 9 | print "Welcome to GarageBerryPi!" 10 | 11 | # Update the mqtt state topic 12 | def update_state(value, topic): 13 | print "State change triggered: %s -> %s" % (topic, value) 14 | 15 | client.publish(topic, value, retain=True) 16 | 17 | # The callback for when the client receives a CONNACK response from the server. 18 | def on_connect(client, userdata, rc): 19 | print "Connected with result code: %s" % mqtt.connack_string(rc) 20 | for config in CONFIG['doors']: 21 | command_topic = config['command_topic'] 22 | print "Listening for commands on %s" % command_topic 23 | client.subscribe(command_topic) 24 | 25 | # Execute the specified command for a door 26 | def execute_command(door, command): 27 | try: 28 | doorName = door.name 29 | except: 30 | doorName = door.id 31 | print "Executing command %s for door %s" % (command, doorName) 32 | if command == "OPEN" and door.state == 'closed': 33 | door.open() 34 | elif command == "CLOSE" and door.state == 'open': 35 | door.close() 36 | elif command == "STOP": 37 | door.stop() 38 | else: 39 | print "Invalid command: %s" % command 40 | 41 | with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'config.yaml'), 'r') as ymlfile: 42 | CONFIG = yaml.load(ymlfile) 43 | 44 | ### SETUP MQTT ### 45 | user = CONFIG['mqtt']['user'] 46 | password = CONFIG['mqtt']['password'] 47 | host = CONFIG['mqtt']['host'] 48 | port = int(CONFIG['mqtt']['port']) 49 | discovery = bool(CONFIG['mqtt'].get('discovery')) 50 | if 'discovery_prefix' not in CONFIG['mqtt']: 51 | discovery_prefix = 'homeassistant' 52 | else: 53 | discovery_prefix = CONFIG['mqtt']['discovery_prefix'] 54 | 55 | client = mqtt.Client(client_id="MQTTGarageDoor_" + binascii.b2a_hex(os.urandom(6)), clean_session=True, userdata=None, protocol=4) 56 | 57 | client.on_connect = on_connect 58 | 59 | client.username_pw_set(user, password=password) 60 | client.connect(host, port, 60) 61 | ### SETUP END ### 62 | 63 | ### MAIN LOOP ### 64 | if __name__ == "__main__": 65 | # Create door objects and create callback functions 66 | for doorCfg in CONFIG['doors']: 67 | 68 | # If no name it set, then set to id 69 | if not doorCfg['name']: 70 | doorCfg['name'] = doorCfg['id'] 71 | 72 | # Sanitize id value for mqtt 73 | doorCfg['id'] = re.sub('\W+', '', re.sub('\s', ' ', doorCfg['id'])) 74 | 75 | if discovery is True: 76 | base_topic = discovery_prefix + "/cover/" + doorCfg['id'] 77 | config_topic = base_topic + "/config" 78 | doorCfg['command_topic'] = base_topic + "/set" 79 | doorCfg['state_topic'] = base_topic + "/state" 80 | 81 | command_topic = doorCfg['command_topic'] 82 | state_topic = doorCfg['state_topic'] 83 | 84 | 85 | door = GarageDoor(doorCfg) 86 | 87 | # Callback per door that passes a reference to the door 88 | def on_message(client, userdata, msg, door=door): 89 | execute_command(door, str(msg.payload)) 90 | 91 | # Callback per door that passes the doors state topic 92 | def on_state_change(value, topic=state_topic): 93 | update_state(value, topic) 94 | 95 | client.message_callback_add(command_topic, on_message) 96 | 97 | # You can add additional listeners here and they will all be executed when the door state changes 98 | door.onStateChange.addHandler(on_state_change) 99 | 100 | # Publish initial door state 101 | client.publish(state_topic, door.state, retain=True) 102 | 103 | # If discovery is enabled publish configuration 104 | if discovery is True: 105 | client.publish(config_topic,'{"name": "' + doorCfg['name'] + '", "command_topic": "' + command_topic + '", "state_topic": "' + state_topic + '"}', retain=True) 106 | 107 | # Main loop 108 | client.loop_forever() 109 | 110 | 111 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | paho_mqtt==1.2 2 | PyYAML==3.12 3 | --------------------------------------------------------------------------------