Von instabilen ESP32-BLE-Proxies zu einem USB-Bluetooth-Adapter (FSC-BP119) — ein Erfahrungsbericht

Kurzfassung: Wochenlange, sporadische Ausfälle meiner Bluetooth-Geräte (SwitchBot-Schloss, Thermometer, Präsenz- und Bettsensoren, LED-Streifen) haben sich als Problem der ESP32-Bluetooth-Proxies herausgestellt — nicht der Geräte selbst. Firmware, Board-Tausch, WLAN-Tuning: alles vergeblich. Gelöst hat es am Ende ein einzelner USB-Bluetooth-Adapter mit externer Antenne (FSC-BP119) direkt am Home-Assistant-Host. Seitdem Ruhe.

Setup

  • Home Assistant Green

  • SwitchBot über Bluetooth/Cloud: Lock Pro, 2× Meter (Thermometer), 2× Deckenpräsenzsensoren, Bettsensoren, LED-Streifen

  • 3–4 ESP32-Boards als bluetooth_proxy (ESPHome), verteilt über Bad, Wohnzimmer, Keller, Schlafzimmer

  • FRITZ!Box 7590 AX + FRITZ!Repeater 600 + FRITZ!Powerline 1260 im Mesh

Das Fehlerbild

Sporadisch, über Wochen, ohne erkennbares Muster:

  • ESPHome-Log voller [Errno 113] Connect call failed, Handshake timed out after 60.0s, EncryptionHelloAPIError und stale advertisement subscriber-Warnungen

  • Geräte fielen in HA auf unavailablewährend das WLAN nachweislich weiterlief (also ein API-/Verbindungsproblem, kein reiner Funkabriss)

  • Auffällig: mehrere Proxies fielen oft gleichzeitig aus, obwohl sie an verschiedenen Access-Points hingen

  • Konkrete Folgeschäden: Türschloss meldete veraltete Zustände (Verriegelung wurde fälschlich als erfolgreich gewertet), LED-Streifen ließ sich nicht zuverlässig schalten, Präsenz-/Bettsensoren froren ein

Was alles NICHT geholfen hat

Der Reihe nach ausprobiert und einzeln widerlegt:

  1. Firmware aktualisieren — Versionsrückstand war nicht die Ursache (Fehlertyp passte nicht).

  2. „Ballast" aus der YAML werfen (web_server, captive_portal, web_server_idf) — kein Effekt auf die Ausfälle.

  3. Board-Tausch (Verdacht: defekte Hardware, Feuchtigkeit im Bad) — neues Board zeigte identisches Verhalten.

  4. Netzteil und USB-Kabel tauschen — keine Änderung.

  5. FRITZ!Box Mesh-Steering deaktivieren — beendete das Wandern der Geräte zwischen Knoten, aber nicht die Abrisse.

  6. Feste 2,4-GHz-Kanäle statt Autokanal — keine Besserung.

  7. Proxies per bssid: an einen festen Access-Point binden — hielt teils nicht (Fallback aktivierte sich) und löste die Abrisse ebenfalls nicht.

  8. CPU/RAM/Temperatur des HA-Hosts geprüft (System-Monitor-Integration) — Host war im Leerlauf, Temperatur unkritisch. Kein Lastproblem.

Die entscheidenden Diagnose-Hinweise

  • FRITZ!Box-Ereignisprotokoll (WLAN-An-/Abmeldungen aktivieren!) zeigte teils echte WLAN-Abmeldungen — aber nicht zu allen HA-Ausfällen. Es gab Ausfälle, bei denen das WLAN stand.

  • Der SwitchBot-Hub am selben WLAN lief durchgehend stabil, während die ESP32-Proxies fielen. Das lenkte den Verdacht weg vom WLAN hin zu den ESP32-Boards selbst.

  • Kernverdacht am Ende: WLAN-/BLE-Koexistenz auf dem ESP32. Ein ESP32 teilt sich eine Antenne für WLAN und Bluetooth. Aktive BLE-Verbindungsversuche (Schalten, Verriegeln, Zustand lesen) blockieren kurzzeitig das Funkmodul — genau dann bricht die WLAN-/API-Seite ein. Das erklärt gleichzeitige Ausfälle mehrerer Proxies (HA probiert aktive Verbindungen reihum) und warum es an jedem Standort, mit jeder Firmware, auf altem wie neuem Board auftrat.

Was es gelöst hat: FSC-BP119 am HA-Host

Ein USB-Bluetooth-Adapter mit externer Antenne (FSC-BP119, CSR8510-Chip) per USB-Verlängerung an den Home-Assistant-Host, räumlich abgesetzt von den anderen Funkmodulen (Zigbee/Thread), um gegenseitige Störung zu minimieren.

Wichtige Punkte beim Umstieg:

  • Adapter im ausgeschalteten Zustand einstecken, damit sich die USB-Port-Zuordnung der anderen Funk-Sticks (Zigbee/Thread) nicht verschiebt.

  • Erst einstecken und messen, nicht sofort Proxies abbauen. Über die Bluetooth-Diagnosedaten (Diagnosedaten herunterladen auf der Adapter-Geräteseite) sieht man pro Gerät, welcher Scanner es mit welchem RSSI empfängt.

  • Der neue Adapter war bei nahezu allen Geräten der stärkste Empfänger im ganzen Haus — vom zentral gelegenen Büro aus deckte er alle Etagen ab (RSSI meist deutlich besser als die Proxies).

  • Proxies einzeln deaktivieren (nicht physisch, sondern die ESPHome-Integrationseinträge), mit Kontrolle nach jedem Schritt. So sieht man sofort, falls ein Gerät doch nur über einen bestimmten Proxy erreichbar war.

Ergebnis

  • Türschloss verriegelt auf die Sekunde pünktlich und liefert frische Zustandsrückmeldung (Reaktionszeit ~4–5 s statt minutenlanger Cloud-Verzögerung). Die falsch-positiven „verriegelt"-Meldungen sind weg.

  • LED-Streifen schaltet zuverlässig in beide Richtungen, beim ersten Versuch, mit Bestätigung.

  • Präsenz- und Bettsensoren melden in Echtzeit, kein Einfrieren mehr.

  • ESPHome-Fehlerlog still, nachdem die Proxies deaktiviert waren.

Fazit / Empfehlung

ESP32-BLE-Proxies sind bequem und für passives Mitlesen von Advertisements oft ausreichend. Sobald aber aktive BLE-Verbindungen ins Spiel kommen (Schlösser, schaltbare Geräte, Zustandsabfragen), stößt die WLAN-/BLE-Koexistenz des ESP32 an Grenzen — und das äußert sich als scheinbar zufällige Errno 113-/Handshake-/unavailable-Fehler, denen man mit Firmware- und WLAN-Tuning ewig hinterherjagen kann.

Wer dieses Fehlerbild hat und dessen HA-Host zentral genug steht, sollte einen USB-Bluetooth-Adapter mit ordentlicher Antenne direkt am Host ernsthaft in Betracht ziehen, bevor man Wochen in Proxy-Diagnose steckt. In meinem Fall hat ein einziges Teil vier Proxies ersetzt und alle Symptome auf einmal beseitigt.

Wichtige Einschränkung: Reichweite ist der Knackpunkt. Bluetooth verliert an Wänden/Decken mehr als WLAN. Das funktioniert nur, wenn der Host zentral steht und die Antenne die Distanzen packt — vorher per Diagnosedaten messen, nicht raten.

Gruß

Daniel

Gott…diese schrecklichen KI Texte heutzutage :face_vomiting:

Aber ich muss dagegen halten, ich habe 10 ESP32 BLE-Proxys seit 2 Jahren im Einsatz die auch immer schön geupdatet werden…laufen alle. Mittlerweile können diese sogar verschieden eingestellt werden. Je nach Szenario brauchen diese nämlich andere Timings.

Aber ja, was ein Problem werden kann ist die Co-Existenz mit dem WLAN, was sich aber mit 2-3 Zeilen lösen lässt

api:
  on_client_connected:
    then:
      - esp32_ble_tracker.start_scan:
          continuous: true
  on_client_disconnected:
    if:
      condition:
        not:
          api.connected:
      then:
        - esp32_ble_tracker.stop_scan:

esp32_ble_tracker:
  scan_parameters:
    active: true
    continuous: false
    interval: 300ms
    window: 211ms
    duration: 60s

bluetooth_proxy:
  active: true
  cache_services: true

Sorgt dafür das Bluetooth erst angeschalten wird wenn WLAN, respektive HA Verbunden ist und gleichermassen das es sich wieder ausschaltet wenn der ESP seine Verbindung verliert. Das verhindert das das WLAN klemmt.

Ich hab halt C3 im Einsatz, weil das umgeflashte Shelly Minis sind (waren billiger, weil Gehäuse und Netzteil schon inkludiert), die haben auch eine abartige Reichweite. Die haben nur einen Kern und sind sogar anfälliger für das BLE/WLAN Co-Existenz Problem…so sagt man zumindest :man_shrugging:


Scan Window wird errechnet aus dem Interval und dem Duty Cycle, ESP Standard ist 37%, man kann aber bequem bis 80% hochgehen, muss man ein bisschen probieren, ohne WLAN Probleme zu bekommen.

3 „Gefällt mir“

Haha, ertappt – ja, das war mit KI gebaut. Ehrlich gesagt könnte ich das Ganze nie so sortiert mit eigenen Worten runterschreiben, dafür nehm ich die KI gern zu Hilfe. Dass es dabei nach Maschine klingt, ist der Preis, geschenkt.

Aber danke fürs Snippet, das kannte ich so nicht – und das ist genau der Hebel, den ich nie probiert hab. Ich hab mich an power_save_mode, Firmware, Boardtausch und am Mesh abgearbeitet, aber den BLE-Scan einfach abzuschalten wenn HA weg ist, war nirgends auf meinem Schirm. Wenn ich das lese, glaub ich sofort, dass das meine „alle Proxys gleichzeitig weg"-Ausfälle stark entschärft hätte. Also ja, mein Bericht klingt kategorischer gegen ESP-Proxys als er sollte.

Dass ich trotzdem beim USB-Stick gelandet bin, ist weniger technisch als bequem: Ich hatte am Ende vier Proxys übers Haus verteilt, und die einzeln zu pflegen und zu tunen war mir schlicht zu viel Gefummel. Ein Stick am HA, der alles abdeckt – fertig. Bei zehn Stück wie bei dir, die eh laufen, würd ich das auch nicht anfassen, never touch a running system.

Die C3 aus umgeflashten Shelly Minis sind übrigens ein feiner Trick, auf die Reichweite wär ich nie gekommen. Das Duty-Cycle-Tuning probier ich bei Gelegenheit mal an einem der übrig gebliebenen Boards aus, rein aus Neugier.

Das mit der Reichweite hab ich durch Zufall gecheckt, da ich mit den Shellys die BLE Gardena-Ventile am anderen Ende des Gartens aufeinmal managen konnte, das sind 14m bis dahin, durch eine Wand durch. Ich hab auch nur mehrere Proxies da wir Raumerkennung mit Bermuda am Laufen haben.

Mit dem Duty Cycle das ist eine gute Rechnung, man kann wohl bis auf 90% hochgehen ohne das es Auswirkungen auf das Wifi hat, ich bleib aber bei 70%, sicherheitshalber, läuft bis dato stabil

Viele Bermuda Nutzer gehen auf 90% und auf ein grösseres window, damit jeder Ping von den Smartphones getrackt wird und so Lichter schneller reagieren wenn man das Smartphone dazu nutzt. Machen wir nicht. Wir tracken eher sowas wie "Sind beide Handys eine halbe Stunde im Schlafzimmer am Laden und es brennen noch Lichter, dann sind wir wohl eingeschlafen, bzw. morgens wenn wir das Ladegerät abziehen und im Schlafzimmer sind, dann mach die Guten Morgen Schaltung. Von daher nicht so kriegsentscheidend. Ich bin auf kürzere Fenster gegangen um Bewegungsmelder und Präsenzsensoren nicht zu verpassen, weil der Sensor sendet ja nur bei Belegt/Frei und nicht dauernd wie ein Smartphone.

Habs heute noch mal angepasst

globals:
  - id: v3_scan_parameters_interval
    type: int
    restore_value: no
    initial_value: "0"

  - id: v3_scan_parameters_window
    type: int
    restore_value: yes
    initial_value: "150"

  - id: v3_scan_parameters_duty_cycle
    type: int
    restore_value: yes
    initial_value: "70"

  - id: v3_scan_parameters_duration
    type: int
    restore_value: yes
    initial_value: "60"

  - id: v3_interval_hard_max
    type: int
    restore_value: no
    initial_value: "1100"

api:
  on_client_connected:
    - delay: 3s
    - switch.turn_on: ble_scan
  on_client_disconnected:
    - if:
        condition:
          not:
            api.connected:
        then:
          - switch.turn_off: ble_scan

script:
  - id: ble_restart_debounce
    mode: restart
    then:
      - delay: 5s
      - switch.turn_on: ble_scan

esp32_ble_tracker:
  id: ble_tracker
  scan_parameters:
    continuous: false

bluetooth_proxy:
  active: true
  cache_services: true

switch:
  - platform: template
    name: BLE Scan
    icon: mdi:bluetooth-audio
    id: ble_scan
    optimistic: true
    restore_mode: ALWAYS_OFF
    turn_on_action:
      - esp32_ble_tracker.stop_scan:
      - lambda: |-
          id(ble_tracker).set_scan_interval(id(v3_scan_parameters_interval) / 0.625);
          id(ble_tracker).set_scan_window(id(v3_scan_parameters_window) / 0.625);
          id(ble_tracker).set_scan_duration(id(v3_scan_parameters_duration));
          id(ble_tracker).set_scan_active(true);
          id(ble_tracker).set_scan_continuous(true);
      - delay: 300ms
      - esp32_ble_tracker.start_scan:
          continuous: true
      - lambda: |-
          id(ble_tracker).dump_config();
    turn_off_action:
      - esp32_ble_tracker.stop_scan:

number:
  - platform: template
    name: "1 Scan Window"
    id: scan_window
    max_value: 400
    min_value: 20
    step: 10
    mode: slider
    unit_of_measurement: "ms"
    initial_value: 150
    restore_value: true
    optimistic: true
    icon: mdi:bluetooth-transfer
    on_value:
      - globals.set:
          id: v3_scan_parameters_window
          value: !lambda 'return x;'
      - lambda: |-
          float min_duty = (float) id(v3_scan_parameters_window) /
                            (float) id(v3_interval_hard_max) * 100.0;
          if (id(duty_cycle).state < min_duty) {
            auto call = id(duty_cycle).make_call();
            call.set_value(ceil(min_duty));
            call.perform();
          } else {
            int interval = (int)(id(v3_scan_parameters_window) * 100.0 /
                                  id(v3_scan_parameters_duty_cycle));
            id(v3_scan_parameters_interval) = interval;
            id(scan_interval_display).publish_state(interval);
          }
      - switch.turn_off: ble_scan
      - script.execute: ble_restart_debounce

  - platform: template
    name: "2 Duty Cycle"
    id: duty_cycle
    max_value: 100
    min_value: 10
    step: 5
    mode: slider
    unit_of_measurement: "%"
    initial_value: 70
    restore_value: true
    optimistic: true
    icon: mdi:bluetooth-transfer
    on_value:
      - lambda: |-
          float min_duty = (float) id(v3_scan_parameters_window) /
                            (float) id(v3_interval_hard_max) * 100.0;
          if (x < min_duty) {
            // Wert unzulässig -> auf erlaubtes Minimum zurückkorrigieren
            auto call = id(duty_cycle).make_call();
            call.set_value(ceil(min_duty));
            call.perform();
            return;   // die Korrektur löst on_value erneut aus, dort greift dann der else-Zweig
          }
          id(v3_scan_parameters_duty_cycle) = (int) x;
          int interval = (int)(id(v3_scan_parameters_window) * 100.0 / x);
          id(v3_scan_parameters_interval) = interval;
          id(scan_interval_display).publish_state(interval);
      - switch.turn_off: ble_scan
      - script.execute: ble_restart_debounce

  - platform: template
    name: "3 Scan Duration"
    id: scan_duration
    max_value: 300
    min_value: 0
    step: 10
    mode: slider
    unit_of_measurement: "s"
    initial_value: 60
    restore_value: true
    optimistic: true
    icon: mdi:bluetooth-transfer
    on_value:
      - if:
          condition:
            wifi.connected:
          then:
            - globals.set:
                id: v3_scan_parameters_duration
                value: !lambda "return x;"
            - switch.turn_off: ble_scan
            - script.execute: ble_restart_debounce

sensor:
  - platform: template
    name: "Calculated Scan Interval"
    id: scan_interval_display
    unit_of_measurement: "ms"
    icon: mdi:bluetooth-transfer
    accuracy_decimals: 0

Viel Spass beim Spielen :slight_smile:

1 „Gefällt mir“