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.
The evidence model
A useful alarm does not promote one noisy reading straight to an emergency. It combines measurements that fail in different ways:
- Value: relative humidity shows the remaining moisture condition.
- Direction: a negative derivative proves the room is drying rather than staying flat or rising.
- Agreement: the bathroom-minus-hallway difference separates whole-house humid weather from local retention.
- 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.
| Measurement | Why it matters | Required value |
|---|---|---|
sensor.bathroom_humidity | Local moisture curve | Check its History after a normal shower and note the peak percentage it reached and how long recovery took |
sensor.hallway_humidity | Whole-house reference | Read the hallway sensor at that same peak moment so you have a like-for-like baseline |
sensor.bathroom_humidity_rate | Recovery speed | Watch a few normal recoveries and note the %/h rate that looks like healthy drying versus a stalled one |
sensor.bathroom_exhaust_power | Proof of electrical operation | Check its History with the fan off and running to find the wattage ranges for each state |
binary_sensor.bathroom_recently_used | Recovery context | Work 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
- Capture at least five normal shower and recovery curves without changing sensor placement.
- Record bathroom and hallway humidity, temperature, fan power and occupancy on the same timeline.
- Measure time from shower end to the normal recovered bathroom-to-hallway difference.
- 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 entity | Purpose | Setting |
|---|---|---|
sensor.bathroom_humidity_rate | Derivative helper in percentage points per hour | Pick 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_w | Separates running fan from standby | The wattage that reliably separates "fan running" from "fan off/standby" for your exhaust fan, read off its History |
input_number.bathroom_recovery_delta_percent | Acceptable bathroom-minus-hallway difference | The 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_hour | Required drying-rate magnitude | The 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_minutes | Persistence before Warning | How many minutes of sustained poor recovery you want before a Warning fires, long enough to ignore a brief blip |
input_number.bathroom_critical_humidity_percent | Site-specific moisture limit | The 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_enabled | Master notification gate | Leave this off until you've tested the automation end to end, then switch it on |
input_number.humidity_recovery_critical_repeat_minutes | Critical repeat spacing | How 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
| Tier | Meaning | Action |
|---|---|---|
| Advisory | The recovery curve or bathroom-to-hallway difference is outside its recent healthy pattern. | Dashboard only |
| Warning | After use, measured fan power persists while local humidity remains high and falls too slowly. | One push notification |
| Critical | The 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
- Fix unreliable entities before trusting alarms
- Separate smart-home devices without breaking discovery
- Reduce false alarms without hiding real faults
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.