mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
Bound Teslemetry Bluetooth disconnect timeout on unload (#181988)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user