Skip to content

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.officeoffice), 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_mode target {{ 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: queued serializes 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 → off for 00:10:00.
  • Condition: {{ is_state(latch, 'on') }} (we turned this zone off).
  • Action:
  • wait_template until ≥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"). No timeout — 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, stamp input_datetime.set_datetime {{ ts }} = {{ now().timestamp() }}, then last climate.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_cool means automation C sees the latch already off and no-ops on our own restore. mode: parallel lets different zones restore concurrently (a queued mode 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/latch derived 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-1 check_config validates their existence. Run make snapshot post-deploy to fold them into entities.txt.
  • New structural test (Tier 2 — added because templating hides the helper refs): tests/test_condensate_safety.py asserts (a) all 6 helpers are defined in configuration.yaml, and (b) the zone_map in 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/alias on the 3 automations (suite enforces uniqueness).
  • make test green (lint + pytest; check_config Tier 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_cool restore.

Acceptance criteria

  • [ ] Leak on any of the 3 air-handler sensors → the mapped thermostat turns off within 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_map covers all 3.
  • [ ] Documented as the 2nd sanctioned HVAC-write exception (CLAUDE.md + climate_system.md).
  • [ ] docs/notifications.md + docs/automations.md updated.
  • [ ] make test passes (incl. the new structural test).