tesla-fleet-api 1.0.9__py3-none-any.whl → 1.0.11__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.9"
6
+ VERSION = "1.0.11"
7
7
  LOGGER = logging.getLogger(__package__)
8
8
  SERVERS = {
9
9
  "na": "https://fleet-api.prd.na.vn.cloud.tesla.com",
@@ -1,10 +1,16 @@
1
1
  """Bluetooth only interface."""
2
2
 
3
+ import asyncio
3
4
  import hashlib
4
5
  import re
6
+ from bleak import BleakClient
7
+ from bleak.backends.device import BLEDevice
8
+ from bleak_retry_connector import establish_connection
5
9
  from google.protobuf.json_format import MessageToJson, MessageToDict
6
10
 
11
+ from tesla_fleet_api.const import LOGGER
7
12
  from tesla_fleet_api.tesla.tesla import Tesla
13
+ from tesla_fleet_api.tesla.vehicle.bluetooth import NAME_UUID
8
14
  from tesla_fleet_api.tesla.vehicle.vehicles import VehiclesBluetooth
9
15
 
10
16
  class TeslaBluetooth(Tesla):
@@ -27,6 +33,30 @@ class TeslaBluetooth(Tesla):
27
33
  """Get the name of a vehicle."""
28
34
  return "S" + hashlib.sha1(vin.encode('utf-8')).hexdigest()[:16] + "C"
29
35
 
36
+ async def query_display_name(self, device: BLEDevice, max_attempts=5) -> str | None:
37
+ """Queries the name of a bluetooth vehicle."""
38
+ client = await establish_connection(
39
+ BleakClient,
40
+ device,
41
+ device.name or "Unknown",
42
+ max_attempts=max_attempts
43
+ )
44
+ name: str | None = None
45
+ for i in range(max_attempts):
46
+ try:
47
+ # Standard GATT Device Name characteristic (0x2A00)
48
+ device_name = (await client.read_gatt_char(NAME_UUID)).decode('utf-8')
49
+ if device_name.startswith("🔑 "):
50
+ name = device_name.replace("🔑 ","")
51
+ break
52
+ await asyncio.sleep(1)
53
+ LOGGER.debug(f"Attempt {i+1} to query display name failed, {device_name}")
54
+ except Exception as e:
55
+ LOGGER.error(f"Failed to read device name: {e}")
56
+
57
+ await client.disconnect()
58
+ return name
59
+
30
60
 
31
61
  # Helpers
32
62
 
@@ -65,6 +65,8 @@ SERVICE_UUID = "00000211-b2d1-43f0-9b88-960cebf8b91e"
65
65
  WRITE_UUID = "00000212-b2d1-43f0-9b88-960cebf8b91e"
66
66
  READ_UUID = "00000213-b2d1-43f0-9b88-960cebf8b91e"
67
67
  VERSION_UUID = "00000214-b2d1-43f0-9b88-960cebf8b91e"
68
+ NAME_UUID = "00002a00-0000-1000-8000-00805f9b34fb"
69
+ APPEARANCE_UUID = "00002a01-0000-1000-8000-00805f9b34fb"
68
70
 
69
71
  if TYPE_CHECKING:
70
72
  from tesla_fleet_api.tesla.tesla import Tesla
@@ -77,8 +79,8 @@ class VehicleBluetooth(Commands):
77
79
  """Class describing the Tesla Fleet API vehicle endpoints and commands for a specific vehicle with command signing."""
78
80
 
79
81
  ble_name: str
80
- device: BLEDevice
81
- client: BleakClient
82
+ device: BLEDevice | None = None
83
+ client: BleakClient | None = None
82
84
  _queues: dict[Domain, asyncio.Queue]
83
85
  _ekey: ec.EllipticCurvePublicKey
84
86
  _recv: bytearray = bytearray()
@@ -94,8 +96,8 @@ class VehicleBluetooth(Commands):
94
96
  Domain.DOMAIN_VEHICLE_SECURITY: asyncio.Queue(),
95
97
  Domain.DOMAIN_INFOTAINMENT: asyncio.Queue(),
96
98
  }
97
- if device is not None:
98
- self.device = device
99
+ self.device = device
100
+ self._connect_lock = asyncio.Lock()
99
101
 
100
102
  async def find_vehicle(self, name: str | None = None, address: str | None = None, scanner: BleakScanner | None = None) -> BLEDevice:
101
103
  """Find the Tesla BLE device."""
@@ -117,12 +119,12 @@ class VehicleBluetooth(Commands):
117
119
  def set_device(self, device: BLEDevice) -> None:
118
120
  self.device = device
119
121
 
120
- def get_device(self) -> BLEDevice:
122
+ def get_device(self) -> BLEDevice | None:
121
123
  return self.device
122
124
 
123
125
  async def connect(self, max_attempts: int = MAX_CONNECT_ATTEMPTS) -> None:
124
126
  """Connect to the Tesla BLE device."""
125
- if not hasattr(self, 'device'):
127
+ if not self.device:
126
128
  raise ValueError(f"BLEDevice {self.ble_name} has not been found or set")
127
129
  self.client = await establish_connection(
128
130
  BleakClient,
@@ -136,8 +138,16 @@ class VehicleBluetooth(Commands):
136
138
 
137
139
  async def disconnect(self) -> bool:
138
140
  """Disconnect from the Tesla BLE device."""
141
+ if not self.client:
142
+ return False
139
143
  return await self.client.disconnect()
140
144
 
145
+ async def connect_if_needed(self, max_attempts: int = MAX_CONNECT_ATTEMPTS) -> None:
146
+ """Connect to the Tesla BLE device if not already connected."""
147
+ async with self._connect_lock:
148
+ if not self.client or not self.client.is_connected:
149
+ await self.connect(max_attempts=max_attempts)
150
+
141
151
  async def __aenter__(self) -> VehicleBluetooth:
142
152
  """Enter the async context."""
143
153
  await self.connect()
@@ -194,6 +204,7 @@ class VehicleBluetooth(Commands):
194
204
 
195
205
  async def _send(self, msg: RoutableMessage, requires: str, timeout: int = 2) -> RoutableMessage:
196
206
  """Serialize a message and send to the vehicle and wait for a response."""
207
+
197
208
  domain = msg.to_destination.domain
198
209
  async with self._sessions[domain].lock:
199
210
  LOGGER.debug(f"Sending message {msg}")
@@ -203,6 +214,9 @@ class VehicleBluetooth(Commands):
203
214
  # Empty the queue before sending the message
204
215
  while not self._queues[domain].empty():
205
216
  await self._queues[domain].get()
217
+
218
+ await self.connect_if_needed()
219
+ assert self.client is not None
206
220
  await self.client.write_gatt_char(WRITE_UUID, payload, True)
207
221
 
208
222
  # Process the response
@@ -216,6 +230,45 @@ class VehicleBluetooth(Commands):
216
230
  if resp.HasField(requires):
217
231
  return resp
218
232
 
233
+ async def query_display_name(self, max_attempts=5) -> str | None:
234
+ """Read the device name via GATT characteristic if available"""
235
+ for i in range(max_attempts):
236
+ try:
237
+ # Standard GATT Device Name characteristic (0x2A00)
238
+ await self.connect_if_needed()
239
+ assert self.client
240
+ device_name = (await self.client.read_gatt_char(NAME_UUID)).decode('utf-8')
241
+ if device_name.startswith("🔑 "):
242
+ return device_name.replace("🔑 ","")
243
+ await asyncio.sleep(1)
244
+ LOGGER.debug(f"Attempt {i+1} to query display name failed, {device_name}")
245
+ except Exception as e:
246
+ LOGGER.error(f"Failed to read device name: {e}")
247
+
248
+ async def query_appearance(self) -> bytearray | None:
249
+ """Read the device appearance via GATT characteristic if available"""
250
+ try:
251
+ # Standard GATT Appearance characteristic (0x2A01)
252
+ await self.connect_if_needed()
253
+ assert self.client
254
+ return await self.client.read_gatt_char(APPEARANCE_UUID)
255
+ except Exception as e:
256
+ LOGGER.error(f"Failed to read device appearance: {e}")
257
+ return None
258
+
259
+ async def query_version(self) -> int | None:
260
+ """Read the device version via GATT characteristic if available"""
261
+ try:
262
+ # Custom GATT Version characteristic (0x2A02)
263
+ await self.connect_if_needed()
264
+ assert self.client
265
+ device_version = await self.client.read_gatt_char(VERSION_UUID)
266
+ # Convert the bytes to an integer
267
+ if device_version and len(device_version) > 0:
268
+ return int.from_bytes(device_version, byteorder='big')
269
+ except Exception as e:
270
+ LOGGER.error(f"Failed to read device version: {e}")
271
+ return None
219
272
 
220
273
  async def pair(self, role: Role = Role.ROLE_OWNER, form: KeyFormFactor = KeyFormFactor.KEY_FORM_FACTOR_CLOUD_KEY, timeout: int = 60):
221
274
  """Pair the key."""
@@ -18,9 +18,11 @@ class Vehicle:
18
18
  """Base class describing a Tesla vehicle."""
19
19
 
20
20
  vin: str
21
+ parent: Tesla
21
22
 
22
23
  def __init__(self, parent: Tesla, vin: str):
23
24
  self.vin = vin
25
+ self.parent = parent
24
26
 
25
27
  @property
26
28
  def pre2021(self) -> bool:
@@ -1,12 +1,16 @@
1
1
  from __future__ import annotations
2
+ import asyncio
2
3
  from typing import TYPE_CHECKING
4
+ from bleak import BleakClient
3
5
  from bleak.backends.device import BLEDevice
6
+ from bleak_retry_connector import establish_connection
4
7
  from cryptography.hazmat.primitives.asymmetric import ec
5
8
 
6
9
  from tesla_fleet_api.tesla.vehicle.signed import VehicleSigned
7
- from tesla_fleet_api.tesla.vehicle.bluetooth import VehicleBluetooth
10
+ from tesla_fleet_api.tesla.vehicle.bluetooth import NAME_UUID, VehicleBluetooth
8
11
  from tesla_fleet_api.tesla.vehicle.fleet import VehicleFleet
9
12
  from tesla_fleet_api.tesla.vehicle.vehicle import Vehicle
13
+ from tesla_fleet_api.const import LOGGER
10
14
 
11
15
  if TYPE_CHECKING:
12
16
  from tesla_fleet_api.tesla.fleet import TeslaFleetApi
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tesla_fleet_api
3
- Version: 1.0.9
3
+ Version: 1.0.11
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,9 +1,9 @@
1
1
  tesla_fleet_api/__init__.py,sha256=3DZMoZ-5srW-7SooAjqcRubQDuZPY8rMKH7eqIp4qtg,392
2
- tesla_fleet_api/const.py,sha256=6avk1nS7GL_OVZaiC1Cmy0a-QoZ3LNoXTUjxFNKiPWA,3748
2
+ tesla_fleet_api/const.py,sha256=TSs1sofg_Dv2Q-NEcVOB10GNbF0cU25NcO7WooMLoOk,3749
3
3
  tesla_fleet_api/exceptions.py,sha256=GvjJBR77xGt2g3vhiAxcNtysj-e1Me7F-9PwO9dyzOo,35430
4
4
  tesla_fleet_api/ratecalculator.py,sha256=4lz8yruUeouHXh_3ezsXX-CTpIegp1T1J4VuRV_qdHA,1791
5
5
  tesla_fleet_api/tesla/__init__.py,sha256=Cvpqu8OaOFmbuwu9KjgYrje8eVluDp2IU_zwdtXbmO0,282
6
- tesla_fleet_api/tesla/bluetooth.py,sha256=vxDPTM-Uq9IAVJjK54-p_llj26hssEEXAQI5-lVGx8Q,1137
6
+ tesla_fleet_api/tesla/bluetooth.py,sha256=lyPRVf1YdcElrYBsKOMCaLwMPE9rO7Iw1a6nE7VUZ94,2369
7
7
  tesla_fleet_api/tesla/charging.py,sha256=D7I7cAf-3-95sIjyP6wpVqCq9Cppj6U-VPFQGpQQ8bs,1704
8
8
  tesla_fleet_api/tesla/energysite.py,sha256=vStffklBQfQNAO_1wrHLFu7BlBCTVVbLh7_IrAUL3wg,6131
9
9
  tesla_fleet_api/tesla/fleet.py,sha256=zfmagXF4TbbVOPWcngCSKebaGB1daXbw8mTJ9o8einY,5432
@@ -12,12 +12,12 @@ tesla_fleet_api/tesla/partner.py,sha256=e-l6sEP6-IupjFEQieSUjhhvRXF3aL4ebPNahcGF
12
12
  tesla_fleet_api/tesla/tesla.py,sha256=Gs8-L3OsEMhs1N_vdDx-E46bOHKGowXTBxmRiP8NKh4,2391
13
13
  tesla_fleet_api/tesla/user.py,sha256=w8rwiAOIFjuDus8M0RpZ0wucJtw8kYFKtJfYVk7Ekr0,1194
14
14
  tesla_fleet_api/tesla/vehicle/__init__.py,sha256=3A5_wTQHofRShof4pUNOtF78-7lUh62uz2jq2ecnmRY,381
15
- tesla_fleet_api/tesla/vehicle/bluetooth.py,sha256=SAzJL0GW8Gp5cCs_Gyc2qBJP9WVVwV_qwUFkQF8waTI,16561
15
+ tesla_fleet_api/tesla/vehicle/bluetooth.py,sha256=8SB3fDdJ1E8jDsaL6cix0Ks0lwlkvOLQ0_3lNZwWs1M,19064
16
16
  tesla_fleet_api/tesla/vehicle/commands.py,sha256=_LXAtuAIkeojJuw8k1QjEabAQQH2MLD5YZyUH58QoSg,51288
17
17
  tesla_fleet_api/tesla/vehicle/fleet.py,sha256=K9BVZj6CChJSDSMFroa7Cz0KrsYWj32ILtQumarkLaU,32080
18
18
  tesla_fleet_api/tesla/vehicle/signed.py,sha256=RUzVnZIfykz3YZW2gaxd1iaN1i8LkLaEoiXrbqZn9kg,1339
19
- tesla_fleet_api/tesla/vehicle/vehicle.py,sha256=BlmHjV5Hly8v0vWNVBUhdNvwsSSuCuF946D7zROUB3s,823
20
- tesla_fleet_api/tesla/vehicle/vehicles.py,sha256=c1KFTAYDvuTVNjM5O23KvT54UbNwo3ofFVpfkY-Mq5Y,2640
19
+ tesla_fleet_api/tesla/vehicle/vehicle.py,sha256=IBvRO6qGkUf4bi-pFongA9fIe9iScvz_wGXtzEJXilY,870
20
+ tesla_fleet_api/tesla/vehicle/vehicles.py,sha256=wU0wXa57BD7InaUC1xoRhVc7oI5CemsocSryuUItyGI,2792
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/vehicles.py,sha256=9nybVg7VHKLa2woMG6fzMmQP6xJIE_jdAd
44
44
  tesla_fleet_api/tessie/__init__.py,sha256=9lhQJaB6X4PObUL9QdaaZYqs2BxiTidu3zmHcBESLVw,78
45
45
  tesla_fleet_api/tessie/tessie.py,sha256=uhg0oOIxpwDvlvdBhKHeF3AGR2PzmdBgzh2-_EkmSq0,2617
46
46
  tesla_fleet_api/tessie/vehicles.py,sha256=gfEatilI_ct-R4CM5xYhrlduqCR9IHlyc56WmJf7v7k,1149
47
- tesla_fleet_api-1.0.9.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
48
- tesla_fleet_api-1.0.9.dist-info/METADATA,sha256=bDVsevE52Rb5wKU46C2oUVlDHMmMp9zClJl2KQlRpjk,4382
49
- tesla_fleet_api-1.0.9.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
50
- tesla_fleet_api-1.0.9.dist-info/top_level.txt,sha256=jeNbog_1saXBFrGpom9WyPWmilxsyP3szL_G7JLWQfM,16
51
- tesla_fleet_api-1.0.9.dist-info/RECORD,,
47
+ tesla_fleet_api-1.0.11.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
48
+ tesla_fleet_api-1.0.11.dist-info/METADATA,sha256=YYe4EpiOI6j7CcrDsOs0HsjKgYQs3YPwKL9wceXFNcU,4383
49
+ tesla_fleet_api-1.0.11.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
50
+ tesla_fleet_api-1.0.11.dist-info/top_level.txt,sha256=jeNbog_1saXBFrGpom9WyPWmilxsyP3szL_G7JLWQfM,16
51
+ tesla_fleet_api-1.0.11.dist-info/RECORD,,