Smart Home Fix

Detect a bathroom extraction fault from recovery time

High bathroom humidity during a shower is expected. The useful question is whether it falls at the normal rate after the shower while the exhaust fan is genuinely drawing power, compared with the rest of the house.

The fault this project detects

This project detects slow post-shower humidity recovery, fan command without measured power, and a bathroom-to-hallway humidity difference that remains abnormal. It avoids treating the ordinary shower peak itself as the fault.

The example assumes a bathroom humidity probe, a hallway reference probe, exhaust-fan power or current evidence, and an occupancy or shower-ended signal. Relative humidity also changes with temperature, so compare repeatable conditions or add absolute-humidity analysis if needed.

Bathroom humidity recovery evidence flow Multiple measurements are combined into advisory, warning and critical fault states. Humidity fall rateHallway comparisonFan powerPost-shower duration Evidence model direction + context agreement + duration Advisory dashboard only Warning one push Critical repeat + audible
The alarm waits for the recovery phase, then checks rate, room difference and proof that the fan is doing electrical work.

The evidence model

A useful alarm does not promote one noisy reading straight to an emergency. It combines measurements that fail in different ways:

  1. Value: relative humidity shows the remaining moisture condition.
  2. Direction: a negative derivative proves the room is drying rather than staying flat or rising.
  3. Agreement: the bathroom-minus-hallway difference separates whole-house humid weather from local retention.
  4. Context and time: fan power and a post-shower window establish when extraction should produce recovery, with persistence for steam and sensor lag.

A shower peak stays on the dashboard as an Advisory. Warning waits until the recovery period and requires the drying curve to be too slow or the powered fan to disagree with the room response.

Freshness is separate evidence

An entity can keep a plausible old value without becoming unavailable. Inspect last_changed on a source that should genuinely change, and pull up its own History graph over a normal stretch of days to find the longest healthy interval between meaningful state changes — that gap is the figure to record. If the measured process can legitimately stay flat, add a changing source heartbeat or timestamp; do not treat an unchanged temperature, level or power value as proof that the transport is alive.

Rate maths: never copy a gradient

The Trend integration expresses min_gradient in units per second. The arithmetic example from the project brief is 4 °C/hour ÷ 3600 = 0.00111 °C/s. That is an example conversion, not a threshold for this equipment. Calculate the rate from your own timestamped readings: take two readings from that entity's History, then work out your measured change (in the sensor's own unit) ÷ the elapsed seconds between those readings = your site-specific gradient. A Derivative helper can instead display a friendlier per-hour unit when configured that way.

Collect this data before choosing a threshold

Record several ordinary showers across different weather, door and window conditions. Use the same sensor positions and note when the shower ends and fan starts or stops.

MeasurementWhy it mattersRequired value
sensor.bathroom_humidityLocal moisture curveCheck its History after a normal shower and note the peak percentage it reached and how long recovery took
sensor.hallway_humidityWhole-house referenceRead the hallway sensor at that same peak moment so you have a like-for-like baseline
sensor.bathroom_humidity_rateRecovery speedWatch a few normal recoveries and note the %/h rate that looks like healthy drying versus a stalled one
sensor.bathroom_exhaust_powerProof of electrical operationCheck its History with the fan off and running to find the wattage ranges for each state
binary_sensor.bathroom_recently_usedRecovery contextWork out, from your own humidity or door-sensor history, what start and end condition reliably marks a shower as finished

The experiment that makes this article defensible

  1. Capture at least five normal shower and recovery curves without changing sensor placement.
  2. Record bathroom and hallway humidity, temperature, fan power and occupancy on the same timeline.
  3. Measure time from shower end to the normal recovered bathroom-to-hallway difference.
  4. Use a naturally occurring blocked-grille or fan fault if available; do not obstruct a live fan or create unsafe moisture deliberately.

Before you rely on this automation, capture your own evidence from that walkthrough: timestamped readings for each step, the Home Assistant version you're running, the exact entity IDs involved, and one or two History or dashboard screenshots. Do not stage an unsafe electrical, refrigeration, pressure, battery or water fault merely to make a graph.

Create the helpers

Use Settings → Devices & services → Helpers. Create the named Number or Toggle helpers before loading the templates. Where this guide uses a Derivative, Statistics, History Stats, Integral, Schedule or Utility Meter helper, the current Home Assistant interface can create it; the source links below describe the current options.

Helper or derived entityPurposeSetting
sensor.bathroom_humidity_rateDerivative helper in percentage points per hourPick a time window that matches how quickly humidity normally falls in your bathroom, then note the recovery rate you'd expect over that window
input_number.bathroom_fan_running_wSeparates running fan from standbyThe wattage that reliably separates "fan running" from "fan off/standby" for your exhaust fan, read off its History
input_number.bathroom_recovery_delta_percentAcceptable bathroom-minus-hallway differenceThe percentage-point gap above the hallway reading you'd consider acceptable once the bathroom's dried out, set from your own comparison of the two sensors
input_number.bathroom_min_drying_percent_per_hourRequired drying-rate magnitudeThe minimum %/h drying rate you'd expect if the fan is actually working, taken from the healthy recoveries you observed earlier
input_number.humidity_recovery_hold_minutesPersistence before WarningHow many minutes of sustained poor recovery you want before a Warning fires, long enough to ignore a brief blip
input_number.bathroom_critical_humidity_percentSite-specific moisture limitThe percentage above which sustained humidity in your bathroom is a real problem rather than an ordinary shower, set from your own peak readings
input_boolean.humidity_recovery_monitoring_enabledMaster notification gateLeave this off until you've tested the automation end to end, then switch it on
input_number.humidity_recovery_critical_repeat_minutesCritical repeat spacingHow many minutes between repeat critical alerts — enough to avoid spamming yourself, short enough not to miss a genuine emergency

Current Home Assistant YAML

Merge top-level keys with your existing configuration instead of duplicating them. Replace the example entity IDs consistently. If you keep automations in automations.yaml, paste only the automation list items there, without a top-level automation: wrapper.

Judge the drying phase, not the shower peak

Create the Derivative helper from the bathroom humidity sensor. The threshold helper stores a positive magnitude; the template compares the actual derivative with its negative equivalent because recovery should fall.

template:
  - sensor:
      - name: "Bathroom hallway humidity difference"
        unique_id: bathroom_hallway_humidity_difference
        default_entity_id: sensor.bathroom_hallway_humidity_difference
        state_class: measurement
        unit_of_measurement: "%"
        availability: >
          {{ has_value('sensor.bathroom_humidity')
             and has_value('sensor.hallway_humidity') }}
        state: >
          {{ ((states('sensor.bathroom_humidity') | float)
              - (states('sensor.hallway_humidity') | float)) | round(1) }}

  - binary_sensor:
      - name: "Humidity recovery warning"
        unique_id: humidity_recovery_warning
        default_entity_id: binary_sensor.humidity_recovery_warning
        device_class: problem
        delay_on:
          minutes: >
            {{ states('input_number.humidity_recovery_hold_minutes') | int(0) }}
        availability: >
          {{ has_value('sensor.bathroom_humidity_rate')
             and has_value('sensor.bathroom_hallway_humidity_difference')
             and has_value('sensor.bathroom_exhaust_power') }}
        state: >
          {% set fan_on = (states('sensor.bathroom_exhaust_power') | float)
             >= (states('input_number.bathroom_fan_running_w') | float) %}
          {% set delta_high = (states('sensor.bathroom_hallway_humidity_difference') | float)
             >= (states('input_number.bathroom_recovery_delta_percent') | float) %}
          {% set too_slow = (states('sensor.bathroom_humidity_rate') | float)
             > -(states('input_number.bathroom_min_drying_percent_per_hour') | float) %}
          {{ is_state('binary_sensor.bathroom_recently_used', 'on')
             and fan_on and delta_high and too_slow }}
        attributes:
          reason: >-
            Bathroom-hallway difference is
            {{ states('sensor.bathroom_hallway_humidity_difference') }} percentage points,
            recovery rate is {{ states('sensor.bathroom_humidity_rate') }} %/h and
            fan power is {{ states('sensor.bathroom_exhaust_power') }} W.

      - name: "Humidity recovery critical"
        unique_id: humidity_recovery_critical
        default_entity_id: binary_sensor.humidity_recovery_critical
        device_class: problem
        state: >
          {{ is_state('binary_sensor.humidity_recovery_warning', 'on')
             and (states('sensor.bathroom_humidity') | float(-999999))
                 >= (states('input_number.bathroom_critical_humidity_percent') | float(999999)) }}

Warning and Critical notification actions

Replace the mobile notification action and media-player entity with your own. Put critical-monitoring-alert.mp3 in /media, test it manually, and leave the monitoring Toggle off until calibration is complete.

automation:
  - id: humidity_recovery_warning_push
    alias: "Humidity recovery - Warning push"
    mode: single
    triggers:
      - trigger: state
        entity_id: binary_sensor.humidity_recovery_warning
        to: "on"
    conditions:
      - condition: state
        entity_id: input_boolean.humidity_recovery_monitoring_enabled
        state: "on"
    actions:
      - action: notify.mobile_app_your_phone
        data:
          title: "Humidity recovery warning"
          message: >-
            {{ state_attr('binary_sensor.humidity_recovery_warning', 'reason')
                or 'The warning condition is active. Check Home Assistant for evidence.' }}

  - id: humidity_recovery_critical_repeat
    alias: "Humidity recovery - Critical repeat"
    mode: restart
    triggers:
      - trigger: state
        entity_id: binary_sensor.humidity_recovery_critical
        to: "on"
    conditions:
      - condition: state
        entity_id: input_boolean.humidity_recovery_monitoring_enabled
        state: "on"
    actions:
      - action: notify.mobile_app_your_phone
        data:
          title: "Humidity recovery critical"
          message: "The critical condition is active. Check the equipment and evidence now."
      - action: media_player.play_media
        target:
          entity_id: media_player.alert_speaker
        data:
          media_content_id: "media-source://media_source/local/critical-monitoring-alert.mp3"
          media_content_type: "audio/mpeg"
      - repeat:
          while:
            - condition: state
              entity_id: binary_sensor.humidity_recovery_critical
              state: "on"
            - condition: template
              value_template: >-
                {{ states('input_number.humidity_recovery_critical_repeat_minutes') | int(0) > 0 }}
          sequence:
            - delay:
                minutes: >-
                  {{ states('input_number.humidity_recovery_critical_repeat_minutes') | int(0) }}
            - condition: state
              entity_id: binary_sensor.humidity_recovery_critical
              state: "on"
            - action: notify.mobile_app_your_phone
              data:
                title: "Humidity recovery still critical"
                message: "The critical condition remains active."
            - action: media_player.play_media
              target:
                entity_id: media_player.alert_speaker
              data:
                media_content_id: "media-source://media_source/local/critical-monitoring-alert.mp3"
                media_content_type: "audio/mpeg"

Why the logic works

The reference probe compensates for humid weather, fan power confirms input, and the derivative measures the delivered drying response. Starting the logic only in the recovery phase avoids nuisance alarms from the expected shower peak.

The design is intentionally diagnostic rather than controlling. It reports an impossible or abnormal relationship; it does not bypass equipment protection or decide that a single suspected cause is proven.

Severity tiers without alert fatigue

TierMeaningAction
AdvisoryThe recovery curve or bathroom-to-hallway difference is outside its recent healthy pattern.Dashboard only
WarningAfter use, measured fan power persists while local humidity remains high and falls too slowly.One push notification
CriticalThe corroborated recovery fault persists and the bathroom reaches the calibrated moisture limit.Push, repeat and audible alert

A weak clue remains visible without interrupting you. A Warning requires persistence or corroboration. Critical is reserved for direct danger or several independent signals. That separation is what stops a useful monitoring system becoming notification wallpaper.

Troubleshooting

Rainy days trigger the alarm
Use the hallway difference and include temperature or absolute-humidity context if relative humidity still misleads.
Opening the door changes recovery
Record door state or calibrate separate profiles for the real positions people use.
Fan power is on but airflow is poor
That contradiction is useful evidence of a blocked grille, duct or failed impeller, but it does not prove which one.
The derivative is noisy
Use stable probe placement and a longer Derivative time window based on measured shower curves.
Occupancy ends too early
Build a reliable post-shower helper or trigger from humidity rise, then verify it against History before enabling alerts.

Safety and limits

Do not place mains-powered sensors in wet zones or work on a fixed exhaust fan without the required electrical licence. Persistent moisture may need a building, ventilation or waterproofing professional rather than another automation.

If a webhook is added later, treat its webhook_id like a password, keep local_only: true unless a reviewed design requires otherwise, and never use an unauthenticated webhook for locks, garage doors, power isolation or anything destructive.

Official sources

These are the primary documentation pages used for the design. Check them against the Home Assistant or ESPHome version you are actually running.

Related Smart Home Fix guides

FAQ

Why not alert as soon as bathroom humidity is high?

High humidity during a shower is expected. Recovery rate and room comparison identify whether moisture clears normally afterwards.

Why compare with the hallway?

It helps distinguish local bathroom retention from a humid day affecting the whole house.

Does fan power prove extraction airflow?

No. It proves electrical input. Poor drying despite fan power is the useful contradiction that justifies inspecting airflow and ducting.

Should I use relative or absolute humidity?

Relative humidity can be sufficient when temperature and conditions are repeatable. Where temperature changes strongly, add reviewed absolute-humidity analysis.

Can this diagnose mould?

No. It identifies slow moisture recovery. Existing mould, leaks, waterproofing defects and building issues need appropriate inspection and remediation.