Skip to content

Commit cf3bfa9

Browse files
authored
feat(teslemetry): make authorized-clients parser public, add raw option (#152)
* 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. * no-mistakes(review): Warn on deprecated alias use, document raw param and public parser * fix(teslemetry): drop raw parameter from find_authorized_clients list_authorized_clients() already returns the raw response, so find_authorized_clients() stays typed-only with no raw option.
1 parent 506c37a commit cf3bfa9

7 files changed

Lines changed: 93 additions & 6 deletions

File tree

docs/teslemetry.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,12 @@ an unrecognized response shape raises
559559
never mistaken for "no authorized clients". The raw response is still
560560
available on `raw` for anything not modeled.
561561

562+
The parsing itself lives in the module-level
563+
`tesla_fleet_api.teslemetry.energysite.parse_authorized_clients` function,
564+
which other callers (e.g. a local/LAN client using the same envelope shape)
565+
can reuse directly to get the same `AuthorizedClients` result. For the
566+
unparsed response, use `list_authorized_clients()`.
567+
562568
`remove_authorized_client(public_key)` accepts raw DER bytes or an already
563569
base64-encoded key string. Removal requires no physical presence proof, so any
564570
paired key can revoke every other key, including the owner's.

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: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import re
66
import socket
77
import struct
8+
import warnings
89
from collections.abc import Awaitable, Callable
910
from dataclasses import dataclass
1011
from typing import Any, cast
@@ -191,9 +192,10 @@ def _authorized_clients_list(payload: Any) -> list[Any]:
191192
return cast("list[Any]", value)
192193

193194

194-
def _parse_authorized_clients(payload: Any) -> AuthorizedClients:
195-
"""Parse a raw ``list_authorized_clients()`` response into typed clients.
195+
def parse_authorized_clients(payload: Any) -> AuthorizedClients:
196+
"""The one parser for a Teslemetry/aiopowerwall authorized-clients cloud envelope.
196197
198+
Parses a raw ``list_authorized_clients()`` response into typed clients.
197199
Raises :class:`~tesla_fleet_api.exceptions.InvalidResponse` if
198200
``payload`` is null or doesn't match the confirmed envelope shape - see
199201
:func:`_authorized_clients_list`.
@@ -206,6 +208,18 @@ def _parse_authorized_clients(payload: Any) -> AuthorizedClients:
206208
return AuthorizedClients(clients=clients, raw=payload)
207209

208210

211+
def _parse_authorized_clients( # pyright: ignore[reportUnusedFunction]
212+
payload: Any,
213+
) -> AuthorizedClients:
214+
"""Deprecated alias for :func:`parse_authorized_clients`, kept for one release."""
215+
warnings.warn(
216+
"_parse_authorized_clients is deprecated; use parse_authorized_clients instead.",
217+
DeprecationWarning,
218+
stacklevel=2,
219+
)
220+
return parse_authorized_clients(payload)
221+
222+
209223
_GATEWAY_INTERFACES = ("eth", "wifi")
210224

211225
_DOTTED_QUAD_OCTET = r"(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])"
@@ -403,8 +417,11 @@ async def find_authorized_clients(self) -> AuthorizedClients:
403417
response body or an unrecognized response shape rather than
404418
treating either as "no clients". See :class:`AuthorizedClients` for
405419
the exact parsing semantics.
420+
421+
For the unparsed response, use :meth:`list_authorized_clients`.
406422
"""
407-
return _parse_authorized_clients(await self.list_authorized_clients())
423+
response = await self.list_authorized_clients()
424+
return parse_authorized_clients(response)
408425

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

tests/test_teslemetry_authorized_clients.py

Lines changed: 44 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,45 @@ 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_default_raw_false_behaviour_unchanged(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()
110+
111+
self.assertEqual(len(result.clients), 1)
112+
self.assertEqual(result.clients[0].public_key, PUBLIC_KEY_B64)
113+
70114
async def test_normal_payload_round_trips(self) -> None:
71115
site = _make_site(
72116
{

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)