Add dynamic coordinator interval to Tesla Fleet (#122234)

* Add dynamic rate limiter

* tweaks

* Revert min polling back to 2min

* Set max 1 hour

* Remove redundant update_interval

* Tuning and fixes

* Reduce double API calls

* Type test

* Remove RateCalculator
This commit is contained in:
Brett Adams 2024-07-26 17:40:49 +10:00 committed by GitHub
parent 9b4cf873c1
commit 621bd5f0c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 38 additions and 27 deletions

View file

@ -13,6 +13,7 @@ from tesla_fleet_api.exceptions import (
TeslaFleetError,
VehicleOffline,
)
from tesla_fleet_api.ratecalculator import RateCalculator
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
@ -20,7 +21,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import LOGGER, TeslaFleetState
VEHICLE_INTERVAL_SECONDS = 120
VEHICLE_INTERVAL_SECONDS = 90
VEHICLE_INTERVAL = timedelta(seconds=VEHICLE_INTERVAL_SECONDS)
VEHICLE_WAIT = timedelta(minutes=15)
@ -56,6 +57,7 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
updated_once: bool
pre2021: bool
last_active: datetime
rate: RateCalculator
def __init__(
self, hass: HomeAssistant, api: VehicleSpecific, product: dict
@ -65,27 +67,31 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
hass,
LOGGER,
name="Tesla Fleet Vehicle",
update_interval=timedelta(seconds=5),
update_interval=VEHICLE_INTERVAL,
)
self.api = api
self.data = flatten(product)
self.updated_once = False
self.last_active = datetime.now()
self.rate = RateCalculator(200, 86400, VEHICLE_INTERVAL_SECONDS, 3600, 5)
async def _async_update_data(self) -> dict[str, Any]:
"""Update vehicle data using TeslaFleet API."""
self.update_interval = VEHICLE_INTERVAL
try:
# Check if the vehicle is awake using a non-rate limited API call
state = (await self.api.vehicle())["response"]
if state and state["state"] != TeslaFleetState.ONLINE:
self.data["state"] = state["state"]
if self.data["state"] != TeslaFleetState.ONLINE:
response = await self.api.vehicle()
self.data["state"] = response["response"]["state"]
if self.data["state"] != TeslaFleetState.ONLINE:
return self.data
# This is a rated limited API call
data = (await self.api.vehicle_data(endpoints=ENDPOINTS))["response"]
self.rate.consume()
response = await self.api.vehicle_data(endpoints=ENDPOINTS)
data = response["response"]
except VehicleOffline:
self.data["state"] = TeslaFleetState.ASLEEP
return self.data
@ -103,6 +109,9 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
except TeslaFleetError as e:
raise UpdateFailed(e.message) from e
# Calculate ideal refresh interval
self.update_interval = timedelta(seconds=self.rate.calculate())
self.updated_once = True
if self.api.pre2021 and data["state"] == TeslaFleetState.ONLINE:

View file

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==0.7.2"]
"requirements": ["tesla-fleet-api==0.7.3"]
}

View file

@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"quality_scale": "platinum",
"requirements": ["tesla-fleet-api==0.7.2"]
"requirements": ["tesla-fleet-api==0.7.3"]
}

View file

@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["tessie"],
"quality_scale": "platinum",
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.7.2"]
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.7.3"]
}

View file

@ -2723,7 +2723,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry
# homeassistant.components.tessie
tesla-fleet-api==0.7.2
tesla-fleet-api==0.7.3
# homeassistant.components.powerwall
tesla-powerwall==0.5.2

View file

@ -2136,7 +2136,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry
# homeassistant.components.tessie
tesla-fleet-api==0.7.2
tesla-fleet-api==0.7.3
# homeassistant.components.powerwall
tesla-powerwall==0.5.2

View file

@ -2,9 +2,10 @@
from __future__ import annotations
from collections.abc import Generator
from copy import deepcopy
import time
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
import jwt
import pytest
@ -83,7 +84,7 @@ async def setup_credentials(hass: HomeAssistant) -> None:
@pytest.fixture(autouse=True)
def mock_products():
def mock_products() -> Generator[AsyncMock]:
"""Mock Tesla Fleet Api products method."""
with patch(
"homeassistant.components.tesla_fleet.TeslaFleetApi.products",
@ -93,7 +94,7 @@ def mock_products():
@pytest.fixture(autouse=True)
def mock_vehicle_state():
def mock_vehicle_state() -> Generator[AsyncMock]:
"""Mock Tesla Fleet API Vehicle Specific vehicle method."""
with patch(
"homeassistant.components.tesla_fleet.VehicleSpecific.vehicle",
@ -103,7 +104,7 @@ def mock_vehicle_state():
@pytest.fixture(autouse=True)
def mock_vehicle_data():
def mock_vehicle_data() -> Generator[AsyncMock]:
"""Mock Tesla Fleet API Vehicle Specific vehicle_data method."""
with patch(
"homeassistant.components.tesla_fleet.VehicleSpecific.vehicle_data",
@ -113,7 +114,7 @@ def mock_vehicle_data():
@pytest.fixture(autouse=True)
def mock_wake_up():
def mock_wake_up() -> Generator[AsyncMock]:
"""Mock Tesla Fleet API Vehicle Specific wake_up method."""
with patch(
"homeassistant.components.tesla_fleet.VehicleSpecific.wake_up",
@ -123,7 +124,7 @@ def mock_wake_up():
@pytest.fixture(autouse=True)
def mock_live_status():
def mock_live_status() -> Generator[AsyncMock]:
"""Mock Teslemetry Energy Specific live_status method."""
with patch(
"homeassistant.components.tesla_fleet.EnergySpecific.live_status",
@ -133,7 +134,7 @@ def mock_live_status():
@pytest.fixture(autouse=True)
def mock_site_info():
def mock_site_info() -> Generator[AsyncMock]:
"""Mock Teslemetry Energy Specific site_info method."""
with patch(
"homeassistant.components.tesla_fleet.EnergySpecific.site_info",

View file

@ -1,5 +1,7 @@
"""Test the Tesla Fleet init."""
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
@ -102,18 +104,17 @@ async def test_vehicle_refresh_offline(
mock_vehicle_state.reset_mock()
mock_vehicle_data.reset_mock()
# Test the unlikely condition that a vehicle state is online but actually offline
# Then the vehicle goes offline
mock_vehicle_data.side_effect = VehicleOffline
freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_vehicle_state.assert_called_once()
mock_vehicle_state.assert_not_called()
mock_vehicle_data.assert_called_once()
mock_vehicle_state.reset_mock()
mock_vehicle_data.reset_mock()
# Test the normal condition that a vehcile state is offline
# And stays offline
mock_vehicle_state.return_value = VEHICLE_ASLEEP
freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass)
@ -127,15 +128,15 @@ async def test_vehicle_refresh_offline(
async def test_vehicle_refresh_error(
hass: HomeAssistant,
normal_config_entry: MockConfigEntry,
mock_vehicle_state,
side_effect,
mock_vehicle_data: AsyncMock,
side_effect: TeslaFleetError,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test coordinator refresh makes entity unavailable."""
await setup_platform(hass, normal_config_entry)
mock_vehicle_state.side_effect = side_effect
mock_vehicle_data.side_effect = side_effect
freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()