This post is a revamped version of the previous post on the same topic. The new instructions include:

  1. installing mosquitto and Z2M in a separate jail,
  2. a symlink from the /dev/cuaXX to /zigbee, and
  3. allowing mosquitto to receive connections from outside the jail.

In a previous post, we got Home Assistant Core and it’s dependencies installed into a Bastille jail. We were able to start HA were greeted by the initial landing page. Now it’s time to make HA more useful by pulling in Zigbee data.

Why Zigbee? It’s what I have

Now there are two ways to let HA use Zigbee, Zigbee Home Automation and Zigbee2MQTT. I am going to use Zigbee2MQTT because I prefer to keep services separate where possible.

Software Link to heading

  • Home Assistant Core 2024.4 (installed in a previous post)
  • Zigbee2MQTT 1.36.1 (installed from Github)
  • Mosquitto 2.0.18 (installed from FreeBSD ports)

Getting the Zigbee USB dongle ready Link to heading

I know that the Zigbee dongle is recognised by the FreeBSD kernel because I get this output from dmesg when I pull it out and plug it back in:

ugen2.3: <ITead Sonoff Zigbee 3.0 USB Dongle Plus> at usbus2 (disconnected)
uslcom0: at uhub1, port 2, addr 2 (disconnected)
uslcom0: detached
ugen0.4: <ITead Sonoff Zigbee 3.0 USB Dongle Plus> at usbus0
uslcom0 on uhub2
uslcom0: <ITead Sonoff Zigbee 3.0 USB Dongle Plus, class 0/0, rev 2.00/1.00, addr 3> on usbus0

On my system, the dongle is accessible at /dev/cuaU0. So far, I haven’t worked out how to rename this to something unique to this dongle, for example /dev/zigbee. I can use devd to create a symlink in /dev but this symlink is not accessible within the jail. I asked on the FreeBSD forums and maximbo suggested that I use devd to create a symlink from /dev/cuaU0 to anywhere else in the jail.

On my system, the dongle shows up at /dev/cuaU0. However, this could change if we have a second USB device and restart the computer. Since we know that the dongle will always have a name that starts with /dev/cuaXX, we could simply allow the jail to see all of the /dev/cuaXX devices. But we would still have the problem of not knowing which /dev/cuaXX device is the zigbee dongle.

So what I want to do is tell the FreeBSD host to create a symlink from /dev/cuaXX to /zigbee inside the jail when the dongle is plugged in. To do this, we can use the devd system on FreeBSD to run some commands when a particular device is attached or detached. We can use the Vendor and Product IDs to ensure that devd creates /zigbee only for that type of device. If you have two identical Zigbee dongles, you can use serial numbers help distinguish between dongles.

Getting the IDs of the Zigbee dongle Link to heading

The dmesg output above shows that the address of the Zigbee dongle is ugen0.4. We could also get this address by running:

host # usbconfig 

<other devices removed>
ugen0.4: <ITead Sonoff Zigbee 3.0 USB Dongle Plus> at usbus0, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (100mA)

What we want is the Vendor and Product IDs of the Zigbee dongle:

host # usbconfig -d ugen0.4 -v

ugen0.4: <ITead Sonoff Zigbee 3.0 USB Dongle Plus> at usbus0, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (100mA)
ugen0.4.0: uslcom0: <ITead Sonoff Zigbee 3.0 USB Dongle Plus, class 0/0, rev 2.00/1.00, addr 4>

  bLength = 0x0012 
  bDescriptorType = 0x0001 
  bcdUSB = 0x0200 
  bDeviceClass = 0x0000  <Probed by interface class>
  bDeviceSubClass = 0x0000 
  bDeviceProtocol = 0x0000 
  bMaxPacketSize0 = 0x0040 
  idVendor = 0x10c4                     <------- Vendor ID
  idProduct = 0xea60                    <------- Product ID
  bcdDevice = 0x0100 
  iManufacturer = 0x0001  <ITead>
  iProduct = 0x0002  <Sonoff Zigbee 3.0 USB Dongle Plus>
  iSerialNumber = 0x0003  <a6716f079912ec11975c20c7bd930c07>    <------ You could also use this
  bNumConfigurations = 0x0001                                           if you have multiple dongles.

<there is more output that is not relevant here>

Creating a custom devd rule Link to heading

Using this information, we create custom devd rules that only works when a device matches the vendor and product. Thanks to tspi.at for some guidance here. The first rule is activated when the dongle is attached and creates a symlink from /dev/cuaXX to <root of the jail>/zigbee and applies the appropriate permissions to the link. The second rule is activated when the dongle is detached and removes the symlink.

host # vim /usr/local/etc/devd/zigbee.conf 

attach 100 {
        match "vendor" "0x10c4";
        match "product" "0xea60";
        action "ln -sf /dev/cua$ttyname /usr/local/bastille/jails/mqtt/root/zigbee && chown -h uucp:dialer /usr/local/bastille/jails/mqtt/root/zigbee";
};

notify 100 {
        match "type" "DETACH";
        match "vendor" "0x10c4";
        match "product" "0xea60";
        action "rm -rf /usr/local/bastille/jails/mqtt/root/zigbee";
};

Now we restart the devd service:

host # service devd restart

Passing the /dev/cuaXX device to the jail Link to heading

By default, the /dev/cuaU0 device is not visible within the jail:

jail # ls -lah /dev/
dr-xr-xr-x   9 root wheel     512B Dec 24 18:15 .
drwxr-xr-x  14 root wheel      24B Dec 10 16:33 ..
crw-------   1 root wheel     0x27 Dec 24 18:15 bpf
lrwxr-xr-x   1 root wheel       3B Dec 24 18:15 bpf0 -> bpf
crw-rw-rw-   1 root wheel     0x42 Dec 13 11:55 crypto
dr-xr-xr-x   2 root wheel     512B Dec 24 18:15 fd
crw-rw-rw-   1 root wheel     0x14 Dec 24 19:11 null
crw-------   1 root wheel     0xab Dec 13 11:56 pf
dr-xr-xr-x   2 root wheel     512B Dec 24 18:15 pts
crw-r--r--   1 root wheel      0x4 Dec 13 11:56 random
lrwxr-xr-x   1 root wheel       4B Dec 24 18:15 stderr -> fd/2
lrwxr-xr-x   1 root wheel       4B Dec 24 18:15 stdin -> fd/0
lrwxr-xr-x   1 root wheel       4B Dec 24 18:15 stdout -> fd/1
lrwxr-xr-x   1 root wheel       6B Dec 24 18:15 urandom -> random
crw-rw-rw-   1 root wheel     0x15 Dec 13 11:55 zero
crw-rw-rw-   1 root operator  0x3f Dec 13 11:55 zfs

To make it visible, we need to allow the mqtt jail to see cuaXX devices.

First, we create a new ruleset on the host in /etc/devfs.rules:

host # vim /etc/devfs.rules

[bastille_vnet=13]        <---- This is what you will have if you are using VNET jails
#add path 'bpf*' unhide
#add include $devfsrules_jail
add include $devfsrules_hide_all
add include $devfsrules_unhide_basic
add include $devfsrules_unhide_login
add include $devfsrules_jail
add include $devfsrules_jail_vnet
add path 'bpf*' unhide

[bastille_mqtt=14] <---- This is what you need to create
add include $devfsrules_hide_all
add include $devfsrules_unhide_basic
add include $devfsrules_unhide_login
add include $devfsrules_jail
add include $devfsrules_jail_vnet
add include $bastille_vnet
add path 'cua*' unhide      <---- The important part

bastille_vnet is what you would have created when you were setting up the jails. bastille_mqtt is what we’ve created specially for the mqtt jail. It includes all of the rules used by the bastille_vnet ruleset, plus another rule that allows cuaXX devices to be visible.

Next, you need to restart the devfs service:

host # service devfs restart

Then, we change the ruleset used by the home-assistant jail:

host # vim /usr/local/bastille/jails/home-assistant/jail.conf

mqtt {
  devfs_ruleset = 14;               <----- Set this to the number of the ruleset you just created
  enforce_statfs = 2;
  exec.clean;
  <Other lines unchanged>
}

Finally, restart the jail.

Now, from inside the jail you can see the newly unhidden devices in /dev and the newly created link /zigbee:

jail # ls -lah /dev/

dr-xr-xr-x   9 root wheel     512B Dec 24 18:15 .
drwxr-xr-x  14 root wheel      24B Dec 10 16:33 ..
crw-------   1 root wheel     0x27 Dec 24 18:15 bpf
lrwxr-xr-x   1 root wheel       3B Dec 24 18:15 bpf0 -> bpf
crw-rw-rw-   1 root wheel     0x42 Dec 13 11:55 crypto
crw-rw----   1 uucp dialer    0xe1 Dec 24 13:22 cuaU0       <----- Here,
crw-rw----   1 uucp dialer    0xee Dec 24 13:22 cuaU0.init  <----- here,
crw-rw----   1 uucp dialer    0xf2 Dec 24 13:22 cuaU0.lock  <----- and here!
dr-xr-xr-x   2 root wheel     512B Dec 24 18:15 fd
crw-rw-rw-   1 root wheel     0x14 Dec 24 19:11 null
crw-------   1 root wheel     0xab Dec 13 11:56 pf
dr-xr-xr-x   2 root wheel     512B Dec 24 18:15 pts
crw-r--r--   1 root wheel      0x4 Dec 13 11:56 random
lrwxr-xr-x   1 root wheel       4B Dec 24 18:15 stderr -> fd/2
lrwxr-xr-x   1 root wheel       4B Dec 24 18:15 stdin -> fd/0
lrwxr-xr-x   1 root wheel       4B Dec 24 18:15 stdout -> fd/1
lrwxr-xr-x   1 root wheel       6B Dec 24 18:15 urandom -> random
crw-rw-rw-   1 root wheel     0x15 Dec 13 11:55 zero
crw-rw-rw-   1 root operator  0x3f Dec 13 11:55 zfs

jail # ls -lah /

drwxr-xr-x  14 root wheel    25B Jan  8 21:07 .
drwx------   3 root wheel     5B Jan  8 20:47 ..
drwxr-xr-x  18 root wheel    22B Nov 23 15:39 .bastille
-rw-r--r--   1 root wheel   1.0K Nov 10 19:11 .cshrc
-rw-r--r--   1 root wheel   495B Nov 10 19:11 .profile
drwxr-xr-x   2 root wheel     2B Jan  7 11:17 .template
-r--r--r--   1 root wheel   6.0K Nov 10 19:49 COPYRIGHT
lrwxr-xr-x   1 root wheel    14B Jan  7 11:17 bin -> /.bastille/bin
lrwxr-xr-x   1 root wheel    15B Jan  7 11:17 boot -> /.bastille/boot
dr-xr-xr-x   9 root wheel   512B Jan  8 20:47 dev
drwxr-xr-x  30 root wheel   108B Jan  7 13:10 etc
lrwxr-xr-x   1 root wheel     8B Jan  7 11:17 home -> usr/home
lrwxr-xr-x   1 root wheel    14B Jan  7 11:17 lib -> /.bastille/lib
lrwxr-xr-x   1 root wheel    18B Jan  7 11:17 libexec -> /.bastille/libexec
drwxr-xr-x   2 root wheel     2B Nov 10 18:48 media
drwxr-xr-x   2 root wheel     2B Nov 10 18:48 mnt
drwxr-xr-x   2 root wheel     2B Nov 10 18:48 net
dr-xr-xr-x   2 root wheel     2B Nov 10 18:48 proc
lrwxr-xr-x   1 root wheel    17B Jan  7 11:17 rescue -> /.bastille/rescue
drwxr-x---   2 root wheel    10B Jan  8 20:47 root
lrwxr-xr-x   1 root wheel    15B Jan  7 11:17 sbin -> /.bastille/sbin
drwxrwxrwt   6 root wheel     6B Jan  8 20:48 tmp
drwxr-xr-x   6 root wheel    15B Jan  7 11:17 usr
drwxr-xr-x  24 root wheel    24B Jan  8 20:47 var
lrwxr-xr-x   1 uucp dialer   10B Jan  8 21:07 zigbee -> /dev/cuaU0    <---- And here

Mosquitto MQTT Link to heading

As I mentioned earlier, I will be using Zigbee2MQTT. This requires a MQTT server. Luckily, Mosquitto is available in the FreeBSD ports. By default, mosquitto will only accept connections coming from the jail where it is installed. You will need to make a couple of changes to the configuration if you want to allow outside connections.

jail # pkg install mosquitto

jail # service mosquitto enable
jail # vim /usr/local/etc/mosquitto/mosquitto.conf

listener 1883                   <------Add this around line 234
allow_anonymous true            <------Add this around line 532

You can’t just add these two configuration options to the bottom of the config file. Mosquitto seems to be picky about the order of options and will fail to start if these lines are at the bottom. (I haven’t tried putting them at the top of the file).

Finally, start the mosquitto service

jail # service mosquitto start

Starting mosquitto.
1703409712: mosquitto version 2.0.15 starting
1703409712: Config loaded from /usr/local/etc/mosquitto/mosquitto.conf.
1703409712: Create a configuration file which defines a listener to allow remote access.
1703409712: Opening ipv4 listen socket on port 1883.
1703409712: Opening ipv6 listen socket on port 1883.

jail # sockstat -4

USER     COMMAND    PID   FD  PROTO  LOCAL ADDRESS         FOREIGN ADDRESS      
nobody   mosquitto  45787 3   tcp4   *:*                   *:*
nobody   mosquitto  45787 4   tcp4   *:1883                *:*

Mosquitto is now accessible from anywhere on your network. I haven’t gone into detail on how to set up security, limit access, etc. That’s for another day

Zigbee2MQTT Link to heading

Unfortunately, Zigbee2MQTT is not available in the ports, but it is very easy to get up and running. I am going to install Z2M on the same jail as the MQTT broker. (These instructions are my adaptation of the official instructions.)

Requirements Link to heading

  1. You will need a working MQTT broker to get Z2M running.
  2. You will also need to install node npm git gmake gcc.

Installation Link to heading

Clone the git repository, install the node dependencies, update the configuration to point to your zigbee controller, and start.

jail # git clone --depth 1 https://github.com/Koenkk/zigbee2mqtt.git

jail # cd zigbee2mqtt

jail ~/zigbee2mqtt # npm ci

added 813 packages, and audited 814 packages in 7s

90 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

jail ~/zigbee2mqtt # vim data/configuration.yaml 

homeassistant: true
permit_join: true
mqtt:
  base_topic: zigbee2mqtt
  server: mqtt://127.0.0.1:1883
serial:
  port: /zigbee
  adapter: zstack
frontend: true
advanced:
  log_output:
    - console
  log_level: info               <----- debug will give a lot of output
  timestamp_format: YYYY-MM-DD HH:mm:ss
  channel: 25

jail ~/zigbee2mqtt # npm start

zigbee2mqtt@1.34.0 start
node index.js

<I've cut down the output here to show you the main things to look for>
Zigbee2MQTT:info  2023-12-24 20:30:26: Starting Zigbee2MQTT version 1.34.0 (commit #56589dc)
Zigbee2MQTT:info  2023-12-24 20:30:26: Starting zigbee-herdsman (0.25.0)
Zigbee2MQTT:info  2023-12-24 20:30:31: Coordinator firmware version: '{"meta":{"maintrel":1,"majorrel":2,"minorrel":7,"product":1,"revision":20210708,"transportrev":2},"type":"zStack3x0"}'
Zigbee2MQTT:warn  2023-12-24 20:30:31: `permit_join` set to  `true` in configuration.yaml.
Zigbee2MQTT:warn  2023-12-24 20:30:31: Allowing new devices to join.
Zigbee2MQTT:warn  2023-12-24 20:30:31: Set `permit_join` to `false` once you joined all devices.
Zigbee2MQTT:info  2023-12-24 20:30:31: Zigbee: allowing new devices to join.
Zigbee2MQTT:info  2023-12-24 20:30:31: Connecting to MQTT server at mqtt://127.0.0.1:1883
Zigbee2MQTT:debug 2023-12-24 20:30:31: Using MQTT anonymous login
Zigbee2MQTT:info  2023-12-24 20:30:31: Connected to MQTT server
Zigbee2MQTT:info  2023-12-24 20:30:31: MQTT publish: topic 'zigbee2mqtt/bridge/state', payload '{"state":"online"}'
Zigbee2MQTT:info  2023-12-24 20:30:31: Started frontend on port 8080
Zigbee2MQTT:info  2023-12-24 20:30:31: Zigbee2MQTT started!

You will see of output here as you add devices to the network, especially if you set the logging to debug.

We can set this up to run as a daemonised service with logging:

jail # vim /usr/local/etc/rc.d/zigbee2mqtt 

#!/bin/sh

# PROVIDE: zigbee2mqtt
# REQUIRE: DAEMON NETWORKING
# BEFORE: LOGIN
# KEYWORD: shutdown

. /etc/rc.subr

name="zigbee2mqtt"
rcvar=zigbee2mqtt_enable

: ${zigbee2mqtt_enable:="NO"}

# daemon
pidfile="/var/run/${name}.pid"
node="/usr/local/bin/node"
script_js="/root/zigbee2mqtt/index.js"
command=/usr/sbin/daemon
#procname="daemon"
command_args="-S -l local0 -s info -P ${pidfile} -r ${node} ${script_js}"

load_rc_config $name
run_rc_command "$1"

jail # vim /etc/syslog.conf 
#
#       Spaces ARE valid field separators in this file. However,
#       other *nix-like systems still insist on using tabs as field
#       separators. If you are sharing this file between systems, you
#       may want to use only tabs as field separators here.
#       Consult the syslog.conf(5) manpage.
*.err;kern.warning;auth.notice;mail.crit                /dev/console
*.notice;authpriv.none;kern.debug;lpr.info;mail.crit;news.err   /var/log/messages
security.*                                      /var/log/security
auth.info;authpriv.info                         /var/log/auth.log
mail.info                                       /var/log/maillog
cron.*                                          /var/log/cron
!-devd
*.=debug                                        /var/log/debug.log
*.emerg                                         *
daemon.info                                     /var/log/daemon.log
local0.info                                     /var/log/zigbee2mqtt.log        <------ Add this line here
<Later lines removed>

After you restart the syslogd service, enable and then start the zigbee2mqtt service, you can head to jail-ip-address:8080 to see the Zigbee2MQTT dashboard. If this is not working, check the log files for any misconfigurations.

Configuring devices Link to heading

In the Z2M dashboard, you can give custom names to devices as you add them. These names will be passed through to Home Assistant as device names.

Getting Zigbee into Home Assistant Link to heading

All that is needed now is to install and configure the MQTT integration into Home Assistant. Go to Settings -> Devices & services -> + ADD INTEGRATION. Search for and add MQTT. Set the broker to the MQTT jail IP address and the port to 1883. Wait a few minutes and your added Zigbee devices should be discovered by Home Assistant.