tesla-fleet-api 1.0.2__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.
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.3"
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,8 @@
2
2
 
3
3
  import hashlib
4
4
  import re
5
+ from bleak.backends.device import BLEDevice
6
+ from cryptography.hazmat.primitives.asymmetric import ec
5
7
 
6
8
  from tesla_fleet_api.tesla.tesla import Tesla
7
9
  from tesla_fleet_api.tesla.vehicle.bluetooth import VehicleBluetooth
@@ -36,8 +38,8 @@ class Vehicles(dict[str, VehicleBluetooth]):
36
38
  """Creates a specific vehicle."""
37
39
  return self.createBluetooth(vin)
38
40
 
39
- def createBluetooth(self, vin: str) -> VehicleBluetooth:
41
+ def createBluetooth(self, vin: str, key: ec.EllipticCurvePrivateKey | None = None, device: None | str | BLEDevice = None) -> VehicleBluetooth:
40
42
  """Creates a specific vehicle."""
41
- vehicle = VehicleBluetooth(self._parent, vin)
43
+ vehicle = VehicleBluetooth(self._parent, vin, key, device)
42
44
  self[vin] = vehicle
43
45
  return vehicle
@@ -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,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
 
@@ -16,19 +16,37 @@ 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
22
  MESSAGE_FAULTS,
22
23
  WHITELIST_OPERATION_STATUS,
23
24
  WhitelistOperationStatus,
25
+ NotOnWhitelistFault,
24
26
  )
25
27
 
26
28
  # Protocol
27
29
  from tesla_fleet_api.tesla.vehicle.proto.car_server_pb2 import (
30
+ Action,
31
+ VehicleAction,
28
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,
29
46
  )
30
47
  from tesla_fleet_api.tesla.vehicle.proto.signatures_pb2 import (
31
48
  SessionInfo,
49
+ Session_Info_Status
32
50
  )
33
51
  from tesla_fleet_api.tesla.vehicle.proto.universal_message_pb2 import (
34
52
  Destination,
@@ -37,15 +55,18 @@ from tesla_fleet_api.tesla.vehicle.proto.universal_message_pb2 import (
37
55
  )
38
56
  from tesla_fleet_api.tesla.vehicle.proto.vcsec_pb2 import (
39
57
  FromVCSECMessage,
58
+ InformationRequest,
59
+ InformationRequestType,
40
60
  KeyFormFactor,
41
61
  KeyMetadata,
42
62
  PermissionChange,
43
63
  PublicKey,
44
64
  RKEAction_E,
45
65
  UnsignedMessage,
66
+ VehicleStatus,
46
67
  WhitelistOperation,
47
-
48
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
49
70
 
50
71
  SERVICE_UUID = "00000211-b2d1-43f0-9b88-960cebf8b91e"
51
72
  WRITE_UUID = "00000212-b2d1-43f0-9b88-960cebf8b91e"
@@ -64,19 +85,23 @@ class VehicleBluetooth(Commands):
64
85
 
65
86
  ble_name: str
66
87
  client: BleakClient
67
- _device: BLEDevice
68
- _futures: dict[Domain, Future]
88
+ _queues: dict[Domain, asyncio.Queue]
69
89
  _ekey: ec.EllipticCurvePublicKey
70
90
  _recv: bytearray = bytearray()
71
91
  _recv_len: int = 0
72
92
  _auth_method = "aes"
73
93
 
74
94
  def __init__(
75
- 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
76
96
  ):
77
97
  super().__init__(parent, vin, key)
78
98
  self.ble_name = "S" + hashlib.sha1(vin.encode('utf-8')).hexdigest()[:16] + "C"
79
- self._futures = {}
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])
80
105
 
81
106
  async def find_client(self, scanner: BleakScanner = BleakScanner()) -> BleakClient:
82
107
  """Find the Tesla BLE device."""
@@ -84,20 +109,19 @@ class VehicleBluetooth(Commands):
84
109
  device = await scanner.find_device_by_name(self.ble_name)
85
110
  if not device:
86
111
  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}")
112
+ self.client = BleakClient(device, services=[SERVICE_UUID])
113
+ LOGGER.debug(f"Discovered device {device.name} {device.address}")
90
114
  return self.client
91
115
 
92
- def create_client(self, mac:str) -> BleakClient:
93
- """Create a client using a MAC address."""
94
- 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])
95
119
  return self.client
96
120
 
97
- async def connect(self, mac:str | None = None) -> None:
121
+ async def connect(self, device: str|BLEDevice | None = None) -> None:
98
122
  """Connect to the Tesla BLE device."""
99
- if mac is not None:
100
- self.create_client(mac)
123
+ if device is not None:
124
+ self.create_client(device)
101
125
  await self.client.connect()
102
126
  await self.client.start_notify(READ_UUID, self._on_notify)
103
127
 
@@ -114,7 +138,7 @@ class VehicleBluetooth(Commands):
114
138
  """Exit the async context."""
115
139
  await self.disconnect()
116
140
 
117
- def _on_notify(self,sender: BleakGATTCharacteristic,data : bytearray) -> None:
141
+ async def _on_notify(self,sender: BleakGATTCharacteristic,data : bytearray) -> None:
118
142
  """Receive data from the Tesla BLE device."""
119
143
  if self._recv_len:
120
144
  self._recv += data
@@ -124,68 +148,55 @@ class VehicleBluetooth(Commands):
124
148
  LOGGER.debug(f"Received {len(self._recv)} of {self._recv_len} bytes")
125
149
  while len(self._recv) > self._recv_len:
126
150
  LOGGER.warn(f"Received more data than expected: {len(self._recv)} > {self._recv_len}")
127
- self._on_message(bytes(self._recv[:self._recv_len]))
151
+ await self._on_message(bytes(self._recv[:self._recv_len]))
128
152
  self._recv_len = int.from_bytes(self._recv[self._recv_len:self._recv_len+2], 'big')
129
153
  self._recv = self._recv[self._recv_len+2:]
130
154
  continue
131
155
  if len(self._recv) == self._recv_len:
132
- self._on_message(bytes(self._recv))
156
+ await self._on_message(bytes(self._recv))
133
157
  self._recv = bytearray()
134
158
  self._recv_len = 0
135
159
 
136
- def _on_message(self, data:bytes) -> None:
160
+ async def _on_message(self, data:bytes) -> None:
137
161
  """Receive messages from the Tesla BLE data."""
138
162
  try:
139
163
  msg = RoutableMessage.FromString(data)
140
164
  except DecodeError as e:
141
165
  LOGGER.error(f"Error parsing message: {e}")
166
+ self._recv = bytearray()
167
+ self._recv_len = 0
142
168
  return
143
169
 
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
170
  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)
171
+ # Ignore ephemeral key broadcasts
157
172
  return
158
173
 
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}")
174
+ LOGGER.info(f"Received response: {msg}")
175
+ await self._queues[msg.from_destination.domain].put(msg)
165
176
 
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:
177
+ async def _send(self, msg: RoutableMessage, requires: str) -> RoutableMessage:
173
178
  """Serialize a message and send to the vehicle and wait for a response."""
174
179
  domain = msg.to_destination.domain
175
180
  async with self._sessions[domain].lock:
176
181
  LOGGER.debug(f"Sending message {msg}")
177
- future = await self._create_future(domain)
182
+
178
183
  payload = prependLength(msg.SerializeToString())
179
184
 
185
+ # Empty the queue before sending the message
186
+ while not self._queues[domain].empty():
187
+ await self._queues[domain].get()
180
188
  await self.client.write_gatt_char(WRITE_UUID, payload, True)
181
189
 
182
- resp = await future
183
- 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}")
184
195
 
185
- if resp.signedMessageStatus.signed_message_fault:
186
- raise MESSAGE_FAULTS[resp.signedMessageStatus.signed_message_fault]
196
+ self.validate_msg(resp)
187
197
 
188
- return resp
198
+ if resp.HasField(requires):
199
+ return resp
189
200
 
190
201
  async def pair(self, role: Role = Role.ROLE_OWNER, form: KeyFormFactor = KeyFormFactor.KEY_FORM_FACTOR_CLOUD_KEY):
191
202
  """Pair the key."""
@@ -224,3 +235,167 @@ class VehicleBluetooth(Commands):
224
235
  return await self._sendVehicleSecurity(
225
236
  UnsignedMessage(RKEAction=RKEAction_E.RKE_ACTION_WAKE_VEHICLE)
226
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
+ )
@@ -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.3
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,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=RB2ZgI0_y0DkLUJtMSpklCRH9aNlrninO4zot5cA-H0,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=UCKXF3u_TfuGlJ-LYEIxgmbbzlElZfuJOInsWJ_ltr0,1471
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=fhV24gni_tkhRJmK5I-xUX0dqxEw_lumRViEwmkXDSw,15665
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.3.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
48
+ tesla_fleet_api-1.0.3.dist-info/METADATA,sha256=ARBm4GR744Ru7F2JumVFrw4UgeSxnYeV1k25KlzgqhY,4056
49
+ tesla_fleet_api-1.0.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
50
+ tesla_fleet_api-1.0.3.dist-info/top_level.txt,sha256=jeNbog_1saXBFrGpom9WyPWmilxsyP3szL_G7JLWQfM,16
51
+ tesla_fleet_api-1.0.3.dist-info/RECORD,,