How to use an Orange Pi Zero 2 running OTBR with openHAB

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 :slight_smile:
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 :slight_smile:

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 end0 ethernet interface.
    If you have to use Wifi with the wlan0 interface, 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 add optional: true to 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/dbus to the otbr docker container.
    • $ sudo apt update && sudo apt install avahi-daemon avahi-utils -y
    • Then change /etc/avahi/avahi-daemon.conf to contain:
      [server]
      use-ipv4=no
      use-ipv6=yes
      enable-dbus=yes
      allow-interfaces=end0
      
    The mDNS works with IPv6 only then.

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/ttyUSB0 usually), 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 docker to 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 end0 interface did not work for me probably because it is not piped to the bash.
    • Check the interface name with $ ip a. It should be end0 for 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
      
  • 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.list and fill it [6]:
    $ nano otbr-env.list
    OTBR_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 end0 interface in the config. Exchange the 192.168.1.70 with your IP.
    $ sudo nano /etc/lighttpd/lighttpd.conf
    server.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 2 and add it with 0x prefix
  • 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 end0 interface IPv6 that starts with fdxx.
      $ docker exec -it otbr ot-ctl prefix add fdxx:xxxx:xxxx:xxxx::/64 pasor
  • 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]
    $ 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
    
    I ran it with Bluetooth support, but it was not necessary in the end.
  • Open http://localhost:8123 in 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 :partying_face:

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 :star_struck:


  1. ↩︎

  2. ↩︎

  3. ↩︎

  4. ↩︎

  5. https://raw.githubusercontent.com/openthread/ot-br-posix/refs/heads/main/etc/docker/border-router/setup-host ↩︎

  6. ↩︎

  7. ↩︎

  8. ↩︎

  9. ↩︎

Whenever Play Services is involved, it means that Google tracks what you’re doing. Why exactly is this needed here, why does it matter if Android “trusts” the network or not? Is it because of a lack of another way to “pair devices”?

If so, the whole Thread network system seems compromised - I’ve always heard that you can operate it completely isolated, without involving Google, Apple etc.

Because Android won’t connect to an untrusted MAtter network as a security precaution.

Yes, there needs to be a way to join devices to the network which almost always requires scannning a QR code, unfortunately.

But what’s presented here is one way to achieve this. It is not the only way. And there are ways to achive a Matter network without involving these other ecosystems. But those ways, for now, are much harder to set up and get working and most importantly to use. In particular, if you want to provision matter devices with your phone instead of through the command line, your network needs to be trusted by the operating system (provisioning is implemented in the OS, not at the application layer so the OS needs to trust it).

If one is OK with the command line, they can use python-matter-server, but provisioning will be all on the command line without a nice phone based QR code interface. I’m sure there are others as well.

I beleive the openHAB add-on is also being enhanced to support provisioning as well, perhaps using the phone apps at some point. But it’s not done yet.

This is the type of thing I expect Google and others to do to make “open” protocols effectively non-open, which is why I ask. Basing pairing on QR codes are incredibly stupid to begin with, because using a phone is the most convenient way to scan it - but if you take a picture of it, you can also “decode” the QR using local software. If it decodes to some URL that requires e.g. a Google account, the device is effectively useless. But, if it decodes to a code that can be fed to a python (or some other) script/command line tool, it should be possible to make the whole pairing process independent of things like Google Play, available for use by any device, “Google approved” or not.

The reason I’m asking is that I would assume that if that’s possible, somebody would already have created it. What’s the “missing link” that makes it so that you need to use command line tools to pair devices without involving Google, Apple etc.?

edit: I have no idea if this works or not, but this should potentially make it possible to “pair” offline, without using command line, as far as I can understand.. ?

I find other information that contradict this. I don’t know where the truth lies, but the “CSA registry” sounds like just the kind of BS they would pull to make a supposedly “open” system proprietary.

I was also surprised when I saw that the Home Assistant app uses the same interface (which turned out to be the Play Services) like the Google Home App… At least there was this hidden way to make the Thread network trusted.

How I understood it is that the only other way is to use the command line chip-tool. But that’s only available as a Snap package or you need to build it on your own by cloning gigabytes of repos first… That’s why I went with the “workaround” via Home Assistant. I didn’t find any other way.
Perhaps Home Assistant OS uses the chip-tool internally..? (See Matter Alpha link above) Idk…

Overall this was a quite underwhelming experience. I initially also thought: hey this is cool and new and Open Source… But in reality every vendor says, “Sure, you can use my TBR based on Open Source stuff; but you must use my app then and are not allowed to control/configure this TBR with any other ecosystem!”

For anybody interested enough, it seems like the “core” of the pairing logic can be found here:

So, at least in theory, it might be possible for somebody to make an alternative implementation, but I don’t know if it’s worth it TBH, because it involved “trust stores” and the usual “authority regime” found with e.g. SSL. And what we have historically seen is that whenever companies like Google want to “take full control” over an ecosystem, they just made changes here and there that what they say is trusted, trusted. It’s like a kill-switch just waiting to be switched, everything anybody else has made is then dead - for good.

See also this issue in the Graphene OS repo:

In the end, chip-tool it is…

There’s no container? I thought I ran across one on dockerhub sometime back. I have terrible service right now so can’t check. But it seems surprising if there isn’t.

Indeed, you probably mean this one:

Probably one can build a more convenient tool based on that chip-tool and the exposed REST API of OTBR. But that’s not the topic here :blush:

Once you have chip-tool running fine (either through a snap of a docker container), I found the process of pairing Thread devices very easy. I followed the instructions given by @reyhard (from step 4) :

I have had first to install a QR code scanner on my iphone (QR Code Scanner) to get the long discriminator (not the one printed on the device)

Duh… that still means that you rely on the proprietary ecosystems. Why are those different (the one printed on the device and the one acquired via QR)? It’s worth trying to read the QR with independent software e.g. on a computer, to see if the “correct code” can be retrieved without involving Apple or Google.

edit: I’m thinking of stuff like, that should be able to decode QRs without “dialing home to check with the corporate overlord”:

You can use any QR reader to get the code :blush:
It’s “MT:xxxx…” with a number/letter combination.