mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 07:25:52 -05:00
Fix Teslemetry insufficient-credits polling storm (#175913)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
6cde36014e
commit
c2a1784340
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, override
|
||||
from tesla_fleet_api.const import TeslaEnergyPeriod, VehicleDataEndpoint
|
||||
from tesla_fleet_api.exceptions import (
|
||||
GatewayTimeout,
|
||||
InsufficientCredits,
|
||||
InvalidResponse,
|
||||
InvalidToken,
|
||||
LoginRequired,
|
||||
@@ -49,6 +50,10 @@ ENERGY_INFO_INTERVAL = timedelta(seconds=30)
|
||||
ENERGY_HISTORY_INTERVAL = timedelta(seconds=60)
|
||||
METADATA_INTERVAL = timedelta(hours=1)
|
||||
|
||||
# Insufficient credits will not resolve themselves quickly, so back off polling
|
||||
# instead of hammering the API at the coordinator's normal interval.
|
||||
INSUFFICIENT_CREDITS_RETRY_AFTER = timedelta(hours=1).total_seconds()
|
||||
|
||||
ENDPOINTS = [
|
||||
VehicleDataEndpoint.CHARGE_STATE,
|
||||
VehicleDataEndpoint.CLIMATE_STATE,
|
||||
@@ -139,6 +144,12 @@ class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
data = (await self.api.vehicle_data(endpoints=ENDPOINTS))["response"]
|
||||
except (InvalidToken, SubscriptionRequired, LoginRequired) as e:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
except InsufficientCredits as e:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed_insufficient_credits",
|
||||
retry_after=INSUFFICIENT_CREDITS_RETRY_AFTER,
|
||||
) from e
|
||||
except RETRY_EXCEPTIONS as e:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -1182,6 +1182,9 @@
|
||||
"update_failed": {
|
||||
"message": "Error fetching data from Teslemetry API: {message}"
|
||||
},
|
||||
"update_failed_insufficient_credits": {
|
||||
"message": "Teslemetry account has insufficient command credits, pausing updates until credits are added"
|
||||
},
|
||||
"update_failed_invalid_data": {
|
||||
"message": "Received invalid data from API"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from tesla_fleet_api.exceptions import (
|
||||
Forbidden,
|
||||
InsufficientCredits,
|
||||
InvalidResponse,
|
||||
InvalidToken,
|
||||
LoginRequired,
|
||||
@@ -25,6 +26,7 @@ from homeassistant.components.teslemetry.coordinator import (
|
||||
ENERGY_HISTORY_INTERVAL,
|
||||
ENERGY_INFO_INTERVAL,
|
||||
ENERGY_LIVE_INTERVAL,
|
||||
INSUFFICIENT_CREDITS_RETRY_AFTER,
|
||||
METADATA_INTERVAL,
|
||||
VEHICLE_INTERVAL,
|
||||
)
|
||||
@@ -39,6 +41,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
|
||||
from . import setup_platform
|
||||
from .const import (
|
||||
@@ -64,6 +67,11 @@ ERRORS = [
|
||||
(TeslaFleetError, ConfigEntryState.SETUP_RETRY),
|
||||
]
|
||||
|
||||
VEHICLE_ERRORS = [
|
||||
*ERRORS,
|
||||
(InsufficientCredits, ConfigEntryState.SETUP_RETRY),
|
||||
]
|
||||
|
||||
|
||||
async def test_load_unload(hass: HomeAssistant) -> None:
|
||||
"""Test load and unload."""
|
||||
@@ -103,7 +111,7 @@ async def test_devices(
|
||||
assert device == snapshot(name=f"{device.identifiers}")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
|
||||
@pytest.mark.parametrize(("side_effect", "state"), VEHICLE_ERRORS)
|
||||
async def test_vehicle_refresh_error(
|
||||
hass: HomeAssistant,
|
||||
mock_vehicle_data: AsyncMock,
|
||||
@@ -998,3 +1006,37 @@ async def test_dynamic_device_discovery_no_reload_without_changes(
|
||||
|
||||
# Verify reload was NOT triggered since no subscription changes
|
||||
mock_reload.assert_not_called()
|
||||
|
||||
|
||||
async def test_insufficient_credits_backs_off_polling(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_vehicle_data: AsyncMock,
|
||||
mock_legacy: AsyncMock,
|
||||
) -> None:
|
||||
"""Running out of command credits should back off, not hammer the API every poll."""
|
||||
call_count = 0
|
||||
|
||||
def vehicle_data_side_effect(**kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
return deepcopy(VEHICLE_DATA)
|
||||
raise InsufficientCredits
|
||||
|
||||
mock_vehicle_data.side_effect = vehicle_data_side_effect
|
||||
|
||||
entry = await setup_platform(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert call_count == 1
|
||||
|
||||
freezer.tick(VEHICLE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert call_count == 2
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
coordinator = entry.runtime_data.vehicles[0].coordinator
|
||||
assert isinstance(coordinator.last_exception, UpdateFailed)
|
||||
assert coordinator.last_exception.retry_after == INSUFFICIENT_CREDITS_RETRY_AFTER
|
||||
|
||||
Reference in New Issue
Block a user