Skip to content

Commit

Permalink
Remove no-longer-needed invalid API key monitor for OpenUV (home-assi…
Browse files Browse the repository at this point in the history
…stant#85573)

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

* Handle re-auth cancellation

* Use automatic API status check
  • Loading branch information
bachya authored Jan 10, 2023
1 parent bf67458 commit 6a801fc
Show file tree
Hide file tree
Showing 6 changed files with 21 additions and 79 deletions.
12 changes: 3 additions & 9 deletions homeassistant/components/openuv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
DOMAIN,
LOGGER,
)
from .coordinator import InvalidApiKeyMonitor, OpenUvCoordinator
from .coordinator import OpenUvCoordinator

PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]

Expand All @@ -45,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data.get(CONF_LONGITUDE, hass.config.longitude),
altitude=entry.data.get(CONF_ELEVATION, hass.config.elevation),
session=websession,
check_status_before_request=True,
)

async def async_update_protection_data() -> dict[str, Any]:
Expand All @@ -53,33 +54,26 @@ async def async_update_protection_data() -> dict[str, Any]:
high = entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW)
return await client.uv_protection_window(low=low, high=high)

invalid_api_key_monitor = InvalidApiKeyMonitor(hass, entry)

coordinators: dict[str, OpenUvCoordinator] = {
coordinator_name: OpenUvCoordinator(
hass,
entry=entry,
name=coordinator_name,
latitude=client.latitude,
longitude=client.longitude,
update_method=update_method,
invalid_api_key_monitor=invalid_api_key_monitor,
)
for coordinator_name, update_method in (
(DATA_UV, client.uv_index),
(DATA_PROTECTION_WINDOW, async_update_protection_data),
)
}

# 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 = [
coordinator.async_config_entry_first_refresh()
for coordinator in coordinators.values()
]
await asyncio.gather(*init_tasks)
client.enable_request_retries()

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinators
Expand Down
1 change: 0 additions & 1 deletion homeassistant/components/openuv/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ async def _async_verify(
"""Verify the credentials and create/re-auth the entry."""
websession = aiohttp_client.async_get_clientsession(self.hass)
client = Client(data.api_key, 0, 0, session=websession)
client.disable_request_retries()

try:
await client.uv_index()
Expand Down
81 changes: 15 additions & 66 deletions homeassistant/components/openuv/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
"""Define an update coordinator for OpenUV."""
from __future__ import annotations

import asyncio
from collections.abc import Awaitable, Callable
from typing import Any, cast

from pyopenuv.errors import InvalidApiKeyError, OpenUvError

from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

Expand All @@ -18,64 +17,6 @@
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):
"""Define an OpenUV data coordinator."""

Expand All @@ -86,11 +27,11 @@ def __init__(
self,
hass: HomeAssistant,
*,
entry: ConfigEntry,
name: str,
latitude: str,
longitude: str,
update_method: Callable[[], Awaitable[dict[str, Any]]],
invalid_api_key_monitor: InvalidApiKeyMonitor,
) -> None:
"""Initialize."""
super().__init__(
Expand All @@ -106,7 +47,7 @@ def __init__(
),
)

self._invalid_api_key_monitor = invalid_api_key_monitor
self._entry = entry
self.latitude = latitude
self.longitude = longitude

Expand All @@ -115,10 +56,18 @@ async def _async_update_data(self) -> dict[str, Any]:
try:
data = await self.update_method()
except InvalidApiKeyError as err:
await self._invalid_api_key_monitor.async_increment()
raise UpdateFailed(str(err)) from err
raise ConfigEntryAuthFailed("Invalid API key") from err
except OpenUvError as 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"])
2 changes: 1 addition & 1 deletion homeassistant/components/openuv/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "OpenUV",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/openuv",
"requirements": ["pyopenuv==2022.04.0"],
"requirements": ["pyopenuv==2023.01.0"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling",
"loggers": ["pyopenuv"],
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1822,7 +1822,7 @@ pyoctoprintapi==0.1.9
pyombi==0.1.10

# homeassistant.components.openuv
pyopenuv==2022.04.0
pyopenuv==2023.01.0

# homeassistant.components.opnsense
pyopnsense==0.2.0
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1305,7 +1305,7 @@ pynzbgetapi==0.2.0
pyoctoprintapi==0.1.9

# homeassistant.components.openuv
pyopenuv==2022.04.0
pyopenuv==2023.01.0

# homeassistant.components.opnsense
pyopnsense==0.2.0
Expand Down

0 comments on commit 6a801fc

Please sign in to comment.