tesla-fleet-api 1.0.1__py3-none-any.whl → 1.0.3__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.
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import hashlib
4
- from asyncio import Future, get_running_loop
4
+ import asyncio
5
5
  from typing import TYPE_CHECKING
6
6
  from google.protobuf.message import DecodeError
7
7
 
@@ -12,38 +12,61 @@ from cryptography.hazmat.primitives.asymmetric import ec
12
12
 
13
13
  from tesla_fleet_api.tesla.vehicle.proto.keys_pb2 import Role
14
14
 
15
- from .commands import Commands
15
+ from tesla_fleet_api.tesla.vehicle.commands import Commands
16
16
 
17
- from ...const import (
17
+ from tesla_fleet_api.const import (
18
18
  LOGGER,
19
+ BluetoothVehicleData
19
20
  )
20
- from ...exceptions import (
21
+ from tesla_fleet_api.exceptions import (
21
22
  MESSAGE_FAULTS,
22
23
  WHITELIST_OPERATION_STATUS,
24
+ WhitelistOperationStatus,
25
+ NotOnWhitelistFault,
23
26
  )
24
27
 
25
28
  # Protocol
26
- from .proto.car_server_pb2 import (
29
+ from tesla_fleet_api.tesla.vehicle.proto.car_server_pb2 import (
30
+ Action,
31
+ VehicleAction,
27
32
  Response,
33
+ GetVehicleData,
34
+ GetChargeState,
35
+ GetClimateState,
36
+ GetDriveState,
37
+ GetLocationState,
38
+ GetClosuresState,
39
+ GetChargeScheduleState,
40
+ GetPreconditioningScheduleState,
41
+ GetTirePressureState,
42
+ GetMediaState,
43
+ GetMediaDetailState,
44
+ GetSoftwareUpdateState,
45
+ GetParentalControlsState,
28
46
  )
29
- from .proto.signatures_pb2 import (
47
+ from tesla_fleet_api.tesla.vehicle.proto.signatures_pb2 import (
30
48
  SessionInfo,
49
+ Session_Info_Status
31
50
  )
32
- from .proto.universal_message_pb2 import (
51
+ from tesla_fleet_api.tesla.vehicle.proto.universal_message_pb2 import (
33
52
  Destination,
34
53
  Domain,
35
54
  RoutableMessage,
36
55
  )
37
- from .proto.vcsec_pb2 import (
56
+ from tesla_fleet_api.tesla.vehicle.proto.vcsec_pb2 import (
38
57
  FromVCSECMessage,
58
+ InformationRequest,
59
+ InformationRequestType,
39
60
  KeyFormFactor,
40
61
  KeyMetadata,
41
62
  PermissionChange,
42
63
  PublicKey,
64
+ RKEAction_E,
43
65
  UnsignedMessage,
66
+ VehicleStatus,
44
67
  WhitelistOperation,
45
-
46
68
  )
69
+ from tesla_fleet_api.tesla.vehicle.proto.vehicle_pb2 import ChargeScheduleState, ChargeState, ClimateState, ClosuresState, DriveState, LocationState, MediaDetailState, MediaState, ParentalControlsState, PreconditioningScheduleState, SoftwareUpdateState, TirePressureState, VehicleData
47
70
 
48
71
  SERVICE_UUID = "00000211-b2d1-43f0-9b88-960cebf8b91e"
49
72
  WRITE_UUID = "00000212-b2d1-43f0-9b88-960cebf8b91e"
@@ -51,7 +74,7 @@ READ_UUID = "00000213-b2d1-43f0-9b88-960cebf8b91e"
51
74
  VERSION_UUID = "00000214-b2d1-43f0-9b88-960cebf8b91e"
52
75
 
53
76
  if TYPE_CHECKING:
54
- from ..tesla import Tesla
77
+ from tesla_fleet_api.tesla.tesla import Tesla
55
78
 
56
79
  def prependLength(message: bytes) -> bytearray:
57
80
  """Prepend a 2-byte length to the payload."""
@@ -62,40 +85,43 @@ class VehicleBluetooth(Commands):
62
85
 
63
86
  ble_name: str
64
87
  client: BleakClient
65
- _device: BLEDevice
66
- _futures: dict[Domain, Future]
88
+ _queues: dict[Domain, asyncio.Queue]
67
89
  _ekey: ec.EllipticCurvePublicKey
68
90
  _recv: bytearray = bytearray()
69
91
  _recv_len: int = 0
70
92
  _auth_method = "aes"
71
93
 
72
94
  def __init__(
73
- self, parent: Tesla, vin: str, key: ec.EllipticCurvePrivateKey | None = None
95
+ self, parent: Tesla, vin: str, key: ec.EllipticCurvePrivateKey | None = None, device: None | str | BLEDevice = None
74
96
  ):
75
97
  super().__init__(parent, vin, key)
76
98
  self.ble_name = "S" + hashlib.sha1(vin.encode('utf-8')).hexdigest()[:16] + "C"
77
- self._futures = {}
78
-
79
- async def discover(self, scanner: BleakScanner = BleakScanner()) -> BleakClient:
99
+ self._queues = {
100
+ Domain.DOMAIN_VEHICLE_SECURITY: asyncio.Queue(),
101
+ Domain.DOMAIN_INFOTAINMENT: asyncio.Queue(),
102
+ }
103
+ if device is not None:
104
+ self.client = BleakClient(device, services=[SERVICE_UUID])
105
+
106
+ async def find_client(self, scanner: BleakScanner = BleakScanner()) -> BleakClient:
80
107
  """Find the Tesla BLE device."""
81
108
 
82
109
  device = await scanner.find_device_by_name(self.ble_name)
83
110
  if not device:
84
111
  raise ValueError(f"Device {self.ble_name} not found")
85
- self._device = device
86
- self.client = BleakClient(self._device, services=[SERVICE_UUID])
87
- LOGGER.debug(f"Discovered device {self._device.name} {self._device.address}")
112
+ self.client = BleakClient(device, services=[SERVICE_UUID])
113
+ LOGGER.debug(f"Discovered device {device.name} {device.address}")
88
114
  return self.client
89
115
 
90
- def create_client(self, mac:str):
91
- """Create a client with a MAC."""
92
- self.client = BleakClient(mac, services=[SERVICE_UUID])
116
+ def create_client(self, device: str|BLEDevice) -> BleakClient:
117
+ """Create a client using a MAC address or Bleak Device."""
118
+ self.client = BleakClient(device, services=[SERVICE_UUID])
93
119
  return self.client
94
120
 
95
- async def connect(self, mac:str | None = None) -> None:
121
+ async def connect(self, device: str|BLEDevice | None = None) -> None:
96
122
  """Connect to the Tesla BLE device."""
97
- if mac is not None:
98
- self.create_client(mac)
123
+ if device is not None:
124
+ self.create_client(device)
99
125
  await self.client.connect()
100
126
  await self.client.start_notify(READ_UUID, self._on_notify)
101
127
 
@@ -108,11 +134,11 @@ class VehicleBluetooth(Commands):
108
134
  await self.connect()
109
135
  return self
110
136
 
111
- async def __aexit__(self, exc_type, exc_val, exc_tb):
137
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
112
138
  """Exit the async context."""
113
139
  await self.disconnect()
114
140
 
115
- def _on_notify(self,sender: BleakGATTCharacteristic,data : bytearray):
141
+ async def _on_notify(self,sender: BleakGATTCharacteristic,data : bytearray) -> None:
116
142
  """Receive data from the Tesla BLE device."""
117
143
  if self._recv_len:
118
144
  self._recv += data
@@ -122,68 +148,55 @@ class VehicleBluetooth(Commands):
122
148
  LOGGER.debug(f"Received {len(self._recv)} of {self._recv_len} bytes")
123
149
  while len(self._recv) > self._recv_len:
124
150
  LOGGER.warn(f"Received more data than expected: {len(self._recv)} > {self._recv_len}")
125
- self._on_message(bytes(self._recv[:self._recv_len]))
151
+ await self._on_message(bytes(self._recv[:self._recv_len]))
126
152
  self._recv_len = int.from_bytes(self._recv[self._recv_len:self._recv_len+2], 'big')
127
153
  self._recv = self._recv[self._recv_len+2:]
128
154
  continue
129
155
  if len(self._recv) == self._recv_len:
130
- self._on_message(bytes(self._recv))
156
+ await self._on_message(bytes(self._recv))
131
157
  self._recv = bytearray()
132
158
  self._recv_len = 0
133
159
 
134
- def _on_message(self, data:bytes):
160
+ async def _on_message(self, data:bytes) -> None:
135
161
  """Receive messages from the Tesla BLE data."""
136
162
  try:
137
163
  msg = RoutableMessage.FromString(data)
138
164
  except DecodeError as e:
139
165
  LOGGER.error(f"Error parsing message: {e}")
166
+ self._recv = bytearray()
167
+ self._recv_len = 0
140
168
  return
141
169
 
142
- # Update Session
143
- if(msg.session_info):
144
- info = SessionInfo.FromString(msg.session_info)
145
- LOGGER.debug(f"Received session info: {info}")
146
- self._sessions[msg.from_destination.domain].update(info)
147
-
148
170
  if(msg.to_destination.routing_address != self._from_destination):
149
- # Get the ephemeral key here and save to self._ekey
171
+ # Ignore ephemeral key broadcasts
150
172
  return
151
173
 
152
- if msg.from_destination.domain == Domain.DOMAIN_VEHICLE_SECURITY:
153
- submsg = FromVCSECMessage.FromString(msg.protobuf_message_as_bytes)
154
- print(submsg)
155
- elif msg.from_destination.domain == Domain.DOMAIN_INFOTAINMENT:
156
- submsg = Response.FromString(msg.protobuf_message_as_bytes)
157
- print(submsg)
174
+ LOGGER.info(f"Received response: {msg}")
175
+ await self._queues[msg.from_destination.domain].put(msg)
158
176
 
159
- if(self._futures[msg.from_destination.domain]):
160
- LOGGER.debug(f"Received response for request {msg.request_uuid}")
161
- self._futures[msg.from_destination.domain].set_result(msg)
162
- return
163
-
164
- async def _create_future(self, domain: Domain) -> Future:
165
- if(not self._sessions[domain].lock.locked):
166
- raise ValueError("Session is not locked")
167
- self._futures[domain] = get_running_loop().create_future()
168
- return self._futures[domain]
169
-
170
- async def _send(self, msg: RoutableMessage) -> RoutableMessage:
177
+ async def _send(self, msg: RoutableMessage, requires: str) -> RoutableMessage:
171
178
  """Serialize a message and send to the vehicle and wait for a response."""
172
179
  domain = msg.to_destination.domain
173
180
  async with self._sessions[domain].lock:
174
181
  LOGGER.debug(f"Sending message {msg}")
175
- future = await self._create_future(domain)
182
+
176
183
  payload = prependLength(msg.SerializeToString())
177
184
 
185
+ # Empty the queue before sending the message
186
+ while not self._queues[domain].empty():
187
+ await self._queues[domain].get()
178
188
  await self.client.write_gatt_char(WRITE_UUID, payload, True)
179
189
 
180
- resp = await future
181
- LOGGER.debug(f"Received message {resp}")
190
+ # Process the response
191
+ async with asyncio.timeout(10):
192
+ while True:
193
+ resp = await self._queues[domain].get()
194
+ LOGGER.debug(f"Received message {resp}")
182
195
 
183
- if resp.signedMessageStatus.signed_message_fault:
184
- raise MESSAGE_FAULTS[resp.signedMessageStatus.signed_message_fault]
196
+ self.validate_msg(resp)
185
197
 
186
- return resp
198
+ if resp.HasField(requires):
199
+ return resp
187
200
 
188
201
  async def pair(self, role: Role = Role.ROLE_OWNER, form: KeyFormFactor = KeyFormFactor.KEY_FORM_FACTOR_CLOUD_KEY):
189
202
  """Pair the key."""
@@ -210,10 +223,179 @@ class VehicleBluetooth(Commands):
210
223
  )
211
224
  resp = await self._send(msg)
212
225
  respMsg = FromVCSECMessage.FromString(resp.protobuf_message_as_bytes)
213
- print(respMsg)
214
226
  if(respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation):
215
227
  if(respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation < len(WHITELIST_OPERATION_STATUS)):
216
228
  raise WHITELIST_OPERATION_STATUS[respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation]
217
229
  else:
218
- raise ValueError(f"Unknown whitelist operation status: {respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation}")
230
+ raise WhitelistOperationStatus(f"Unknown whitelist operation failure: {respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation}")
219
231
  return
232
+
233
+ async def wake_up(self):
234
+ """Wake up the vehicle."""
235
+ return await self._sendVehicleSecurity(
236
+ UnsignedMessage(RKEAction=RKEAction_E.RKE_ACTION_WAKE_VEHICLE)
237
+ )
238
+
239
+ async def vehicle_data(self, endpoints: list[BluetoothVehicleData]) -> VehicleData:
240
+ """Get vehicle data."""
241
+ return await self._getInfotainment(
242
+ Action(
243
+ vehicleAction=VehicleAction(
244
+ getVehicleData=GetVehicleData(
245
+ getChargeState = GetChargeState() if BluetoothVehicleData.CHARGE_STATE in endpoints else None,
246
+ getClimateState = GetClimateState() if BluetoothVehicleData.CLIMATE_STATE in endpoints else None,
247
+ getDriveState = GetDriveState() if BluetoothVehicleData.DRIVE_STATE in endpoints else None,
248
+ getLocationState = GetLocationState() if BluetoothVehicleData.LOCATION_STATE in endpoints else None,
249
+ getClosuresState = GetClosuresState() if BluetoothVehicleData.CLOSURES_STATE in endpoints else None,
250
+ getChargeScheduleState = GetChargeScheduleState() if BluetoothVehicleData.CHARGE_SCHEDULE_STATE in endpoints else None,
251
+ getPreconditioningScheduleState = GetPreconditioningScheduleState() if BluetoothVehicleData.PRECONDITIONING_SCHEDULE_STATE in endpoints else None,
252
+ getTirePressureState = GetTirePressureState() if BluetoothVehicleData.TIRE_PRESSURE_STATE in endpoints else None,
253
+ getMediaState = GetMediaState() if BluetoothVehicleData.MEDIA_STATE in endpoints else None,
254
+ getMediaDetailState = GetMediaDetailState() if BluetoothVehicleData.MEDIA_DETAIL_STATE in endpoints else None,
255
+ getSoftwareUpdateState = GetSoftwareUpdateState() if BluetoothVehicleData.SOFTWARE_UPDATE_STATE in endpoints else None,
256
+ getParentalControlsState = GetParentalControlsState() if BluetoothVehicleData.PARENTAL_CONTROLS_STATE in endpoints else None,
257
+ )
258
+ )
259
+ )
260
+ )
261
+
262
+ async def charge_state(self) -> ChargeState:
263
+ return (await self._getInfotainment(
264
+ Action(
265
+ vehicleAction=VehicleAction(
266
+ getVehicleData=GetVehicleData(
267
+ getChargeState=GetChargeState()
268
+ )
269
+ )
270
+ )
271
+ )).charge_state
272
+
273
+ async def climate_state(self) -> ClimateState:
274
+ return (await self._getInfotainment(
275
+ Action(
276
+ vehicleAction=VehicleAction(
277
+ getVehicleData=GetVehicleData(
278
+ getClimateState=GetClimateState()
279
+ )
280
+ )
281
+ )
282
+ )).climate_state
283
+
284
+ async def drive_state(self) -> DriveState:
285
+ return (await self._getInfotainment(
286
+ Action(
287
+ vehicleAction=VehicleAction(
288
+ getVehicleData=GetVehicleData(
289
+ getDriveState=GetDriveState()
290
+ )
291
+ )
292
+ )
293
+ )).drive_state
294
+
295
+ async def location_state(self) -> LocationState:
296
+ return (await self._getInfotainment(
297
+ Action(
298
+ vehicleAction=VehicleAction(
299
+ getVehicleData=GetVehicleData(
300
+ getLocationState=GetLocationState()
301
+ )
302
+ )
303
+ )
304
+ )).location_state
305
+
306
+ async def closures_state(self) -> ClosuresState:
307
+ return (await self._getInfotainment(
308
+ Action(
309
+ vehicleAction=VehicleAction(
310
+ getVehicleData=GetVehicleData(
311
+ getClosuresState=GetClosuresState()
312
+ )
313
+ )
314
+ )
315
+ )).closures_state
316
+
317
+ async def charge_schedule_state(self) -> ChargeScheduleState:
318
+ return (await self._getInfotainment(
319
+ Action(
320
+ vehicleAction=VehicleAction(
321
+ getVehicleData=GetVehicleData(
322
+ getChargeScheduleState=GetChargeScheduleState()
323
+ )
324
+ )
325
+ )
326
+ )).charge_schedule_state
327
+
328
+ async def preconditioning_schedule_state(self) -> PreconditioningScheduleState:
329
+ return (await self._getInfotainment(
330
+ Action(
331
+ vehicleAction=VehicleAction(
332
+ getVehicleData=GetVehicleData(
333
+ getPreconditioningScheduleState=GetPreconditioningScheduleState()
334
+ )
335
+ )
336
+ )
337
+ )).preconditioning_schedule_state
338
+
339
+ async def tire_pressure_state(self) -> TirePressureState:
340
+ return (await self._getInfotainment(
341
+ Action(
342
+ vehicleAction=VehicleAction(
343
+ getVehicleData=GetVehicleData(
344
+ getTirePressureState=GetTirePressureState()
345
+ )
346
+ )
347
+ )
348
+ )).tire_pressure_state
349
+
350
+ async def media_state(self) -> MediaState:
351
+ return (await self._getInfotainment(
352
+ Action(
353
+ vehicleAction=VehicleAction(
354
+ getVehicleData=GetVehicleData(
355
+ getMediaState=GetMediaState()
356
+ )
357
+ )
358
+ )
359
+ )).media_state
360
+
361
+ async def media_detail_state(self) -> MediaDetailState:
362
+ return (await self._getInfotainment(
363
+ Action(
364
+ vehicleAction=VehicleAction(
365
+ getVehicleData=GetVehicleData(
366
+ getMediaDetailState=GetMediaDetailState()
367
+ )
368
+ )
369
+ )
370
+ )).media_detail_state
371
+
372
+ async def software_update_state(self) -> SoftwareUpdateState:
373
+ return (await self._getInfotainment(
374
+ Action(
375
+ vehicleAction=VehicleAction(
376
+ getVehicleData=GetVehicleData(
377
+ getSoftwareUpdateState=GetSoftwareUpdateState()
378
+ )
379
+ )
380
+ )
381
+ )).software_update_state
382
+
383
+ async def parental_controls_state(self) -> ParentalControlsState:
384
+ return (await self._getInfotainment(
385
+ Action(
386
+ vehicleAction=VehicleAction(
387
+ getVehicleData=GetVehicleData(
388
+ getParentalControlsState=GetParentalControlsState()
389
+ )
390
+ )
391
+ )
392
+ )).parental_controls_state
393
+
394
+ async def vehicle_state(self) -> VehicleStatus:
395
+ return await self._getVehicleSecurity(
396
+ UnsignedMessage(
397
+ InformationRequest=InformationRequest(
398
+ informationRequestType=InformationRequestType.INFORMATION_REQUEST_TYPE_GET_STATUS
399
+ )
400
+ )
401
+ )