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

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet", "documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"], "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", "iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"], "loggers": ["tesla-fleet-api"],
"quality_scale": "platinum", "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", "iot_class": "cloud_polling",
"loggers": ["tessie"], "loggers": ["tessie"],
"quality_scale": "platinum", "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.tesla_fleet
# homeassistant.components.teslemetry # homeassistant.components.teslemetry
# homeassistant.components.tessie # homeassistant.components.tessie
tesla-fleet-api==0.7.2 tesla-fleet-api==0.7.3
# homeassistant.components.powerwall # homeassistant.components.powerwall
tesla-powerwall==0.5.2 tesla-powerwall==0.5.2

View File

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

View File

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

View File

@ -1,5 +1,7 @@
"""Test the Tesla Fleet init.""" """Test the Tesla Fleet init."""
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -102,18 +104,17 @@ async def test_vehicle_refresh_offline(
mock_vehicle_state.reset_mock() mock_vehicle_state.reset_mock()
mock_vehicle_data.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 mock_vehicle_data.side_effect = VehicleOffline
freezer.tick(VEHICLE_INTERVAL) freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() 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_data.assert_called_once()
mock_vehicle_state.reset_mock()
mock_vehicle_data.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 mock_vehicle_state.return_value = VEHICLE_ASLEEP
freezer.tick(VEHICLE_INTERVAL) freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)
@ -127,15 +128,15 @@ async def test_vehicle_refresh_offline(
async def test_vehicle_refresh_error( async def test_vehicle_refresh_error(
hass: HomeAssistant, hass: HomeAssistant,
normal_config_entry: MockConfigEntry, normal_config_entry: MockConfigEntry,
mock_vehicle_state, mock_vehicle_data: AsyncMock,
side_effect, side_effect: TeslaFleetError,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test coordinator refresh makes entity unavailable.""" """Test coordinator refresh makes entity unavailable."""
await setup_platform(hass, normal_config_entry) 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) freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()