Bump here_transit/here_routing and Implement backoff mechanism for here_travel_time (#83976)

* Add failing test

* Add backoff mechanism for too many requests

* Increase async_fire_time_changed

* Minimize try/except block
This commit is contained in:
Kevin Stillhammer 2022-12-21 16:00:15 +01:00 committed by GitHub
parent b85e175812
commit 588211223b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 178 additions and 22 deletions

View File

@ -6,13 +6,21 @@ import logging
from typing import Any from typing import Any
import here_routing import here_routing
from here_routing import HERERoutingApi, Return, RoutingMode, Spans, TransportMode from here_routing import (
HERERoutingApi,
HERERoutingTooManyRequestsError,
Return,
RoutingMode,
Spans,
TransportMode,
)
import here_transit import here_transit
from here_transit import ( from here_transit import (
HERETransitApi, HERETransitApi,
HERETransitConnectionError, HERETransitConnectionError,
HERETransitDepartureArrivalTooCloseError, HERETransitDepartureArrivalTooCloseError,
HERETransitNoRouteFoundError, HERETransitNoRouteFoundError,
HERETransitTooManyRequestsError,
) )
import voluptuous as vol import voluptuous as vol
@ -27,6 +35,8 @@ from homeassistant.util.unit_conversion import DistanceConverter
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTE_MODE_FASTEST from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTE_MODE_FASTEST
from .model import HERETravelTimeConfig, HERETravelTimeData from .model import HERETravelTimeConfig, HERETravelTimeData
BACKOFF_MULTIPLIER = 1.1
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -71,19 +81,35 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator):
departure, departure,
) )
response = await self._api.route( try:
transport_mode=TransportMode(self.config.travel_mode), response = await self._api.route(
origin=here_routing.Place(origin[0], origin[1]), transport_mode=TransportMode(self.config.travel_mode),
destination=here_routing.Place(destination[0], destination[1]), origin=here_routing.Place(origin[0], origin[1]),
routing_mode=route_mode, destination=here_routing.Place(destination[0], destination[1]),
arrival_time=arrival, routing_mode=route_mode,
departure_time=departure, arrival_time=arrival,
return_values=[Return.POLYINE, Return.SUMMARY], departure_time=departure,
spans=[Spans.NAMES], return_values=[Return.POLYINE, Return.SUMMARY],
) spans=[Spans.NAMES],
)
except HERERoutingTooManyRequestsError as error:
assert self.update_interval is not None
_LOGGER.debug(
"Rate limit has been reached. Increasing update interval to %s",
self.update_interval.total_seconds() * BACKOFF_MULTIPLIER,
)
self.update_interval = timedelta(
seconds=self.update_interval.total_seconds() * BACKOFF_MULTIPLIER
)
raise UpdateFailed("Rate limit has been reached") from error
_LOGGER.debug("Raw response is: %s", response) _LOGGER.debug("Raw response is: %s", response)
if self.update_interval != timedelta(seconds=DEFAULT_SCAN_INTERVAL):
_LOGGER.debug(
"Resetting update interval to %s",
DEFAULT_SCAN_INTERVAL,
)
self.update_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
return self._parse_routing_response(response) return self._parse_routing_response(response)
def _parse_routing_response(self, response: dict[str, Any]) -> HERETravelTimeData: def _parse_routing_response(self, response: dict[str, Any]) -> HERETravelTimeData:
@ -160,16 +186,31 @@ class HERETransitDataUpdateCoordinator(DataUpdateCoordinator):
here_transit.Return.TRAVEL_SUMMARY, here_transit.Return.TRAVEL_SUMMARY,
], ],
) )
except HERETransitTooManyRequestsError as error:
_LOGGER.debug("Raw response is: %s", response) assert self.update_interval is not None
_LOGGER.debug(
return self._parse_transit_response(response) "Rate limit has been reached. Increasing update interval to %s",
self.update_interval.total_seconds() * BACKOFF_MULTIPLIER,
)
self.update_interval = timedelta(
seconds=self.update_interval.total_seconds() * BACKOFF_MULTIPLIER
)
raise UpdateFailed("Rate limit has been reached") from error
except HERETransitDepartureArrivalTooCloseError: except HERETransitDepartureArrivalTooCloseError:
_LOGGER.debug("Ignoring HERETransitDepartureArrivalTooCloseError") _LOGGER.debug("Ignoring HERETransitDepartureArrivalTooCloseError")
return None return None
except (HERETransitConnectionError, HERETransitNoRouteFoundError) as error: except (HERETransitConnectionError, HERETransitNoRouteFoundError) as error:
raise UpdateFailed from error raise UpdateFailed from error
_LOGGER.debug("Raw response is: %s", response)
if self.update_interval != timedelta(seconds=DEFAULT_SCAN_INTERVAL):
_LOGGER.debug(
"Resetting update interval to %s",
DEFAULT_SCAN_INTERVAL,
)
self.update_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
return self._parse_transit_response(response)
def _parse_transit_response(self, response: dict[str, Any]) -> HERETravelTimeData: def _parse_transit_response(self, response: dict[str, Any]) -> HERETravelTimeData:
"""Parse the transit response dict to a HERETravelTimeData.""" """Parse the transit response dict to a HERETravelTimeData."""
sections: list[dict[str, Any]] = response["routes"][0]["sections"] sections: list[dict[str, Any]] = response["routes"][0]["sections"]

View File

@ -3,7 +3,7 @@
"name": "HERE Travel Time", "name": "HERE Travel Time",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/here_travel_time", "documentation": "https://www.home-assistant.io/integrations/here_travel_time",
"requirements": ["here_routing==0.1.1", "here_transit==1.1.1"], "requirements": ["here_routing==0.2.0", "here_transit==1.2.0"],
"codeowners": ["@eifinger"], "codeowners": ["@eifinger"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"] "loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"]

View File

@ -867,10 +867,10 @@ hdate==0.10.4
heatmiserV3==1.1.18 heatmiserV3==1.1.18
# homeassistant.components.here_travel_time # homeassistant.components.here_travel_time
here_routing==0.1.1 here_routing==0.2.0
# homeassistant.components.here_travel_time # homeassistant.components.here_travel_time
here_transit==1.1.1 here_transit==1.2.0
# homeassistant.components.hikvisioncam # homeassistant.components.hikvisioncam
hikvision==0.4 hikvision==0.4

View File

@ -653,10 +653,10 @@ hatasmota==0.6.1
hdate==0.10.4 hdate==0.10.4
# homeassistant.components.here_travel_time # homeassistant.components.here_travel_time
here_routing==0.1.1 here_routing==0.2.0
# homeassistant.components.here_travel_time # homeassistant.components.here_travel_time
here_transit==1.1.1 here_transit==1.2.0
# homeassistant.components.hlk_sw16 # homeassistant.components.hlk_sw16
hlk-sw16==0.0.9 hlk-sw16==0.0.9

View File

@ -1,8 +1,10 @@
"""The test for the HERE Travel Time sensor platform.""" """The test for the HERE Travel Time sensor platform."""
from datetime import timedelta
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from here_routing import ( from here_routing import (
HERERoutingError, HERERoutingError,
HERERoutingTooManyRequestsError,
Place, Place,
Return, Return,
RoutingMode, RoutingMode,
@ -13,6 +15,7 @@ from here_transit import (
HERETransitDepartureArrivalTooCloseError, HERETransitDepartureArrivalTooCloseError,
HERETransitNoRouteFoundError, HERETransitNoRouteFoundError,
HERETransitNoTransitRouteFoundError, HERETransitNoTransitRouteFoundError,
HERETransitTooManyRequestsError,
) )
import pytest import pytest
@ -27,6 +30,7 @@ from homeassistant.components.here_travel_time.const import (
CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LATITUDE,
CONF_ORIGIN_LONGITUDE, CONF_ORIGIN_LONGITUDE,
CONF_ROUTE_MODE, CONF_ROUTE_MODE,
DEFAULT_SCAN_INTERVAL,
DOMAIN, DOMAIN,
ICON_BICYCLE, ICON_BICYCLE,
ICON_CAR, ICON_CAR,
@ -39,6 +43,7 @@ from homeassistant.components.here_travel_time.const import (
TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC,
TRAVEL_MODE_TRUCK, TRAVEL_MODE_TRUCK,
) )
from homeassistant.components.here_travel_time.coordinator import BACKOFF_MULTIPLIER
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
ATTR_LAST_RESET, ATTR_LAST_RESET,
ATTR_STATE_CLASS, ATTR_STATE_CLASS,
@ -59,7 +64,9 @@ from homeassistant.const import (
) )
from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.core import CoreState, HomeAssistant, State
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from .conftest import RESPONSE, TRANSIT_RESPONSE
from .const import ( from .const import (
API_KEY, API_KEY,
DEFAULT_CONFIG, DEFAULT_CONFIG,
@ -69,7 +76,11 @@ from .const import (
ORIGIN_LONGITUDE, ORIGIN_LONGITUDE,
) )
from tests.common import MockConfigEntry, mock_restore_cache_with_extra_data from tests.common import (
MockConfigEntry,
async_fire_time_changed,
mock_restore_cache_with_extra_data,
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -634,3 +645,107 @@ async def test_transit_errors(hass: HomeAssistant, caplog, exception, expected_m
await hass.async_block_till_done() await hass.async_block_till_done()
assert expected_message in caplog.text assert expected_message in caplog.text
async def test_routing_rate_limit(hass: HomeAssistant, caplog):
"""Test that rate limiting is applied when encountering HTTP 429."""
with patch(
"here_routing.HERERoutingApi.route",
return_value=RESPONSE,
):
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="0123456789",
data=DEFAULT_CONFIG,
options=DEFAULT_OPTIONS,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert hass.states.get("sensor.test_distance").state == "13.682"
with patch(
"here_routing.HERERoutingApi.route",
side_effect=HERERoutingTooManyRequestsError(
"Rate limit for this service has been reached"
),
):
async_fire_time_changed(
hass, utcnow() + timedelta(seconds=DEFAULT_SCAN_INTERVAL + 1)
)
await hass.async_block_till_done()
assert hass.states.get("sensor.test_distance").state == "unavailable"
assert "Increasing update interval to" in caplog.text
with patch(
"here_routing.HERERoutingApi.route",
return_value=RESPONSE,
):
async_fire_time_changed(
hass,
utcnow()
+ timedelta(seconds=DEFAULT_SCAN_INTERVAL * BACKOFF_MULTIPLIER + 1),
)
await hass.async_block_till_done()
assert hass.states.get("sensor.test_distance").state == "13.682"
assert "Resetting update interval to" in caplog.text
async def test_transit_rate_limit(hass: HomeAssistant, caplog):
"""Test that rate limiting is applied when encountering HTTP 429."""
with patch(
"here_transit.HERETransitApi.route",
return_value=TRANSIT_RESPONSE,
):
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="0123456789",
data={
CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE),
CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE),
CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE),
CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE),
CONF_API_KEY: API_KEY,
CONF_MODE: TRAVEL_MODE_PUBLIC,
CONF_NAME: "test",
},
options=DEFAULT_OPTIONS,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert hass.states.get("sensor.test_distance").state == "1.883"
with patch(
"here_transit.HERETransitApi.route",
side_effect=HERETransitTooManyRequestsError(
"Rate limit for this service has been reached"
),
):
async_fire_time_changed(
hass, utcnow() + timedelta(seconds=DEFAULT_SCAN_INTERVAL + 1)
)
await hass.async_block_till_done()
assert hass.states.get("sensor.test_distance").state == "unavailable"
assert "Increasing update interval to" in caplog.text
with patch(
"here_transit.HERETransitApi.route",
return_value=TRANSIT_RESPONSE,
):
async_fire_time_changed(
hass,
utcnow()
+ timedelta(seconds=DEFAULT_SCAN_INTERVAL * BACKOFF_MULTIPLIER + 1),
)
await hass.async_block_till_done()
assert hass.states.get("sensor.test_distance").state == "1.883"
assert "Resetting update interval to" in caplog.text