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.
The evidence model
A useful alarm does not promote one noisy reading straight to an emergency. It combines measurements that fail in different ways:
- Value: the score shows the share of reviewed inputs currently healthy.
- Direction: a Derivative or Statistics helper can show broad deterioration before a low threshold is crossed.
- Agreement: several independent fault entities turning on together supports a whole-house problem.
- 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.
| Measurement | Why it matters | Required value |
|---|---|---|
group.house_health_faults | Reviewed problem entities | List every binary sensor you plan to add to this group and who is responsible for acting on each one |
group.house_health_critical_faults | Immediate-action subset | List which of those faults justify immediate action, and what the expected response is for each |
sensor.house_health_score | Summary history | Check 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_count | Score confidence | Watch 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_rate | Broad direction | Compare 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
- Create the groups with only two or three well-understood problem sensors, then confirm every name appears in the score attributes.
- Temporarily use a safe test Toggle represented by a template problem sensor to verify counting and notifications.
- Restart Home Assistant and observe unknown inputs; the score must not report false health while dependencies initialise.
- 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 entity | Purpose | Setting |
|---|---|---|
group.house_health_faults | Group helper containing reviewed problem binary sensors | The exact entity IDs of the binary sensors you're grouping, taken from Developer Tools → States |
group.house_health_critical_faults | Group helper containing the immediate-action subset | The exact entity IDs of the immediate-action binary sensors, taken the same way |
sensor.house_health_score_rate | Optional Derivative helper in percentage points per hour | A 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_score | Summary threshold for persistent broad degradation | The 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_minutes | Persistence before Warning | How many minutes of sustained low score you want before a Warning fires, long enough to ignore a brief blip |
input_boolean.house_health_monitoring_enabled | Master notification gate | Leave this off until you've tested the automation end to end, then switch it on |
input_number.house_health_critical_repeat_minutes | Critical repeat spacing | How 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
| Tier | Meaning | Action |
|---|---|---|
| Advisory | One named low-consequence problem is active or the score trend is deteriorating. | Dashboard only |
| Warning | Several reviewed problems persist and the explainable score crosses its calibrated threshold. | One push notification |
| Critical | Any 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
- Fix unreliable entities before trusting alarms
- Separate smart-home devices without breaking discovery
- Reduce false alarms without hiding real faults
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.