Skip to content

Instantly share code, notes, and snippets.

@shbatm
Last active November 9, 2024 01:06
Show Gist options
  • Save shbatm/1526174a9d285c342eb05c9efa35c02d to your computer and use it in GitHub Desktop.
Save shbatm/1526174a9d285c342eb05c9efa35c02d to your computer and use it in GitHub Desktop.
Home Assistant USBIP Z-Wave Setup w/ More Robust Restart Management

Files Included:

  1. usbipd.service (server/physical device)
  2. usbip.service (client/HASS host)
  3. udev rules file for client host -- maps USB device to /dev/zwave (add to /etc/udev/rules.d/)
  4. Client Manager Script

Setup:

See the original instructions here: https://community.home-assistant.io/t/rpi-as-z-wave-zigbee-over-ip-server-for-hass/23006 also see the notes below in each file.

About the Manager Script on the client

This uses a manager script (basically an expanded init script) on the hass.io host machine to control the USB-IP device and manage the connections.

  1. Before connecting: Ping the host and see if it's available. If not, wait and retry using an exponential delay (1s, 2s, 4s, 16s, etc. to 256s). This gives time after, for example, a whole home power outage, where everything comes back at the same time, but you have to wait for the other server to come up first).
  2. After confirming the host is there, check if the USB-IP service is available. If it isn't, try to restart the service on the remote machine using SSH. FOR THIS TO WORK you must have Public Key Authorization set up both directions on the 2 machines.
  3. After mounting the device, restart the homeassistant docker, if it's already running.

About the server service file

The server's .service file actually includes a line to call the client (via SSH) and attempt to restart the client's service. It will continue on if it fails, but if it succeeds, this will cascade to re-mount the port on the client and restart Home Assistant.

# UDEV RULES FOR HASS.IO HOST MACHINE (ADD TO /etc/udev/rules.d)
SUBSYSTEM=="tty", ATTRS{idVendor}=="0658", ATTRS{idProduct}=="0200", SYMLINK+="zwave", TAG+="systemd"
# SERVICE FILE FOR USBIP CLIENT ON HASS.IO HOST MACHINE
# UDPATE THE PATH BELOW TO YOUR HOME DIRECTORY. USING EXAMPLE FOR DEFAULT
# RASPBERRY PI USERS: /home/pi/
# SAVE TO `/lib/systemd/system/` AND RUN `sudo systemctl enable usbip.service` TO ENABLE.
[Unit]
Description=usbip client
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/home/pi/scripts/usbip/usbipManager.sh start
ExecStop=/home/pi/scripts/usbip/usbipManager.sh stop
[Install]
WantedBy=multi-user.target
# SERVICE FILE FOR USBIP SERVER ON HOST MACHINE WITH USB CONNECTED
# UPDATE LINE 13 TO THE DETAILS FOR YOUR HASS.IO HOST MACHINE
# MAKE SURE THE ROOT USER ON THIS MACHINE CAN USE PUBLIC KEYS TO LOGIN VIA SSH
# TO THE HASS.IO HOST.
# SAVE TO `/lib/systemd/system/` AND RUN `sudo systemctl enable usbip.service` TO ENABLE.
[Unit]
Description=usbip host daemon
After=network-online.target
[Service]
Type=forking
ExecStart=/usr/sbin/usbipd -D
ExecStartPost=/bin/sh -c "/usr/sbin/usbip bind --$(/usr/sbin/usbip list -p -l | grep '#usbid=0658:0200#' | cut '-d#' -f1)"
ExecStartPost=-/bin/sh -c "/usr/bin/ssh -t [email protected] 'sudo systemctl restart usbip.service'"
ExecStop=/bin/sh -c "/usr/sbin/usbip unbind --$(/usr/sbin/usbip list -p -l | grep '#usbid=0658:0200#' | cut '-d#' -f1); killall usbipd"
[Install]
WantedBy=multi-user.target
#!/bin/bash
###################################################################
#Script Name : usbipManager
#Description : USB IP Client Manger
#Args : start / stop / restart / status
#Author : shbatm
#Email : [email protected]
#Installation : Install to a convenient location and update the
# path used in usbip.service file.
# Make sure you update the variables below
###################################################################
USBIP_SERVER_IP='192.168.1.3'
USB_SERIAL_ID='0658:0200'
REMOTE_USER='pi'
REMOTE_SERVICE='usbipd.service'
HASS_DOCKER_NAME='homeassistant'
ATTEMPTS=8
USBIP="/usr/local/sbin/usbip"
name=`basename $0`
SUDO=''
if (( $EUID != 0 )); then
SUDO='sudo'
fi
# Backoff script from http://stackoverflow.com/a/8351489/580412
function with_backoff {
local max_attempts=${ATTEMPTS-5}
local timeout=${TIMEOUT-1}
local attempt=0
local exitCode=0
while [[ $attempt < $max_attempts ]]
do
"$@" > /dev/null 2>&1
exitCode=$?
if [[ $exitCode == 0 ]]
then
break
fi
echo "Failure! Retrying in $timeout.." 1>&2
sleep $timeout
attempt=$(( attempt + 1 ))
timeout=$(( timeout * 2 ))
done
if [[ $exitCode != 0 ]]
then
echo "Giving up! ($@)" 1>&2
fi
echo $exitCode
return $exitCode
}
check_host() {
# Check if the USB IP Host is online.
reachable=$(with_backoff ping -c1 -W1 $USBIP_SERVER_IP)
if [[ $reachable != 0 ]]
then
# TODO: Send WOL or other wakeup command to make sure host is on.
echo "Error: Host unreachable at $USBIP_SERVER_IP." 1>&2
exit 1
fi
if $SUDO $USBIP list -r $USBIP_SERVER_IP 2>&1 | grep -q 'could not connect'; then
echo "Could not connect to USBIP Host Service at $USBIP_SERVER_IP." 1>&2
# Attempt to restart remote service using ssh.
# Note: Public Key Auth must be setup between the servers.
echo "Attempting to restart $REMOTE_SERVICE on $USBIP_SERVER_IP..."
ssh -t ${REMOTE_USER}@${USBIP_SERVER_IP} "sudo systemctl restart $REMOTE_SERVICE"
# Script exits here because service start on remote host will re-call this script on start.
exit 1
else
USB_BUS_ID=$($SUDO $USBIP list -r $USBIP_SERVER_IP | grep $USB_SERIAL_ID | cut -d: -f1)
echo "Using USB Bus ID: $USB_BUS_ID"
return 0
fi
}
attach_host() {
$SUDO $USBIP attach -r $USBIP_SERVER_IP -b $USB_BUS_ID
}
restart_hass() {
if [[ "$(/usr/bin/docker ps -q -f name=$HASS_DOCKER_NAME)" ]]; then
echo "Home Assistant is running. Restarting..."
/usr/bin/docker restart $HASS_DOCKER_NAME
fi
}
is_running() {
$SUDO $USBIP port | grep '<Port in Use>' -q
}
case "$1" in
start)
if is_running; then
echo "Port is active."
else
echo "Attempting connection"
check_host
attach_host
sleep 1
if ! is_running; then
echo "Unable to start $name. Unspecified error." 1>&2
exit 1
fi
echo "Successfully started $name."
restart_hass
fi
;;
stop)
if is_running; then
echo -n "Stopping $name..."
$SUDO $USBIP detach --port=$($USBIP port | grep '<Port in Use>' | sed -E 's/^Port ([0-9][0-9]).*/\1/') > /dev/null 2>&1
for i in 1 2 3 4 5 6 7 8 9 10
# for i in `seq 10`
do
if ! is_running; then
break
fi
echo -n "."
sleep 1
done
echo
if is_running; then
echo "Not stopped; may still be shutting down or shutdown may have failed."
exit 1
else
echo "Stopped."
fi
else
echo "Not running."
fi
;;
restart)
$0 stop
if is_running; then
echo "Unable to stop, will not attempt to start"
exit 1
fi
$0 start
;;
status)
if is_running; then
echo "Running"
else
echo "Stopped"
exit 1
fi
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment