---
title: The Veto That Never Retries
description: A humidity reading of 51 against a limit of 50 kept a bathroom lit for nearly nine hours. Why edge-triggered Home Assistant vetoes never retry, and the fix.
image: https://willcurran.com/hubfs/will-curran-brand/the-veto-that-never-retries-cover-nolane.png
---

[Skip to content](https://willcurran.com/blog/the-veto-that-never-retries#main)

[Will Curran](https://willcurran.com/)

[Home](https://willcurran.com/) [Portfolio](https://willcurran.com/portfolio) [Press](https://willcurran.com/media) [Blog](https://willcurran.com/blog) [Favorites](https://willcurran.com/favorites) [Speaking](https://willcurran.com/speaking) [Consulting](https://willcurran.com/consult) 

[Hire Will](https://willcurran.com/hire)

[Writing](https://willcurran.com/blog) Home — The Veto That Never Retries [Subscribe](https://willcurran.com/blog/subscribe)

Home

# The Veto That Never Retries

![](https://willcurran.com/hs-fs/hubfs/will-curran-brand/will-curran-avatar.jpg?width=44&height=44&name=will-curran-avatar.jpg)

Will Curran Sep 18, 2026 · 7 min read

<https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwillcurran.com%2Fblog%2Fthe-veto-that-never-retries>

[Part of: Checks that lie](https://willcurran.com/blog/series/checks-that-lie) Part 7 of 7

On this page

![An abstract dark graphic: a horizontal timeline in grey with one orange pulse that hits a closed gate and stops, and a second orange pulse further along that loops back and passes through, next to the title The veto that never retries.](https://willcurran.com/hs-fs/hubfs/will-curran-brand/the-veto-that-never-retries-cover-nolane.png?width=1600&height=900&name=the-veto-that-never-retries-cover-nolane.png)

In September a bathroom in my house stayed lit for nearly nine hours because a humidity sensor read 51 when the rule wanted it under 50. Thirteen minutes later it read 48, and nothing happened. The automation that turned the lights off had already used its one chance, and in Home Assistant a condition that turns true again does not start anything.

The fix was two changes: give every veto a trigger that fires when the veto clears, and stop using humidity as the main signal for a shower that has already ended.

## One point of humidity, one missed edge

The lights-off automation was edge-triggered. It ran when the motion sensor went quiet, and before turning anything off it checked a steam guard: if the room was still humid, someone was probably in the shower, so leave the lights alone.

When it failed, the trigger fired on time. The guard read 51 against a limit of under 50 and said no. The automation stopped there, exactly as written. Humidity dropped below the limit thirteen minutes later, but a condition becoming true is not an event. Only triggers start automations, and the only triggers here were motion edges. The next one came nearly nine hours later.

Any missed edge does the same: a veto, a reload, a restart.

![Two timelines. Before: motion went quiet, the lights-off automation fired, the steam guard read humidity 51 against a limit under 50 and vetoed, humidity reached 48 thirteen minutes later but nothing fired, and the lights stayed on for nearly nine hours until the next motion. After: the same veto, then a re-arm trigger at humidity below 50, the guard allowing the run, and lights off.](https://willcurran.com/hs-fs/hubfs/will-curran-brand/the-veto-that-never-retries-veto-timeline.png?width=1440&height=846&name=the-veto-that-never-retries-veto-timeline.png)

## The trace knew, and the theories didn't

There were two theories, and both were wrong. Mine was that the motion sensor had hung. Its own history killed that: it had been reporting normally, with a full battery. The other was that the bulbs had lost power and come back on by themselves. That one died on a detail. The bulbs had all changed together, each to a different colour, which is what a scene does. Ours would have come back from a power cut identical.

The automation trace had the answer in one line: the condition was false, and it named the humidity sensor. Opening it first would have saved the guessing.

There was also a tell I learned to look for. A run that stops at a condition did not update the automation's `last_triggered`. So if an automation should have run and its `last_triggered` looks stale, suspect a veto before you suspect a dead trigger.

## Re-arm on the release

The first fix kept the guard and the threshold exactly as they were. I added a trigger for the moment the veto cleared: humidity crossing below 50. I also added a slow backstop that checked every five minutes but only acted after 15 minutes of no motion instead of the usual two.

Reloading automations is not free in an edge-triggered system. If a reload lands just after a motion edge, that edge can be lost, and the room latches the same way. One of our reloads landed seconds after a motion edge.

## Humidity was the wrong clock

The latch was fixed, but the lights were still slow to go off. Two days later I measured why. The house already had a shower sensor, and no lighting automation was using it. On one shower, that sensor said the shower had ended, and humidity didn't fall below 50 until about 45 minutes later.

My first move was the obvious one, and it didn't work. I raised the humidity limit from 50 to 75. That only cut the wait to about 32 minutes, and I replaced it within the hour. A threshold can't fix a signal that lags by most of an hour.

## Shower sensor first, humidity as the backup

The guard was rebuilt in priority order:

1. **Shower sensor on: veto.** A person standing still behind a curtain in steam often doesn't trip a motion sensor.
2. **Five minutes after the shower sensor went off: veto.** Time to towel off.
3. **Humidity at 50 or more: veto, but only if the shower sensor was unavailable.** Humidity became the fail-safe, so a low limit was fine again.
4. **Both sensors dead: allow.** A broken sensor should never mean "shower forever".

Each veto got its re-arm. One trigger fired once the shower sensor had been off for five minutes, exactly when the grace period ended. The humidity trigger stayed at 50, matching the backup limit. That pairing is the rule this taught me: move a threshold and its re-arm trigger together, because the gap between them is where the latch lives.

Before trusting any of it, I rendered the guard against seven cases, then once with the real states: shower sensor off past the grace period, humidity 51.0. It returned allow. The reading that had held the room on for nearly nine hours would let the lights go off.

## If you are about to try this

- **List every condition that can say no.** For each one, add a trigger that fires when it goes back to yes.
- **Move thresholds in pairs.** If you change a guard's limit, change its re-arm trigger in the same edit.
- **Read the trace before you theorise.** It names the condition that failed and the entity behind it.
- **Ask what actually ends the event.** Pick the fastest honest signal as the primary, and keep the slow one as a backup.
- **Fail toward normal.** When every sensor is dead, let the room behave as if they all read normal.

## Build it yourself

What it does: turns a room's lights off after motion stops, holds them on while a shower is running, and never gets stuck when a hold clears between triggers. You need a motion sensor, a humidity sensor, a binary sensor that is on while water is running in the shower, and lights assigned to the room's area. The entity names below are placeholders, and the numbers are starting points.

```
triggers:
  - trigger: state            # the normal path
    entity_id: binary_sensor.example_motion
    to: "off"
    for: "00:02:00"
  - trigger: state            # re-arm: shower hold released
    entity_id: binary_sensor.example_water_running
    to: "off"
    for: "00:05:00"
  - trigger: numeric_state    # re-arm: backup humidity hold released
    entity_id: sensor.example_humidity
    below: 50
  - trigger: time_pattern     # slow backstop
    minutes: "/5"
    id: backstop
conditions:
  - condition: state
    entity_id: binary_sensor.example_motion
    state: "off"
    for: "00:02:00"
  - condition: template       # backstop waits for 15 quiet minutes
    value_template: >
      {{ trigger.id != 'backstop' or
         (now() - states.binary_sensor.example_motion.last_changed)
           .total_seconds() > 900 }}
  - condition: template       # steam guard: water first, humidity as backup
    value_template: >
      {% set w = states('binary_sensor.example_water_running') %}
      {% if w == 'on' %} false
      {% elif w == 'off' %}
        {{ (now() - states.binary_sensor.example_water_running.last_changed)
             .total_seconds() >= 295 }}
      {% else %}
        {{ states('sensor.example_humidity') | float(0) < 50 }}
      {% endif %}
actions:
  - action: light.turn_off
    target:
      area_id: example_bathroom
```

The grace check uses 295 seconds against the trigger's five minutes, so the re-arm always lands just past the line instead of racing it. If both sensors are unavailable, the humidity value falls back to 0 and the guard allows the turn-off. Render the guard in Developer Tools with each sensor state you can fake before you trust it.

Give this to your AIReview my Home Assistant automations for latches. For each automation, list its triggers and its conditions. Flag every one where a trigger is an edge (a state change, or a state held "for" some time) and a condition can block the action, because if the condition clears later, nothing re-runs it. For each blocking condition, write a matching trigger that fires when that condition becomes true again, and make its threshold or duration match the condition's exactly. Where a room could be left in the wrong state for a long time, suggest a slow time-pattern backstop with a longer quiet window than the main trigger, so it doesn't override a light someone set by hand. Point out any guard that relies on a slow signal, like humidity after a shower, when a faster one exists, and suggest using the fast one as primary with the slow one as backup only when the fast one is unavailable. Make sure every guard allows the normal action when all of its sensors are unavailable. Then tell me how to test each guard in Developer Tools, including the case that originally failed.

If a room in your house stays lit for no reason, open the trace first, and I'd like to hear what it said.

Filed under [Home](https://willcurran.com/blog/tag/home)

![](https://willcurran.com/hs-fs/hubfs/will-curran-brand/will-curran-avatar.jpg?width=64&height=64&name=will-curran-avatar.jpg)

Will Curran

I spent sixteen years building one company, and I have been in the event industry since 2007. These days I write about the technology I actually use — trucks, radios, home infrastructure, and the tools that survive past the first invoice.

[All posts by Will →](https://willcurran.com/blog/author/will-curran)

Get the next one by email

One email per post. No digest, no sponsors.

[← Previous A Switch That Feeds Smart Bulbs Is Not a Light](https://willcurran.com/blog/a-switch-that-feeds-smart-bulbs-is-not-a-light) [Next → Trivia Over LoRa: a game show on a slow, lossy radio mesh](https://willcurran.com/blog/trivia-over-lora)

## Related reading

[Home The Heat Rating That Decides a Desert Garage Mini-Split 4 min read](https://willcurran.com/blog/desert-garage-mini-split-heat-rating) [Home Movie Mode Means the Robot Vacuum Waits 6 min read](https://willcurran.com/blog/movie-mode-vacuum) [Home A Holiday Color Engine with a Calendar Computed to 2099 7 min read](https://willcurran.com/blog/holiday-color-engine) [Home Party Mode: 49 Ramps Blocked, Zero During a Six-Hour Party 6 min read](https://willcurran.com/blog/party-mode)

[Will Curran](https://willcurran.com/)

[LinkedIn — 29,339+ followers](https://www.linkedin.com/in/willcurran) [GitHub](https://github.com/rancur) [YouTube](https://www.youtube.com/channel/UCJgscc27l4fpkWx1SeK05xg/) [Spotify](https://open.spotify.com/user/rancur) [SoundCloud DJ mixes](https://www.soundcloud.com/thewillsee) [Twitch](https://www.twitch.tv/thewillsee) [Last.fm](https://www.last.fm/user/default) [Trakt — movies & TV](https://trakt.tv/users/willcurran) [Goodreads](https://www.goodreads.com/user/show/55302838-will-curran) [Will See Music](https://www.thewillsee.com) [Arizona Meshtastic](https://azmsh.net) [Electric Forest Meshtastic](https://efmesh.com)

© 2026 Will Curran — All rights reserved. [RSS](https://willcurran.com/blog/rss.xml) [Back to top](https://willcurran.com/blog/the-veto-that-never-retries#main)

```json
{
  "@context" : "https://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Will Curran",
    "url" : "https://willcurran.com/blog/author/will-curran"
  },
  "dateModified" : "2026-10-22T18:10:51.227Z",
  "datePublished" : "2026-09-18T16:00:00.000Z",
  "headline" : "The Veto That Never Retries",
  "image" : [ "https://willcurran.com/hubfs/will-curran-brand/the-veto-that-never-retries-cover-nolane.png" ],
  "mainEntityOfPage" : {
    "@id" : "https://willcurran.com/blog/the-veto-that-never-retries",
    "@type" : "WebPage"
  },
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "url" : "https://willcurran.com/hubfs/image-1.png"
    },
    "name" : "Will Curran LLC"
  }
}
```