Badlüfter-Überwachung in Home Assistant: XIAO ESP32-C3 Bluetooth-Proxy + Fresh Intellivent Sky

XIAO ESP32-C3 als Bluetooth-Proxy für Home Assistant, Fresh Intellivent Sky Lüfter über BLE, und ein Absturz, der wie ein Reichweitenproblem aussah.

8. Juli 2026 Updated 8/27/2026 Verifiziert mit HA 2026.8
Badlüfter-Überwachung in Home Assistant: XIAO ESP32-C3 Bluetooth-Proxy + Fresh Intellivent Sky

Zwei Fresh Intellivent Sky Badlüfter, beide senden Bluetooth Low Energy, beide weiter vom Home Assistant-Host entfernt, als dessen eingebautes Funkmodul zuverlässig reicht. Die Lösung war kein neuer HA-Server und kein Netz aus Repeatern — sondern ein paar Euro teurer Mikrocontroller und rund zehn Minuten mit dem Browser. Dieser Beitrag behandelt die Einrichtung des Bluetooth-Proxys, das Hinzufügen der Lüfter, und einen Fehler, der exakt wie ein Hardwareproblem aussah — und keins war.

Die Hardware: Seeed Studio XIAO ESP32-C3

Der XIAO ESP32-C3 ist ein daumennagelgroßes Board — WiFi, Bluetooth 5 und ein USB-C-Anschluss, mehr muss man nicht bedenken. Ich habe es an ein USB-Netzteil im Flur zwischen den beiden Bädern angeschlossen und die Verkabelung seitdem nicht mehr angefasst.

Nahaufnahme des Seeed Studio XIAO ESP32-C3-Boards mit USB-C-Anschluss und externem Antennenstecker

Es zu flashen ist kein Projekt in dem Sinne, wie “Firmware flashen” normalerweise klingt. ESPHomes browserbasierter Projekt-Installer hat ein fertiges Bluetooth Proxy-Firmware-Image — Board per USB an den Computer anschließen, den Installer in Chrome oder Edge öffnen, Board und Bluetooth-Proxy-Projekt auswählen, und es flasht über WebSerial. Kein YAML zu schreiben, kein lokal laufendes ESPHome-Dashboard. Während der geführten Einrichtung mit WLAN verbinden, und es erscheint innerhalb einer Minute in der Home-Assistant-Erkennung.

Einmal als ESPHome-Gerät hinzugefügt, registriert es sich selbst als Bluetooth-Adapter unter Einstellungen → Geräte & Dienste → Bluetooth. Home Assistant bevorzugt automatisch Proxys gegenüber dem eigenen eingebauten Adapter für jedes Gerät, das näher an einem Proxy liegt — ganz ohne Konfiguration Ihrerseits.

Warum ein Proxy statt einfach nur mehr Reichweite

Das Bluetooth-Funkmodul des Home-Assistant-Hosts deckt etwa einen Raum ab, vielleicht zwei bei freier Sichtlinie. Alles darüber hinaus ist eine Funkloch für jede reine BLE-Integration — kein Zigbee-artiges Mesh, keine Repeater-Logik, nur “in Reichweite” oder “nicht in Reichweite”. Ein einziges günstiges ESP32-Board behebt ein ganzes Funkloch auf einmal, und Sie können beliebig viele weitere dort platzieren, wo Sie USB-Strom haben — jedes weitere fügt einfach Abdeckung hinzu, keine Kopplung oder Pro-Gerät-Konfiguration auf der Proxy-Seite nötig.

Ich betreibe das parallel zu einem Google-Find-My-Setup, das ebenfalls von Bluetooth abhängt — die FAQ dort nennt explizit einen ESP32-BLE-Proxy als richtiges Werkzeug, wenn man eine Ablesung innerhalb von Sekunden braucht statt “wann auch immer gerade ein Handy vorbeikommt”.

Die Fresh-Intellivent-Sky-Lüfter hinzufügen

Freshs Intellivent Sky ist ein feuchtigkeitsgesteuerter Badlüfter mit eigener Bluetooth-Schnittstelle, normalerweise über die Fresh-SmartLink-App gesteuert. Home Assistant hat keine offizielle Integration dafür — die, die funktioniert, ist ein Community-Projekt, angoyd/freshintelliventHacs, installiert über HACS als benutzerdefiniertes Repository.

Fresh Intellivent Sky Badlüfter, montiert in einem Deckendiffusor

Mit dem Proxy an Ort und Stelle ist die Einrichtung im Grunde nur Erkennung: Sobald die BLE-Ankündigungen des Lüfters einen Proxy in Reichweite erreichen, bietet Home Assistant ihn unter Einstellungen → Geräte & Dienste → Erkannt an, mit fresh_intellivent_sky als Quelle. Bestätigen, und es wird ein Gerät mit fünf Sensoren erstellt:

  • sensor.<name>_humidity
  • sensor.<name>_temperature
  • sensor.<name>_current_speed
  • sensor.<name>_mode (lesbar — Off, Trickle, Boost usw.)
  • sensor.<name>_mode_raw (der numerische Code dahinter)

Das ist nur lesbar. Es gibt keine fan.*-, switch.*- oder select.*-Entität — nichts in Home Assistant kann die Lüftergeschwindigkeit ändern oder einen Boost auslösen. Auf dieser Integration aufgebaute Automatisierungen können benachrichtigen, den Lüfter manuell zu boosten — sie können es nicht selbst tun. Das ist eine reale Einschränkung, die man kennen sollte, bevor man Automatisierungen darauf aufbaut.

Update 26.08.2026: nicht mehr die ganze Geschichte — siehe den Update-Abschnitt am Ende dieses Beitrags.

Feuchtigkeits-Automatisierungen, die auch mit reinen Lesedaten funktionieren

Zwei Lüfter, je fünf Sensoren, keine Steuerung — aber Luftfeuchtigkeit und ihre Änderungsrate reichen trotzdem, um etwas Nützliches zu bauen. Das Muster, bei dem ich gelandet bin:

  • Ein Derivative-Helfer (Einstellungen → Geräte & Dienste → Helfer, kein YAML) auf jedem Feuchtigkeitssensor, der einen %/min-Wert liefert — ein anhaltender Anstieg über etwa 3,3 %/min signalisiert “jemand duscht” schneller, als es ein fester Feuchtigkeitsschwellenwert je könnte.
  • Eine Automatisierung mit dauerhaftem Schwellenwert für echtes Schimmelrisiko: Feuchtigkeit über 70 % für 45+ Minuten, unabhängig von der Rate.

Beide senden nur eine Benachrichtigung — siehe die Nur-Lese-Einschränkung oben —, aber der Änderungsraten-Trigger erkennt eine beginnende Dusche innerhalb weniger Minuten, deutlich bevor ein fester “Feuchtigkeit über 65 %“-Schwellenwert auslösen würde.

Der Fehler, der wie ein Reichweitenproblem aussah

Ein paar Tage nach der Einrichtung gingen alle fünf Sensoren eines Lüfters auf “unavailable”, während der andere weiterhin normal meldete. Die naheliegende Annahme ist Signal — Proxy-Platzierung, BLE-Reichweite, vielleicht muss der Lüfter neu gekoppelt werden. Ich habe zuerst die Abdeckung des Proxys geprüft und nichts Falsches daran gefunden.

Die tatsächliche Ursache lag im Config Entry der Integration, nicht im Funkmodul. Home Assistant verfolgt den Einrichtungsstatus jeder Integrationsinstanz — einer der möglichen Werte ist failed_unload, der auftritt, wenn der eigene Reload/Unload-Handler einer Integration mitten im Abbau einen Fehler wirft. In der Praxis erholt er sich nicht selbst: nur ein vollständiger Neustart behebt ihn. Das Fehlerprotokoll zeigte genau das: Ein Options-Update-Listener löste einen Reload aus, die Unload-Hälfte scheiterte, und der Eintrag blieb tot zurück. Der Config Entry des identischen, nicht betroffenen Lüfters war währenddessen durchgehend im normalen Status loaded.

Nichts davon lässt sich allein am Entity-Status diagnostizieren — “unavailable” sieht identisch aus, egal ob die Ursache “Integration abgestürzt” oder “Gerät außer Reichweite” ist. Der Hinweis findet sich unter Einstellungen → Geräte & Dienste, beim Integrations-Eintrag selbst, oder im Home-Assistant-Protokoll um den Zeitpunkt, an dem die Entität veraltete. Ein vollständiger Neustart hat den festgefahrenen Eintrag sofort bereinigt; jeder Sensor meldete innerhalb von zwei Minuten wieder Daten.

alias: "Bathroom: Fan Sensor Stale Alert"
description: >
  Notifies if a bathroom fan's sensors go and stay unavailable for over an
  hour — includes a one-tap Restart HA action button rather than
  restarting automatically, since staleness can also mean the fan is
  genuinely out of range or powered off, where a restart wouldn't help.
mode: parallel
trigger:
  - platform: state
    entity_id: sensor.example_fan_mode
    to: "unavailable"
    for:
      hours: 1
action:
  - action: notify.mobile_app_pixel_10
    data:
      title: "📡 Fan sensor offline"
      message: "Unavailable for over an hour — likely a crashed integration entry, not range."
      data:
        actions:
          - action: restart_ha_bathroom_fan
            title: "Restart HA"

Der Aktionsbutton löst eine zweite, winzige Automatisierung aus, die auf genau dieses mobile_app_notification_action-Event lauscht und homeassistant.restart aufruft — bewusst nicht automatisch, denn ein tatsächlich außer Reichweite befindlicher Lüfter würde nur einen nutzlosen Neustart in einer Schleife bekommen statt der eigentlichen Lösung.

Einbindung ins Dashboard

Die Karten beider Lüfter verwenden dieselben Frosted-Glass-card_mod-Konstanten wie der Rest des Dashboards — ein Feuchtigkeitstrend-Diagramm, Temperatur-/Modus-/Geschwindigkeits-Kacheln und eine kompakte Status-Kachel, die nur erscheint, wenn der Lüfter aktiv ist oder die Feuchtigkeit steigt, damit ein untätiges Bad keinen dauerhaften Dashboard-Platz beansprucht. Die Karte des ehemals defekten Lüfters trägt zusätzlich eine kleine “Connectivity: Unavailable”-Fallback-Kachel, gesteuert vom selben mode-Sensor, der auf “unavailable” springt — sie verschwindet in dem Moment, in dem die Sensoren wieder melden, ohne manuelle Dashboard-Bearbeitung in beiden Fällen.

Update: ein Repo mit echter Kontrolle

Sieben Wochen später meldete sich jemand von Spittie.dk — einer dänischen Community-Seite für Home-Assistant-Fans — und wies mich auf ein Repo hin, das ich selbst nicht gefunden hatte: Spit68/Fresh-Intellivent-Sky. Die oben beschriebene angoyd/freshintelliventHacs-Integration ändert daran nichts — sie bleibt, was sie ist: reine Anzeige, ganz gleich wie oft man sie neu lädt. Das neue Repo dagegen ist ein völlig anderes Kaliber, ebenfalls über HACS installiert.

Bevor ich mich auf die neuen Entities verlassen habe, habe ich im Quellcode nachgesehen, was eigentlich hinter dem humidity_sensor_raw-Sensor steckt — dem Nachfolger der alten, kalibrierten Feuchtigkeitsangabe. Ergebnis: nichts. sky_sensors.py liest den BLE-Statuswert und reicht ihn unverändert durch:

values = unpack("<2B5H3B", data)
self.humidity_raw = values[2]

Keine Skalierung, kein Referenzwert, keine Umrechnung — nur eine rohe 16-Bit-Zahl aus dem Gerät, und der Sensor ist obendrein standardmäßig deaktiviert. Damit ließen sich die beiden Feuchtigkeits-Automatisierungen von oben nicht einfach mit neuen Entity-Namen weiterbetreiben — dachte ich zumindest, bis ich eine Ebene tiefer geschaut habe. Die alte angoyd-Integration parst BLE-Daten gar nicht selbst; sie stützt sich auf eine separate Bibliothek, pyfreshintellivent. Deren sensors.py liest genau dieselbe Charakteristik (528b80e8-…, in der Protokoll-Doku wörtlich “Sensor data” genannt) am exakt gleichen Byte-Offset wie humidity_raw oben — und rechnet sie um:

if values[2] != 0:
    self.humidity = round((log(values[2] / 10) * 10), 1)

Ein natürlicher Logarithmus, keine lineare Skala. Nichts, was Fresh selbst je veröffentlicht hat — nur etwas, das ein anderer Entwickler herausgefunden hat und seit Jahren produktiv einsetzt. Ich habe die Formel an den rohen Zahlen getestet, die ich schon hatte: 858 → 41,3 %, ~2800-3150 → 39,5-58 %. Beide landen mitten im normalen Bereich für Bad-Luftfeuchtigkeit — kein Zufallstreffer, den man auch mit einer beliebigen Formel bekäme. Als zwei template-Sensoren in Home Assistant gebaut, und beide Automatisierungen wieder auf ihre ursprünglichen Schwellenwerte gesetzt — 70 % für 45 Minuten beim Schimmelrisiko, 3,33 %/min beim schnellen Anstieg — nur mit einer anderen Quell-Entität als vorher.

Zum Ausgleich: mehr Kontrolle, als die alte Integration je hatte. Boost und Pause mit Dauer und Drehzahl, ein Schalter für konstante Geschwindigkeit, Drehzahlvorgaben je Betriebsmodus, Night Mode und Silent Hours mit eigenen Start- und Endzeiten, dazu Empfindlichkeitsstufen für Feuchte-, VOC- und Lichterkennung. Automatisierungen können den Lüfter jetzt wirklich boosten, statt nur eine Erinnerung dafür zu verschicken.

Der Wechsel selbst war kein reines Konfigurationsproblem. Die Neuinstallation des Bluetooth-Proxys — dasselbe Board wie oben im Beitrag — kappte für rund 20 Minuten die ESPHome-Verbindung, was zunächst wie ein erneuter Totalausfall aussah, sich aber von selbst löste. Automatisierungen und Dashboard-Karten hatten außerdem beide noch hartcodierte Entity-IDs der alten Integration, die einzeln aufgespürt und ersetzt werden mussten — dabei kam heraus, dass die beiden Bad-Karten im Dashboard schon länger die Werte des jeweils anderen Bads anzeigten, weil Kartentitel und Entity-Zuordnung irgendwann vertauscht worden waren, ohne dass ich es bemerkt hatte.

Update: die Zuverlässigkeit der Formel ist weiterhin ungeklärt

Nach der Veröffentlichung dieses Beitrags habe ich vom Entwickler hinter Spit68/Fresh-Intellivent-Sky gehört. Er hatte bereits ernsthafte Arbeit in genau dieses Problem gesteckt — die LaStrada-Log-Formel oben ausprobiert, mehrere andere Formeln, eine SKY-Einheit auseinandergenommen, um den eigentlichen Feuchtigkeitssensor-Chip zu sehen, mit dessen Datenblatt gearbeitet, und sogar eine KI mit einem Haufen Rohwert/Referenz-Feuchtigkeit-Paaren gefüttert. Nichts davon ergab eine Formel, die solide genug wäre, um als echten Prozent-Sensor auszuliefern — genau deshalb bleibt humidity_sensor_raw in seinem Repo standardmäßig deaktiviert, statt eine Vermutung als Daten zu verkaufen. Respekt dafür, diese Linie zu halten, statt etwas auszuliefern, das richtig aussieht, es aber nicht ist.

Der wichtigste Datenpunkt: zwei SKY-Einheiten nebeneinander, bei vermutlich ähnlicher Luftfeuchtigkeit, können Rohwerte melden, die sich so stark unterscheiden wie 100 gegenüber 300. Das ist kein linearer Versatz, den man wegkalibrieren kann — es deutet darauf hin, dass das Rohregister geräteübergreifend gar nicht auf derselben Skala liegt.

Das relativiert, was ich oben tatsächlich validiert habe. Die Log-Formel lieferte plausible Zahlen für meinen konkreten Lüfter, und meine beiden Automatisierungen funktionieren weiterhin, weil sie konsequent gegen die eigene Baseline genau dieses einen Lüfters über die Zeit schwellenwerten — nicht weil die Formel eine bewiesene, allgemeingültige Roh-zu-Prozent-Umrechnung ist. Wer das für die eigenen SKY-Einheiten nachbaut, sollte die resultierende Zahl als relativen Trendindikator behandeln, nicht als kalibrierten Prozentwert — bis man sie gegen ein echtes Hygrometer im eigenen Bad geprüft hat. Auch er ist mit der Suche noch nicht fertig — neue Vergleichssensoren sind bei ihm unterwegs — taucht also irgendwann eine echte Formel auf, aktualisiere ich diesen Beitrag.

Eine Sache, die man wissen sollte

Wer die Bluetooth-Reichweite von Home Assistant für mehr als ein einzelnes Gerät erweitert — Govee-Sensoren, andere BLE-Tracker, weitere solcher Lüfter —, dem Proxy selbst ist es egal, was er weiterleitet. Ein XIAO ESP32-C3 pro Funkloch deckt alles in Reichweite ab, nicht nur das Gerät, für das er eingerichtet wurde. Es ist dieselbe Logik wie beim Betrieb eines LoRa-Gateways für einen 200 Meter entfernten Briefkastensensor — günstige, zweckgebaute Funkhardware löst Reichweitenprobleme, die keine Menge an Home-Assistant-Konfiguration lösen kann.