Automatically reattach USB devices to KVM VM on using serial number (e.g. for Zigbee/Thread sticks)

:jigsaw: Automatically reattach USB devices to KVM VM using serial number (e.g. for Zigbee/Thread sticks)

:question: Problem

When passing USB devices like Zigbee or Thread dongles (e.g. Silicon Labs CP210x) through to a KVM virtual machine (e.g. Home Assistant OS), you typically use the vendor and product ID for passthrough via virsh.

However, after a server reboot, the bus/device address can change, and sometimes the device is no longer correctly attached to the VM β€” especially if multiple devices share the same vendor/product ID, like two identical Silicon Labs sticks (one for Zigbee, one for Thread).

This guide shows how to reliably re-attach USB devices to a VM based on their unique serial number β€” automatically at boot.


:white_check_mark: Solution Overview

  • Use a Bash script to identify the correct USB device via serial number.
  • Detach any previously attached device with the same vendor/product ID.
  • Attach the correct one using its current bus/device address.
  • Run this script via systemd after boot, once libvirtd is ready.

:gear: Step 1: The USB Attach Script

:page_facing_up: Path: /home/user/scripts/hassos_vm_usb_attach.sh

#!/bin/bash

exec >> /var/log/hassos_vm_usb_attach.log 2>&1
echo "[INFO] Starting script $(date)"

VM_NAME="hassos"
SUCCESS=0

handle_usb_device() {
    SERIAL="$1"
    VENDOR="$2"
    PRODUCT="$3"

    echo "[INFO] Handling USB device with serial $SERIAL"

    virsh dumpxml "$VM_NAME" > /tmp/vm_config.xml
    xmlstarlet sel -t -c "//hostdev[source/vendor[@id='0x$VENDOR'] and source/product[@id='0x$PRODUCT']]" /tmp/vm_config.xml > /tmp/old_usb.xml

    if [[ -s /tmp/old_usb.xml ]]; then
        echo "[INFO] Removing old USB device entries..."
        virsh detach-device "$VM_NAME" /tmp/old_usb.xml --config
        virsh detach-device "$VM_NAME" /tmp/old_usb.xml --live
    fi

    for dev in /sys/bus/usb/devices/*; do
        if [[ -f "$dev/serial" ]] && [[ $(cat "$dev/serial") == "$SERIAL" ]]; then
            BUSNUM=$(printf "%03d" "$(cat "$dev/busnum")")
            DEVNUM=$(printf "%03d" "$(cat "$dev/devnum")")

            BUSNUM=$(echo $BUSNUM | sed 's/^0*//')
            DEVNUM=$(echo $DEVNUM | sed 's/^0*//')

            cat > /tmp/usb-device.xml <<EOF
<hostdev mode='subsystem' type='usb' managed='yes'>
  <source>
    <vendor id='0x$VENDOR'/>
    <product id='0x$PRODUCT'/>
    <address bus='$BUSNUM' device='$DEVNUM'/>
  </source>
</hostdev>
EOF

            virsh attach-device "$VM_NAME" /tmp/usb-device.xml --config
            virsh attach-device "$VM_NAME" /tmp/usb-device.xml --live
            SUCCESS=1
            return
        fi
    done

    echo "[WARN] Device with serial $SERIAL not found"
}

# === Add your USB devices here ===
handle_usb_device "0ec5a3f1de49ef1185b9d58cff00cc63" "10c4" "ea60"  # Example: Zigbee
# handle_usb_device "SERIAL2" "VENDOR2" "PRODUCT2"  # Example: Thread

if [[ $SUCCESS -eq 1 ]]; then
    echo "[INFO] Script finished successfully"
    exit 0
else
    echo "[ERROR] No USB device was attached"
    exit 1
fi

:pushpin: Don’t forget to make it executable:

chmod +x /home/user/scripts/hassos_vm_usb_attach.sh

:safety_pin: Step 2: Create a systemd service

:page_facing_up: Path: /etc/systemd/system/usb-vm-attach.service

[Unit]
Description=Attach USB device to KVM VM
Requires=libvirtd.service
After=libvirtd.service

[Service]
Type=oneshot
ExecStart=/home/user/scripts/hassos_vm_usb_attach.sh
RemainAfterExit=true

[Install]
WantedBy=multi-user.target

Enable and test the service:

sudo systemctl daemon-reload
sudo systemctl enable usb-vm-attach.service
sudo systemctl start usb-vm-attach.service
journalctl -u usb-vm-attach.service -b

:mag: How to get your USB device info (Vendor, Product, Serial)

  1. First list USB devices:
lsusb

Example:

Bus 001 Device 005: ID 10c4:ea60 Silicon Labs CP210x UART Bridge
  1. Then get detailed info:
udevadm info --name=/dev/bus/usb/001/005 --query=all

Look for:

  • ID_VENDOR_ID
  • ID_MODEL_ID
  • ID_SERIAL

:screwdriver: Notes

  • The script uses xmlstarlet, which you may need to install:
sudo apt install xmlstarlet
  • Works with any Linux distribution using KVM + libvirt (virsh).
  • Tested with Home Assistant OS VM and Silicon Labs CP210x sticks for Zigbee and Thread.

Happy automating! :brain::electric_plug:

1 Like

Working fine, Thanks :+1::+1::+1: