feat(teslemetry): add OAuth dynamic client registration helper - #120
Merged
Conversation
Adds register_client()/TeslemetryClientRegistration/TeslemetryRegistrationError so the Home Assistant Teslemetry integration's client-registration transport (RFC 7591 DCR: HTTP POST, response parsing, error handling) can move out of its own oauth.py and into this library.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Add an OAuth Dynamic Client Registration (DCR, RFC 7591) transport helper to the tesla-fleet-api library, so the Home Assistant Teslemetry integration can stop carrying the client-registration HTTP transport itself (per HA maintainer MartinHjelmare's requirement on home-assistant/core PR #175656 that the POST-and-parse logic and its error type live in this library, not the integration's oauth.py). HA core PR 175656 (Bre77's fork branch) is the reference spec/contract to mirror faithfully: endpoint URL, request JSON payload (client_name/software_id/software_version), response parsing (client_id extraction), and the three failure modes (transport/timeout error, non-2xx response, malformed/missing-client_id response). Implemented: a module-level async function register_client(session, client_name, software_id, software_version) in tesla_fleet_api/teslemetry/teslemetry.py (not a Teslemetry instance method, since registration precedes having a client_id or access token), returning a typed frozen-dataclass TeslemetryClientRegistration (client_id + raw), and a new TeslemetryRegistrationError(TeslaFleetError) in exceptions.py following this library's existing exception-hierarchy/message conventions. Scoping constraint from the captain: DCR is Teslemetry-only - it must live in the Teslemetry-specific module/exception, NOT in the shared base TeslaFleetApi class or any other backend (Tessie, plain Fleet API), since Teslemetry is the only service offering DCR today; do not add it to shared auth machinery or speculate about other registration flows Tesla might add. No HA imports or HA-specific types - usable by any aiohttp-based consumer the same way this library's other entry points are, matching this repo's existing free-function exports (e.g. firmware_at_least). One deliberate deviation from HA's code: guard registration.get(...) with isinstance(registration, dict) so a valid-but-non-dict JSON body (list/scalar) raises the same typed TeslemetryRegistrationError instead of an uncaught AttributeError - matches this library's existing 'malformed data never escapes uncaught' convention (e.g. find_authorized_clients). KISS: this is a transport move, not an auth redesign - no new config knobs, no refactor of unrelated auth code. Added tests/test_teslemetry_register_client.py covering success, server-rejected registration, transport/timeout failure, and malformed-response paths (non-JSON, missing/empty/non-string client_id, non-dict body, null body), and a new 'OAuth Dynamic Client Registration' section in docs/teslemetry.md plus an AGENTS.md knowledge entry, following this repo's existing doc/test conventions. Full existing test suite, ruff check/format, and pyright strict all verified green locally before this run. Goal: one lean PR; no release/version-bump/tag work (that and the HA-side consumption bump are separate follow-up tasks).
What Changed
register_client(session, client_name, software_id, software_version)helper intesla_fleet_api/teslemetry/teslemetry.pyimplementing OAuth Dynamic Client Registration (RFC 7591) against Teslemetry, returning a typed frozen-dataclassTeslemetryClientRegistration(parsedclient_idplus the raw response).TeslemetryRegistrationError(TeslaFleetError)inexceptions.pycovering transport/timeout failures, non-2xx responses, and malformed or missing-client_idresponse bodies (including non-dict JSON), and export both new symbols fromtesla_fleet_api/__init__.pyandtesla_fleet_api/teslemetry/__init__.py.tests/test_teslemetry_register_client.py) covering success, server rejection, connection/timeout errors, and malformed-response paths, plus a new "OAuth Dynamic Client Registration" section indocs/teslemetry.mdand anAGENTS.mdknowledge entry.Risk Assessment
✅ Low: Small, additive, Teslemetry-scoped module-level helper with a dedicated exception type, faithfully mirroring the reference HA PR's endpoint/payload/failure-mode contract (verified against home-assistant/core#175656's oauth.py), no changes to shared/base classes, and thorough test coverage of all documented failure modes.
Testing
Ran the focused unit test suite for the new register_client() DCR helper (10/10 passing) and additionally drove the function against a real local aiohttp HTTP server (success, 400 rejection, non-JSON body, missing client_id, and closed-port transport failure) to confirm the request payload, response parsing, and all three documented failure modes behave end-to-end exactly as the user intent specifies; no issues found.
Evidence: Real-HTTP end-to-end transcript for register_client() (success + 4 failure paths)
[success] request={'client_name': 'Home Assistant', 'software_id': 'home-assistant', 'software_version': '2026.8.1'} -> TeslemetryClientRegistration(client_id='abc123-registered-client-id', raw={...}) [rejected] raised TeslemetryRegistrationError: message='Teslemetry dynamic client registration failed.' status=400 data='Could not reach Teslemetry to register a client' [malformed_non_json] raised TeslemetryRegistrationError: ... data='Teslemetry returned a malformed registration response' [missing_client_id] raised TeslemetryRegistrationError: ... data='Teslemetry registration response did not contain a client_id' [transport] raised TeslemetryRegistrationError: ... data='Could not reach Teslemetry to register a client'Evidence: Manual end-to-end verification script (real aiohttp client+server round trip)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
uv run pytest tests/test_teslemetry_register_client.py -v— all 10 cases (success, server rejection, connection error, timeout, non-JSON body, missing/non-string/empty client_id, non-dict body, null body) passManual end-to-end run of register_client() against a real local aiohttp server (not mocks): verified the actual HTTP request carries {client_name, software_id, software_version}, a 200 JSON response with client_id parses into TeslemetryClientRegistration, a 400 response, a non-JSON body, and a missing-client_id body each raise TeslemetryRegistrationError with the expected message/status/data, and a connection to a closed port raises the same typed error for the transport-failure path✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.