Remove no-longer-needed invalid API key monitor for OpenUV (#85573)

* Remove no-longer-needed invalid API key monitor for OpenUV

* Handle re-auth cancellation

* Use automatic API status check
This commit is contained in:
Aaron Bach 2023-01-10 01:48:39 -07:00 committed by GitHub
parent bf67458d83
commit 6a801fc058
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 21 additions and 79 deletions

View File

@ -31,7 +31,7 @@ from .const import (
DOMAIN, DOMAIN,
LOGGER, LOGGER,
) )
from .coordinator import InvalidApiKeyMonitor, OpenUvCoordinator from .coordinator import OpenUvCoordinator
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
@ -45,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data.get(CONF_LONGITUDE, hass.config.longitude), entry.data.get(CONF_LONGITUDE, hass.config.longitude),
altitude=entry.data.get(CONF_ELEVATION, hass.config.elevation), altitude=entry.data.get(CONF_ELEVATION, hass.config.elevation),
session=websession, session=websession,
check_status_before_request=True,
) )
async def async_update_protection_data() -> dict[str, Any]: async def async_update_protection_data() -> dict[str, Any]:
@ -53,16 +54,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
high = entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW) high = entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW)
return await client.uv_protection_window(low=low, high=high) return await client.uv_protection_window(low=low, high=high)
invalid_api_key_monitor = InvalidApiKeyMonitor(hass, entry)
coordinators: dict[str, OpenUvCoordinator] = { coordinators: dict[str, OpenUvCoordinator] = {
coordinator_name: OpenUvCoordinator( coordinator_name: OpenUvCoordinator(
hass, hass,
entry=entry,
name=coordinator_name, name=coordinator_name,
latitude=client.latitude, latitude=client.latitude,
longitude=client.longitude, longitude=client.longitude,
update_method=update_method, update_method=update_method,
invalid_api_key_monitor=invalid_api_key_monitor,
) )
for coordinator_name, update_method in ( for coordinator_name, update_method in (
(DATA_UV, client.uv_index), (DATA_UV, client.uv_index),
@ -70,16 +69,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) )
} }
# We disable the client's request retry abilities here to avoid a lengthy (and
# blocking) startup; then, if the initial update is successful, we re-enable client
# request retries:
client.disable_request_retries()
init_tasks = [ init_tasks = [
coordinator.async_config_entry_first_refresh() coordinator.async_config_entry_first_refresh()
for coordinator in coordinators.values() for coordinator in coordinators.values()
] ]
await asyncio.gather(*init_tasks) await asyncio.gather(*init_tasks)
client.enable_request_retries()
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinators hass.data[DOMAIN][entry.entry_id] = coordinators

View File

@ -103,7 +103,6 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Verify the credentials and create/re-auth the entry.""" """Verify the credentials and create/re-auth the entry."""
websession = aiohttp_client.async_get_clientsession(self.hass) websession = aiohttp_client.async_get_clientsession(self.hass)
client = Client(data.api_key, 0, 0, session=websession) client = Client(data.api_key, 0, 0, session=websession)
client.disable_request_retries()
try: try:
await client.uv_index() await client.uv_index()

View File

@ -1,15 +1,14 @@
"""Define an update coordinator for OpenUV.""" """Define an update coordinator for OpenUV."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from typing import Any, cast from typing import Any, cast
from pyopenuv.errors import InvalidApiKeyError, OpenUvError from pyopenuv.errors import InvalidApiKeyError, OpenUvError
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -18,64 +17,6 @@ from .const import LOGGER
DEFAULT_DEBOUNCER_COOLDOWN_SECONDS = 15 * 60 DEFAULT_DEBOUNCER_COOLDOWN_SECONDS = 15 * 60
class InvalidApiKeyMonitor:
"""Define a monitor for failed API calls (due to bad keys) across coordinators."""
DEFAULT_FAILED_API_CALL_THRESHOLD = 5
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize."""
self._count = 1
self._lock = asyncio.Lock()
self._reauth_flow_manager = ReauthFlowManager(hass, entry)
self.entry = entry
async def async_increment(self) -> None:
"""Increment the counter."""
async with self._lock:
self._count += 1
if self._count > self.DEFAULT_FAILED_API_CALL_THRESHOLD:
LOGGER.info("Starting reauth after multiple failed API calls")
self._reauth_flow_manager.start_reauth()
async def async_reset(self) -> None:
"""Reset the counter."""
async with self._lock:
self._count = 0
self._reauth_flow_manager.cancel_reauth()
class ReauthFlowManager:
"""Define an OpenUV reauth flow manager."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize."""
self.entry = entry
self.hass = hass
@callback
def _get_active_reauth_flow(self) -> FlowResult | None:
"""Get an active reauth flow (if it exists)."""
return next(
iter(self.entry.async_get_active_flows(self.hass, {SOURCE_REAUTH})),
None,
)
@callback
def cancel_reauth(self) -> None:
"""Cancel a reauth flow (if appropriate)."""
if reauth_flow := self._get_active_reauth_flow():
LOGGER.debug("API seems to have recovered; canceling reauth flow")
self.hass.config_entries.flow.async_abort(reauth_flow["flow_id"])
@callback
def start_reauth(self) -> None:
"""Start a reauth flow (if appropriate)."""
if not self._get_active_reauth_flow():
LOGGER.debug("Multiple API failures in a row; starting reauth flow")
self.entry.async_start_reauth(self.hass)
class OpenUvCoordinator(DataUpdateCoordinator): class OpenUvCoordinator(DataUpdateCoordinator):
"""Define an OpenUV data coordinator.""" """Define an OpenUV data coordinator."""
@ -86,11 +27,11 @@ class OpenUvCoordinator(DataUpdateCoordinator):
self, self,
hass: HomeAssistant, hass: HomeAssistant,
*, *,
entry: ConfigEntry,
name: str, name: str,
latitude: str, latitude: str,
longitude: str, longitude: str,
update_method: Callable[[], Awaitable[dict[str, Any]]], update_method: Callable[[], Awaitable[dict[str, Any]]],
invalid_api_key_monitor: InvalidApiKeyMonitor,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__( super().__init__(
@ -106,7 +47,7 @@ class OpenUvCoordinator(DataUpdateCoordinator):
), ),
) )
self._invalid_api_key_monitor = invalid_api_key_monitor self._entry = entry
self.latitude = latitude self.latitude = latitude
self.longitude = longitude self.longitude = longitude
@ -115,10 +56,18 @@ class OpenUvCoordinator(DataUpdateCoordinator):
try: try:
data = await self.update_method() data = await self.update_method()
except InvalidApiKeyError as err: except InvalidApiKeyError as err:
await self._invalid_api_key_monitor.async_increment() raise ConfigEntryAuthFailed("Invalid API key") from err
raise UpdateFailed(str(err)) from err
except OpenUvError as err: except OpenUvError as err:
raise UpdateFailed(str(err)) from err raise UpdateFailed(str(err)) from err
await self._invalid_api_key_monitor.async_reset() # OpenUV uses HTTP 403 to indicate both an invalid API key and an API key that
# has hit its daily/monthly limit; both cases will result in a reauth flow. If
# coordinator update succeeds after a reauth flow has been started, terminate
# it:
if reauth_flow := next(
iter(self._entry.async_get_active_flows(self.hass, {SOURCE_REAUTH})),
None,
):
self.hass.config_entries.flow.async_abort(reauth_flow["flow_id"])
return cast(dict[str, Any], data["result"]) return cast(dict[str, Any], data["result"])

View File

@ -3,7 +3,7 @@
"name": "OpenUV", "name": "OpenUV",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/openuv", "documentation": "https://www.home-assistant.io/integrations/openuv",
"requirements": ["pyopenuv==2022.04.0"], "requirements": ["pyopenuv==2023.01.0"],
"codeowners": ["@bachya"], "codeowners": ["@bachya"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyopenuv"], "loggers": ["pyopenuv"],

View File

@ -1822,7 +1822,7 @@ pyoctoprintapi==0.1.9
pyombi==0.1.10 pyombi==0.1.10
# homeassistant.components.openuv # homeassistant.components.openuv
pyopenuv==2022.04.0 pyopenuv==2023.01.0
# homeassistant.components.opnsense # homeassistant.components.opnsense
pyopnsense==0.2.0 pyopnsense==0.2.0

View File

@ -1305,7 +1305,7 @@ pynzbgetapi==0.2.0
pyoctoprintapi==0.1.9 pyoctoprintapi==0.1.9
# homeassistant.components.openuv # homeassistant.components.openuv
pyopenuv==2022.04.0 pyopenuv==2023.01.0
# homeassistant.components.opnsense # homeassistant.components.opnsense
pyopnsense==0.2.0 pyopnsense==0.2.0