Schlagwortarchiv: Package

Home Assistant Health-Check

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:

StufeAmpelBedeutungWas passiert
2 – Kritisch🔴 RotHier muss ich handelnPush aufs Handy, wenn der Zustand 5 Minuten anhält
1 – Warnung🟡 GelbSollte ich mir ansehenErscheint im Tagesreport um 07:00
0 – Info🟢 GrünGut zu wissenNur 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

BereichWas geprüft wirdStufe
AppsMosquitto, Cloudflared, Matter Server, Matter Hub, Vaultwarden, ESPHome Device Builder, Grafana: alles außer on (auch unavailable)Kritisch
BackupLetztes erfolgreiches Backup älter als 36 Stunden, oder letzter Versuch fehlgeschlagenKritisch
Recorder / DatenbankLänger als 5 Minuten kein neuer Eintrag in der DatenbankKritisch
FestplatteUnter 20 % frei, unter 10 % freiWarnung / Kritisch
WassersensorenKüche und Keller unavailable oder unknownKritisch
Geräte offlineESPHome-Präsenzsensor, Kameras, TürklingelWarnung
BatterienProzentwerte unter Schwelle, Sensoren die sich zu lange nicht melden, Batterie-Flags der Ventile und KontakteWarnung
EntfeuchterOffline bei eingeschalteter Steckdose, Tank voll, Filter wechselnWarnung
ReparaturenOffene Meldungen aus den Home-Assistant-ReparaturenKritisch / Warnung
UpdatesHA Core, OS, Supervisor, Firmware der Kameras und TürklingelInfo

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:

TeilInhalt
1. PrüflisteSchwellwerte und Listen: Apps, Geräte, Batterien, Flags, Entfeuchter, Festplatte, Updates. Nur hier pflege ich.
2. AuswertungEin Template, das aus der Prüfliste die Befundliste baut. Normalerweise nicht ändern.
3. SensorenStatus (ok/warnung/kritisch), Level (0/1/2), Zusammenfassung, Reparaturen, Recorder-Alter, Zeit der letzten Auswertung
4. SQL-SensorSchreibt der Recorder noch?
5. ScriptZentrales Benachrichtigungsziel
6. AutomationenSofort-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
Health-Check

📷 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:

VersionWas neu war
V1 → V2Ampel und Befundliste statt Prozent-Score, Prüfliste als zentrale Pflegestelle
V3 / V4Batterie-Schwellen angepasst, Flags für die Ventile
V5Flags in der richtigen Namensform, neue Flags für Türkontakte, Wassersensor-Batterien
V6Reparaturen als eigener Sensor und in der Ampel
V7Wächter: „Auswertung hängt“ und „läuft wieder“
V8Weitere 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 auf unknown und 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 unavailable und 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“ wird lauft. 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.