Smart Home Fix

Detect a sensor that has frozen on a believable value

The dangerous sensor failure is not always unavailable. Sometimes it is a perfectly believable number that never changes again. A watchdog must ask whether the physical process moved while the sensor did not.

The fault this project detects

The target is stale measurement behaviour: the primary entity remains numeric beyond its normal unchanged period, disagrees with an independent reference, or stays flat while the monitored equipment operates.

The example uses a primary plant sensor, an independent reference sensor and a plant-running binary sensor. The measured quantity may be temperature, pressure, humidity or level, provided both numeric sensors use the same unit and represent the same process closely enough to compare.

Frozen sensor watchdog flow Multiple measurements are combined into advisory, warning and critical fault states. Primary last_changedReference differencePlant runningEntity availability Evidence model direction + context agreement + duration Advisory dashboard only Warning one push Critical repeat + audible
A believable value becomes suspicious when time, process activity and an independent reference contradict it.

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. State age. A trigger-based template checks last_changed even if no new source event arrives.
  2. Availability. Unknown and unavailable states remain explicit faults rather than being coerced into zero.
  3. Independent comparison. A second sensor provides evidence that the process changed or that the two measurements diverged.
  4. Process context. A flat reading is more suspicious while a compressor, pump, fan or heater has been active.

A stable value is not automatically stale. The calibration must include the longest legitimate unchanged period caused by rounding, reporting policy and a genuinely steady process.

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 work out your own longest healthy interval between meaningful state changes by scrolling back through that entity's History over a normal week and timing the longest gap between real updates. 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: pick two points from that entity's History, subtract the earlier value from the later one for your measured change, divide by the number of seconds between their timestamps for the elapsed seconds, and that division gives you the 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

Measure the normal reporting interval and the longest period the displayed state remains identical. Check whether attributes update without the state changing; this design intentionally uses last_changed because the alarm is about a frozen value.

MeasurementWhy it mattersRequired value
sensor.plant_primary_valueNormal update interval and unchanged durationCheck this sensor's History and note its normal update interval and how long it can stay unchanged during genuinely healthy operation.
sensor.plant_reference_valueNormal difference from primaryCompare this sensor against the primary value over a normal period and note the usual difference between them, in the same units.
binary_sensor.plant_runningPeriods when the process should cause movementLook at this binary sensor's on/off History and note the actual times the process was running versus idle.
recorder historyRounding steps and repeated identical valuesExport this entity's History from the History page and check for rounding steps or repeated identical values that could mask a frozen reading.
failure responseWhether the device goes unavailable or holds its last stateDisconnect or power down the device briefly and safely, then watch whether Home Assistant marks it unavailable or simply holds its last reading; that observed behaviour decides which check below you actually need.

The experiment that makes this article defensible

  1. Export a steady operating period and find the longest legitimate primary last_changed age.
  2. Capture a full equipment cycle and confirm that the primary and reference both move enough to be observable.
  3. Disconnect only the sensor through a safe test method and record whether Home Assistant shows unavailable or preserves the old number.
  4. Run the watchdog with notifications disabled until normal flat periods no longer create Warnings.

Keep your own record as you go: timestamped readings, your Home Assistant version, the exact entity IDs you used, 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
input_number.primary_sensor_stale_minutesLongest allowed unchanged primary stateSet this to a few minutes longer than the longest healthy unchanged period you saw in the sensor's own History.
input_number.primary_reference_difference_limitLargest normal same-unit differenceSet this a little above the largest normal difference you measured between the primary and reference sensors, in their shared units.
input_number.sensor_watchdog_hold_minutesPersistence before WarningSet a hold time long enough to avoid normal short dropouts, based on what you saw in your own History.
input_boolean.primary_sensor_safety_criticalMarks loss of this measurement as urgentDecide for your own site whether losing this measurement is safety-critical, such as a freezer or medical sensor, and set this switch accordingly.
input_boolean.frozen_sensor_monitoring_enabledMaster notification gateLeave this switched off until you have tested the automation end to end, then turn it on.
input_number.frozen_sensor_critical_repeat_minutesCritical repeat spacingSet a repeat interval greater than zero that matches how often you actually want to be reminded of a critical fault.

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.

Re-evaluate state age even when nothing changes

The time-pattern trigger wakes the template each minute. It does not pretend that the source reported; it lets Home Assistant notice that last_changed is getting older.

template:
  - sensor:
      - name: "Plant sensor difference"
        unique_id: plant_sensor_difference
        default_entity_id: sensor.plant_sensor_difference
        state_class: measurement
        availability: >
          {{ has_value('sensor.plant_primary_value')
             and has_value('sensor.plant_reference_value') }}
        state: >
          {{ ((states('sensor.plant_primary_value') | float)
              - (states('sensor.plant_reference_value') | float))
             | abs | round(2) }}

  - triggers:
      - trigger: time_pattern
        minutes: "*"
      - trigger: state
        entity_id:
          - sensor.plant_primary_value
          - sensor.plant_reference_value
    binary_sensor:
      - name: "Primary sensor stale"
        unique_id: primary_sensor_stale
        default_entity_id: binary_sensor.primary_sensor_stale
        device_class: problem
        state: >
          {% set source = states.sensor.plant_primary_value %}
          {% set limit = states('input_number.primary_sensor_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)) }}

  - binary_sensor:
      - name: "Sensor watchdog warning"
        unique_id: sensor_watchdog_warning
        default_entity_id: binary_sensor.sensor_watchdog_warning
        device_class: problem
        delay_on:
          minutes: >
            {{ states('input_number.sensor_watchdog_hold_minutes') | int(0) }}
        state: >
          {{ is_state('binary_sensor.primary_sensor_stale', 'on')
             or (has_value('sensor.plant_sensor_difference')
                 and (states('sensor.plant_sensor_difference') | float)
                     >= (states('input_number.primary_reference_difference_limit') | float)) }}
        attributes:
          reason: >-
            Primary stale: {{ states('binary_sensor.primary_sensor_stale') }};
            difference: {{ states('sensor.plant_sensor_difference') }}.

      - name: "Sensor watchdog critical"
        unique_id: sensor_watchdog_critical
        default_entity_id: binary_sensor.sensor_watchdog_critical
        device_class: problem
        state: >
          {{ is_state('binary_sensor.primary_sensor_stale', 'on')
             and is_state('binary_sensor.plant_running', 'on')
             and is_state('input_boolean.primary_sensor_safety_critical', 'on') }}

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: frozen_sensor_warning_push
    alias: "Sensor watchdog - Warning push"
    mode: single
    triggers:
      - trigger: state
        entity_id: binary_sensor.sensor_watchdog_warning
        to: "on"
    conditions:
      - condition: state
        entity_id: input_boolean.frozen_sensor_monitoring_enabled
        state: "on"
    actions:
      - action: notify.mobile_app_your_phone
        data:
          title: "Sensor watchdog warning"
          message: >-
            {{ state_attr('binary_sensor.sensor_watchdog_warning', 'reason')
                or 'The warning condition is active. Check Home Assistant for evidence.' }}

  - id: frozen_sensor_critical_repeat
    alias: "Sensor watchdog - Critical repeat"
    mode: restart
    triggers:
      - trigger: state
        entity_id: binary_sensor.sensor_watchdog_critical
        to: "on"
    conditions:
      - condition: state
        entity_id: input_boolean.frozen_sensor_monitoring_enabled
        state: "on"
    actions:
      - action: notify.mobile_app_your_phone
        data:
          title: "Sensor watchdog 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.sensor_watchdog_critical
              state: "on"
            - condition: template
              value_template: >-
                {{ states('input_number.frozen_sensor_critical_repeat_minutes') | int(0) > 0 }}
          sequence:
            - delay:
                minutes: >-
                  {{ states('input_number.frozen_sensor_critical_repeat_minutes') | int(0) }}
            - condition: state
              entity_id: binary_sensor.sensor_watchdog_critical
              state: "on"
            - action: notify.mobile_app_your_phone
              data:
                title: "Sensor watchdog 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

last_changed exposes a state that has stopped moving, the reference catches drift or a stuck primary, and plant state tells you whether movement should be expected. Keeping unknown and unavailable separate avoids the common mistake of turning missing data into a harmless zero.

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 primary is older than usual or the two sensors are beginning to separate.Dashboard only
WarningStaleness or disagreement persists beyond normal rounding and reporting behaviour.One push notification
CriticalA safety-critical primary remains stale while the monitored plant is operating.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

A stable room causes stale warnings
Increase the state-age limit from measured history or use a source that reports enough precision to show real movement.
The device reports but last_changed stays old
That is expected when the numeric state is identical. If packet receipt matters instead, expose a heartbeat or timestamp as a separate entity.
Both sensors freeze together
Independence is weak if they share power, radio path, gateway or conversion code. Separate the failure domains.
The difference sensor is unavailable
Check that both sources are numeric and use compatible units; do not coerce unknown to zero.
Restart resets a pending delay
Template delay timers are not a certified persistence mechanism across reloads. Use a persisted deadline helper where restart survival is essential.

Safety and limits

A watchdog does not validate calibration or prove which sensor is correct. Safety-rated processes need certified instruments and independent protection. Do not let a derived Home Assistant entity override those systems.

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 check only for unavailable?

Some devices keep their last numeric state when communication or sensing fails. That value looks valid, so availability alone can miss the failure.

What is the difference between last_changed and last_updated?

last_changed tracks when the entity's state value changed. Attributes can update without changing that state. This watchdog is deliberately interested in a value that remains identical.

Can a healthy sensor remain unchanged for hours?

Yes, especially when it rounds heavily or monitors a stable process. Measure the longest normal unchanged period before choosing a stale limit.

Does disagreement show which sensor is wrong?

No. It shows that the pair cannot both be trusted as equivalent measurements. Placement, calibration, response time and actual sensor failure must then be checked.

Should both sensors use the same network?

They can, but independence is stronger when they do not share every failure point. Separate power, radio paths or technologies may be worthwhile for critical monitoring.