tesla-fleet-api 1.0.11__py3-none-any.whl → 1.0.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tesla_fleet_api/const.py CHANGED
@@ -3,7 +3,7 @@
3
3
  from enum import Enum
4
4
  import logging
5
5
 
6
- VERSION = "1.0.11"
6
+ VERSION = "1.0.12"
7
7
  LOGGER = logging.getLogger(__package__)
8
8
  SERVERS = {
9
9
  "na": "https://fleet-api.prd.na.vn.cloud.tesla.com",
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  import asyncio
5
- from typing import TYPE_CHECKING
5
+ import struct
6
+ from typing import TYPE_CHECKING, Callable, Any
6
7
  from google.protobuf.message import DecodeError
7
8
  from bleak_retry_connector import establish_connection, MAX_CONNECT_ATTEMPTS
8
9
  from bleak import BleakClient, BleakScanner
@@ -75,6 +76,70 @@ def prependLength(message: bytes) -> bytearray:
75
76
  """Prepend a 2-byte length to the payload."""
76
77
  return bytearray([len(message) >> 8, len(message) & 0xFF]) + message
77
78
 
79
+ class ReassemblingBuffer:
80
+ """
81
+ Reassembles bytearray streams where the first two bytes indicate the length of the message.
82
+ Handles potential packet corruption by discarding *entire* packets and retrying.
83
+ Uses a callback to process parsed messages.
84
+ """
85
+
86
+ def __init__(self, callback: Callable[[RoutableMessage], None]):
87
+ """
88
+ Initializes the buffer.
89
+
90
+ Args:
91
+ message_type: The protobuf message type (e.g., RoutableMessage) to parse the assembled data.
92
+ callback: A function that will be called with each parsed message.
93
+ """
94
+ self.buffer = bytearray()
95
+ self.expected_length = None
96
+ self.packet_starts = []
97
+ self.callback = callback
98
+
99
+ def receive_data(self, data: bytearray):
100
+ """
101
+ Receives a chunk of bytearray data and attempts to assemble a complete message.
102
+
103
+ Args:
104
+ data: The received bytearray data.
105
+ """
106
+ self.packet_starts.append(len(self.buffer))
107
+ self.buffer.extend(data)
108
+
109
+ while True:
110
+ if self.expected_length is None and len(self.buffer) >= 2:
111
+ self.expected_length = struct.unpack(">H", self.buffer[:2])[0] + 2
112
+
113
+ LOGGER.info(f"Buffer length: {len(self.buffer)}, Packet starts: {self.packet_starts}, Expected length: {self.expected_length}")
114
+
115
+ if self.expected_length is not None and self.expected_length > 1024:
116
+ LOGGER.warning(f"Expected length too large: {self.expected_length}")
117
+ self.discard_packet()
118
+
119
+ elif self.expected_length is not None and len(self.buffer) >= self.expected_length:
120
+ try:
121
+ message = RoutableMessage()
122
+ message.ParseFromString(bytes(self.buffer[2:self.expected_length]))
123
+ self.buffer = self.buffer[self.expected_length:]
124
+ self.packet_starts = [x - self.expected_length for x in self.packet_starts if x >= self.expected_length]
125
+ self.expected_length = None
126
+ self.callback(message) # Call the callback with the parsed message
127
+
128
+ except DecodeError:
129
+ self.discard_packet()
130
+ else:
131
+ return
132
+
133
+ def discard_packet(self):
134
+ self.packet_starts.pop(0)
135
+ if len(self.packet_starts) > 0:
136
+ self.buffer = self.buffer[self.packet_starts[0]:]
137
+ self.packet_starts = [x - self.packet_starts[0] for x in self.packet_starts]
138
+ else:
139
+ self.buffer = bytearray()
140
+ self.packet_starts = []
141
+ self.expected_length = None
142
+
78
143
  class VehicleBluetooth(Commands):
79
144
  """Class describing the Tesla Fleet API vehicle endpoints and commands for a specific vehicle with command signing."""
80
145
 
@@ -83,8 +148,7 @@ class VehicleBluetooth(Commands):
83
148
  client: BleakClient | None = None
84
149
  _queues: dict[Domain, asyncio.Queue]
85
150
  _ekey: ec.EllipticCurvePublicKey
86
- _recv: bytearray = bytearray()
87
- _recv_len: int = 0
151
+ _buffer: ReassemblingBuffer
88
152
  _auth_method = "aes"
89
153
 
90
154
  def __init__(
@@ -98,6 +162,7 @@ class VehicleBluetooth(Commands):
98
162
  }
99
163
  self.device = device
100
164
  self._connect_lock = asyncio.Lock()
165
+ self._buffer = ReassemblingBuffer(self._on_message)
101
166
 
102
167
  async def find_vehicle(self, name: str | None = None, address: str | None = None, scanner: BleakScanner | None = None) -> BLEDevice:
103
168
  """Find the Tesla BLE device."""
@@ -146,6 +211,7 @@ class VehicleBluetooth(Commands):
146
211
  """Connect to the Tesla BLE device if not already connected."""
147
212
  async with self._connect_lock:
148
213
  if not self.client or not self.client.is_connected:
214
+ LOGGER.info(f"Reconnecting to {self.ble_name}")
149
215
  await self.connect(max_attempts=max_attempts)
150
216
 
151
217
  async def __aenter__(self) -> VehicleBluetooth:
@@ -157,63 +223,36 @@ class VehicleBluetooth(Commands):
157
223
  """Exit the async context."""
158
224
  await self.disconnect()
159
225
 
160
- async def _on_notify(self,sender: BleakGATTCharacteristic,data : bytearray) -> None:
226
+ def _on_notify(self,sender: BleakGATTCharacteristic, data: bytearray) -> None:
161
227
  """Receive data from the Tesla BLE device."""
162
- if self._recv_len:
163
- self._recv += data
164
- else:
165
- self._recv_len = int.from_bytes(data[:2], 'big')
166
- if self._recv_len > 1024:
167
- LOGGER.error("Parsed very large message length")
168
- self._recv = bytearray()
169
- self._recv_len = 0
170
- return
171
- self._recv = data[2:]
172
- #while len(self._recv) > self._recv_len:
173
- #
174
- # # Maybe this needs to trigger a reset
175
- # await self._on_message(bytes(self._recv[:self._recv_len]))
176
- # self._recv_len = int.from_bytes(self._recv[self._recv_len:self._recv_len+2], 'big')
177
- # self._recv = self._recv[self._recv_len+2:]
178
- # continue
179
- if len(self._recv) >= self._recv_len:
180
- if len(self._recv) > self._recv_len:
181
- LOGGER.debug(f"Received more data than expected: {len(self._recv)} > {self._recv_len}")
182
- try:
183
- msg = RoutableMessage.FromString(bytes(self._recv[:self._recv_len]))
184
- await self._on_message(msg)
185
- self._recv = bytearray()
186
- self._recv_len = 0
187
- except DecodeError:
188
- # Attempt parsing the whole payload
189
- msg = RoutableMessage.FromString(bytes(self._recv))
190
- LOGGER.warn(f"Parsed more data than length: {len(self._recv)} > {self._recv_len}")
191
- await self._on_message(msg)
192
- self._recv = bytearray()
193
- self._recv_len = 0
194
-
195
- async def _on_message(self, msg: RoutableMessage) -> None:
228
+ if sender.uuid != READ_UUID:
229
+ LOGGER.error(f"Unexpected sender: {sender}")
230
+ return
231
+ self._buffer.receive_data(data)
232
+
233
+ def _on_message(self, msg: RoutableMessage) -> None:
196
234
  """Receive messages from the Tesla BLE data."""
197
235
 
198
236
  if(msg.to_destination.routing_address != self._from_destination):
199
237
  # Ignore ephemeral key broadcasts
200
238
  return
201
239
 
202
- LOGGER.debug(f"Received response: {msg}")
203
- await self._queues[msg.from_destination.domain].put(msg)
240
+ LOGGER.info(f"Received response: {msg}")
241
+ self._queues[msg.from_destination.domain].put_nowait(msg)
204
242
 
205
- async def _send(self, msg: RoutableMessage, requires: str, timeout: int = 2) -> RoutableMessage:
243
+ async def _send(self, msg: RoutableMessage, requires: str, timeout: int = 5) -> RoutableMessage:
206
244
  """Serialize a message and send to the vehicle and wait for a response."""
207
245
 
208
246
  domain = msg.to_destination.domain
209
247
  async with self._sessions[domain].lock:
210
- LOGGER.debug(f"Sending message {msg}")
248
+ LOGGER.info(f"Sending message {msg}")
211
249
 
212
250
  payload = prependLength(msg.SerializeToString())
213
251
 
214
252
  # Empty the queue before sending the message
215
253
  while not self._queues[domain].empty():
216
- await self._queues[domain].get()
254
+ msg = await self._queues[domain].get()
255
+ LOGGER.warning(f"Discarded message {msg}")
217
256
 
218
257
  await self.connect_if_needed()
219
258
  assert self.client is not None
@@ -221,6 +260,7 @@ class VehicleBluetooth(Commands):
221
260
 
222
261
  # Process the response
223
262
  async with asyncio.timeout(timeout):
263
+ LOGGER.info(f"Waiting for response with {requires}")
224
264
  while True:
225
265
  resp = await self._queues[domain].get()
226
266
  LOGGER.debug(f"Received message {resp}")
@@ -229,6 +269,8 @@ class VehicleBluetooth(Commands):
229
269
 
230
270
  if resp.HasField(requires):
231
271
  return resp
272
+ else:
273
+ LOGGER.warning(f"Ignoring message since it does not contain the required field {requires}, {resp.HasField(requires)}")
232
274
 
233
275
  async def query_display_name(self, max_attempts=5) -> str | None:
234
276
  """Read the device name via GATT characteristic if available"""
@@ -270,6 +312,11 @@ class VehicleBluetooth(Commands):
270
312
  LOGGER.error(f"Failed to read device version: {e}")
271
313
  return None
272
314
 
315
+ async def _command(self, domain: Domain, command: bytes, attempt: int = 0) -> dict[str, Any]:
316
+ """Serialize a message and send to the signed command endpoint."""
317
+ await self.connect_if_needed()
318
+ return await super()._command(domain, command, attempt)
319
+
273
320
  async def pair(self, role: Role = Role.ROLE_OWNER, form: KeyFormFactor = KeyFormFactor.KEY_FORM_FACTOR_CLOUD_KEY, timeout: int = 60):
274
321
  """Pair the key."""
275
322
 
@@ -269,7 +269,6 @@ class Commands(Vehicle):
269
269
  if msg.signedMessageStatus.signed_message_fault > 0:
270
270
  raise MESSAGE_FAULTS[msg.signedMessageStatus.signed_message_fault]
271
271
 
272
- @abstractmethod
273
272
  async def _command(self, domain: Domain, command: bytes, attempt: int = 0) -> dict[str, Any]:
274
273
  """Serialize a message and send to the signed command endpoint."""
275
274
  session = self._sessions[domain]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tesla_fleet_api
3
- Version: 1.0.11
3
+ Version: 1.0.12
4
4
  Summary: Tesla Fleet API library for Python
5
5
  Home-page: https://github.com/Teslemetry/python-tesla-fleet-api
6
6
  Author: Brett Adams
@@ -1,5 +1,5 @@
1
1
  tesla_fleet_api/__init__.py,sha256=3DZMoZ-5srW-7SooAjqcRubQDuZPY8rMKH7eqIp4qtg,392
2
- tesla_fleet_api/const.py,sha256=TSs1sofg_Dv2Q-NEcVOB10GNbF0cU25NcO7WooMLoOk,3749
2
+ tesla_fleet_api/const.py,sha256=GyNepuHCbWnq7w_1TgJT5PjBpTNQhoCZdWCwIPKz4a8,3749
3
3
  tesla_fleet_api/exceptions.py,sha256=GvjJBR77xGt2g3vhiAxcNtysj-e1Me7F-9PwO9dyzOo,35430
4
4
  tesla_fleet_api/ratecalculator.py,sha256=4lz8yruUeouHXh_3ezsXX-CTpIegp1T1J4VuRV_qdHA,1791
5
5
  tesla_fleet_api/tesla/__init__.py,sha256=Cvpqu8OaOFmbuwu9KjgYrje8eVluDp2IU_zwdtXbmO0,282
@@ -12,8 +12,8 @@ tesla_fleet_api/tesla/partner.py,sha256=e-l6sEP6-IupjFEQieSUjhhvRXF3aL4ebPNahcGF
12
12
  tesla_fleet_api/tesla/tesla.py,sha256=Gs8-L3OsEMhs1N_vdDx-E46bOHKGowXTBxmRiP8NKh4,2391
13
13
  tesla_fleet_api/tesla/user.py,sha256=w8rwiAOIFjuDus8M0RpZ0wucJtw8kYFKtJfYVk7Ekr0,1194
14
14
  tesla_fleet_api/tesla/vehicle/__init__.py,sha256=3A5_wTQHofRShof4pUNOtF78-7lUh62uz2jq2ecnmRY,381
15
- tesla_fleet_api/tesla/vehicle/bluetooth.py,sha256=8SB3fDdJ1E8jDsaL6cix0Ks0lwlkvOLQ0_3lNZwWs1M,19064
16
- tesla_fleet_api/tesla/vehicle/commands.py,sha256=_LXAtuAIkeojJuw8k1QjEabAQQH2MLD5YZyUH58QoSg,51288
15
+ tesla_fleet_api/tesla/vehicle/bluetooth.py,sha256=rMxKOrc8T3Wz_sbuGYFot4ffCbOVP2jFSCd5tVuSWv0,21035
16
+ tesla_fleet_api/tesla/vehicle/commands.py,sha256=L3apfGwry-IBuSSLbuoPPjW_q4new2xTr6Iy_kUa2pA,51268
17
17
  tesla_fleet_api/tesla/vehicle/fleet.py,sha256=K9BVZj6CChJSDSMFroa7Cz0KrsYWj32ILtQumarkLaU,32080
18
18
  tesla_fleet_api/tesla/vehicle/signed.py,sha256=RUzVnZIfykz3YZW2gaxd1iaN1i8LkLaEoiXrbqZn9kg,1339
19
19
  tesla_fleet_api/tesla/vehicle/vehicle.py,sha256=IBvRO6qGkUf4bi-pFongA9fIe9iScvz_wGXtzEJXilY,870
@@ -44,8 +44,8 @@ tesla_fleet_api/teslemetry/vehicles.py,sha256=9nybVg7VHKLa2woMG6fzMmQP6xJIE_jdAd
44
44
  tesla_fleet_api/tessie/__init__.py,sha256=9lhQJaB6X4PObUL9QdaaZYqs2BxiTidu3zmHcBESLVw,78
45
45
  tesla_fleet_api/tessie/tessie.py,sha256=uhg0oOIxpwDvlvdBhKHeF3AGR2PzmdBgzh2-_EkmSq0,2617
46
46
  tesla_fleet_api/tessie/vehicles.py,sha256=gfEatilI_ct-R4CM5xYhrlduqCR9IHlyc56WmJf7v7k,1149
47
- tesla_fleet_api-1.0.11.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
48
- tesla_fleet_api-1.0.11.dist-info/METADATA,sha256=YYe4EpiOI6j7CcrDsOs0HsjKgYQs3YPwKL9wceXFNcU,4383
49
- tesla_fleet_api-1.0.11.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
50
- tesla_fleet_api-1.0.11.dist-info/top_level.txt,sha256=jeNbog_1saXBFrGpom9WyPWmilxsyP3szL_G7JLWQfM,16
51
- tesla_fleet_api-1.0.11.dist-info/RECORD,,
47
+ tesla_fleet_api-1.0.12.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
48
+ tesla_fleet_api-1.0.12.dist-info/METADATA,sha256=Z3qVhTn4BJs_P54gNAn4W02VUgvFhXkRh7yTveJvPn4,4383
49
+ tesla_fleet_api-1.0.12.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
50
+ tesla_fleet_api-1.0.12.dist-info/top_level.txt,sha256=jeNbog_1saXBFrGpom9WyPWmilxsyP3szL_G7JLWQfM,16
51
+ tesla_fleet_api-1.0.12.dist-info/RECORD,,