mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 02:07:09 +00:00
Throttle Nextbus if we are reaching the rate limit (#146064)
Co-authored-by: Josef Zweck <josef@zweck.dev> Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
parent
20b5d5a755
commit
f448f488ba
@ -1,8 +1,8 @@
|
|||||||
"""NextBus data update coordinator."""
|
"""NextBus data update coordinator."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, override
|
||||||
|
|
||||||
from py_nextbus import NextBusClient
|
from py_nextbus import NextBusClient
|
||||||
from py_nextbus.client import NextBusFormatError, NextBusHTTPError
|
from py_nextbus.client import NextBusFormatError, NextBusHTTPError
|
||||||
@ -15,8 +15,14 @@ from .util import RouteStop
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# At what percentage of the request limit should the coordinator pause making requests
|
||||||
|
UPDATE_INTERVAL_SECONDS = 30
|
||||||
|
THROTTLE_PRECENTAGE = 80
|
||||||
|
|
||||||
class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
|
|
||||||
|
class NextBusDataUpdateCoordinator(
|
||||||
|
DataUpdateCoordinator[dict[RouteStop, dict[str, Any]]]
|
||||||
|
):
|
||||||
"""Class to manage fetching NextBus data."""
|
"""Class to manage fetching NextBus data."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, agency: str) -> None:
|
def __init__(self, hass: HomeAssistant, agency: str) -> None:
|
||||||
@ -26,7 +32,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
_LOGGER,
|
_LOGGER,
|
||||||
config_entry=None, # It is shared between multiple entries
|
config_entry=None, # It is shared between multiple entries
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=timedelta(seconds=30),
|
update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS),
|
||||||
)
|
)
|
||||||
self.client = NextBusClient(agency_id=agency)
|
self.client = NextBusClient(agency_id=agency)
|
||||||
self._agency = agency
|
self._agency = agency
|
||||||
@ -49,9 +55,26 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
"""Check if this coordinator is tracking any routes."""
|
"""Check if this coordinator is tracking any routes."""
|
||||||
return len(self._route_stops) > 0
|
return len(self._route_stops) > 0
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, Any]:
|
@override
|
||||||
|
async def _async_update_data(self) -> dict[RouteStop, dict[str, Any]]:
|
||||||
"""Fetch data from NextBus."""
|
"""Fetch data from NextBus."""
|
||||||
|
|
||||||
|
if (
|
||||||
|
# If we have predictions, check the rate limit
|
||||||
|
self._predictions
|
||||||
|
# If are over our rate limit percentage, we should throttle
|
||||||
|
and self.client.rate_limit_percent >= THROTTLE_PRECENTAGE
|
||||||
|
# But only if we have a reset time to unthrottle
|
||||||
|
and self.client.rate_limit_reset is not None
|
||||||
|
# Unless we are after the reset time
|
||||||
|
and datetime.now() < self.client.rate_limit_reset
|
||||||
|
):
|
||||||
|
self.logger.debug(
|
||||||
|
"Rate limit threshold reached. Skipping updates for. Routes: %s",
|
||||||
|
str(self._route_stops),
|
||||||
|
)
|
||||||
|
return self._predictions
|
||||||
|
|
||||||
_stops_to_route_stops: dict[str, set[RouteStop]] = {}
|
_stops_to_route_stops: dict[str, set[RouteStop]] = {}
|
||||||
for route_stop in self._route_stops:
|
for route_stop in self._route_stops:
|
||||||
_stops_to_route_stops.setdefault(route_stop.stop_id, set()).add(route_stop)
|
_stops_to_route_stops.setdefault(route_stop.stop_id, set()).add(route_stop)
|
||||||
@ -60,7 +83,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
"Updating data from API. Routes: %s", str(_stops_to_route_stops)
|
"Updating data from API. Routes: %s", str(_stops_to_route_stops)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _update_data() -> dict:
|
def _update_data() -> dict[RouteStop, dict[str, Any]]:
|
||||||
"""Fetch data from NextBus."""
|
"""Fetch data from NextBus."""
|
||||||
self.logger.debug("Updating data from API (executor)")
|
self.logger.debug("Updating data from API (executor)")
|
||||||
predictions: dict[RouteStop, dict[str, Any]] = {}
|
predictions: dict[RouteStop, dict[str, Any]] = {}
|
||||||
|
@ -137,6 +137,13 @@ def mock_nextbus_lists(
|
|||||||
def mock_nextbus() -> Generator[MagicMock]:
|
def mock_nextbus() -> Generator[MagicMock]:
|
||||||
"""Create a mock py_nextbus module."""
|
"""Create a mock py_nextbus module."""
|
||||||
with patch("homeassistant.components.nextbus.coordinator.NextBusClient") as client:
|
with patch("homeassistant.components.nextbus.coordinator.NextBusClient") as client:
|
||||||
|
instance = client.return_value
|
||||||
|
|
||||||
|
# Set some mocked rate limit values
|
||||||
|
instance.rate_limit = 450
|
||||||
|
instance.rate_limit_remaining = 225
|
||||||
|
instance.rate_limit_percent = 50.0
|
||||||
|
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""The tests for the nexbus sensor component."""
|
"""The tests for the nexbus sensor component."""
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
from urllib.error import HTTPError
|
from urllib.error import HTTPError
|
||||||
|
|
||||||
@ -122,6 +123,57 @@ async def test_verify_no_upcoming(
|
|||||||
assert state.state == "unknown"
|
assert state.state == "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_verify_throttle(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_nextbus: MagicMock,
|
||||||
|
mock_nextbus_lists: MagicMock,
|
||||||
|
mock_nextbus_predictions: MagicMock,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Verify that the sensor coordinator is throttled correctly."""
|
||||||
|
|
||||||
|
# Set rate limit past threshold, should be ignored for first request
|
||||||
|
mock_client = mock_nextbus.return_value
|
||||||
|
mock_client.rate_limit_percent = 99.0
|
||||||
|
mock_client.rate_limit_reset = datetime.now() + timedelta(seconds=30)
|
||||||
|
|
||||||
|
# Do a request with the initial config and get predictions
|
||||||
|
await assert_setup_sensor(hass, CONFIG_BASIC)
|
||||||
|
|
||||||
|
# Validate the predictions are present
|
||||||
|
state = hass.states.get(SENSOR_ID)
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == "2019-03-28T21:09:31+00:00"
|
||||||
|
assert state.attributes["agency"] == VALID_AGENCY
|
||||||
|
assert state.attributes["route"] == VALID_ROUTE_TITLE
|
||||||
|
assert state.attributes["stop"] == VALID_STOP_TITLE
|
||||||
|
assert state.attributes["upcoming"] == "1, 2, 3, 10"
|
||||||
|
|
||||||
|
# Update the predictions mock to return a different result
|
||||||
|
mock_nextbus_predictions.return_value = NO_UPCOMING
|
||||||
|
|
||||||
|
# Move time forward and bump the rate limit reset time
|
||||||
|
mock_client.rate_limit_reset = freezer.tick(31) + timedelta(seconds=30)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
|
||||||
|
# Verify that the sensor state is unchanged
|
||||||
|
state = hass.states.get(SENSOR_ID)
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == "2019-03-28T21:09:31+00:00"
|
||||||
|
|
||||||
|
# Move time forward past the rate limit reset time
|
||||||
|
freezer.tick(31)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
|
||||||
|
# Verify that the sensor state is updated with the new predictions
|
||||||
|
state = hass.states.get(SENSOR_ID)
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes["upcoming"] == "No upcoming predictions"
|
||||||
|
assert state.state == "unknown"
|
||||||
|
|
||||||
|
|
||||||
async def test_unload_entry(
|
async def test_unload_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_nextbus: MagicMock,
|
mock_nextbus: MagicMock,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user