Smart Home Fix

Build a house health score without hiding faults

One green percentage can conceal a serious freezer alarm among dozens of healthy lights. Build the score from named problem entities, make unknown inputs visible, and keep safety-critical faults on a separate path that no average can dilute.

The fault this project detects

This project detects a broad deterioration in monitored house systems while preserving the reason for every lost point. It also detects when the score itself is untrustworthy because inputs are unknown or unavailable.

The example uses two UI-created groups: one containing reviewed fault binary sensors and a smaller critical group containing only conditions that justify immediate action. Start with a few reliable inputs, not every device in Home Assistant.

Explainable house health evidence flow Multiple measurements are combined into advisory, warning and critical fault states. Active fault countUnknown input countHealth-score trendCritical fault group Evidence model direction + context agreement + duration Advisory dashboard only Warning one push Critical repeat + audible
Named inputs feed a summary, but critical faults bypass the average and unknown inputs invalidate false confidence.

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. Value: the score shows the share of reviewed inputs currently healthy.
  2. Direction: a Derivative or Statistics helper can show broad deterioration before a low threshold is crossed.
  3. Agreement: several independent fault entities turning on together supports a whole-house problem.
  4. Context and time: unknown inputs invalidate the score, persistence rejects flapping, and the critical group bypasses averaging.

The score is a navigation aid, not a safety decision. Every contributing problem remains visible, and any critical input can trigger Critical even if the percentage still looks high.

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

Choose inputs only after their individual logic is stable. Record how often they are unknown, how long ordinary maintenance lasts and which combinations genuinely deserve interruption.

MeasurementWhy it mattersRequired value
group.house_health_faultsReviewed problem entitiesList every binary sensor you plan to add to this group and who is responsible for acting on each one
group.house_health_critical_faultsImmediate-action subsetList which of those faults justify immediate action, and what the expected response is for each
sensor.house_health_scoreSummary historyCheck its History graph over a couple of ordinary weeks and note the normal daily range, plus how far it dips during routine maintenance
sensor.house_health_unknown_countScore confidenceWatch this count for a few days including a Home Assistant restart, and note the normal count versus what only happens while integrations are still loading
sensor.house_health_score_rateBroad directionCompare its value on an ordinary day against a day with a real fault, and note the normal range versus what you saw during that incident

The experiment that makes this article defensible

  1. Create the groups with only two or three well-understood problem sensors, then confirm every name appears in the score attributes.
  2. Temporarily use a safe test Toggle represented by a template problem sensor to verify counting and notifications.
  3. Restart Home Assistant and observe unknown inputs; the score must not report false health while dependencies initialise.
  4. Add one subsystem at a time and preserve a change log explaining its criticality and owner.

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 entityPurposeSetting
group.house_health_faultsGroup helper containing reviewed problem binary sensorsThe exact entity IDs of the binary sensors you're grouping, taken from Developer Tools → States
group.house_health_critical_faultsGroup helper containing the immediate-action subsetThe exact entity IDs of the immediate-action binary sensors, taken the same way
sensor.house_health_score_rateOptional Derivative helper in percentage points per hourA time window that matches how quickly you want deterioration to register — check the Derivative helper's own settings for the available choices
input_number.house_health_warning_scoreSummary threshold for persistent broad degradationThe percentage below which you'd call the house genuinely degraded, set after watching the score's normal daily range for a week or two
input_number.house_health_hold_minutesPersistence before WarningHow many minutes of sustained low score you want before a Warning fires, long enough to ignore a brief blip
input_boolean.house_health_monitoring_enabledMaster notification gateLeave this off until you've tested the automation end to end, then switch it on
input_number.house_health_critical_repeat_minutesCritical repeat spacingHow 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.

Count named faults and expose the reasons

Create both Groups in the helper interface first. The template makes the score unavailable when any member is unknown or unavailable, because an incomplete score must not look reassuring.

template:
  - sensor:
      - name: "House health active fault count"
        unique_id: house_health_active_fault_count
        default_entity_id: sensor.house_health_active_fault_count
        state_class: measurement
        state: >
          {{ expand('group.house_health_faults')
             | selectattr('state', 'eq', 'on') | list | count }}
        attributes:
          active_faults: >
            {{ expand('group.house_health_faults')
               | selectattr('state', 'eq', 'on')
               | map(attribute='entity_id') | list }}

      - name: "House health unknown count"
        unique_id: house_health_unknown_count
        default_entity_id: sensor.house_health_unknown_count
        state_class: measurement
        state: >
          {{ expand('group.house_health_faults')
             | selectattr('state', 'in', ['unknown', 'unavailable']) | list | count }}

      - name: "House health score"
        unique_id: house_health_score
        default_entity_id: sensor.house_health_score
        state_class: measurement
        unit_of_measurement: "%"
        availability: >
          {% set members = expand('group.house_health_faults') | list %}
          {{ members | count > 0
             and (members | selectattr('state', 'in', ['unknown', 'unavailable']) | list | count) == 0 }}
        state: >
          {% set members = expand('group.house_health_faults') | list %}
          {% set active = members | selectattr('state', 'eq', 'on') | list | count %}
          {{ (((members | count - active) / (members | count)) * 100) | round(0) }}
        attributes:
          active_faults: >
            {{ expand('group.house_health_faults')
               | selectattr('state', 'eq', 'on')
               | map(attribute='entity_id') | list }}

  - binary_sensor:
      - name: "House health warning"
        unique_id: house_health_warning
        default_entity_id: binary_sensor.house_health_warning
        device_class: problem
        delay_on:
          minutes: >
            {{ states('input_number.house_health_hold_minutes') | int(0) }}
        availability: >
          {{ has_value('sensor.house_health_score') }}
        state: >
          {{ (states('sensor.house_health_score') | float)
             <= (states('input_number.house_health_warning_score') | float) }}
        attributes:
          reason: >-
            Score is {{ states('sensor.house_health_score') }}%; active faults are
            {{ state_attr('sensor.house_health_score', 'active_faults') }} and unknown input count is
            {{ states('sensor.house_health_unknown_count') }}.

      - name: "House health critical"
        unique_id: house_health_critical
        default_entity_id: binary_sensor.house_health_critical
        device_class: problem
        state: >
          {{ is_state('group.house_health_critical_faults', '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: house_health_warning_push
    alias: "House health - Warning push"
    mode: single
    triggers:
      - trigger: state
        entity_id: binary_sensor.house_health_warning
        to: "on"
    conditions:
      - condition: state
        entity_id: input_boolean.house_health_monitoring_enabled
        state: "on"
    actions:
      - action: notify.mobile_app_your_phone
        data:
          title: "House health warning"
          message: >-
            {{ state_attr('binary_sensor.house_health_warning', 'reason')
                or 'The warning condition is active. Check Home Assistant for evidence.' }}

  - id: house_health_critical_repeat
    alias: "House health - Critical repeat"
    mode: restart
    triggers:
      - trigger: state
        entity_id: binary_sensor.house_health_critical
        to: "on"
    conditions:
      - condition: state
        entity_id: input_boolean.house_health_monitoring_enabled
        state: "on"
    actions:
      - action: notify.mobile_app_your_phone
        data:
          title: "House health 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.house_health_critical
              state: "on"
            - condition: template
              value_template: >-
                {{ states('input_number.house_health_critical_repeat_minutes') | int(0) > 0 }}
          sequence:
            - delay:
                minutes: >-
                  {{ states('input_number.house_health_critical_repeat_minutes') | int(0) }}
            - condition: state
              entity_id: binary_sensor.house_health_critical
              state: "on"
            - action: notify.mobile_app_your_phone
              data:
                title: "House health 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 unweighted calculation is transparent: each reviewed input is either healthy or active. Unknown inputs remove the score instead of improving it accidentally, and the critical group ensures a serious fault cannot be averaged away.

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
AdvisoryOne named low-consequence problem is active or the score trend is deteriorating.Dashboard only
WarningSeveral reviewed problems persist and the explainable score crosses its calibrated threshold.One push notification
CriticalAny separately reviewed critical fault is active, regardless of the overall percentage.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

The score is unavailable after restart
That is intentional while members are unknown. Fix slow or missing inputs instead of coercing them to healthy.
One chatty entity dominates attention
Repair its individual debounce and evidence model before keeping it in the group.
A serious fault barely changes the score
Put it in the critical group. Do not rely on an average for asymmetric consequences.
The group includes device-health noise
Start with actionable faults that have an owner and response; leave decorative diagnostics out.
The template divides by zero
Ensure the group contains at least one entity. Availability prevents state evaluation from being trusted when it is empty.

Safety and limits

A score must never replace smoke alarms, electrical protection, security systems, medical alarms or equipment interlocks. Keep critical paths independent, visible and testable, with named human responses.

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

Is a house health score safe for critical decisions?

No. It is a summary and navigation aid. Critical faults need their own direct paths and must not be diluted by an average.

Why make the score unavailable when an input is unknown?

Missing evidence cannot honestly be counted as healthy. Unavailability exposes a monitoring fault instead of inflating the score.

Should every device be included?

No. Include reviewed, actionable problem entities with clear ownership. Hundreds of decorative device states make the score noisy and meaningless.

Why use an unweighted score?

It is easy to audit and explain. Consequence is handled by the separate critical group rather than hidden weights.

How do I find what lowered the score?

The template stores active entity IDs in an attribute. Put those named faults beside the score on the dashboard and in Warning messages.