A Home Assistant custom integration that monitors and controls a garage door which has only a single button (open, close, and stop are all the same press) plus two contact sensors — one at the bottom of the travel and one at the top. It infers the full door state (open / closed / opening / closing / stopped), drives the button, and learns how long the door takes to open and close so it can report a live position percentage.
This is a rebuild of the original monitorgarage AppDaemon app as a proper
UI-configurable integration: no input_select helper and no template cover
are needed — the integration provides the cover entity itself.
| Bottom sensor | Top sensor | Meaning | Position |
|---|---|---|---|
| off | off | Fully closed | 0% |
| on | off | Mid-travel | inferred |
| on | on | Fully open | 100% |
| off | on | invalid (ignored) | — |
- Bottom sensor turns on as soon as the door leaves the fully-closed position.
- Top sensor turns on once the door reaches fully-open.
The sensors can't reveal direction while the door is mid-travel, so direction is inferred from the endpoint the door most recently left (leaving closed → opening, leaving open → closing), exactly as the AppDaemon app did. The single button is what stops a moving door or reverses a stopped one — something the contacts can never show on their own.
The integration times each clean run and averages it into a learned open-duration and close-duration, then interpolates the position while the door moves.
A run only counts if it goes endpoint → the opposite endpoint with no button press in between. Any press mid-run is a stop or a reverse, so that run is marked dirty and its timing is discarded — partial or interrupted travels never corrupt the estimate. Until a direction has been learned, a default travel time is used for the interpolation.
The learned durations persist across restarts and adapt slowly over time (a capped running average), so a door whose timing drifts is tracked rather than frozen.
The door can also be operated by a remote or the wall button, which never touch the toggle switch — so those presses are invisible to the integration and are tracked purely from the two contact sensors. Normal opens and closes by any means are handled fully (state, position, and learning all update).
The one thing that can't be seen directly is a mid-travel stop by a remote or wall button (no switch event, and the contacts don't change while the door sits between them). Two guards handle it:
- Learning guard — a travel that takes more than twice the already-learned time is discarded, so a paused-then-resumed run can't inflate the average.
- Stuck guard — once a move overruns ~2.5× its expected duration, the door is assumed to have been stopped mid-travel and is reported stopped, with the direction remembered. That hidden stop also advances the opener's own single-button toggle, so remembering the direction keeps the integration in sync: the next press it can see (the HA switch) then reverses correctly — a paused open becomes a close. The sensors remain the source of truth: if the door actually reaches an endpoint, that supersedes the guess.
- Cover — the garage door: open / close / stop,
is_opening/is_closing, a live position percentage, and best-effort set position (see below). - Binary sensor ×2 — Top sensor and Bottom sensor, mirroring the two contacts (diagnostic).
- Sensor: State — the inferred door state as an enum.
- Sensor: Position — the door's open percentage (0–100%). A
device_class: garagecover is shown as open/closed in the HA frontend and doesn't render the percentage, so the learned position is exposed here as its own value for dashboards and templates. (The cover still carries it ascurrent_position.) - Sensor: Learned open time / Learned close time — the learned durations in
seconds, with a
samplesattribute (diagnostic). - Sensor: Sensor status — health of the two battery contacts as text:
OK, or a warning such as "Top door sensor is not reporting - check its battery". - Binary sensor: Sensor problem —
on(device class problem) whenever a contact isn't reporting or reads an impossible state; the clean trigger for a notification (detail in itsmessageattribute). - Button: Press — the raw single-button action: one pulse, whatever the door is doing. Handy when you want the literal wall-button behaviour.
These are battery contact sensors, so the integration is defensive about one of them going quiet:
- A contact reporting
unavailable/unknown(dead battery) is never acted on — it can't move the door state or corrupt the learned timings. The door simply holds its last known state until the contact reports again. - The impossible reading top open while bottom closed is treated as a faulty sensor and ignored the same way.
- Both conditions raise Sensor problem and set Sensor status to a warning, so you can be told the moment a sensor stops reporting.
Example notification automation:
automation:
- alias: Garage sensor not reporting
trigger:
- platform: state
entity_id: binary_sensor.garage_door_sensor_problem
to: "on"
for: "00:05:00"
action:
- service: notify.mobile_app_your_phone
data:
title: Garage door
message: "{{ state_attr('binary_sensor.garage_door_sensor_problem', 'message') }}"Because the opener has a single button, the cover's open/close/stop each issue a single guarded press:
- Open — pressed unless the door is already open or opening.
- Close — pressed unless the door is already closed or closing.
- Stop — pressed only while the door is moving.
Each press pulses the configured switch (on, briefly, then off) so a momentary relay re-arms for the next press.
The cover also supports set position, so the HA frontend shows and controls the open percentage. Since there's only one button, the position is reached by timing: the door is started in the right direction and the button is pressed again to stop it once the door's interpolated position — derived from the learned travel time — reaches the target. For example, on a door that takes 14 s to open, asking for 50 % from closed stops it about 7 s in.
This is deliberately best-effort: the stop is judged from the moment the sensors confirm the door actually moved (so the press-to-move delay doesn't skew it), but the timing won't be perfectly accurate, and it needs a learned open/close duration to be meaningful (until then it uses a default estimate). Full open (100 %) and full close (0 %) skip the timing and let the opener's own limit switch stop the door.
Settings → Devices & Services → Add Integration → Garage Door Monitor, then choose:
- Top sensor (binary_sensor, on when fully open)
- Bottom sensor (binary_sensor, on once the door leaves closed)
- Toggle switch (the switch / input_boolean that operates the opener)
You can change any of these later via the integration's Configure option.
Your old AppDaemon config mapped straight across:
| AppDaemon arg | Integration field |
|---|---|
top_sensor |
Top sensor |
lower_sensor |
Bottom sensor |
toggle_switch |
Toggle switch |
The input_select helper and the cover.garage_template template cover are no
longer needed — remove them once the integration's own cover entity is in use.
Copy custom_components/monitorgarage into your Home Assistant config/custom_components
directory (or add this repository to HACS as a custom repository), then restart
Home Assistant.