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:
parent
9b4cf873c1
commit
621bd5f0c3
8 changed files with 38 additions and 27 deletions
|
@ -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:
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue