Skip to content

fix(teslemetry): accept clients key in authorized-clients response#80

Merged
Bre77 merged 2 commits into
mainfrom
fm/tfa-clients-key-fix-d6
Jul 14, 2026
Merged

fix(teslemetry): accept clients key in authorized-clients response#80
Bre77 merged 2 commits into
mainfrom
fm/tfa-clients-key-fix-d6

Conversation

@Bre77

@Bre77 Bre77 commented Jul 14, 2026

Copy link
Copy Markdown
Member

Intent

Fix TeslemetryEnergySite.find_authorized_clients() to accept Tesla's real response key: the authorized-clients list arrives under 'clients', not 'authorized_clients' as originally documented in PR #76/#79. Evidence: a live capture (2026-07-14) from a Powerwall 3 site showed Tesla's Release-953 endpoint returning a fully-populated 5-entry list under the 'clients' key, which the existing parser didn't recognize, causing it to raise/collapse to empty on a perfectly valid response. Fix: _authorized_clients_list() now checks both 'authorized_clients' and 'clients' key names (using the existing key-presence-check _field() helper, symmetric to the public_key/publicKey handling), preserving all PR #79 behavior (null body and unrecognized shapes still raise InvalidResponse; empty list under either key still parses to []). Added regression tests covering the real captured 5-client sample (structure verbatim, identifiers anonymized to synthetic values) plus both key variants and the empty-list case. This unblocks the Home Assistant energy local-control config flow (core PR #176328 is pinned waiting on this fix), so a library release (version bump + git tag per this repo's release process in AGENTS.md) is needed promptly after merge.

What Changed

  • TeslemetryEnergySite._authorized_clients_list() now unwraps the authorized-clients envelope under either authorized_clients or clients - a live capture from Tesla's Release-953 endpoint showed the real response uses the clients key, which the previous parser rejected with InvalidResponse on a valid populated response. All existing strictness is preserved: null bodies and unrecognized shapes still raise InvalidResponse, and an empty list under either key still parses to clients == [].
  • Added regression tests in tests/test_teslemetry_authorized_clients.py covering the captured 5-client Release-953 payload (identifiers anonymized), both key variants, and the empty-list case; full suite passes (347 tests) and an E2E before/after demo confirmed the base commit raises while this branch parses all 5 clients.
  • Updated docs/teslemetry.md and the AGENTS.md entry to document both accepted response keys (the docs previously described only the authorized_clients envelope shape).

Risk Assessment

✅ Low: A one-line parser fix using an existing presence-checking helper, backed by a live-captured sample, comprehensive regression tests covering both key variants and all prior invariants (null body, unrecognized shape, empty list), with docs and AGENTS.md updated to match.

Testing

Ran the new regression tests and full suite (347 passed), then demonstrated the fix end-to-end: the same real-world Release-953 response shape (clients list under the clients key) that made the base commit raise InvalidResponse now parses into 5 typed AuthorizedClient entries through the full HTTP request/parse path; null-body and unrecognized-shape responses still raise as before. No UI surface is involved (async library), so evidence is a CLI transcript.

Evidence: Before/after CLI transcript (base rejects, fix parses 5 clients)

[BASE 704281f] GET .../command/authorized_clients -> Release-953 body (list under 'clients' key, 5 entries) [BASE 704281f] find_authorized_clients() RAISED InvalidResponse: valid populated response rejected [FIXED efbd4f6] GET .../command/authorized_clients -> Release-953 body (list under 'clients' key, 5 entries) [FIXED efbd4f6] find_authorized_clients() parsed 5 clients: public_key=SYNTHETIC_PUBLIC_KEY_1 state=VERIFIED public_key=SYNTHETIC_PUBLIC_KEY_2 state=PENDING_VERIFICATION public_key=SYNTHETIC_PUBLIC_KEY_3 state=PENDING_VERIFICATION public_key=SYNTHETIC_PUBLIC_KEY_4 state=VERIFIED public_key=SYNTHETIC_PUBLIC_KEY_5 state=PENDING_VERIFICATION

[BASE 704281f] GET .../command/authorized_clients -> Release-953 body (list under 'clients' key, 5 entries)
[BASE 704281f] find_authorized_clients() RAISED InvalidResponse: valid populated response rejected

[FIXED efbd4f6] GET .../command/authorized_clients -> Release-953 body (list under 'clients' key, 5 entries)
[FIXED efbd4f6] find_authorized_clients() parsed 5 clients:
    public_key=SYNTHETIC_PUBLIC_KEY_1  state=VERIFIED
    public_key=SYNTHETIC_PUBLIC_KEY_2  state=PENDING_VERIFICATION
    public_key=SYNTHETIC_PUBLIC_KEY_3  state=PENDING_VERIFICATION
    public_key=SYNTHETIC_PUBLIC_KEY_4  state=VERIFIED
    public_key=SYNTHETIC_PUBLIC_KEY_5  state=PENDING_VERIFICATION
Evidence: E2E demo script (mocked-session Teslemetry client, real parse path)
"""E2E demo: parse Tesla's real Release-953 authorized-clients response shape.

Feeds a payload shaped exactly like the live 2026-07-14 Powerwall 3 capture
(list under the ``clients`` key, 5 fully-populated entries; identifiers
synthetic) through Teslemetry.energySites -> find_authorized_clients(), with
only the aiohttp session mocked - the full HTTP request/response and parsing
path runs for real.
"""

import asyncio
import sys
from contextlib import asynccontextmanager
from unittest.mock import AsyncMock, MagicMock

from tesla_fleet_api.teslemetry.teslemetry import Teslemetry
from tesla_fleet_api.exceptions import TeslaFleetError

# Shape verbatim from the live capture (Tesla Release 953, Powerwall 3 site);
# keys/public-key values anonymized.
RELEASE_953_PAYLOAD = {
    "response": {
        "clients": [
            {"type": 1, "description": "Teslemetry.com", "key_type": 1,
             "public_key": "SYNTHETIC_PUBLIC_KEY_1", "roles": [1], "state": 3,
             "verification": 1, "added_time": {"seconds": 1777328846}},
            {"type": 1, "description": "PowerSync Cloud", "key_type": 1,
             "public_key": "SYNTHETIC_PUBLIC_KEY_2", "roles": [1], "state": 2,
             "verification": 1, "added_time": {"seconds": 1777288515}},
            {"type": 1, "description": "Powerwall V1R", "key_type": 1,
             "public_key": "SYNTHETIC_PUBLIC_KEY_3", "roles": [1], "state": 2,
             "verification": 1, "added_time": {"seconds": 1778476550}},
            {"type": 1, "description": "Pixel 10 Pro", "key_type": 1,
             "public_key": "SYNTHETIC_PUBLIC_KEY_4", "roles": [1], "state": 3,
             "verification": 1, "added_time": {"seconds": 1780101174}},
            {"type": 1, "description": "Home Assistant", "key_type": 1,
             "public_key": "SYNTHETIC_PUBLIC_KEY_5", "roles": [1], "state": 2,
             "verification": 1, "added_time": {"seconds": 1783984580}},
        ]
    }
}


def make_session(json_body):
    resp = MagicMock()
    resp.status = 200
    resp.ok = True
    resp.content_type = "application/json"
    resp.url = "https://api.teslemetry.com/api/1/energy_sites/12345/command/authorized_clients"
    resp.headers = {}
    resp.json = AsyncMock(return_value=json_body)
    resp.text = AsyncMock(return_value="")

    session = MagicMock()

    @asynccontextmanager
    async def ctx(*args, **kwargs):
        yield resp

    session.request = MagicMock(side_effect=lambda *a, **k: ctx(*a, **k))
    return session


async def main():
    label = sys.argv[1] if len(sys.argv) > 1 else "?"
    api = Teslemetry(session=make_session(RELEASE_953_PAYLOAD), access_token="token")
    site = api.energySites.create(12345)
    print(f"[{label}] GET .../command/authorized_clients -> Release-953 body "
          f"(list under 'clients' key, 5 entries)")
    try:
        result = await site.find_authorized_clients()
    except TeslaFleetError as err:
        print(f"[{label}] find_authorized_clients() RAISED "
              f"{type(err).__name__}: valid populated response rejected")
        return 1
    print(f"[{label}] find_authorized_clients() parsed "
          f"{len(result.clients)} clients:")
    for c in result.clients:
        state = c.state.name if hasattr(c.state, "name") else c.state
        print(f"    public_key={c.public_key}  state={state}")
    return 0


if __name__ == "__main__":
    sys.exit(asyncio.run(main()))

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ docs/teslemetry.md:551 - User-facing docs are stale after the parser change: docs/teslemetry.md still says the typed helper "only unwraps the one envelope shape" and that "Only an explicitly empty authorized_clients list parses to clients == []", but _authorized_clients_list() now also accepts the clients key (which is what Tesla's live Release-953 endpoint actually returns). Update the paragraph to mention both accepted keys, mirroring the AGENTS.md entry that was updated in this commit.

🔧 Fix: document both accepted authorized-clients response keys
1 info still open:

  • ℹ️ tesla_fleet_api.egg-info/PKG-INFO:140 - The docs-only commit efbd4f6 also commits regenerated tesla_fleet_api.egg-info/ build artifacts (PKG-INFO, SOURCES.txt) whose content changes come from earlier unrelated commits (BLE broadcast listeners, router docs). These files are auto-generated by setuptools; they were already tracked at the base commit so this matches existing repo practice, but the unrelated churn inflates the diff. Consider untracking/gitignoring egg-info in a follow-up.
✅ **Test** - passed

✅ No issues found.

  • uv run pytest tests/test_teslemetry_authorized_clients.py -v (16 tests incl. new ClientsKeyVariantTests with the anonymized 5-client Release-953 capture)
  • uv run pytest tests (full suite, 347 passed)
  • E2E before/after demo: fed the Release-953-shaped payload (list under clients, 5 entries) through a real Teslemetry client with only the aiohttp session mocked, via energySites.create(12345).find_authorized_clients() - base commit 704281f raises InvalidResponse, target efbd4f6 parses 5 clients with VERIFIED/PENDING_VERIFICATION states (demo_clients_key.py run against both trees)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

firstmate crewmate added 2 commits July 14, 2026 13:32
Tesla's live endpoint (Release 953) carries the authorized-client list
under `clients`, not `authorized_clients` as originally documented -
confirmed via a live capture of a Powerwall 3 site with five populated
entries. `_authorized_clients_list()` now checks both key variants,
symmetric to the existing public_key/publicKey handling, so a
perfectly valid response no longer parses to an empty/failed result.
@Bre77 Bre77 merged commit 2718d46 into main Jul 14, 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