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_ausfür Geräte, die nach dem Ausfall aus sein sollen, undnach_stromausfall_anfür Geräte, die an sein sollen - Hilfsentitäten:
timer.stromausfall_wiederherstellung(bei mir 30 Minuten),counter.stromausfaelle,input_datetime.letzter_stromausfallundinput_datetime.inselbetrieb_seit - Ein Template-Sensor
sensor.stromausfall_geraete_nicht_erreichbar, der zählt, wie viele Label-Geräteunavailablesind - 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: singlealias: "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: silentalias: "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: silentalias: "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: silentalias: "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: silentalias: "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: singlealias: "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: 3Anpassen
- Vergib die beiden Labels an deine Geräte. Alles, was nach dem Ausfall aus sein soll, bekommt
nach_stromausfall_aus, alles anderenach_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.
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 PizzaWeitere Beiträge
- Rollladen-Steuerung pro Raum: Taster, Fenster, Kälte und Regen Eine Automation pro Raum, die Taster, Fensterkontakt, Kälte, Regen und die Zeit vor Sonnenaufgang zusammenführt. Zwei Merker-Helfer verhindern, dass sich die Auslöser gegenseitig überfahren.
- Rauchmelder löst aus: Alle Rollläden fahren hoch (Home Assistant) Bei Rauchalarm sollen die Rollläden im ganzen Haus hochfahren, damit Fenster als Fluchtweg frei sind. Die Automation ist kurz – aber ein fehlendes „from“ im Trigger entscheidet darüber, ob sie im Ernstfall überhaupt feuert.
- Marstek Modbus Suite 3.0.1 und 3.1-Beta: Ladeprognose und Registerprüfung 3.0.1 repariert den Start nach einem nicht erreichbaren Speicher. Die 3.1-Betas planen „Zeit bis voll“ in der Reihenfolge des BMS und prüfen die Registerkarten von Venus D, A und E v3 gegen die Firmware.