This article is part of the Homelab Self-Hosting Guide – the curated learning path for your own homelab.
The problem: after a reboot everything is scrambled
Anyone running a Raspberry Pi or homelab server with several USB devices knows this: a Zigbee coordinator stick sits at /dev/ttyUSB0, a Z-Wave stick at /dev/ttyUSB1. Everything works – until the next reboot. Suddenly the numbers are swapped, Zigbee2MQTT talks to the Z-Wave stick, and the smart home is upside down.
The cause: Linux assigns the ttyUSB numbers in the order the USB devices are detected at boot. That order isn't deterministic. The fix is a udev rule that gives each device a fixed symlink name – set it up once, never think about it again.
Ad · Affiliate link – if you buy through it, I may earn a commission. It doesn’t change the price for you.
Read the USB device's vendor and product ID with udevadm info, create a rule in /etc/udev/rules.d/, run udevadm trigger – done. From then on the device is reachable under a fixed name like /dev/ttyZCOORD.
Identify the USB device
First we need the device's unique IDs. Plug in the stick and read the attributes:
udevadm info --name=/dev/ttyUSB0 --attribute-walkThe output is long – we're looking for idVendor and idProduct. This combination uniquely identifies the device type:
looking at parent device '/devices/platform/.../usb1/1-1/1-1.2':
KERNELS=="1-1.2"
SUBSYSTEMS=="usb"
DRIVERS=="usb"
ATTRS{bDeviceClass}=="00"
ATTRS{bMaxPower}=="100mA"
ATTRS{idProduct}=="ea60"
ATTRS{idVendor}=="10c4"
ATTRS{manufacturer}=="Silicon Labs"
ATTRS{product}=="slae.sh cc2652rb stick - slaesh's iot stuff"In this case: idVendor="10c4" and idProduct="ea60" – a Silicon Labs CP2102-based Zigbee stick. Note the values, we'll need them in a moment.
Create the udev rule
Now we create a rule file that automatically creates a fixed symlink on plug-in or at boot:
sudo nano /etc/udev/rules.d/98-usb-device.rulesFile contents – replace idVendor and idProduct with your own values; SYMLINK is the name the device should be reachable under from now on:
KERNEL=="ttyUSB?", SUBSYSTEMS=="usb", ATTRS{idProduct}=="ea60", ATTRS{idVendor}=="10c4", SYMLINK+="ttyZCOORD"If you have two USB devices with identical vendor/product IDs (e.g. two Silicon Labs sticks), this method alone isn't enough. You then also have to filter by ATTRS{serial} or the physical USB port (KERNELS). Otherwise both end up on the same symlink.
Apply and test the rule
Load the new rules – a full reboot isn't necessary:
sudo udevadm triggerCheck whether the symlink is there:
ls -l /dev/ttyZ*
lrwxrwxrwx 1 root root 7 28. Jun 00:17 /dev/ttyZCOORD -> ttyUSB0From now on /dev/ttyZCOORD always points to the right stick – no matter which ttyUSB number Linux gives it at boot. In Zigbee2MQTT, Z-Wave JS or ESPHome, just enter the symlink as the device path.
Where this matters in the homelab
This pattern comes up for me all the time:
- Zigbee coordinator (CC2652, SkyConnect) →
/dev/ttyZCOORD - Z-Wave stick (Aeotec Z-Stick) →
/dev/ttyZWAVE - ESP32 flasher via UART →
/dev/ttyESPFLASH
Especially when several sticks hang off the same host at once, this is the only sensible solution. Without udev rules, every reboot is a gamble.
What I left out
A few things deliberately not covered here:
- USB devices without a ttyUSB interface (e.g. pure HID devices) – a different udev mechanism applies there.
- systemd dependencies. If a service expects the symlink at boot before udev runs the rule, you need an
After=dependency. That's a topic of its own. - Docker passthrough. If the stick is used inside a Docker container, you additionally have to map
--device /dev/ttyZCOORDinto the container.
Conclusion
A udev rule is five minutes of work and saves you years of debugging after every reboot. For every USB device you run permanently – Zigbee, Z-Wave, GPS, serial adapter – set up a fixed symlink. It's one of those "why didn't I do this right away" moments.
Ad · Affiliate link – if you buy through it, I may earn a commission. It doesn’t change the price for you.