There’s about $400 of meat, milk, and miscellaneous condiments in my kitchen fridge at any given time. It runs 24/7, makes a quiet humming noise, and gives no indication when something’s wrong until you open the door three days later and recoil. The freezer compartment is worse: a slow failure can defrost everything before you notice the puddle.

I already had a TP-Link P110 smart plug on the fridge — originally for energy monitoring, because I’m on a spot-priced electricity tariff and I like knowing what each appliance costs me. But the same wattage stream that tells you “the fridge used 1.4 kWh today” tells you almost everything you need to know about whether the fridge is healthy.

The signal

The P110 reports sensor.fridge_current_consumption in watts, updating every ~5 seconds (the current tplink integration default). A typical fridge has two power states:

  • Standby — 1 to 3 W. Everything dark, compressor off, just the controller and a sensor or two.
  • Compressor running — 50 to 200 W, depending on age and ambient temperature.

If you imagine that as a graph, you get a square wave: long flats near zero, periodic blocks at ~120 W, repeating every 30–60 minutes. Almost everything interesting about the fridge — door open, seal failing, mechanical struggle, total failure — shows up as a deformation of that square wave.

The first thing to build is a binary sensor that tracks compressor on/off.

Step one: detecting the compressor cycle (with hysteresis)

The naive version is “compressor is on if watts > 30.” This breaks immediately, because the wattage can hover within a few watts of any threshold you pick, flapping the sensor on and off ten times a second. You need hysteresis: a high threshold to turn on, a lower threshold to turn off.

Home Assistant ships a binary_sensor platform called threshold that does upper/lower bounds with hysteresis in three lines of YAML. It’s the obvious tool for this job, and for a long time it was what I would have reached for. I don’t anymore, for three reasons.

It can’t have a unique_id. platform: threshold predates the entity registry and was never updated to support it. That means the entity it produces lives outside the registry — you can’t rename it from the UI, can’t apply labels, can’t hide it from a dashboard cleanly, and tools like Spook that operate on registry entities can’t see it. In a config where every other YAML-defined entity is registry-managed, having one or two threshold sensors floating outside that system is a constant small irritation.

It can’t propagate unavailable. When the source sensor goes unavailable, platform: threshold evaluates against a missing value and the result is unhelpful — usually it sticks at its last state. A template sensor with an explicit availability: block bubbles the unknown state through, which is what every downstream alert and history-stats sensor actually wants.

The hysteresis you can express is limited. platform: threshold gives you one upper bound and one lower bound. The self-referential template pattern lets the thresholds be anything you can compute — different in summer vs. winter, different by time of day, different depending on whether another appliance is also drawing power. You almost never need that flexibility, but on the day you do, you don’t have to rewrite the sensor.

So instead I use a template: binary_sensor that references its own state. It’s six more lines than the threshold version. For a sensor I’m going to depend on for years, that’s a trade I’ll happily make every time.

template:
  - binary_sensor:
      - name: "Fridge Compressor Running"
        unique_id: 2236e0fe-63a0-4d11-b6a0-c0efd94d5f69
        device_class: running
        availability: >
          {{ states('sensor.fridge_current_consumption')
             not in ['unknown', 'unavailable', 'none'] }}
        state: >
          {% set w = states('sensor.fridge_current_consumption') | float(0) %}
          {% if is_state('binary_sensor.fridge_compressor_running', 'on') %}
            {{ w > 10 }}
          {% else %}
            {{ w > 20 }}
          {% endif %}

The availability block matters: if the smart plug loses Wi-Fi or the integration glitches, states(...) returns 'unavailable', and | float(0) would silently turn that into “compressor is off” — exactly the wrong story to tell. Marking the sensor itself unavailable when its source is unavailable propagates the unknown state through everything downstream instead of papering over it with zeros.

The state block self-references: binary_sensor.fridge_compressor_running reads its own current state to decide which threshold to apply. The HA template engine is fine with this — it just uses the previous state from before this evaluation. On startup, with no prior state to read, the template falls through to the “currently off” branch and uses the higher 20 W threshold — the safe direction to fail.

This binary sensor is the foundation. Everything that follows is built on it.

Step two: rolling-window aggregations

Two history_stats sensors derive the shape:

sensor:
  - platform: history_stats
    name: "Fridge Compressor Duty 6h"
    entity_id: binary_sensor.fridge_compressor_running
    state: "on"
    type: ratio
    duration: { hours: 6 }
    end: "{{ now() }}"

  - platform: history_stats
    name: "Fridge Compressor Cycle 6h"
    entity_id: binary_sensor.fridge_compressor_running
    state: "on"
    type: count
    duration: { hours: 6 }
    end: "{{ now() }}"

type: ratio gives me a percentage: “the compressor was on 32 % of the last six hours.” type: count gives me a cycle count: “the compressor turned on 8 times in the last six hours.” Together, those two numbers describe the shape of the duty cycle — and the shape is what the alerts key off.

history_stats and statistics sensors reload via homeassistant.reload_all (or their own history_stats.reload / statistics.reload services) most of the time. I’ve occasionally hit cases where a brand-new sensor wouldn’t appear in the entity registry until after a full HA restart — if reload_all returns success but the entity never shows up, that’s the next thing to try.

Step three: the three alerts

All three are binary_sensor entities with device_class: problem, which makes them render as “OK / Problem” in the UI. The convention I use across the house is: the binary sensor is the signal, a separate automation is the action. Notification logic, cooldowns, channel routing — all of that lives in automations subscribed to these entities, and the alert sensors themselves stay declarative.

Alert 1: Fridge offline

- name: "Alert: Fridge offline"
  device_class: problem
  delay_on: "00:02:00"
  state: >
    {{ states('switch.fridge') in ['off', 'unavailable', 'unknown', 'none'] }}

Either the smart plug lost comms, or the fridge lost mains power. The two-minute delay_on filters out the brief blips that happen when the broker reconnects or the plug reboots itself.

Alert 2: Fridge stuck on

- name: "Alert: Fridge stuck on"
  device_class: problem
  state: >
    {% set duty = states('sensor.fridge_compressor_duty_6h') | float(0) %}
    {% set cyc  = states('sensor.fridge_compressor_cycle_6h') | int(0) %}
    {{ duty > 85 and cyc < 3 }}

This is the interesting one. A healthy fridge cycles: maybe 8–15 times in 6 hours, with the compressor on perhaps 25–40 % of the time. High duty plus low cycle count is a specific failure shape — the compressor is running almost continuously without ever satisfying the thermostat. That’s either a thermostat fault, a refrigerant problem, or a seal so bad the compressor can’t keep up. Distinguishing it from “it’s just a hot day” is exactly what the cycle-count condition does: a hot-day fridge still cycles, just more often.

Alert 3: Fridge door open

template:
  - trigger:
      - platform: state
        entity_id: binary_sensor.fridge_compressor_running
        to: "on"
        for: "00:45:00"
        id: alert_on
      - platform: state
        entity_id: binary_sensor.fridge_compressor_running
        to: "off"
        id: alert_off
    binary_sensor:
      - name: "Alert: Fridge door open"
        device_class: problem
        state: "{{ trigger.id == 'alert_on' }}"

A single compressor run longer than 45 minutes almost always means the door is ajar — the compressor can’t pull the temperature down because warm air keeps coming in.

This one is structured as a trigger-based template binary sensor, not a regular state-based one with delay_on. Two reasons:

  1. delay_on leaves the sensor in unknown if its template evaluates true at HA startup, until the next state change. A trigger-based sensor has a clean state machine.
  2. Templates that look at last_changed and now() don’t re-evaluate on a clock — they only re-evaluate when one of the entities they reference changes state. A trigger-based template wakes up on a time-bounded state change (to: "on" for: "00:45:00"), which is exactly the right shape for “this condition has been true continuously for X.”

This pattern shows up everywhere once you start looking for it: laundry left running, garage door open, lights on with no motion. Memorise it.

Bonus: the alerts gave me cost tracking for free

Once you have a wattage stream and a binary “is it running” sensor, multiplying watts by the live spot price gives you instantaneous dollars per hour. (My $/kWh sensor comes from cabberley/amber2mqtt, a community bridge that polls Amber Electric’s API and republishes 5-minute spot prices to my Mosquitto broker. The official Amber HA integration would do the same job, with minor entity-name changes.) Feed that into a Riemann-sum integration sensor and you have cumulative dollars. Wrap it in a utility_meter and you get daily and monthly totals that reset on schedule.

- platform: integration
  name: "Fridge Cost Total"
  source: sensor.fridge_cost_rate
  unit_time: h
  method: left   # left-Riemann: correct for step-function spot prices
  round: 4

utility_meter:
  fridge_cost_today:
    source: sensor.fridge_cost_total
    cycle: daily
  fridge_cost_month:
    source: sensor.fridge_cost_total
    cycle: monthly

The method: left matters: spot prices are step functions (they hold a value for 5 minutes, then jump), and left-Riemann sums them correctly. Trapezoidal would smear the jumps and give you slightly wrong numbers.

The fridge runs me about $3.20 a month at current prices.

What it has actually caught

In the months since deploying this:

  • The “fridge offline” alert fires occasionally when the IoT VLAN has a hiccup. Two-minute delay_on filters most of it; what remains is genuinely useful.
  • The “door open” alert has fired three times. All three were genuinely ajar doors.
  • “Stuck on” has not fired. My fridge is fine and the alert is correctly silent.

The point of monitoring isn’t to generate notifications — it’s to give you confidence that the absence of a notification means something.

The general pattern

This whole thing — power signal → derived running state → rolling windows → behaviour-shaped alerts — generalises. I have the same skeleton on the dishwasher, washing machine, and dryer, each with appliance-specific thresholds. Anything that draws meaningful power and has a “is it doing its job” question worth answering is a candidate.

The cost is one smart plug and a few hundred lines of YAML. The benefit is silent confidence that, when something does fail, you’ll know within minutes — not when the smell hits you.


Setup notes: Home Assistant 2026.x, TP-Link P110 via the Kasa integration, MQTT broker for Amber Electric pricing data. Full YAML available on request.