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.





