One button, nine states: a gate that cannot tell you what it is doing
My entrance gate has one button. Press it and the gate opens; press it while it moves and the gate stops; press it again and the gate closes. The controller knows which of those comes next. It does not tell anyone.
Most guides to a “smart gate” wire a relay in parallel with that button and show it in Home Assistant as a switch. That works until someone presses at the wrong moment. With a single-button gate, “open” can open it, stop it or send it back the way it came, and Home Assistant cannot say which. Some controllers have separate open and close inputs or an output that reports the state, and if yours does, use them. Mine has only the button.
So the ESP32 at my gate keeps its own model of the gate’s state and checks it against the only facts available: a limit switch at each end of the travel and the photocell across the driveway.
Nine states, three sources
The model has nine states: closed, opening, opening and blocked, opening and stopped, open, the same three for closing, and unknown. Three kinds of event move it.
A press the ESP32 sends is a prediction. It knows the cycle, so after a press it moves the model to the state the gate should be in next.
A limit switch is a fact, and it overrules the prediction. Pressed means the gate is at that end. Released means it has just left that end, which also gives the direction.
The photocell is a modifier. When a car breaks the beam, the gate’s controller stops or reverses on its own, and the model only marks the movement as blocked.
| State | After a press | After a sensor event |
|---|---|---|
| closed | opening | closed limit released: opening |
| opening | opening, stopped | beam blocked: opening, blocked; open limit pressed: open |
| opening, blocked | opening, stopped | beam clear: opening |
| opening, stopped | closing | |
| open | closing | open limit released: closing |
| closing | closing, stopped | beam blocked: closing, blocked; closed limit pressed: closed |
| closing, blocked | closing, stopped | beam clear: closing |
| closing, stopped | opening | |
| unknown | no change | closed limit pressed: closed; open limit pressed: open |
A limit switch sets closed or open from any state, and a gate that keeps “moving” for longer than a set time without reaching either switch becomes unknown.
The model can be wrong. A press the ESP32 does not see, from the radio remote or the button on the controller, leaves it behind the real gate. The next limit switch puts it right, and the timeout covers a gate stopped halfway by a remote.
Intent becomes a number of presses
On top of the model sits an ordinary ESPHome cover, so Home Assistant shows a gate with open, close and stop. Each action looks up the current state and turns the intent into one, two or three presses. “Close” on an open gate is one press. On a gate that is opening it is two (stop, then close). On a gate that stopped while closing it is three, because the next press would open it. Every press goes through the same transition table, so the model follows the gate one step at a time.
In unknown the cover refuses to press at all. After a reboot with the gate halfway, nobody knows which way the next press sends it. The configuration running at my gate still presses once in that state and hopes; the published package logs a warning and waits for a limit switch.
The whole state machine, the cover and the timeout fit in one YAML file. That is Firmware-as-Config: the device’s behaviour declared in a file, from which the firmware can be rebuilt. The package is gate-controller.yaml in my snippets repo, with pins, travel times, sensor polarity and the timeout as parameters. It assumes the open-stop-close-stop cycle described above; many controllers can be switched to other step modes, so check which one yours uses. The numbers in it (22 seconds of travel, the debounce times) belong to my gate. Measure yours.
What the code does not protect
A powered gate can injure or kill. This package only simulates a press of the controller’s button. Stopping for an obstacle is the controller’s job, with its own photocells, safety edges and force limit, and the cover deliberately ignores the photocell for that purpose. Nothing in the package may be wired so that it bypasses them, and the wiring into a gate controller belongs to someone qualified for that controller.
The code also does not know whether anyone is standing in the gateway. Closing from Home Assistant or from an automation moves a gate nobody may be watching. That is acceptable only because the controller’s own safety system stops the gate for anything in the way. If your gate lacks one, or has no warning light or buzzer before movement, add them before you automate closing.
Two conveniences at my gate close it without anyone at a button. The first is auto-close after passing: a switch opens the gate and arms a flag, and when the photocell clears above a set position, which the cover estimates from travel time (about half the travel at my gate), the gate closes. The flag is not restored after a power cut, so the feature starts off. The second is a knock sequence: blocking the photocell by hand in a rhythm closes the gate from the driver’s seat. It never opens the gate, but it has no authentication, so anyone at the beam who knows the rhythm can close it. In the package it is off by default, uses a different rhythm from the one at my gate, and the comment says to change it.
Systems that do not report their state
A system that does not report its state looks the same in a company: a vendor’s black box or a legacy service with one endpoint that does something. The approach is the same too. Name every state, including “unknown”. Predict what your own commands do, but let independent signals at the boundaries correct the prediction. Decide in advance what the system does when it does not know, and make that the harmless option. Keep the safety mechanisms in a layer your code cannot switch off.
An AI agent connected to Home Assistant could close my gate today. It would send an intent, the layer underneath would turn it into presses or refuse because the state is unknown, and the controller’s safety system would still stop the gate for an obstacle.