Badeværelsesventilator-overvågning i Home Assistant: XIAO ESP32-C3 Bluetooth-proxy + Fresh Intellivent Sky

XIAO ESP32-C3 som Bluetooth-proxy til Home Assistant, Fresh Intellivent Sky over BLE, og et nedbrud der lignede dårlig rækkevidde, men ikke var det.

8. juli 2026 Updated 8/27/2026 Verificeret på HA 2026.8
Badeværelsesventilator-overvågning i Home Assistant: XIAO ESP32-C3 Bluetooth-proxy + Fresh Intellivent Sky

To Fresh Intellivent Sky-badeværelsesventilatorer, begge udsendende Bluetooth Low Energy, begge længere fra Home Assistant-værten end dens indbyggede radio pålideligt rækker. Løsningen var ikke en ny HA-server eller et net af repeatere — det var en mikrocontroller til nogle få dollars og omkring ti minutter med en browser. Dette indlæg dækker opsætningen af Bluetooth-proxyen, tilføjelsen af ventilatorerne, og en fejl der lignede præcis et hardwareproblem — og ikke var det.

Hardwaren: Seeed Studio XIAO ESP32-C3

XIAO ESP32-C3 er et kort på størrelse med en tommelfingernegl — WiFi, Bluetooth 5 og en USB-C-port, ikke andet du behøver at tænke over. Jeg satte det i en USB-strømadapter i gangen mellem de to badeværelser og har ikke rørt ledningerne siden.

Seeed Studio XIAO ESP32-C3-kort i nærbillede, med USB-C-port og ekstern antennestik

At flashe det er ikke et projekt på den måde, “at flashe firmware” normalt antyder. ESPHomes browserbaserede projekt-installer har et færdiglavet Bluetooth Proxy-firmware-image — sæt kortet i computeren via USB, åbn installeren i Chrome eller Edge, vælg kortet og Bluetooth Proxy-projektet, og det flasher over WebSerial. Ingen YAML at skrive, ingen ESPHome-dashboard at køre lokalt. Forbind det til WiFi under den guidede opsætning, og det dukker op i Home Assistants discovery inden for et minut.

Når det er tilføjet som en ESPHome-enhed, registrerer det sig selv som en Bluetooth-adapter under Indstillinger → Enheder & Tjenester → Bluetooth. Home Assistant foretrækker automatisk proxyer frem for sin egen indbyggede adapter for enhver enhed tættere på en proxy — uden nogen konfiguration fra din side.

Hvorfor en proxy frem for bare mere rækkevidde

Home Assistant-værtens Bluetooth-radio dækker cirka ét rum, måske to med fri sigtlinje. Alt derudover er en dødzone for enhver ren BLE-integration — ingen Zigbee-lignende mesh, ingen repeater-logik, bare “inden for rækkevidde” eller “ikke inden for rækkevidde”. Et enkelt billigt ESP32-kort løser en hel dødzone på én gang, og du kan sætte flere op hvor som helst du har USB-strøm — hver ny en tilføjer bare dækning, ingen parring eller per-enhed-konfiguration krævet på proxysiden.

Jeg kører dette sammen med en Google Find My-opsætning, der også afhænger af Bluetooth — FAQ’en der peger specifikt på en ESP32 BLE-proxy som det rigtige værktøj, når du har brug for en aflæsning inden for sekunder frem for “når en telefon nu engang går forbi.”

Tilføjelse af Fresh Intellivent Sky-ventilatorerne

Fresh Intellivent Sky er en fugtstyret badeværelsesventilator med sin egen Bluetooth-grænseflade, normalt styret gennem Fresh SmartLink-appen. Home Assistant har ingen officiel integration til den — den der virker, er et community-projekt, angoyd/freshintelliventHacs, installeret via HACS som et brugerdefineret repository.

Fresh Intellivent Sky-badeværelsesventilator monteret i en loftsdiffusor

Med proxyen på plads er opsætningen reelt bare discovery: så snart ventilatorens BLE-annonceringer når en proxy inden for rækkevidde, tilbyder Home Assistant den under Indstillinger → Enheder & Tjenester → Fundet med fresh_intellivent_sky som kilde. Bekræft den, og den opretter en enhed med fem sensorer:

  • sensor.<navn>_humidity
  • sensor.<navn>_temperature
  • sensor.<navn>_current_speed
  • sensor.<navn>_mode (læsbar — Off, Trickle, Boost osv.)
  • sensor.<navn>_mode_raw (den numeriske kode bag)

Det er skrivebeskyttet. Der er ingen fan.*, switch.* eller select.*-entitet — intet i Home Assistant kan ændre ventilatorens hastighed eller udløse en boost. Automationer bygget på denne integration kan give besked om, at du selv skal boost’e ventilatoren manuelt — de kan ikke gøre det for dig. Det er en reel begrænsning, værd at kende, før du planlægger automationer omkring den.

Opdatering 26/08/2026: ikke længere hele historien — se opdateringsafsnittet nederst i indlægget.

Fugtautomationer der rent faktisk virker med skrivebeskyttet data

To ventilatorer, fem sensorer hver, ingen styring — men luftfugtighed og dens ændringshastighed er stadig nok til at bygge noget nyttigt. Mønsteret jeg landede på:

  • En Derivative-hjælper (Indstillinger → Enheder & Tjenester → Hjælpere, ingen YAML) på hver fugtighedssensor, der giver en %/min-værdi — en vedvarende stigning forbi cirka 3,3%/min flager “nogen er i bad” hurtigere, end en fast fugtighedsgrænse nogensinde kunne.
  • En automation med vedvarende grænseværdi for reel skimmelrisiko: fugtighed over 70% i 45+ minutter, uafhængigt af hastighed.

Begge sender bare en notifikation — se skrivebeskyttelsesbegrænsningen ovenfor — men ændringshastigheds-triggeren fanger en brusertur, der starter, inden for et par minutter, længe før en fast “fugtighed over 65%“-grænse ville udløse.

Fejlen der lignede et rækkeviddeproblem

Et par dage efter opsætningen gik alle fem sensorer på den ene ventilator “unavailable”, mens den anden fortsatte med at rapportere normalt. Den oplagte antagelse er signal — proxyplacering, BLE-rækkevidde, måske skal ventilatoren parres igen. Jeg tjekkede proxyens dækning først og fandt intet galt med den.

Den egentlige årsag lå i integrationens config entry, ikke radioen. Home Assistant sporer opsætningsstatus for hver integrationsinstans — én af de mulige værdier er failed_unload, som opstår, når en integrations egen reload/unload-handler kaster en fejl midt i nedlukningen. I praksis selvhelbreder den ikke: kun en fuld genstart rydder den. Fejlloggen viste præcis det: en options-update-listener udløste en reload, unload-halvdelen fejlede, og entryen blev efterladt død. Den identiske, upåvirkede ventilators config entry sad derimod i en normal loaded-status hele tiden.

Intet af dette kan diagnosticeres ud fra entity-status alene — “unavailable” ser identisk ud, uanset om årsagen er “integration crashede” eller “enhed er uden for rækkevidde”. Fingerpeget er i Indstillinger → Enheder & Tjenester, på selve integrations-entryen, eller i Home Assistant-loggen omkring det tidspunkt, hvor entiteten blev forældet. En fuld genstart ryddede den fastlåste entry med det samme; hver sensor rapporterede igen inden for to minutter.

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"

Handlingsknappen udløser en anden, lille automation, der lytter efter netop den specifikke mobile_app_notification_action-event og kalder homeassistant.restart — bevidst ikke automatisk, da en ventilator, der reelt er uden for rækkevidde, bare ville få en ubrugelig genstart i en løkke i stedet for den faktiske løsning.

At style det ind i dashboardet

Begge ventilatorers kort bruger de samme frostede glas-card_mod-konstanter som resten af dashboardet — et fugtighedstrend-diagram, temperatur/mode/hastighed-fliser, og en kompakt statusflise, der kun vises, når ventilatoren er aktiv, eller fugtigheden stiger, så et inaktivt badeværelse ikke optager permanent plads på dashboardet. Den tidligere fejlramte ventilators kort bærer også en lille “Connectivity: Unavailable”-fallback-flise, drevet af den samme mode-sensor, der går “unavailable” — den forsvinder, i det øjeblik sensorerne begynder at rapportere igen, ingen manuel dashboard-redigering nødvendig i nogen af tilfældene.

Opdatering: skiftet til et repo, der faktisk kan noget

Syv uger senere blev jeg kontaktet af en fra Spittie.dk — et dansk site for Home Assistant-entusiaster — der gjorde mig opmærksom på et repo, jeg ikke selv havde fundet: Spit68/Fresh-Intellivent-Sky. angoyd/freshintelliventHacs — integrationen omtalt ovenfor — er stadig rent skrivebeskyttet, uanset hvor mange gange man genindlæser den; det nye repo er noget helt andet, installeret på samme måde via HACS, men med et helt andet sæt entiteter i den anden ende.

Og det er markant flere entiteter: boost- og pauseknapper med varighed og RPM, en switch til konstant hastighed, RPM pr. driftstilstand, night mode og silent hours med hver deres start- og sluttidspunkt, og følsomhedsvalg for fugt-, VOC- og lysdetektion. Automationer kan nu rent faktisk trykke på boost-knappen selv — ikke bare sende en påmindelse om, at nogen andre skal gøre det.

Det, der forsvandt til gengæld, er en brugbar fugtighedsprocent. Det nye repo har ganske vist en humidity_sensor_raw-entitet, men den er slået fra som standard, og før jeg byggede noget videre på den, gik jeg ind i kildekoden for at lede efter en skjult formel. Der er ingen. sky_sensors.py pakker BLE-statusbeskeden ud direkte:

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

Et råt 16-bit tal, lige fra enhedens eget register, uden division eller reference at holde det op imod. De to automationer fra afsnittet ovenfor kunne derfor ikke bare kopieres over med et nyt entity-navn — indtil jeg tænkte mig lidt bedre om. Den gamle angoyd-integration parser slet ikke selv BLE-data; den læner sig op ad et separat bibliotek, pyfreshintellivent. Dét biblioteks sensors.py læser samme karakteristik (528b80e8-…, bogstaveligt navngivet “Sensor data” i protokol-dokumentationen) på samme byte-offset som humidity_raw ovenfor — og regner den om:

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

En naturlig logaritme, ikke en lineær skalering. Ikke noget Fresh selv har publiceret — bare noget en anden udvikler fandt frem til og har kørt i produktion i årevis. Jeg testede den på de rå tal, jeg allerede havde: 858 → 41,3%, ~2800-3150 → 39,5-58%. Begge lander lige midt i almindelige badeværelses-fugtighedsniveauer, ikke et tilfældigt resultat af at proppe tal ind i en tilfældig formel. Byggede den som to template-sensorer i Home Assistant, og satte begge automationer tilbage til deres oprindelige thresholds — 70% i 45 minutter for skimmelrisiko, 3,33%/min for hurtig stigning — nu bare mod en anden kildeentitet end før.

Selve skiftet var ikke kun et spørgsmål om at pege på et nyt GitHub-repo i HACS. Geninstallationen af Bluetooth-proxyen — samme kort som tidligere i indlægget — udløste en kortvarig afbrydelse af dens ESPHome-forbindelse på omkring 20 minutter, der ved første øjekast lignede endnu et nedbrud, men rettede sig selv. Og fordi automationer og dashboardkort begge havde hardcodede entity_id’er fra den gamle integration, skulle hver reference findes og rettes manuelt — hvilket samtidig afslørede, at de to badeværelseskort på dashboardet i lang tid havde vist hinandens data, fordi titel og entity-mapping på et tidspunkt var blevet byttet om uden at jeg havde opdaget det.

Opdatering: formlens pålidelighed er stadig uafklaret

Efter dette indlæg blev publiceret, hørte jeg fra udvikleren bag Spit68/Fresh-Intellivent-Sky. Det viser sig, at han allerede havde lagt et solidt stykke arbejde i præcis dette problem — prøvet LaStrada’s log-formel ovenfor, en række andre formler, skilt en SKY-enhed ad for at se selve fugtsensor-chippen, arbejdet ud fra dens datablad, og endda fodret en AI med en bunke rå-tal/reference-fugtighed-par. Intet af det gav en formel solid nok til at sende som en rigtig procent-sensor — hvilket er præcis derfor humidity_sensor_raw er slået fra som standard i hans repo, i stedet for et gæt udklædt som data. Respekt for at holde den linje i stedet for at sende noget der ser rigtigt ud, men ikke er det.

Det vigtigste datapunkt: to SKY-enheder side om side, i formodentlig ens fugtighed, kan vise rå-tal så forskellige som 100 vs. 300. Det er ikke en lineær forskydning man kan kalibrere væk — det peger på at det rå register slet ikke er på samme skala på tværs af enheder.

Det ændrer, hvad jeg egentlig har valideret ovenfor. Log-formlen gav plausible tal for min specifikke ventilator, og mine to automationer virker stadig, fordi de sætter thresholds konsekvent mod netop den ventilators egen baseline over tid — ikke fordi formlen er en bevist, generel rå-til-procent-konvertering. Genskaber du dette for dine egne SKY-enheder, så behandl tallet som en relativ trend-indikator, ikke en kalibreret procent, indtil du har tjekket det mod et rigtigt hygrometer i dit eget badeværelse. Han er heller ikke færdig med at jagte det endnu — nye sammenligningssensorer er på vej hans side — så dukker der en rigtig formel op, opdaterer jeg dette indlæg.

Én ting værd at vide

Hvis du udvider Home Assistants Bluetooth-rækkevidde til andet end en enkelt enhed — Govee-sensorer, andre BLE-trackere, flere af disse ventilatorer — er det proxyen ligegyldigt, hvad den proxyer. Ét XIAO ESP32-C3-kort per dødzone dækker alt inden for rækkevidde, ikke kun den enhed, du satte det op til. Det er samme logik som at køre en LoRa-gateway til en postkassesensor 200 meter væk — billig, formålsbygget radiohardware løser rækkeviddeproblemer, som ingen mængde Home Assistant-konfiguration kan.