Alternative KVM HAOS USB zigbee adapter config

Hello from a new user,

In most guides I run across the common way to connect the HAOS KVM guest with a USB zigbee adapter is with USB passthrough, but unfortunately USB passthrough protocol might use more CPU in the host than the HAOS guest itself.

It’s perhaps simpler to configure ZHA with the guest using USB passthrough first because the UI seem to like to see the USB device, but once you successfully configured ZHA, and you notice the increased CPU load in the host, you may want to switch the config to use pci-serial.

In qemu command line terms it means to replace:

-device usb-host,vendorid=0x0000,productid=0x0000

with:

-chardev serial,path=/dev/ttyUSB0,id=serial-usb0 -device pci-serial,chardev=serial-usb0

You also need the cp210x kernel module in loaded in the host (east to check with lsmod). At last you need to be in the dialout group (as in /etc/group) or otherwise to have enough permission to open /dev/ttyUSB0.

After the KVM config change, you can boot again and then ssh into HAOS (inside the ssh container, host root not required) and edit ~/config/.storage/core.config_entries, search 115200 and change the line above that starts with “/dev/serial…” to point to “/dev/ttyS1” or maybe S0 if you had no other serial line. The actual number depends on your machine type topology. Then reboot (the guest).

At the next boot, assuming you picked the right guest serial line number, ZHA should be working again, no need to touch the ZHA configuration UI.

If pci-serial doesn’t work for whatever reason, it should be possible to reboot the guest back into USB passthrough mode. Once back in USB passthrough mode, the HAOS UI should be able to fixup ZHA with a few clicks. I successfully switched back and forth from USB passthrough and pci-serial with the above procedure no apparent regression.

Hope this helps save some CPU power. It should be applicable to all KVM based solutions using the qcow2 image, enjoy.

Thanks for this.
I’m making a few notes along the way and was wondering if you could also dump the portion of the xml file that pertains to this as I’ve seen other HA Forum users use <serial type="dev"> in their xml which I suspect results in the same qemu paramter settings as you mentioned, but just want to see the xml for comparison.

Also, can you comment on roughly how much CPU power was saved?

Thanks :slight_smile:

I’m invoking qemu directly without xml, the only tricky part of doing that is the clean shutdown that requires sending the system_powerdown command to the monitor and to wait 20sec.

So I haven’t tested with the xml, so I’m just guessing here that it may be something along these lines:

<devices>
 <serial type='serial'>
   <source path='/dev/ttyUSB0/>
   <target port='1'/>
 </serial>
</devices>

You may want to try to use virsh domxml-to-native to convert from xml to the qemu command line, then make the edits to the command line, and then re-import with virsh domxml-from-native. Then you may also want to double check with “pgrep -al qemu” that you got the right command line executed with chardev serial, pci-serial and no usbdevice.

Also I forgot to mention the qemu backend serial driver implementation probably only works for serial devices supporting baud rate 115200. The adapter I use was configured at 115200 by default by ZHA incidentally.

In the worst case the xml has an option to go manual and append a string to the qemu command line, but it shouldn’t be necessary. So the last resort if the conversion to xml fails could be:

<qemu:commandline>
          <qemu:arg value='......'/>
</qemu:commandline>

The CPU utilization in my case (on a fairly old i7 x86-64) was about 9% of one core, let’s say it was a bit too far from idle to do nothing about it. strace -p $(pgrep qemu) would show a ppoll flood caused by the ppoll timeout set around 1msec, so I’d run thousand of ppoll syscall per second and ppoll isn’t exactly fast either, unlike epoll. After removing USB passthrough the ppoll timeout of the main loop is up to 0.5sec and the CPU utilization is 0.7% according to top.

If your qemu process uses ~0.x% of CPU in top with USB passthrough already, I’d be interested to know just in case some other sw version fares better. I almost never used USB passthrough before with KVM, so I’m not exactly sure what kind of overhead is to be expected when emulating the USB protocol in the guest… Thanks!

I finally got around to trying to replicate this QEMU setup by using virt-manager/libvirt, and after several attempts/hours, I hit a snag that I couldn’t overcome.

  1. XML: As suggested using:
<devices>
 <serial type='serial'>
   <source path='/dev/ttyUSB0/'>
   <target port='1'/>
 </serial>
</devices>

I found out that serial type='serial' is not supported by libvirt.

  1. So let’s try re-importing the qemu command/args using virsh domxml-from-native
    This produces an error saying the driver does not support this. After further investigation, it turns out libvirt dropped support of this.
  2. Let’s try QEMU Command Passthrough
    I found using virt-xml DOMAIN --edit --confirm --qemu-commandline="-device FOO" was the easiest way to do this. So I did the following (where hassio is the DOMAIN):
virt-xml hassio --edit --confirm --qemu-commandline="-device pci-serial,chardev=serial-usb0"
virt-xml hassio --edit --confirm --qemu-commandline="-chardev serial,path=/dev/ttyUSB0,id=serial-usb0"

This will add to the hassio.xml file:

  <qemu:commandline>
    <qemu:arg value='-chardev'/>
    <qemu:arg value='serial,id=serial-usb0,path=/dev/ttyUSB0'/>
    <qemu:arg value='-device'/>
    <qemu:arg value='pci-serial,chardev=serial-usb0'/>
  </qemu:commandline>

I used the domxml-to-native command to confirm that this produces the correct qemu command/args.

Now I tried to run the “hassio” VM with virt-manager and it fails saying:

libvirt.libvirtError: internal error: process exited while connecting to monitor: 2023-10-09T17:29:04.146767Z qemu-system-x86_64: -chardev serial,id=serial-usb0,path=/dev/ttyUSB0: Could not open '/dev/ttyUSB0': No such file or directory

well /dev/ttyUSB0 is definitely there.

I notice too that the following works from the command line without any error (me as user):
$ /usr/bin/qemu-system-x86_64 -chardev serial,path=/dev/ttyUSB0,id=serial-usb0

I notice too that if I add the following to the XML file:

       <serial type="dev">
         <source path="/dev/ttyUSB0"/>
         <target type="pci-serial">
         </target>
         <alias name="serial4"/>
       </serial>

that virt-manager runs fine for the VM and there is no error about finding/reading /dev/ttyUSB0.

I just could not get this to work, and kinda conclude that there is a bug in libvirt somewhere along the way regarding the file pathname.

However to finish out with one more thing. I did continue running with the above XML file, and virt-manager/libvirt fill out the rest to produce the following:

    <serial type='dev'>
      <source path='/dev/ttyUSB0'/>
      <target type='pci-serial' port='2'>
        <model name='pci-serial'/>
      </target>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x08' function='0x0'/>
    </serial>

I restarted the VM, and see that from UI->Settings->System->Hardware->All hardware that I find ttyS2 matches the PCI properties for the “pci-serial” target.
DEVNAME: /dev/ttyS2 DEVPATH: /devices/pci0000:00/0000:00:08.0/tty/ttyS2

But for the SkyConnect used with multi-protocol, this results in a failure:

[15:58:08:889282] Info : Connecting to Secondary...
[15:58:10:889436] Info : Failed to connect, secondary seems unresponsive

So at this point, I’ve given up and gone back to the USB Host Device pass-through.

Did you edit the config file in the guest to swap /dev/serial… with /dev/ttyS2 and set to 115200? That I had to do by hand searching for 115200.

On my side PCI serial has been working perfectly and now that the guest containerd stopped consuming CPU, the host is fully idle. So it saved a significant amount of CPU over almost 1 year.

I would try the same config with a fedora vm image and then try to interact with tryS2 on the guest to see if it behaves like on the host. You can also use fuser on the host to see if the VM opened /dev/tryUSB0 on the host. You may also want to check selinux isn’t blocking /dev/ttyUSB0 just in case.

Frankly I haven’t tried with libvirt as I preferred to use passt as network data plane for the VM so haos network works more like a container, with all egress connections owned by the kvm user or whatever user runs KVM. This way I need no bridge, nothing runs as root, nor libvirt, nor any network helper script, etc…

Your approach of passing the qemu command line directly to libvirt looks good, I can’t tell for sure why it didn’t work.

Have you tried pci-serial with SkyConnect? and in particular, for Thread?

Sorry no, I just use it with the sonoff zigbee controller. I noticed in the current version of ZHA detects ttyS? without having to edit the config file anymore, so now it’s even easier.

So the other option you have to reduce the CPU usage to 0% in the host that will sure work, is to assign to the guest the whole pci usb controller your usb device is connected to, so pci-passthrough instead of usb-passthrough.

I just did that for the yale bluetooth integration, certainly I couldn’t use pci-serial to export the bluetooth usb device to the guest…

You’re going to have multiple usb pci controllers (see lspci -vv). With lsusb you should try to plug your usb device in a way that is the only device in one of the usb controllers.

Then the command line is like this:

echo 0000:0?:00.0 > /sys/bus/pci/drivers/xhci_hcd/unbind # you find the PCI bus number in lspci -vv
modprobe vfio-pci
echo ??? ??? >/sys/bus/pci/drivers/vfio-pci/new_id # you find the PCI ID in lspci -vvn

check with “readlink /sys/bus/pci/devices/0000:0?:00.0/iommu_group” the iommu group and then:
chown haos /dev/vfio/22

Then you can run kvm normally as user “haos” and the usb pci controller will be recognized fine by HA in the guest.

You also need to increase the mlock ulimit to the size of the VM or unlimited with /etc/security/limits.conf

With libvirt the the device assignment might be easier than what I wrote above, unlike pci-serial it’s a supported config, but I prefer to use passt as network transport for haos.