Skip to content

Import Thread datasets from Matter border routers - #179102

Open
LorbusChris wants to merge 2 commits into
home-assistant:devfrom
LorbusChris:ha2-matter-thread-dataset-import
Open

Import Thread datasets from Matter border routers#179102
LorbusChris wants to merge 2 commits into
home-assistant:devfrom
LorbusChris:ha2-matter-thread-dataset-import

Conversation

@LorbusChris

@LorbusChris LorbusChris commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Proposed change

A router implementing the Network Infrastructure Manager device type carries the Thread
Border Router Management cluster, which lets an authorised member of the Matter fabric
read the active operational dataset over an authenticated session -- an alternative to
reading the same credentials from a vendor-specific API such as the OTBR REST interface.

When a node exposes a border router endpoint, read its active dataset by command and add
it to the Thread dataset store with source "matter". The read is keyed on the active
dataset timestamp so node updates do not reissue the command while the dataset is
unchanged, and an attribute subscription on that timestamp re-imports when the dataset
changes underneath a quiet node -- which is exactly what a scheduled migration does.

Details covered by tests:

  • Command responses arrive as plain dicts with octet strings base64 encoded, so the
    dataset cannot be read off the response as an attribute.
  • A border router with no Thread stack running reports ExtAddress as NullValue rather
    than omitting it; the store rejects a preferred border agent ID without an extended
    address, so NullValue is excluded explicitly.
  • The get_node mock in the Matter test helpers found the requested node and then
    returned None; this is its first caller, so the helper is fixed here.
  • Coexistence with the otbr integration: a test seeds the store as otbr would, imports
    the same network over Matter, and asserts no duplicate entry and no change to the
    entry's source or preferred border agent.

Thread is declared as an after dependency, not a dependency: Matter does not require
Thread, and a hard dependency would set up the Thread integration (and so zeroconf) for
every Matter config entry.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

@LorbusChris
LorbusChris requested a review from a team as a code owner August 13, 2026 23:46
Copilot AI balanced review requested due to automatic review settings August 13, 2026 23:46
@home-assistant home-assistant Bot added cla-signed has-tests integration: matter new-feature Top 100 Integration is ranked within the top 100 by usage Top 200 Integration is ranked within the top 200 by usage Top 50 Integration is ranked within the top 50 by usage Quality Scale: No score labels Aug 13, 2026
@home-assistant

Copy link
Copy Markdown
Contributor

Hey there @home-assistant/matter, mind taking a look at this pull request as it has been labeled with an integration (matter) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of matter can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant mark-draft Mark the pull request as draft.
  • @home-assistant ready-for-review Remove the draft status from the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign matter Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant update-branch Update the pull request branch with the base branch.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Imports Thread datasets from Matter border routers into Home Assistant’s shared Thread dataset store.

Changes:

  • Adds dataset discovery, import, timestamp tracking, and subscriptions.
  • Adds Thread ordering as an optional after-dependency.
  • Adds fixtures, tests, and a Matter test-helper fix.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
homeassistant/components/matter/adapter.py Schedules imports and subscribes to dataset changes.
homeassistant/components/matter/thread_border_router.py Reads and stores border-router datasets.
homeassistant/components/matter/manifest.json Adds the Thread after-dependency.
tests/components/matter/common.py Corrects mocked node lookup.
tests/components/matter/fixtures/nodes/thread_border_router.json Adds a border-router fixture.
tests/components/matter/test_thread_border_router.py Tests import and coexistence behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread homeassistant/components/matter/adapter.py Outdated
Comment thread homeassistant/components/matter/adapter.py
Comment thread homeassistant/components/matter/adapter.py Outdated
Copilot AI review requested due to automatic review settings August 13, 2026 23:50
@LorbusChris
LorbusChris force-pushed the ha2-matter-thread-dataset-import branch from 083e1a9 to d781f6c Compare August 13, 2026 23:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

homeassistant/components/matter/adapter.py:227

  • Tie the retry timer to the config entry's unload lifecycle. Otherwise a pending timer can fire after Matter has unloaded, access the stopped client, and add a new background task after unload has already cancelled tracked tasks.
            async_call_later(self.hass, THREAD_DATASET_RETRY_DELAY, _retry)

tests/components/matter/test_thread_border_router.py:34

  • Stub the discovery timeout in this fixture as well as zeroconf. The mocked listener never reports a router, so each dataset addition leaves _set_preferred_dataset_if_only_network waiting for the production 30-second timeout; subsequent async_block_till_done() calls can make these tests take 30 seconds each.
@pytest.fixture(autouse=True)
def mock_thread_discovery(mock_async_zeroconf: MagicMock) -> MagicMock:
    """Adding a dataset starts Thread discovery, which would open a real socket."""
    return mock_async_zeroconf

Copilot AI review requested due to automatic review settings August 13, 2026 23:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

homeassistant/components/matter/adapter.py:227

  • Register the retry timer with the config entry so it is canceled on unload. As written, a NodeNotReady timer survives a Matter reload, then uses the disconnected old client and can create a new background task after that entry's lifecycle has ended.
            async_call_later(self.hass, THREAD_DATASET_RETRY_DELAY, _retry)

homeassistant/components/matter/adapter.py:217

  • Add coverage for the NodeNotReady recovery path. None of the new tests raises this exception, so timestamp clearing, delayed retry execution, and retry cleanup can regress without detection.
        except NodeNotReady:
            # The node is mid-resubscription after a restart; the availability
            # event can even arrive while it is still not ready, so a plain
            # retrigger is not enough. Try again once things have settled.
            self._thread_dataset_timestamps.pop(key, None)

Copilot AI review requested due to automatic review settings August 14, 2026 00:02
@LorbusChris
LorbusChris force-pushed the ha2-matter-thread-dataset-import branch from d781f6c to 23763a5 Compare August 14, 2026 00:02
@LorbusChris
LorbusChris force-pushed the ha2-matter-thread-dataset-import branch from 23763a5 to c44cd86 Compare August 14, 2026 00:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (3)

homeassistant/components/matter/adapter.py:176

  • Invoke dataset scheduling from endpoint-added events as well. This path is only reached through _setup_node, while endpoint_added_callback (lines 84–93) only calls _setup_endpoint; an ENDPOINT_ADDED event can be the sole notification for a new endpoint, so a newly exposed border-router endpoint is not imported or subscribed until an unrelated node update occurs.
        try:

homeassistant/components/matter/adapter.py:175

  • Replace this explanation with the actual reason for the call placement. The dataset read runs in a background task, so its exceptions cannot be caught by the preceding synchronous try regardless of this call's placement; the relevant behavior is that imports are still scheduled after entity setup fails.
    def _setup_node(self, node: MatterNode) -> None:
        """Set up an node."""
        LOGGER.debug("Setting up entities for node %s", node.node_id)

homeassistant/components/matter/adapter.py:227

  • Cancel delayed retries when the config entry unloads. async_call_later returns a cancellation callback, but it is dropped here; unloading Matter during this delay lets _retry later access the disconnected client and create a background task after unload processing has already canceled tracked tasks.
        dataset changes again.

Copilot AI review requested due to automatic review settings August 14, 2026 00:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

homeassistant/components/matter/adapter.py:113

  • Clear the Thread import state in node_removed_callback before looking up the node. That callback returns when the client has already evicted the node, so without an ENDPOINT_REMOVED event these entries remain and a later node reusing the same ID and timestamp can have its dataset import suppressed.
            key = (data["node_id"], data["endpoint_id"])
            if unsubscribe := self._thread_dataset_subscriptions.pop(key, None):
                unsubscribe()
            self._thread_dataset_timestamps.pop(key, None)

homeassistant/components/matter/adapter.py:209

  • Re-import when the preferred border router's extended address changes, not only when the dataset timestamp changes. DatasetStore._async_maybe_update_preferred_border_agent explicitly refreshes this address because a router can regenerate it while keeping the same dataset and border-agent ID; the timestamp-only cache and subscription leave Matter-sourced entries stale until another dataset change or restart.
            timestamp = get_active_dataset_timestamp(endpoint)
            # _setup_node also runs on every node update; only re-read the
            # dataset when its timestamp shows it actually changed.
            if key in self._thread_dataset_timestamps and (
                self._thread_dataset_timestamps[key] == timestamp

Copilot AI review requested due to automatic review settings August 14, 2026 00:30
Copilot AI review requested due to automatic review settings August 14, 2026 01:19
@LorbusChris
LorbusChris force-pushed the ha2-matter-thread-dataset-import branch from 97c56b4 to b7309c0 Compare August 14, 2026 01:19
@LorbusChris
LorbusChris force-pushed the ha2-matter-thread-dataset-import branch from b7309c0 to 8bf3599 Compare August 14, 2026 01:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

homeassistant/components/matter/thread_border_router.py:89

  • Decode with validate=True so malformed base64 is rejected. The default decoder silently discards non-alphabet characters, so a corrupted value containing an otherwise valid dataset can be imported instead of taking the error path.
            return b64decode(raw)

homeassistant/components/matter/adapter.py:278

  • Bound or availability-gate the NodeNotReady retries. Every retry failure schedules another timer, so an offline or persistently unready border router is polled every 30 seconds indefinitely; rely on the availability event after a bounded retry or track a retry limit.
            if cancel_previous := self._thread_dataset_retries.pop(key, None):
                cancel_previous()
            self._thread_dataset_retries[key] = async_call_later(
                self.hass, THREAD_DATASET_RETRY_DELAY, _retry
            )

Copilot AI review requested due to automatic review settings August 14, 2026 01:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

homeassistant/components/matter/adapter.py:225

  • Skip dataset imports while the node is unavailable. setup_nodes() also visits cached offline nodes, and every resulting NodeNotReady currently rearms the 30-second callback, so an offline border router is polled indefinitely; the existing NODE_UPDATED path will schedule the import once it becomes available.
        for endpoint in get_border_router_endpoints(node):

homeassistant/components/matter/thread_border_router.py:89

  • Decode the response with strict base64 validation. b64decode() defaults to validate=False and silently discards non-alphabet characters, so malformed values such as invalid characters prefixed to an otherwise valid dataset are imported instead of being rejected as this function intends.
            return b64decode(raw)

Copilot AI review requested due to automatic review settings August 14, 2026 01:35
@LorbusChris
LorbusChris force-pushed the ha2-matter-thread-dataset-import branch from 8bf3599 to f119e91 Compare August 14, 2026 01:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

homeassistant/components/matter/thread_border_router.py:113

  • Distinguish malformed responses from a legitimate empty dataset before marking the import complete. _dataset_from_response also returns None for missing, wrong-typed, or invalid-base64 payloads, so this normal return leaves the adapter's timestamp cached and suppresses every later read while the timestamp is unchanged. Propagate a parse-failure status or exception so the adapter clears/retries that state, while keeping empty bytes as the unprovisioned case.
    dataset = _dataset_from_response(response)

    if not dataset:

Copilot AI review requested due to automatic review settings August 14, 2026 02:42
@LorbusChris
LorbusChris force-pushed the ha2-matter-thread-dataset-import branch from f119e91 to f7ad0de Compare August 14, 2026 02:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

homeassistant/components/matter/thread_border_router.py:140

  • Guard the delayed preferred-dataset selection before enabling this automatic add. On a first import, DatasetStore.async_add starts a discovery task that later assigns preferred_dataset without rechecking it (dataset_store.py:355-366, 492-496), so a user or migration choosing another network during that window is overwritten; merge #179098 or add its equivalent guard first.
    await async_add_dataset(hass, DOMAIN, dataset.hex(), **preferred)

A router implementing the Network Infrastructure Manager device type carries
the Thread Border Router Management cluster, which lets an authorised member
of the Matter fabric read the active operational dataset over an
authenticated session. That is an alternative to reading the same
credentials from a vendor specific API such as the OpenThread Border Router
REST interface.

When a node exposes a border router endpoint, read its active dataset by
command and add it to the Thread dataset store with source "matter". The
read is keyed on the active dataset timestamp so node updates do not reissue
the command while the dataset is unchanged, and an attribute subscription on
that timestamp re-imports when the dataset changes underneath a quiet node,
which is exactly what a scheduled migration does: without it the change
would go unnoticed until the next interview.

Three details are load bearing and are covered by tests:

- The Matter client returns command responses as plain dicts with octet
  strings base64 encoded, so the dataset cannot be read off the response as
  an attribute.
- A border router with no Thread stack running reports ExtAddress as
  NullValue rather than omitting it. NullValue is not falsy, and the store
  rejects a preferred border agent ID that is not accompanied by an extended
  address, so it has to be excluded explicitly.
- The get_node mock in the Matter test helpers found the requested node and
  then returned None; this is its first caller, so the helper is fixed here.

Thread is declared as an after dependency rather than a dependency. Matter
does not require Thread, and making it a hard dependency would set up the
Thread integration, and so zeroconf, for every Matter config entry.

Assisted-By: Claude Opus 5
The store already handles two integrations writing to it: async_add()
deduplicates on dataset and extended PAN ID, and the preference setter
only acts when none is stored or the border agent ID matches. Assert that
importing over Matter leaves an otbr-seeded entry alone, and cover the
NodeNotReady retry (including cancellation on unload and replacement on
re-arm), state cleanup on endpoint and node removal, and the response
shapes the dataset read accepts.

Assisted-By: Claude Opus 5
Copilot AI review requested due to automatic review settings August 14, 2026 14:12
@LorbusChris
LorbusChris force-pushed the ha2-matter-thread-dataset-import branch from f7ad0de to 9199bfb Compare August 14, 2026 14:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@lboue

lboue commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

How can I test this PR?

@LorbusChris

Copy link
Copy Markdown
Contributor Author

You can e.g. set up an OpenWrt device (with a Thread dongle/antenna) as TBR + Matter NIM. With that, one can then migrate devices between HA's internal TBR and the external OpenWrt one.

@lboue

lboue commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

You can e.g. set up an OpenWrt device (with a Thread dongle/antenna) as TBR + Matter NIM. With that, one can then migrate devices between HA's internal TBR and the external OpenWrt one.

Can OpenWRT be run in a VM with a Thread dongle/antenna?

@LorbusChris

Copy link
Copy Markdown
Contributor Author

You can e.g. set up an OpenWrt device (with a Thread dongle/antenna) as TBR + Matter NIM. With that, one can then migrate devices between HA's internal TBR and the external OpenWrt one.

Can OpenWRT be run in a VM with a Thread dongle/antenna?

@lboue I don't see why not, but it's not something I've tested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed has-tests integration: matter new-feature Quality Scale: No score Top 50 Integration is ranked within the top 50 by usage Top 100 Integration is ranked within the top 100 by usage Top 200 Integration is ranked within the top 200 by usage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants