Wi-Fi Remote Garage Opener
Ever wanted to remotely open your garage door from your phone or computer? Want to interface your garage door controller with a home control solution like Home Assistant/Hass.io?
Status | Operational |
Platform | Arduino ESP8266 |
Budget | $25 |
Date Completed | December 2019 |
Summary of operation
Ever wanted to remotely open your garage door from your phone or computer?
Want to interface your garage door controller with a home control solution like Home Assistant/Hass.io?
Want to know if you've accidentally left the garage open for longer than 15 minutes?
Most garage door controllers have the ability to accept an external hard-wired push button.
The idea here is to use these terminals on the door controller to connect a WiFi-enabled microcontroller via a small relay for electrical isolation.
Parts used
- Garage door operator/controller with terminals for a hard-wired remote push button. In my case, the door is a Marantec Comfort 270.
- 3D printed bracket to fit the roller door channel to mount both the position switch and the controller board.
- I've used the "LC Technology" 1CH ESP-8266 based single-channel relay. Available from eBay, Amazon and similar. Otherwise you can go for a WiFi-enabled microcontroller such as an ESP8266 "Wemos D1 Mini" or similar. Such as https://www.jaycar.com.au/wifi-mini-esp8266-main-board/p/XC3802 with a small relay board to electrically separate the ESP8266's output from the garage door operator's interface terminals. Such as https://www.jaycar.com.au/arduino-compatible-5v-relay-board/p/XC4419 which will work with a 3.3V digital output signal from the ESP.
- Power supply for the microcontroller such as USB iPhone charger or DC-DC to harness power from the garage controller such as https://www.jaycar.com.au/twin-usb-panel-mount-outlet-5v-3-1a/p/MP3618 or a basic eBay-special DC-DC step-down converter.
Some engineering detail
Basically the wiring is as follows:
- 24VDC power supply comes from Pin 1 (GND) and Pin 3 (+24VDC) of the XB03 terminals on the garage door motor unit. This is converted to USB by the adapter above to power the D1 Mini. Lucky it's DC - an older garage door controller I had was AC... that would have required more messing about to rectify etc.
- The D1 output of the D1 Mini is connected to the signal input of the small relay board. The relay board 5V and GND pins are connected to the corresponding 5V and GND pins on the D1 Mini. Note that although the D1 output pin is 3.3V only, this is enough to drive the NPN transistor to switch the relay.
- Be careful not to use an output on the D1 Mini that goes high during boot. You don't want to come home to find your garage door open due to a power outage! See https://randomnerdtutorials.com/esp8266-pinout-reference-gpios/ for more info.
- The relay board "normally open" output is wired to GND and Pin 1 (impulse button input) pins on the XB03 terminals on the garage door motor to remotely trigger it.
My first attempt was setup to work with Tasmota:
- Flashed with Tasmota using Tasmotizer.
- Set Relay 1 to be connected to D1 output in the setup.
- Use command "PulseTime1 10" in the Tasmota console to make relay 1 momentary for 1 second when it's activated.
- The code on the D1 Mini acts as a web server. Once connected to the Wi-Fi, visiting "http://<ip address>/" from an iPhone or similar to use a "toggle" button to open/close the door.
- Use the Tasmota integration for Home Assistant to make it available remotely!
I've later migrated to ESPHome with the "LC Technology" 1CH ESP-8266 based single-channel relay which I just find easier and works better with Home Assistant out of the box. The benefit of ESPHome is I've been able to make it use a "cover" control in Home Assistant based on the work done here but modified for only a single reed switch with the ESPHome yaml code:
# thanks to https://github.com/juaigl/esphome-garage-cover-single-control esphome: name: esp-garage-door on_boot: priority: -200 then: - wait_until: wifi.connected: - delay: 2s - lambda: |- id(new_door_state) = id(garage_closed).state ? 2 : 1; - script.execute: set_door_state - script.execute: publish_door_state esp8266: board: esp01_1m # Enable logging logger: baud_rate: 0 #need this to free up UART pins # Enable Home Assistant API api: ota: password: !secret ota_password wifi: ssid: !secret wifi_ssid password: !secret wifi_password # Enable fallback hotspot (captive portal) in case wifi connection fails ap: ssid: "Esp-Garage-Door Fallback Hotspot" password: !secret fallback_password captive_portal: # Example configuration entry web_server: port: 80 uart: baud_rate: 9600 # speed to STC15L101EW tx_pin: GPIO1 rx_pin: GPIO3 globals: - id: door_state type: int restore_value: no # STOPED = 0, OPENED = 1, CLOSED = 2, OPENING = 3, CLOSING = 4 initial_value: "0" - id: last_door_state type: int restore_value: no # STOPED = 0, OPENED = 1, CLOSED = 2, OPENING = 3, CLOSING = 4 initial_value: "-1" - id: new_door_state type: int restore_value: no # STOPED = 0, OPENED = 1, CLOSED = 2, OPENING = 3, CLOSING = 4 initial_value: "-1" - id: door_duration type: float restore_value: no initial_value: "20000.0" binary_sensor: - platform: gpio pin: number: GPIO2 inverted: False mode: input: True pullup: True id: garage_closed # internal: True name: "Garage Door Closed Switch" # filters: # - delayed_on_off: 20ms on_release: # door externally closed then: - script.stop: garage_door_timer - lambda: !lambda |- id(new_door_state) = 3; // OPENING - script.execute: set_door_state - script.execute: publish_door_state - script.execute: garage_door_timer on_press: # door externally opened then: - script.stop: garage_door_timer - lambda: !lambda |- id(new_door_state) = 2; // CLOSED - script.execute: set_door_state - script.execute: publish_door_state - platform: template name: "Garage Door" device_class: garage_door lambda: !lambda |- return !id(garage_closed).state; switch: - platform: uart #name: "Garage Door Toggle" internal: true id: A1on data: [0xA0, 0x01, 0x01, 0xA2] on_turn_on: - delay: 500ms - switch.turn_on: A1off - platform: uart internal: true #name: "A1off" id: A1off data: [0xA0, 0x01, 0x00, 0xA1] - platform: template icon: "mdi:arrow-up-down-bold-outline" name: "Garage Control" id: garage_control lambda: 'return id(A1on).state;' turn_on_action: - lambda: !lambda |- if (id(door_state) == 0) // when door stopped { if (id(last_door_state) == 3) // when was opening { id(new_door_state) = 4; // CLOSING } else if (id(last_door_state) == 4) // when was closing { id(new_door_state) = 3; // OPENING } } else if (id(door_state) == 1) // when door opened { id(new_door_state) = 4; // CLOSING } else if (id(door_state) == 2) // when door closed { id(new_door_state) = 3; // OPENING } else // when opening or closing { id(new_door_state) = 0; // STOPPED } - switch.turn_on: A1on - script.execute: set_door_state - script.execute: publish_door_state - script.execute: garage_door_timer text_sensor: - platform: template name: "Garage Door State" id: door_state_text cover: - platform: template name: "Garage Door Cover" id: garage_door_cover device_class: garage optimistic: False has_position: True assumed_state: False open_action: - switch.turn_on: garage_control close_action: - switch.turn_on: garage_control stop_action: - switch.turn_on: garage_control lambda: |- static uint32_t last_recompute_time = 0; static uint32_t last_publish_time = 0; static uint8_t calculated_current_operation = -1; // Guard that the door is closed or in idle state then do not calculate position // Tree hazard checks if (id(garage_closed).state == 1 // Door closed || id(garage_door_cover).current_operation == COVER_OPERATION_IDLE || id(garage_door_timer).is_running() == false) { calculated_current_operation = -1; return {}; } // Safety check do distinguish direction change if (calculated_current_operation != id(garage_door_cover).current_operation) { last_recompute_time = millis(); last_publish_time = millis(); calculated_current_operation = id(garage_door_cover).current_operation; } // set dir and duration depending on current movement float dir = (id(garage_door_cover).current_operation == COVER_OPERATION_CLOSING) ? -1.0f : 1.0f; // calculate position float position = id(garage_door_cover).position; position += dir * (millis() - last_recompute_time) / id(door_duration); id(garage_door_cover).position = clamp(position, 0.0f, 1.0f); // publish position every second if (millis() - last_publish_time > 1000) { id(garage_door_cover).publish_state(); last_publish_time = millis(); } last_recompute_time = millis(); return {}; script: - id: set_door_state mode: "single" then: - lambda: !lambda |- if (id(new_door_state) < 0) { return; } id(last_door_state) = id(door_state); id(door_state) = id(new_door_state); id(new_door_state) = -1; - id: garage_door_timer mode: "single" then: - delay: 20s - lambda: !lambda |- // when was opening // simulated opened state based on door duration if (id(door_state) == 3) { id(new_door_state) = 1; // OPENED } if (id(door_state) == 4) { id(new_door_state) = 0; // STOPPED } if (id(garage_closed).state) { // reed switch has the biggest priority // when the door really closed then set the state id(new_door_state) = 2; // CLOSED } - script.execute: set_door_state - script.execute: publish_door_state - id: publish_door_state mode: "single" then: - lambda: !lambda |- switch (id(door_state)) { case 0: // STOPPED id(door_state_text).publish_state("Stopped"); id(garage_door_cover).current_operation = COVER_OPERATION_IDLE; id(garage_door_cover).position = 0.5; id(garage_door_cover).publish_state(); break; case 1: // OPENED id(door_state_text).publish_state("Opened"); id(garage_door_cover).current_operation = COVER_OPERATION_IDLE; id(garage_door_cover).position = COVER_OPEN; id(garage_door_cover).publish_state(); break; case 2: // CLOSED id(door_state_text).publish_state("Closed"); id(garage_door_cover).current_operation = COVER_OPERATION_IDLE; id(garage_door_cover).position = COVER_CLOSED; id(garage_door_cover).publish_state(); break; case 3: // OPENING id(door_state_text).publish_state("Opening"); id(garage_door_cover).current_operation = COVER_OPERATION_OPENING; id(garage_door_cover).position = 0.0; id(garage_door_cover).publish_state(); break; case 4: // CLOSING id(door_state_text).publish_state("Closing"); id(garage_door_cover).current_operation = COVER_OPERATION_CLOSING; id(garage_door_cover).position = 1.0; id(garage_door_cover).publish_state(); break; }
Future improvements
I've used a microswitch to give basic position - i.e. it only knows when the door is properly closed (important!)
Further enhancement could be to use a hall effect sensor or an additional reed switch to count motor/cog revolutions to determine (via dead reckoning) the current door position. That would allow you to send it opening commands with a percentage - e.g. open 10% or open 50%.
The above would allow the position to be returned to your home automation platform e.g. Home Assistant/hass.io.