Smart Home Fix

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.

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.

Freezer early-fault detection flow Temperature, rate, recent mean, second probe, power and freshness checks feed advisory, warning and critical severity tiers. Temperature + rate Recent behaviour Reference probe Power + expected run Freshness + availability Combine evidence direction + duration agreement + context Advisory dashboard only Warning push once Critical repeat + audible
One weak clue is visible; sustained or corroborated evidence earns a notification.
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 template delay_on prevents 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. Check last_changed as 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.

  1. Baseline: export 24 hours of steady-state history and calculate the observed minimum, maximum and average in °C.
  2. 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.
  3. Real compressor cycle: capture compressor on and off times, power, the temperature band it holds and the rate before, during and after the run.
  4. Calculated entities: record the actual Trend and Derivative values during the baseline, each door test and the compressor cycle.
TestTemperature curveCalculated rateRecovery or cycle
Steady state, 24 hRecord your min / max / avg °CRecord your Trend state and Derivative rangeRecord your normal compressor on/off pattern
Door open, 30 sRecord both probes, lid and powerRecord the peak Trend/DerivativeRecord the time to recover
Door open, 2 minRecord both probes, lid and powerRecord the peak Trend/DerivativeRecord the time to recover
Door open, 5 minRecord both probes, lid and powerRecord the peak Trend/DerivativeRecord the time to recover
Normal compressor cycleRecord on/off timestamps and held °C rangeRecord cooling and warming ratesRecord 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 entityPurposeValue
input_number.freezer_warning_rate_c_per_hourSustained abnormal warming rateYour measured abnormal warming rate (°C/h)
input_number.freezer_mean_deviation_cWarmer than recent meanHow far above the recent mean is unusual (°C)
input_number.freezer_sensor_difference_cMaximum normal probe differenceYour largest normal gap between the two probes (°C)
input_number.freezer_stale_minutesLongest acceptable unchanged stateLonger than your longest normal gap between changes (min)
input_number.freezer_compressor_running_wattsSeparates running from standbyA watt figure between your standby and running draw (W)
input_number.freezer_cooling_expected_above_cPoint where cooling should be expectedThe temperature above which the compressor should run (°C)
input_number.freezer_critical_temperature_cConventional last-line thresholdYour conventional high-temperature limit (°C)
input_number.freezer_warning_hold_minutesPersistence before WarningHow long a pattern must hold before Warning (min)
input_number.freezer_critical_hold_minutesPersistence before CriticalHow long a pattern must hold before Critical (min)
input_number.freezer_critical_repeat_minutesCritical repeat spacingHow often Critical should repeat while active (min)
input_boolean.freezer_monitoring_enabledMaster notification gateLeave 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:

TierEvidenceAction
AdvisoryAny single early clueDashboard only; collect context
WarningA stale source, or multiple early clues sustained for the measured hold timeOne push notification
CriticalThe conventional temperature line, or a sustained high-confidence combinationPush, 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

  1. Leave input_boolean.freezer_monitoring_enabled off.
  2. Check every source entity for correct units, realistic values and regular history.
  3. Confirm the reference probe is independent and not merely mirroring the primary reading.
  4. Use Developer Tools to inspect every derived entity and its availability.
  5. Open the lid briefly and confirm that a single expected disturbance remains Advisory.
  6. Safely simulate missing data by taking only the sensor offline; confirm the stale logic after your measured delay.
  7. Test the Warning push and Critical audible action manually.
  8. 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.