Skip to content

Commit 19a4a13

Browse files
committed
no-mistakes(document): Fix AGENTS.md unwrapped-fields list; format new BLE reader test file
1 parent 3c2d8a6 commit 19a4a13

2 files changed

Lines changed: 2 additions & 4 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ Keep the `tesla-protocol` floor at `>=1.4.0`; earlier releases have generated `.
151151
- **`_log_request_result` (`fleet.py`) must tolerate any JSON-legal REST body, not just dicts**: it runs after the HTTP request already succeeded, so it's a logging convenience only — a non-dict body (`null`, a list, a bare scalar) must never raise there. It guards with `isinstance(data, dict)` before calling `.get()`, logging `result=success` and returning for anything else. Regression tests in `tests/test_command_logging.py`.
152152
- **Typed accessor pattern for undocumented raw-dict responses**: `TeslemetryEnergySite.find_authorized_clients()` and `find_gateway_address()` (`teslemetry/energysite.py`) are frozen-dataclass typed wrappers over raw `dict[str, Any]`/`None`/`list` REST responses, so API-parsing logic (envelope unwrap, field lookup, shape validation, enum typing) lives in the library instead of each consumer reimplementing it. Any future typed accessor over an undocumented response shape should keep two rules: (1) field lookup must check key presence (`key in payload`), never `payload.get(key) or default` — a legal falsy value is not "missing"; (2) a `None` body and an unrecognized response shape are malformed data, not "empty" — raise `InvalidResponse` (`exceptions.py`) rather than collapsing to an empty/default result; only a genuinely well-formed-but-empty response should parse to an empty result without raising. `find_authorized_clients()`'s envelope unwrap accepts `{"response": {"authorized_clients": [...]}}` or `{"response": {"clients": [...]}}`, or a bare list. `find_gateway_address()` decodes `networking_status.ipv4_config.address` as either a raw big-endian uint32 (`struct.pack(">I", ...)`, not little-endian) or a dotted-quad string — the API has been observed serving both forms — considers only `eth`/`wifi` (never `gsm`), preferring whichever has `active_route` set and a decodable address; `0`/`0xFFFFFFFF` (and their string equivalents) are treated as undecodable. Tesla has not published an OpenAPI schema for these endpoints, so `const.py`'s enums are the schema of record; widen modeled fields only against a further live sample, not speculatively. Untyped escape-hatch methods (e.g. `list_authorized_clients()`) remain available alongside. Tests: `tests/test_teslemetry_authorized_clients.py`, `tests/test_teslemetry_gateway_address.py`.
153153
- **`_stream_sinks` peels subscription pushes off the command-reply queue before routing**: a `vehicleDataSubscription`'s pushes arrive addressed to us on the same domain queue (`_queues`) an ordinary command's reply uses, correlated by the subscribe request's own `request_uuid`. `_on_message` (`bluetooth.py`) checks `self._stream_sinks.get(msg.request_uuid)` before touching `_queues` — a match routes into that subscription's own bounded, drop-oldest `_StreamSink` instead, so `_send`'s pre-send drain can never discard a push and `_await_response` can never return one as an unrelated command's reply. `_register_stream_sink`/`_unregister_stream_sink` are the only entry points into the registry; there is no public subscription API yet. Tests: `tests/test_ble_stream_sink.py`.
154-
- **`VehicleAction`/`GetVehicleData` proto coverage is locked by test, not just by convention**: `tests/test_proto_coverage_lock.py` walks both descriptors and fails if any field has no wrapper (`commands.py`) or reader (`bluetooth.py`) and isn't on one of its two small, reasoned allowlists — keep that test in sync with any future `tesla-protocol` bump rather than special-casing new fields elsewhere. The only fields deliberately left unwrapped today are the 7-field push-style subscription/streaming family (`createStreamSession`/`streamMessage`/`vehicleDataSubscription`/`vehicleDataAck`/`vitalsSubscription`/`vitalsAck`/`cancelVehicleDataSubscription`, which need a public lifecycle/iterator API atop the private `_stream_sinks` routing above) and `getVehicleImageState` (needs chunked binary-transfer paging). CarServer's `GetVehicleState` sub-state is exposed as `legacy_vehicle_state()` (`bluetooth.py`), matching the `VehicleData.legacy_vehicle_state` reply field name, to avoid confusion with `vehicle_state()` (VCSEC `VehicleStatus`, a different message/domain). `set_rate_tariff`/`add_managed_charging_site` (`commands.py`) take `tesla_protocol` message types directly for their deeply-nested arguments rather than a parallel flattened dataclass API.
154+
- **`VehicleAction`/`GetVehicleData` proto coverage is locked by test, not just by convention**: `tests/test_proto_coverage_lock.py` walks both descriptors and fails if any field has no wrapper (`commands.py`) or reader (`bluetooth.py`) and isn't on one of its two small, reasoned allowlists — keep that test in sync with any future `tesla-protocol` bump rather than special-casing new fields elsewhere. The only fields deliberately left unwrapped today are the 7-field push-style subscription/streaming family (`createStreamSession`/`streamMessage`/`vehicleDataSubscription`/`vehicleDataAck`/`vitalsSubscription`/`vitalsAck`/`cancelVehicleDataSubscription`, which need a public lifecycle/iterator API atop the private `_stream_sinks` routing above) and `getLegacyVehicleState` (ambiguous versus the pre-existing `getVehicleState`/`legacy_vehicle_state()` reader, pending live verification of how the two differ before wrapping a second method for what may be the same reply data). `getVehicleImageState` is wrapped as `vehicle_image_state()` (`commands.py`), which pages through chunked binary transfers. CarServer's `GetVehicleState` sub-state is exposed as `legacy_vehicle_state()` (`bluetooth.py`), matching the `VehicleData.legacy_vehicle_state` reply field name, to avoid confusion with `vehicle_state()` (VCSEC `VehicleStatus`, a different message/domain). `set_rate_tariff`/`add_managed_charging_site` (`commands.py`) take `tesla_protocol` message types directly for their deeply-nested arguments rather than a parallel flattened dataclass API.
155155
- **Energy-gateway authorized-client pairing has security- and protocol-specific constraints**: use RSA for LAN TEDapi v1r, treat `PENDING_VERIFICATION_TIMEOUT` as terminal, and account for presence-free key removal. The authoritative pairing, retry, encoding, and removal guidance is in `docs/energy_local_control.md`; enum values and API contracts live in `const.py` and the relevant method docstrings.
156156
- **`register_client()` (`teslemetry/teslemetry.py`) is Teslemetry-only OAuth Dynamic Client Registration (RFC 7591)**: a module-level function, not a `Teslemetry` instance method, since registration precedes having a `client_id` or access token — callers pass a bare `aiohttp.ClientSession`. It always registers a new client (no dedup/caching) and raises `TeslemetryRegistrationError` (`exceptions.py`) on transport failure, a non-2xx response, a non-JSON body, or a response missing a usable `client_id`; a non-dict-but-valid-JSON body (list/scalar) is treated as the same malformed-response error rather than raising an uncaught `AttributeError`. Fleet API and Tessie have no equivalent — don't add one speculatively. See `docs/teslemetry.md`'s "OAuth Dynamic Client Registration" section and `tests/test_teslemetry_register_client.py`.
157157
- **`False`, not `None`, is the "signing is disabled" value for `Commands.__init__`'s `private_key` (and `VehicleBluetooth.__init__`/`Vehicles.createBluetooth`/`VehiclesBluetooth.create`/`createBluetooth`'s `key`)**: `None` — the default and an explicit `None` — keeps its long-standing meaning of falling back to the parent's key, raising `ValueError("No private key.")` if it has none; `False` disables signing for a passive BLE listener that only observes broadcasts. `None` is deliberately *not* the opt-out: a caller already passing `private_key=None` to mean "I haven't got one" must keep getting that `ValueError`, not a silently unsignable vehicle. Because `False` and `None` are both falsy, every branch on this argument must test **identity** (`is False`/`is not None`) — a truthiness check (`if private_key:`) collapses the two states and reintroduces the bug. `self.private_key` is `EllipticCurvePrivateKey | None`, its `None` meaning signing-disabled — `_handshake` (reached by `_command`, i.e. every signed command, and by `_ensure_handshake`, used by signed reads) raises `SigningDisabled` (`exceptions.py`) up front rather than failing deep in the signing/crypto path. `pair()`'s fast path never calls `_handshake` (it builds and sends its own whitelist request directly), so it carries its own identical guard at the top instead — `_handshake` is not a single choke point every signed-session entry point routes through; each entry point that doesn't call it needs its own `self.private_key is None` check. Tests: `tests/test_ble_null_key.py`.

tests/test_ble_mocked_state_readers_new.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,7 @@ async def test_soh_state(self) -> None:
129129
vehicle, send = self.make_vehicle()
130130
send.return_value = infotainment_vehicle_data_reply(
131131
VehicleData(
132-
soh_state=SohState(
133-
soh_result=SohState.SohResult(soh_calibrated=True)
134-
)
132+
soh_state=SohState(soh_result=SohState.SohResult(soh_calibrated=True))
135133
)
136134
)
137135
result = await vehicle.soh_state()

0 commit comments

Comments
 (0)