Setting up an ESP32-C6 for Thread with a container-based Home Assistant
This is a hopefully useful bit of documentation for the things I ran into whilst setting this up, since I've not found anything that covered all of the components together in the way I needed them. You probably don't have my setup, but maybe some aspects of it apply here.
What I had and what I wanted
I had an existing Home Assistant installation running in Podman on my (Debian) server. This was using a Podman network with both IPv4 and IPv6. Crucially, this was running as a normal container, without any access to interfaces outside the Podman network or similar.
What I wanted was the ability to add Thread devices to my Home Assistant instance, having bought some IKEA GRILLPLATS smart plugs for energy monitoring (not an affiliate link).
I wanted to do this using an ESP32-C6, as I happened to have a Seeed Studio Xiao ESP32-C6 (also not an affiliate link, but as of writing you can apparently use code IP4A0Q50 on any of the variants for a 2% discount and some credit to me)
How I eventually got it working
There are three chunks here; the radio firmware, the supporting software to run outside of Home Assistant (OpenThread Border Router – OTBR – and matterjs-server), and the Home Assistant services. They should probably be tackled in that order.
One big caveat here is that I'm not sure to what extent you need IPv6 on your wider network for this to work. I already have IPv6 everywhere, including in my Podman network, so this wasn't a concern, but Matter is an IPv6-based protocol.
Another important note is that I found a repository claiming to have firmware to run an ESP32-C6 as a standalone border router, which I dismissed when I realised it was just some Claude output, but when I found the upstream version it made the point that you probably don't want to do this because the ESP32-C6 can't do Wi-Fi and Thread at the same time, so performance will take a hit.
Radio firmware
This is one of the more straightforward parts if you're familiar with using a Linux command line. This post on the Home Assistant forums documents the process. I built my firmware using ESP-IDF 6.1 and manually applying the patch for the external antenna in that thread.
Supporting software
There are two bits of software you need to run; OpenThread Border Router to connect your Thread network to your network, and the Open Home Foundation Matter Server, which is the Matter Server. The name gave that away, really. OTBR is the more complicated of the two to get running, with the Matter server being pretty straightforward. For my setup I also needed mdns-reflector to get mDNS traffic into my Podman network for Home Assistant.
As mentioned earlier, I'm using Podman on my server, and have an existing Home Assistant container running on it. I use Ansible for my management of this system, and so I'll use excerpts from my Ansible playbook to document how I've configured things, but it's not particularly difficult to map between it and other methods of running it like docker compose. I'll include the task in full, but only parts in the containers.podman.podman_container section are important for actually running the container.
OpenThread Border Router
First thing's first, I created a udev rule to create a symlink from /dev/thread to the /dev/ttyACM* entry for the ESP32-C6. This is the rule I added in /etc/udev/rules.d/49-thread.rules:
ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", \
ENV{ID_MM_DEVICE_IGNORE}="1"
ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", \
ENV{MTP_NO_PROBE}="1"
ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", \
SUBSYSTEMS=="usb", MODE:="0666"
ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", \
KERNEL=="ttyACM*", MODE:="0666"
ACTION=="add", \
ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", \
SYMLINK+="thread"
If your ESP32 doesn't show up as 303a:1001 in lsusb, then replace those numbers in the file to match yours.
For OTBR itself, I'm using https://github.com/ownbee/hass-otbr-docker as the image. My Ansible task for setting this up is the following:
- name: Create OTBR container
become: yes
containers.podman.podman_container:
image: "ghcr.io/ownbee/hass-otbr-docker"
name: otbr
network: host
state: present
privileged: true
volume:
- "/home/dave/otbr_data:/data/thread"
device:
- "/dev/thread:/dev/thread"
- "/dev/net/tun:/dev/net/tun"
generate_systemd:
path: "/etc/systemd/system/"
cap_add:
- SYS_ADMIN
- NET_ADMIN
env:
DEVICE: "/dev/thread"
BACKBONE_IF: br0
FLOW_CONTROL: 0
FIREWALL: 1
NAT64: 1
BAUDRATE: 460800
OTBR_REST_PORT: 8081
OTBR_WEB_PORT: 7586
AUTOFLASH_FIRMWARE: 0
OTBR_LOG_LEVEL: error
OTBR_WEB: 0
Things to note here:
- Change
/home/dave/otbr_datato wherever you want your persistent data to be stored - This container is running with a lot of privileges. Make sure this is something you're OK with before continuing.
- I'm not actually sure if
/dev/net/tunhas to be passed through, I was just following their docker-compose.yml - That
docker-compose.ymlalso hasFLOW_CONTROLset to 1. This doesn't work with the ESP32 BACKBONE_IFshould be whatever your primary interface is, regardless of whether it's a bridge or a physical interface. I did a bit of experimentation with this, and caused myself some (minor) issues by setting it to the underlying interface rather than the bridge I'm running on top of it- When you run the container, you will probably get some slightly concerning-looking error messages. I would advise not looking too deeply until you've set the rest up, unless something's an obviously catastrophic error that causes it to exit. If you're worried, try enabling
OTBR_WEBand going to http://:7586/ to see what it thinks.
Open Home Foundation Matter Server
This is a pretty simple one:
- name: Create matterjs container
become: yes
containers.podman.podman_container:
name: matterjs-server
image: "ghcr.io/matter-js/matterjs-server:latest"
network: host
state: present
volume:
- "/home/dave/matterjs-server:/data"
generate_systemd:
path: "/etc/systemd/system/"
As before, the /home/dave/matterjs-server should be changed to wherever you want the state to be stored.
I think this could probably be run inside a Podman network with appropriate configuration, but I decided not to bother when I was already facing so many unknowns. Maybe if you've found this documentation you'll be braver since you know what should work.
mdns-reflector
If you're running your Home Assistant with host networking, you shouldn't need this, as mdns-reflector is for repeating mDNS multicasts between networks. I've got my OTBR running with host networking and Home Assistant in a Podman network, so I need this to repeat messages between the two networks.
I'm not entirely sure if mdns-reflector is strictly needed, even in my setting, but it made the Thread network page in Home Assistant less concerning by removing the text “No border routers were found. Check that your border router is configured correctly, or reset it to factory settings.”, and showing that yes, everything is definitely configured properly and talking to each other. I say I'm not sure if it's needed because I now have Thread devices set up and reporting correctly, whilst Home Assistant is once again saying “No border routers were found. Check that your border router is configured correctly, or reset it to factory settings.” This seems to be a bug in the HA integration where it's not actively searching for the _meshcop._udp entry so it expires from its cache fairly quickly.
Anyway, running this is also pretty simple, so I don't feel I'm losing anything by running it:
- name: Create mDNS reflector container
become: yes
containers.podman.podman_container:
network: host
name: mdns-reflector
privileged: true
image: "docker.io/yuxzhu/mdns-reflector"
state: present
recreate: yes
generate_systemd:
path: "/etc/systemd/system/"
command: 'mdns-reflector -fn br0 podman0 podman1'
Replace br0 podman0 podman1 with whatever combination of interfaces you want to share mDNS messages.
Home Assistant
I followed the steps as documented in hass-otbr-docker's README.md, point 2 onwards. They worked, apart from the Thread network configuration. To avoid this problem, ideally run avahi-browse -rt _meshcop._udp from a machine that is on the same network as OTBR every couple of minutes, or at least restart OTBR to ensure the _meshcop._udp mDNS entry is in Home Assistant's cache. This should save yourself a lot of headaches.
Issues I ran into/things of note
This will hopefully either be helpful for people who have tried doing this using other instructions and run into the same issues as me, or people who have misread something or have some other issue in following the above instructions. Some of this has also been covered in the earlier instructions, but I feel it's still worth calling out separately.
Firstly I seem to be incapable of remembering it's OTBR and not OBTR. This didn't affect much, but it's still there
I struggled to figure out how to verify the radio firmware was working properly. I eventually gave up and moved on and relied on OTBR telling me if it could talk to it or not, and it was fine
When I first tried out the radio with OTBR, it wasn't able to talk to it properly and there were errors in the OTBR output. This is just because I had to turn flow control off, as documented in the relevant thread on the Home Assistant forums, but overlooked by me. In hindsight it would have been worth capturing the errors for this blog post, but I didn't know I was going to be writing it at the time.
There are some errors that show up in the OTBR output on startup and when attempting to reconfigure the Thread network, but they don't seem to actually be anything. I believe they're just attempting to add a multicast subscription to an interface that already has it. Very much a red herring. I also didn't notice until too late that the build of OTBR has configurable logging.
The big one was at the end when Home Assistant would tell me “No border routers were found. Check your border router is configured correctly or reset it to factory settings.” or just “No border routers found.” This was two issues. The biggest one was that I had no way of mDNS getting from the host network that OTBR is running on to my Podman network, and that is crucial for this part of the HA setup. This is what the mdns-reflector container does. The second is that when I was trying to get things working earlier on I wondered if the issue was that the infrastructure interface was set to a bridge instead of a specific interface, so I changed it, but I only configured mdns-reflector to forward between the bridge and the podman networks so the OTBR mDNS messages still weren't getting through.