HomeBlogAboutTools

Proxying Bluetooth to a Home Assistant VM


If you run Home Assistant OS in a virtual machine on a host with an Intel CNVi Bluetooth adapter, you’ve probably discovered the hard way that USB passthrough simply doesn’t work. I spent a lot of time trying before building a solution: a proxy that forwards raw HCI packets over a serial port.

See the repo here

Add BT Proxy to Home Assistant

The Problem

Intel CNVi Bluetooth adapters (the combo WiFi+BT cards integrated into many Intel chipsets) are fundamentally incompatible with QEMU USB passthrough. The adapter depends on the host’s btusb driver for firmware loading and USB enumeration. Unloading btusb causes the device to disappear from the USB bus entirely. QEMU’s USB reset during passthrough puts the device into bootloader mode, and firmware reload fails from the VM side. See my detailed description here if you want more details.

In my case, I’m running a fanless Bosgame AG40 with a Celeron N4020. This doesn’t seem available anymore, but has been running HomeAssistant very well for 12 months.

The Solution: HCI Packet Proxying

Instead of passing the USB device, we can proxy raw HCI packets between the host and VM over a serial port backed by a UNIX socket.

The architecture looks like this:

  1. Host side: A Python script (hci-proxy.py) opens the Bluetooth adapter using HCI_CHANNEL_USER, which gives exclusive raw HCI access, bypassing the host’s BlueZ stack. It forwards packets to a UNIX socket using H4 (UART Transport) framing.

  2. VM side: libvirt exposes the UNIX socket as an ISA serial port (/dev/ttyS1). A Home Assistant Add-on runs btattach to create a standard HCI device from the serial port. BlueZ picks it up as a normal Bluetooth controller.

The result: Home Assistant sees a real hci0 device and its Bluetooth integration works normally — BLE scanning, device discovery, the lot.

How H4 Transport Works

The H4 UART Transport protocol is straightforward. Each HCI packet gets a 1-byte type prefix:

Type BytePacket TypeDirection
0x01HCI CommandHost -> Controller
0x02ACL DataBidirectional
0x04HCI EventController -> Host

The proxy parses HCI packet headers to determine packet boundaries (each type has a defined header format with an embedded length field), reassembles complete packets from the stream, and forwards them in both directions.

Key Implementation Details

HCI_CHANNEL_USER is a Linux-specific raw HCI socket mode (AF_BLUETOOTH, BTPROTO_HCI, channel=1). It provides exclusive, unfiltered access to the Bluetooth controller. The trade-off is that the host loses Bluetooth while the proxy is running — bluetooth.service gets stopped automatically.

Why ISA serial and not virtio-serial? btattach requires a device that supports termios ioctls. Virtio-serial char devices don’t support these, so we need an old-school ISA serial port (/dev/ttyS1) which is a proper tty device. This was a fun one to debug.

HCI Reset on startup. The proxy sends an HCI Reset command when it starts to clear any stale scanning or advertising state left from the host’s BlueZ session. Without this, you get command timeouts.

ctypes for socket bind. Python’s socket.bind() doesn’t support the sockaddr_hci format needed for HCI_CHANNEL_USER, so we call libc’s bind() directly via ctypes. Not pretty, but it works.

Passive Scanning

One caveat: the latency added by the proxy path can cause issues with active BLE scanning. The add-on enables passive scanning by default (via btmgmt), which is sufficient for most BLE devices. You may also need to enable passive scanning in the relevant Home Assistant integration settings.

This was fairly confusing - I thought active scanning would find more devices for sure, but no - passive works much better.

Getting It Running

The setup involves three pieces:

  1. Add a serial port to the VM — a libvirt XML snippet that connects a UNIX socket to an ISA serial port
  2. Run the proxy on the host — a systemd service that manages the Python proxy script
  3. Install the HA add-on — either as a local add-on or from a custom repository, which runs btattach and monitors the HCI device

The full installation instructions and code are available on GitHub.

Was It Worth It?

Absolutely. The proxy has been running reliably, forwarding BLE advertisements from temperature sensors, plant monitors, and other devices through to Home Assistant. It’s not the most elegant solution — proxying Bluetooth over a fake serial port feels like a hack — but it solves a real problem with no hardware workarounds available.

If you’re running HAOS in a VM on Intel hardware and struggling with Bluetooth, give it a try.

Disclaimer

Mostly vibe coded. Thanks Claude!