Skip to content

Instantly share code, notes, and snippets.

@JeremiahChurch
Last active March 11, 2026 14:09
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 are properly reflected in HA
  • Position tracking (time-interpolated with endstop correction)

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 (×2) 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→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). No GPIO inversion needed — the combination of relay logic and pullup naturally gives us the correct polarity.

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 at that endpoint → marks it as externally opening/closing
  4. Door Moving and Door Opening/Door Closing sensors fire normally
  5. When the opposite limit switch triggers, position snaps correctly

This means your HA automations (lights on when door opens, notifications, etc.) work regardless of whether the door was triggered from HA or the wall panel.

Boot behavior

On boot, ESPHome reads the physical limit switches and snaps the cover to the correct state. No errant door movement — unlike ratgdo, 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: The endstop cover's time-based interpolation only tracks accurately for HA-initiated operations. For wall panel operations, the position jumps from 50% to the endpoint rather than smoothly interpolating. The final position (0% or 100%) is always accurate once a limit is reached.
  • Mid-travel stop detection for external ops: If someone presses the wall panel to open, then presses it again to stop mid-travel, the cover will keep showing "Opening" until the next limit switch event or HA operation. The physical limit switches are the only inputs — there's no current sensor or encoder.
  • 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
  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 {
ESP_LOGI("boot", "Door is mid-travel on boot - keeping last position");
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
web_server:
port: 80
# -----------------------
# Relay outputs
# -----------------------
# Three relays control the Maxum's low-voltage command inputs.
# The Maxum has separate OPEN, CLOSE, and STOP terminals on its
# control board. Each relay momentarily shorts the command terminal
# to COM (common) to simulate a button press.
#
# Relays are active-low (inverted: true) — the D1 Mini's GPIO
# goes LOW to energize the relay coil.
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
# Pulse scripts — momentarily close each relay contact
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)
# -----------------------
# The Maxum's AUXREL relay boards provide dry-contact limit signals:
# AUXREL 1 — energizes at OPEN limit (wired NC → ON when door is fully open)
# AUXREL 2 — energizes when NOT at CLOSE limit (wired NO → ON when door is fully closed)
#
# Both are wired to D1 Mini GPIO with INPUT_PULLUP. No inversion needed:
# when the contact is open (at the limit), the pullup holds the pin HIGH → ON.
# When the contact is closed (not at limit), the pin is pulled to GND → OFF.
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: |-
// Detect external open (wall panel, remote, etc.)
// The endstop cover only tracks movements it initiates. When the
// door is opened externally, the closed limit releases but the
// cover still thinks it's closed+idle. Fix that here.
if (id(garage_door).current_operation == esphome::cover::COVER_OPERATION_IDLE
&& id(garage_door).position == 0.0f) {
ESP_LOGI("cover", "External open detected via closed limit release");
id(garage_door).position = 0.5f;
id(garage_door).current_operation = esphome::cover::COVER_OPERATION_OPENING;
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: |-
// Detect external close (wall panel, remote, etc.)
if (id(garage_door).current_operation == esphome::cover::COVER_OPERATION_IDLE
&& id(garage_door).position == 1.0f) {
ESP_LOGI("cover", "External close detected via open limit release");
id(garage_door).position = 0.5f;
id(garage_door).current_operation = esphome::cover::COVER_OPERATION_CLOSING;
id(garage_door).publish_state();
}
# Derived template sensors — useful for HA automations
- 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
# -----------------------
# Uses ESPHome's endstop cover platform — position is interpolated by
# time but the endstop switches are authoritative for fully-open/closed.
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
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
# Debug text sensor — shows human-readable door state
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