Connect ZBT-2 Thread to Home Assistant Container

i have a raspberry pi 5, running raspbian-os (Debian GNU/Linux 13 (trixie)).

Just quick view from my side. I had a problem with ipv6 forwarding. (causes new matter devices refuseses to join)
i have this setting in /etc/sysctl.d/99-thread-routing.conf (change end0 to you network)
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
net.ipv6.conf.default.forwarding=1
net.ipv6.conf.all.accept_ra=2
net.ipv6.conf.end0.accept_ra=2
net.ipv6.conf.all.accept_ra_rt_info_max_plen=64
net.ipv6.conf.end0.accept_ra_rt_info_max_plen=64

(Also be carefull of order - as i undersend all can rewrite my end0 network)

but it keeps changing sysctl net.ipv6.conf.end0.accept_ra to 0 after RP5 reboot.
when i just
sudo sysctl --system
then restart threadborder container its starts to work.
And i can ping ipv6 devices throught my network => matter devices are able to connect.

any ideas why?

Stack: RP5 , openmediavalut,
docker dcontainers: HA, openthread/border-router:latest, matter, ZBT-2.

I have this compose setup

  otbr:
    image: ghcr.io/ownbee/hass-otbr-docker:latest
    container_name: otbr
    restart: unless-stopped
    privileged: true
    network_mode: host
    cap_add:
      - SYS_ADMIN
      - NET_ADMIN
    devices:
      - /dev/serial/by-id/${zbt-2_serial}:/dev/${zbt-2}
      - /dev/net/tun:/dev/net/tun
    volumes:
      - ${DOCKER}/otbr_data:/var/lib/thread
    environment:
      - TZ=${TZONE}
      - DEVICE=/dev/${zbt-2}
      - BACKBONE_IF=wlp0s20f3
      - FLOW_CONTROL=1
      - FIREWALL=1
      - NAT64=1
      - BAUDRATE=460800
      - OTBR_REST_PORT=8081
      - OTBR_WEB_PORT=8083
      - AUTOFLASH_FIRMWARE=0
    labels:
      - ***

  matter-server:
    image: ghcr.io/home-assistant-libs/python-matter-server:stable  
    container_name: matter-server
    restart: unless-stopped
    privileged: true  
    security_opt:
      - apparmor:unconfined 
    network_mode: host
    volumes:
      - ${DOCKER}/matter_server:/data  # Persistent storage
      - /run/dbus:/run/dbus:ro  # For Bluetooth/DBus access
    environment:
      - TZ=${TZONE}
      - MATTER_LOG_LEVEL=DEBUG
      - CHIP_IFACE=wlp0s20f3
      - MATTER_SERVER__PRIMARY_INTERFACE=wlp0s20f3
    command: --storage-path /data --primary-interface wlp0s20f3
    ports:
      - "5580:5580"  # debugging
    labels:
      - ***

With


net.ipv4.conf.all.forwarding = 1
net.ipv6.conf.all.disable_ipv6 = 0
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.all.accept_ra_rt_info_max_plen = 0
net.ipv6.conf.all.accept_ra = 2
net.ipv6.conf.wlp0s20f3.forwarding = 1
net.ipv6.conf.wlp0s20f3.accept_ra = 0
net.ipv6.conf.wlp0s20f3.accept_ra_rt_info_max_plen = 0

So far, it works fine with a caveat - from time to time, I just get random N/A for my IKEA Matter devices - fixed easily by otbr restart that is scripted this day. Still have to find why.

@sphinxbox There are some known issues with Ikea devices, see this thread on matter.js. Ikea is aware and working on firmware updates. Maybe that fixes your issues.

Sietse

Thank you very much for this very helpful post.

I’d like to share a solution to the only thing I had struggled with - to keep IPv6 forwarding persistent over reboot on my Raspberry Pi OS.

In addition to the script from openthread.io mentioned in the OP, I had to add instructions to /etc/dhcpcd.conf to leave IPv6 alone - otherwise it was overwriting the sysctl accept_ra and accept_ra_rt_info_max_plen.

This is documented at Thread Border Router - Bidirectional IPv6 Connectivity and DNS-Based Service Discovery  |  OpenThread, but it might be helpful for others as well to point it out in the OP.

Can you give me the steps you had to take? I will incorporate them in the OP.

Sietse

Sure thing. I used the following section from Thread Border Router - Bidirectional IPv6 Connectivity and DNS-Based Service Discovery  |  OpenThread :

If you are trying to reconnect Wi-Fi or restart Ethernet interface, make sure that dhcpcd is not used for managing your WiFi-/Ethernet IPv6 network. Because dhcpcd always overrides the accept_ra option everytime the interface is restarted and your accept_ra configuration will be lost.

Append below lines to the dhcpcd configuration file (e.g. /etc/dhcpcd.conf) to explicitly disable IPv6 in dhcpcd:

noipv6
noipv6rs

You need to reboot to take the change into effect.

hey all. i reinstalled the home-assistant app recently on my iphone and now i cant pair any new IKEA accessories. here is what AI said the following…

anyone having similar issue? while my accessories paired before the app reinstall still works fine. but i cant pair any new ones. )=

never a dull moment with this setup!!! :sob:

:record_button: Theory: older iOS accepted OTBR’s _meshcop._udp as valid Thread Border Router. Newer iOS (post iOS 17/18) requires Apple-certified TBR only.
App reinstall likely triggered iOS/app update → stricter TBR validation → OTBR now rejected.
What iOS version running on iPhone 13 mini?
❯ 26.3.1 ios
:record_button: iOS 26 = root cause. Apple tightened Thread TBR requirement in iOS 17+ and further in iOS 26. OTBR was accepted as TBR in older iOS. iOS 26 requires Apple-certified
hardware TBR only.
Reinstall triggered iOS 26 update → broke OTBR compatibility.
No server-side fix. Options unchanged:
1. HomePod mini (~$99) — Apple TBR
2. Android phone — bypasses Apple stack entirely

thanks for sharing all the steps, super helpful! i remember getting stuck on the ipv6 forwarding part too, took me a while to figure out the interface thing (mine wasn’t called eno1 either). did you notice any weirdness with thread devices dropping off after a reboot? i had to double check my docker permissions, just in case. anyway, glad you got it running!

:man_facepalming: turns out it was firewall. i guess i must have manually ran the firewall steps and not set it permanently during bootup. :man_shrugging:

Just reporting back in that my Thread network has been running solid with 0 drop outs for months now. I only have IKEA Myggspray devices connected for now.

The only small issue that I have is that I have to re-select my border router in the thread integration after every full server restart. But that is only once a month so that is a non issue. It survives restarts and docker upgrades.

I'm stuck on configuring the IPv6 forwarding and RA acceptance. I'm using Debian on a Raspberry Pi 5, no dhcpcd as far as I can tell, since I don't have /etc/dhcpcd.conf or a dhcpcd service, just systemd-networkd. I have the following set:

└[~]> cat /etc/sysctl.d/60-otbr-accept-ra.conf
net.ipv6.conf.eth0.accept_ra = 2
net.ipv6.conf.eth0.accept_ra_rt_info_max_plen = 64

and

└[~]> cat /etc/sysctl.d/60-otbr-ip-forward.conf
net.ipv6.conf.all.forwarding = 1
net.ipv4.ip_forward = 1

I also set

└[~]> cat /etc/systemd/networkd.conf
[Network]
...
IPv6Forwarding=yes
IPv6AcceptRA=yes

But after a reboot, accept_ra and accept_ra_rt_info_max_plen are at 0 again. If I run sudo sysctl --system, it applies the configs for the session, but something is clobbering the settings on reboot, and I'm not sure what it is.

Other than these items, everything seems fine. I can see the ZBT-2 adapter connecting fine in otbr, I can see my Matter and Thread configs in HA are fine and the Open Thread border router is fine, it's just that when I try to connect a device using my Android phone, it spins for a bit before it tells me that the device requires a Thread border router. I assume that's because the RA acceptance and forwarding aren't sticking, just because that's the only thing I haven't managed to get working.

I feel like I'm missing something silly. Does anyone have any ideas?

EDIT: I solved it, and it was silly. I was doing Settings → Thread → Send credentials to Home Assistant when I needed to be doing Settings → Companion App → Troubleshooting → Sync Thread credentials. Once I did that, I managed to sync a device!

I do not know what I am doing wrong, but I cannot start OTBR. I have flashed as instructed by OP to the latest Thread firmware:

name: Border-router
services:
  border-router:
    image: openthread/border-router:latest
    container_name: otbr
    environment:
      OT_RCP_DEVICE: "spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800"
      OT_INFRA_IF: "ens18"
      OT_THREAD_IF: "wpan0"
      OT_LOG_LEVEL: 7
      OT_REST_LISTEN_ADDR: 0.0.0.0
      OT_REST_LISTEN_PORT: 8981
      OT_WEB_LISTEN_ADDR: 0.0.0.0
      OT_WEB_LISTEN_PORT: 8980
      TZ: Europe/Berlin
    network_mode: host
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/serial/by-id/usb-Nabu_Casa_ZBT-2_E072A1D8B4B0-if00:/dev/ttyACM0
      - /dev/net/tun
    volumes:
      - /var/lib/otbr:/data
    restart: unless-stopped

But it crashes with the following:

Starting otbr-agent...

[NOTE]-AGENT---: Running 0.3.0-a654913

[NOTE]-AGENT---: Thread version: 1.4.0

[NOTE]-AGENT---: Thread interface: wpan0

[NOTE]-AGENT---: Radio URL: spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800

[NOTE]-AGENT---: Radio URL: trel://ens18

[NOTE]-ILS-----: Infra link selected: ens18

00:00:00.000 [C] P-RadioSpinel-: RCP => [C] Platform------: Reset info: 0x4 (PWR)

00:00:00.000 [C] P-RadioSpinel-: RCP => [C] Platform------: Extended Reset info: 0x401 (HV )

00:00:00.022 [C] Platform------: otPlatRadioSetMacKey() at radio.cpp:964: InvalidArgument

otbr-agent exited with code 2 (by signal 0).

Any ideas? This is running inside a VM.

Hi, I had the same problem a couple of days ago, apparently there is a bug in the 2026-08 release of OTBR that affects ZBT-2. I pulled and ran a older image and it worked without any problems.

Hi, I would just like to confirm that everything works for me and thanks to OP and everyone else on this forum who contributed. Here is my input.
I have a Raspberry Pi 3 B+ with official Raspberry Pi OS Lite.
I flashed my ZBT-2 via the home assistant actually (at first I thought the zbt-2 device goes into HA container).

Here are my 3 docker compose files:

Home Assistant:

services:
  homeassistant:
    container_name: homeassistant
    image: ghcr.io/home-assistant/home-assistant:stable
#    privileged: true
    restart: unless-stopped
    environment:
      TZ: Europe/Ljubljana
    volumes:
      - ./config:/config
      - /run/dbus:/run/dbus:ro #for bluetooth
    network_mode: host

Matter server:

services:
  matter-server:
    image: ghcr.io/matter-js/python-matter-server:stable
    container_name: matter-server

    #restart: unless-stopped

    network_mode: host

    security_opt:
      - apparmor=unconfined #perhaps needed for bluetooth device comissioning

    volumes:
      - ./data:/data
      - /run/dbus:/run/dbus:ro #perhaps needed for bluetooth device comissioning

Thread router:

services:
  otbr:
    image: openthread/border-router:latest
    container_name: thread-router

    network_mode: host
    cap_add: #allows container to change docker host's network settings, create a new interface...
      - NET_ADMIN

#    restart: unless-stopped

    environment:
      TZ: Europe/Ljubljana
      OT_RCP_DEVICE: "spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800&uart-flow-control" #set proper settings for communication with ZBT-2

    devices:
      - /dev/serial/by-id/usb-Nabu_Casa_ZBT-2_DCB4D910DCE0-if00:/dev/ttyACM0 #first part is id of zbt-2 on docker host, second part is ACM0, which is default in this container. Must match OT_RCP_DEVICE
      - /dev/net/tun:/dev/net/tun #tun is a virtual network interface

In addition to these compose files, the script vogon linked is also needed, or else device commissioning failed.

So the setup was overall quite easy and logical. Perhaps it helps someone or confirms to someone that it is possible on RPI 3.

In my setup HA is hosted on a server in a rack outside my main building, which is not an ideal location for the ZBT-2. Now an old RPi 3 is running just the thread container as border router in my living room. Works perfectly.

1 Like