Skip to content

Add pending-dataset TLV write support - #269

Open
LorbusChris wants to merge 1 commit into
home-assistant-libs:mainfrom
LorbusChris:set-pending-dataset-tlvs
Open

Add pending-dataset TLV write support#269
LorbusChris wants to merge 1 commit into
home-assistant-libs:mainfrom
LorbusChris:set-pending-dataset-tlvs

Conversation

@LorbusChris

@LorbusChris LorbusChris commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Add set_pending_dataset_tlvs(), the pending-endpoint twin of set_active_dataset_tlvs(), so a controller can hand a border router a new operational dataset as a raw TLV pending set.

The TLV parser gains the write-side counterparts callers need to build such a dataset: Timestamp.from_values(), with range checks for the 48-bit seconds and 15-bit ticks fields, and
DelayTimer.from_milliseconds(). PENDINGTIMESTAMP and DELAYTIMER now decode to typed items, matching ACTIVETIMESTAMP.

Also raise TLVError instead of leaking struct.error on a malformed timestamp TLV, and log unknown TLVs by type and length only -- the value may hold network credentials.

The write is refused while a pending dataset is already in place. Superseding one is never safe to do implicitly: the replacement races the delay timer on every device that already holds the old dataset, so a late replacement can split the mesh, and it would silently undo whatever the in-flight dataset was doing. Both refusal paths raise PendingDatasetConflictError, an OTBRError subclass, so callers can tell the refusal apart without matching messages.

The check runs locally first, and again on the border router: the request always carries If-None-Match: *, which a border router with openthread/ot-br-posix#3552 evaluates atomically with the write, closing the race the local GET-then-PUT check leaves open. Older border routers ignore the header and keep relying on the local check. This PR is the client-side counterpart of that one.

Required by: home-assistant/core#178291
Counterpart to: openthread/ot-br-posix#3552

AI use: Written with AI assistance -- the commit carries an Assisted-By trailer. I've reviewed and understand all of it, and will be answering questions myself.

@LorbusChris

Copy link
Copy Markdown
Contributor Author

Kindly requesting CI workflow approval to see if anything pops up. cc @puddly

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds raw pending-dataset TLV write support needed for coordinated Thread network migration.

Changes:

  • Adds set_pending_dataset_tlvs().
  • Adds typed timestamp and delay-timer construction/parsing.
  • Improves malformed-TLV errors and credential-safe logging.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
python_otbr_api/__init__.py Adds the pending-dataset TLV endpoint.
python_otbr_api/tlv_parser.py Adds TLV builders, typed decoding, and safer errors/logging.
tests/test_init_legacy.py Tests pending-dataset requests and errors.
tests/test_tlv_parser.py Tests new parser and builder behavior.
pyproject.toml Bumps the release to 2.11.0.

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

Comment thread python_otbr_api/tlv_parser.py Outdated
@LorbusChris
LorbusChris force-pushed the set-pending-dataset-tlvs branch from 758881b to 85ec955 Compare August 18, 2026 18:28
@lboue

lboue commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Can we try this PR with any TBR?

@LorbusChris

LorbusChris commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

The way I've tested and am using this is with an OpenWrt-based TBR + Matter Network Infrastructure Manager, i.e. the OpenWrt device is added to the Matter fabric and its TBR can then be managed through HA via Matter.

See also project-chip/matter-openwrt#53

Edit: Forgot to mention, this also enables Thread cred rotation on the HA managed OTBR.

@agners agners left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Besides the pending dataset overwrite protection, I wonder if we should move some of the logic of home-assistant/core#178291 into the library.

Or maybe even one step further: Move it into the OTBR backend (REST API). But since we generally support older BRs, we'd probably need validation here still 🤔 .

Comment thread python_otbr_api/__init__.py
@LorbusChris

Copy link
Copy Markdown
Contributor Author

I believe this should be good to go now

Comment thread python_otbr_api/__init__.py Outdated
mesh silently ignores a pending dataset that is not newer than the one
it already holds, while the router accepts the write. A caller that
deliberately supersedes the in-flight dataset -- having stamped the new
one above it -- passes allow_replace=True.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why is allow_replace needed actually? The driving feature here is a feature which allows to merge Thread networks (home-assistant/core#178291), but do we need to be able to merge one while another pending dataset is in flight? Similar to what we discussed in #272 (review), this seems unnecessary risk to me... Or maybe I miss something?

I get that we added support to make writing pending dataset safer in openthread/ot-br-posix#3552 (not accidentally overwriting something we did not intend to). But from how I understood it was mostly about supporting If-None-Match: *, allowing to safely prevent overwriting a pending dataset if one is present. And that is a nice addition to the OTBR REST API.

But from what I can tell, the use cases we want to support should not require pending dataset overwrite.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The use case is aborting a migration, not merging a dataset during an ongoing one.

Once a pending dataset has propagated, every device is counting down; delete_pending_dataset()
only drops the local copy, so the sole way to stop the switch is a newer pending dataset.
Otherwise a mistaken migration is irreversible until it completes.
test_in_flight_migration_away_is_superseded in home-assistant/core#178291
covers that.

I agree that this isn't ideal yet, but the "replace the dataset I just read" case needs #274 on
openthread/ot-br-posix#3553, which I'd like to defer for now. So this flag stays the only
mechanism for the time being, and as-is home-assistant/core#178291 depends on it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Otherwise a mistaken migration is irreversible until it completes.

Yes, that is the consequence which I would accept.

As stated in #272 (review), the problem of sending a new pending dataset is that it's not certain that all devices receive it in time. So you may end up some devices migrated, and some not. Now you can make the argument so what, it is a "best effort" cancellation. But the problem I see is that with silently incrementing timestamp and sending another dataset doesn't make the user aware that there was already one pending, and the possible consequences.

Also, this potentially reverts a pending set channel operation without the user knowing. And as we add more features, how can we make sure those don't interfere. Rejecting any changes while a pending dataset is present is also a cheap way to avoid any such concurrency issue.

IMHO, home-assistant/core#178291 should just handle the upstream rejection and tell the user. But maybe we should discuss the Core PR in general first.

@LorbusChris
LorbusChris force-pushed the set-pending-dataset-tlvs branch 2 times, most recently from 162e5b3 to 08fffc2 Compare August 28, 2026 23:47
Add set_pending_dataset_tlvs(), the pending-endpoint twin of
set_active_dataset_tlvs(), so a controller can hand a border router a
new operational dataset as a raw TLV pending set.

The TLV parser gains the write-side counterparts callers need to build
such a dataset: Timestamp.from_values(), with range checks for the
48-bit seconds and 15-bit ticks fields, and
DelayTimer.from_milliseconds(). PENDINGTIMESTAMP and DELAYTIMER now
decode to typed items, matching ACTIVETIMESTAMP.

Also raise TLVError instead of leaking struct.error on a malformed
timestamp or delay timer TLV, and log unknown TLVs by type and length only -- the
value may hold network credentials.

Like the channel change, the write is refused while a pending dataset
is already in place. A not-newer one would be silently ignored by the
mesh, and superseding is never safe to do implicitly: the replacement
races the delay timer on every device that already holds the old
dataset, so a late replacement can split the mesh, and it would also
silently undo whatever the in-flight dataset was doing. A caller that
finds the write refused should surface that, not retry.

The check runs locally first, and again on the border router: unless
allow_replace is set the request carries "If-None-Match: *", which a
border router with openthread/ot-br-posix#3552 evaluates atomically
with the write, closing the race the local check leaves open. Older
border routers ignore the header and keep relying on the local check.

Both refusals raise PendingDatasetConflictError rather than a plain
OTBRError, so a caller can tell them apart from a transport or protocol
failure without matching on the message: nothing was written, and the
state that made the write wrong is there to be read back.

Assisted-By: Claude Fable 5
@LorbusChris
LorbusChris force-pushed the set-pending-dataset-tlvs branch from 08fffc2 to ea41b20 Compare September 1, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants