tesla-fleet-api 1.0.2__py3-none-any.whl → 1.0.4__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.2"
6
+ VERSION = "1.0.4"
7
7
  LOGGER = logging.getLogger(__package__)
8
8
  SERVERS = {
9
9
  "na": "https://fleet-api.prd.na.vn.cloud.tesla.com",
@@ -159,3 +159,17 @@ class TeslaEnergyPeriod(StrEnum):
159
159
  MONTH = "month"
160
160
  YEAR = "year"
161
161
  LIFETIME = "lifetime"
162
+
163
+ class BluetoothVehicleData(StrEnum):
164
+ CHARGE_STATE = "GetChargeState"
165
+ CLIMATE_STATE = "GetClimateState"
166
+ DRIVE_STATE = "GetDriveState"
167
+ LOCATION_STATE = "GetLocationState"
168
+ CLOSURES_STATE = "GetClosuresState"
169
+ CHARGE_SCHEDULE_STATE = "GetChargeScheduleState"
170
+ PRECONDITIONING_SCHEDULE_STATE = "GetPreconditioningScheduleState"
171
+ TIRE_PRESSURE_STATE = "GetTirePressureState"
172
+ MEDIA_STATE = "GetMediaState"
173
+ MEDIA_DETAIL_STATE = "GetMediaDetailState"
174
+ SOFTWARE_UPDATE_STATE = "GetSoftwareUpdateState"
175
+ PARENTAL_CONTROLS_STATE = "GetParentalControlsState"
@@ -656,6 +656,26 @@ class TeslaFleetMessageFaultResponseSizeExceedsMTU(TeslaFleetMessageFault):
656
656
  message = "Client's request was received, but response size exceeded MTU"
657
657
  code = 25
658
658
 
659
+ class TeslaFleetMessageFaultRepeatedCounter(TeslaFleetMessageFault):
660
+ """The vehicle has seen this counter value before. Reset the counter and try again"""
661
+
662
+ message = "The vehicle has seen this counter value before. Reset the counter and try again"
663
+ code = 26
664
+
665
+
666
+ class TeslaFleetMessageFaultInvalidKeyHandle(TeslaFleetMessageFault):
667
+ """The key handle is not valid. The key may have been revoked or expired"""
668
+
669
+ message = "The key handle is not valid. The key may have been revoked or expired"
670
+ code = 27
671
+
672
+
673
+ class TeslaFleetMessageFaultRequiresResponseEncryption(TeslaFleetMessageFault):
674
+ """The response requires encryption but encryption was not requested"""
675
+
676
+ message = "The response requires encryption but encryption was not requested"
677
+ code = 28
678
+
659
679
 
660
680
  MESSAGE_FAULTS = [
661
681
  None,
@@ -684,9 +704,9 @@ MESSAGE_FAULTS = [
684
704
  TeslaFleetMessageFaultCommandRequiresAccountCredentials,
685
705
  TeslaFleetMessageFaultFieldExceedsMTU,
686
706
  TeslaFleetMessageFaultResponseSizeExceedsMTU,
687
- None,
688
- None,
689
- None,
707
+ TeslaFleetMessageFaultRepeatedCounter,
708
+ TeslaFleetMessageFaultInvalidKeyHandle,
709
+ TeslaFleetMessageFaultRequiresResponseEncryption,
690
710
  ]
691
711
 
692
712
  class SignedMessageInformationFault(TeslaFleetError):
@@ -2,6 +2,9 @@
2
2
 
3
3
  import hashlib
4
4
  import re
5
+ from google.protobuf.json_format import MessageToJson, MessageToDict
6
+ from bleak.backends.device import BLEDevice
7
+ from cryptography.hazmat.primitives.asymmetric import ec
5
8
 
6
9
  from tesla_fleet_api.tesla.tesla import Tesla
7
10
  from tesla_fleet_api.tesla.vehicle.bluetooth import VehicleBluetooth
@@ -36,8 +39,16 @@ class Vehicles(dict[str, VehicleBluetooth]):
36
39
  """Creates a specific vehicle."""
37
40
  return self.createBluetooth(vin)
38
41
 
39
- def createBluetooth(self, vin: str) -> VehicleBluetooth:
42
+ def createBluetooth(self, vin: str, key: ec.EllipticCurvePrivateKey | None = None, device: None | str | BLEDevice = None) -> VehicleBluetooth:
40
43
  """Creates a specific vehicle."""
41
- vehicle = VehicleBluetooth(self._parent, vin)
44
+ vehicle = VehicleBluetooth(self._parent, vin, key, device)
42
45
  self[vin] = vehicle
43
46
  return vehicle
47
+
48
+ def toJson(message) -> str:
49
+ """Convert a protobuf message to JSON."""
50
+ return MessageToJson(message, preserving_proto_field_name=True)
51
+
52
+ def toDict(message) -> dict:
53
+ """Convert a protobuf message to a dictionary."""
54
+ return MessageToDict(message, preserving_proto_field_name=True)
@@ -1,5 +1,5 @@
1
1
  from typing import Any
2
- from ..const import Method
2
+ from tesla_fleet_api.const import Method
3
3
 
4
4
 
5
5
  class Partner:
@@ -1,10 +1,10 @@
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
+ from bleak_retry_connector import establish_connection, MAX_CONNECT_ATTEMPTS
8
8
  from bleak import BleakClient, BleakScanner
9
9
  from bleak.backends.characteristic import BleakGATTCharacteristic
10
10
  from bleak.backends.device import BLEDevice
@@ -16,19 +16,30 @@ from tesla_fleet_api.tesla.vehicle.commands import Commands
16
16
 
17
17
  from tesla_fleet_api.const import (
18
18
  LOGGER,
19
+ BluetoothVehicleData
19
20
  )
20
21
  from tesla_fleet_api.exceptions import (
21
- MESSAGE_FAULTS,
22
22
  WHITELIST_OPERATION_STATUS,
23
23
  WhitelistOperationStatus,
24
24
  )
25
25
 
26
26
  # Protocol
27
27
  from tesla_fleet_api.tesla.vehicle.proto.car_server_pb2 import (
28
- Response,
29
- )
30
- from tesla_fleet_api.tesla.vehicle.proto.signatures_pb2 import (
31
- SessionInfo,
28
+ Action,
29
+ VehicleAction,
30
+ GetVehicleData,
31
+ GetChargeState,
32
+ GetClimateState,
33
+ GetDriveState,
34
+ GetLocationState,
35
+ GetClosuresState,
36
+ GetChargeScheduleState,
37
+ GetPreconditioningScheduleState,
38
+ GetTirePressureState,
39
+ GetMediaState,
40
+ GetMediaDetailState,
41
+ GetSoftwareUpdateState,
42
+ GetParentalControlsState,
32
43
  )
33
44
  from tesla_fleet_api.tesla.vehicle.proto.universal_message_pb2 import (
34
45
  Destination,
@@ -37,15 +48,18 @@ from tesla_fleet_api.tesla.vehicle.proto.universal_message_pb2 import (
37
48
  )
38
49
  from tesla_fleet_api.tesla.vehicle.proto.vcsec_pb2 import (
39
50
  FromVCSECMessage,
51
+ InformationRequest,
52
+ InformationRequestType,
40
53
  KeyFormFactor,
41
54
  KeyMetadata,
42
55
  PermissionChange,
43
56
  PublicKey,
44
57
  RKEAction_E,
45
58
  UnsignedMessage,
59
+ VehicleStatus,
46
60
  WhitelistOperation,
47
-
48
61
  )
62
+ from tesla_fleet_api.tesla.vehicle.proto.vehicle_pb2 import ChargeScheduleState, ChargeState, ClimateState, ClosuresState, DriveState, LocationState, MediaDetailState, MediaState, ParentalControlsState, PreconditioningScheduleState, SoftwareUpdateState, TirePressureState, VehicleData
49
63
 
50
64
  SERVICE_UUID = "00000211-b2d1-43f0-9b88-960cebf8b91e"
51
65
  WRITE_UUID = "00000212-b2d1-43f0-9b88-960cebf8b91e"
@@ -63,42 +77,48 @@ class VehicleBluetooth(Commands):
63
77
  """Class describing the Tesla Fleet API vehicle endpoints and commands for a specific vehicle with command signing."""
64
78
 
65
79
  ble_name: str
80
+ device: BLEDevice
66
81
  client: BleakClient
67
- _device: BLEDevice
68
- _futures: dict[Domain, Future]
82
+ _queues: dict[Domain, asyncio.Queue]
69
83
  _ekey: ec.EllipticCurvePublicKey
70
84
  _recv: bytearray = bytearray()
71
85
  _recv_len: int = 0
72
86
  _auth_method = "aes"
73
87
 
74
88
  def __init__(
75
- self, parent: Tesla, vin: str, key: ec.EllipticCurvePrivateKey | None = None
89
+ self, parent: Tesla, vin: str, key: ec.EllipticCurvePrivateKey | None = None, device: None | str | BLEDevice = None
76
90
  ):
77
91
  super().__init__(parent, vin, key)
78
92
  self.ble_name = "S" + hashlib.sha1(vin.encode('utf-8')).hexdigest()[:16] + "C"
79
- self._futures = {}
80
-
81
- async def find_client(self, scanner: BleakScanner = BleakScanner()) -> BleakClient:
93
+ self._queues = {
94
+ Domain.DOMAIN_VEHICLE_SECURITY: asyncio.Queue(),
95
+ Domain.DOMAIN_INFOTAINMENT: asyncio.Queue(),
96
+ }
97
+ if device is not None:
98
+ self.client = BleakClient(device, services=[SERVICE_UUID])
99
+
100
+ async def find_vehicle(self, name: str | None = None, address: str | None = None, scanner: BleakScanner = BleakScanner()) -> BLEDevice:
82
101
  """Find the Tesla BLE device."""
83
-
84
- device = await scanner.find_device_by_name(self.ble_name)
102
+ if name is not None:
103
+ device = await scanner.find_device_by_name(name)
104
+ elif address is not None:
105
+ device = await scanner.find_device_by_address(address)
106
+ else:
107
+ device = await scanner.find_device_by_name(self.ble_name)
85
108
  if not device:
86
109
  raise ValueError(f"Device {self.ble_name} not found")
87
- self._device = device
88
- self.client = BleakClient(self._device, services=[SERVICE_UUID])
89
- LOGGER.debug(f"Discovered device {self._device.name} {self._device.address}")
90
- return self.client
110
+ self.device = device
111
+ return self.device
91
112
 
92
- def create_client(self, mac:str) -> BleakClient:
93
- """Create a client using a MAC address."""
94
- self.client = BleakClient(mac, services=[SERVICE_UUID])
95
- return self.client
113
+ def set_device(self, device: BLEDevice) -> None:
114
+ self.device = device
96
115
 
97
- async def connect(self, mac:str | None = None) -> None:
116
+ def get_device(self) -> BLEDevice:
117
+ return self.device
118
+
119
+ async def connect(self, device: BLEDevice | None = None, max_attempts: int = MAX_CONNECT_ATTEMPTS) -> None:
98
120
  """Connect to the Tesla BLE device."""
99
- if mac is not None:
100
- self.create_client(mac)
101
- await self.client.connect()
121
+ self.client = await establish_connection(BleakClient, self.device, self.vin, max_attempts=max_attempts, ble_device_callback=self.get_device)
102
122
  await self.client.start_notify(READ_UUID, self._on_notify)
103
123
 
104
124
  async def disconnect(self) -> bool:
@@ -114,7 +134,7 @@ class VehicleBluetooth(Commands):
114
134
  """Exit the async context."""
115
135
  await self.disconnect()
116
136
 
117
- def _on_notify(self,sender: BleakGATTCharacteristic,data : bytearray) -> None:
137
+ async def _on_notify(self,sender: BleakGATTCharacteristic,data : bytearray) -> None:
118
138
  """Receive data from the Tesla BLE device."""
119
139
  if self._recv_len:
120
140
  self._recv += data
@@ -124,68 +144,55 @@ class VehicleBluetooth(Commands):
124
144
  LOGGER.debug(f"Received {len(self._recv)} of {self._recv_len} bytes")
125
145
  while len(self._recv) > self._recv_len:
126
146
  LOGGER.warn(f"Received more data than expected: {len(self._recv)} > {self._recv_len}")
127
- self._on_message(bytes(self._recv[:self._recv_len]))
147
+ await self._on_message(bytes(self._recv[:self._recv_len]))
128
148
  self._recv_len = int.from_bytes(self._recv[self._recv_len:self._recv_len+2], 'big')
129
149
  self._recv = self._recv[self._recv_len+2:]
130
150
  continue
131
151
  if len(self._recv) == self._recv_len:
132
- self._on_message(bytes(self._recv))
152
+ await self._on_message(bytes(self._recv))
133
153
  self._recv = bytearray()
134
154
  self._recv_len = 0
135
155
 
136
- def _on_message(self, data:bytes) -> None:
156
+ async def _on_message(self, data:bytes) -> None:
137
157
  """Receive messages from the Tesla BLE data."""
138
158
  try:
139
159
  msg = RoutableMessage.FromString(data)
140
160
  except DecodeError as e:
141
161
  LOGGER.error(f"Error parsing message: {e}")
162
+ self._recv = bytearray()
163
+ self._recv_len = 0
142
164
  return
143
165
 
144
- # Update Session
145
- if(msg.session_info):
146
- info = SessionInfo.FromString(msg.session_info)
147
- LOGGER.debug(f"Received session info: {info}")
148
- self._sessions[msg.from_destination.domain].update(info)
149
-
150
166
  if(msg.to_destination.routing_address != self._from_destination):
151
- # Get the ephemeral key here and save to self._ekey
152
- return
153
-
154
- if(self._futures[msg.from_destination.domain]):
155
- LOGGER.debug(f"Received response for request {msg.request_uuid}")
156
- self._futures[msg.from_destination.domain].set_result(msg)
167
+ # Ignore ephemeral key broadcasts
157
168
  return
158
169
 
159
- if msg.from_destination.domain == Domain.DOMAIN_VEHICLE_SECURITY:
160
- submsg = FromVCSECMessage.FromString(msg.protobuf_message_as_bytes)
161
- LOGGER.warning(f"Received orphaned VCSEC response: {submsg}")
162
- elif msg.from_destination.domain == Domain.DOMAIN_INFOTAINMENT:
163
- submsg = Response.FromString(msg.protobuf_message_as_bytes)
164
- LOGGER.warning(f"Received orphaned INFOTAINMENT response: {submsg}")
170
+ LOGGER.info(f"Received response: {msg}")
171
+ await self._queues[msg.from_destination.domain].put(msg)
165
172
 
166
- async def _create_future(self, domain: Domain) -> Future:
167
- if(not self._sessions[domain].lock.locked):
168
- raise ValueError("Session is not locked")
169
- self._futures[domain] = get_running_loop().create_future()
170
- return self._futures[domain]
171
-
172
- async def _send(self, msg: RoutableMessage) -> RoutableMessage:
173
+ async def _send(self, msg: RoutableMessage, requires: str) -> RoutableMessage:
173
174
  """Serialize a message and send to the vehicle and wait for a response."""
174
175
  domain = msg.to_destination.domain
175
176
  async with self._sessions[domain].lock:
176
177
  LOGGER.debug(f"Sending message {msg}")
177
- future = await self._create_future(domain)
178
+
178
179
  payload = prependLength(msg.SerializeToString())
179
180
 
181
+ # Empty the queue before sending the message
182
+ while not self._queues[domain].empty():
183
+ await self._queues[domain].get()
180
184
  await self.client.write_gatt_char(WRITE_UUID, payload, True)
181
185
 
182
- resp = await future
183
- LOGGER.debug(f"Received message {resp}")
186
+ # Process the response
187
+ async with asyncio.timeout(10):
188
+ while True:
189
+ resp = await self._queues[domain].get()
190
+ LOGGER.debug(f"Received message {resp}")
184
191
 
185
- if resp.signedMessageStatus.signed_message_fault:
186
- raise MESSAGE_FAULTS[resp.signedMessageStatus.signed_message_fault]
192
+ self.validate_msg(resp)
187
193
 
188
- return resp
194
+ if resp.HasField(requires):
195
+ return resp
189
196
 
190
197
  async def pair(self, role: Role = Role.ROLE_OWNER, form: KeyFormFactor = KeyFormFactor.KEY_FORM_FACTOR_CLOUD_KEY):
191
198
  """Pair the key."""
@@ -224,3 +231,167 @@ class VehicleBluetooth(Commands):
224
231
  return await self._sendVehicleSecurity(
225
232
  UnsignedMessage(RKEAction=RKEAction_E.RKE_ACTION_WAKE_VEHICLE)
226
233
  )
234
+
235
+ async def vehicle_data(self, endpoints: list[BluetoothVehicleData]) -> VehicleData:
236
+ """Get vehicle data."""
237
+ return await self._getInfotainment(
238
+ Action(
239
+ vehicleAction=VehicleAction(
240
+ getVehicleData=GetVehicleData(
241
+ getChargeState = GetChargeState() if BluetoothVehicleData.CHARGE_STATE in endpoints else None,
242
+ getClimateState = GetClimateState() if BluetoothVehicleData.CLIMATE_STATE in endpoints else None,
243
+ getDriveState = GetDriveState() if BluetoothVehicleData.DRIVE_STATE in endpoints else None,
244
+ getLocationState = GetLocationState() if BluetoothVehicleData.LOCATION_STATE in endpoints else None,
245
+ getClosuresState = GetClosuresState() if BluetoothVehicleData.CLOSURES_STATE in endpoints else None,
246
+ getChargeScheduleState = GetChargeScheduleState() if BluetoothVehicleData.CHARGE_SCHEDULE_STATE in endpoints else None,
247
+ getPreconditioningScheduleState = GetPreconditioningScheduleState() if BluetoothVehicleData.PRECONDITIONING_SCHEDULE_STATE in endpoints else None,
248
+ getTirePressureState = GetTirePressureState() if BluetoothVehicleData.TIRE_PRESSURE_STATE in endpoints else None,
249
+ getMediaState = GetMediaState() if BluetoothVehicleData.MEDIA_STATE in endpoints else None,
250
+ getMediaDetailState = GetMediaDetailState() if BluetoothVehicleData.MEDIA_DETAIL_STATE in endpoints else None,
251
+ getSoftwareUpdateState = GetSoftwareUpdateState() if BluetoothVehicleData.SOFTWARE_UPDATE_STATE in endpoints else None,
252
+ getParentalControlsState = GetParentalControlsState() if BluetoothVehicleData.PARENTAL_CONTROLS_STATE in endpoints else None,
253
+ )
254
+ )
255
+ )
256
+ )
257
+
258
+ async def charge_state(self) -> ChargeState:
259
+ return (await self._getInfotainment(
260
+ Action(
261
+ vehicleAction=VehicleAction(
262
+ getVehicleData=GetVehicleData(
263
+ getChargeState=GetChargeState()
264
+ )
265
+ )
266
+ )
267
+ )).charge_state
268
+
269
+ async def climate_state(self) -> ClimateState:
270
+ return (await self._getInfotainment(
271
+ Action(
272
+ vehicleAction=VehicleAction(
273
+ getVehicleData=GetVehicleData(
274
+ getClimateState=GetClimateState()
275
+ )
276
+ )
277
+ )
278
+ )).climate_state
279
+
280
+ async def drive_state(self) -> DriveState:
281
+ return (await self._getInfotainment(
282
+ Action(
283
+ vehicleAction=VehicleAction(
284
+ getVehicleData=GetVehicleData(
285
+ getDriveState=GetDriveState()
286
+ )
287
+ )
288
+ )
289
+ )).drive_state
290
+
291
+ async def location_state(self) -> LocationState:
292
+ return (await self._getInfotainment(
293
+ Action(
294
+ vehicleAction=VehicleAction(
295
+ getVehicleData=GetVehicleData(
296
+ getLocationState=GetLocationState()
297
+ )
298
+ )
299
+ )
300
+ )).location_state
301
+
302
+ async def closures_state(self) -> ClosuresState:
303
+ return (await self._getInfotainment(
304
+ Action(
305
+ vehicleAction=VehicleAction(
306
+ getVehicleData=GetVehicleData(
307
+ getClosuresState=GetClosuresState()
308
+ )
309
+ )
310
+ )
311
+ )).closures_state
312
+
313
+ async def charge_schedule_state(self) -> ChargeScheduleState:
314
+ return (await self._getInfotainment(
315
+ Action(
316
+ vehicleAction=VehicleAction(
317
+ getVehicleData=GetVehicleData(
318
+ getChargeScheduleState=GetChargeScheduleState()
319
+ )
320
+ )
321
+ )
322
+ )).charge_schedule_state
323
+
324
+ async def preconditioning_schedule_state(self) -> PreconditioningScheduleState:
325
+ return (await self._getInfotainment(
326
+ Action(
327
+ vehicleAction=VehicleAction(
328
+ getVehicleData=GetVehicleData(
329
+ getPreconditioningScheduleState=GetPreconditioningScheduleState()
330
+ )
331
+ )
332
+ )
333
+ )).preconditioning_schedule_state
334
+
335
+ async def tire_pressure_state(self) -> TirePressureState:
336
+ return (await self._getInfotainment(
337
+ Action(
338
+ vehicleAction=VehicleAction(
339
+ getVehicleData=GetVehicleData(
340
+ getTirePressureState=GetTirePressureState()
341
+ )
342
+ )
343
+ )
344
+ )).tire_pressure_state
345
+
346
+ async def media_state(self) -> MediaState:
347
+ return (await self._getInfotainment(
348
+ Action(
349
+ vehicleAction=VehicleAction(
350
+ getVehicleData=GetVehicleData(
351
+ getMediaState=GetMediaState()
352
+ )
353
+ )
354
+ )
355
+ )).media_state
356
+
357
+ async def media_detail_state(self) -> MediaDetailState:
358
+ return (await self._getInfotainment(
359
+ Action(
360
+ vehicleAction=VehicleAction(
361
+ getVehicleData=GetVehicleData(
362
+ getMediaDetailState=GetMediaDetailState()
363
+ )
364
+ )
365
+ )
366
+ )).media_detail_state
367
+
368
+ async def software_update_state(self) -> SoftwareUpdateState:
369
+ return (await self._getInfotainment(
370
+ Action(
371
+ vehicleAction=VehicleAction(
372
+ getVehicleData=GetVehicleData(
373
+ getSoftwareUpdateState=GetSoftwareUpdateState()
374
+ )
375
+ )
376
+ )
377
+ )).software_update_state
378
+
379
+ async def parental_controls_state(self) -> ParentalControlsState:
380
+ return (await self._getInfotainment(
381
+ Action(
382
+ vehicleAction=VehicleAction(
383
+ getVehicleData=GetVehicleData(
384
+ getParentalControlsState=GetParentalControlsState()
385
+ )
386
+ )
387
+ )
388
+ )).parental_controls_state
389
+
390
+ async def vehicle_state(self) -> VehicleStatus:
391
+ return await self._getVehicleSecurity(
392
+ UnsignedMessage(
393
+ InformationRequest=InformationRequest(
394
+ informationRequestType=InformationRequestType.INFORMATION_REQUEST_TYPE_GET_STATUS
395
+ )
396
+ )
397
+ )
@@ -14,7 +14,10 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
14
14
  from asyncio import Lock, sleep
15
15
 
16
16
  from tesla_fleet_api.exceptions import (
17
+ MESSAGE_FAULTS,
17
18
  SIGNED_MESSAGE_INFORMATION_FAULTS,
19
+ NotOnWhitelistFault,
20
+ #TeslaFleetMessageFaultInvalidSignature,
18
21
  TeslaFleetMessageFaultIncorrectEpoch,
19
22
  TeslaFleetMessageFaultInvalidTokenOrCounter,
20
23
  )
@@ -37,33 +40,25 @@ from tesla_fleet_api.tesla.vehicle.proto.car_server_pb2 import (
37
40
  Response,
38
41
  )
39
42
  from tesla_fleet_api.tesla.vehicle.proto.signatures_pb2 import (
40
- SIGNATURE_TYPE_AES_GCM_PERSONALIZED,
41
- SIGNATURE_TYPE_HMAC_PERSONALIZED,
42
- TAG_COUNTER,
43
- TAG_DOMAIN,
44
- TAG_END,
45
- TAG_EPOCH,
46
- TAG_EXPIRES_AT,
47
- TAG_PERSONALIZATION,
48
- TAG_SIGNATURE_TYPE,
43
+ Session_Info_Status,
44
+ SignatureType,
45
+ Tag,
49
46
  AES_GCM_Personalized_Signature_Data,
50
47
  KeyIdentity,
51
48
  SessionInfo,
52
49
  SignatureData,
53
50
  )
54
51
  from tesla_fleet_api.tesla.vehicle.proto.universal_message_pb2 import (
55
- DOMAIN_INFOTAINMENT,
56
- DOMAIN_VEHICLE_SECURITY,
57
- OPERATIONSTATUS_ERROR,
58
- OPERATIONSTATUS_WAIT,
52
+ OperationStatus_E,
59
53
  Destination,
60
54
  Domain,
61
55
  RoutableMessage,
62
56
  SessionInfoRequest,
57
+ Flags,
63
58
  )
64
59
  from tesla_fleet_api.tesla.vehicle.proto.vcsec_pb2 import (
65
- OPERATIONSTATUS_OK,
66
60
  FromVCSECMessage,
61
+ VehicleStatus,
67
62
  )
68
63
  from tesla_fleet_api.tesla.vehicle.proto.car_server_pb2 import (
69
64
  Action,
@@ -110,7 +105,7 @@ from tesla_fleet_api.tesla.vehicle.proto.car_server_pb2 import (
110
105
  MediaPreviousTrack,
111
106
  MediaPreviousFavorite,
112
107
  )
113
- from tesla_fleet_api.tesla.vehicle.proto.vehicle_pb2 import VehicleState, ClimateState
108
+ from tesla_fleet_api.tesla.vehicle.proto.vehicle_pb2 import VehicleData, VehicleState, ClimateState
114
109
  from tesla_fleet_api.tesla.vehicle.proto.vcsec_pb2 import (
115
110
  UnsignedMessage,
116
111
  RKEAction_E,
@@ -173,6 +168,7 @@ class Session:
173
168
  hmac: bytes
174
169
  publicKey: bytes
175
170
  lock: Lock
171
+ ready: bool
176
172
 
177
173
  def __init__(self, parent: Commands, domain: Domain):
178
174
  self.parent = parent
@@ -258,10 +254,21 @@ class Commands(Vehicle):
258
254
 
259
255
 
260
256
  @abstractmethod
261
- async def _send(self, msg: RoutableMessage) -> RoutableMessage:
257
+ async def _send(self, msg: RoutableMessage, requires: str) -> RoutableMessage:
262
258
  """Transmit the message to the vehicle."""
263
259
  raise NotImplementedError
264
260
 
261
+ def validate_msg(self, msg: RoutableMessage) -> None:
262
+ """Validate the message."""
263
+ if(msg.session_info):
264
+ info = SessionInfo.FromString(msg.session_info)
265
+ if(info.status == Session_Info_Status.SESSION_INFO_STATUS_KEY_NOT_ON_WHITELIST):
266
+ raise NotOnWhitelistFault
267
+ self._sessions[msg.from_destination.domain].update(info)
268
+
269
+ if msg.signedMessageStatus.signed_message_fault > 0:
270
+ raise MESSAGE_FAULTS[msg.signedMessageStatus.signed_message_fault]
271
+
265
272
  @abstractmethod
266
273
  async def _command(self, domain: Domain, command: bytes, attempt: int = 0) -> dict[str, Any]:
267
274
  """Serialize a message and send to the signed command endpoint."""
@@ -277,8 +284,9 @@ class Commands(Vehicle):
277
284
  raise ValueError(f"Unknown auth method: {self._auth_method}")
278
285
 
279
286
  try:
280
- resp = await self._send(msg)
287
+ resp = await self._send(msg, "protobuf_message_as_bytes")
281
288
  except (
289
+ #TeslaFleetMessageFaultInvalidSignature,
282
290
  TeslaFleetMessageFaultIncorrectEpoch,
283
291
  TeslaFleetMessageFaultInvalidTokenOrCounter,
284
292
  ) as e:
@@ -288,7 +296,7 @@ class Commands(Vehicle):
288
296
  raise e
289
297
  return await self._command(domain, command, attempt)
290
298
 
291
- if resp.signedMessageStatus.operation_status == OPERATIONSTATUS_WAIT:
299
+ if resp.signedMessageStatus.operation_status == OperationStatus_E.OPERATIONSTATUS_WAIT:
292
300
  attempt += 1
293
301
  if attempt > 3:
294
302
  # We tried 3 times, give up, raise the error
@@ -298,8 +306,53 @@ class Commands(Vehicle):
298
306
  return await self._command(domain, command, attempt)
299
307
 
300
308
  if resp.HasField("protobuf_message_as_bytes"):
301
- if(resp.from_destination.domain == DOMAIN_VEHICLE_SECURITY):
302
- vcsec = FromVCSECMessage.FromString(resp.protobuf_message_as_bytes)
309
+ #decrypt
310
+ if(resp.signature_data.HasField("AES_GCM_Response_data")):
311
+ if(msg.signature_data.HasField("AES_GCM_Personalized_data")):
312
+ request_hash = bytes([SignatureType.SIGNATURE_TYPE_AES_GCM_PERSONALIZED]) + msg.signature_data.AES_GCM_Personalized_data.tag
313
+ elif(msg.signature_data.HasField("HMAC_Personalized_data")):
314
+ request_hash = bytes([SignatureType.SIGNATURE_TYPE_HMAC_PERSONALIZED]) + msg.signature_data.HMAC_Personalized_data.tag
315
+ if(session.domain == Domain.DOMAIN_VEHICLE_SECURITY):
316
+ request_hash = request_hash[:17]
317
+ else:
318
+ raise ValueError("Invalid request signature data")
319
+
320
+ metadata = bytes([
321
+ Tag.TAG_SIGNATURE_TYPE,
322
+ 1,
323
+ SignatureType.SIGNATURE_TYPE_AES_GCM_RESPONSE,
324
+ Tag.TAG_DOMAIN,
325
+ 1,
326
+ resp.from_destination.domain,
327
+ Tag.TAG_PERSONALIZATION,
328
+ 17,
329
+ *self.vin.encode(),
330
+ Tag.TAG_COUNTER,
331
+ 4,
332
+ *struct.pack(">I", resp.signature_data.AES_GCM_Response_data.counter),
333
+ Tag.TAG_FLAGS,
334
+ 4,
335
+ *struct.pack(">I", resp.flags),
336
+ Tag.TAG_REQUEST_HASH,
337
+ 17,
338
+ *request_hash,
339
+ Tag.TAG_FAULT,
340
+ 4,
341
+ *struct.pack(">I", resp.signedMessageStatus.signed_message_fault),
342
+ Tag.TAG_END,
343
+ ])
344
+
345
+ aad = Hash(SHA256())
346
+ aad.update(metadata)
347
+ aesgcm = AESGCM(session.sharedKey)
348
+ resp.protobuf_message_as_bytes = aesgcm.decrypt(resp.signature_data.AES_GCM_Response_data.nonce, resp.protobuf_message_as_bytes + resp.signature_data.AES_GCM_Response_data.tag, aad.finalize())
349
+
350
+ if(resp.from_destination.domain == Domain.DOMAIN_VEHICLE_SECURITY):
351
+ try:
352
+ vcsec = FromVCSECMessage.FromString(resp.protobuf_message_as_bytes)
353
+ except Exception as e:
354
+ LOGGER.error("Failed to parse VCSEC message: %s %s", e, resp)
355
+ raise e
303
356
  LOGGER.debug("VCSEC Response: %s", vcsec)
304
357
  if vcsec.HasField("nominalError"):
305
358
  LOGGER.error("Command failed with reason: %s", vcsec.nominalError.genericError)
@@ -309,9 +362,13 @@ class Commands(Vehicle):
309
362
  "reason": GenericError_E.Name(vcsec.nominalError.genericError)
310
363
  }
311
364
  }
312
- elif vcsec.commandStatus.operationStatus == OPERATIONSTATUS_OK:
365
+ elif vcsec.HasField("vehicleStatus"):
366
+ return {
367
+ "response": vcsec.vehicleStatus
368
+ }
369
+ elif vcsec.commandStatus.operationStatus == OperationStatus_E.OPERATIONSTATUS_OK:
313
370
  return {"response": {"result": True, "reason": ""}}
314
- elif vcsec.commandStatus.operationStatus == OPERATIONSTATUS_WAIT:
371
+ elif vcsec.commandStatus.operationStatus == OperationStatus_E.OPERATIONSTATUS_WAIT:
315
372
  attempt += 1
316
373
  if attempt > 3:
317
374
  # We tried 3 times, give up, raise the error
@@ -319,29 +376,37 @@ class Commands(Vehicle):
319
376
  async with session.lock:
320
377
  await sleep(2)
321
378
  return await self._command(domain, command, attempt)
322
- elif vcsec.commandStatus.operationStatus == OPERATIONSTATUS_ERROR:
379
+ elif vcsec.commandStatus.operationStatus == OperationStatus_E.OPERATIONSTATUS_ERROR:
323
380
  if(resp.HasField("signedMessageStatus")):
324
381
  raise SIGNED_MESSAGE_INFORMATION_FAULTS[vcsec.commandStatus.signedMessageStatus.signedMessageInformation]
325
382
 
326
- elif(resp.from_destination.domain == DOMAIN_INFOTAINMENT):
327
- response = Response.FromString(resp.protobuf_message_as_bytes)
383
+ elif(resp.from_destination.domain == Domain.DOMAIN_INFOTAINMENT):
384
+ try:
385
+ response = Response.FromString(resp.protobuf_message_as_bytes)
386
+ except Exception as e:
387
+ LOGGER.error("Failed to parse Infotainment Response: %s %s", e, resp)
388
+ raise e
328
389
  LOGGER.debug("Infotainment Response: %s", response)
329
390
  if (response.HasField("ping")):
330
- print(response.ping)
331
391
  return {
332
392
  "response": {
333
393
  "result": True,
334
394
  "reason": response.ping.local_timestamp
335
395
  }
336
396
  }
397
+ if response.HasField("vehicleData"):
398
+ return {
399
+ "response": response.vehicleData
400
+ }
337
401
  if response.HasField("actionStatus"):
338
402
  return {
339
403
  "response": {
340
- "result": response.actionStatus.result == OPERATIONSTATUS_OK,
404
+ "result": response.actionStatus.result == OperationStatus_E.OPERATIONSTATUS_OK,
341
405
  "reason": response.actionStatus.result_reason.plain_text or ""
342
406
  }
343
407
  }
344
408
 
409
+
345
410
  return {"response": {"result": True, "reason": ""}}
346
411
 
347
412
  async def _commandHmac(self, session: Session, command: bytes, attempt: int = 1) -> RoutableMessage:
@@ -351,25 +416,25 @@ class Commands(Vehicle):
351
416
  hmac_personalized = session.hmac_personalized()
352
417
 
353
418
  metadata = bytes([
354
- TAG_SIGNATURE_TYPE,
419
+ Tag.TAG_SIGNATURE_TYPE,
355
420
  1,
356
- SIGNATURE_TYPE_HMAC_PERSONALIZED,
357
- TAG_DOMAIN,
421
+ SignatureType.SIGNATURE_TYPE_HMAC_PERSONALIZED,
422
+ Tag.TAG_DOMAIN,
358
423
  1,
359
424
  session.domain,
360
- TAG_PERSONALIZATION,
425
+ Tag.TAG_PERSONALIZATION,
361
426
  17,
362
427
  *self.vin.encode(),
363
- TAG_EPOCH,
428
+ Tag.TAG_EPOCH,
364
429
  len(hmac_personalized.epoch),
365
430
  *hmac_personalized.epoch,
366
- TAG_EXPIRES_AT,
431
+ Tag.TAG_EXPIRES_AT,
367
432
  4,
368
433
  *struct.pack(">I", hmac_personalized.expires_at),
369
- TAG_COUNTER,
434
+ Tag.TAG_COUNTER,
370
435
  4,
371
436
  *struct.pack(">I", hmac_personalized.counter),
372
- TAG_END,
437
+ Tag.TAG_END,
373
438
  ])
374
439
 
375
440
  hmac_personalized.tag = hmac.new(
@@ -398,27 +463,31 @@ class Commands(Vehicle):
398
463
  LOGGER.debug(f"Sending AES to domain {Domain.Name(session.domain)}")
399
464
 
400
465
  aes_personalized = session.aes_gcm_personalized()
466
+ flags = 1 << Flags.FLAG_ENCRYPT_RESPONSE
401
467
 
402
468
  metadata = bytes([
403
- TAG_SIGNATURE_TYPE,
469
+ Tag.TAG_SIGNATURE_TYPE,
404
470
  1,
405
- SIGNATURE_TYPE_AES_GCM_PERSONALIZED,
406
- TAG_DOMAIN,
471
+ SignatureType.SIGNATURE_TYPE_AES_GCM_PERSONALIZED,
472
+ Tag.TAG_DOMAIN,
407
473
  1,
408
474
  session.domain,
409
- TAG_PERSONALIZATION,
475
+ Tag.TAG_PERSONALIZATION,
410
476
  17,
411
477
  *self.vin.encode(),
412
- TAG_EPOCH,
478
+ Tag.TAG_EPOCH,
413
479
  len(aes_personalized.epoch),
414
480
  *aes_personalized.epoch,
415
- TAG_EXPIRES_AT,
481
+ Tag.TAG_EXPIRES_AT,
416
482
  4,
417
483
  *struct.pack(">I", aes_personalized.expires_at),
418
- TAG_COUNTER,
484
+ Tag.TAG_COUNTER,
419
485
  4,
420
486
  *struct.pack(">I", aes_personalized.counter),
421
- TAG_END,
487
+ Tag.TAG_FLAGS,
488
+ 4,
489
+ *struct.pack(">I", flags),
490
+ Tag.TAG_END,
422
491
  ])
423
492
 
424
493
  aad = Hash(SHA256())
@@ -429,7 +498,6 @@ class Commands(Vehicle):
429
498
 
430
499
  aes_personalized.tag = ct[-16:]
431
500
 
432
- # I think this whole section could be improved
433
501
  return RoutableMessage(
434
502
  to_destination=Destination(
435
503
  domain=session.domain,
@@ -444,7 +512,8 @@ class Commands(Vehicle):
444
512
  public_key=self._public_key
445
513
  ),
446
514
  AES_GCM_Personalized_data=aes_personalized,
447
- )
515
+ ),
516
+ flags=flags,
448
517
  )
449
518
 
450
519
 
@@ -452,10 +521,20 @@ class Commands(Vehicle):
452
521
  """Sign and send a message to Infotainment computer."""
453
522
  return await self._command(Domain.DOMAIN_VEHICLE_SECURITY, command.SerializeToString())
454
523
 
524
+ async def _getVehicleSecurity(self, command: UnsignedMessage) -> VehicleStatus:
525
+ """Sign and send a message to Infotainment computer."""
526
+ reply = await self._command(Domain.DOMAIN_VEHICLE_SECURITY, command.SerializeToString())
527
+ return reply["response"]
528
+
455
529
  async def _sendInfotainment(self, command: Action) -> dict[str, Any]:
456
530
  """Sign and send a message to Infotainment computer."""
457
531
  return await self._command(Domain.DOMAIN_INFOTAINMENT, command.SerializeToString())
458
532
 
533
+ async def _getInfotainment(self, command: Action) -> VehicleData:
534
+ """Sign and send a message to Infotainment computer."""
535
+ reply = await self._command(Domain.DOMAIN_INFOTAINMENT, command.SerializeToString())
536
+ return reply["response"]
537
+
459
538
  async def handshakeVehicleSecurity(self) -> None:
460
539
  """Perform a handshake with the vehicle security domain."""
461
540
  await self._handshake(Domain.DOMAIN_VEHICLE_SECURITY)
@@ -464,7 +543,7 @@ class Commands(Vehicle):
464
543
  """Perform a handshake with the infotainment domain."""
465
544
  await self._handshake(Domain.DOMAIN_INFOTAINMENT)
466
545
 
467
- async def _handshake(self, domain: Domain) -> None:
546
+ async def _handshake(self, domain: Domain) -> bool:
468
547
  """Perform a handshake with the vehicle."""
469
548
 
470
549
  LOGGER.debug(f"Handshake with domain {Domain.Name(domain)}")
@@ -481,7 +560,8 @@ class Commands(Vehicle):
481
560
  uuid=randbytes(16)
482
561
  )
483
562
 
484
- await self._send(msg)
563
+ await self._send(msg, "session_info")
564
+ return self._sessions[domain].ready
485
565
 
486
566
  async def ping(self) -> dict[str, Any]:
487
567
  """Ping the vehicle."""
@@ -7,8 +7,10 @@ from tesla_fleet_api.tesla.vehicle.fleet import VehicleFleet
7
7
  from tesla_fleet_api.tesla.vehicle.commands import Commands
8
8
  from tesla_fleet_api.exceptions import (
9
9
  MESSAGE_FAULTS,
10
+ NotOnWhitelistFault,
10
11
  )
11
12
  from tesla_fleet_api.tesla.vehicle.proto.signatures_pb2 import (
13
+ Session_Info_Status,
12
14
  SessionInfo,
13
15
  )
14
16
  from tesla_fleet_api.tesla.vehicle.proto.universal_message_pb2 import (
@@ -31,25 +33,17 @@ class VehicleSigned(VehicleFleet, Commands):
31
33
  super(Commands, self).__init__(parent, vin)
32
34
 
33
35
 
34
- async def _send(self, msg: RoutableMessage) -> RoutableMessage:
36
+ async def _send(self, msg: RoutableMessage, requires: str) -> RoutableMessage:
35
37
  """Serialize a message and send to the signed command endpoint."""
38
+ # requires isnt used because Fleet API messages are singular
36
39
 
37
40
  async with self._sessions[msg.to_destination.domain].lock:
38
- resp = await self.signed_command(
41
+ json = await self.signed_command(
39
42
  base64.b64encode(msg.SerializeToString()).decode()
40
43
  )
41
44
 
42
- resp_msg = RoutableMessage.FromString(base64.b64decode(resp["response"]))
45
+ resp = RoutableMessage.FromString(base64.b64decode(json["response"]))
43
46
 
44
- # Check UUID?
45
- # Check RoutingAdress?
47
+ self.validate_msg(resp)
46
48
 
47
- if resp_msg.session_info:
48
- self._sessions[resp_msg.from_destination.domain].update(
49
- SessionInfo.FromString(resp_msg.session_info), self.private_key
50
- )
51
-
52
- if resp_msg.signedMessageStatus.signed_message_fault:
53
- raise MESSAGE_FAULTS[resp_msg.signedMessageStatus.signed_message_fault]
54
-
55
- return resp_msg
49
+ return resp
@@ -31,7 +31,7 @@ class Vehicles(dict[str, Vehicle]):
31
31
  self[vin] = vehicle
32
32
  return vehicle
33
33
 
34
- def createBluetooth(self, vin: str) -> VehicleBluetooth:
34
+ def createBluetooth(self, vin: str):
35
35
  """Creates a bluetooth vehicle that uses command protocol."""
36
36
  vehicle = VehicleBluetooth(self._parent, vin)
37
37
  self[vin] = vehicle
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tesla_fleet_api
3
- Version: 1.0.2
3
+ Version: 1.0.4
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
@@ -18,6 +18,7 @@ Requires-Dist: aiolimiter
18
18
  Requires-Dist: cryptography
19
19
  Requires-Dist: protobuf
20
20
  Requires-Dist: bleak
21
+ Requires-Dist: bleak-retry-connector
21
22
  Dynamic: author
22
23
  Dynamic: author-email
23
24
  Dynamic: classifier
@@ -29,10 +30,11 @@ Dynamic: requires-python
29
30
  Dynamic: summary
30
31
 
31
32
  # Tesla Fleet Api
32
- Python library for Tesla Fleet API and Teslemetry.
33
+ Python library for Tesla Fleet API and Tesla Command Protocol, including signed commands and encrypted local Bluetooth (BLE). Also provides interfaces for Teslemetry and Tessie.
33
34
 
34
- Based on [Tesla Developer documentation](https://developer.tesla.com/docs/fleet-api).
35
+ Based on [Tesla Developer documentation](https://developer.tesla.com/docs/fleet-api) and [Tesla Command Protocol](https://github.com/teslamotors/vehicle-command/blob/main/pkg/protocol/protocol.md)
35
36
 
37
+ **Documentation is currently outdated for V1.0.X**
36
38
 
37
39
  ## TeslaFleetApi
38
40
  This is the base class, however can also be used directly if you have a valid user access_token.
@@ -1,23 +1,23 @@
1
1
  tesla_fleet_api/__init__.py,sha256=3DZMoZ-5srW-7SooAjqcRubQDuZPY8rMKH7eqIp4qtg,392
2
- tesla_fleet_api/const.py,sha256=GZFCZXq6-dRlECb7auojMGg0y03zg-Dt_zfeQ6aT8oY,3158
3
- tesla_fleet_api/exceptions.py,sha256=c-Was9VCfyiVG3H5XwygTBVXJ4Ib8H1WoPe3srXWRTk,34633
2
+ tesla_fleet_api/const.py,sha256=HK5PibaarH3CjrtN2Ma-xytZyhkpTdlbQ8rpVbL9DDo,3748
3
+ tesla_fleet_api/exceptions.py,sha256=1EkMnDxQYxZBn4ApjGodIBZyNPu9oa_Pgpdl6lQQ5gk,35523
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
6
- tesla_fleet_api/tesla/bluetooth.py,sha256=l2HOew_iCOw5RRr190NQasz080NYiea47fgjQIh_kGs,1271
6
+ tesla_fleet_api/tesla/bluetooth.py,sha256=WhDo66OEwCGhvsGskvWcGUiNv6eyFsdEiQdIm571jQM,1835
7
7
  tesla_fleet_api/tesla/charging.py,sha256=D7I7cAf-3-95sIjyP6wpVqCq9Cppj6U-VPFQGpQQ8bs,1704
8
8
  tesla_fleet_api/tesla/energysite.py,sha256=96Q5npsJ2YIa257o_NL5_3gJNUS-ioAL7sTeQeGPgAM,6110
9
9
  tesla_fleet_api/tesla/fleet.py,sha256=X74tzwGO9w65j9YUGuW04CwG7Bx6biEHwxIjWGCzB4c,5670
10
10
  tesla_fleet_api/tesla/oauth.py,sha256=aWBsWmnM-QxzaU8W9TXVNxGsYn_LraXnpexwdE8wOqo,4104
11
- tesla_fleet_api/tesla/partner.py,sha256=TU3Xg18x2w3PHv6Dy3Mo40pb417pp5lqnF0c1vDCt6g,1224
11
+ tesla_fleet_api/tesla/partner.py,sha256=e-l6sEP6-IupjFEQieSUjhhvRXF3aL4ebPNahcGFRCE,1238
12
12
  tesla_fleet_api/tesla/tesla.py,sha256=Jlz90-fM0nJbhnQN0k3ukNv59-9KqZZbyQ91IiLIbfo,2010
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=BQ5r7Fpogs8_zL9EBAq3XW--AbuDh1eUmTYm-wJok0M,8735
16
- tesla_fleet_api/tesla/vehicle/commands.py,sha256=RBK_tQmrtjIAUjTCmGyRE5x0U1UeFJRHVmlG2y9u2PA,47191
15
+ tesla_fleet_api/tesla/vehicle/bluetooth.py,sha256=tEbuUMjG3PfDLc1s1ACg-3zdnF-lSwDsH1U5tg5qbRI,15746
16
+ tesla_fleet_api/tesla/vehicle/commands.py,sha256=zgfc2yo1UxEh8ePqSb-4h0UTK0RmpCG_9LZX44CES2c,51268
17
17
  tesla_fleet_api/tesla/vehicle/fleet.py,sha256=K9BVZj6CChJSDSMFroa7Cz0KrsYWj32ILtQumarkLaU,32080
18
- tesla_fleet_api/tesla/vehicle/signed.py,sha256=OPQy_LHxTvRcY8bAHrzRLPlN_vYOupdr_TEmHY9PGuQ,1807
18
+ tesla_fleet_api/tesla/vehicle/signed.py,sha256=ggdtq8PydKoCk044L5b2262nfNzZqPBnsJ4SonTFbb4,1539
19
19
  tesla_fleet_api/tesla/vehicle/vehicle.py,sha256=TyW5-LRlgRulWsm2indE3utSTdrJJRfG7H45Cc-ZASk,505
20
- tesla_fleet_api/tesla/vehicle/vehicles.py,sha256=kQWOlvpseDqs2c8Co3Elo4RPeoHbK7AeSyX-t4Q5MDM,1585
20
+ tesla_fleet_api/tesla/vehicle/vehicles.py,sha256=wyMLfHNK_QaNTDtU9mkGRl2fB3Gb6lvmSfcgXzKu7WY,1565
21
21
  tesla_fleet_api/tesla/vehicle/proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  tesla_fleet_api/tesla/vehicle/proto/__init__.pyi,sha256=qFXWNIgl71wB260u-XPzaAwWAHL6krw21q-aXnBtop0,252
23
23
  tesla_fleet_api/tesla/vehicle/proto/car_server_pb2.py,sha256=v_eb4NDIkx_ZYPYW29_EFRky5vQ4b2q14gwuQSbouYw,29202
@@ -44,8 +44,8 @@ tesla_fleet_api/teslemetry/vehicle.py,sha256=9_2N1iNNDouqfb6YBBWAFjnlzVRTf5frhXi
44
44
  tesla_fleet_api/tessie/__init__.py,sha256=9lhQJaB6X4PObUL9QdaaZYqs2BxiTidu3zmHcBESLVw,78
45
45
  tesla_fleet_api/tessie/tessie.py,sha256=qdMZ61TcQi5JRuv2qaxuLHtOuy8WZJ1WNqWg5WDAwwU,2615
46
46
  tesla_fleet_api/tessie/vehicle.py,sha256=9khv4oCkGGLxHzQ2FYhDPH7wczxEDiUppsDXalawarE,1125
47
- tesla_fleet_api-1.0.2.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
48
- tesla_fleet_api-1.0.2.dist-info/METADATA,sha256=vxPX8yS0x5TVB11oyZeKQQV3NIVd3ZRF_nYSGIDgEhk,4056
49
- tesla_fleet_api-1.0.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
50
- tesla_fleet_api-1.0.2.dist-info/top_level.txt,sha256=jeNbog_1saXBFrGpom9WyPWmilxsyP3szL_G7JLWQfM,16
51
- tesla_fleet_api-1.0.2.dist-info/RECORD,,
47
+ tesla_fleet_api-1.0.4.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
48
+ tesla_fleet_api-1.0.4.dist-info/METADATA,sha256=KxQg4FAxCoWBheKhRSo104TIDTtcswTz-LhgFN7UAUo,4382
49
+ tesla_fleet_api-1.0.4.dist-info/WHEEL,sha256=nn6H5-ilmfVryoAQl3ZQ2l8SH5imPWFpm1A5FgEuFV4,91
50
+ tesla_fleet_api-1.0.4.dist-info/top_level.txt,sha256=jeNbog_1saXBFrGpom9WyPWmilxsyP3szL_G7JLWQfM,16
51
+ tesla_fleet_api-1.0.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (75.8.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5