Home Assistant + Z-Wave

For the last 3 years or so, I’ve been running Home Assistant for my home automation needs. I’ve gone through a couple iterations with it being hosted on bare metal on a raspberry pi, in a python virtual environment, and within a docker container. I started off mainly with just smart lights so wi-fi bulbs worked pretty well. After time, concerns with security and general scale led me to start using Z-Wave and Zigbee devices. These required physical hardware to hook up to the raspberry pi (After switching around a bit I currently use the Nortek HUSBZB-1).

Within Home Assistant, this requires linking to the tty device like so

zwave:
  usb_path: /dev/ttyUSB0
zha:
  usb_path: /dev/ttyUSB1

On bare metal, and a virtual python environment, this is dead simple. Docker has an extra step but it isn’t that complicated. When starting the container you just need to add an extra --device flag like so.

docker run --device=/dev/ttyUSB0:/dev/ttyUSB0 --device=/dev/ttyUSB1:/dev/ttyUSB1 homeassistant/home-assistant

Everything was working great and I could control my devices. So I should have probably stopped there, but of course I couldn’t just leave well enough alone.

Enter Kubernetes

Around a year ago I upgraded to an Intel NUC as I wanted to host more containers for various other services (this blog for example). As part of that I wanted to toy around with orchestrating everything with Kubernetes. I had some experience with it at work and even though a single node, bare metal environment isn’t what it’s suited for, I still wanted to give it a whirl.

Cue several weekends of banging my head against Kubernetes documentation

After a couple weeks I had most of what I wanted set up. All of the new services that I wanted running were behaving satisfactorily. I had one issue though, Home Assistant, the first service that started me down the path of running containers, wasn’t playing so nicely with Kubernetes. The main problem was there is no direct equivalent to the --device flag (See a related GitHub Issue). There was a workaround that did make everything seem alright. On the pod container configuration, under securityContext if privileged was set to true the container could mount devices successfully.

containers:
  - name: homeassistant
    ...
    volumeMounts:
    - mountPath: /dev/ttyUSB
      name: dev-usb0
    securityContext:
      privileged: true
volumes:
  - name: dev-usb0
    hostPath:
      path: /dev/ttyUSB0

However making a container privileged comes with a whole slew of other consequences besides being able to mount devices like allowing root users running in the container essentially to act like root users on the host machine (As of writing, Home Assistant does run as root within its container). This left a sour taste in my mouth. I didn’t like running the container with such privileges, but there wasn’t an alternative that I could find at the time so I dealt with it.

Enter ser2net & socat

Several months later, I stumbled upon a post talking about two programs ser2net and socat. In a grossly oversimplified explanation. ser2net will expose devices (like the Z-Wave / Zigbee usb stick) over a TCP connection, while socat can consume a TCP connection and “recreate” the device on a different machine. This combination would allow the usb stick to be mounted on an entirely different machine, but for my purposes ser2net will be run on the host machine, while socat will be run inside the container.

Setting up ser2net

After installing ser2net, modifying /etc/ser2net.conf was needed to expose the usb device. The Nortek HUSBZB-1, while being one physical USB stick, actually exposes two tty devices (/dev/ttyUSB0 for Z-Wave, and dev/ttyUSB1 for Zigbee) so both will need to be exposed.

3333:raw:0:/dev/ttyUSB0:115200 8DATABITS NONE 1STOPBIT
3334:raw:0:/dev/ttyUSB1:57600 8DATABITS NONE 1STOPBIT

Essentially each line exposes one device, the first one on port 3333, the second one on port 3334. As an aside, I was only able to get this setup to work with having the devices at different baud rates.

Setting up socat

Initially, I had wanted to use a separate container in the home assistant pod but due to how socat handles the user defined location of the device file (a symbolic link to /dev/pts), sharing the mounted device between containers was a hurdle I couldn’t overcome. This left the option of running socat within the homeassistant container itself. Ideally this would probably be a custom built docker image with additional binaries installed and a custom entrypoint, but I enjoy the frequent updates that home assistant provides and didn’t want to maintain a new image. This left installing, and running socat from the kubernetes configuration! Firstly, I added the required init.d scripts to get socat running on boot in a configmap.

apiVersion: v1
kind: ConfigMap
metadata:
  name: homeassistant-systemd-services
data:
  zwave: |
    #!/bin/bash
    case "$1" in 
        start)
          socat pty,link=/dev/ttyUSB0,raw,user=0,group=0,mode=777 tcp:[NODE_IP]:3333 &
          ;;
        stop)
          stop
          ;;
        restart)
          stop
          start
          ;;
        status)
          ;;
        *)
          echo "Usage: $0 {start|stop|status|restart}"
    esac
    exit 0 
  zha: |
    #!/bin/bash
    case "$1" in 
        start)
          socat pty,link=/dev/ttyUSB1,raw,user=0,group=0,mode=777 tcp:[NODE_IP]:3334 &
          ;;
        stop)
          stop
          ;;
        restart)
          stop
          start
          ;;
        status)
          ;;
        *)
          echo "Usage: $0 {start|stop|status|restart}"
    esac
    exit 0 

The important part of the script being:

socat pty,link=/dev/ttyUSB0,raw,user=0,group=0,mode=777 tcp:[NODE_IP]:3333

which attempts to open a connection to the node at port 3333(where [NODE_IP] is the actual ip address of the device running ser2net).

The configmap then had to be mounted as a volume so the container could see it. (I had to manually change the permissions of the file so it could be used)

volumes:
  - name: zwave-config
    configMap:
    name: homeassistant-systemd-services
    defaultMode: 0755
    items: 
        - key: zwave
          path: zwave
  - name: zha-config
    configMap:
    defaultMode: 0755
    name: homeassistant-systemd-services
    items: 
        - key: zha
          path: zha

Then socat needed to be installed when the container started. Additionally the two services needed to be run. This was done modifying the container’s command and args properties.

spec:
  containers:
    - name: homeassistant
        image: homeassistant/home-assistant
        ports: 
          - containerPort: 8123
          - containerPort: 8300
        command: ["sh"]
        args: ["-c", "apt-get update && apt-get install socat && service zwave start && service zha start && python -m homeassistant --config /config"]
        volumeMounts:
          - name: config
            mountPath: "/config"
          - name: zwave-config
            mountPath: /etc/init.d/zwave
            subPath: zwave
          - name: zha-config
            mountPath: /etc/init.d/zha
            subPath: zha

Once all the configurations had been applied, home assistant picked up on the z-wave and zigbee devices and my lights were connected once more.

Not the most elegant solution, but getting to remove that privileged configuration felt pretty good.