sphings-dev.de

Stromausfall erkennen und Geräte wiederherstellen in Home Assistant

Nach einem Stromausfall starten Lampen, Zwischenstecker und Schalter nicht in dem Zustand, in dem sie vorher waren. Manche gehen an, manche melden kurz einen alten Zustand und kippen dann um. Ich wollte, dass nach einem Ausfall jedes Gerät wieder dort steht, wo es hingehört, und dass ich erfahre, wann der Strom weg war und wie lange. Dafür sind es sieben Automationen und zwei Scripte geworden. Das klingt nach viel, folgt aber einem einfachen Muster.

Was du brauchst

  • Geräte, die nach einem Ausfall eigenständig hochfahren: Lampen, Zwischenstecker, Schalter (bei mir Zigbee über Zigbee2MQTT und WLAN-Geräte über die Integration tuya_local)
  • Zwei Labels in Home Assistant: nach_stromausfall_aus für Geräte, die nach dem Ausfall aus sein sollen, und nach_stromausfall_an für Geräte, die an sein sollen
  • Hilfsentitäten: timer.stromausfall_wiederherstellung (bei mir 30 Minuten), counter.stromausfaelle, input_datetime.letzter_stromausfall und input_datetime.inselbetrieb_seit
  • Ein Template-Sensor sensor.stromausfall_geraete_nicht_erreichbar, der zählt, wie viele Label-Geräte unavailable sind
  • Die Integration Uptime (sensor.uptime), ein SQL-Sensor für den letzten Recorder-Lauf und ein Messenger-Kanal für die Meldungen (bei mir Telegram)
  • Optional: ein Wechselrichter mit Notstrom- bzw. Inselbetrieb, der seinen Netzstatus als Sensor liefert

So funktioniert es

Es gibt drei unabhängige Wege, einen Ausfall zu erkennen, weil kein einzelner alle Fälle abdeckt.

Erstens der Neustart: Läuft Home Assistant nicht mehr, merkt es das erst beim nächsten Start. Der Recorder markiert einen Lauf, der ohne sauberes Herunterfahren endete. Ein SQL-Sensor liest diese Markierung samt letztem Datensatz vor dem Ausfall, die Automation fragt ihn 20 Sekunden nach dem Start einmal ab. Wichtig: Ein Absturz des Hosts ohne Stromausfall sieht genauso aus.

Zweitens die Geräte: Läuft Home Assistant weiter, etwa an einer Notstromsteckdose, fallen trotzdem viele Geräte gleichzeitig aus. Der Zählsensor muss dazu mindestens 30 Sekunden über 4 liegen, also mindestens fünf Geräte. Die Automation greift nicht in den ersten zehn Minuten nach einem Start, wenn Geräte sich noch verbinden, und nicht im Inselbetrieb. Danach wartet sie bis zu 24 Stunden darauf, dass der Wert wieder unter 3 fällt, und meldet „Strom wieder da“.

Drittens kurze Ausfälle: WLAN-Geräte gelten erst nach etwa vier Minuten als nicht erreichbar, Zigbee-Geräte nach etwa zehn. Ein Ausfall, der vorher endet, wäre unsichtbar. Hier hilft eine Beobachtung: Geräte, die nach der Rückkehr des Stroms hochfahren, melden on ohne Benutzer und ohne Parent-Kontext, Befehle aus Home Assistant tragen immer einen Kontext. Die Automation lauscht deshalb auf alle state_changed-Ereignisse und prüft zuerst die billigen Bedingungen. Sie wertet nur, wenn mindestens drei Label-Geräte in mindestens drei verschiedenen Bereichen innerhalb von 15 Sekunden ohne Befehl angehen. Das schließt Wandschalter in ein, zwei Räumen aus, ebenso eine per App geschaltete Raumgruppe. Außerdem muss jede Zigbee-Bridge seit fünf Minuten verbunden sein.

Alle drei Wege rufen dasselbe Script auf. Es zählt den Ausfall hoch, setzt den Zeitstempel, schaltet die Wiederherstellungs-Automation ein, schaltet auf Wunsch sofort alle Label-Geräte und startet den Timer, sobald das Ende bekannt ist. Zuletzt schickt es die Meldung. Ein zweites kleines Script wartet dafür bis zu 15 Minuten auf die Internetverbindung, weil der Router nach einem Ausfall oft später zurück ist als Home Assistant.

Die Wiederherstellungs-Automation ist im Normalbetrieb aus. Läuft das Fenster, reagiert sie auf jedes Gerät, sobald es zurückkommt. Zigbee2MQTT meldet erst den zwischengespeicherten Zustand off und etwa zwei Sekunden später den echten. Deshalb wird ein Gerät nach dem Zurückkommen 60 Sekunden beobachtet. Geräte mit dem Label nach_stromausfall_aus werden ausgeschaltet, wenn sie on melden, Geräte mit nach_stromausfall_an eingeschaltet, falls sie nicht von selbst angehen. WLAN-Lampen schalteten sich bei mir nach dem Ausschalten beim Booten nach etwa 30 Sekunden selbst wieder ein, deshalb wird ein Wechsel von off auf on ohne Kontext ebenfalls zurückgenommen: aber nur innerhalb von drei Minuten, nachdem eine Automation das Gerät ausgeschaltet hat, oder in den ersten fünf Minuten des Fensters. Ein Tastendruck später im Fenster bleibt unangetastet.

Eine weitere Automation lädt während des Fensters alle fünf Minuten die tuya_local-Einträge neu, deren Entitäten komplett unavailable sind. Einzelne Entitäten sind bei funktionierenden Geräten dauerhaft nicht verfügbar, deshalb zählt nur der Fall, dass alle ausgefallen sind. Am Ende des Timers schaltet die letzte Automation die Wiederherstellung wieder aus und meldet, was noch nicht stimmt: noch an, noch aus, nicht erreichbar.

Der Inselbetrieb hat eine eigene Automation, die ebenfalls das gemeinsame Script nutzt. Der Wert des Netzstatus bei getrenntem Netz ist bei mir noch nie aufgetreten, deshalb zählt jeder Wert außer On-grid, unknown und unavailable als Inselbetrieb. Beim Beginn wird gezählt und der Akkustand gemeldet, beim Netzwechsel zurück auf On-grid kommt eine zweite Meldung mit der Dauer. Die Außenstromkreise hängen nicht am Notstrom, deshalb startet das Script mit dem Ende des Inselbetriebs das Wiederherstellungsfenster für die Außengeräte.

Die Automation

Die Auswahl der Geräte erledigt bei mir ein eigenes Jinja-Makro, das Entitäten- und Geräte-Labels auflöst. Im Beispiel steht stattdessen label_entities(), das nur Entitäten-Labels kennt. Die Entitäts-IDs sind vereinheitlicht, die Messenger-Texte ohne Zeichenschmuck.

# configuration.yaml, im Original ein Template-Helfer in der Oberfläche
template:
  - sensor:
      - name: Stromausfall Geräte nicht erreichbar
        state_class: measurement
        state: >-
          {{ (label_entities('nach_stromausfall_aus') + label_entities('nach_stromausfall_an'))
             | unique | select('is_state', 'unavailable') | list | count }}
# script.stromausfall_nachricht
alias: "Stromausfall: Nachricht"
mode: parallel
max: 10
fields:
  message:
    description: HTML-formatierter Meldungstext
sequence:
  # Nach einem Ausfall ist der Router oft später wieder da als Home Assistant
  - wait_template: "{{ is_state('binary_sensor.wan_internet', 'on') }}"
    timeout: "00:15:00"
    continue_on_timeout: true
  - delay: "00:00:10"
  - action: telegram_bot.send_message
    continue_on_error: true
    data:
      message: "{{ message }}"
      parse_mode: html
      entity_id: notify.telegram
# script.stromausfall_erkannt
alias: "Stromausfall: erkannt"
mode: queued
max: 5
fields:
  ursache: {required: true}
  beginn: {required: true}
  ende: {required: false}
  sofort_schalten: {default: false}
  zaehlen: {default: true}
  zusatz: {required: false}
sequence:
  - variables:
      t_beginn: "{{ as_datetime(beginn) | as_local }}"
      t_ende: "{{ (as_datetime(ende) | as_local) if ende | default('') else none }}"
      dauer_text: >-
        {% if t_ende %}
          {% set s = ((as_datetime(t_ende) - as_datetime(t_beginn)).total_seconds()) | int %}
          {% set h = s // 3600 %}{% set m = (s % 3600) // 60 %}{% set sec = s % 60 %}
          {% if s <= 0 %}unbekannt
          {% elif h > 0 %}{{ h }} h {{ m }} min
          {% elif m > 0 %}{{ m }} min {{ sec }} s
          {% else %}{{ sec }} s{% endif %}
        {% else %}läuft noch{% endif %}
  - if: "{{ zaehlen | default(true) }}"
    then:
      - action: counter.increment
        target: {entity_id: counter.stromausfaelle}
      - action: input_datetime.set_datetime
        target: {entity_id: input_datetime.letzter_stromausfall}
        data:
          datetime: "{{ as_datetime(t_beginn).strftime('%Y-%m-%d %H:%M:%S') }}"
  - action: automation.turn_on
    target: {entity_id: automation.stromausfall_geraete_wiederherstellen}
  - if: "{{ sofort_schalten | default(false) }}"
    then:
      - variables:
          ziel_aus: "{{ label_entities('nach_stromausfall_aus') }}"
          ziel_an: "{{ label_entities('nach_stromausfall_an') }}"
      - if: "{{ ziel_aus | count > 0 }}"
        then:
          - action: homeassistant.turn_off
            target: {entity_id: "{{ ziel_aus }}"}
            continue_on_error: true
      - if: "{{ ziel_an | count > 0 }}"
        then:
          - action: homeassistant.turn_on
            target: {entity_id: "{{ ziel_an }}"}
            continue_on_error: true
  - if: "{{ t_ende is not none }}"
    then:
      - action: timer.start
        target: {entity_id: timer.stromausfall_wiederherstellung}
  - action: script.turn_on
    target: {entity_id: script.stromausfall_nachricht}
    data:
      variables:
        message: >-
          <b>{{ ursache }}</b>
          Beginn: {{ as_datetime(t_beginn).strftime('%d.%m.%Y %H:%M:%S') }}
          {% if t_ende %}Ende: {{ as_datetime(t_ende).strftime('%d.%m.%Y %H:%M:%S') }}{% endif %}
          Dauer: {{ dauer_text }}{% if zusatz | default('') %} – {{ zusatz }}{% endif %}
alias: "Stromausfall: Erkennung nach Neustart"
description: >-
  Home Assistant selbst war aus. Der Recorder markiert einen Lauf, der ohne
  sauberes Herunterfahren endete. Ein Absturz ohne Stromausfall sieht genauso aus.
triggers:
  - trigger: homeassistant
    event: start
actions:
  - delay: "00:00:20"   # Recorder erst fertig starten lassen
  - action: homeassistant.update_entity
    target: {entity_id: sensor.letzter_recorder_lauf}
  - condition: state
    entity_id: sensor.letzter_recorder_lauf
    state: "1"
  - action: script.stromausfall_erkannt
    data:
      ursache: "Stromausfall – Home Assistant war aus"
      beginn: "{{ state_attr('sensor.letzter_recorder_lauf', 'letzter_eintrag') }}"
      ende: "{{ state_attr('sensor.letzter_recorder_lauf', 'neustart') }}"
      sofort_schalten: true
      zaehlen: true
      zusatz: "Beginn = letzter Eintrag vor dem Absturz. Ein Absturz ohne Stromausfall sieht genauso aus."
mode: single
alias: "Stromausfall: Erkennung über Geräte"
description: >-
  Home Assistant läuft weiter, aber mindestens 5 markierte Geräte fallen
  gleichzeitig aus. Greift auch bei Teilausfall (Sicherung, WLAN).
triggers:
  - trigger: numeric_state
    entity_id: sensor.stromausfall_geraete_nicht_erreichbar
    above: 4
    for: "00:00:30"
conditions:
  # nicht in den ersten 10 Minuten nach dem Start
  - condition: template
    value_template: "{{ now() - as_datetime(states('sensor.uptime')) > timedelta(minutes=10) }}"
  # nicht im Inselbetrieb, den übernimmt eine eigene Automation
  - condition: state
    entity_id: sensor.wechselrichter_netztrennung_status
    state: ["On-grid", "unknown", "unavailable"]
  - condition: state
    entity_id: timer.stromausfall_wiederherstellung
    state: idle
actions:
  - variables:
      beginn: "{{ (now() - timedelta(seconds=30)).isoformat() }}"
  - action: script.stromausfall_erkannt
    data:
      ursache: "Stromausfall erkannt – {{ states('sensor.stromausfall_geraete_nicht_erreichbar') }} Geräte nicht erreichbar"
      beginn: "{{ beginn }}"
      zaehlen: true
  - wait_for_trigger:
      - trigger: numeric_state
        entity_id: sensor.stromausfall_geraete_nicht_erreichbar
        below: 3
    timeout: "24:00:00"
    continue_on_timeout: false
  - action: script.stromausfall_erkannt
    data:
      ursache: "Strom wieder da"
      beginn: "{{ beginn }}"
      ende: "{{ now().isoformat() }}"
      zaehlen: false
mode: single
max_exceeded: silent
alias: "Stromausfall: Erkennung über Einschalten ohne Befehl"
description: >-
  Kurze Ausfälle, die enden, bevor Geräte als nicht erreichbar gelten.
  Nach der Rückkehr des Stroms melden Geräte "on" ohne Benutzer- und
  Parent-Kontext. Mindestens 3 markierte Geräte in mindestens 3 Bereichen
  innerhalb von 15 s nach dem ersten solchen Einschalten gelten als Ausfall.
triggers:
  - trigger: event
    event_type: state_changed
conditions:
  - condition: template
    value_template: >-
      {{ trigger.event.data.new_state is not none
         and trigger.event.data.old_state is not none
         and trigger.event.data.new_state.state == 'on'
         and trigger.event.data.old_state.state == 'off'
         and trigger.event.data.new_state.context.user_id is none
         and trigger.event.data.new_state.context.parent_id is none
         and trigger.event.data.entity_id.split('.')[0] in ['light', 'switch'] }}
  - condition: template
    value_template: >-
      {{ trigger.event.data.entity_id in
         (label_entities('nach_stromausfall_aus') + label_entities('nach_stromausfall_an')) }}
  - condition: state
    entity_id: timer.stromausfall_wiederherstellung
    state: idle
  # alle drei Zigbee2MQTT-Bridges seit 5 Minuten verbunden
  - condition: state
    entity_id:
      - binary_sensor.zigbee2mqtt_bridge_1_connection_state
      - binary_sensor.zigbee2mqtt_bridge_2_connection_state
      - binary_sensor.zigbee2mqtt_bridge_3_connection_state
    state: "on"
    for: "00:05:00"
  - condition: template
    value_template: "{{ now() - as_datetime(states('sensor.uptime')) > timedelta(minutes=10) }}"
actions:
  - variables:
      t0: "{{ trigger.event.data.new_state.last_changed.isoformat() }}"
  - delay: "00:00:20"   # die übrigen Geräte sammeln, Bootzeiten sind unterschiedlich
  - variables:
      kandidaten: >-
        {% set start = as_datetime(t0) %}
        {% set ns = namespace(out=[]) %}
        {% for e in (label_entities('nach_stromausfall_aus') + label_entities('nach_stromausfall_an')) | unique %}
          {% set s = states[e] %}
          {% if s is not none and s.state == 'on' and ((s.last_changed - start).total_seconds() | abs) <= 15 %}
            {% set ns.out = ns.out + [e] %}
          {% endif %}
        {% endfor %}
        {{ ns.out }}
      bereiche: "{{ kandidaten | map('area_id') | reject('none') | unique | list }}"
  - condition: template
    value_template: "{{ kandidaten | count >= 3 and bereiche | count >= 3 }}"
  - delay: "00:00:40"   # Geräte fertig booten lassen
  - action: script.stromausfall_erkannt
    data:
      ursache: "Kurzer Stromausfall erkannt – {{ kandidaten | count }} Geräte in {{ bereiche | count }} Räumen ohne Befehl eingeschaltet"
      beginn: "{{ now().isoformat() }}"
      ende: "{{ now().isoformat() }}"
      sofort_schalten: true
      zaehlen: true
      zusatz: "Dauer unbekannt"
mode: single
max_exceeded: silent
alias: "Stromausfall: Geräte wiederherstellen"
description: >-
  Normalerweise aus. Wird vom Script eingeschaltet und am Ende des
  Wiederherstellungsfensters wieder ausgeschaltet.
triggers:
  - trigger: event
    event_type: state_changed
conditions:
  - condition: template
    value_template: >-
      {% set o = trigger.event.data.old_state %}
      {% set n = trigger.event.data.new_state %}
      {% set ende = state_attr('timer.stromausfall_wiederherstellung', 'finishes_at') %}
      {% set fenster_seit = ((now() - (as_datetime(ende) - as_timedelta(state_attr('timer.stromausfall_wiederherstellung', 'duration')))).total_seconds()) if ende else 99999 %}
      {{ o is not none and n is not none and (
           (o.state in ['unavailable', 'unknown'] and n.state in ['on', 'off'])
           or (o.state == 'off' and n.state == 'on'
               and n.context.user_id is none and n.context.parent_id is none
               and ((o.context.parent_id is not none and (n.last_changed - o.last_changed).total_seconds() < 180)
                    or fenster_seit < 300))) }}
actions:
  - variables:
      eid: "{{ trigger.event.data.entity_id }}"
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ eid in label_entities('nach_stromausfall_aus') }}"
        sequence:
          - wait_template: "{{ is_state(eid, 'on') }}"
            timeout: "00:01:00"
            continue_on_timeout: false
          - delay: "00:01:00"
          - condition: template
            value_template: "{{ is_state(eid, 'on') }}"
          - action: homeassistant.turn_off
            target: {entity_id: "{{ eid }}"}
            continue_on_error: true
      - conditions:
          - condition: template
            value_template: "{{ eid in label_entities('nach_stromausfall_an') }}"
        sequence:
          - wait_template: "{{ is_state(eid, 'on') }}"
            timeout: "00:01:00"
            continue_on_timeout: true
          - if: "{{ not wait.completed }}"
            then:
              - action: homeassistant.turn_on
                target: {entity_id: "{{ eid }}"}
                continue_on_error: true
mode: parallel
max: 100
max_exceeded: silent
alias: "Stromausfall: WLAN-Geräte neu laden"
description: >-
  Während des Wiederherstellungsfensters alle 5 Minuten jeden Konfigurationseintrag
  neu laden, dessen Entitäten alle unavailable sind.
triggers:
  - trigger: time_pattern
    minutes: "/5"
conditions:
  - condition: state
    entity_id: timer.stromausfall_wiederherstellung
    state: active
actions:
  - variables:
      haengend: >-
        {% set alle = integration_entities('tuya_local') | list %}
        {% set ns = namespace(down=[]) %}
        {% for cid in alle | map('config_entry_id') | unique | list %}
          {% set m = namespace(n=0, un=0) %}
          {% for x in alle %}
            {% if config_entry_id(x) == cid %}
              {% set m.n = m.n + 1 %}
              {% if is_state(x, 'unavailable') %}{% set m.un = m.un + 1 %}{% endif %}
            {% endif %}
          {% endfor %}
          {% if m.n > 0 and m.un == m.n %}{% set ns.down = ns.down + [cid] %}{% endif %}
        {% endfor %}
        {{ ns.down }}
  - repeat:
      for_each: "{{ haengend }}"
      sequence:
        - action: homeassistant.reload_config_entry
          data: {entry_id: "{{ repeat.item }}"}
          continue_on_error: true
mode: single
max_exceeded: silent
alias: "Stromausfall: Wiederherstellung beenden"
triggers:
  - trigger: timer.finished
    target: {entity_id: timer.stromausfall_wiederherstellung}
actions:
  - action: automation.turn_off
    target: {entity_id: automation.stromausfall_geraete_wiederherstellen}
    data: {stop_actions: false}
  - variables:
      aus: "{{ label_entities('nach_stromausfall_aus') }}"
      an: "{{ label_entities('nach_stromausfall_an') }}"
      noch_an: "{{ aus | select('is_state', 'on') | map('state_attr', 'friendly_name') | list }}"
      noch_aus: "{{ an | select('is_state', 'off') | map('state_attr', 'friendly_name') | list }}"
      weg: "{{ (aus + an) | unique | select('is_state', 'unavailable') | map('state_attr', 'friendly_name') | list }}"
  - action: script.turn_on
    target: {entity_id: script.stromausfall_nachricht}
    data:
      variables:
        message: >-
          <b>Wiederherstellung nach Stromausfall abgeschlossen</b>
          {% if not (noch_an or noch_aus or weg) %}Alle Geräte sind wie vorgesehen.{% else %}
          {% if noch_an %}Noch an: {{ noch_an | join(', ') }}{% endif %}
          {% if noch_aus %}Noch aus: {{ noch_aus | join(', ') }}{% endif %}
          {% if weg %}Nicht erreichbar: {{ weg | join(', ') }}{% endif %}{% endif %}
mode: single
alias: "Stromausfall: Wechselrichter im Inselbetrieb"
description: >-
  Netzausfall, überbrückt durch den Notstrombetrieb des Wechselrichters.
  Jeder Wert außer On-grid/unknown/unavailable zählt als Inselbetrieb.
triggers:
  - trigger: state
    entity_id: sensor.wechselrichter_netztrennung_status
    not_to: ["On-grid", "unknown", "unavailable"]
    id: insel
  - trigger: state
    entity_id: sensor.wechselrichter_netztrennung_status
    to: "On-grid"
    id: netz
actions:
  - choose:
      - conditions:
          - condition: trigger
            id: insel
        sequence:
          - action: input_datetime.set_datetime
            target: {entity_id: input_datetime.inselbetrieb_seit}
            data:
              datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
          - action: script.stromausfall_erkannt
            data:
              ursache: "Netzausfall – Inselbetrieb aktiv ({{ trigger.to_state.state }})"
              beginn: "{{ now().isoformat() }}"
              zaehlen: true
              zusatz: "Akku: {{ states('sensor.wechselrichter_batterie_soc') | replace('.', ',') }} %"
      - conditions:
          - condition: trigger
            id: netz
          - condition: not
            conditions:
              - condition: template
                value_template: "{{ trigger.from_state.state in ['unknown', 'unavailable'] }}"
        sequence:
          - action: script.stromausfall_erkannt
            data:
              ursache: "Netz wieder da – Inselbetrieb beendet"
              beginn: "{{ as_datetime(states('input_datetime.inselbetrieb_seit')) | as_local }}"
              ende: "{{ now().isoformat() }}"
              zaehlen: false
              zusatz: "Akku: {{ states('sensor.wechselrichter_batterie_soc') | replace('.', ',') }} %"
mode: queued
max: 3

Anpassen

  • Vergib die beiden Labels an deine Geräte. Alles, was nach dem Ausfall aus sein soll, bekommt nach_stromausfall_aus, alles andere nach_stromausfall_an.
  • Die Schwellen (5 Geräte, 30 Sekunden, 10 Minuten Startsperre, 3 Geräte in 3 Bereichen, 15 Sekunden) hängen an der Zahl deiner Geräte. Mit nur zehn Label-Geräten musst du sie senken.
  • Hast du keine WLAN-Geräte mit tuya_local, lässt du die Neuladen-Automation weg. Ohne Wechselrichter mit Inselbetrieb entfällt die letzte Automation samt Bedingung in der Geräte-Erkennung.
  • Die Dauer des Wiederherstellungsfensters steht im Timer. Sie muss lang genug sein, dass auch langsam bootende Geräte noch im Fenster zurückkommen.

Stolpersteine

Die Erkennung per Neustart kann ohne Stromausfall auslösen: Ein Host-Absturz sieht aus Sicht des Recorders identisch aus. Die Meldung weist darauf hin. Außerdem ist mode: parallel mit max: 100 bei der Wiederherstellung nötig, weil viele Geräte gleichzeitig zurückkommen. Und die generische state_changed-Auswertung kostet Rechenzeit, deshalb ist die Wiederherstellung nur im Fenster eingeschaltet.

Tags

Unterstützen

Diese Tools entstehen in meiner Freizeit und bleiben kostenlos, offen und cloud-frei. Wenn dir eins davon einen Nachmittag gespart hat, freue ich mich über einen Kaffee oder ein Stück Pizza.

Kaffee oder Pizza

Weitere Beiträge