Hi community!
Here’s how I set up a thread border router and connected it with openHAB. There are already a few tutorials, so here’s yet another one ![]()
My goal was to use it as stable and convenient as possible, but without a “proprietary” TBR (thread border router) e.g. from Apple, Google or Amazon.
During my journey I learned that the key riddle to be solved is to bring the thread credentials to the Google Play Services and have a trusted border router in Android.
It turned out to be not so convenient at all to set everything up, but I can live with the final setup.
In the end it’s a tutorial to use a standalone OTBR with Home Assistant. The bonus is to also use this then for openHAB.
Be sure to read everything (twice) before starting any setup ![]()
I used the following:
Hardware
- Orange Pi Zero 2: quite cheap (~30€ on eBay) with powerful enough hardware and LAN/Wifi interface
- Sonoff Dongle Plus MG24: as RCP, reliable with a good antenna
- SD Card (class 10 A1 recommended for OrPiZ2)
- Power Supply for OrPiZ2 (min 2A recommended)
- a Linux laptop to temporarily run Home Assistant and HA Matter Server in Docker
- an Android phone to run the Home Assistant app for Matter Commissioning via Play Services (must be installed from the Play Store, not F-Droid)
Setup the border router
Setup Orange Pi
- Download the stable Armbian Bookworm image for the Orange Pi Zero 2 [1]
- Flash it with Balena Etcher to the SD Card (Armbian Flasher might work as well)
- Start it up with LAN and do the initial setup via ssh. I didn’t change any config files for the first boot. Set up the root and default user and do the available updates with the
armbian-upgrade. - Give a static IP to the device
- Initially I planned to use the Wifi interface, but it turned out to be too unreliable. That’s why I just used the
end0ethernet interface.
If you have to use Wifi with thewlan0interface, follow the Network guide [2]
NB: If you set up wifi in parallel to ethernet, make sure to mark BOTH connections as optional in the Netplan yaml files! You need to modify the already existing yaml file for ethernet as well and addoptional: trueto the config. Otherwise systemd always “thinks” that the device is offline, if no ethernet cable is plugged in. And then systemd-timesyncd never synchronizes a time from ntp! - Install avahi-daemon (not quite sure if this was necessary at all, but it also didn’t break anything). The difference was that I didn’t have to expose the local
/var/run/dbusto theotbrdocker container.$ sudo apt update && sudo apt install avahi-daemon avahi-utils -y- Then change
/etc/avahi/avahi-daemon.confto contain:[server] use-ipv4=no use-ipv6=yes enable-dbus=yes allow-interfaces=end0
Setup Sonoff Dongle Plus MG24
- Open the Sonoff Web Flasher [3] in a Chromium based browser (Chrome, Brave, …) on the laptop and plug the dongle into the USB port.
- Follow the web flasher, i.e. select the device (
/dev/ttyUSB0usually), then select the Thread software and choose OpenThread 2.4.4 (latest/only version at the moment). Then flash it.
Connect MG24 to OrPiZ2
- Just plug it in the USB port
- Then check with
$ ls -la /dev/ttyUSB*, it should be listed
Setup OTBR
Basically I followed the official guide [4]. But it turned out that the docker image openthread/border-router is just a minimum setup, e.g. without web UI. It needs to be configured from command line and you cannot see the network state on a map (similar to Zigbee2MQTT). That’s why I used the openthread/otbr image instead. This is also used in Home Assistant, I think.
The following commands are executed in the home folder of the default user on the OrPiZ2.
Setup otbr docker container
- Install Docker on the OrPiZ2:
$ curl -sSL https://get.docker.com | sh - Add the current user to the docker group:
$ sudo usermod -aG docker $USER - Reboot or at least call
$ newgrp dockerto activate the group setting in the active session - Configure IP forwarding:
- The shell script from the openthread/ot-br-posix repo [5] can be used. But I had to download it; passing the environment variable to use the
end0interface did not work for me probably because it is not piped to the bash. - Check the interface name with
$ ip a. It should beend0for ethernet. - Then execute
$ wget https://raw.githubusercontent.com/openthread/ot-br-posix/refs/heads/main/etc/docker/border-router/setup-host $ chmod +x setup-host $ INFRA_IF_NAME=end0 ./setup-host
- The shell script from the openthread/ot-br-posix repo [5] can be used. But I had to download it; passing the environment variable to use the
- Download the docker container
$ docker pull openthread/otbr - Create a folder where the container can persist its thread data
$ mkdir otbr-data - Create the environment file
otbr-env.listand fill it [6]:
$ nano otbr-env.listOTBR_AGENT_OPTS=spinel+hdlc+uart:///dev/ttyUSB0?uart-baudrate=460800 NAT64=0 FIREWALL=0 WEB_GUI=1 BACKBONE_INTERFACE=end0 - Start the docker container:
$ docker run --name=otbr --detach --privileged --network=host --cap-add=NET_ADMIN --device=/dev/ttyUSB0 --device=/dev/net/tun --volume=./otbr-data:/var/lib/thread --env-file=otbr-env.list --restart=always openthread/otbr - The local REST API of the OTBR is available on port 8081 on the loopback interface. You can test it with curl, it should show a JSON:
$ curl http://127.0.0.1:8081/node
Make the web UI available on the LAN
The web UI available on port 80 on the loopback interface uses the REST API on port 8081 to show its data. To make both available on the LAN, I set up lighttpd as reverse proxy on the OrPiZ2.
- Install lighttpd:
$ sudo apt install lighttpd - Change its configuration. Make sure to set a default port different from 80 and a document-root, otherwise lighttpd doesn’t start. Use the IPv4 address of the
end0interface in the config. Exchange the192.168.1.70with your IP.
$ sudo nano /etc/lighttpd/lighttpd.confserver.port = 8888 server.modules += ( "mod_proxy" ) server.document-root = "/var/www/html" # LAN interface to port 80 $SERVER["socket"] == "192.168.1.70:80" { proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => 80 ) ) )} # LAN interface to port 8081 $SERVER["socket"] == "192.168.1.70:8081" { proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => 8081 ) ) )} - Then restart the lighttpd
$ sudo systemctl restart lighttpd.service
Now finally the web UI should be available from the LAN, in this example via http://192.168.1.70.
Configure the Thread net
Open the Web UI in a browser on the Linux laptop and navigate to “Form”:
- Network Name: any name you like, e.g. MyOpenThreadNet
- Network Extended PAN ID: use openssl on the Linux laptop and generate with
$ openssl rand -hex 8 - PAN ID: generate with
$ openssl rand -hex 2and add it with0xprefix - Passphrase/Commissioner Credential: generate with
$ openssl rand -base64 12 - Network Key: generate with
$ openssl rand -hex 16 - Channel: 15 should be ok, it depends on other Wifi/Zigbee networks around
- On-Mesh-Prefix: generate with
$ printf "fd%02x:%04x:%04x:%04x::/64\n" \ $((0x$(openssl rand -hex 1))) \ $((0x$(openssl rand -hex 2))) \ $((0x$(openssl rand -hex 2))) \ $((0x$(openssl rand -hex 2)))
Then click “Form”. The thread network should be created. Wait a few seconds and check the “Status” page. The “RCP:state” should switch to “leader”.
Now login to the Orange Pi Zero 2 via ssh again.
I added two additional routing settings to the Thread net, where I’m actually not sure if they were really necessary, but they didn’t cause any harm anyways (I did a lot of back and forth debugging with IPv6 and mDNS at that stage…)
On the OrPi2Z:
- Add the On-Mesh-Prefix depending on the prefix of your LAN:
- Read out the IPv6 LAN prefix with
$ ip -6 a. - Use the first four parts from the
end0interface IPv6 that starts withfdxx.
$ docker exec -it otbr ot-ctl prefix add fdxx:xxxx:xxxx:xxxx::/64 pasor
- Read out the IPv6 LAN prefix with
- Set the Off-Mesh-Route
$ docker exec -it otbr ot-ctl route add ::/0 s med - Persist
$ docker exec -it otbr ot-ctl netdata register
Get the TVR for Home Assistant
To connect Home Assistant you need to get the whole Thread data set:
- Execute
$ docker exec -it otbr ot-ctl dataset active -x - Copy the whole letter/number string and save it, it’s needed later
Check the visibility from Android
- Go to the settings on Android and search for “Thread”; open the Thread networks from the Google Play Services
- The Thread network should be visible as “other network”
- While you’re here you can select the network settings at the bottom and disable saving the credentials in the Google cloud if you don’t want that Google persists it
Add the Thread network to the trusted networks in the Google Play Services
Now comes another tricky part which took me most of the time to figure out. Android only uses trusted Thread networks. To achieve this, I used the Home Assistant Android app and for this I needed a running Home Assistant.
The commands are executed in the home folder of the user on the Linux laptop.
Setup Home Assistant and Home Assistant Matter Server
- Install Docker on the Linux laptop, just like above on the OrPiZ2 and add the own user to the docker group.
- Create a folder where the container can persist its data:
$ mkdir homeassistant-config - Start Home Assistant
$ docker run -d --name=homeassistant --privileged --restart=unless-stopped --network=host -v ./homeassistant-config:/config -v /run/dbus:/run/dbus:ro homeassistant/home-assistant:stable - Create another folder for the Matter server config
$ mkdir homeassistant-config/matter-server-data - Start the Home Assistant Matter server [7]
I ran it with Bluetooth support, but it was not necessary in the end.$ docker run -d --name=matter-server --restart=unless-stopped --security-opt apparmor=unconfined -v ./homeassistant-config/matter-server-data:/data -v /run/dbus:/run/dbus:ro --network=host ghcr.io/matter-js/python-matter-server:stable --storage-path /data --paa-root-cert-dir /data/credentials --bluetooth-adapter 0 - Open
http://localhost:8123in a browser and do the onboarding (create a local user, location etc. doesn’t matter) - Go to Settings > Devices & Services and open Thread there. You should see your Thread network, but it’s not connected yet. Click the config gear and at the top three dot menu on the right, select “Add dataset from TLV”. Paste the long TLV letter/number string that you got from the OTBR.
- Home Assistant should now add the Thread network. I additionally clicked the small three dot menu on the Thread network and selected to use the access data for Android & iOS
- Go back to the settings and configure Matter [8]; when asked for a websocket URL, just leave the default. Home Assistant should connect to the Matter server.
Configure Android with the Home Assistant app
- Install the Home Assistant app from the Play Store (not F-Droid)
- Start the app, configure the Home Assistant server (auto-detected) and do the onboarding
- To synchronize the Thread credentials to Android [9] go to go to Settings > Companion app > Troubleshooting, then select Sync Thread credentials.
- After this was successful, you should also see that the Thread network is now an available network in the Google Play Services settings!
Add the device to Home Assistant
- Go back to the Home Assistant app and add a new device. Select Matter and follow the instructions on the screen. The connection is done via the Google Play Services and your OTBR should be used.
Add the device to openHAB
After the device was added to Home Assistant, it can be added to openHAB via Matter Multi Admin.
- Select the device in the Home Assistant app and click “Share device”. Select “Share” again on the following screen.
- Note down the code (11 digits)
- Go to openHAB > Things > (+) > Matter Binding
- Enter the code and click scan
- The device should appear and can then be added as new thing

Clean up
The Home Assistant docker containers can be shut down on the Laptop, they are not necessary anymore at the moment.
$ docker stop homeassistant
$ docker stop matter-server
If you want to pair other devices you can start them again with
$ docker start homeassistant
$ docker start matter-server
It should now also be possible to pair Matter over Thread devices with the Google Home App without Home Assistant, because the Thread credentials are already trusted. But using Home Assistant is the most “local” way, I guess.
In the end you have a Matter over Thread device connected to openHAB communicating via your own Thread network ![]()