Am 11. Oktober um 15:32 ging die Heizung im Gäste-WC an. Sie lief 36 Minuten, und kein Automatismus bekam sie wieder aus. Um 16:08 habe ich sie von Hand abgeschaltet. Automatisch wäre sie nicht mehr ausgegangen: Die Bedingung, die das tun sollte, war rechnerisch unerreichbar.

Sechs Tage vorher hätte derselbe Handgriff anders ausgesehen. Damals schaltete das Öffnen des Fensters die Espressomaschine ab.

Das Problem

Im Gäste-WC hängt eine elektrische Heizung an einer schaltbaren Steckdose, daneben ein Zigbee-Fensterkontakt. Die Idee ist ein Zweipunktregler mit Sperren: Fenster auf, Heizung aus. Fenster zu, niemand zu Hause, Nacht, dann ebenfalls aus. Sonst heizen, wenn der Raum kalt ist.

Home Assistant läuft auf srv-rebellion, der MQTT-Broker mit Zigbee2MQTT auf srv-coruscant. Der Kontakt meldet über Zigbee2MQTT, die Steckdose hängt als Tuya-Gerät im selben Netz.

Die Automation lief seit Monaten, und sie lief falsch. Am 5. Oktober stand das Fenster offen, die Heizung lief weiter. Weder Funk noch der Kontakt waren das Problem.

Die Diagnose

Der Weg führte ins Logbuch, nicht in den YAML-Editor. Dort steht bei jedem Zustandswechsel, welche Entity sich wann geändert hat. Vergleicht man das Fenster-Öffnen mit den Schaltvorgängen derselben Sekunde, fällt der Treffer sofort auf: Die Heizungssteckdose blieb an. Dafür ging der Siebträger-Plug auf aus.

Der zweite Blick galt der Historie des Fühlers. Im Zeitraum 30.09. bis 10.10. fiel der Gäste-WC-Kontakt nie unter 23 °C. Die Heizung lief im selben Zeitraum trotzdem 16 Stunden in sieben Tagen. Das passt nicht zusammen, wenn der Kontakt die Raumtemperatur abbildet. Die Langzeitstatistik seit Dezember 2023 zeigt für denselben Raum einen Tiefstwert von 9 °C im Januar 2026, in Winternächten meist 13 bis 19 °C. Der Raum war also kalt, der Fühler behauptete etwas anderes.

Der dritte Blick ging auf die Funkseite, mit einem MQTT-Mitschnitt am Broker. Der Kontakt sendet nicht nur bei Zustandsänderungen, sondern auch ein Lebenszeichen. Gemessen waren das rund alle 50 Minuten, zehn Nachrichten im beobachteten Fenster.

Der vierte Blick war ein Heiztest am 11. Oktober, diesmal mit zwei Stromzählern im Blick. Dazu später, weil er die Verkabelung geklärt hat.

Die Ursache

Ursache A: eine device_id, die niemand liest

In der Automation standen die Ziele als device_id, also als interne Geräte-Kennung, drei Stück. Eines gehörte nicht zur Heizungssteckdose, sondern zum Siebträger-Plug. Ein Copy-Paste-Fehler, den Home Assistant nicht als Fehler erkennt: Jede existierende device_id wird akzeptiert, egal ob sie zum Raum, zum Gerät oder zur Automation passt. Im YAML ist die Kennung eine Hex-Zeichenkette ohne Namen. Wer nicht nachzählt, sieht nichts.

Der zweite Teil von Ursache A war mode: single zusammen mit einem delay von 30 Minuten im Heiz-Zweig. Solange dieser Zweig wartete, verwarf die Automation jeden weiteren Trigger. Das Fenster konnte sich in diesen 30 Minuten öffnen, die Heizung erfuhr es nicht.

Ursache B: der Fühler hieß nicht Gäste-WC

Die Automation regelte nach der Gerätetemperatur eines Fensterkontakts. Nur nicht des Gäste-WC-Kontakts, sondern des Garagentor-Kontakts. Ein Thermostat, das auf das Garagentor hört, ist ungefähr so hilfreich wie ein Türsteher, der nur auf R2-D2 achtet.

Dazu kamen 30-Minuten-Blöcke zu geraden Stunden plus 05:00, jeweils mit delay. Ein Reload mitten im Delay beendete den laufenden Zweig, ohne den Schaltbefehl zurückzunehmen. Die Heizung blieb an, und niemand wartete mehr auf sie.

Die 16 Stunden erklären sich damit: Der falsche Fühler stand nie unter 23 °C, also heizte die Regel nach ihren eigenen Schwellen 16 Stunden in sieben Tagen, ohne dass der Raum das erforderte.

Der tote Kontakt, der weiterlügt

Bevor der richtige Fühler überhaupt etwas taugen konnte, musste ein grundsätzliches Problem weg. Zigbee2MQTT stand auf last_seen: disable und availability.enabled: false. Ein Kontakt ohne Lebenszeichen wird damit nicht als offline markiert, sondern behält seinen letzten Wert. Steht der auf 23 °C, bekommt die Heizung für immer eine plausible Zahl. Ein toter Sensor, der weiterlügt, ist schlimmer als ein fehlender.

Der Heartbeat lag gemessen bei rund 50 Minuten. Die Tot-Erkennung wurde deshalb auf 3 Stunden gesetzt, also 10800 Sekunden. Das ist mehr als das Dreifache, genug Luft für einen verlorenen Funkrahmen und trotzdem schnell genug, um nicht tagelang auf einen Batterietod zu warten.

Ursache C: der Boost ohne Abschaltpfad

Am 11. Oktober kam der Solar-Boost dazu: Wenn draußen kalt ist und die PV mehr liefert, als das Haus braucht, darf die Heizung länger laufen, bis der Kontakt 28 °C zeigt. In der Design-Notiz stand eine Außenschwelle von 12 °C, im Live-YAML später 13 °C. Beides sind belegte Stände, ich ziehe sie hier nicht zu einer Zahl zusammen.

Der Test um 15:32 deckte zwei Fehler auf. Erstens: Der aktive Boost-Zweig wurde bei jedem Trigger neu ausgewertet und schaltete die Steckdose sofort wieder ein, auch direkt nachdem die Automation selbst sie ausgeschaltet hatte. Ein Latch, gebaut aus einer Zustandsregel, die ihren eigenen Befehl nicht als Endzustand akzeptiert. Zweitens lieferte der Kontakt 35 Minuten lang keine neue Temperatur. Die Abschaltung bei 28 °C war damit nicht erreichbar, und der Lauf endete nach 36 Minuten von Hand um 16:08.

Der Boost scheiterte nicht am Überschuss. Der war da. Er scheiterte an einer Abschaltbedingung, die nie eintreten konnte.

Der Fix

Zuerst die Ziele: alle drei device_id-Einträge raus, entity_id: switch.gaste_wc_heizung rein. Das ist lesbar, diffbar und überlebt ein Neu-Pairing des Geräts, weil die Entity-ID bleibt. mode: single wurde auf restart gesetzt. Der eigentliche Fix war aber, das delay ganz zu entfernen.

Die V8-Regel arbeitet zustandslos. Kein Warten, keine 30-Minuten-Blöcke, stattdessen ein Abgleich alle 15 Minuten plus Trigger auf Fühler, Fenster, Anwesenheit, Schalter, HA-Start und automation_reloaded. Befehle gehen nur raus, wenn der Ist-Zustand vom Soll abweicht. Nach einem Reload gibt es keinen hängenden Zweig mehr, weil es keinen Zweig gibt.

Die Schwellen liegen seit dem 11. Oktober bei AN unter 23 °C, AUS ab 24 °C, bei 23 °C wird der Zustand gehalten. Vorher waren es 18 und 19. Das Zeitfenster ist 08:00 bis 23:00 Uhr. Die Sperre schaltet aus, wenn das Fenster nicht ausdrücklich geschlossen ist, niemand zu Hause ist, die Zeit außerhalb des Fensters liegt, die Temperatur nicht numerisch ist oder der Kontakt tot ist:

- alias: Kontakt tot (kein Lebenszeichen >3 h oder unbekannt) -> fail-closed
  condition: template
  value_template: >-
    {% set ls = states('sensor.gaste_wc_fensterkontakt_last_seen') | as_datetime(none) %}
    {{ ls is none or (now() - ls).total_seconds() > 10800 }}

Der Lebenszeichen-Sensor ist ein eigener MQTT-Sensor auf dem Bridge-Topic, der last_seen aus der JSON-Nutzlast zieht. Zigbee2MQTT liefert das Feld erst, wenn advanced.last_seen auf ISO_8601 steht. Die Änderung greift zur Laufzeit über bridge/request/options, ohne Neustart des Adapters. Eine eigene Automation meldet Ausfall und Rückkehr per Telegram.

Der Boost bekam fünf Bremsen. Eine harte Laufzeitgrenze von 45 Minuten über einen Timer, der per timer.reload ohne Core-Neustart nachziehbar ist. Ein Sperr-Flag, das einen zweiten Boost verhindert, bis der Überschuss-Sensor einmal aus war. Ein aktives Ausschalten am Episoden-Ende, weil die Normalregel bei Kontakt 23 sonst weiterheizt. Und ein Fail-safe nach HA-Start, dazu eine Schutzabschaltung bei mehr als 75 Minuten Dauerlauf, also 4500 Sekunden. Der Latch innerhalb der 45 Minuten ist bewusst akzeptiert: Ein manuelles Ausschalten während eines laufenden Boosts wird übersteuert, die Zeitgrenze begrenzt es.

Dazu kam die Topologie, gemessen statt vermutet. Die Heizung hängt am Hauszähler, nicht am Heizungszähler. Nach dem AUS im Heiztest fiel der Bezug am Hauszähler um rund 470 W, der Heizungszähler blieb unverändert. Deshalb ist die Steckdose für die Verbrauchsmessung wertlos: Sie hat keinen echten Leistungsmesser, ihr Power-Sensor ist bedeutungslos. Wer daraus eine Ersparnis ableitet, misst Rauschen.

Der erste echte Boost-Tag war der 12. Oktober. 05:32 bis 07:00, 88 Minuten, 100 Prozent Netz, vor Sonnenaufgang. Danach 08:34 bis 08:44, zehn Minuten, abgebrochen weil Waschmaschine und Trockner den Überschuss zogen. Und 09:01 bis 09:46, beendet durch die 45-Minuten-Kappe. Die Heizung kam an diesem Tag auf etwa 0,57 kWh laut Zähler, 0,59 kWh laut Integration. Davon rund 0,34 kWh Netz und 0,25 kWh PV, also etwa 42 Prozent. Der PV-Tag lag bei 28,7 kWh Produktion und 23,7 kWh Export.

Diese Zahlen sind eine Randzuordnung: Die Heizung bekommt den Netto-Netz-Import, der Rest gilt als PV. Die Netzwerte kommen aus einem derivative mit 5-Minuten-Fenster. Die Genauigkeit liegt bei plus/minus 10 bis 15 Prozent, nicht bei der Minute.

Wegen der 88 Minuten vor Sonnenaufgang wanderte das Zeitfenster von 05:00 auf 08:00 Uhr. Die Kalibrierung des Fühlers bleibt offen. Sieben Messpunkte bei ausgeschalteter Heizung ergaben einen Versatz von +4,2 bis +5,2 °C, als Spanne, nicht als eine Zahl. Kontakt 23 °C deckt damit einen Raum von 18,1 bis 18,8 °C ab. Der Heiztest am 11. Oktober lief 771,818 Sekunden, von 08:23:18.593 bis 08:36:10.411, der Kontakt sprang von 23 auf 25 °C, das Thermometer zeigte danach 18,9 statt 18,5 °C, also +0,4 °C. Synchrone Raum-Messwerte zum Abschaltzeitpunkt gibt es nicht, eine belegte Aufheizrate damit auch nicht. Bis zur nächsten Ansage gilt: nichts an der Regel ändern, bis er es sagt.

Ein Detail zum Suchen: Die Automation heißt V8, ihre entity_id bleibt auf V7. Wer nach dem alten Namen sucht, findet die neue Automation. Nur die Beschriftung lügt.

Hardware

Ein Fensterkontakt mit Gerätetemperatur ist ein Kompromiss: für die Sperre reicht er, für eine Raumregelung ist er die falsche Quelle: Aqara Tür-/Fensterkontakt MCCGQ11LM. Wer mehrere Zigbee-Geräte und dauerhaft last_seen braucht, fährt mit einem Netzwerk-Coordinator statt eines USB-Sticks besser: SMLIGHT SLZB-06 (Zigbee-Gateway Ethernet/PoE/WiFi). Wenn es doch ein USB-Stick sein soll, ist der Zigbee USB-Coordinator (ConBee II) die günstige Variante.

Was du mitnimmst

Eine device_id ist kein Name, sondern eine Nummer. Home Assistant akzeptiert jede gültige Kennung, auch die des Nachbargeräts. Der Fehler taucht in keinem Config-Check auf, nur im Logbuch, wenn zur falschen Sekunde das falsche Gerät schaltet. entity_id ist lesbar und bleibt beim Neu-Pairing stabil.

Ein Fühler, der nie unter 23 °C fällt, während der Raum bei 13 liegt, ist kein Fühler für diesen Raum. Die Historie gegen die Langzeitstatistik zu legen hat den Garagentor-Kontakt entlarvt, nicht das YAML.

last_seen: disable plus availability.enabled: false macht aus einem toten Kontakt einen ewigen Lügner. Der letzte Wert bleibt gültig, und die Regel baut darauf. Ein Lebenszeichen-Sensor mit einer Schwelle beim Dreifachen des gemessenen Heartbeats ist billiger als jeder Raumfühler.

Ein Boost, der auf eine Temperatur wartet, die nie kommt, ist ein Timer ohne Uhr. Der Überschuss war nicht das Problem, die Abschaltbedingung war es. Wer eine Zustandsregel baut, die ihren eigenen Ausschaltbefehl sofort wieder aufhebt, braucht eine harte Zeitgrenze und ein Sperr-Flag, sonst hält nur der Netzstecker.