Skip to content

feat(teslemetry): add typed authorized clients accessor#76

Merged
Bre77 merged 6 commits into
mainfrom
fm/tfa-authclients-accessor-g7
Jul 12, 2026
Merged

feat(teslemetry): add typed authorized clients accessor#76
Bre77 merged 6 commits into
mainfrom
fm/tfa-authclients-accessor-g7

Conversation

@Bre77

@Bre77 Bre77 commented Jul 12, 2026

Copy link
Copy Markdown
Member

Intent

Two rounds of amendment on top of the original typed-accessor PR, per upstream-consumer (captain) review feedback:

Round 1 - tighten to evidenced wire shape: the original commit's accessor mirrored HA's config_flow.py _iter_clients with a broad recursive/depth-first wrapper-key search and a 3-way None/absent/empty distinction. Replaced with a precise, single-path envelope unwrap of only the confirmed real shape ({"response": {"authorized_clients": [...]}}, or a bare list with no envelope) - no recursive search - because the site used for validation currently has zero paired clients, so the populated multi-wrapper-key form _iter_clients defends against cannot be sampled to confirm, and building broad normalization against unconfirmed shapes was ruled out. AuthorizedClient is trimmed to only public_key and state - the two fields the pairing flow actually reads - dropping unevidenced description/key_type/authorized_client_type fields; each accepts only the specific key-name variant pairs _iter_clients/_is_verified actually check (public_key/publicKey, state/authorized_client_state). state is typed via AuthorizedClientState (const.py - the schema of record, since Tesla has not published an OpenAPI schema for this pairing endpoint), normalizing both int and string wire forms; a present-but-unrecognized value passes through raw rather than being dropped to None. A None response body, an unrecognized response shape, and an explicit empty authorized_clients list are no longer distinguished - all three collapse to a typed empty list (AuthorizedClients.clients is always a list, never None), since there is no current evidence those three inputs mean anything different from each other for this endpoint. This lets the HA integration fully delete its own _iter_clients helper and consume this accessor directly.

IMPORTANT for the PR body: note explicitly that the canonical wire form (the envelope shape and the public_key/state field names) is pinned from the consuming integration's own observed/defensive handling, not an official Tesla spec - Tesla has not published an OpenAPI schema for pairing endpoints - and that live-sample confirmation with an actual populated client list is a follow-up, not yet done.

Round 2 - rename per inline PR review comment (Bre77, on tesla_fleet_api/teslemetry/energysite.py line 181: "I think this should be called find_authorized_clients"): renamed get_authorized_clients to find_authorized_clients across the method, its docstring, AGENTS.md, docs/teslemetry.md, and all tests, since the method isn't a straight getter - it normalizes/parses the raw response into the typed model - and this library already uses a find_ prefix for exactly that kind of lookup/normalize method (find_server in tesla/fleet.py and teslemetry/teslemetry.py and tessie/tessie.py; find_vehicle in tesla/vehicle/bluetooth.py), so this matches existing naming convention rather than inventing a new one.

New tests added in round 1 cover: bare-list-no-envelope, camelCase/alternate-key-name entry fields, state-as-string normalization, a present-but-unrecognized state value surviving parsing (not coerced to None), explicit-empty-list, absent-field, and null-body - all producing the typed empty list.

What Changed

  • Added TeslemetryEnergySite.find_authorized_clients() with typed normalization for the evidenced authorized-clients response shapes, returning AuthorizedClients.clients as a list of AuthorizedClient entries with normalized AuthorizedClientState values where recognized.
  • Documented the Teslemetry authorized-clients accessor, including that the pinned envelope and field names come from observed consuming-integration handling rather than an official Tesla pairing-endpoint schema.
  • Added focused coverage for bare-list and enveloped responses, alternate key names, string and unknown state values, empty or absent clients, and null response bodies.

Risk Assessment

✅ Low: The change is well-bounded to a new typed Teslemetry accessor with conservative parsing semantics and focused coverage for the documented shapes; I found no material merge-blocking or follow-up issues in the changed code.

Testing

Ran the focused authorized-clients tests, generated an API-level transcript showing find_authorized_clients() calling the Teslemetry endpoint and normalizing the intended wire shapes, then ran the full test suite; all exercised checks passed.

Evidence: find_authorized_clients API transcript
{
  "http_requests": [
    {
      "method": "GET",
      "url": "https://api.teslemetry.com/api/1/energy_sites/12345/command/authorized_clients"
    },
    {
      "method": "GET",
      "url": "https://api.teslemetry.com/api/1/energy_sites/12345/command/authorized_clients"
    },
    {
      "method": "GET",
      "url": "https://api.teslemetry.com/api/1/energy_sites/12345/command/authorized_clients"
    },
    {
      "method": "GET",
      "url": "https://api.teslemetry.com/api/1/energy_sites/12345/command/authorized_clients"
    },
    {
      "method": "GET",
      "url": "https://api.teslemetry.com/api/1/energy_sites/12345/command/authorized_clients"
    }
  ],
  "public_api": {
    "find_authorized_clients_callable": true,
    "get_authorized_clients_present": false
  },
  "results": [
    {
      "client_count": 1,
      "clients": [
        {
          "public_key": "MIIBCgKCAQEAsomeBase64EncodedRsaPublicKeyBytes==",
          "raw": {
            "public_key": "MIIBCgKCAQEAsomeBase64EncodedRsaPublicKeyBytes==",
            "state": 3
          },
          "state": {
            "enum": "AuthorizedClientState",
            "name": "VERIFIED",
            "value": 3
          }
        }
      ],
      "clients_type": "list",
      "raw_input": {
        "response": {
          "authorized_clients": [
            {
              "public_key": "MIIBCgKCAQEAsomeBase64EncodedRsaPublicKeyBytes==",
              "state": 3
            }
          ]
        }
      },
      "scenario": "canonical envelope"
    },
    {
      "client_count": 1,
      "clients": [
        {
          "public_key": "MIIBCgKCAQEAsomeBase64EncodedRsaPublicKeyBytes==",
          "raw": {
            "authorized_client_state": "verified",
            "publicKey": "MIIBCgKCAQEAsomeBase64EncodedRsaPublicKeyBytes=="
          },
          "state": {
            "enum": "AuthorizedClientState",
            "name": "VERIFIED",
            "value": 3
          }
        }
      ],
      "clients_type": "list",
      "raw_input": [
        {
          "authorized_client_state": "verified",
          "publicKey": "MIIBCgKCAQEAsomeBase64EncodedRsaPublicKeyBytes=="
        }
      ],
      "scenario": "bare list with alternate field names"
    },
    {
      "client_count": 0,
      "clients": [],
      "clients_type": "list",
      "raw_input": {
        "response": {
          "authorized_clients": []
        }
      },
      "scenario": "explicit empty list"
    },
    {
      "client_count": 0,
      "clients": [],
      "clients_type": "list",
      "raw_input": null,
      "scenario": "null body"
    },
    {
      "client_count": 1,
      "clients": [
        {
          "public_key": "MIIBCgKCAQEAsomeBase64EncodedRsaPublicKeyBytes==",
          "raw": {
            "public_key": "MIIBCgKCAQEAsomeBase64EncodedRsaPublicKeyBytes==",
            "state": 0
          },
          "state": 0
        }
      ],
      "clients_type": "list",
      "raw_input": {
        "response": {
          "authorized_clients": [
            {
              "public_key": "MIIBCgKCAQEAsomeBase64EncodedRsaPublicKeyBytes==",
              "state": 0
            }
          ]
        }
      },
      "scenario": "present unrecognized state preserved"
    }
  ]
}

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_authorized_clients.py
  • uv run python - <<'PY' > /tmp/no-mistakes-evidence/01KXC5AWPQB347GEW657J3FMYK/find_authorized_clients_transcript.json ...
  • uv run pytest tests
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

firstmate crewmate added 2 commits July 13, 2026 07:26
Adds a typed dataclass accessor over TeslemetryEnergySite's undocumented
list_authorized_clients() response so API-response parsing lives in the
library instead of being reimplemented in the Home Assistant integration
(HA core PR #176328). Field access checks key presence rather than
truthiness so a legal falsy value (e.g. authorized_client_type=0) isn't
mistaken for a missing field, and an explicitly present but empty
authorized_clients list is returned as [] (authoritative: zero clients)
rather than collapsed into the same None used for a genuinely unknown
(absent field / null body) outcome.
f"api/1/energy_sites/{self.energy_site_id}/command/authorized_clients",
)

async def get_authorized_clients(self) -> AuthorizedClients:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think this should be called find_authorized_clients

firstmate crewmate added 4 commits July 13, 2026 07:52
…hape

Narrows the typed accessor to what's actually confirmed: a precise,
non-recursive envelope unwrap (no multi-key/depth-first search) instead of
mirroring the HA config flow's speculative _iter_clients fallback search,
and the AuthorizedClient model trimmed to only public_key/state - the two
fields a pairing flow reads - since no site has a populated client list
yet to confirm any other field names against.

state is now typed via AuthorizedClientState (const.py, the schema of
record - Tesla has not published this endpoint's schema), accepting both
int and string wire forms; a present-but-unrecognized value passes through
raw rather than being dropped to None. A None body, an unrecognized
response shape, and an explicit empty list all collapse to a typed empty
list (AuthorizedClients.clients is now always a list, never None) - this
endpoint has no evidence yet that those cases mean anything different from
each other.
…essor

The auto-generated docs section described the pre-amendment API
(clients is None for unknown vs [] for empty, a description field).
Updates it to match: clients is always a list, and only public_key/state
are modeled.
…d_clients

Per PR review: this isn't a straight getter, it normalizes/parses the raw
response into the typed model, so it should use the find_ prefix already
established for lookup/discovery methods in this library (find_server,
find_vehicle).
@Bre77 Bre77 merged commit 0283459 into main Jul 12, 2026
5 checks passed
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.

1 participant