Skip to content

Commit ae3cd2d

Browse files
committed
Reconnect as required
1 parent a91d03f commit ae3cd2d

File tree

3 files changed

+54
-30
lines changed

3 files changed

+54
-30
lines changed

tesla_fleet_api/tesla/bluetooth.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
"""Bluetooth only interface."""
22

3+
import asyncio
34
import hashlib
45
import re
6+
from bleak import BleakClient
7+
from bleak.backends.device import BLEDevice
8+
from bleak_retry_connector import establish_connection
59
from google.protobuf.json_format import MessageToJson, MessageToDict
610

11+
from tesla_fleet_api.const import LOGGER
712
from tesla_fleet_api.tesla.tesla import Tesla
13+
from tesla_fleet_api.tesla.vehicle.bluetooth import NAME_UUID
814
from tesla_fleet_api.tesla.vehicle.vehicles import VehiclesBluetooth
915

1016
class TeslaBluetooth(Tesla):
@@ -27,6 +33,30 @@ def get_name(self, vin: str) -> str:
2733
"""Get the name of a vehicle."""
2834
return "S" + hashlib.sha1(vin.encode('utf-8')).hexdigest()[:16] + "C"
2935

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+
3060

3161
# Helpers
3262

tesla_fleet_api/tesla/vehicle/bluetooth.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ class VehicleBluetooth(Commands):
7979
"""Class describing the Tesla Fleet API vehicle endpoints and commands for a specific vehicle with command signing."""
8080

8181
ble_name: str
82-
device: BLEDevice
83-
client: BleakClient
82+
device: BLEDevice | None = None
83+
client: BleakClient | None = None
8484
_queues: dict[Domain, asyncio.Queue]
8585
_ekey: ec.EllipticCurvePublicKey
8686
_recv: bytearray = bytearray()
@@ -96,8 +96,8 @@ def __init__(
9696
Domain.DOMAIN_VEHICLE_SECURITY: asyncio.Queue(),
9797
Domain.DOMAIN_INFOTAINMENT: asyncio.Queue(),
9898
}
99-
if device is not None:
100-
self.device = device
99+
self.device = device
100+
self._connect_lock = asyncio.Lock()
101101

102102
async def find_vehicle(self, name: str | None = None, address: str | None = None, scanner: BleakScanner | None = None) -> BLEDevice:
103103
"""Find the Tesla BLE device."""
@@ -119,12 +119,12 @@ async def find_vehicle(self, name: str | None = None, address: str | None = None
119119
def set_device(self, device: BLEDevice) -> None:
120120
self.device = device
121121

122-
def get_device(self) -> BLEDevice:
122+
def get_device(self) -> BLEDevice | None:
123123
return self.device
124124

125125
async def connect(self, max_attempts: int = MAX_CONNECT_ATTEMPTS) -> None:
126126
"""Connect to the Tesla BLE device."""
127-
if not hasattr(self, 'device'):
127+
if not self.device:
128128
raise ValueError(f"BLEDevice {self.ble_name} has not been found or set")
129129
self.client = await establish_connection(
130130
BleakClient,
@@ -138,8 +138,16 @@ async def connect(self, max_attempts: int = MAX_CONNECT_ATTEMPTS) -> None:
138138

139139
async def disconnect(self) -> bool:
140140
"""Disconnect from the Tesla BLE device."""
141+
if not self.client:
142+
return False
141143
return await self.client.disconnect()
142144

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+
143151
async def __aenter__(self) -> VehicleBluetooth:
144152
"""Enter the async context."""
145153
await self.connect()
@@ -196,6 +204,7 @@ async def _on_message(self, msg: RoutableMessage) -> None:
196204

197205
async def _send(self, msg: RoutableMessage, requires: str, timeout: int = 2) -> RoutableMessage:
198206
"""Serialize a message and send to the vehicle and wait for a response."""
207+
199208
domain = msg.to_destination.domain
200209
async with self._sessions[domain].lock:
201210
LOGGER.debug(f"Sending message {msg}")
@@ -205,6 +214,9 @@ async def _send(self, msg: RoutableMessage, requires: str, timeout: int = 2) ->
205214
# Empty the queue before sending the message
206215
while not self._queues[domain].empty():
207216
await self._queues[domain].get()
217+
218+
await self.connect_if_needed()
219+
assert self.client is not None
208220
await self.client.write_gatt_char(WRITE_UUID, payload, True)
209221

210222
# Process the response
@@ -223,6 +235,8 @@ async def query_display_name(self, max_attempts=5) -> str | None:
223235
for i in range(max_attempts):
224236
try:
225237
# Standard GATT Device Name characteristic (0x2A00)
238+
await self.connect_if_needed()
239+
assert self.client
226240
device_name = (await self.client.read_gatt_char(NAME_UUID)).decode('utf-8')
227241
if device_name.startswith("🔑 "):
228242
return device_name.replace("🔑 ","")
@@ -235,6 +249,8 @@ async def query_appearance(self) -> bytearray | None:
235249
"""Read the device appearance via GATT characteristic if available"""
236250
try:
237251
# Standard GATT Appearance characteristic (0x2A01)
252+
await self.connect_if_needed()
253+
assert self.client
238254
return await self.client.read_gatt_char(APPEARANCE_UUID)
239255
except Exception as e:
240256
LOGGER.error(f"Failed to read device appearance: {e}")
@@ -244,6 +260,8 @@ async def query_version(self) -> int | None:
244260
"""Read the device version via GATT characteristic if available"""
245261
try:
246262
# Custom GATT Version characteristic (0x2A02)
263+
await self.connect_if_needed()
264+
assert self.client
247265
device_version = await self.client.read_gatt_char(VERSION_UUID)
248266
# Convert the bytes to an integer
249267
if device_version and len(device_version) > 0:

tesla_fleet_api/tesla/vehicle/vehicles.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -73,27 +73,3 @@ def createBluetooth(self, vin: str, key: ec.EllipticCurvePrivateKey | None = Non
7373
vehicle = self.Bluetooth(self._parent, vin, key, device)
7474
self[vin] = vehicle
7575
return vehicle
76-
77-
async def query_display_name(self, device: BLEDevice, max_attempts=5) -> str | None:
78-
"""Queries the name of a bluetooth vehicle."""
79-
client = await establish_connection(
80-
BleakClient,
81-
device,
82-
device.name or "Unknown",
83-
max_attempts=max_attempts
84-
)
85-
name: str | None = None
86-
for i in range(max_attempts):
87-
try:
88-
# Standard GATT Device Name characteristic (0x2A00)
89-
device_name = (await client.read_gatt_char(NAME_UUID)).decode('utf-8')
90-
if device_name.startswith("🔑 "):
91-
name = device_name.replace("🔑 ","")
92-
break
93-
await asyncio.sleep(1)
94-
LOGGER.debug(f"Attempt {i+1} to query display name failed, {device_name}")
95-
except Exception as e:
96-
LOGGER.error(f"Failed to read device name: {e}")
97-
98-
await client.disconnect()
99-
return name

0 commit comments

Comments
 (0)