mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
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
@ -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:
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user