Braucht mein Smart Home wirklich einen Health-Check, es läuft ziemlich unauffällig. Und genau das ist das Problem: Wenn etwas ausfällt, merke ich es oft erst, wenn es auffällt. Das Backup läuft seit zwei Tagen nicht mehr, der Wassersensor im Keller meldet sich nicht, eine Batterie ist leer, oder die Datenbank schreibt nichts mehr. Alles Dinge, die man im Alltag nicht sieht, bis man sie braucht.
Deshalb habe ich mir einen Health-Check gebaut: ein einzelnes Home-Assistant-Package, das alle zwei Minuten prüft, ob alles in Ordnung ist, und das Ergebnis als Ampel im Dashboard und in der App anzeigt. Gemeldet wird nur, was wirklich wichtig ist. In diesem Beitrag zeige ich dir, was ich überwache, wie das Package aufgebaut ist, welche Entscheidungen ich unterwegs getroffen habe und wo ich noch Lücken habe.

📷 Das Health-Dashboard
🔧 Ausgangslage
Mein Setup ist über die Jahre gewachsen: Home Assistant mit einer MariaDB als Recorder-Datenbank, mehreren Apps (Mosquitto, Cloudflared, Matter, Vaultwarden, ESPHome, Grafana), Reolink-Kameras und Türklingel, Wassersensoren, Heizungsventile und diverse batteriebetriebene Sensoren. Zu jedem Teil gibt es irgendwo eine Einzelanzeige, aber keinen Ort, an dem ich auf einen Blick sehe, ob alles gesund ist.
🎯 Die Ziele
- ✅ Eine Ampel (grün, gelb, rot) statt eines Prozent-Scores, der nichts aussagt
- ✅ Eine Datei, in der ich alles pflege: neue Geräte oder Schwellwerte kommen in eine Liste am Dateianfang
- ✅ Nur bei wirklich kritischen Problemen ein Push aufs Handy, alles andere im Tagesreport
- ✅ Eine Entwarnung, wenn das Problem weg ist
- ❌ Keine Flut an Meldungen. Ein Health-Check, den man abstellt, weil er nervt, ist wertlos.
🚦 Das Konzept: Drei Schweregrade
Das Herzstück ist eine Liste von Befunden. Jeder Befund hat einen Text und einen Schweregrad. Aus dem höchsten Schweregrad ergibt sich die Ampel:
| Stufe | Ampel | Bedeutung | Was passiert |
|---|---|---|---|
| 2 – Kritisch | 🔴 Rot | Hier muss ich handeln | Push aufs Handy, wenn der Zustand 5 Minuten anhält |
| 1 – Warnung | 🟡 Gelb | Sollte ich mir ansehen | Erscheint im Tagesreport um 07:00 |
| 0 – Info | 🟢 Grün | Gut zu wissen | Nur Hinweis, z. B. verfügbares Update |
💡 Warum 5 Minuten Wartezeit? Viele Geräte sind kurz unavailable, etwa nach einem Neustart oder bei einem WLAN-Aussetzer. Ohne Wartezeit hätte ich ständig Fehlalarme. Erst wenn der Zustand anhält, ist es ein echtes Problem.
🔍 Was überwacht wird
| Bereich | Was geprüft wird | Stufe |
|---|---|---|
| Apps | Mosquitto, Cloudflared, Matter Server, Matter Hub, Vaultwarden, ESPHome Device Builder, Grafana: alles außer on (auch unavailable) | Kritisch |
| Backup | Letztes erfolgreiches Backup älter als 36 Stunden, oder letzter Versuch fehlgeschlagen | Kritisch |
| Recorder / Datenbank | Länger als 5 Minuten kein neuer Eintrag in der Datenbank | Kritisch |
| Festplatte | Unter 20 % frei, unter 10 % frei | Warnung / Kritisch |
| Wassersensoren | Küche und Keller unavailable oder unknown | Kritisch |
| Geräte offline | ESPHome-Präsenzsensor, Kameras, Türklingel | Warnung |
| Batterien | Prozentwerte unter Schwelle, Sensoren die sich zu lange nicht melden, Batterie-Flags der Ventile und Kontakte | Warnung |
| Entfeuchter | Offline bei eingeschalteter Steckdose, Tank voll, Filter wechseln | Warnung |
| Reparaturen | Offene Meldungen aus den Home-Assistant-Reparaturen | Kritisch / Warnung |
| Updates | HA Core, OS, Supervisor, Firmware der Kameras und Türklingel | Info |
Ein paar Punkte aus dieser Liste haben mehr Gedanken gekostet, als man ihnen ansieht:
Wassersensoren: tot ist gleich unsicher
Ein Wassersensor, der unavailable ist, meldet auch kein Wasser. Deshalb ist ein toter Wassersensor kritisch, nicht nur eine Warnung. Im Normalfall sieht man den Ausfall nie, im Ernstfall ist er der Unterschied zwischen Schaden und kein Schaden.
Batterien: Prozentwert ist nicht genug
Ein Sensor mit 80 % Batterie, der seit drei Tagen nichts mehr gesendet hat, zeigt weiter 80 %. Deshalb prüft der Health-Check bei den Temperatursensoren zusätzlich, wie lange sie sich nicht gemeldet haben (stale_h). Die Wassersensoren (Floos) habe ich bewusst davon ausgenommen: Sie melden sich nur bei Wasser oder niedrigem Batteriestand, ein langes Schweigen ist dort normal.
Manche Geräte, etwa meine Heizungsventile und Türkontakte, liefern gar keinen Prozentwert, sondern nur ein Flag „Batterie schwach“ (on/off). Dafür gibt es in der Prüfliste eine eigene Gruppe.
Entfeuchter: offline ist nicht immer ein Fehler
Mein Midea-Entfeuchter im Keller hängt an einer schaltbaren Steckdose. Ist die Steckdose aus, ist das Gerät unavailable. Das ist kein Fehler. Der Befund entsteht nur, wenn die Steckdose an ist und der Entfeuchter trotzdem nicht antwortet.
Recorder: schreibt die Datenbank noch?
Das ist der Check, den man am ehesten vergisst, und der am meisten Ärger macht, wenn er fehlt. Ein sql-Sensor fragt den Zeitstempel des jüngsten Eintrags in der Datenbank ab. Ist er älter als 5 Minuten, schreibt der Recorder nicht mehr, und die Historie bekommt Lücken. Der Sensor kommt ohne db_url aus und nutzt die Datenbank, die Home Assistant ohnehin verwendet. So stehen keine Zugangsdaten im Package.
💡 Bei einer großen Datenbank lohnt ein Blick, ob die Spalte mit dem Zeitstempel einen Index hat. Bei mir ist das der Fall (rund 98 Millionen Zeilen), die Abfrage ist unkritisch. Wie ich die Datenbank übrigens von 75 auf 35 GB verkleinert habe, steht in einem früheren Beitrag.
Reparaturen: erst aufräumen, dann überwachen
Home Assistant sammelt unter „Reparaturen“ alle Probleme der Integrationen. Das wollte ich in die Ampel aufnehmen, ein Blick in die Liste zeigte aber: Altlasten. Mehrere Meldungen stammten von Helfern, deren Quellen es nicht mehr gab. Hätte ich die Reparaturen sofort eingebunden, wäre die Ampel dauerhaft gelb gewesen. Also habe ich zuerst aufgeräumt und die Reparaturen erst danach in die Auswertung genommen. Abgefragt werden sie über die Aktion repairs.list, die die Integration Spook bereitstellt. Fehler und kritische Meldungen zählen als kritisch, der Rest als Warnung.
🧩 Der Aufbau des Packages
Das Ganze liegt in einer Datei im Ordner /config/packages/. Sie ist so aufgebaut, dass ich im Alltag nur den oberen Teil anfasse:
| Teil | Inhalt |
|---|---|
| 1. Prüfliste | Schwellwerte und Listen: Apps, Geräte, Batterien, Flags, Entfeuchter, Festplatte, Updates. Nur hier pflege ich. |
| 2. Auswertung | Ein Template, das aus der Prüfliste die Befundliste baut. Normalerweise nicht ändern. |
| 3. Sensoren | Status (ok/warnung/kritisch), Level (0/1/2), Zusammenfassung, Reparaturen, Recorder-Alter, Zeit der letzten Auswertung |
| 4. SQL-Sensor | Schreibt der Recorder noch? |
| 5. Script | Zentrales Benachrichtigungsziel |
| 6. Automationen | Sofort-Push, Entwarnung, Tagesreport, Wächter |
Das Schöne an dieser Aufteilung: Ein neues Gerät aufzunehmen heißt, eine Zeile in die Prüfliste zu schreiben. Ein Beispiel für die Gruppe der kritischen Apps:
# -----------------------------------------------------
# PRÜFLISTE - hier pflegen
# -----------------------------------------------------
# Schwellwerte
backup_max_h: 36 # Backup älter als x Stunden = kritisch
recorder_max_s: 300 # Recorder schreibt seit x Sekunden nichts = kritisch
disk_warn_pct: 20 # freier Plattenplatz unter x % = Warnung
disk_crit_pct: 10 # freier Plattenplatz unter x % = kritisch
# Kritische Apps: alles ausser "on" = kritisch (auch unavailable)
apps:
- {name: Mosquitto, entity: binary_sensor.mosquitto_broker_lauft}
- {name: Cloudflared, entity: binary_sensor.cloudflared_lauft}
- {name: Matter Server, entity: binary_sensor.matter_server_aktiv}
- {name: Matter Hub, entity: binary_sensor.home_assistant_matter_hub_aktiv}
- {name: Vaultwarden, entity: binary_sensor.vaultwarden_bitwarden_aktiv}
- {name: ESPHome Device Builder, entity: binary_sensor.esphome_device_builder_aktiv}
- {name: Grafana, entity: binary_sensor.grafana_aktiv}
# Geräte mit Lebenszeichen: unavailable/unknown = Befund (sev laut Eintrag)
devices:
# ESPHome (Wohnzimmer-Test-Sensor laeuft im Dauerbetrieb, aber nur Info)
- {name: ESPHome Praesenz Flur, entity: sensor.prasenz_flur_temperatur, sev: 1}
- {name: ESPHome Test-Sensor WZ, entity: sensor.test_sensor_temperatur, sev: 0}
# Reolink
- {name: Kamera Einfahrt, entity: sensor.einfahrt_tag_nacht_zustand, sev: 1}
- {name: Kamera Gartenfreisitz, entity: sensor.gartenfreisitz_tag_nacht_zustand, sev: 1}
- {name: Kamera Gartengarage, entity: sensor.gartengarage_tag_nacht_zustand, sev: 1}
- {name: Tuerklingel V8, entity: sensor.v8_doorbell_wlan_signal, sev: 1}
# Wasseralarm: tot = unsicher, deshalb kritisch
- {name: Wassersensor Kueche, entity: binary_sensor.kuche_floos_sensor_kuche_uberflutung, sev: 2}
- {name: Wassersensor Keller, entity: binary_sensor.keller_floos_sensor_keller_uberflutung, sev: 2}
# Batterien (Prozent). stale_h: Befund, wenn der Sensor so lange nichts gemeldet hat
batteries:
- {name: Temp Spitzboden, entity: sensor.shellyhtg3_543204575c64_battery, warn: 10, stale_h: 24}
- {name: Temp 1.OG, entity: sensor.shellyhtg3_543204575c94_battery, warn: 10, stale_h: 24}
- {name: Temp Keller, entity: sensor.shellyhtg3_54320462b098_battery, warn: 10, stale_h: 24}
- {name: Morpheus Dual Control, entity: sensor.morpheus_reich_dual_control_battery_level, warn: 10}
# Floos melden sich nur bei Wasser oder niedrigem Batteriestand: kein stale_h
- {name: Wassersensor Kueche, entity: sensor.kuche_floos_sensor_kuche_batterie, warn: 20}
- {name: Wassersensor Keller, entity: sensor.keller_floos_sensor_keller_batterie, warn: 20}
# Einzelzustände: bei bad_state (klein geschrieben verglichen) entsteht ein Befund
flags:
- {name: Ventil Wohnzimmer, entity: binary_sensor.wohnzimmer_wohnzimmer_batteriestand, bad_state: "on", text: Ventil Wohnzimmer - Batterie schwach, sev: 1}
- {name: Ventil Ankleide, entity: binary_sensor.ankleide_ankleide_batteriestand, bad_state: "on", text: Ventil Ankleide - Batterie schwach, sev: 1}
- {name: Ventil Schlafzimmer, entity: binary_sensor.schlafzimmer_schlafzimmer_batteriestand, bad_state: "on", text: Ventil Schlafzimmer - Batterie schwach, sev: 1}
- {name: Ventil Küche, entity: binary_sensor.kuche_kuche_batteriestand, bad_state: "on", text: Ventil Küche - Batterie schwach, sev: 1}
- {name: Haustür, entity: binary_sensor.haustur_batteriestand, bad_state: "on", text: Haustür - Batterie schwach, sev: 1}
- {name: Keller, entity: binary_sensor.keller_batteriestand, bad_state: "on", text: Keller - Batterie schwach, sev: 1}
- {name: Fahrradhaus, entity: binary_sensor.fahrradhaus_batteriestand, bad_state: "on", text: Fahrradhaus - Batterie schwach, sev: 1}
# Midea-Entfeuchter: unavailable ist normal, wenn die Steckdose aus ist
midea:
humidity: sensor.dehumidifier_3106_humidity
socket: switch.keller_steckdose_17
tank_full: binary_sensor.dehumidifier_3106_tank_full
replace_filter: binary_sensor.dehumidifier_3106_replace_filter
# Host-Festplatte (alle drei Sensoren muessen dieselbe Einheit haben)
disk:
free: sensor.home_assistant_host_disk_free
total: sensor.home_assistant_host_disk_total
# Updates: nur Info
updates:
- {name: Home Assistant Core, entity: update.home_assistant_core_update}
- {name: Home Assistant OS, entity: update.home_assistant_operating_system_update}
- {name: Home Assistant Supervisor, entity: update.home_assistant_supervisor_update}
- {name: Firmware Einfahrt, entity: update.einfahrt_firmware}
- {name: Firmware Gartenfreisitz, entity: update.gartenfreisitz_firmware}
- {name: Firmware Gartengarage, entity: update.gartengarage_firmware}
- {name: Firmware Tuerklingel, entity: update.v8_doorbell_firmware}Die Auswertung läuft über einen trigger-basierten Template-Sensor, der alle zwei Minuten und beim Start von Home Assistant feuert. Er geht die Listen durch und sammelt alles, was nicht stimmt, in einer Befundliste. Daraus werden dann Status, Level und Zusammenfassung abgeleitet.
template:
# Reparaturen: repairs.list (Spook) alle 10 Minuten abfragen.
# State = Anzahl nicht ignorierter Meldungen, Details in den Attributen.
- triggers:
- trigger: time_pattern
minutes: "/10"
- trigger: homeassistant
event: start
actions:
- action: repairs.list
response_variable: rep
sensor:
- name: "Health Reparaturen"
unique_id: health_v6_repairs
state: "{{ rep.issues | rejectattr('ignored') | list | count }}"
icon: mdi:wrench-clock
attributes:
texts: >-
{{ rep.issues | rejectattr('ignored') | map(attribute='title') | list }}
critical_texts: >-
{{ rep.issues | rejectattr('ignored') | selectattr('severity', 'in', ['critical', 'error']) | map(attribute='title') | list }}
- triggers:
- trigger: time_pattern
minutes: "/2"
- trigger: homeassistant
event: start
variables:
# -----------------------------------------------------
# PRÜFLISTE - hier pflegen
# -----------------------------------------------------
# Schwellwerte
backup_max_h: 36 # Backup älter als x Stunden = kritisch
recorder_max_s: 300 # Recorder schreibt seit x Sekunden nichts = kritisch
disk_warn_pct: 20 # freier Plattenplatz unter x % = Warnung
disk_crit_pct: 10 # freier Plattenplatz unter x % = kritisch
# Kritische Apps: alles ausser "on" = kritisch (auch unavailable)
apps:
- {name: Mosquitto, entity: binary_sensor.mosquitto_broker_lauft}
- {name: Cloudflared, entity: binary_sensor.cloudflared_lauft}
- {name: Matter Server, entity: binary_sensor.matter_server_aktiv}
- {name: Matter Hub, entity: binary_sensor.home_assistant_matter_hub_aktiv}
- {name: Vaultwarden, entity: binary_sensor.vaultwarden_bitwarden_aktiv}
- {name: ESPHome Device Builder, entity: binary_sensor.esphome_device_builder_aktiv}
- {name: Grafana, entity: binary_sensor.grafana_aktiv}
# Geräte mit Lebenszeichen: unavailable/unknown = Befund (sev laut Eintrag)
devices:
# ESPHome (Wohnzimmer-Test-Sensor laeuft im Dauerbetrieb, aber nur Info)
- {name: ESPHome Praesenz Flur, entity: sensor.prasenz_flur_temperatur, sev: 1}
- {name: ESPHome Test-Sensor WZ, entity: sensor.test_sensor_temperatur, sev: 0}
# Reolink
- {name: Kamera Einfahrt, entity: sensor.einfahrt_tag_nacht_zustand, sev: 1}
- {name: Kamera Gartenfreisitz, entity: sensor.gartenfreisitz_tag_nacht_zustand, sev: 1}
- {name: Kamera Gartengarage, entity: sensor.gartengarage_tag_nacht_zustand, sev: 1}
- {name: Tuerklingel V8, entity: sensor.v8_doorbell_wlan_signal, sev: 1}
# Wasseralarm: tot = unsicher, deshalb kritisch
- {name: Wassersensor Kueche, entity: binary_sensor.kuche_floos_sensor_kuche_uberflutung, sev: 2}
- {name: Wassersensor Keller, entity: binary_sensor.keller_floos_sensor_keller_uberflutung, sev: 2}
# Batterien (Prozent). stale_h: Befund, wenn der Sensor so lange nichts gemeldet hat
batteries:
- {name: Temp Spitzboden, entity: sensor.shellyhtg3_543204575c64_battery, warn: 10, stale_h: 24}
- {name: Temp 1.OG, entity: sensor.shellyhtg3_543204575c94_battery, warn: 10, stale_h: 24}
- {name: Temp Keller, entity: sensor.shellyhtg3_54320462b098_battery, warn: 10, stale_h: 24}
- {name: Morpheus Dual Control, entity: sensor.morpheus_reich_dual_control_battery_level, warn: 10}
# Floos melden sich nur bei Wasser oder niedrigem Batteriestand: kein stale_h
- {name: Wassersensor Kueche, entity: sensor.kuche_floos_sensor_kuche_batterie, warn: 20}
- {name: Wassersensor Keller, entity: sensor.keller_floos_sensor_keller_batterie, warn: 20}
# Einzelzustände: bei bad_state (klein geschrieben verglichen) entsteht ein Befund
flags:
- {name: Ventil Wohnzimmer, entity: binary_sensor.wohnzimmer_wohnzimmer_batteriestand, bad_state: "on", text: Ventil Wohnzimmer - Batterie schwach, sev: 1}
- {name: Ventil Ankleide, entity: binary_sensor.ankleide_ankleide_batteriestand, bad_state: "on", text: Ventil Ankleide - Batterie schwach, sev: 1}
- {name: Ventil Schlafzimmer, entity: binary_sensor.schlafzimmer_schlafzimmer_batteriestand, bad_state: "on", text: Ventil Schlafzimmer - Batterie schwach, sev: 1}
- {name: Ventil Küche, entity: binary_sensor.kuche_kuche_batteriestand, bad_state: "on", text: Ventil Küche - Batterie schwach, sev: 1}
- {name: Haustür, entity: binary_sensor.haustur_batteriestand, bad_state: "on", text: Haustür - Batterie schwach, sev: 1}
- {name: Keller, entity: binary_sensor.keller_batteriestand, bad_state: "on", text: Keller - Batterie schwach, sev: 1}
- {name: Fahrradhaus, entity: binary_sensor.fahrradhaus_batteriestand, bad_state: "on", text: Fahrradhaus - Batterie schwach, sev: 1}
# Midea-Entfeuchter: unavailable ist normal, wenn die Steckdose aus ist
midea:
humidity: sensor.dehumidifier_3106_humidity
socket: switch.keller_steckdose_17
tank_full: binary_sensor.dehumidifier_3106_tank_full
replace_filter: binary_sensor.dehumidifier_3106_replace_filter
# Host-Festplatte (alle drei Sensoren muessen dieselbe Einheit haben)
disk:
free: sensor.home_assistant_host_disk_free
total: sensor.home_assistant_host_disk_total
# Updates: nur Info
updates:
- {name: Home Assistant Core, entity: update.home_assistant_core_update}
- {name: Home Assistant OS, entity: update.home_assistant_operating_system_update}
- {name: Home Assistant Supervisor, entity: update.home_assistant_supervisor_update}
- {name: Firmware Einfahrt, entity: update.einfahrt_firmware}
- {name: Firmware Gartenfreisitz, entity: update.gartenfreisitz_firmware}
- {name: Firmware Gartengarage, entity: update.gartengarage_firmware}
- {name: Firmware Tuerklingel, entity: update.v8_doorbell_firmware}
# -----------------------------------------------------
# AUSWERTUNG - normalerweise nicht aendern
# -----------------------------------------------------
findings: >-
{%- set ns = namespace(f=[]) -%}
{%- set bad = ['unavailable', 'unknown', 'none', ''] -%}
{#- Apps -#}
{%- for a in apps -%}
{%- if states(a.entity) != 'on' -%}
{%- set ns.f = ns.f + [{'sev': 2, 'text': a.name ~ ' nicht aktiv (' ~ states(a.entity) ~ ')'}] -%}
{%- endif -%}
{%- endfor -%}
{#- Backup: Alter und fehlgeschlagener Versuch -#}
{%- set last = states('sensor.backup_letztes_erfolgreiches_automatisches_backup') -%}
{%- set tried = states('sensor.backup_zuletzt_versuchtes_automatisches_backup') -%}
{%- if last in bad -%}
{%- set ns.f = ns.f + [{'sev': 1, 'text': 'Backup-Zeitstempel ohne Wert'}] -%}
{%- else -%}
{%- set age = (now() - as_datetime(last)).total_seconds() / 3600 -%}
{%- if age > backup_max_h -%}
{%- set ns.f = ns.f + [{'sev': 2, 'text': 'Backup zu alt (' ~ (age | round(0) | int) ~ ' h)'}] -%}
{%- endif -%}
{%- if tried not in bad and states('sensor.backup_backup_manager_zustand') == 'idle' and as_datetime(tried) > as_datetime(last) -%}
{%- set ns.f = ns.f + [{'sev': 2, 'text': 'Letztes Backup fehlgeschlagen'}] -%}
{%- endif -%}
{%- endif -%}
{#- Recorder / Datenbank -#}
{%- set rec = states('sensor.health_recorder_alter') -%}
{%- if rec in bad -%}
{%- set ns.f = ns.f + [{'sev': 2, 'text': 'Recorder/Datenbank liefert keine Schreibzeit'}] -%}
{%- elif rec | int(0) > recorder_max_s -%}
{%- set ns.f = ns.f + [{'sev': 2, 'text': 'Recorder schreibt seit ' ~ (rec | int(0) // 60) ~ ' min nichts'}] -%}
{%- endif -%}
{#- Host-Festplatte -#}
{%- set d_free = states(disk.free) -%}
{%- set d_total = states(disk.total) -%}
{%- if d_free in bad or d_total in bad or d_total | float(0) <= 0 -%}
{%- set ns.f = ns.f + [{'sev': 1, 'text': 'Disk-Sensor ohne Wert'}] -%}
{%- else -%}
{%- set pct = (d_free | float(0)) / (d_total | float(1)) * 100 -%}
{%- if pct < disk_crit_pct -%}
{%- set ns.f = ns.f + [{'sev': 2, 'text': 'Festplatte fast voll (' ~ (pct | round(0) | int) ~ ' % frei)'}] -%}
{%- elif pct < disk_warn_pct -%}
{%- set ns.f = ns.f + [{'sev': 1, 'text': 'Festplatte wird voll (' ~ (pct | round(0) | int) ~ ' % frei)'}] -%}
{%- endif -%}
{%- endif -%}
{#- Geraete mit Lebenszeichen -#}
{%- for dev in devices -%}
{%- if states(dev.entity) in ['unavailable', 'unknown'] -%}
{%- set ns.f = ns.f + [{'sev': dev.sev, 'text': dev.name ~ ' offline'}] -%}
{%- endif -%}
{%- endfor -%}
{#- Batterien: niedrig, ohne Wert, lange nichts gemeldet -#}
{%- for b in batteries -%}
{%- set v = states(b.entity) -%}
{%- if v in bad -%}
{%- set ns.f = ns.f + [{'sev': 1, 'text': 'Batterie ' ~ b.name ~ ' ohne Wert'}] -%}
{%- else -%}
{%- if v | float(100) < b.warn -%}
{%- set ns.f = ns.f + [{'sev': 1, 'text': 'Batterie ' ~ b.name ~ ' niedrig (' ~ (v | float(0) | round(0) | int) ~ ' %)'}] -%}
{%- endif -%}
{%- if b.stale_h is defined -%}
{%- set so = expand(b.entity) | list -%}
{%- if so | count > 0 and (now() - so[0].last_reported).total_seconds() > b.stale_h * 3600 -%}
{%- set ns.f = ns.f + [{'sev': 1, 'text': b.name ~ ' meldet sich nicht (>' ~ b.stale_h ~ ' h)'}] -%}
{%- endif -%}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{#- Einzelzustaende -#}
{%- for fl in flags -%}
{%- if states(fl.entity) | lower == fl.bad_state -%}
{%- set ns.f = ns.f + [{'sev': fl.sev, 'text': fl.text}] -%}
{%- endif -%}
{%- endfor -%}
{#- Midea-Entfeuchter -#}
{%- if states(midea.humidity) in bad -%}
{%- if states(midea.socket) == 'on' -%}
{%- set ns.f = ns.f + [{'sev': 1, 'text': 'Midea-Entfeuchter offline (Steckdose an)'}] -%}
{%- elif states(midea.socket) in bad -%}
{%- set ns.f = ns.f + [{'sev': 1, 'text': 'Midea-Entfeuchter offline, Steckdose ohne Wert'}] -%}
{%- endif -%}
{%- else -%}
{%- if is_state(midea.tank_full, 'on') -%}
{%- set ns.f = ns.f + [{'sev': 1, 'text': 'Midea-Entfeuchter: Tank voll'}] -%}
{%- endif -%}
{%- if is_state(midea.replace_filter, 'on') -%}
{%- set ns.f = ns.f + [{'sev': 1, 'text': 'Midea-Entfeuchter: Filter wechseln'}] -%}
{%- endif -%}
{%- endif -%}
{#- Reparaturen (Spook repairs.list): kritisch/Fehler = 2, Rest = 1 -#}
{%- set rep_n = states('sensor.health_reparaturen') -%}
{%- if rep_n in bad -%}
{%- set ns.f = ns.f + [{'sev': 1, 'text': 'Reparaturen-Abfrage ohne Wert'}] -%}
{%- else -%}
{%- set crit = state_attr('sensor.health_reparaturen', 'critical_texts') | default([], true) -%}
{%- for t in state_attr('sensor.health_reparaturen', 'texts') | default([], true) -%}
{%- set ns.f = ns.f + [{'sev': 2 if t in crit else 1, 'text': 'Reparatur: ' ~ t}] -%}
{%- endfor -%}
{%- endif -%}
{#- Updates (Info) -#}
{%- for u in updates -%}
{%- if is_state(u.entity, 'on') -%}
{%- set ns.f = ns.f + [{'sev': 0, 'text': 'Update: ' ~ u.name}] -%}
{%- endif -%}
{%- endfor -%}
{{- ns.f -}}
red_list: "{{ findings | selectattr('sev', 'eq', 2) | map(attribute='text') | list }}"
yellow_list: "{{ findings | selectattr('sev', 'eq', 1) | map(attribute='text') | list }}"
info_list: "{{ findings | selectattr('sev', 'eq', 0) | map(attribute='text') | list }}"
level: "{{ 2 if red_list | count > 0 else (1 if yellow_list | count > 0 else 0) }}"
sensor:
- name: "Health Status"
unique_id: health_v2_status
state: >-
{{ 'kritisch' if level == 2 else ('warnung' if level == 1 else 'ok') }}
icon: >-
{{ 'mdi:alert-octagon' if level == 2 else ('mdi:alert' if level == 1 else 'mdi:check-circle') }}
attributes:
level: "{{ level }}"
red_count: "{{ red_list | count }}"
yellow_count: "{{ yellow_list | count }}"
info_count: "{{ info_list | count }}"
red_text: "{{ red_list | join(' | ') }}"
yellow_text: "{{ yellow_list | join(' | ') }}"
info_text: "{{ info_list | join(' | ') }}"
findings: "{{ findings }}"
# Numerisch fuer Grafana: 0 = ok, 1 = Warnung, 2 = kritisch
- name: "Health Level"
unique_id: health_v2_level
state: "{{ level }}"
state_class: measurement
icon: mdi:traffic-light
# Text fuer Karten/Pushes (State max. 255 Zeichen, deshalb gekuerzt)
- name: "Health Zusammenfassung"
unique_id: health_v2_summary
state: >-
{%- set all = red_list + yellow_list -%}
{{- 'Keine Auffälligkeiten' if all | count == 0 else (all | join(' | '))[:250] -}}
icon: mdi:text-box-outline
- name: "Health Auswertung Zeit"
unique_id: health_v2_timestamp
device_class: timestamp
state: "{{ now().isoformat() }}"
icon: mdi:clock-check
#########################################################
# 3. SQL-Sensor: Alter des letzten Datenbank-Eintrags in Sekunden
# Ohne db_url nutzt die Integration die Recorder-Datenbank.
# Keine Zugangsdaten in dieser Datei.
# Annahme: Schema mit Spalte states.last_updated_ts (HA ab 2023.4).
# Kein Eintrag in der letzten Stunde -> Wert leer -> Health meldet kritisch.
#########################################################
sql:
- name: "Health Recorder Alter"
unique_id: health_v2_recorder_age
query: >-
SELECT ROUND(UNIX_TIMESTAMP() - MAX(last_updated_ts)) AS age
FROM states
WHERE last_updated_ts > UNIX_TIMESTAMP() - 3600;
column: "age"
unit_of_measurement: "s"
#########################################################
# 4. Script: zentrales Notify-Ziel
#########################################################
script:
health_notify:
alias: "Health - Nachricht an Rene"
fields:
title:
description: Titel
message:
description: Text
critical:
description: true = kritische iOS-Mitteilung (umgeht Nicht stören)
sequence:
- action: notify.mobile_app_relucas_phone_2
data:
title: "{{ title }}"
message: "{{ message }}"
data:
push:
sound:
name: default
critical: "{{ 1 if critical | default(false) else 0 }}"
volume: 1.0
mode: queued
#########################################################
# 5. Automationen
#########################################################
automation:
# Kritisch: sofort, wenn der Zustand 5 Minuten anhaelt.
# Feuert auch erneut, wenn sich die Liste der kritischen Befunde aendert.
- id: health_v2_critical
alias: "HA Health Agent - Kritisch"
triggers:
- trigger: state
entity_id: sensor.health_status
attribute: red_text
for: "00:05:00"
conditions:
- condition: template
value_template: "{{ state_attr('sensor.health_status', 'red_text') | default('', true) != '' }}"
actions:
- action: script.health_notify
data:
title: "🔴 Smart Home kritisch"
message: "{{ state_attr('sensor.health_status', 'red_text') }}"
critical: true
mode: single
# Entwarnung: kein kritischer Befund mehr seit 5 Minuten
- id: health_v2_all_clear
alias: "HA Health Agent - Entwarnung"
triggers:
- trigger: state
entity_id: sensor.health_status
from: "kritisch"
to:
- "ok"
- "warnung"
for: "00:05:00"
actions:
- action: script.health_notify
data:
title: "✅ Smart Home wieder in Ordnung"
message: >-
Status: {{ states('sensor.health_status') }}.
{{ states('sensor.health_zusammenfassung') }}
mode: single
# Wächter: Auswertung haengt (Zeitstempel aelter als 10 Minuten oder ohne Wert).
# now() im Template laesst den Trigger jede Minute neu pruefen.
- id: health_v7_stale
alias: "HA Health Agent - Auswertung haengt"
triggers:
- trigger: template
value_template: >-
{{ now().timestamp() - as_timestamp(states('sensor.health_auswertung_zeit'), 0) > 600 }}
for: "00:02:00"
actions:
- action: script.health_notify
data:
title: "🔴 Health-Agent hängt"
message: >-
Die Auswertung wurde seit über 10 Minuten nicht aktualisiert
(letzte: {{ states('sensor.health_auswertung_zeit') }}).
Die Ampel ist solange nicht verlässlich.
critical: true
mode: single
# Wächter: Auswertung laeuft wieder
- id: health_v7_stale_clear
alias: "HA Health Agent - Auswertung läuft wieder"
triggers:
- trigger: template
value_template: >-
{{ now().timestamp() - as_timestamp(states('sensor.health_auswertung_zeit'), 0) <= 600 }}
for: "00:02:00"
conditions:
- condition: template
value_template: >-
{{ (as_timestamp(states.automation.ha_health_agent_auswertung_haengt.last_triggered, 0) or 0)
> (as_timestamp(states.automation.ha_health_agent_auswertung_lauft_wieder.last_triggered, 0) or 0) }}
actions:
- action: script.health_notify
data:
title: "✅ Health-Agent läuft wieder"
message: "Die Auswertung wird wieder aktualisiert."
mode: single
# Tagesreport 07:00
- id: health_v2_daily_report
alias: "HA Health Agent - Tagesreport"
triggers:
- trigger: time
at: "07:00:00"
actions:
- action: script.health_notify
data:
title: >-
{{ {'ok': '🟢', 'warnung': '🟡', 'kritisch': '🔴'}[states('sensor.health_status')] | default('⚪') }}
Smart Home {{ states('sensor.health_status') }}
message: >-
{{ states('sensor.health_zusammenfassung') }}
{%- set info = state_attr('sensor.health_status', 'info_text') | default('', true) %}
{%- if info != '' %}
Info: {{ info }}
{%- endif %}
critical: false
mode: single
⚠️ Wichtig beim Einspielen einer neuen Version: Die alte Datei ersetzen, nicht daneben legen. Sonst gibt es doppelte unique_ids und zwei Tagesreports um 07:00. Weil die unique_ids über alle Versionen gleich bleiben, behalten die Entitäten ihren Verlauf.
🔔 Die Benachrichtigungen
Informiert wird ausschließlich ich, nicht die ganze Familie. Es gibt vier Automationen:
- 🚨 Sofort-Push (kritisch): Der kritische Zustand hält 5 Minuten an. Der Push wird erneut gesendet, wenn sich die Liste der kritischen Befunde ändert, und durchbricht „Nicht stören“.
- ✅ Entwarnung: Seit 5 Minuten ist nichts Kritisches mehr da.
- ☀️ Tagesreport um 07:00: Ampel, Warnungen und Infos als Zusammenfassung zum Start in den Tag
- 🐕 Wächter: Wer überwacht den Überwacher? Wenn die Auswertung selbst länger als 10 Minuten hängt, bekomme ich einen kritischen Push, danach ebenfalls eine Entwarnung.


📷 Push-Nachricht und Tagesreport auf dem Handy
Der Wächter beruht auf dem Sensor sensor.health_auswertung_zeit, der bei jeder Auswertung aktualisiert wird. Ist er älter als 10 Minuten, hängt etwas. Er fängt allerdings nicht ab, wenn Home Assistant komplett steht, denn dann läuft auch der Wächter nicht mehr. Dafür braucht es einen Heartbeat von außen, den ich separat löse und der nicht Teil des Packages ist.
📊 Das Dashboard
Auf der Ansicht „System“ meines Dashboards gibt es eine eigene Karte im selben Button-Stil wie der Rest meiner Oberfläche:
- 🚦 Ein Ampel-Button mit den Zählern für Kritisch, Warnung und Info
- 📋 Die Befundliste im Klartext: was genau ist das Problem?
- 🔧 Eine Zeile mit den offenen Reparaturen
- ⏱️ Eine Anzeige zur Aktualität der Auswertung: grün, solange sie jünger als 10 Minuten ist, sonst rot

📷 Dashboard-Karte im Browser
- type: heading
heading: Smart Home Health
heading_style: title
- type: custom:button-card
entity: sensor.health_status
name: Health
show_icon: true
tap_action:
action: more-info
state_display: |
[[[
const a = entity.attributes;
const r = parseInt(a.red_count) || 0;
const y = parseInt(a.yellow_count) || 0;
if (entity.state === 'ok') return 'in Ordnung';
const parts = [];
if (r) parts.push(r + ' kritisch');
if (y) parts.push(y + (y === 1 ? ' Warnung' : ' Warnungen'));
return parts.length ? parts.join(', ') : entity.state;
]]]
styles:
card:
- height: 60px
- padding: 6px 10px
- border-radius: 10px
- border: 2px solid
- cursor: pointer
- background: >
[[[ return ({ok: '#2e7d32', warnung: '#f9a825', kritisch:
'#c62828'})[entity.state] || '#424242'; ]]]
grid:
- grid-template-areas: '"i n" "i s"'
- grid-template-columns: 32px auto
- grid-template-rows: auto auto
- align-items: center
- justify-items: start
- column-gap: 8px
icon:
- width: 22px
- height: 22px
- color: >
[[[ return entity.state === 'warnung' ? '#3e2f00' :
'#ffffff'; ]]]
name:
- font-size: 14px
- font-weight: 600
- justify-self: start
- color: >
[[[ return entity.state === 'warnung' ? '#3e2f00' :
'#ffffff'; ]]]
state:
- font-size: 12px
- opacity: 0.9
- justify-self: start
- color: >
[[[ return entity.state === 'warnung' ? '#3e2f00' :
'#ffffff'; ]]]
- type: markdown
content: |-
{% set s = 'sensor.health_status' %}
{% for t in (state_attr(s, 'red_text') or '').split(' | ') if t %}
🔴 {{ t }}
{% endfor %}
{% for t in (state_attr(s, 'yellow_text') or '').split(' | ') if t %}
🟡 {{ t }}
{% endfor %}
{% for t in (state_attr(s, 'info_text') or '').split(' | ') if t %}
ℹ️ {{ t }}
{% endfor %}
{% if not state_attr(s, 'red_text') and not state_attr(s, 'yellow_text') and not state_attr(s, 'info_text') %}
Keine Auffälligkeiten
{% endif %}
- type: markdown
content: >-
{% set t = states('sensor.health_auswertung_zeit') %}
{% if t in ['unavailable', 'unknown', 'none', ''] %}
🔴 **Health-Auswertung ohne Zeitstempel**
{% elif (now() - as_datetime(t)).total_seconds() > 600 %}
🔴 **Health-Auswertung veraltet** (zuletzt vor {{ ((now() -
as_datetime(t)).total_seconds() // 60) | int }} min)
{% else %}
🟢 Health-Auswertung aktuell (vor {{ ((now() -
as_datetime(t)).total_seconds() // 60) | int }} min)
{% endif %}
- type: entities
entities:
- entity: sensor.health_auswertung_zeit
name: Letzte Auswertung
- entity: sensor.health_reparaturen
name: Offene Reparaturen
- entity: sensor.health_recorder_alter
name: Recorder-Alter
- entity: sensor.health_level
name: Level (0/1/2)Die Karte ist so gebaut, dass sie auch auf dem Handy in der App gut lesbar bleibt. Ein Blick genügt: grün heißt nichts zu tun, gelb heißt bei Gelegenheit ansehen, rot heißt sofort
🛤️ Wie es dazu kam: Von V1 zu V8
Der Health-Check ist nicht in einem Zug entstanden, sondern in acht Versionen. Ein paar Stationen:
| Version | Was neu war |
|---|---|
| V1 → V2 | Ampel und Befundliste statt Prozent-Score, Prüfliste als zentrale Pflegestelle |
| V3 / V4 | Batterie-Schwellen angepasst, Flags für die Ventile |
| V5 | Flags in der richtigen Namensform, neue Flags für Türkontakte, Wassersensor-Batterien |
| V6 | Reparaturen als eigener Sensor und in der Ampel |
| V7 | Wächter: „Auswertung hängt“ und „läuft wieder“ |
| V8 | Weitere Apps unter Überwachung: Vaultwarden, ESPHome Device Builder, Grafana |
Stolpersteine unterwegs
- 🔤 Entitätsnamen stimmen nicht mit der Erwartung überein. Meine Ventile heißen
ankleide_ankleide_batteriestand, also der Name doppelt. Die Flags für Wohnzimmer, Schlafzimmer und Küche hatte ich zunächst in der einfachen Form angelegt, sie standen dauerhaft aufunknownund konnten nie auslösen. Ein Befund, der nie auslösen kann, ist schlimmer als keiner, weil er Sicherheit vortäuscht. Seither prüfe ich jede neue Entität in den Entwicklerwerkzeugen unter „Zustände“, bevor ich sie aufnehme. - 🔋 Ein Wert, der nicht stimmt, aber trotzdem da ist. Die Batterie-Typ-Sensoren meiner Ventile stehen alle auf „Replace Batteries“, ob als echter Alarm oder als Dauerwert, ist unklar. Ich habe sie deshalb nicht genutzt.
- 🧹 Altlasten aus früheren Versionen. Ein Sensor der Vorversion stand auf
unavailableund musste gelöscht werden, und die Entitäten der Apps waren zunächst deaktiviert. - 🔢 Automations-IDs mit Umlauten. Aus „hängt“ wird in der Entity-ID
haengt, aus „läuft“ wirdlauft. Eine falsch geschriebene ID in der Entwarnungs-Bedingung führt dazu, dass die Entwarnung nie kommt. Auch das gehört in die Rubrik „prüfen, nicht annehmen“.
🙈 Was ich bewusst nicht überwache
Mindestens so wichtig wie die Liste der Checks ist die Liste dessen, was draußen bleibt. Sonst wird die Ampel dauerhaft gelb und verliert ihren Wert:
- 🤖 Der Mähroboter: wird ersetzt und ist ohnehin offline
- 🌐 Die Internet-Access-Schalter der FRITZ!Box: die überarbeite ich gerade
- 🔌 Zwei Steckdosen mit Geräten, die ich nur saisonal nutze
- 📱 Handys und Browser-Mod-Batterien
- 💀 Der komplette Ausfall von Home Assistant selbst, siehe Wächter
🧪 Was noch ungetestet ist
Ehrlich gesagt: Der Health-Check läuft, die Ampel ist im Alltag zuverlässig, und die Konfigurationsprüfung zeigt keine Fehler. Einige Fälle habe ich aber noch nicht im Ernstfall erlebt:
- Ein echter Alarm der Ventil-Flags. Alle stehen derzeit auf
off, ob die doppelte Namensform tatsächlich auslöst, zeigt erst eine schwache Batterie. - Die Entwarnung des Wächters
- Ein möglicher Fehlalarm des Wächters nach einem Neustart von Home Assistant. Sollte er auftreten, erhöhe ich die Wartezeit von 2 Minuten.
Deshalb habe ich eine Störung gezielt nachgestellt und geprüft, ob der Health-Check sie erkennt. Das Ergebnis: Die entsprechenden Fehler wurden gemeldet. Ein Monitoring, das man nie ausgelöst hat, ist eine Annahme, kein Beweis. Jetzt ist es ein Beweis.
🚀 Was noch möglich wäre
- 📈 Batterieprognose: Nicht nur „unter 10 %“, sondern „in drei Wochen leer“
- 💾 Plattenplatz der Datenbank-VM (bei mir auf Proxmox) mit überwachen
- 🔁 Wackelkandidaten: Geräte, die immer wieder kurz ausfallen, aus der SQL-Auswertung gezielt in die Prüfliste aufnehmen
- 💓 Externer Heartbeat, damit auch der Totalausfall von Home Assistant auffällt
- 📊 Grafana-Verlauf der Ampel über 30 Tage. Das habe ich vorerst verworfen, weil ich keinen echten Bedarf sehe.
✅ Fazit und Lessons Learned
Seit der Health-Check läuft, schaue ich nicht mehr „nach dem Rechten“, sondern lasse mir Bescheid geben. Das ist ein ganz anderes Gefühl: Ist die Ampel grün, kann ich mich darauf verlassen. Das nehme ich mit:
- 🚦 Eine Ampel schlägt einen Score. „Grün, Gelb, Rot“ versteht jeder sofort, „87 %“ sagt nichts.
- 🔕 Weniger Meldungen sind mehr. Push nur bei kritisch und nach Wartezeit, der Rest im Tagesreport. Sonst stellt man das System irgendwann ab.
- 📝 Eine Datei, eine Pflegestelle. Wenn ein neues Gerät eine Zeile kostet, wird es auch eingetragen.
- 🔎 Prüfe jede Entität, bevor du sie aufnimmst. Ein Check auf eine falsch benannte Entität löst nie aus und täuscht Sicherheit vor.
- 🧹 Erst aufräumen, dann überwachen. Sonst ist die Ampel von Anfang an gelb, und niemand schaut mehr hin.
- 🐕 Auch der Überwacher braucht einen Wächter. Und selbst der fängt den Totalausfall nicht ab, dafür braucht es etwas außerhalb von Home Assistant.
- 🧪 Löse den Ernstfall einmal absichtlich aus. Erst dann weißt du, dass das Monitoring funktioniert.
Überwachst du dein Smart Home auch aktiv? Dann erzähl mir in den Kommentaren, was bei dir auf der Liste steht und welche Ausfälle dich schon überrascht haben.
Wenn dir dieses Projekt gefallen hat, schau dir auch meine weiteren Smart-Home- und Home-Assistant-Projekte an. Dort findest du viele weitere Ideen zum Nachbauen.

Vielen Dank für diesen hilfreichen Beitrag. Die Idee mit der Ampel-Logik finde ich klasse. Ein paar Tipps zur Integration von Zigbee wären noch toll.