mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 02:37:08 +00:00
Improve UniFi Protect re-auth (#110021)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
da9d71cb6b
commit
fb04df5392
@ -20,6 +20,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity
|
|||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
AUTH_RETRIES,
|
||||||
CONF_ALLOW_EA,
|
CONF_ALLOW_EA,
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DEVICES_THAT_ADOPT,
|
DEVICES_THAT_ADOPT,
|
||||||
@ -62,6 +63,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
try:
|
try:
|
||||||
nvr_info = await protect.get_nvr()
|
nvr_info = await protect.get_nvr()
|
||||||
except NotAuthorized as err:
|
except NotAuthorized as err:
|
||||||
|
retry_key = f"{entry.entry_id}_auth"
|
||||||
|
retries = hass.data.setdefault(DOMAIN, {}).get(retry_key, 0)
|
||||||
|
if retries < AUTH_RETRIES:
|
||||||
|
retries += 1
|
||||||
|
hass.data[DOMAIN][retry_key] = retries
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
raise ConfigEntryAuthFailed(err) from err
|
raise ConfigEntryAuthFailed(err) from err
|
||||||
except (TimeoutError, ClientError, ServerDisconnectedError) as err:
|
except (TimeoutError, ClientError, ServerDisconnectedError) as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
@ -5,6 +5,9 @@ from pyunifiprotect.data import ModelType, Version
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
DOMAIN = "unifiprotect"
|
DOMAIN = "unifiprotect"
|
||||||
|
# some UniFi OS consoles have an unknown rate limit on auth
|
||||||
|
# if rate limit is triggered a 401 is returned
|
||||||
|
AUTH_RETRIES = 11 # ~12 hours of retries with the last waiting ~6 hours
|
||||||
|
|
||||||
ATTR_EVENT_SCORE = "event_score"
|
ATTR_EVENT_SCORE = "event_score"
|
||||||
ATTR_EVENT_ID = "event_id"
|
ATTR_EVENT_ID = "event_id"
|
||||||
|
@ -27,6 +27,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|||||||
from homeassistant.helpers.event import async_track_time_interval
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
AUTH_RETRIES,
|
||||||
CONF_DISABLE_RTSP,
|
CONF_DISABLE_RTSP,
|
||||||
CONF_MAX_MEDIA,
|
CONF_MAX_MEDIA,
|
||||||
DEFAULT_MAX_MEDIA,
|
DEFAULT_MAX_MEDIA,
|
||||||
@ -133,7 +134,7 @@ class ProtectData:
|
|||||||
try:
|
try:
|
||||||
updates = await self.api.update(force=force)
|
updates = await self.api.update(force=force)
|
||||||
except NotAuthorized:
|
except NotAuthorized:
|
||||||
if self._auth_failures < 10:
|
if self._auth_failures < AUTH_RETRIES:
|
||||||
_LOGGER.exception("Auth error while updating")
|
_LOGGER.exception("Auth error while updating")
|
||||||
self._auth_failures += 1
|
self._auth_failures += 1
|
||||||
else:
|
else:
|
||||||
|
@ -9,6 +9,7 @@ from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
|
|||||||
from pyunifiprotect.data import NVR, Bootstrap, Light
|
from pyunifiprotect.data import NVR, Bootstrap, Light
|
||||||
|
|
||||||
from homeassistant.components.unifiprotect.const import (
|
from homeassistant.components.unifiprotect.const import (
|
||||||
|
AUTH_RETRIES,
|
||||||
CONF_DISABLE_RTSP,
|
CONF_DISABLE_RTSP,
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -47,7 +48,7 @@ async def test_setup(hass: HomeAssistant, ufp: MockUFPFixture) -> None:
|
|||||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert ufp.entry.state == ConfigEntryState.LOADED
|
assert ufp.entry.state is ConfigEntryState.LOADED
|
||||||
assert ufp.api.update.called
|
assert ufp.api.update.called
|
||||||
assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
|
assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ async def test_setup_multiple(
|
|||||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert ufp.entry.state == ConfigEntryState.LOADED
|
assert ufp.entry.state is ConfigEntryState.LOADED
|
||||||
assert ufp.api.update.called
|
assert ufp.api.update.called
|
||||||
assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
|
assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
|
||||||
|
|
||||||
@ -104,14 +105,14 @@ async def test_reload(hass: HomeAssistant, ufp: MockUFPFixture) -> None:
|
|||||||
|
|
||||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert ufp.entry.state == ConfigEntryState.LOADED
|
assert ufp.entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
options = dict(ufp.entry.options)
|
options = dict(ufp.entry.options)
|
||||||
options[CONF_DISABLE_RTSP] = True
|
options[CONF_DISABLE_RTSP] = True
|
||||||
hass.config_entries.async_update_entry(ufp.entry, options=options)
|
hass.config_entries.async_update_entry(ufp.entry, options=options)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert ufp.entry.state == ConfigEntryState.LOADED
|
assert ufp.entry.state is ConfigEntryState.LOADED
|
||||||
assert ufp.api.async_disconnect_ws.called
|
assert ufp.api.async_disconnect_ws.called
|
||||||
|
|
||||||
|
|
||||||
@ -119,10 +120,10 @@ async def test_unload(hass: HomeAssistant, ufp: MockUFPFixture, light: Light) ->
|
|||||||
"""Test unloading of unifiprotect entry."""
|
"""Test unloading of unifiprotect entry."""
|
||||||
|
|
||||||
await init_entry(hass, ufp, [light])
|
await init_entry(hass, ufp, [light])
|
||||||
assert ufp.entry.state == ConfigEntryState.LOADED
|
assert ufp.entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
await hass.config_entries.async_unload(ufp.entry.entry_id)
|
await hass.config_entries.async_unload(ufp.entry.entry_id)
|
||||||
assert ufp.entry.state == ConfigEntryState.NOT_LOADED
|
assert ufp.entry.state is ConfigEntryState.NOT_LOADED
|
||||||
assert ufp.api.async_disconnect_ws.called
|
assert ufp.api.async_disconnect_ws.called
|
||||||
|
|
||||||
|
|
||||||
@ -135,7 +136,7 @@ async def test_setup_too_old(
|
|||||||
|
|
||||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert ufp.entry.state == ConfigEntryState.SETUP_ERROR
|
assert ufp.entry.state is ConfigEntryState.SETUP_ERROR
|
||||||
assert not ufp.api.update.called
|
assert not ufp.api.update.called
|
||||||
|
|
||||||
|
|
||||||
@ -146,7 +147,7 @@ async def test_setup_failed_update(hass: HomeAssistant, ufp: MockUFPFixture) ->
|
|||||||
|
|
||||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert ufp.entry.state == ConfigEntryState.SETUP_RETRY
|
assert ufp.entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
assert ufp.api.update.called
|
assert ufp.api.update.called
|
||||||
|
|
||||||
|
|
||||||
@ -157,20 +158,20 @@ async def test_setup_failed_update_reauth(
|
|||||||
|
|
||||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert ufp.entry.state == ConfigEntryState.LOADED
|
assert ufp.entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
# reauth should not be triggered until there are 10 auth failures in a row
|
# reauth should not be triggered until there are 10 auth failures in a row
|
||||||
# to verify it is not transient
|
# to verify it is not transient
|
||||||
ufp.api.update = AsyncMock(side_effect=NotAuthorized)
|
ufp.api.update = AsyncMock(side_effect=NotAuthorized)
|
||||||
for _ in range(10):
|
for _ in range(AUTH_RETRIES):
|
||||||
await time_changed(hass, DEFAULT_SCAN_INTERVAL)
|
await time_changed(hass, DEFAULT_SCAN_INTERVAL)
|
||||||
assert len(hass.config_entries.flow._progress) == 0
|
assert len(hass.config_entries.flow._progress) == 0
|
||||||
|
|
||||||
assert ufp.api.update.call_count == 10
|
assert ufp.api.update.call_count == AUTH_RETRIES
|
||||||
assert ufp.entry.state == ConfigEntryState.LOADED
|
assert ufp.entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
await time_changed(hass, DEFAULT_SCAN_INTERVAL)
|
await time_changed(hass, DEFAULT_SCAN_INTERVAL)
|
||||||
assert ufp.api.update.call_count == 11
|
assert ufp.api.update.call_count == AUTH_RETRIES + 1
|
||||||
assert len(hass.config_entries.flow._progress) == 1
|
assert len(hass.config_entries.flow._progress) == 1
|
||||||
|
|
||||||
|
|
||||||
@ -181,17 +182,24 @@ async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture) -> N
|
|||||||
|
|
||||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert ufp.entry.state == ConfigEntryState.SETUP_RETRY
|
assert ufp.entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
assert not ufp.api.update.called
|
assert not ufp.api.update.called
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_failed_auth(hass: HomeAssistant, ufp: MockUFPFixture) -> None:
|
async def test_setup_failed_auth(hass: HomeAssistant, ufp: MockUFPFixture) -> None:
|
||||||
"""Test setup of unifiprotect entry with unauthorized error."""
|
"""Test setup of unifiprotect entry with unauthorized error after multiple retries."""
|
||||||
|
|
||||||
ufp.api.get_nvr = AsyncMock(side_effect=NotAuthorized)
|
ufp.api.get_nvr = AsyncMock(side_effect=NotAuthorized)
|
||||||
|
|
||||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||||
assert ufp.entry.state == ConfigEntryState.SETUP_ERROR
|
assert ufp.entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
for _ in range(AUTH_RETRIES - 1):
|
||||||
|
await hass.config_entries.async_reload(ufp.entry.entry_id)
|
||||||
|
assert ufp.entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
await hass.config_entries.async_reload(ufp.entry.entry_id)
|
||||||
|
assert ufp.entry.state is ConfigEntryState.SETUP_ERROR
|
||||||
assert not ufp.api.update.called
|
assert not ufp.api.update.called
|
||||||
|
|
||||||
|
|
||||||
@ -208,7 +216,7 @@ async def test_setup_starts_discovery(
|
|||||||
|
|
||||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert ufp.entry.state == ConfigEntryState.LOADED
|
assert ufp.entry.state is ConfigEntryState.LOADED
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1
|
assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user