Siri‑controlled garage door,
without cloud or hub

Adding HomeKit voice control to a Challenger AC9500 opener using a XIAO ESP32‑C3 — while leaving the original wall button completely untouched.


The goal here is narrow and worth stating up front: say “Hey Siri, open the garage” from an iPhone on the home network, and have the door open. No subscription, no bridge server, no round trip through somebody’s cloud. The ESP32 becomes a native HomeKit accessory and talks directly to the phone over the LAN.

The existing wall button keeps working exactly as it does today, because we never interrupt it — we wire a relay in parallel with it. Both are momentary contacts across the same pair of screws, and the opener cannot tell them apart.

  1. Do this first: the safety tests
  2. How it works
  3. Parts
  4. Understanding this opener
  5. Wiring
  6. Mounting the reed switch
  7. Flashing the firmware
  8. Pairing with Apple Home
  9. Testing and troubleshooting

1. Do this first: the safety tests

This is not boilerplate

A garage door is the heaviest moving object in most houses. Every argument for letting a phone close it rests on the opener’s photo eyes actually working. On a unit that is likely 25‑plus years old, that is an assumption you need to verify rather than inherit — before you attach a relay to it.

Run both of the tests the manual mandates. If either fails, stop and get the opener serviced. Neither takes more than five minutes.

Photoelectric obstruction test

Place a 6″ × 12″ object on the floor in the beam path, and repeat it in three positions: one foot in from the left jamb, dead center, and one foot in from the right jamb. In each position:

If the door closes anyway in any of the three positions, the Safe Finish photosystem needs realignment — lower the sensors and retest until it responds. The manual’s own language on failure here is blunt: “the door may cause severe injury or death.”

Force reversal test

Lay a 2×4 flat on the floor under the door and close it. The door must strike the board, stop, and reverse. If it doesn’t, the down-force needs adjusting on the operator before anything else happens.

Both of these are supposed to be monthly checks on a door nobody is operating remotely. Once a phone can close it while you’re in another room, they matter considerably more. Put a reminder in your calendar.

2. How it works

Four pieces. The phone talks HomeKit to the ESP32 over WiFi; the ESP32 closes a relay for half a second; the relay shorts the opener’s wall-button terminals; and a reed switch on the door tells the ESP32 whether the door is actually shut.

iPhone Siri / Home app HomeKit over WiFi XIAO ESP32‑C3 HomeSpan GPIO4 500 ms pulse Relay opto‑isolated dry contact shorts term. 0 + 1 Challenger AC9500 opener Reed switch on the door door position → GPIO5 Wall button unchanged same terminal pair, in parallel
The relay and the wall button are electrically identical from the opener’s point of view — two momentary contacts across the same terminal pair. The reed switch is what makes honest state reporting possible.

That last piece is not optional. Without door position, the accessory can only guess, and an accessory that guesses will eventually tell you the door is shut when it is standing wide open.

3. Parts

PartNotes
Seeed XIAO ESP32‑C3The C3 has a u.FL connector for an external antenna, which matters in a garage full of sheet metal and a steel door. Worth it over the bare-chip-antenna variants.
Opto‑isolated relay module3.3 V‑compatible trigger. Use a real mechanical relay rather than a bare MOSFET — the isolation means you don’t care what voltage or polarity the opener puts on its terminals.
Magnetic reed switchWired, normally‑open, the kind sold for alarm systems. Surface‑mount with its own magnet.
10k Ω resistorPulldown on the relay trigger line. Do not skip this — see the boot note below.
2‑conductor wire, #22 or heavierSame spec the opener manual calls for on the wall button run. You’ll want a length for the reed switch too.
USB‑C power supplyInto the ceiling outlet the opener already uses.

The boot glitch, and why the pulldown matters

ESP32 pins can float or briefly pulse during boot and reset. On an ordinary project that’s a harmless flicker. Here it is a garage door that opens itself during a power blip. Three defenses, all cheap: put a 10k pulldown from the trigger line to ground, avoid the strapping pins (GPIO2, GPIO8, GPIO9 on the C3), and let the firmware drive the pin low before anything else runs. The sketch does the third; the other two are on you.

4. Understanding this opener

The Challenger AC9500 is part of the Allstar AC9000 series — not a Chamberlain, despite what the brand name suggests. That rules out the Chamberlain-specific hacks like ratgdo, but it works in your favor: this opener uses a plain dry-contact wall button with no digital protocol to reverse-engineer.

The terminal strip

Operator rear panel — terminal strip optional Deluxe Wall Station (1–2–3) 0 1 2 3 4 5 wall push button ◀ wire your relay here Safe Finish photo eyes ⚠ leave alone Terminal 1 is shared The standard button uses 0 and 1; the Deluxe Wall Station uses 1, 2 and 3. If a Deluxe Station is already fitted, your relay still goes across 0 and 1 alongside it. Power down the operator before touching any of this. Strip about ½″ of insulation and land the relay leads under the terminal screws next to the wires already there.
Terminals 0 and 1 are the standard wall push button — a plain momentary dry contact. That is the pair your relay joins.

Two behaviors that shape the firmware

It is alternating action, not a toggle. From a settled position the button does what you’d expect: closed → opens, open → closes. But mid‑travel, a press means stop, and the press after that reverses direction. A naive controller that just pulses the relay whenever asked will desynchronise from the door within a few commands. The firmware therefore only ever fires from a settled end state, and refuses commands while the door is moving.

“Constant Contact To Close.” This one is worth quoting from the manual:

“If the light begins to flash and the door does not move in the close direction from a push button or radio, the external safety device is activated or defective. To temporarily override and close door, activate pushbutton for 2 seconds; opener will begin moving in the down direction. The button must remain depressed until the cycle is completed. If the button is released before cycle is completed, the door will reverse and come to a fully open position.”

So when the photo eyes are blocked, misaligned, or failed, a half‑second pulse does nothing at all, and radio remotes cannot close the door either. That is the opener working correctly.

The firmware deliberately does not automate that override. Holding the button for a full cycle is a feature meant for a person standing in the garage watching the door. Wiring it to a relay you trigger from your phone in another room removes exactly the entrapment protection this opener was built around. Instead, the sketch detects that the close didn’t happen and raises HomeKit’s ObstructionDetected flag — which is precisely what that characteristic exists for. You get a warning on the tile in the Home app rather than a door that silently stayed open.

The run timer

The AC9000 control board has a run timer that decides how long the motor is allowed to run: 17 seconds from the factory, or 29 seconds if the jumper on the board has been cut (required for doors with more than 9 feet of travel). This gives you an exact basis for the firmware’s travel window instead of guessing. Check which applies to your unit and set TRAVEL_MS a few seconds above it.

5. Wiring

Kill power to the opener at the breaker or unplug it before touching the terminal strip. The manual says the same thing, and it means it — the operator has line voltage inside the housing.

Everything on the ESP32 side runs at 3.3 V and 5 V. The only connection to the opener is through the relay’s isolated contacts, which is why you never need to know what voltage sits on terminals 0 and 1.

Complete wiring USB‑C D0 / GPIO2 D1 / GPIO3 D2 / GPIO4 D3 / GPIO5 D4 / GPIO6 D5 / GPIO7 D6 / GPIO21 5V GND 3V3 D10 D9 D8 D7 XIAO ESP32‑C3 Relay module opto‑isolated VCC GND IN COM NO 10k pulldown to GND — required holds the relay off at boot, when the pin would otherwise float Reed switch normally open to XIAO GND (pull‑up is in firmware) Opener terminal strip 0 1 the wall‑button pair Wall button already wired — untouched Legend 5 V ground trigger reed contact existing
The relay’s COM and NO contacts land on terminals 0 and 1 alongside the wires already there. Nothing crosses the isolation barrier between the ESP32 and the opener.
FromToWhy
5VRelay VCCRelay coils want 5 V; the XIAO passes USB power straight through on this pin.
GNDRelay GNDCommon ground for the opto input.
D2 / GPIO4Relay INTrigger. Add the 10k pulldown from this line to GND.
D3 / GPIO5Reed switch leg 1Firmware enables the internal pull‑up, so a closed switch reads LOW.
GNDReed switch leg 2Other side of the switch.
Relay COMOpener terminal 0Isolated contact — polarity does not matter.
Relay NOOpener terminal 1Normally open, so the door does nothing until commanded.

6. Mounting the reed switch

The reed switch answers exactly one question: is the door fully closed? So mount it where the magnet and the switch line up only when the door is all the way down — typically the switch on the track or floor bracket, and the magnet on the bottom door panel or its bracket.

Door closed — contacts aligned garage floor track door magnet reed to D3 + GND < ½″ Door open — magnet gone, reads OPEN garage floor door retracted overhead magnet has travelled up with the door reed alone → circuit open → reads OPEN
Alignment should happen only in the last inch or so of travel. Keep the gap under about half an inch when closed — most alarm‑grade reed switches want to be closer than you’d expect.

Before you mount anything permanently, hold the parts in position and watch the ESP32’s serial output (or the Home app tile) while you run the door by hand. Confirm it reads closed only when the door is genuinely all the way down — not two inches up.

7. Flashing the firmware

The sketch is a HomeSpan accessory implementing HomeKit’s GarageDoorOpener service. Its whole job is to reconcile what HomeKit asks for with what this particular opener will actually do.

Download: garage_door.ino · flash.sh — put the .ino in a folder named garage_door, with flash.sh one level above it.

Firmware state machine CLOSED reed engaged OPENING travel window OPEN reed released CLOSING travel window OPEN + Obstruction check the photo eyes pulse travel elapsed pulse reed engaged travel elapsed, reed still released Commands arriving mid‑travel are rejected rather than queued — on this opener an extra press would mean STOP, not hurry up. A door moved by the wall button is picked up from the reed switch, and the HomeKit target state re‑syncs on its own.
Pulses only ever fire from a settled end state. If a close doesn’t take, the accessory reports an obstruction rather than retrying — retrying cannot help when the opener wants constant contact.

Build and upload with arduino-cli

Install the toolchain once:

curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
  | BINDIR=~/.local/bin sh

arduino-cli config init
arduino-cli config add board_manager.additional_urls \
  https://espressif.github.io/arduino-esp32/package_esp32_index.json
arduino-cli core update-index
arduino-cli core install esp32:esp32
arduino-cli lib install HomeSpan

You must change the partition scheme

HomeSpan plus WiFi plus OTA comes to roughly 1.5 MB, which overflows the XIAO’s stock 1.2 MB app partition — the build fails with text section exceeds available space in board. Switch to Minimal SPIFFS (1.9MB APP with OTA), which leaves OTA intact and lands at about 76% full. In the Arduino IDE that’s Tools → Partition Scheme → Minimal SPIFFS; on the command line it’s an FQBN suffix.

Then, with the XIAO plugged in over USB‑C:

# confirm the board enumerated
arduino-cli board list

FQBN=esp32:esp32:XIAO_ESP32C3:PartitionScheme=min_spiffs

# compile
arduino-cli compile --fqbn $FQBN ./garage_door

# upload (substitute your port)
arduino-cli upload -p /dev/ttyACM0 --fqbn $FQBN ./garage_door

# watch it come up
arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

A wrapper script that sets all of this and auto-detects the port:

./flash.sh build             # compile only
./flash.sh upload            # compile + upload
./flash.sh monitor           # serial console at 115200

If the board doesn’t appear, hold the BOOT button while plugging it in to force the bootloader, then release. On Linux you may also need your user added to the dialout group: sudo usermod -aG dialout $USER followed by a re‑login.

Before you flash: two edits

8. Pairing with Apple Home

  1. Get it on WiFi. On first boot HomeSpan raises a temporary access point called HomeSpan‑Setup. Join it from your phone and enter your network credentials. (The serial console’s W command does the same thing if you prefer a cable.)
  2. Add the accessory. Home app → +Add AccessoryMore options… → it appears as Garage Door.
  3. Enter your pairing code — the eight digits you set, not the default.
  4. Assign it a room and let Siri learn the name. “Hey Siri, open the garage” works from then on.

Firmware updates after this point go over the air, since the sketch calls enableOTA(). Its default password is homespan‑ota; pass your own to enableOTA("…") if that bothers you, which it probably should.

No hub required — for this

Everything above works with nothing but the phone and the ESP32 on the same network. A HomePod mini or Apple TV only becomes necessary if you later want the door reachable from outside the house, or want it to participate in automations that run while you’re away. The firmware doesn’t change either way.

9. Testing and troubleshooting

Bring it up in stages rather than wiring everything and hoping.

  1. Relay alone, opener powered down. Flash the firmware, trigger it from the Home app, and listen for the relay click. Nothing is connected to the opener yet.
  2. Relay on the terminals. Restore power. Confirm the wall button still works first — that’s your proof you haven’t disturbed the existing circuit. Then trigger from the phone.
  3. Reed switch. Watch the serial monitor while running the door by hand and confirm the state flips only at full close.
  4. Time a real cycle with a stopwatch and set TRAVEL_MS a few seconds above it.
SymptomLikely cause
Door opens by itself on power‑upMissing pulldown on the trigger line, or the relay is landed on a strapping pin. Fix before doing anything else.
Home app shows an obstruction after every closeEither TRAVEL_MS is shorter than the real cycle, or the reed switch isn’t engaging at full close. Check the reed first.
Close does nothing; opener light flashesWorking as designed — the photo eyes are blocked or misaligned. Go look at the sensors; the red LED on the receiver should be lit and steady.
Commands rejected as busyThe travel window hasn’t expired. Expected if you issue commands faster than the door moves.
State is stale after using the wall buttonReed switch wiring or debounce. The firmware polls it continuously, so this is almost always a physical connection.
text section exceeds available spaceDefault partition scheme. Rebuild with Minimal SPIFFS — see the warning above.
Accessory drops off WiFi in the garageFit the external u.FL antenna. Sheet metal doors and steel framing are unkind to chip antennas.

What this deliberately does not do

It won’t force a close past a photo‑eye fault, it won’t work from outside the house without an Apple home hub, and it won’t appear in Google Home. The last one is a hard limit rather than an omission — Google has no path for local custom devices, and Matter still has no garage door device type, so anything Google‑side would mean a bridge server and a round trip through the cloud to open a door you’re standing next to.