Detect a failing freezer before it gets warm
A normal temperature alarm waits for your food to get warm. This Home Assistant freezer temperature alarm looks for the earlier change in behaviour: a rising rate, a departure from the freezer's own recent pattern, two probes disagreeing, a compressor that should be running but is not, or a sensor that has silently stopped changing.
The scenario and gear
This example is for a chest freezer in a detached NSW shed. It uses a primary internal temperature probe, an independently placed reference probe, a shed ambient-temperature sensor, an energy monitor that reports freezer power, and a lid contact sensor. The entity names used throughout are:
Swap these example entity IDs for the ones your own devices actually created, and keep the swap consistent through every block below. The make and model do not change the logic; what matters is that each entity reports reliably and sits where it can genuinely sense what it claims to. No brand or certification claim is implied by the generic equipment description.
sensor.shed_freezer_temperature- primary probe inside the freezer.sensor.shed_freezer_reference_temperature- independent internal reference probe.sensor.shed_ambient_temperature- the shed temperature, useful during a heatwave.sensor.shed_freezer_power- power measurement only; do not automate the freezer off.binary_sensor.shed_freezer_lid- contact sensor, withonmeaning open.
Use hardware suitable for the freezer's compressor load and Australian supply. A plug-in energy monitor should be RCM-compliant, correctly rated and configured so a software command cannot casually switch the freezer off. A switchboard current clamp or any fixed wiring is licensed-electrical work. In a shed, 2.4 GHz Wi-Fi usually reaches farther than 5 GHz, but metal walls and the freezer cabinet can still block it; test the link with the lid closed. A local Zigbee mesh or wired probe can be more dependable than a marginal Wi-Fi path back through the NBN modem.
If your entities drop out, fix that before building alarms. Start with our smart-home troubleshooting guide, then check whether a separate network for smart-home devices would improve reliability. Keep a recoverable copy of the working configuration off the Home Assistant host too.
What changes before the freezer is warm?
A conventional rule asks one question: is the temperature above a limit? An early-fault rule asks several smaller questions and waits for them to agree.
- How fast is it changing?
- A rate can expose a cooling failure while the absolute temperature still looks acceptable.
- Is it moving the wrong way?
- After a normal compressor cycle starts, the cabinet should not keep warming.
- Is it different from recent behaviour?
- A recent mean gives the freezer its own local baseline instead of assuming every installation behaves alike.
- How long has it lasted?
- A
for:duration or templatedelay_onprevents a lid opening or a single noisy packet becoming an alarm. - Does a second sensor agree?
- Two independent probes moving together make a real thermal event more likely. A large difference points to placement, radio or sensor trouble.
- Should it be running?
- Power below the measured compressor-running level while cooling is expected can expose a tripped supply, failed thermostat or stalled compressor.
- Has the sensor gone stale?
- An entity can remain on an old numeric value without becoming
unavailable. Checklast_changedas a separate fault signal.
Build a baseline before setting thresholds
Record enough normal operation to include closed-lid cycles, a normal lid opening, a warm day in the shed and any legitimate defrost behaviour. Then record or safely simulate the failure modes you actually care about, without risking food or bypassing electrical protection.
Before you set any threshold, read each of these from your own history: the primary and reference temperature traces, how often the sensor really reports, the longest gap between genuine reading changes, the normal warming and cooling rates, the usual difference between the two probes, the compressor's running and standby watts, the thermostat's cycle points, how an ordinary lid-opening looks, and the recent-mean window, warning duration, critical temperature and repeat interval you want to use. Each of these has a home in one of the helpers below.
The Sensor documentation explains how sensor entities expose measurements. For calculations, Home Assistant provides Derivative, Trend, Statistics and Template. Check the current Home Assistant docs before changing syntax on an older or newer installation.
The killer experiment: door-open event or real fault?
The useful comparison is not a made-up “normal” curve. It is your freezer, in your shed, measured through controlled events. Run this on an empty or non-critical freezer where the door tests will not put food at risk. Do not stage an electrical or compressor fault.
- Baseline: export 24 hours of steady-state history and calculate the observed minimum, maximum and average in °C.
- Door-open tests: open the lid for 30 seconds, 2 minutes and 5 minutes. For each test, retain a few timestamped readings from both probes, the lid state, power, Trend or Derivative result, and the time from closing the lid to recovery.
- Real compressor cycle: capture compressor on and off times, power, the temperature band it holds and the rate before, during and after the run.
- Calculated entities: record the actual Trend and Derivative values during the baseline, each door test and the compressor cycle.
| Test | Temperature curve | Calculated rate | Recovery or cycle |
|---|---|---|---|
| Steady state, 24 h | Record your min / max / avg °C | Record your Trend state and Derivative range | Record your normal compressor on/off pattern |
| Door open, 30 s | Record both probes, lid and power | Record the peak Trend/Derivative | Record the time to recover |
| Door open, 2 min | Record both probes, lid and power | Record the peak Trend/Derivative | Record the time to recover |
| Door open, 5 min | Record both probes, lid and power | Record the peak Trend/Derivative | Record the time to recover |
| Normal compressor cycle | Record on/off timestamps and held °C range | Record cooling and warming rates | Record on-time and off-time |
The hypothesis to verify is straightforward: a door-open event has a matching lid signal and should recover after closure; a cooling fault can happen with the lid closed, both probes moving the wrong way and either missing compressor power or power without the expected cooling response. Those are expected signatures, not claimed results. The completed table is what lets you show “a door-open looks like this; a real fault looks like that” without inventing a curve.
Note your Home Assistant version and keep one or two History or dashboard screenshots from these tests; side by side they make a door-opening and a genuine fault easy to tell apart later. Use your real entity IDs throughout, and if they differ from the examples here, change every reference consistently before you run Check configuration.
Worked gradient conversion
Trend's min_gradient is in source-sensor units per second, not per hour. Using the supplied maths example only:
4 °C/hour = 4 ÷ 3600 = 0.00111 °C/s
This does not say that 0.00111 is right for a freezer. Compute the rate for your gear from its own history, choose a sampling window that survives sensor rounding and short lid openings, and never blind-copy a min_gradient. If you configure Derivative with unit_time: h, its displayed rate is in °C/h; if you configure Trend directly, convert your chosen °C/h value to °C/s.
Create the helpers first
Go to Settings → Devices & services → Helpers. Create a Derivative helper from sensor.shed_freezer_temperature, name it Shed freezer temperature rate, use hours as the time unit, and choose a smoothing window from your recorded cycle data. Create a Statistics helper from the same source, name it Shed freezer recent mean, choose the mean characteristic, and set its age or sample limit from the same evidence. Trend, Derivative, Statistics and Template all have helper equivalents in current Home Assistant; YAML is still useful for the combined logic below.
Confirm that Home Assistant created sensor.shed_freezer_temperature_rate and sensor.shed_freezer_recent_mean. Entity IDs can differ, so rename them or update the YAML.
Create these Number helpers and one Toggle helper, then set each value from what you measured above:
| Helper entity | Purpose | Value |
|---|---|---|
input_number.freezer_warning_rate_c_per_hour | Sustained abnormal warming rate | Your measured abnormal warming rate (°C/h) |
input_number.freezer_mean_deviation_c | Warmer than recent mean | How far above the recent mean is unusual (°C) |
input_number.freezer_sensor_difference_c | Maximum normal probe difference | Your largest normal gap between the two probes (°C) |
input_number.freezer_stale_minutes | Longest acceptable unchanged state | Longer than your longest normal gap between changes (min) |
input_number.freezer_compressor_running_watts | Separates running from standby | A watt figure between your standby and running draw (W) |
input_number.freezer_cooling_expected_above_c | Point where cooling should be expected | The temperature above which the compressor should run (°C) |
input_number.freezer_critical_temperature_c | Conventional last-line threshold | Your conventional high-temperature limit (°C) |
input_number.freezer_warning_hold_minutes | Persistence before Warning | How long a pattern must hold before Warning (min) |
input_number.freezer_critical_hold_minutes | Persistence before Critical | How long a pattern must hold before Critical (min) |
input_number.freezer_critical_repeat_minutes | Critical repeat spacing | How often Critical should repeat while active (min) |
input_boolean.freezer_monitoring_enabled | Master notification gate | Leave off until tested |
Set sensible minimums and maximums in each Number helper based on your measurements. Do not enable notifications while a required helper is blank, zero or unverified.
Template the evidence, including a stale sensor
Add this under the top-level template: key in configuration.yaml. If that key already exists, merge the list items rather than adding a second top-level key. The first block makes numeric comparison sensors. The trigger-based block reevaluates freshness when the source changes and once each minute even when it does not.
template:
- sensor:
- name: "Shed freezer probe difference"
unique_id: shed_freezer_probe_difference
default_entity_id: sensor.shed_freezer_probe_difference
device_class: temperature
state_class: measurement
unit_of_measurement: "°C"
availability: >
{{ has_value('sensor.shed_freezer_temperature')
and has_value('sensor.shed_freezer_reference_temperature') }}
state: >
{% set primary = states('sensor.shed_freezer_temperature') | float %}
{% set reference = states('sensor.shed_freezer_reference_temperature') | float %}
{{ ((primary - reference) | abs) | round(2) }}
- name: "Shed freezer deviation from recent mean"
unique_id: shed_freezer_deviation_from_recent_mean
default_entity_id: sensor.shed_freezer_deviation_from_recent_mean
device_class: temperature
state_class: measurement
unit_of_measurement: "°C"
availability: >
{{ has_value('sensor.shed_freezer_temperature')
and has_value('sensor.shed_freezer_recent_mean') }}
state: >
{% set current = states('sensor.shed_freezer_temperature') | float %}
{% set mean = states('sensor.shed_freezer_recent_mean') | float %}
{{ (current - mean) | round(2) }}
- triggers:
- trigger: time_pattern
minutes: "*"
- trigger: state
entity_id: sensor.shed_freezer_temperature
binary_sensor:
- name: "Shed freezer sensor stale"
unique_id: shed_freezer_sensor_stale
default_entity_id: binary_sensor.shed_freezer_sensor_stale
device_class: problem
state: >
{% set source = states.sensor.shed_freezer_temperature %}
{% set limit = states('input_number.freezer_stale_minutes') %}
{{ source is not defined
or source.state in ['unknown', 'unavailable']
or (is_number(limit)
and (as_timestamp(now()) - as_timestamp(source.last_changed))
> ((limit | float) * 60)) }}
attributes:
minutes_since_change: >
{% set source = states.sensor.shed_freezer_temperature %}
{% if source is defined %}
{{ ((as_timestamp(now()) - as_timestamp(source.last_changed)) / 60) | round(1) }}
{% else %}
{{ none }}
{% endif %}
last_changed is intentionally different from checking only unavailable. It catches a plausible-looking value that has stopped changing. It can also flag a healthy sensor whose rounded temperature legitimately stays identical, so the stale duration must come from your recorded update behaviour. The trigger-based entity's timer does not mean the source sensor reported; it merely gives Home Assistant a chance to notice that it did not change.
Decide whether cooling should be happening
The next block turns power and temperature into context. It never switches the freezer. A monitoring system should not create the failure it is meant to detect.
template:
- binary_sensor:
- name: "Shed freezer warming wrong way"
unique_id: shed_freezer_warming_wrong_way
default_entity_id: binary_sensor.shed_freezer_warming_wrong_way
device_class: problem
availability: >
{{ has_value('sensor.shed_freezer_temperature_rate')
and has_value('input_number.freezer_warning_rate_c_per_hour') }}
state: >
{{ (states('sensor.shed_freezer_temperature_rate') | float)
>= (states('input_number.freezer_warning_rate_c_per_hour') | float) }}
- name: "Shed freezer unusual for recent behaviour"
unique_id: shed_freezer_unusual_for_recent_behaviour
default_entity_id: binary_sensor.shed_freezer_unusual_for_recent_behaviour
device_class: problem
availability: >
{{ has_value('sensor.shed_freezer_deviation_from_recent_mean')
and has_value('input_number.freezer_mean_deviation_c') }}
state: >
{{ (states('sensor.shed_freezer_deviation_from_recent_mean') | float)
>= (states('input_number.freezer_mean_deviation_c') | float) }}
- name: "Shed freezer probes disagree"
unique_id: shed_freezer_probes_disagree
default_entity_id: binary_sensor.shed_freezer_probes_disagree
device_class: problem
availability: >
{{ has_value('sensor.shed_freezer_probe_difference')
and has_value('input_number.freezer_sensor_difference_c') }}
state: >
{{ (states('sensor.shed_freezer_probe_difference') | float)
>= (states('input_number.freezer_sensor_difference_c') | float) }}
- name: "Shed freezer compressor running"
unique_id: shed_freezer_compressor_running
default_entity_id: binary_sensor.shed_freezer_compressor_running
device_class: running
availability: >
{{ has_value('sensor.shed_freezer_power')
and has_value('input_number.freezer_compressor_running_watts') }}
state: >
{{ (states('sensor.shed_freezer_power') | float)
>= (states('input_number.freezer_compressor_running_watts') | float) }}
- name: "Shed freezer cooling expected"
unique_id: shed_freezer_cooling_expected
default_entity_id: binary_sensor.shed_freezer_cooling_expected
availability: >
{{ has_value('sensor.shed_freezer_temperature')
and has_value('input_number.freezer_cooling_expected_above_c') }}
state: >
{{ (states('sensor.shed_freezer_temperature') | float)
>= (states('input_number.freezer_cooling_expected_above_c') | float) }}
- name: "Shed freezer expected cooling missing"
unique_id: shed_freezer_expected_cooling_missing
default_entity_id: binary_sensor.shed_freezer_expected_cooling_missing
device_class: problem
state: >
{{ is_state('binary_sensor.shed_freezer_cooling_expected', 'on')
and is_state('binary_sensor.shed_freezer_compressor_running', 'off')
and is_state('binary_sensor.shed_freezer_lid', 'off') }}
If your freezer has defrost cycles, variable-speed electronics or meaningful standby consumption, a simple watts rule may be misleading. Compare power with the temperature response over time. If the power sensor can control the outlet, disable that control in the device where possible and never expose it as a casual dashboard button.
Turn evidence into severity tiers
The tiers deliberately separate awareness from interruption:
| Tier | Evidence | Action |
|---|---|---|
| Advisory | Any single early clue | Dashboard only; collect context |
| Warning | A stale source, or multiple early clues sustained for the measured hold time | One push notification |
| Critical | The conventional temperature line, or a sustained high-confidence combination | Push, repeat and audible alert |
This prevents alert fatigue because a warm puff after opening the lid does not earn the same interruption as a rising cabinet, abnormal recent deviation and missing compressor run. You still see weak clues on the dashboard, so they are not discarded; they simply do not train you to ignore your phone.
Add this list item beneath an existing template: key:
template:
- binary_sensor:
- name: "Shed freezer advisory"
unique_id: shed_freezer_advisory
default_entity_id: binary_sensor.shed_freezer_advisory
device_class: problem
state: >
{{ is_state('binary_sensor.shed_freezer_warming_wrong_way', 'on')
or is_state('binary_sensor.shed_freezer_unusual_for_recent_behaviour', 'on')
or is_state('binary_sensor.shed_freezer_probes_disagree', 'on')
or is_state('binary_sensor.shed_freezer_expected_cooling_missing', 'on')
or is_state('binary_sensor.shed_freezer_sensor_stale', 'on') }}
- name: "Shed freezer warning"
unique_id: shed_freezer_warning
default_entity_id: binary_sensor.shed_freezer_warning
device_class: problem
delay_on:
minutes: >
{{ states('input_number.freezer_warning_hold_minutes') | int(0) }}
state: >
{% set score =
(is_state('binary_sensor.shed_freezer_warming_wrong_way', 'on') | int)
+ (is_state('binary_sensor.shed_freezer_unusual_for_recent_behaviour', 'on') | int)
+ (is_state('binary_sensor.shed_freezer_probes_disagree', 'on') | int)
+ (is_state('binary_sensor.shed_freezer_expected_cooling_missing', 'on') | int) %}
{{ is_state('binary_sensor.shed_freezer_sensor_stale', 'on')
or score >= 2 }}
attributes:
reason: >
{% set ns = namespace(items=[]) %}
{% if is_state('binary_sensor.shed_freezer_warming_wrong_way', 'on') %}
{% set ns.items = ns.items + ['warming rate'] %}
{% endif %}
{% if is_state('binary_sensor.shed_freezer_unusual_for_recent_behaviour', 'on') %}
{% set ns.items = ns.items + ['above recent behaviour'] %}
{% endif %}
{% if is_state('binary_sensor.shed_freezer_probes_disagree', 'on') %}
{% set ns.items = ns.items + ['probes disagree'] %}
{% endif %}
{% if is_state('binary_sensor.shed_freezer_expected_cooling_missing', 'on') %}
{% set ns.items = ns.items + ['expected cooling missing'] %}
{% endif %}
{% if is_state('binary_sensor.shed_freezer_sensor_stale', 'on') %}
{% set ns.items = ns.items + ['sensor stale'] %}
{% endif %}
{{ ns.items | join(', ') }}
- name: "Shed freezer critical"
unique_id: shed_freezer_critical
default_entity_id: binary_sensor.shed_freezer_critical
device_class: problem
delay_on:
minutes: >
{{ states('input_number.freezer_critical_hold_minutes') | int(0) }}
availability: >
{{ has_value('input_number.freezer_critical_temperature_c')
and (has_value('sensor.shed_freezer_temperature')
or has_value('sensor.shed_freezer_reference_temperature')) }}
state: >
{% set primary = states('sensor.shed_freezer_temperature') %}
{% set reference = states('sensor.shed_freezer_reference_temperature') %}
{% set limit = states('input_number.freezer_critical_temperature_c') | float(0) %}
{% set over_limit =
(is_number(primary) and (primary | float) >= limit)
or (is_number(reference) and (reference | float) >= limit) %}
{% set confirmed_thermal_fault =
is_state('binary_sensor.shed_freezer_warming_wrong_way', 'on')
and is_state('binary_sensor.shed_freezer_unusual_for_recent_behaviour', 'on')
and (is_state('binary_sensor.shed_freezer_probes_disagree', 'on')
or is_state('binary_sensor.shed_freezer_expected_cooling_missing', 'on')) %}
{{ over_limit or confirmed_thermal_fault }}
The Warning and Critical delay_on values provide the sustained-duration check. These timers reset on a Home Assistant restart or template reload, so do not treat them as certified safety logging. If the alert must survive restarts with an exact deadline, use a persisted helper design and check the current Home Assistant docs.
Send Warning once and repeat Critical
Replace notify.mobile_app_your_phone with the notify action created by your Companion app. Replace media_player.alert_speaker with a suitable local speaker and place an audio file at /media/freezer-critical.mp3. Test both actions manually before relying on them.
The example is shown beneath a top-level automation: key. If your installation uses the usual automation: !include automations.yaml, paste only the two list items into automations.yaml, remove the wrapper line and reduce their indentation by two spaces.
automation:
- id: shed_freezer_warning_push
alias: "Shed freezer - Warning push"
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.shed_freezer_warning
to: "on"
conditions:
- condition: state
entity_id: input_boolean.freezer_monitoring_enabled
state: "on"
actions:
- action: notify.mobile_app_your_phone
data:
title: "Shed freezer warning"
message: >-
Early fault pattern: {{ state_attr('binary_sensor.shed_freezer_warning', 'reason') }}.
Primary {{ states('sensor.shed_freezer_temperature') }} °C;
rate {{ states('sensor.shed_freezer_temperature_rate') }} °C/h.
- id: shed_freezer_critical_repeat
alias: "Shed freezer - Critical repeat and audible"
mode: restart
triggers:
- trigger: state
entity_id: binary_sensor.shed_freezer_critical
to: "on"
conditions:
- condition: state
entity_id: input_boolean.freezer_monitoring_enabled
state: "on"
actions:
- action: notify.mobile_app_your_phone
data:
title: "Shed freezer critical"
message: >-
Check the freezer now. Primary temperature is
{{ states('sensor.shed_freezer_temperature') }} °C and reference is
{{ states('sensor.shed_freezer_reference_temperature') }} °C.
- action: media_player.play_media
target:
entity_id: media_player.alert_speaker
data:
media_content_id: "media-source://media_source/local/freezer-critical.mp3"
media_content_type: "audio/mpeg"
- repeat:
while:
- condition: state
entity_id: binary_sensor.shed_freezer_critical
state: "on"
- condition: template
value_template: >-
{{ states('input_number.freezer_critical_repeat_minutes') | int(0) > 0 }}
sequence:
- delay:
minutes: >-
{{ states('input_number.freezer_critical_repeat_minutes') | int(0) }}
- condition: state
entity_id: binary_sensor.shed_freezer_critical
state: "on"
- action: notify.mobile_app_your_phone
data:
title: "Shed freezer still critical"
message: >-
The critical condition remains active. Primary temperature is
{{ states('sensor.shed_freezer_temperature') }} °C and reference is
{{ states('sensor.shed_freezer_reference_temperature') }} °C.
- action: media_player.play_media
target:
entity_id: media_player.alert_speaker
data:
media_content_id: "media-source://media_source/local/freezer-critical.mp3"
media_content_type: "audio/mpeg"
Keep the Advisory entity on the dashboard without a notification automation. For more ways to stop nuisance alerts without hiding real faults, read how to fix smart-sensor false alarms.
Commission it without trusting it too early
- Leave
input_boolean.freezer_monitoring_enabledoff. - Check every source entity for correct units, realistic values and regular history.
- Confirm the reference probe is independent and not merely mirroring the primary reading.
- Use Developer Tools to inspect every derived entity and its availability.
- Open the lid briefly and confirm that a single expected disturbance remains Advisory.
- Safely simulate missing data by taking only the sensor offline; confirm the stale logic after your measured delay.
- Test the Warning push and Critical audible action manually.
- Review the dashboard through normal cycling and a hot shed period, adjust from evidence, then enable notifications.
Do not unplug a loaded freezer, defeat its thermostat, bypass a breaker or deliberately warm food to test an automation. Home Assistant is an early-warning layer, not a calibrated food-safety instrument. If the history is incomplete or food may already be unsafe, verify it independently and follow the appliance and local food-safety guidance.
Webhook safety
This build does not need a webhook. If you later add one, treat its webhook_id like a password, keep local_only: true unless there is a carefully reviewed reason not to, and never use an unauthenticated webhook for a lock, garage door, power cut-off or anything destructive. A leaked webhook URL is enough to trigger an unauthenticated webhook.
FAQ
Can Home Assistant detect a freezer fault before the temperature limit is crossed?
Yes, if the failure changes the freezer's behaviour first. A sustained warming rate, an unusual departure from its recent mean, disagreement between probes, or a missing compressor run can become visible before the final high-temperature threshold is crossed. No rule can guarantee advance notice for every failure, so keep a conventional high-temperature alarm as the last line of defence.
Should I use Trend or Derivative for a freezer?
Use Derivative when you want a numeric rate you can graph and compare with a helper. Use Trend when you want a simple binary result for a minimum gradient. Both depend on useful source updates and a sensible observation window; neither repairs a slow, stale, or badly placed sensor.
What should I use for min_gradient?
Calculate it from your own freezer history. Trend uses sensor units per second, so the supplied worked example of 4 °C/hour becomes 4 ÷ 3600 = 0.00111 °C/s. That is a unit conversion, not a recommended freezer setting. Never blind-copy it; measure normal door openings, defrost behaviour and genuine warming on your gear first.
Why use two temperature sensors?
A second independent probe helps distinguish a real cabinet change from a flat battery, radio dropout, bad placement or a failed probe. Agreement increases confidence; disagreement is itself a useful fault signal. Do not mount both probes together or power them from the same failure point if independence matters.
How does the stale-sensor check work?
It checks the source entity's last_changed time as well as unknown and unavailable states. If the reading has not changed for longer than your measured normal unchanged period, the stale flag turns on. Because a stable freezer can legitimately report the same rounded value for a while, calibrate this delay from history rather than guessing.
Will this create too many notifications?
It should not if the tiers are tuned properly. A single weak clue stays as an Advisory on the dashboard, a sustained or corroborated pattern becomes a Warning push, and only a high-confidence or high-temperature condition becomes a repeating Critical alert with sound. Review early events before enabling loud alerts.