BETA — Open to testers. Tell us what to fix on @vomehome or via a tester code.

A bulb that would not join

One IKEA bulb, and a border router that was healthy the entire time. Chasing why Home Assistant could not see it took us off Thread entirely and into the tunnel underneath — and the evidence that turned it was that nothing had been auto-discovered in seven months.

Updated 28 August 2026 matterthreadnetworkingengineering


Part 1 of 3 — the network series

A bulb that would not join · Four megabits on a three-hundred line · Isolation that fails closed

1bulb, eventually
7 monthssince anything was auto-discovered
3missing firmware files
2.1 GBwe did not install on a 3.5 GB disk

The goal was small: commission one IKEA Kajplats bulb — Matter over Thread — on a Home Assistant we host in a datacentre for a house in Sweden. The radio has to be in the house, so an Orange Pi Zero 3 with a Sonoff MG24 dongle became the border router.

Everything that follows came out of that bulb. By the end it had taken us through the tunnel to the house, the firewall in front of it, and eventually the design of tenant isolation on the whole host.

What we wanted

A Thread device speaks 802.15.4 to a border router. Home Assistant needs to find that border router, hold its network credentials, and hand them to a new device during pairing. The pairing itself happens over Bluetooth.

Three requirements, one of which — the discovery — turned out not to be about Thread at all. The picture of how the radios actually fit together is how Thread and Matter actually work. The how-to for a hosted instance is Thread and Matter with a hosted Home Assistant.

The setup traps, briefly

Before any interesting problem there was a queue of ordinary ones, and they are worth a paragraph because every one of them presents as something else.

A docker pull had run out of space mid-extraction on a 3.5 GB root partition (828 MB of it stale .deb files), and left containerd's own snapshot database referencing a directory that no longer existed. Freeing space did not help: fresh pulls of the same image kept failing on the stale metadata rather than on disk.

Then the container itself. The backbone interface variable we set was a build argument that does nothing at runtime, so it silently defaulted to an interface this board does not have. The stable /dev/serial/by-id/... symlink does not resolve through Docker's device mapping, so the path inside the container was empty and otbr-agent reported a missing file. The firewall setup failed with Table does not exist because a kernel module was not loaded, and the container's own modprobe failed silently — there is no sudo in the image. The REST API bound to loopback, so Home Assistant was refused while curl localhost worked perfectly.

And then we clicked Form Network in the border router's web UI. That button arrives with the OpenThread project's tutorial dataset pre-filled — network key 00112233445566778899aabbccddeeff, network name OpenThreadDemo. It replaced a properly random network with credentials published in every getting-started guide, live on a customer's home network. That is a trap in the software, not a mistake we invented, and it is the single loudest warning in build a Thread border router.

The first wrong answer: the image

With all that fixed, the border router was Thread leader and advertising itself. Home Assistant said "No border routers were found".

We chased the API first. A theory about the REST endpoint ignoring the header Home Assistant's client sends, and returning JSON where raw dataset bytes were wanted, was verified — and then turned out to be equally true of the image we eventually switched to. A real behaviour, and a red herring.

The actual find was that openthread/otbr:latest ships two entrypoint flavours and wires the project's internal test one as the container entrypoint. It is a CI build. Community reports of the exact symptom, with the exact image, were sitting there once we knew to look. We moved to a community image built from Home Assistant's own add-on source, pinned to a tag rather than tracking a rolling build.

It made no difference. Home Assistant still saw nothing.

What actually turned it

At this point the useful move was to stop looking at Thread.

Three pieces of evidence, none of them Thread-specific:

  • Unicast worked. Home Assistant's backend had made a successful outbound HTTP call to the border router's REST API while setting up the integration. So there was a working path between the two machines.
  • Nothing had been auto-discovered in seven months. Checking every config entry's source and creation date, the most recent zeroconf discovery on that instance was a printer in January.
  • A Google Cast device was stuck unavailable. Cast depends on live mDNS presence.
Three facts, none of them about Thread Unicast worked an outbound HTTP call to the border router succeeded so the path exists Seven months nothing auto-discovered since a printer, in January so it predates the bulb Cast unavailable and Cast needs live mDNS presence so it is not Thread-specific Multicast is not crossing the link and has not been, for months before anyone tried to pair a bulb
None of the three is about Thread, which is exactly why they were worth having. Handing the network side these, rather than “it isn’t discovering”, is what moved the investigation in one step.

That is not a Thread fault. That is multicast not crossing the link, and it had been true for months before anyone tried to commission anything. We handed it to the network side with those three facts rather than "it isn't discovering".

The answer came back, and it is the best gotcha in the whole exercise: multicast was traversing the link — just not IPv6. The bridge had IGMP/MLD snooping on with no querier, which blackholes IPv6 multicast toward the tunnel port. IPv4 is immune, because link-local multicast is flooded whatever snooping says. Every check that looks at IPv4 mDNS reported the link healthy. Meanwhile Thread, Matter, and anything else IPv6-native was never discovered at all.

Enabling a querier — the tidy fix — did not work. Disabling snooping did, immediately.

House LAN Chromecast mDNS Border router Thread, Matter Bridge snooping, no querier Hosted HA discovers discovers nothing IPv4 — flooded regardless IPv6 — black-holed toward the tunnel
The reason this hid for months: every check anyone runs is an IPv4 check, and IPv4 was never affected. Only the IPv6-native things — Thread, Matter — were missing, and those were the ones nobody had tried yet.

A healthy IPv4 test proves nothing about IPv6. That sentence is now in three of our articles, and it was bought here.

That handover is where this story becomes the next one. The tunnel and the firewall in front of it turned out to have several more faults of exactly this shape, and finding them is Four megabits on a three-hundred line.

A fix that looked like a failure

Several rounds of "restart the border router, check Home Assistant" came back negative after the multicast fix. Once, the router appeared in the Thread panel and then vanished.

Best explanation: rapid back-to-back container restarts each send an mDNS goodbye for the old service instance immediately before re-registering. So the moment multicast started working, Home Assistant almost certainly caught a withdrawal from one of our own test restarts, seconds after discovering the router. The churn was masking the success it was meant to be testing.

On the next single, isolated restart it appeared and stayed.

What we changed our minds about

Commissioning still failed — instantly, on the QR scan. Failing that early meant it never reached Bluetooth, which put the fault upstream of everything we had been working on.

Matter commissioning goes: scan the code, connect over Bluetooth Low Energy, then hand over network credentials. What we believed at this point — and it is written down here because it drove the next two days — was that iOS uses the phone's own radio while Android routes through the server's Bluetooth stack, and that a hosted virtual machine, having none, therefore needed one. The last section of this story is the evidence that this is not what happens.

So we gave the Pi some. Its onboard combo chip was in a failed state, and the reason was three vendor calibration files simply missing from the distribution image; the box had always been wired, so nobody had noticed. Installing the full firmware package would have been about 2.1 GB on a 3.5 GB disk, so we took the three files — a few kilobytes each — from the board vendor's own repository. Restarting the services was not enough: the chip had already crashed at boot before the files existed, and needed a cold reboot to initialise. Then we stood up a standalone Matter Server next to the border router, pointed Home Assistant at it, and it worked.

Except it was never used. Zero connection attempts reached it. Chasing that turned up the better answer, and it is the find worth carrying to the next house: the Matter Server add-on has a ble_proxy setting that drives commissioning through Home Assistant's own Bluetooth integration — which already knows how to use remote ESPHome Bluetooth proxies. This house has eight of them.

Eight proxies spread around a house is better physical coverage than one radio on a Pi could ever be, precisely because devices get commissioned where they are being installed. It needs no radio on the server at all, and it is not gated on iOS versus Android. The steps are commission Matter with a Bluetooth proxy.

The Bluetooth work on the Pi still stands: it works, and it is the right answer for a house with no proxies deployed. What we have not done is watch a proxy carry a commissioning end to end, and the logs are blunt about it. ble_proxy is on and the endpoint is registered, and the Matter Server completes its proxy handshake one second after Home Assistant's WebSocket connects — on a start with no pairing anywhere in it. The plumbing is connected and idle. Every commissioning we have a record of went somewhere else.

Adding a second Kajplats from the Android companion app, the server's own log reads Skipping NetworkCommissioning steps because the device is already on IP network (udp). Google Play Services had put the bulb on Thread using the phone's radio before Home Assistant was involved; Home Assistant then joined it to its own fabric over IP. PASE went straight to udp://[fd54:…]:5540. No Bluetooth on our side at any point.

Which undercuts the reason we went looking for a radio at all. We had it that Android commissioning routes through the server's Bluetooth stack, and that a hosted VM therefore needs one. On this evidence it does not: the phone does the onboarding, and the server joins over IP afterwards.

The phone is also the route the software itself pushes you down. Ask Home Assistant to add a Matter device from the web interface and it tells you to use the companion app. So ble_proxy sits connected and unexercised — plumbing with nothing yet flowing through it.

Which is roughly where Matter and Thread are in general, and it is worth saying plainly rather than dressing up. The pieces exist and mostly work. They are young. The path that succeeds is not always the path the documentation describes, and more than once here the documented one was a dead end while an undocumented option a menu deep turned out to be the answer. Commissioning driven entirely through the proxies, with no phone anywhere in it, is something we would like to be able to offer. It is on the list. It is not a thing we can claim today.

Two reports, one of them stale

Late on, with the border router visible again, a second troubleshooting pass concluded that it had stopped advertising itself entirely — zero _meshcop._udp packets in a 60-second capture while other mDNS traffic on the LAN was fine — and suggested looking at which daemon was publishing, and on which interface.

We did not act on it, and the reasoning matters more than the conclusion:

  • Re-running our own check live got an immediate discovery event back from Home Assistant, with the border router's real LAN address in the resolved address list, at the moment the other report claimed silence. Services do not rebroadcast continuously once cached, so a 60-second window can land between two announcements. A mundane explanation beats a regression.
  • The suggested daemon was not the one this image runs, and nothing related to it is even installed.
  • The wrong-interface theory was contradicted by that same address list. A Thread-only interface cannot produce a real LAN address.
Why a clean capture proved nothing time announce announce announce announce 60-second capture records nothing — and nothing is exactly what a dead service looks like
Services do not rebroadcast continuously once they are cached. The window landed in a gap, and a live re-check at the same moment got an immediate discovery event carrying the router’s real LAN address.

Weigh a diagnosis against your own freshest first-hand evidence, especially in a system with a documented history of intermittent behaviour. A mistimed test looks exactly like a real fault.

That said, the sharper point underneath that report was correct: being visible in the Thread panel is not the same as Home Assistant holding the dataset. It is what prompted us to re-check the integration entry — which, after a further firewall fix on our side, persisted on the first attempt for the first time in the whole exercise.

Where it landed

The bulb paired after one more fix, and it was not Thread either: IPv6 forwarding was off on the border-router host. The device joined the mesh — a new neighbour appeared in the router table while the phone sat on checking connectivity to Thread network — and then nothing could route to it. With network_mode: host, a container cannot set that sysctl; it has to be a host file.

Forwarding on, retry, and the border router's counters went from zero to hundreds of packets in both directions. light.kajplats_e14_ws_globe_806lm, colour temperature and all, on a Home Assistant in a datacentre, joined to a mesh in a Swedish house. It asked to update its own firmware a minute later.

And it is worth recording the anticlimax, because it is the shape of this entire kind of work. The bulb came on and it genuinely felt like something to announce — right up until you picture announcing it. Look. I can turn a light on. Nobody has been impressed by that since roughly 1880.

The thing actually fixed is the part nobody can see. Discovery works now, so the next device joins in a minute instead of a fortnight, and the person it joins for never learns that any of this happened. A lamp behaving like a lamp is the entire product. You do not get a "wow" for it, and if you ever did, something would have gone badly wrong somewhere.

Every fix along the way is written up as a how-to, because all of it will recur on the next house: the architecture, the border router, the pairing, and the Bluetooth-proxy path we should have taken first.

Still open, and honest about it: we do not fully understand why the Thread panel could show a live border router before an integration entry ever persisted, and the seven-month zeroconf gap predates the tunnel by half a year — it is unexplained rather than fixed. The stuck Cast device is IPv4 mDNS, which was never broken, and needs its own diagnosis.

Next: the tunnel underneath all of this had its own problems, starting with 4.3 Mbps on a 300/300 line — Four megabits on a three-hundred line.

Rather not do this alone?
The Vome assistant will walk you through this guide a step at a time, and can do some of it for you.
Sign in for AI help

Something wrong or missing here? Tell us — these pages are written from real work, so corrections are welcome.