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
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:
- An open door must refuse to close from the button or remote.
- A closing door must stop and reverse to fully open.
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.
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
| Part | Notes |
|---|---|
| Seeed XIAO ESP32‑C3 | The 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 module | 3.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 switch | Wired, normally‑open, the kind sold for alarm systems. Surface‑mount with its own magnet. |
| 10k Ω resistor | Pulldown on the relay trigger line. Do not skip this — see the boot note below. |
| 2‑conductor wire, #22 or heavier | Same spec the opener manual calls for on the wall button run. You’ll want a length for the reed switch too. |
| USB‑C power supply | Into 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
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.
| From | To | Why |
|---|---|---|
5V | Relay VCC | Relay coils want 5 V; the XIAO passes USB power straight through on this pin. |
GND | Relay GND | Common ground for the opto input. |
D2 / GPIO4 | Relay IN | Trigger. Add the 10k pulldown from this line to GND. |
D3 / GPIO5 | Reed switch leg 1 | Firmware enables the internal pull‑up, so a closed switch reads LOW. |
GND | Reed switch leg 2 | Other side of the switch. |
Relay COM | Opener terminal 0 | Isolated contact — polarity does not matter. |
Relay NO | Opener terminal 1 | Normally 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.
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.
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
- Change the pairing code.
setPairingCode("46637726")is HomeSpan’s published default. Pick any eight digits that aren’t all the same or a straight run. - Set the travel window.
TRAVEL_MSships at 20 000 to suit the stock 17‑second run timer. If the board jumper was cut for a tall door, make it 32 000. Too short and you’ll get false obstruction reports.
8. Pairing with Apple Home
- 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’sWcommand does the same thing if you prefer a cable.) - Add the accessory. Home app → + → Add Accessory → More options… → it appears as Garage Door.
- Enter your pairing code — the eight digits you set, not the default.
- 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.
- 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.
- 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.
- Reed switch. Watch the serial monitor while running the door by hand and confirm the state flips only at full close.
- Time a real cycle with a stopwatch and set
TRAVEL_MSa few seconds above it.
| Symptom | Likely cause |
|---|---|
| Door opens by itself on power‑up | Missing 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 close | Either 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 flashes | Working 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 busy | The travel window hasn’t expired. Expected if you issue commands faster than the door moves. |
| State is stale after using the wall button | Reed switch wiring or debounce. The firmware polls it continuously, so this is almost always a physical connection. |
text section exceeds available space | Default partition scheme. Rebuild with Minimal SPIFFS — see the warning above. |
| Accessory drops off WiFi in the garage | Fit 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.