otbr 2026.08.0: pending dataset, full NIM cluster data - #53
Conversation
There was a problem hiding this comment.
Code Review
This pull request enhances the matter-netman service on OpenWrt to fully support the Network Infrastructure Manager (NIM) cluster set using real system data. Key updates include publishing a 'matter' ubus object for onboarding and commissioning control, integrating real-time diagnostics for Wi-Fi, Thread, and Ethernet clusters, and migrating the OpenThread border router agent to a netifd protocol handler. Feedback on these changes identifies a build issue in the Makefile due to an invalid $(INSTALL_CONF) macro, potential information leaks and globbing bugs in the init script's reload logic, and a deprecated -a shell operator in the RCP script.
b0c79de to
50a0964
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request refactors the OpenThread Border Router (OTBR) agent configuration on OpenWrt by moving it to a netifd protocol handler (thread-proto.sh) and implements real data providers for the NIM clusters (Wi-Fi, Thread, and Ethernet diagnostics). It also publishes a new 'matter' ubus object to expose commissioning status and control. The review feedback is highly constructive, pointing out that in multi-interface setups, ot-ctl needs to be directed to the correct socket path. It suggests introducing an otctl helper function in thread-proto.sh to ensure commands are routed correctly based on the active device.
50a0964 to
dd5bab4
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request integrates the full Network Infrastructure Manager (NIM) cluster set for OpenWrt, including Wi-Fi Network Management, Thread Network Diagnostics, Thread Network Directory, and Ethernet Network Diagnostics. It also updates the openthread-br package to support pending datasets, asynchronous provisioning/deprovisioning, and publishing a matter ubus object. The feedback recommends avoiding the suppression of stderr from the RCP discovery tool in thread-proto.sh to facilitate debugging.
dd5bab4 to
116342a
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request updates the matter-netman service and openthread-br package to improve Thread network management and integration with OpenWrt. Key changes include migrating to a new netifd protocol for Thread, implementing asynchronous ubus calls for provisioning, and adding a matter ubus object for commissioning. I have reviewed the code and identified two issues: a potential race condition in the protocol handler that could permanently block the interface at boot, and an unsafe string comparison in the OsReleaseField helper that could lead to undefined behavior.
116342a to
a8c3077
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request integrates the Matter Network Infrastructure Manager (NIM) service with OpenWrt, enabling full support for Thread Border Router Management, Wi-Fi Network Management, and diagnostics clusters. It replaces the legacy procd service for otbr-agent with a native netifd protocol handler (thread) and introduces a series of patches to OpenThread Border Router (OTBR) and the Matter daemon to support asynchronous provisioning, pending datasets, and local commissioning control via a published Ubus object. The review feedback highlights two shell scripting issues: a syntax error in thread-proto.sh due to unsupported default value syntax in json_get_vars, and a potential false positive in the hotplug script when matching unconfigured backbone networks.
a8c3077 to
79885b9
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces comprehensive improvements to the Matter Network Infrastructure Manager (NIM) and OpenThread Border Router (OTBR) integration on OpenWrt. Key changes include migrating the OTBR service to a native netifd protocol handler (thread), implementing full NIM cluster support (Wi-Fi Network Management, Thread Network Diagnostics, Thread Network Directory, and Ethernet Network Diagnostics) backed by real system data via ubus, publishing a matter ubus object for local commissioning control, and resolving several commissioning-flow issues such as asynchronous provisioning and fail-safe revert guarding. Additionally, the openthread-br package is updated, and several patches are introduced to enhance dataset handling, mDNS hostname advertising, and robustness. As there are no review comments, no further feedback is provided.
79885b9 to
07049b4
Compare
|
Hi @LorbusChris, thanks for this PR, it looks like a very substantial piece of work! I will have a detailed look next week. In regards to upstreaming my OTBR patches, yes, I’m going through our company internal contribution review process at the moment, so I should be able to upstream them in a few weeks time. |
caee27d to
015a51b
Compare
015a51b to
cc2895b
Compare
cc2895b to
61503f7
Compare
61503f7 to
b105240
Compare
| @@ -0,0 +1,24 @@ | |||
| #!/bin/sh | |||
There was a problem hiding this comment.
I think this is a real issue with the minimal-mdns provider matter-netman currently uses, but I don't think this is quite the right fix; in particular gating this on the AIL of the otbr seems like a strange way of phrasing the policy that we don't care about the Thread (wpan*) interface going up or down.
Getting Claude to look through the minimal-mdns implementation, it looks like this should be relatively straightforward to fix directly in that code. (Separately, we probably want to factor out a copy of the mDNSResponder-based dnssd backend out of the Darwin platform code, so it can be used on Linux, but that's a larger piece of work.)
Here's Claude's take / suggestion:
What's going on. minimal-mdns creates one socket per (interface, address family), each pinned to a specific interface index (Server.cpp:198 (https://github.com/project-chip/connectedhomeip/blob/master/src/lib/dnssd/minimal_mdns/Server.cpp#L198)) — but the interface list is enumerated exactly once, at Advertiser::Init(). The code says so itself (Advertiser_ImplMinimalMdns.cpp:365 (https://github.com/project-chip/connectedhomeip/blob/master/src/lib/dnssd/Advertiser_ImplMinimalMdns.cpp#L365)):
// TODO: ... we don't handle updates to our set of interfaces right now,
// so rely on the logic in this function to shut down and restart the
// GlobalMinimalMdnsServer to handle that.
So when netifd recreates a device, the new ifindex is never picked up and the existing sockets point at a dead one. Same if the daemon starts before the bridge exists. There's also no re-announcement (RFC 6762 §8) on an address-set change, so caches keep stale data until TTL. The A/AAAA records themselves are generated live per query from the source interface, so addresses do self-heal — it really is just socket/interface lifecycle.
Suggested fix. The plumbing is already there: DeviceEventType::kDnssdRestartNeeded → DnssdServer::StartServer() → Advertiser::Init(), which tears down endpoints, re-enumerates, re-listens and re-announces (Dnssd.cpp:59 (https://github.com/project-chip/connectedhomeip/blob/master/src/app/server/Dnssd.cpp#L59)). Several Zephyr WiFi backends post that event on reconnect; Linux posts it from nowhere. So:
- Add a netlink watcher in src/platform/Linux on RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR that posts a debounced kDnssdRestartNeeded. There's already a netlink socket in PlatformManagerImpl.cpp, but it's IPv4-addr-only, WiFi-only, and posts an event no DNS-SD code consumes. Debouncing matters — a bounce emits a storm of messages, and announcements should be rate-limited anyway.
- Harden Listen() first. It currently uses ReturnErrorOnFailure on Bind() under a ShutdownOnError guard, so one interface that's reported IFF_UP but fails to bind (normal mid-bounce) tears down every endpoint with no retry. Per-interface failures need to be non-fatal, like JoinMulticastGroup already is — otherwise a re-listen racing a bounce is worse than what we have today.
- On the Thread interface: AddressPolicy_DefaultImpl already skips wpan* (#72559), but AddressPolicy_LibNlImpl doesn't — it only filters IFF_UP/IFF_LOOPBACK/BROADCAST|MULTICAST. If the OpenWrt build uses the libnl policy, that's the gap worth closing, and it's the right place to express "we don't care about Thread" rather than reaching into otbr's infra_if_name.
With that, the hotplug script goes away entirely.
Meanwhile, if you want something that works before the SDK change lands: the expensive part of the current workaround isn't the rebind, it's that a full daemon restart drops all CASE sessions and subscriptions — which is also why it has to be gated so narrowly. Handling SIGHUP by posting kDnssdRestartNeeded (AppMain.cpp only wires SIGINT/SIGTERM today) would let procd's reload_signal give you /etc/init.d/matter reload, and then the hook can fire on any ifup without caring which interface it was.
There was a problem hiding this comment.
All of this is now up, following your sketch:
- [minmdns] do not lose every endpoint when one interface fails to bind connectedhomeip#73558 hardens Listen() first, as you put it: a per-interface bind failure no longer takes every endpoint down.
- [linux] restart DNS-SD when the network interfaces change connectedhomeip#73559 is the netlink watcher in src/platform/Linux posting a debounced kDnssdRestartNeeded. It ignores events for interfaces the address policy never binds (wpan*, lo), so "we don't care about Thread" lives there rather than in otbr's config. Its description states the merge order you called out.
- [minmdns] skip Thread interfaces in the libnl address policy too connectedhomeip#73560 closes the AddressPolicy_LibNlImpl gap from #72559. To answer your question: this build does not use the libnl policy -- chip_minmdns_default_policy defaults to "default" and nothing here overrides it, so the wpan* skip was already in effect. The parity fix is still worth having.
- [linux][nuttx][webos] read the netlink message the loop is on connectedhomeip#73557 is a drive-by from reading that code: the existing IP-change listener reads the first netlink message of a batch regardless of the iterator.
For the meantime, this PR now does the cheap half of your interim suggestion: 75aa8ac makes the hook call a new rebind action that SIGHUPs the daemon (patch 046) instead of restarting it, so the sessions survive, and patch 045 carries the Listen() hardening so a rebind racing a bounce can't end with no endpoints. The SIGHUP patch is deliberately not proposed upstream -- #73559 is the real fix and removes the need for the signal and the hook entirely; both patches and the hook go when it lands.
One deviation from your suggestion: I used a dedicated rebind action rather than reload_signal + /etc/init.d/matter reload, because reload_service() already means "the configuration changed" (it diffs the command line and either reloads wifi credentials or restarts), and overloading it with "your interfaces moved" would conflate the two. Happy to switch if you prefer reload.
The hook also no longer gates on otbr's infra_if_name -- it fires on any ifup and skips wpan* by name, which is what that lookup was trying to say.
b105240 to
9359bd4
Compare
Moves the pin from the 13 June revision to 72ecdc2b of 30 July. PKG_RELEASE goes back to 1 with the new version. Both patches still apply at fuzz zero against the new revision, so neither needed refreshing. The mirror hash is the one this same revision already produces in CI on project-chip#53, where the source is fetched with the same URL, proto and submodule list, so the archive -- and therefore the hash -- is byte for byte the same. Assisted-By: Claude Opus 5 Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
Moves the pin from the 13 June revision to 72ecdc2b of 30 July. PKG_RELEASE goes back to 1 with the new version. Both patches still apply at fuzz zero against the new revision, so neither needed refreshing. The mirror hash is the one this same revision already produces in CI on project-chip#53, where the source is fetched with the same URL, proto and submodule list, so the archive -- and therefore the hash -- is byte for byte the same. Assisted-By: Claude Opus 5 Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
Moves the pin from the 13 June revision to 72ecdc2b of 30 July. PKG_RELEASE goes back to 1 with the new version. Both patches still apply at fuzz zero against the new revision, so neither needed refreshing. The mirror hash is the one this same revision already produces in CI on project-chip#53, where the source is fetched with the same URL, proto and submodule list, so the archive -- and therefore the hash -- is byte for byte the same. Assisted-By: Claude Opus 5 Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
…equirement (#58) * matter-netman: say in the package what the attestation is worth The daemon reports a real manufacturer and product name, read from the firmware, beside the Connectivity Standards Alliance test vendor id 0xFFF1. That pairing is the right one — the names are accurate and useful, and a community build cannot claim a vendor id it does not hold — but it leaves the package silent about what the device attestation actually establishes. Not much. The build attests with the SDK's development certificate for 0xFFF1/0x8013. The same certificate ships in every build of that example and its private key is published in the SDK source, so attestation cannot distinguish this router from anything else presenting the same credential, and succeeding at it says nothing about who built the device or what is running on it. Whether a controller notices depends on which roots it trusts: the chain ends at a development root that some trust stores carry and some do not. So the package says so itself, in terms of what is true of this device rather than what some other implementation might be expected to do about it. Assisted-By: Claude Opus 5 Signed-off-by: Christian Glombek <c.glombek@cosa.systems> * matter-netman: state the mbedTLS and base-name facts in the package Two things the package knew but did not say. CHIP's mbedTLS crypto backend calls AES-CCM, so the mbedtls variant does not link unless mbedtls is built with MBEDTLS_CCM_C. That option is `default n` in OpenWrt, and the requirement was recorded only as an English sentence in the package description, where the build system cannot act on it. .config.ci sets the option, so this repository's own builds were fine — but anyone building without that seed got an undefined reference to mbedtls_ccm_init rather than an unmet dependency they could see in menuconfig. Say it in DEPENDS instead. Neither variant declared PROVIDES, so the name matter-netman existed for nobody to depend on or install; the variants had to be named explicitly. Declare it in the shared block, the way miniupnpd, geoip-shell and mtr do. Assisted-By: Claude Opus 5 Signed-off-by: Christian Glombek <c.glombek@cosa.systems> * matter-netman: update to the 2026-07-30 Matter SDK Moves the pin from the 13 June revision to 72ecdc2b of 30 July. PKG_RELEASE goes back to 1 with the new version. Both patches still apply at fuzz zero against the new revision, so neither needed refreshing. The mirror hash is the one this same revision already produces in CI on #53, where the source is fetched with the same URL, proto and submodule list, so the archive -- and therefore the hash -- is byte for byte the same. Assisted-By: Claude Opus 5 Signed-off-by: Christian Glombek <c.glombek@cosa.systems> * Remove CONFIG_MBEDTLS_CCM_C from CI config now that it's set via DEPENDS --------- Signed-off-by: Christian Glombek <c.glombek@cosa.systems> Co-authored-by: Karsten Sperling <ksperling@apple.com>
Moves the pin 108 commits forward, from 9e56492e to the v2026.08.0 tag. Drop OTBR_SRP_ADVERTISING_PROXY. The 2026.07.0 release turned the OT core Advertising Proxy on by default whenever OTBR_MDNS is set, and the two are mutually exclusive, so forcing the POSIX proxy on now fails configure with "Only one Advertising Proxy can be enabled." Dropping the flag adopts the OT core proxy, which is upstream's default build. The mDNS provider is unchanged; both the mDNSResponder and avahi variants keep their own responder. OTBR_PRODUCT_NAME is not needed even though vendor and model are mandatory now, because otbr-agent.init already passes --model-name from board.json at runtime. 020-external-mbedtls stopped applying with strict context at 2026.07.0. A three way apply resolves every hunk without conflict, so it is refreshed with no change in what it does. The other build patches and the whole ubus series apply unchanged, as nothing upstream touched src/openwrt/ubus in that range. PKG_MIRROR_HASH is regenerated for the new tarball. Signed-off-by: Christian Glombek <c.glombek@cosa.systems> Assisted-By: Claude Fable 5
The ubus surface can form a Thread network but cannot change one that is already running. Replacing the credentials or the channel of a live network has to go through the pending dataset, so every node switches together when the delay timer expires rather than being orphaned. The existing ubus series (100-106, 110) stays untouched: the original patch files apply cleanly to 2026.07.0 as they are. New patches continue the series in three clean blocks, generated with git format-patch from the upstream PR branches: - 122-125: host groundwork (openthread/ot-br-posix#3489) — do not report the result of Leave twice, value-initialise the dataset in ScheduleMigration, erase the dataset in Leave based on the device role, report a dataset erase to state-change subscribers. That series also carries two NCP-mode patches, which this feed leaves out: it builds and starts otbr-agent in RCP mode only, and the ubus object is constructed only there. - 130-135: the ubus methods — provision through the host abstraction, set_pending forwarding to ThreadHost::ScheduleMigration() (MGMT_PENDING_SET), a pending_dataset_changed notification, a deprovision method (detach + erase; gentler than factory-reset leave, needed for Matter's RevertActiveDataset()), respond to provision once the join is under way, and threadstart/threadstop through the host abstraction. - 140: advertise mDNS under the system host name (openthread/ot-br-posix#3488). Signed-off-by: Christian Glombek <c.glombek@cosa.systems> Assisted-By: Claude Opus 5
This release rejects it outright: configure fails with "OTBR_MDNS=avahi is no longer supported", so the variant has stopped being buildable and only mDNSResponder remains. With no backend choice left there is nothing for the variants to vary, and openthread-br becomes a single package selecting mDNSResponder explicitly. Assisted-By: Claude Fable 5
Enable the border router feature set the other packagings build with: OT_BORDER_ROUTER for the management surface the ubus methods sit on, the channel manager and monitor so a channel can be changed and judged at runtime, and the SRP server's auto-enable. The REST API is switched off explicitly rather than by relying on the upstream default, since nothing here consumes it and it would listen on the infrastructure interface. Assisted-By: Claude Fable 5
The Matter Thread Network Diagnostics cluster needs a router table, which the neighbour list does not cover: a leader with a populated mesh has routers it is not a direct neighbour of. The neighbour rows also carry two mandatory frame counters that otNeighborInfo has had all along. Stacked on the ubus method series; see the openthread-br patch table for what is proposed upstream and what is carried here. Assisted-By: Claude Opus 5
The delegate advertised no PANChange support and answered SetPendingDataset and RevertActiveDataset with NOT_IMPLEMENTED, so a controller could form a Thread network through it but never change one already running, and a fail-safe expiry left the border router holding a dataset the controller had abandoned. With the ubus methods added to openthread-br, the delegate can now report PANChange, invoke set_pending to schedule a migration and deprovision to revert, and serve the pending dataset that status and pending_dataset_changed report. 030 also implements RevertActiveDataset in the fake delegate, so fail-safe rollback can be exercised without hardware. Also install an interface hotplug hook that restarts the daemon when the network device otbr-agent is configured with as infra_if_name comes back up: the daemon's mDNS sockets do not survive the interface they are bound to bouncing, which leaves the node undiscoverable for CASE until a restart. Signed-off-by: Christian Glombek <c.glombek@cosa.systems> Assisted-By: Claude Fable 5
Patch 035 replaces the demo stubs behind the NIM device type with live router state: the Wi-Fi Network Management cluster shares the LAN access point credentials read from netifd (guest networks excluded; wifi_share, wifi_network and wifi_iface in /etc/config/matter adjust or disable the sharing), Thread Network Diagnostics is fed from otbr-agent over ubus, the Thread Network Directory is seeded with the border router own network, and Ethernet Network Diagnostics reports the uplink counters. The service gains a procd reload trigger on wireless/network config changes that pokes the daemon over ubus, and the ubusd ACL grants the matter user read access to exactly network.wireless status. Applying a new ubusd ACL needs a ubusd restart, which breaks clients holding the old connection (rpcd, lighttpd, foris) - a reboot after install is the clean path. Assisted-By: Claude Fable 5
010-zap-disable-arl removes the ReviewFabricRestrictions commands from the checked-in data model. It has no effect on what the device does: the Access Control cluster is written in code and ignores its ZAP input entirely — the kManagedDevice feature bit, the Arl and CommissioningARL attributes and the command dispatch are all hardcoded under #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS (access-control-cluster.cpp:507, :552-554, :591), and the example provider is instantiated under the same guard. What actually turns the feature off is --enable-access-restrictions=no in CONFIGURE_OPTIONS, which has been there since the same commit that added this patch. That still holds, so removing the patch changes nothing except one less file to rebase. Assisted-By: Claude Opus 5
Five more patches out of the network-manager branch, from an audit of what this node tells a controller against what it can actually establish. - the setup passcode no longer goes to the system log on every start, where it outlived commissioning by the life of the device - CONFIG_BUILD_FOR_HOST_UNIT_TEST is off, so the encoder keeps the range check the specification requires of a device rather than of a test host - ProductName comes from the distribution name in /etc/os-release, with the product_name uci option to override; it read as an empty string before - BorderRouterName comes from the host name rather than being the same literal on every unit, which makes a mesh with more than one border router readable - ThreadVersion and InterfaceEnabled come from otbr rather than a constant and a guess at what a configured dataset implies - the Thread state cached from otbr-agent is dropped when it goes away, instead of being reported as current - SecurityPolicy, ChannelPage0Mask, the route table and the neighbour frame counters are served instead of encoded as null, empty or zero - the Ethernet counters answer with zero rather than a Failure status when they are switched off, ResetCounts does what it says, and RebootCount counts reboots of the router rather than restarts of this daemon Assisted-By: Claude Opus 5
The Thread Network Directory kept every network this node had ever been on. An entry is a whole operational dataset, network key included, readable by any commissioned fabric; Thread Border Router Management only returns the current one, so a superseded network's key was reachable through the directory and nowhere else. On a router that offers credential rotation that is a key the operator believes they have replaced. Two patches, because they belong in different places upstream. The first adds AddOrUpdateNetwork and ForgetNetwork to the ThreadNetworkDirectory cluster, so an application recording a network goes through the cluster rather than behind it and subscribers are told. The second is the application: it retracts the entry this node seeded when that network is superseded or when otbr reports none, and only that entry. Assisted-By: Claude Opus 5
ThreadBorderRouterManagementCluster uses the delegate Init() for two opposite things: Startup() passes the attribute-change callback, Shutdown() passes nullptr to detach it. The OpenWrt delegate treated both as initialisation and registered its ubus watch either way -- but the second call arrives after ApplicationShutdown() has stopped the ubus manager, whose Register() asserts it is initialised. Every clean stop ended in VerifyOrDie. The flaw is upstream, present since the OpenWrt border router integration landed, so the patch is a candidate to send back rather than something this packaging invented. Assisted-By: Claude Opus 5
The hotplug hook restarted the daemon when the backbone device came back up, because minimal-mDNS binds its sockets to the interfaces that existed when the Matter server started and has no way to be told they changed. A restart does fix the sockets, but it also drops every CASE session and subscription, so each controller has to re-establish the connection and attribute reporting stops until it does. On a border router the backbone comes up on every network reconfiguration, so this is not a rare event. Carry two patches to make a cheaper answer possible: - 045 keeps a single interface that fails to bind from taking the whole mDNS server down with it (proposed upstream as project-chip/connectedhomeip#73558). Without this, re-binding while interfaces are still settling can leave the daemon with no endpoints at all -- worse than the state it started from. - 046 makes the daemon re-run its interface enumeration on SIGHUP. This one is deliberately not proposed upstream: the real fix is the SDK noticing interface changes itself (project-chip/connectedhomeip#73559), which removes the need for the signal and for this hook entirely. Until that lands, the signal is the cheapest interim. The init script grows a `rebind` action that signals the running instance, and the hotplug hook calls that instead of `restart`. Sessions survive. Because a rebind is cheap, the hook no longer works out whether the device that came up is the one otbr-agent uses as its backbone -- asking otbr which interface it cares about was a roundabout way of saying "not the Thread interface". It now fires on any ifup and skips wpan* by name, which is the thing that was actually meant: the Thread interface is never advertised on, so it cannot change what is bound, and on a border router it changes often. The handler is installed just before the event loop starts, so a hotplug event in the window between the daemon forking and reaching that point still finds SIGHUP at its default action and kills it. procd respawns it, which lands on the old restart behaviour rather than anything worse, and the window is the daemon's startup. Both patches can go, along with the hook, once the upstream watcher lands. Assisted-By: Claude Opus 5 Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
75aa8ac to
86b987f
Compare
Updates the border-router stack to enable Matter management of the Thread network, including migration to a different dataset via the pending-dataset mechanism. In daily use on OpenWrt-based hardware:
git format-patchfrom the upstream PR branches, rebased onto v2026.08.0.matterubus object, pending-dataset support in the ubus delegate, and real data on all NIM clusters (upstream as [network-manager-app] pending dataset support over ubus connectedhomeip#73309).Gone since the last revision, per review: the netifd protocol integration — its proto script derived from the GPL-2.0-only one in openwrt/packages, which the CLA cannot accept, so the feed stays with the init-based model — and the GCC 14 patch for the vendored mbedtls, which this feed does not compile.
(On our Turris OS builds we additionally run the nftables firewall backend from openthread/ot-br-posix#3325; it is not carried in this PR.)
@ksperling-apple — the ubus series upstream: the 100–106/110 patches are your commits, and I'd like to see them land in ot-br-posix proper so the packagings can stop carrying them. Since the CLA has to come from the author, would you open that upstream PR yourself? My follow-on work is already in flight — openthread/ot-br-posix#3487 and openthread/ot-br-posix#3489 — and the ubus methods series that builds on yours (130–135 here) would follow, stacked on your PR.
The controller side works as well: Home Assistant imports the Thread dataset and Wi-Fi credentials from the network manager, surfaces the diagnostics, and can hand a border router a new network as a pending migration — those changes are heading upstream to home-assistant/core as a series of PRs.