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 +1 -1
- tesla_fleet_api/tesla/vehicle/bluetooth.py +90 -43
- tesla_fleet_api/tesla/vehicle/commands.py +0 -1
- {tesla_fleet_api-1.0.11.dist-info → tesla_fleet_api-1.0.12.dist-info}/METADATA +1 -1
- {tesla_fleet_api-1.0.11.dist-info → tesla_fleet_api-1.0.12.dist-info}/RECORD +8 -8
- {tesla_fleet_api-1.0.11.dist-info → tesla_fleet_api-1.0.12.dist-info}/LICENSE +0 -0
- {tesla_fleet_api-1.0.11.dist-info → tesla_fleet_api-1.0.12.dist-info}/WHEEL +0 -0
- {tesla_fleet_api-1.0.11.dist-info → tesla_fleet_api-1.0.12.dist-info}/top_level.txt +0 -0
tesla_fleet_api/const.py
CHANGED
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import hashlib
|
4
4
|
import asyncio
|
5
|
-
|
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
|
-
|
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
|
-
|
226
|
+
def _on_notify(self,sender: BleakGATTCharacteristic, data: bytearray) -> None:
|
161
227
|
"""Receive data from the Tesla BLE device."""
|
162
|
-
if
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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.
|
203
|
-
|
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 =
|
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.
|
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,5 +1,5 @@
|
|
1
1
|
tesla_fleet_api/__init__.py,sha256=3DZMoZ-5srW-7SooAjqcRubQDuZPY8rMKH7eqIp4qtg,392
|
2
|
-
tesla_fleet_api/const.py,sha256=
|
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=
|
16
|
-
tesla_fleet_api/tesla/vehicle/commands.py,sha256=
|
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.
|
48
|
-
tesla_fleet_api-1.0.
|
49
|
-
tesla_fleet_api-1.0.
|
50
|
-
tesla_fleet_api-1.0.
|
51
|
-
tesla_fleet_api-1.0.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|