diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 27bd504e9bb..a3e49f1f526 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -38,6 +38,7 @@ PLATFORMS = [ ] DEVICE_UPDATE_INTERVAL = timedelta(seconds=60) FIRMWARE_UPDATE_INTERVAL = timedelta(hours=12) +NUM_CRED_ERRORS = 3 @dataclass @@ -82,10 +83,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: await host.update_states() except CredentialsInvalidError as err: - await host.stop() - raise ConfigEntryAuthFailed(err) from err - except ReolinkError as err: + host.credential_errors += 1 + if host.credential_errors >= NUM_CRED_ERRORS: + await host.stop() + raise ConfigEntryAuthFailed(err) from err raise UpdateFailed(str(err)) from err + except ReolinkError as err: + host.credential_errors = 0 + raise UpdateFailed(str(err)) from err + + host.credential_errors = 0 async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): await host.renew() diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index c69a80ce972..bccb5c5b684 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -79,6 +79,8 @@ class ReolinkHost: ) self.firmware_ch_list: list[int | None] = [] + self.credential_errors: int = 0 + self.webhook_id: str | None = None self._onvif_push_supported: bool = True self._onvif_long_poll_supported: bool = True diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 466836e52ef..922fe0829f6 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -7,11 +7,16 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError -from homeassistant.components.reolink import FIRMWARE_UPDATE_INTERVAL, const +from homeassistant.components.reolink import ( + DEVICE_UPDATE_INTERVAL, + FIRMWARE_UPDATE_INTERVAL, + NUM_CRED_ERRORS, + const, +) from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers import ( device_registry as dr, entity_registry as er, @@ -58,7 +63,7 @@ pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") ConfigEntryState.SETUP_RETRY, ), ( - "get_states", + "get_host_data", AsyncMock(side_effect=CredentialsInvalidError("Test error")), ConfigEntryState.SETUP_ERROR, ), @@ -113,6 +118,33 @@ async def test_firmware_error_twice( assert hass.states.is_state(entity_id, STATE_UNAVAILABLE) +async def test_credential_error_three( + hass: HomeAssistant, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, +) -> None: + """Test when the update gives credential error 3 times.""" + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + reolink_connect.get_states = AsyncMock( + side_effect=CredentialsInvalidError("Test error") + ) + + issue_id = f"config_entry_reauth_{const.DOMAIN}_{config_entry.entry_id}" + for _ in range(NUM_CRED_ERRORS): + assert (HA_DOMAIN, issue_id) not in issue_registry.issues + async_fire_time_changed( + hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) + ) + await hass.async_block_till_done() + + assert (HA_DOMAIN, issue_id) in issue_registry.issues + + async def test_entry_reloading( hass: HomeAssistant, config_entry: MockConfigEntry,