Skip to content

Commit a000b24

Browse files
committed
feat(teslemetry): make authorized-clients parser public, add raw option
Rename _parse_authorized_clients to parse_authorized_clients and export it alongside AuthorizedClients/AuthorizedClient and the related enums from tesla_fleet_api.teslemetry, so a local aiopowerwall reader can reuse the same parser instead of duplicating it. find_authorized_clients() gains raw=False to return the same unparsed dict as list_authorized_clients() for callers that want the raw shape. The private name stays as an alias for one release.
1 parent 506c37a commit a000b24

6 files changed

Lines changed: 107 additions & 8 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ requires = ["setuptools>=77.0"]
44

55
[project]
66
name = "tesla_fleet_api"
7-
version = "1.13.0"
7+
version = "1.14.0"
88
license = "Apache-2.0"
99
description = "Tesla Fleet API library for Python"
1010
readme = "README.md"

tesla_fleet_api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tesla Fleet API"""
22

33
__author__ = "hello@teslemetry.com"
4-
__version__ = "1.13.0"
4+
__version__ = "1.14.0"
55

66
from tesla_fleet_api.const import Region, is_valid_region
77
from tesla_fleet_api.funnel import (

tesla_fleet_api/teslemetry/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1+
from tesla_fleet_api.const import (
2+
AuthorizationRole,
3+
AuthorizedClientKeyType,
4+
AuthorizedClientState,
5+
AuthorizedClientType,
6+
AuthorizedVerificationType,
7+
)
18
from tesla_fleet_api.tesla.charging import Charging
29
from tesla_fleet_api.tesla.energysite import EnergySite, EnergySites
310
from tesla_fleet_api.tesla.user import User
11+
from tesla_fleet_api.teslemetry.energysite import (
12+
AuthorizedClient,
13+
AuthorizedClients,
14+
parse_authorized_clients,
15+
)
416
from tesla_fleet_api.teslemetry.teslemetry import (
517
Teslemetry,
618
TeslemetryClientRegistration,
@@ -13,6 +25,14 @@
1325
"Teslemetry",
1426
"TeslemetryClientRegistration",
1527
"register_client",
28+
"AuthorizationRole",
29+
"AuthorizedClient",
30+
"AuthorizedClientKeyType",
31+
"AuthorizedClients",
32+
"AuthorizedClientState",
33+
"AuthorizedClientType",
34+
"AuthorizedVerificationType",
35+
"parse_authorized_clients",
1636
"Charging",
1737
"EnergySites",
1838
"EnergySite",

tesla_fleet_api/teslemetry/energysite.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import struct
88
from collections.abc import Awaitable, Callable
99
from dataclasses import dataclass
10-
from typing import Any, cast
10+
from typing import Any, Literal, overload, cast
1111

1212
from tesla_fleet_api.const import (
1313
AuthorizationRole,
@@ -191,9 +191,10 @@ def _authorized_clients_list(payload: Any) -> list[Any]:
191191
return cast("list[Any]", value)
192192

193193

194-
def _parse_authorized_clients(payload: Any) -> AuthorizedClients:
195-
"""Parse a raw ``list_authorized_clients()`` response into typed clients.
194+
def parse_authorized_clients(payload: Any) -> AuthorizedClients:
195+
"""The one parser for a Teslemetry/aiopowerwall authorized-clients cloud envelope.
196196
197+
Parses a raw ``list_authorized_clients()`` response into typed clients.
197198
Raises :class:`~tesla_fleet_api.exceptions.InvalidResponse` if
198199
``payload`` is null or doesn't match the confirmed envelope shape - see
199200
:func:`_authorized_clients_list`.
@@ -206,6 +207,10 @@ def _parse_authorized_clients(payload: Any) -> AuthorizedClients:
206207
return AuthorizedClients(clients=clients, raw=payload)
207208

208209

210+
# Deprecated alias, kept for one release.
211+
_parse_authorized_clients = parse_authorized_clients
212+
213+
209214
_GATEWAY_INTERFACES = ("eth", "wifi")
210215

211216
_DOTTED_QUAD_OCTET = r"(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])"
@@ -392,7 +397,15 @@ async def list_authorized_clients(self) -> dict[str, Any]:
392397
f"api/1/energy_sites/{self.energy_site_id}/command/authorized_clients",
393398
)
394399

395-
async def find_authorized_clients(self) -> AuthorizedClients:
400+
@overload
401+
async def find_authorized_clients(
402+
self, raw: Literal[False] = False
403+
) -> AuthorizedClients: ...
404+
@overload
405+
async def find_authorized_clients(self, raw: Literal[True]) -> dict[str, Any]: ...
406+
async def find_authorized_clients(
407+
self, raw: bool = False
408+
) -> AuthorizedClients | dict[str, Any]:
396409
"""List authorized clients on the energy gateway, parsed into a typed result.
397410
398411
Prefer this over :meth:`list_authorized_clients` for consumers that
@@ -403,8 +416,16 @@ async def find_authorized_clients(self) -> AuthorizedClients:
403416
response body or an unrecognized response shape rather than
404417
treating either as "no clients". See :class:`AuthorizedClients` for
405418
the exact parsing semantics.
419+
420+
``raw=True`` returns the unparsed response exactly as
421+
:meth:`list_authorized_clients` does, skipping the typed parse - for
422+
callers that want the same call shape aligned with the local
423+
gateway path while still opting out of typing.
406424
"""
407-
return _parse_authorized_clients(await self.list_authorized_clients())
425+
response = await self.list_authorized_clients()
426+
if raw:
427+
return response
428+
return parse_authorized_clients(response)
408429

409430
async def remove_authorized_client(self, public_key: bytes | str) -> dict[str, Any]:
410431
"""Remove an authorized client from the energy gateway via the

tests/test_teslemetry_authorized_clients.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
AuthorizedVerificationType,
2929
)
3030
from tesla_fleet_api.exceptions import InvalidResponse
31+
from tesla_fleet_api.teslemetry import (
32+
AuthorizedClient,
33+
AuthorizedClients,
34+
parse_authorized_clients,
35+
)
36+
from tesla_fleet_api.teslemetry.energysite import _parse_authorized_clients
3137
from tesla_fleet_api.teslemetry.teslemetry import Teslemetry
3238

3339
_UNSET = object()
@@ -66,7 +72,59 @@ def _make_site(json_body: object):
6672
return api.energySites.create(12345)
6773

6874

75+
class PublicParserImportTests(IsolatedAsyncioTestCase):
76+
async def test_parser_is_importable_from_public_teslemetry_path(self) -> None:
77+
payload = {
78+
"response": {
79+
"authorized_clients": [
80+
{"public_key": PUBLIC_KEY_B64, "state": 3},
81+
]
82+
}
83+
}
84+
85+
result = parse_authorized_clients(payload)
86+
87+
self.assertIsInstance(result, AuthorizedClients)
88+
self.assertIsInstance(result.clients[0], AuthorizedClient)
89+
90+
async def test_private_alias_still_works_for_one_release(self) -> None:
91+
payload = {"response": {"authorized_clients": []}}
92+
93+
self.assertEqual(
94+
_parse_authorized_clients(payload), parse_authorized_clients(payload)
95+
)
96+
97+
6998
class GetAuthorizedClientsTests(IsolatedAsyncioTestCase):
99+
async def test_raw_true_returns_unparsed_response(self) -> None:
100+
payload = {
101+
"response": {
102+
"authorized_clients": [
103+
{"public_key": PUBLIC_KEY_B64, "state": 3},
104+
]
105+
}
106+
}
107+
site = _make_site(payload)
108+
109+
result = await site.find_authorized_clients(raw=True)
110+
111+
self.assertEqual(result, payload)
112+
113+
async def test_default_raw_false_behaviour_unchanged(self) -> None:
114+
payload = {
115+
"response": {
116+
"authorized_clients": [
117+
{"public_key": PUBLIC_KEY_B64, "state": 3},
118+
]
119+
}
120+
}
121+
site = _make_site(payload)
122+
123+
result = await site.find_authorized_clients()
124+
125+
self.assertEqual(len(result.clients), 1)
126+
self.assertEqual(result.clients[0].public_key, PUBLIC_KEY_B64)
127+
70128
async def test_normal_payload_round_trips(self) -> None:
71129
site = _make_site(
72130
{

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)