Skip to content

Instantly share code, notes, and snippets.

@JeremiahChurch
Last active April 4, 2026 22:04
Show Gist options
  • Select an option

  • Save JeremiahChurch/d1e0bcd07dd2c637f438fe8cbe76c0a2 to your computer and use it in GitHub Desktop.

Select an option

Save JeremiahChurch/d1e0bcd07dd2c637f438fe8cbe76c0a2 to your computer and use it in GitHub Desktop.
Maxum JDHC Garage Door — ESPHome control with D1 Mini + relay board (replaces ratgdo)

Maxum JDHC Garage Door - ESPHome Control (no ratgdo needed)

A standalone ESPHome solution for Maxum JDHC commercial garage door openers using a D1 Mini (ESP8266) and a cheap 3- or 4-channel relay board. This replaces ratgdo, which has known issues with Maxum openers (door opening on power cycle, unreliable close commands).

Tested and running on two Maxum doors for several months.

What You Get

  • Full open / close / stop control from Home Assistant
  • Real-time door state via physical limit switches (AUXREL boards)
  • Door Moving, Door Opening, Door Closing binary sensors for automations
  • External operation detection - wall panel / remote operations update HA immediately (shows partial open with both up/down controls)
  • Position tracking (time-interpolated with endstop correction)
  • WiFi fallback AP + captive portal for recovery if WiFi credentials change
  • Correct mid-travel state on boot (shows partial open, not stale closed/open)

Hardware Required

Component Notes
D1 Mini (ESP8266) Any ESP8266 dev board works. D1 Mini is compact and cheap.
3- or 4-channel relay board 5V coil, active-low input. The common cheapo boards on Amazon/AliExpress work fine. Only 3 relays are used (open, close, stop).
Maxum AUXREL boards (x2) These are the auxiliary relay option boards that plug into the Maxum's control board. They provide dry-contact limit switch signals. Part numbers vary by Maxum model - check your manual.
24V-to-5V buck converter To power the D1 Mini and relay board from the Maxum's 24V supply (or use USB power).
Wire 18-22 AWG for relay/limit connections.

Wiring Overview

                    +----------------------------------+
                    |         MAXUM CONTROL BOARD      |
                    |                                  |
                    |  OPEN -------+                   |
                    |  CLOSE ------+  Low-voltage      |
                    |  STOP -------+  command inputs   |
                    |  COM --------+  (dry contact)    |
                    |                                  |
                    |  AUXREL 1 slot  <- plug in board |
                    |  AUXREL 2 slot  <- plug in board |
                    +----------------------------------+
                              |           |
            +-----------------+           +------------------+
            |                                                |
   +--------+--------+                            +---------+--------+
   |    AUXREL 1     |                            |    AUXREL 2      |
   |  (Open Limit)   |                            |  (Closed Limit)  |
   |                 |                            |                  |
   |  COM -+  NC -+  |                            |  COM -+  NO -+   |
   +-------+------+--+                            +-------+------+---+
           |      |                                       |      |
           |      +-->  D7 (open limit input)             |      +--> D6 (closed limit input)
           +--------->  GND on D1 Mini                    +--------> GND on D1 Mini


   +----------------------------------------------+
   |          D1 MINI (ESP8266)                   |
   |                                              |
   |  D1 --> Relay 1 IN (OPEN)                    |
   |  D2 --> Relay 2 IN (CLOSE)                   |
   |  D5 --> Relay 3 IN (STOP)                    |
   |  D6 <-- AUXREL 2 (closed limit)              |
   |  D7 <-- AUXREL 1 (open limit)                |
   |  GND -- AUXREL COMs + Relay GND              |
   |  5V --- Relay VCC + Buck converter           |
   +----------------------------------------------+

   Relay outputs (NO contacts) -> Maxum command terminals:
     Relay 1 NO --> OPEN on Maxum
     Relay 2 NO --> CLOSE on Maxum
     Relay 3 NO --> STOP on Maxum
     Relay COMs --> COM on Maxum

AUXREL Configuration

Refer to your Maxum manual (page 57+ in newer manuals) for the AUXREL DIP switch settings:

Board DIP Switches Behavior Wiring
AUXREL 1 OFF ON OFF Energizes at open limit Use NC (normally closed) contact - ON = door fully open
AUXREL 2 OFF OFF ON Energizes when not at close limit Use NO (normally open) contact - ON = door fully closed

The key insight: AUXREL 2's "energizes when not at close limit" with a NO contact and INPUT_PULLUP means: when the door IS at the close limit, the relay is de-energized, the NO contact is open, and the pullup holds the pin HIGH (sensor ON). When the door is NOT at the close limit, the relay energizes, the NO contact closes to GND, and the pin reads LOW (sensor OFF).

Pin inversion note: Whether you need inverted: true on the limit switch GPIO pins depends on your AUXREL wiring (NO vs NC contacts) and DIP switch config. With the wiring described above, no inversion is needed. If your limit reads backwards (shows closed when open), add inverted: true under the pin config.

GPIO Pin Summary

Pin Direction Function
D1 Output OPEN relay (active low)
D2 Output CLOSE relay (active low)
D5 Output STOP relay (active low)
D6 Input (pullup) Closed limit switch (from AUXREL 2)
D7 Input (pullup) Open limit switch (from AUXREL 1)

How It Works

HA-initiated operations

  1. HA sends open/close/stop command
  2. ESPHome pulses the corresponding relay for 200ms
  3. The cover: endstop platform starts time-based position tracking
  4. When the physical limit switch triggers, position snaps to 0% or 100%

Wall panel / remote operations (external)

  1. Door starts moving from wall panel or remote
  2. The limit switch releases (e.g., closed limit goes OFF)
  3. ESPHome detects the cover was idle when the limit released - this means the movement was external
  4. Cover position is set to 50% (partial open) and stays idle - HA shows the door as "open" with both up/down controls available
  5. If the door reaches the opposite limit, position snaps to 0% or 100% as normal
  6. If the door stops mid-travel (wall panel stop), it stays at 50% showing "Stopped Mid-Travel"

Why idle instead of opening/closing? The previous approach set the cover to OPENING/CLOSING on external limit release, which started the endstop platform's timing engine. This caused incorrect position interpolation since we don't know when external movement will stop. Keeping IDLE with a 50% position gives HA the correct controls without the timing engine drifting to a wrong position.

Boot behavior

On boot, ESPHome reads the physical limit switches and snaps the cover to the correct state:

  • Closed limit active - position 0% (closed)
  • Open limit active - position 100% (open)
  • Neither limit active - position 50% (partial open, "Stopped Mid-Travel")

This ensures the door always shows the correct state after a reboot or power cycle, even if the door was manually moved while the ESP was offline. No errant door movement - the relay outputs use restore_mode: ALWAYS_OFF and the GPIO pins are held via inverted: true (active-low relays stay de-energized on boot).

Known Limitations

  • Position accuracy during external operations: For wall panel/remote operations, the position shows as 50% (partial) until a limit switch is reached, rather than smoothly interpolating. The final position (0% or 100%) is always accurate once a limit is hit.
  • Mid-travel stop for external ops: If someone opens via wall panel then stops mid-travel, HA correctly shows "Stopped Mid-Travel" at 50%. The exact position is unknown but both open/close controls are available.
  • No obstruction sensor: The Maxum's obstruction sensor circuit isn't tapped. It could potentially be wired through the ESP (like ratgdo does) but I haven't tested this to avoid introducing a failure point in a safety circuit.

Installation

  1. Copy maxum-esphome-garage-door.yaml to your ESPHome config directory
  2. Create or update your secrets.yaml with api_key, ota_password, wifi_ssid, wifi_password, fallback_ap_password
  3. Update the substitutions block with your door's name and measured travel times
  4. Flash via USB the first time, then OTA for updates
  5. The device will appear in Home Assistant's ESPHome integration automatically

Multiple Doors

For multiple Maxum doors, copy the YAML and change only the substitutions block (name, friendly_name, travel times). Each door gets its own D1 Mini + relay board + AUXREL pair. The GPIO pin assignments stay the same.

# ESPHome config for Maxum JDHC garage door opener
# Replaces ratgdo with a D1 Mini (ESP8266) + 3-relay board + Maxum AUXREL limit switches
#
# See README.md in this gist for full wiring details.
#
# Adjust substitutions for your door's actual travel times.
# The endstop cover uses these for position interpolation, but the
# physical limit switches are the real source of truth.
substitutions:
name: shop-maxum-door # change per door
friendly_name: Shop Maxum Door # change per door
open_time: "30s" # measured open travel time
close_time: "24s" # measured close travel time
pulse_ms: "200ms" # relay pulse duration
esphome:
name: ${name}
friendly_name: ${friendly_name}
on_boot:
priority: 600 # run after sensors initialize
then:
- delay: 500ms
- lambda: |-
// Snap cover state to the physical endstops at boot
if (id(bs_closed_limit).state) {
ESP_LOGI("boot", "Door is at CLOSED limit on boot");
id(garage_door).position = 0.0f;
id(garage_door).current_operation = esphome::cover::COVER_OPERATION_IDLE;
id(garage_door).publish_state();
} else if (id(bs_open_limit).state) {
ESP_LOGI("boot", "Door is at OPEN limit on boot");
id(garage_door).position = 1.0f;
id(garage_door).current_operation = esphome::cover::COVER_OPERATION_IDLE;
id(garage_door).publish_state();
} else {
// Neither limit active - door is mid-travel or position unknown.
// Set to 50% so HA shows "open" with both up/down controls available.
ESP_LOGI("boot", "Door is mid-travel on boot - setting partial position");
id(garage_door).position = 0.5f;
id(garage_door).current_operation = esphome::cover::COVER_OPERATION_IDLE;
id(garage_door).publish_state();
}
esp8266:
board: d1_mini
logger:
level: INFO
logs:
cover: INFO
binary_sensor: INFO
api:
encryption:
key: !secret api_key # generate with: esphome wizard
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "${name}-fallback"
password: !secret fallback_ap_password
captive_portal:
web_server:
port: 80
# -----------------------
# Relay outputs
# -----------------------
switch:
- platform: gpio
id: sw_open
pin:
number: D1
inverted: true
restore_mode: ALWAYS_OFF
internal: true
- platform: gpio
id: sw_close
pin:
number: D2
inverted: true
restore_mode: ALWAYS_OFF
internal: true
- platform: gpio
id: sw_stop
pin:
number: D5
inverted: true
restore_mode: ALWAYS_OFF
internal: true
script:
- id: pulse_open
mode: restart
then:
- logger.log: "Pulsing OPEN relay"
- switch.turn_on: sw_open
- delay: ${pulse_ms}
- switch.turn_off: sw_open
- id: pulse_close
mode: restart
then:
- logger.log: "Pulsing CLOSE relay"
- switch.turn_on: sw_close
- delay: ${pulse_ms}
- switch.turn_off: sw_close
- id: pulse_stop
mode: restart
then:
- logger.log: "Pulsing STOP relay"
- switch.turn_on: sw_stop
- delay: ${pulse_ms}
- switch.turn_off: sw_stop
# -----------------------
# Limit switch inputs (from Maxum AUXREL boards)
#
# NOTE on pin inversion: Whether you need `inverted: true` depends on
# your AUXREL wiring (NO vs NC contacts) and DIP switch config. With
# the wiring described in the README (AUXREL 2 = NO contact + pullup),
# no inversion is needed. If your limit reads backwards, add
# `inverted: true` under the pin config.
# -----------------------
binary_sensor:
- platform: gpio
id: bs_closed_limit
name: "Door Closed Limit"
pin:
number: D6
mode: INPUT_PULLUP
filters:
- delayed_on_off: 150ms
on_press:
then:
- logger.log: "CLOSED limit reached"
- lambda: |-
id(garage_door).position = 0.0f;
id(garage_door).current_operation = esphome::cover::COVER_OPERATION_IDLE;
id(garage_door).publish_state();
on_release:
then:
- logger.log: "CLOSED limit released (door leaving closed position)"
- lambda: |-
// If ESPHome didn't command this movement (cover is idle),
// the door was moved externally (wall panel / remote).
// Set partial position so HA shows open with both controls.
if (id(garage_door).current_operation == esphome::cover::COVER_OPERATION_IDLE) {
ESP_LOGI("limit", "External open detected - setting partial position");
id(garage_door).position = 0.5f;
id(garage_door).publish_state();
}
- platform: gpio
id: bs_open_limit
name: "Door Open Limit"
pin:
number: D7
mode: INPUT_PULLUP
filters:
- delayed_on_off: 150ms
on_press:
then:
- logger.log: "OPEN limit reached"
- lambda: |-
id(garage_door).position = 1.0f;
id(garage_door).current_operation = esphome::cover::COVER_OPERATION_IDLE;
id(garage_door).publish_state();
on_release:
then:
- logger.log: "OPEN limit released (door leaving open position)"
- lambda: |-
if (id(garage_door).current_operation == esphome::cover::COVER_OPERATION_IDLE) {
ESP_LOGI("limit", "External close detected - setting partial position");
id(garage_door).position = 0.5f;
id(garage_door).publish_state();
}
- platform: template
name: "Door Moving"
id: door_moving
device_class: moving
lambda: |-
return id(garage_door).current_operation != COVER_OPERATION_IDLE;
on_press:
- logger.log: "Door started moving"
on_release:
- logger.log: "Door stopped moving"
- platform: template
name: "Door Opening"
id: door_opening
lambda: |-
return id(garage_door).current_operation == COVER_OPERATION_OPENING;
on_press:
- logger.log: "Door is OPENING"
on_release:
- logger.log: "Door is no longer OPENING"
- platform: template
name: "Door Closing"
id: door_closing
lambda: |-
return id(garage_door).current_operation == COVER_OPERATION_CLOSING;
on_press:
- logger.log: "Door is CLOSING"
on_release:
- logger.log: "Door is no longer CLOSING"
# -----------------------
# Cover entity
# -----------------------
cover:
- platform: endstop
id: garage_door
name: "Garage Door"
device_class: garage
open_action:
- logger.log: "Cover OPEN action triggered"
- script.execute: pulse_open
- delay: 100ms
- lambda: |-
id(garage_door).publish_state();
open_duration: ${open_time}
open_endstop: bs_open_limit
max_duration: 60s
close_action:
- logger.log: "Cover CLOSE action triggered"
- script.execute: pulse_close
- delay: 100ms
- lambda: |-
id(garage_door).publish_state();
close_duration: ${close_time}
close_endstop: bs_closed_limit
stop_action:
- logger.log: "Cover STOP action triggered"
- script.execute: pulse_stop
text_sensor:
- platform: template
name: "Door State Debug"
lambda: |-
auto op = id(garage_door).current_operation;
if (op == COVER_OPERATION_IDLE) {
if (id(garage_door).position == 0.0f) return {"Closed (Idle)"};
if (id(garage_door).position == 1.0f) return {"Open (Idle)"};
return {"Stopped Mid-Travel"};
} else if (op == COVER_OPERATION_OPENING) {
return {"Opening"};
} else if (op == COVER_OPERATION_CLOSING) {
return {"Closing"};
}
return {"Unknown"};
update_interval: 1s
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment