From 7ca4d36d72d58c7689346c8a616c2f99f8700fc6 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 8 Sep 2026 20:11:09 +1000 Subject: [PATCH] Bound Tesla Fleet vehicle first refresh with a timeout (#181606) --- .../components/tesla_fleet/__init__.py | 12 ++++++++++- .../components/tesla_fleet/coordinator.py | 5 +++++ tests/components/tesla_fleet/test_init.py | 21 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index d450d1fd1669..ff721a03970e 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -1,5 +1,6 @@ """Tesla Fleet integration.""" +import asyncio from typing import Final import jwt @@ -35,6 +36,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from .const import DOMAIN, LOGGER from .coordinator import ( + VEHICLE_FIRST_REFRESH_TIMEOUT, TeslaFleetEnergySiteHistoryCoordinator, TeslaFleetEnergySiteInfoCoordinator, TeslaFleetEnergySiteLiveCoordinator, @@ -177,7 +179,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - hass, entry, api_vehicle, product, Scope.VEHICLE_LOCATION in scopes ) - await coordinator.async_config_entry_first_refresh() + # A sleeping vehicle can take minutes to answer vehicle_data; bound the + # first refresh so setup retries instead of stalling HA's bootstrap. + try: + async with asyncio.timeout(VEHICLE_FIRST_REFRESH_TIMEOUT): + await coordinator.async_config_entry_first_refresh() + except TimeoutError as err: + raise ConfigEntryNotReady( + f"Timed out waiting for vehicle {vin} to respond" + ) from err device = DeviceInfo( identifiers={(DOMAIN, vin)}, diff --git a/homeassistant/components/tesla_fleet/coordinator.py b/homeassistant/components/tesla_fleet/coordinator.py index bb9b5b6e82d7..8fb8a3f3cf6d 100644 --- a/homeassistant/components/tesla_fleet/coordinator.py +++ b/homeassistant/components/tesla_fleet/coordinator.py @@ -36,6 +36,11 @@ VEHICLE_WAIT_SECONDS = 900 VEHICLE_WAIT = timedelta(seconds=VEHICLE_WAIT_SECONDS) VEHICLE_STUCK_SECONDS = 1200 +# Kept well under Home Assistant's stage-2 setup budget (SLOW_SETUP_MAX_WAIT, 300s) +# so a sleeping vehicle raises ConfigEntryNotReady and the entry retries instead of +# being cancelled into a non-retried setup error. +VEHICLE_FIRST_REFRESH_TIMEOUT = 60 + ENERGY_INTERVAL_SECONDS = 60 ENERGY_INTERVAL = timedelta(seconds=ENERGY_INTERVAL_SECONDS) ENERGY_HISTORY_INTERVAL = timedelta(minutes=5) diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py index 1cb26d9e647e..c1097db7159c 100644 --- a/tests/components/tesla_fleet/test_init.py +++ b/tests/components/tesla_fleet/test_init.py @@ -1,5 +1,6 @@ """Test the Tesla Fleet init.""" +import asyncio from copy import deepcopy from datetime import timedelta from unittest.mock import AsyncMock, Mock, PropertyMock, patch @@ -96,6 +97,26 @@ async def test_init_error( assert normal_config_entry.state is state +async def test_vehicle_first_refresh_timeout( + hass: HomeAssistant, + normal_config_entry: MockConfigEntry, + mock_vehicle_data: AsyncMock, +) -> None: + """Test a slow first vehicle refresh retries instead of blocking setup.""" + never = asyncio.Event() + + async def _hang(*args: object, **kwargs: object) -> None: + await never.wait() + + mock_vehicle_data.side_effect = _hang + + with patch("homeassistant.components.tesla_fleet.VEHICLE_FIRST_REFRESH_TIMEOUT", 0): + await setup_platform(hass, normal_config_entry) + + assert normal_config_entry.state is ConfigEntryState.SETUP_RETRY + never.set() + + async def test_oauth_refresh_expired( hass: HomeAssistant, normal_config_entry: MockConfigEntry,