Bound Teslemetry Bluetooth disconnect timeout on unload (#181988)

This commit is contained in:
Brett Adams
2026-09-12 23:38:37 +02:00
committed by GitHub
parent 041b678725
commit 55066d4ed9
3 changed files with 94 additions and 5 deletions
@@ -59,6 +59,7 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import UpdateFailed
from .const import (
BLE_DISCONNECT_TIMEOUT,
CLIENT_ID,
CONF_VIN,
DOMAIN,
@@ -905,12 +906,23 @@ async def async_unload_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry)
for vehicle in entry.runtime_data.vehicles:
if isinstance(vehicle.api, VehicleRouter):
try:
await vehicle.api.primary.disconnect()
except (BleakError, TeslaFleetError, TimeoutError) as err:
# Swallowed so one stuck link cannot block the unload, but
# warn: a leaked BLE connection can keep the vehicle awake.
async with asyncio.timeout(BLE_DISCONNECT_TIMEOUT):
try:
await vehicle.api.primary.disconnect()
except (BleakError, TeslaFleetError, TimeoutError) as err:
# Swallowed so one stuck link cannot block the
# unload, but warn: a leaked BLE connection can
# keep the vehicle awake.
LOGGER.warning(
"Error disconnecting Bluetooth for %s: %s",
vehicle.vin,
err,
)
except TimeoutError:
LOGGER.warning(
"Error disconnecting Bluetooth for %s: %s", vehicle.vin, err
"Bluetooth disconnect for %s timed out after %ss",
vehicle.vin,
BLE_DISCONNECT_TIMEOUT,
)
return unloaded
@@ -17,6 +17,7 @@ CONF_VIN = "vin"
VEHICLE_KEY_FILE = "tesla_vehicle.key"
BLE_PARENT_KEY = f"{DOMAIN}_ble_parent"
BLE_PARENT_LOCK_KEY = f"{DOMAIN}_ble_parent_lock"
BLE_DISCONNECT_TIMEOUT = 10
SUBENTRY_TYPE_ENERGY_SITE = "energy_site"
CONF_SITE_ID = "site_id"
+76
View File
@@ -2319,6 +2319,82 @@ async def test_unload_never_connected_bluetooth(hass: HomeAssistant) -> None:
bluetooth_vehicle.disconnect.assert_awaited_once()
async def test_unload_disconnect_timeout(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""A hung Bluetooth disconnect cannot block unload past the timeout."""
entry = _entry_with_ble()
entry.add_to_hass(hass)
bluetooth_vehicle = AsyncMock()
never_set = asyncio.Event()
async def _hang(*args: object, **kwargs: object) -> None:
await never_set.wait()
bluetooth_vehicle.disconnect = AsyncMock(side_effect=_hang)
with (
patch(
"homeassistant.components.teslemetry.async_ble_device_from_address",
return_value=MagicMock(),
),
patch(
"homeassistant.components.teslemetry.helpers.TeslaBluetooth"
) as mock_parent,
patch("homeassistant.components.teslemetry.PLATFORMS", []),
patch("homeassistant.components.teslemetry.BLE_DISCONNECT_TIMEOUT", 0),
caplog.at_level(logging.WARNING),
):
mock_parent.return_value.get_private_key = AsyncMock()
mock_parent.return_value.vehicles.createBluetooth.return_value = (
bluetooth_vehicle
)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
bluetooth_vehicle.disconnect.assert_awaited_once()
assert "timed out after 0s" in caplog.text
async def test_unload_disconnect_instant_timeout(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""A TimeoutError raised by disconnect() itself is not mistaken for the deadline."""
entry = _entry_with_ble()
entry.add_to_hass(hass)
bluetooth_vehicle = AsyncMock()
bluetooth_vehicle.disconnect = AsyncMock(side_effect=TimeoutError("device busy"))
with (
patch(
"homeassistant.components.teslemetry.async_ble_device_from_address",
return_value=MagicMock(),
),
patch(
"homeassistant.components.teslemetry.helpers.TeslaBluetooth"
) as mock_parent,
patch("homeassistant.components.teslemetry.PLATFORMS", []),
caplog.at_level(logging.WARNING),
):
mock_parent.return_value.get_private_key = AsyncMock()
mock_parent.return_value.vehicles.createBluetooth.return_value = (
bluetooth_vehicle
)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
bluetooth_vehicle.disconnect.assert_awaited_once()
assert "Error disconnecting Bluetooth for" in caplog.text
assert "device busy" in caplog.text
assert "timed out after" not in caplog.text
async def test_ble_parent_shared_and_cached(hass: HomeAssistant) -> None:
"""The BLE parent (holding the private key) is created once and reused."""
with patch(