# HA Proactive Health Audit Methodology Systematic health assessment of a Home Assistant instance — not tied to a specific outage, but used to find improvement opportunities, dead integrations, and hidden issues. Complements the reactive "Bulk Entity Audit" section in the SKILL.md (which diagnoses a *known* outage). This procedure finds problems the user doesn't know about yet. Based on a 2026-07-12 audit session that reduced 28.5% unavailable entities to a prioritized action list. ## Audit Procedure (6 Steps) ### Step 1: Instance Metadata ```bash curl -s "$HA_URL/api/config" -H "Authorization: Bearer $TOKEN" | python3 -c " import sys,json d=json.load(sys.stdin) print(f'Version: {d.get(\"version\",\"?\")}') print(f'State: {d.get(\"state\",\"?\")}') comps=d.get('components',[]) print(f'Components: {len(comps)}') " ``` Records HA version, running state, and component count. Useful for tracking growth over time and confirming the instance is responsive. ### Step 2: Entity Health Statistics ```python import json from collections import Counter data = json.loads(open('/tmp/ha_states.json','rb').read().decode('utf-8','replace')) total = len(data) unavail = [e for e in data if e['state'] == 'unavailable'] unknown = [e for e in data if e['state'] == 'unknown'] print(f'Total entities: {total}') print(f'Unavailable: {len(unavail)} ({len(unavail)/total*100:.1f}%)') print(f'Unknown: {len(unknown)} ({len(unknown)/total*100:.1f}%)') # Domain distribution domains = Counter(e['entity_id'].split('.')[0] for e in data) print(f'Domains (top 15): {domains.most_common(15)}') ``` **Benchmark**: <5% unavailable is healthy. 10-20% suggests stale integrations. >25% indicates systemic issues (dead integrations, network migration remnants, or a recent outage that didn't fully recover). ### Step 3: Group Unavailable Entities by Source This is the key diagnostic step — group unavailable entities by their integration/device prefix to identify which integrations are responsible for the bulk of unavailability. ```python groups = Counter() for e in unavail: eid = e['entity_id'] parts = eid.replace('.', '_').split('_') key = '_'.join(parts[:3]) # e.g. "sensor_spoolman_spool" groups[key] += 1 for g, c in groups.most_common(20): print(f' {g}: {c}') ``` **Interpretation**: Groups with 50+ unavailable entities indicate an entire integration is down, not individual device failures. Common patterns: - `sensor_spoolman_*` → Spoolman server offline - `*_hame_energy` → HAME Energy integration dead (often replaced by Modbus) - `select_x40_master` / `select_staubsauger_oben` → Vacuum "feature-not-supported" entities (normal, but bloat) - `*_double_take` → Face recognition dependent on Frigate (cascade failure) ### Step 4: Config Entry State Audit ```bash curl -s "$HA_URL/api/config/config_entries/entry" -H "Authorization: Bearer $TOKEN" | python3 -c " import sys,json from collections import Counter data=json.load(sys.stdin) states=Counter(e.get('state','?') for e in data) print(f'Config entry states: {dict(states)}') for state in ['setup_retry','setup_error']: entries=[e for e in data if e.get('state')==state] for e in entries: print(f' {e.get(\"domain\")} | {e.get(\"title\")} | {e.get(\"source\")}') " ``` **States**: | State | Meaning | Action | |-------|---------|--------| | `loaded` | Working | None | | `setup_retry` | Discovered but can't connect | Check device/network, reconfigure | | `setup_error` | Configuration failed | Check credentials, re-setup | | `not_loaded` | Disabled/ignored | Clean up if no longer needed | **Common findings**: - Stale IPs from network migrations (e.g. `192.168.100.x` titles when network is now `10.0.x`) - `not_loaded` entries from abandoned integrations (Nest, DLNA, UPnP, HomeKit bridges) - `setup_retry` from VLAN-boundary mDNS failures (see SKILL.md "VLAN/Network Segmentation Diagnosis") ### Step 5: Automation Status ```python autos = [e for e in data if e['entity_id'].startswith('automation.')] on_a = sum(1 for a in autos if a['state'] == 'on') off_a = sum(1 for a in autos if a['state'] == 'off') print(f'Automations: {len(autos)} total, {on_a} on, {off_a} off') # List disabled ones with friendly names for a in autos: if a['state'] == 'off': name = a.get('attributes',{}).get('friendly_name', a['entity_id']) print(f' {a["entity_id"]}: {name}') ``` Disabled automations are often forgotten after debugging. Check if they were disabled intentionally or left off after a fix was applied but never re-enabled. **Especially check automations that were recently fixed** — the fix may have been applied but the automation never reactivated. ### Step 6: Staleness Detection ```python import datetime now = datetime.datetime.now(datetime.timezone.utc) stale = [] for e in data: lc = e.get('last_changed', '') if not lc: continue try: dt = datetime.datetime.fromisoformat(lc.replace('Z', '+00:00')) age = (now - dt).total_seconds() / 3600 if age > 24 and e['state'] not in ('unavailable', 'unknown'): stale.append((e['entity_id'], e['state'], round(age))) except: pass stale.sort(key=lambda x: x[2], reverse=True) print(f'Stale entities (>24h, not unavail/unknown): {len(stale)}') ``` Large stale counts (hundreds) often correlate with `update.*` entities that haven't been polled since last restart, or sensors from integrations that silently stopped publishing. Cross-reference with Step 3 groups. ## Synthesis: Prioritized Improvement List Combine all 6 steps into a ranked table: | Priority | Issue | Entities Affected | Effort | Rationale | |----------|-------|--------------------|--------|-----------| | 1 | Dead integration removal (HAME, Spoolman, Kia) | 194 | S | Largest reduction in unavailable count | | 2 | Frigate recovery chain | 13+ | M | Security cascade dependency | | 3 | Reactivate disabled automations | 7 | S | Immediate functional gain | | 4 | Network migration cleanup | 10+ | S | Stale IPs blocking reconnection | | 5 | Entity bloat reduction | 245+ | M | Disable unsupported vacuum entities | **Scoring criteria** (adapted from `ideate` skill): - **Impact** (30%): How many entities/users are affected? - **Feasibility** (25%): Can it be done without breaking production? - **Strategic fit** (20%): Does it align with the user's direction (e.g. Marstek replacing HAME)? - **Effort** (15% inversely weighted): S < 30 min, M < 2h, L > 2h - **Compound potential** (10%): Does fixing this unlock other improvements? ## Audit Results Snapshot (2026-07-12) | Metric | Value | |--------|-------| | HA Version | 2026.7.1 | | Total entities | 2,282 | | Unavailable | 650 (28.5%) | | Unknown | 197 (8.6%) | | Automations | 35 (25 on, 7 off) | | Config entries | 152 loaded, 5 setup_retry, 13 not_loaded, 1 setup_error | | Stale (>24h) | 680 | ### Top Unavailable Groups | Group | Count | Root Cause | |-------|-------|------------| | `sensor_spoolman_spool` | 101 | Spoolman server down | | `select_x40_master` | 85 | Vacuum feature-not-supported (normal bloat) | | `select_staubsauger_oben` | 48 | Same | | `text/sensor/number/switch_hame_energy` | 74 | HAME Energy completely dead (replaced by Marstek Modbus) | | `sensor_double_take` | 13 | Frigate dependency (cascade) | | `light_tradfri_*` | 16+ | Stale IP 192.168.100.60 (pre-migration) | ### Setup Retry Entries | Domain | Title | Source | |--------|-------|--------| | lifx | Kinderzimmer 1 | dhcp | | lifx | Kinderzimmer 2 | dhcp | | tradfri | 192.168.100.60 | user | | frigate | 10.0.30.104:5000 | user | | ollama | http://10.0.30.98:11434 | user | ### Disabled Automations | Automation | Friendly Name | Likely Reason | |------------|---------------|---------------| | `automation.warmwasserbereitung_an` | DHW onetimecharge | Disabled during tuning | | `automation.dhw_onetimecharge_when_dhw_charging_active` | DHW onetimecharge when DHW Charging active | Related to above | | `automation.tibber_charge_battery` | Tibber Charge Battery | Disabled during testing | | `automation.vogelabwehr_1/2/3` | Vogelabwehr 1/2/3 | Seasonal? | | `automation.emhass_batterie_kaskadensteuerung_sma_marstek` | EMHASS Kaskadensteuerung | Fixed but not reactivated! | ## Pitfalls - **Error log endpoint may 404 on some HA versions.** `/api/error_log` returned 404 on HA 2026.7.1. Try `/api/error/all` or check via SSH instead. Don't treat the 404 as "no errors" — it means the endpoint isn't available, not that the log is empty. - **Vacuum "feature-not-supported" entities inflate the unavailable count dramatically.** Two Dreame vacuums contributed ~250 unavailable entities. These are normal (the integration creates entities for all possible features, unsupported ones stay unavailable). Don't try to "fix" them — consider disabling them in the entity registry to reduce noise. - **`update.*` entities dominate the stale list.** Most stale entities are `update.*` sensors that only change when an update is available. This is normal behavior, not a defect. Filter them out when assessing real staleness. - **Config entry titles can be stale.** A `setup_retry` TRADFRI entry titled "192.168.100.60" doesn't mean the integration is trying that IP — the title is cosmetic. Check the actual `data` field in `.storage/core.config_entries` for the real configured address.