Yesterday it rained 15.5 millimeters at my house, the wettest day in two weeks. My sump pump is on its timed schedule: two minutes on, ten minutes off, around the clock. It pulsed 118 times on the rainy day. Every pulse drew about 500 watts. Nothing else happened. No state change, no safety cutoff, no "pump stopped on its own." The six dry days before it looked exactly the same: 741 pulses since October 6, and all 741 drew power.
A power sensor can't tell me whether that is groundwater or a jammed float. I've written about that gap before, and the two-minute inspection that would close it is still on my list. So this week I asked the code a different question. The pump has drawn 500 watts on every scheduled pulse for six days. What would my system say if pulse 742 drew nothing?
I traced it through all three programs on the monitoring host. Nothing below is inferred from logs. Every line is a code path, and I'll give the line numbers.
The first ten seconds
During a timed pulse the monitor keeps polling the plug. From second 10 onward, if the latest reading shows the pump not drawing power, this branch fires:
# sump_pump_monitor.py, line 916
if elapsed > 10 and not pump_running:
log(f"{sm.state}: Pump stopped on its own ({power:.1f}W) — float may be unstuck!")
sm.transition(COOLDOWN, "pump stopped during ON pulse")
That branch exists for a good reason. The whole point of the timed schedule is to work a float that got stuck in the up position. If the pump stops drawing power partway through a pulse, the most hopeful reading is that the float released and the pit emptied. This path has fired 84 times in seven months, and on some of those occasions it was right.
But look at what it can't see. A float that releases produces a pulse that reads 500 watts for a while and then zero. A motor that has died produces a pulse that reads zero from the first poll. The monitor tests only the latest reading, so it reaches the same conclusion about both: float may be unstuck.
Ninety seconds later
COOLDOWN has one test, and I've written about how short it is. If the pump stays idle for 90 seconds, the monitor is satisfied:
# sump_pump_monitor.py, line 942
log(f"COOLDOWN: Pump idle for {elapsed:.0f}s — confirmed unstuck!")
send_notification(
"Sump pump: float unstuck, back to normal",
"Pump stayed idle for 90s during cooldown.\n"
"Float switch appears to have unstuck.\n\n"
"Returning to normal monitoring mode.",
)
turn_on()
sm.transition(NORMAL, "cooldown confirmed")
A dead pump passes this test perfectly. It will stay idle for 90 seconds. It will stay idle forever.
Then the notification. The call has no urgent=True, so it goes to the LOG tier, which reads its recipient list from a config line that has been blank since late summer. The log records LOG email skipped: no recipients configured, as it has 6,263 times so far. The "back to normal" message is addressed to nobody. The relay is left on, the state is NORMAL, and the system settles in to watch for the next float-driven run that will never come.
Twelve hours later
Three separate components keep a no-run clock, and all three are set to twelve hours. Here is what each one does when it expires, on a day the weather model calls dry.
| Component | What it says | Where it goes |
|---|---|---|
| Main monitor (line 582) | NO-RUN WARNING. Body text ends: "Dry conditions: pump idling is likely expected, no action needed." | urgent=wet. Dry day: LOG tier, nobody. Then a latch is set, so this message is never sent again, including when it finally rains. |
| Guardian (line 443) | no pump run observed. Body text includes: "or the pump may have failed." | Weather-conditional. Dry: LOG tier, nobody. It re-checks hourly and re-evaluates the weather each time, so it does page me at the first hour the model calls wet. |
| Smart assessor (line 316) | FLOOD_RISK WARN | No pump run seen in 12.0h. Suggested action: eyeball the pit. | Delivered. The assessor has its own populated recipient list. This is the alarm I wrote about last time, the one that has fired 200 times and been wrong 200 times. |
So on a dry day, the dead-pump timeline is: a hopeful log line at ten seconds, a "back to normal" sent to nobody at a hundred seconds, twelve hours of silence, then two more messages to nobody and one to me. The one I get has the same subject line as two hundred false alarms. And the only component that explains itself in plain English, the main monitor, explains that no action is needed.
On a wet day it's better: the guardian pages me at twelve hours. That's the real backstop in this scenario, and it's worth saying so. But it's also twelve hours, it depends on the weather fetch succeeding (which, I found last month, fails silently as "dry"), and it's the same program that has at least 21 lost alerts on its record from the same blank config line.
Correction to a September post. In "Forty-Two Days at Maximum" I wrote that the dry-streak exit "has fired once in the entire history of the system." It has fired zero times. Exactly one pulse has ever logged as DRY (June 18, 13:55:48, streak 1 of 3), and the next pulse drew 504 watts, which reset the streak. The post's argument stands: the mid-pulse check above catches every dry pulse before the streak counter can see it. The count was wrong, and I'm the one who got it wrong.
Why the dead pump reads as good news
The state machine's docstring lists seven states and twelve transitions. Read them and you'll find exactly one fault in the model: a stuck float. Every tier, every unstick routine, every cooldown test is a way of working a float. The pump itself is not a thing that can fail in this design. Across the three programs, a failed pump is mentioned in exactly two alert bodies, both in the guardian, both on the weather-conditional tier that goes to nobody on a dry day. The assessor's docstring calls a dead pump "the catastrophic case," and its code has no way to recognize one.
When a model has one fault, every reading gets interpreted as evidence about that fault. Zero watts on a pulse means "the float released," because that is the only thing zero watts is allowed to mean. The system doesn't choose the happy reading because it's optimistic. It chooses it because the unhappy reading has no name.
Here is the part that bothers me most. The system has the information to tell a dead pump from a dry pit, and throws it away twice. First, the polls during a pulse see the difference between "ran, then stopped" and "never started," and the check only ever looks at the last one. Second, 741 consecutive 500-watt pulses followed by a zero is not a water table dropping in twelve minutes; the consecutive-pulse count would say so, except that the transition into COOLDOWN resets it to zero on entry. I documented that reset last month as the reason the escalation ladder has no memory. It's also the reason the ladder can't recognize a dead motor.
Three alarms that are only names
While I was in the guardian, I checked its list of URGENT keys, the ones that page me "regardless of weather." There are six. I counted how many times each one appears in the file:
| Key | Occurrences | Meaning |
|---|---|---|
hard_overtemp | 2 | Declared, and one alert() call fires it |
shelly_unreachable | 2 | Declared, and fires |
shelly_status_failed | 2 | Declared, and fires |
wet_but_no_pump | 1 | Declared. Nothing fires it. |
high_water_flood | 1 | Declared. Nothing fires it. |
high_water_level | 1 | Declared. Nothing fires it. |
The three that work are about the plug's temperature and the network. The three about water are strings in a set. I wrote in August that every alert this system had ever escalated was about its own health, not the thing it watches. I thought that was a property of the delivery record. It's a property of the source.
What I'm changing
- Make "never drew power" a different event from "stopped drawing power." In the mid-pulse check, track whether any poll in this pulse exceeded the running threshold. A pulse that never did, after a run of wet pulses, goes out as URGENT, "possible pump failure," instead of into COOLDOWN.
- Remove the latch on the monitor's no-run warning, or re-arm it hourly the way the guardian does. A warning that evaluates the weather once and then goes quiet forever is a warning that fires on the wrong day.
- Implement or delete the three orphan URGENT keys. A declared alarm that nothing can raise is worse than no alarm, because it shows up in every review as coverage.
- Gate no-run alerts on recent energy, not weather. If the plug's energy counter rose by hundreds of watt-hours in the last 24 hours and then stopped, that pump was busy and went silent. That deserves a page whatever the forecast says.
The guardian changes need no restart. The monitor changes join the bundle of fixes waiting for one.
Two rules I'm taking from this:
Every "recovered" transition should name the failure it ruled out. If the sensor can't distinguish the two, route by consequence, not by the happier reading. A dry pit costs nothing if I'm wrong about it. A dead pump costs a basement.
A declared alert key is a promise. Grep that each one has a caller. It takes thirty seconds and I had not done it in seven months.
I still haven't looked in the pit. But now that it's rained, a two-minute pulse at the discharge outlet should look different from the way it looked on the six dry days, and that's the cheapest experiment I own. It's on the list for today.
Want a monitor that knows what a dead pump looks like?
I build and audit monitoring for small buildings: sump pumps, boilers, compressors, anything with a motor and a power draw. The audit starts with the question in this post: what does your system say when the thing it watches stops?
See the Physical AI practice