Add Thread credential sharing to OTBR - #177371
Conversation
Add an otbr/create_ephemeral_key websocket command which activates ephemeral key (ePSKc) mode on the border router and returns the Thread Administration Passcode it generated, so another border router can be given temporary access to the Thread network. The REST endpoints are not in python-otbr-api yet, so they are called directly for now.
|
Hey there @home-assistant/core, mind taking a look at this pull request as it has been labeled with an integration ( Code owner commandsCode owners of
|
There was a problem hiding this comment.
Pull request overview
Adds an admin WebSocket command for temporary Thread credential sharing through OTBR.
Changes:
- Activates and replaces ephemeral keys through OTBR REST endpoints.
- Returns the passcode, lifetime, and UDP port.
- Adds success and error-path tests.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
homeassistant/components/otbr/const.py |
Defines the key lifetime. |
homeassistant/components/otbr/util.py |
Implements REST activation logic. |
homeassistant/components/otbr/websocket_api.py |
Registers and handles the command. |
tests/components/otbr/test_websocket_api.py |
Tests command behavior and failures. |
There was a problem hiding this comment.
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/otbr/util.py:237
- Return a conflict instead of deleting another caller's active key. With two concurrent requests, both callers can receive success, but the second request deletes the first caller's key before returning its own, leaving the first UI displaying credentials that no longer work; preserving the active session also prevents its dialog lifecycle from interfering with a replacement session.
response = await activate()
if response.status == HTTPStatus.CONFLICT:
# A key is already active, and one can only be started from the
# stopped state, so drop it and ask for a replacement.
delete_response = await session.delete(
homeassistant/components/otbr/init.py:54
- Catch support-probe failures locally so this optional capability cannot block OTBR setup.
_handle_otbr_errorconverts a timeout or connection error here intoHomeAssistantError, which the surrounding setup handler turns intoConfigEntryNotReadyeven though all required OTBR calls already succeeded.
otbrdata.ephemeral_key_supported = await otbrdata.get_ephemeral_key_supported(
hass
)
|
Based on what puddly mentioned within python-otbr-api#267, it now probes Only a 200 counts as supported, so the probe can't break setup for existing routers. (there may be a more optimal way of doing this, this is just how i thought would work well-ish) |
…, extend lifetime
| response = await activate() | ||
| if response.status == HTTPStatus.CONFLICT: | ||
| # A key is already active, and one can only be started from the | ||
| # stopped state, so replace it unless a device is using it right now |
| if ephemeral_key is not None and ephemeral_key != self.active_ephemeral_key: | ||
| return | ||
| self.active_ephemeral_key = None |
| connection.send_error(msg["id"], "delete_ephemeral_key_failed", str(exc)) | ||
| return | ||
|
|
||
| _LOGGER.info("Ephemeral key for %s deleted by %s", data.url, connection.user.name) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
homeassistant/components/otbr/util.py:110
- Preserve or revoke the active key when the config entry unloads. These fields exist only on
OTBRData, which setup recreates; after a reload, the dialog’s keyed delete compares againstNoneand reports success without sending DELETE, leaving the credential active until timeout, while a new create can also replace the still-displayed key.
active_ephemeral_key: str | None = None
active_ephemeral_key_expires: datetime | None = None
homeassistant/components/otbr/util.py:195
- Move these endpoint calls into
python-otbr-apibefore merging this feature. The integration currently duplicates protocol models, status handling, and raw REST calls as a temporary library workaround; upstream PR #267 has merged, but the latest release is still 2.10.0, so this should wait for a release and then consume the client API instead.
# The ephemeral key endpoints are called directly until a python-otbr-api
# release includes them (home-assistant-libs/python-otbr-api#267)
@_handle_otbr_error
| ], | ||
| ) | ||
| @pytest.mark.usefixtures("otbr_config_entry_multipan") | ||
| async def test_create_ephemeral_key_twice( |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (1)
homeassistant/components/otbr/util.py:195
- Use the released
python-otbr-apiePSKc methods instead of implementing its REST protocol in Core. The client implementation has already merged in home-assistant-libs/python-otbr-api#267, and integration code should remain a thin wrapper; please wait for its release/dependency bump and retain only the Home Assistant-specific locking and lifecycle policy here.
# The ephemeral key endpoints are called directly until a python-otbr-api
# release includes them (home-assistant-libs/python-otbr-api#267)
@_handle_otbr_error
|
Vallidation/Testing was done with a M5Stack CoreS3 Thread BR running custom software (cores3-epskc-commissioner) to help emulate/debug any issues that |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
homeassistant/components/otbr/util.py:195
- Use the
python-otbr-apiePSKc client methods instead of implementing its REST protocol in Core. Upstream PR #267 already contains the models and status/error handling, so this temporary duplication violates the integration thin-wrapper boundary; wait for a release and dependency bump, then retain only Home Assistant-specific lifecycle policy here.
# The ephemeral key endpoints are called directly until a python-otbr-api
# release includes them (home-assistant-libs/python-otbr-api#267)
@_handle_otbr_error
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Suppressed comments (1)
homeassistant/components/otbr/util.py:290
- Revoke the key when a successful activation response cannot be parsed. A 200 POST has already activated ePSKc, but this exception leaves the key uncached, so unload cannot revoke it and the router continues accepting the undisclosed credential until expiry; attempt cleanup before returning the failure and cover malformed responses with a DELETE assertion.
try:
activation = await response.json()
ephemeral_key, port = activation["tap"], activation["port"]
except (ValueError, KeyError, TypeError) as exc:
raise python_otbr_api.OTBRError("unexpected API response") from exc
| # Only 200 proves support; any other status hides the optional feature, | ||
| # while connection errors fail setup like the other startup calls | ||
| return response.status == HTTPStatus.OK |
| if otbrdata.active_ephemeral_key is not None: | ||
| try: | ||
| await otbrdata.deactivate_ephemeral_key(hass) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (4)
Previously missed (1) — in code that hasn't changed since the last review.
homeassistant/components/otbr/util.py:288
- Revoke a key when a successful activation response cannot be parsed. A 200 means the router already activated ePSKc, but invalid or missing
tap/portdata raises before the key is recorded, so unload cannot clean it up and the administration credential remains active until expiry. Perform a best-effort DELETE before surfacing this failure and cover it in the malformed-response tests.
try:
activation = await response.json()
ephemeral_key, port = activation["tap"], activation["port"]
except (ValueError, KeyError, TypeError) as exc:
raise python_otbr_api.OTBRError("unexpected API response") from exc
homeassistant/components/otbr/util.py:201
- Move the ePSKc HTTP protocol into
python-otbr-apibefore merging. Core currently pins 2.10.0, while upstream PR #267 has merged these methods but they are not released; duplicating routes, states, and status mapping here violates the integration's thin-wrapper boundary and leaves two client implementations to maintain. Wait for a library release/bump and keep only Home Assistant-specific lifecycle policy here.
session = async_get_clientsession(hass)
response = await session.get(
f"{self.url}/node/ba-epskc/state",
timeout=aiohttp.ClientTimeout(total=10),
)
homeassistant/components/otbr/util.py:204
- Keep transient support-probe failures retryable. This maps every non-200 response, including 500/503, to
False; becauseotbr/infoprobes again only while the value isNone, one temporary server error hides the feature until the entry reloads. ReturnFalseonly for explicit unsupported statuses and raise for other responses.
# Only 200 proves support; any other status hides the optional feature,
# while connection errors fail setup like the other startup calls
return response.status == HTTPStatus.OK
homeassistant/components/otbr/init.py:103
- Coordinate unload with in-flight key operations and reject activation after unload begins. This unlocked check can see
Nonewhile a WebSocket create holds the lifecycle lock awaiting the router; unload then completes, the create stores a live key afterward, and no config entry remains to revoke it.
if otbrdata.active_ephemeral_key is not None:
try:
await otbrdata.deactivate_ephemeral_key(hass)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
homeassistant/components/otbr/util.py:273
- Only replace a key when the reported state is known to be safe. OTBR can serialize unrecognized/future states as
"unknown"; the current denylist treats every such state as idle and sends DELETE, which could disconnect an active commissioner. Permit replacement only for"started"and fail closed for all other unexpected states.
if state in EPHEMERAL_KEY_IN_USE_STATES:
raise EphemeralKeyInUse
| try: | ||
| activation = await response.json() | ||
| ephemeral_key, port = activation["tap"], activation["port"] | ||
| except (ValueError, KeyError, TypeError) as exc: | ||
| raise python_otbr_api.OTBRError("unexpected API response") from exc |


Proposed change
Add an otbr/create_ephemeral_key websocket command which activates ephemeral key (ePSKc) mode on the border router and returns the Thread Administration Passcode it generated, so another border router can be given temporary access to the Thread network.
ePSKc.mp4
Type of change
Additional information
Checklist
ruff format homeassistant tests)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest.requirements_all.txt.Updated by running
python3 -m script.gen_requirements_all.To help with the load of incoming pull requests: