HVAC Condensate-Leak Auto-Shutoff (all 3 air-handler zones)¶
Date: 2026-07-07 Ticket: #71 Status: Approved (design settled via brainstorm) Related: #69 (WS2 Floor 3 FLAG), memory third-floor-ac-condensate-backup
Goal¶
When an HVAC air-handler/furnace condensate pan goes wet, automatically turn off that zone's Ecobee to stop it producing more condensate, notify both phones, and auto-restore the unit once the pan is dry — with a 2-hour cooldown between restores so a persistently-backing-up line can't thrash the compressor.
Generalized across all three zones via a single sensor→thermostat map, so adding a future unit is a one-line change.
Zone map (confirmed with owner)¶
| Leak sensor | Thermostat | Display name | Slug (helper prefix) |
|---|---|---|---|
binary_sensor.attic_air_handler_leak_detector | climate.office | Office | office |
binary_sensor.3rd_floor_furnace_leak_detector | climate.bedroom | Master Bedroom | bedroom |
binary_sensor.crawl_space_furnace_leak_detector | climate.dining_room | Dining Room | dining_room |
All six entities are already in the registry (the 3 sensors are also in group.leak_sensors). The crawl sensor's device may not be physically placed yet — if it never reports wet, its zone simply never triggers; no code change is needed when it's installed.
Slug convention: the helper prefix is the thermostat's object_id (climate.office → office), so per-zone helpers are derived in-template from the thermostat, not stored in the map.
Sanctioned HVAC-write exception¶
HA does not control the HVAC — the Ecobees own it — with two sanctioned exceptions: 1. §9b hot-day pre-cool (existing; writes setpoints 13:00→14:59 on hot summer weekdays). 2. This automation set (new): a safety hvac_mode: off on the affected zone's Ecobee when its condensate pan leaks, with automatic restore.
Both must be documented so neither is later removed as a rule violation. This one writes hvac_mode (off/heat_cool), not a setpoint.
Helpers (6 — added in configuration.yaml, no initial: so they persist)¶
Per zone (<slug> ∈ {office, bedroom, dining_room}): | Helper | Role | |---|---| | input_boolean.<slug>_ac_condensate_shutoff | on while we hold that zone off (set at shutoff, cleared at restore / owner-override). Gates auto-restore + suppresses duplicate pushes on re-flood. | | input_datetime.<slug>_ac_condensate_last_restore | timestamp of that zone's last auto-restore, for the 2h cooldown. has_date: true, has_time: true. |
Behavior — three generalized automations (in automations.yaml)¶
New "Condensate Safety" subsection near §1 Leak Detection. Each automation triggers on all three sensors/thermostats and resolves the target zone from the trigger. Unique ids/aliases: condensate_leak_hvac_shutoff, condensate_leak_hvac_restore, condensate_leak_hvac_override.
Shared variables block (in shutoff + restore — the two sensor-triggered automations):
variables:
zone_map: # sensor object_id -> {climate, name}
attic_air_handler_leak_detector: {climate: climate.office, name: Office}
3rd_floor_furnace_leak_detector: {climate: climate.bedroom, name: Master Bedroom}
crawl_space_furnace_leak_detector: {climate: climate.dining_room, name: Dining Room}
zone: "{{ zone_map[trigger.to_state.object_id] }}"
clim: "{{ zone.climate }}"
slug: "{{ clim.split('.')[1] }}"
latch: "{{ 'input_boolean.' ~ slug ~ '_ac_condensate_shutoff' }}"
ts: "{{ 'input_datetime.' ~ slug ~ '_ac_condensate_last_restore' }}"
A. Shutoff — condensate_leak_hvac_shutoff (mode: queued)¶
- Trigger: state of
[attic_air_handler_leak_detector, 3rd_floor_furnace_leak_detector, crawl_space_furnace_leak_detector]→on. - Action (always):
climate.set_hvac_modetarget{{ clim }}→off(idempotent). - Then only if
{{ latch }}is off (if):input_boolean.turn_on{{ latch }}+ push both phones (message names{{ zone.name }}):🌡️ {{name}} AC turned OFF — condensate leak at its air handler. It'll auto-restore once the pan is dry (≥2h between restarts). Worth checking the drain line.
mode: queuedserializes fast shutoff actions and avoids a double-notify race on sensor flap.
B. Auto-restore — condensate_leak_hvac_restore (mode: parallel)¶
- Trigger: the 3 sensors →
offfor00:10:00. - Condition:
{{ is_state(latch, 'on') }}(we turned this zone off). - Action:
wait_templateuntil ≥2h since this zone's last restore:{{ now().timestamp() - state_attr(ts,'timestamp') | float(0) >= 7200 }}(resolves instantly when the last restore was >2h ago / never;float(0)= "long ago"). Notimeout— time only increases, so it always resolves.- Re-check after the wait:
{{ is_state(trigger.to_state.entity_id, 'off') and is_state(latch, 'on') }}— aborts if the pan re-flooded or an owner-override cleared the latch during the cooldown. input_boolean.turn_off{{ latch }}first, then push restore notice, stampinput_datetime.set_datetime{{ ts }}={{ now().timestamp() }}, then lastclimate.set_hvac_mode{{ clim }}→heat_cool.- Push both phones: "✅ {{name}} AC restored — its condensate pan is dry again."
- Clearing the latch and stamping before setting
heat_coolmeans automation C sees the latch already off and no-ops on our own restore.mode: parallellets different zones restore concurrently (aqueuedmode would block one zone behind another's ≤2h wait); duplicate restore instances for the same zone self-cancel via the step-2 latch re-check.
C. Owner-override clear — condensate_leak_hvac_override (mode: queued)¶
- Trigger: state of
[climate.office, climate.bedroom, climate.dining_room]changes. - Variables:
slug/latchderived from{{ trigger.to_state.object_id }}(the thermostat). - Condition: new state not in
['off','unavailable','unknown']AND{{ is_state(latch,'on') }}. - Action:
input_boolean.turn_off{{ latch }}+ push both phones:{{name}} AC turned back on manually — condensate auto-restore cancelled. (If the pan's still wet, keep an eye on it.)
- Because B clears the latch before setting
heat_cool, B's own restore does not satisfy the latch condition here — only a genuine manual turn-on does. A pending B instance is neutralized by its step-2 latch re-check.
Notifications¶
Both phones (notify.mobile_app_louis_iphone + notify.mobile_app_lindsays_iphone) — matching the household leak convention — normal actionable push (alarm-level critical interruption is already carried by the §1 generic leak alert, which still fires since these sensors stay in group.leak_sensors). No notify group (they're broken in 2026.6 — memory notify-group-broken-2026-6); call both services. Message text names {{ zone.name }}.
Known limitation¶
If HA restarts during the 2h cooldown wait, that zone's pending restore is lost — and since its pan is already dry, no new state-transition trigger fires, so the zone stays off until its pan re-floods or the owner turns it back on. Accepted edge (an HA restart landing inside a 2h window on a leak day is rare; owner-override + manual turn-on both cover it). Not worth a startup-rehydration automation.
HVAC mode note¶
All three Ecobees run heat_cool year-round (per climate docs), so restore sets heat_cool explicitly rather than storing/restoring each zone's prior mode. (If a zone ever runs a different mode, restore would need to capture the pre-shutoff mode per zone.)
Testing¶
- Entity-ref (Tier 3): the 3 sensors + 3 thermostats are literal in the YAML and already in the registry → pass. The 6 helpers are referenced only via templates (derived from slug), so the literal-scan entity-ref test does not flag them; they are defined in
configuration.yaml, so Tier-1check_configvalidates their existence. Runmake snapshotpost-deploy to fold them intoentities.txt. - New structural test (Tier 2 — added because templating hides the helper refs):
tests/test_condensate_safety.pyasserts (a) all 6 helpers are defined inconfiguration.yaml, and (b) thezone_mapin both shutoff+restore covers exactly the 3 sensors mapped to the 3 thermostats. This replaces the coverage the entity-ref test can't give through templates. - Unique
id/aliason the 3 automations (suite enforces uniqueness). make testgreen (lint + pytest;check_configTier 1 in CI).- Manual post-deploy check (per zone): trip the sensor → thermostat goes
off+ push names the right room; dry ≥10 min → restore + timestamp stamped; manual turn-on clears the latch. Verify the right zone's thermostat responds (mapping correctness).
Docs¶
- CLAUDE.md — add the "Condensate Safety" automations + record the 2nd sanctioned HVAC-write exception (Climate System section).
projects/climate_system.md— document the exception, the zone map, and the 3 automations.docs/notifications.md— the 3 new pushes (shutoff / restore / owner-override) per zone.docs/automations.md— technical reference for the new subsection.
Out of scope¶
- Fixing the physical drain lines (the real root cause — #71 body / #69).
- Any change to the generic §1 leak automations (they keep covering these sensors for the critical water alert).
- A "turn back on" notification action button — unnecessary given auto-restore.
- Per-zone different cooldowns / modes — all three share the 2h cooldown and
heat_coolrestore.
Acceptance criteria¶
- [ ] Leak on any of the 3 air-handler sensors → the mapped thermostat turns
offwithin seconds + a specific push (naming the right room) to both phones. - [ ] Pan dry ≥10 min AND ≥2h since that zone's last restore → thermostat restored to
heat_cool+ per-zone timestamp stamped + push. - [ ] Re-flood during cooldown, or a manual turn-on, correctly aborts/cancels that zone's pending restore; no duplicate shutoff push on a re-flood while already held off.
- [ ] Zones are independent (one zone's leak/restore never affects another).
- [ ] 6 helpers added (no
initial:); 3 automations with unique id/alias;zone_mapcovers all 3. - [ ] Documented as the 2nd sanctioned HVAC-write exception (CLAUDE.md + climate_system.md).
- [ ]
docs/notifications.md+docs/automations.mdupdated. - [ ]
make testpasses (incl. the new structural test).