zigbee2mqtt workaround

Posted on Oct 7, 2023

I recently ran into a annoying bug with zigbee2mqtt (which otherwise is excellent!) which requires me to restart the application if it ever loses it’s connection to it’s MQTT-broker. This means that my indoor lights won’t work after any updates or restarts of the machine running Mosquitto.

I solved this by making everything a bit more complicated.

Old setup

The old setup looked something like below – one SBC running zigbee2mqtt, another host running Eclipse Mosquitto, and yet another machine running Home Assistant.

Old setup, one MQTT broker

Very straight forward. However, if I ever restarted mqtt.example.com I had to restart my zigbee2mqtt container as well. After a few power outages recently I got sick of this.

New setup

So the new setup looks something like this:

New setup, two MQTT brokers

I’ve introduced a Mosquitto broker on the SBC running zigbee2mqtt. Both are deployed using docker-compose, so if I restart z2m I will restart the broker as well. The broker will bridge the topic zigbee2mqtt/# to/from the main broker, and if I restart the main broker the local broker will reconnect automatically, while remembering any messages not delivered yet.

For zigbee2mqtt, I just had to change which broker it connected to. My main broker required no configuration change.

docker-compose.yml

This is the yaml file that runs on my SBC:

# In my case, this file is saved to /srv/docker-compose.yml
version: '3'
services:
  zigbee2mqtt:
    container_name: zigbee2mqtt
    restart: unless-stopped
    image: koenkk/zigbee2mqtt
    volumes:
      - /srv/zigbee2mqtt/data:/app/data
      - /run/udev:/run/udev:ro
    environment:
      - TZ=Europe/Stockholm
    devices:
      - /dev/ttyUSB0:/dev/ttyUSB0
  mosquitto:
    container_name: mosquitto
    image: eclipse-mosquitto:2
    restart: unless-stopped
    volumes:
      - /srv/mosquitto/config:/mosquitto/config:ro
      - /srv/mosquitto/data:/mosquitto/data
      - /srv/mosquitto/log:/mosquitto/log

I’m not exporting any MQTT ports in this case, and my zigbee2mqtt configuration connects to mosquitto:1883 (this hostname will resolve to the container IP as they share namespace).

# cd /srv/
# docker-compose pull && docker-compose up -d

mosquitto.conf

This is the mosquitto.conf running on the SBC, the “local broker”.

acl_file /mosquitto/config/acl
allow_anonymous false
log_dest file /mosquitto/log/mosquitto.log
password_file /mosquitto/config/users
persistence true
persistence_location /mosquitto/data

listener 1883
protocol mqtt

connection mainbroker
address mqtt.example.com:1883
keepalive_interval 59

local_password replacemeplz
local_username zigbee2mqtt
remote_password replacemeplz
remote_username zigbee2mqtt

topic zigbee2mqtt/# both 0

You have two usernames and passwords here – one for the local broker, and one for the remote broker. The local user needs to have sufficient permissions to read/write the queues on the local broker, and the same on the remote broker. If you only want to setup a one-way mirror (mirroring a local topic to a remote server) it’s enough with read permissions on the local broker, and readwrite on the remote system. In this case we want readwrite on both sides.

You can probably disable authentication for the local broker (as it’s not exposed outside of the docker namespace) but I’ve kept it due to reasons.

The last line in the config file sets up the bridge. In this case we will bridge topic zigbee2mqtt/# on the remote server as a topic with the same name, and will bridge in both directions, so Home Assistant can write to the main broker and make zigbee2mqtt apply the actions.

acl & password file

To make it easy, you can use the same username, password and ACLs on both sides. For the local broker, these are saved to /srv/mosquitto/config.

The password file can be generated like this:

# cd /srv/mosquitto/config
# mosquitto_password -c users zigbee2mqtt

The ACL file can look something like this:

user zigbee2mqtt
topic readwrite zigbee2mqtt/#

Conclusion

Now I can restart my main MQTT broker without having to restart the container as well. Yay.