diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 23e2541e6d8..e183edf4259 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -8,6 +8,7 @@ from typing import Any from aiohttp import CookieJar from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect.data import NVR +from unifi_discovery import async_console_is_alive import voluptuous as vol from homeassistant import config_entries @@ -22,7 +23,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.aiohttp_client import ( + async_create_clientsession, + async_get_clientsession, +) from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.loader import async_get_integration from homeassistant.util.network import is_ip_address @@ -37,11 +41,17 @@ from .const import ( MIN_REQUIRED_PROTECT_V, OUTDATED_LOG_MESSAGE, ) +from .data import async_last_update_was_successful from .discovery import async_start_discovery from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass _LOGGER = logging.getLogger(__name__) +ENTRY_FAILURE_STATES = ( + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.SETUP_RETRY, +) + async def async_local_user_documentation_url(hass: HomeAssistant) -> str: """Get the documentation url for creating a local user.""" @@ -54,6 +64,25 @@ def _host_is_direct_connect(host: str) -> bool: return host.endswith(".ui.direct") +async def _async_console_is_offline( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, +) -> bool: + """Check if a console is offline. + + We define offline by the config entry + is in a failure/retry state or the updates + are failing and the console is unreachable + since protect may be updating. + """ + return bool( + entry.state in ENTRY_FAILURE_STATES + or not async_last_update_was_successful(hass, entry) + ) and not await async_console_is_alive( + async_get_clientsession(hass, verify_ssl=False), entry.data[CONF_HOST] + ) + + class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a UniFi Protect config flow.""" @@ -111,6 +140,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): not entry_has_direct_connect and is_ip_address(entry_host) and entry_host != source_ip + and await _async_console_is_offline(self.hass, entry) ): new_host = source_ip if new_host: diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 68c8873c17e..bcc1e561e99 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -26,6 +26,16 @@ from .utils import async_get_adoptable_devices_by_type, async_get_devices _LOGGER = logging.getLogger(__name__) +@callback +def async_last_update_was_successful(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Check if the last update was successful for a config entry.""" + return bool( + DOMAIN in hass.data + and entry.entry_id in hass.data[DOMAIN] + and hass.data[DOMAIN][entry.entry_id].last_update_success + ) + + class ProtectData: """Coordinate updates.""" diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 199298d76ca..2554d12c866 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 843680f5227..f0418e6596f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2355,7 +2355,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.3 +unifi-discovery==1.1.4 # homeassistant.components.unifiled unifiled==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 151a427470b..cdb907b731e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1552,7 +1552,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.3 +unifi-discovery==1.1.4 # homeassistant.components.upb upb_lib==0.4.12 diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 80e845591b1..75f08acb37c 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -402,7 +402,10 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin ) mock_config.add_to_hass(hass) - with _patch_discovery(): + with _patch_discovery(), patch( + "homeassistant.components.unifiprotect.config_flow.async_console_is_alive", + return_value=False, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, @@ -415,6 +418,41 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin assert mock_config.data[CONF_HOST] == "127.0.0.1" +async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_still_online( + hass: HomeAssistant, mock_nvr: NVR +) -> None: + """Test a discovery from unifi-discovery does not update the ip unless the console at the old ip is offline.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.2.2.2", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": False, + }, + version=2, + unique_id=DEVICE_MAC_ADDRESS.replace(":", "").upper(), + ) + mock_config.add_to_hass(hass) + + with _patch_discovery(), patch( + "homeassistant.components.unifiprotect.config_flow.async_console_is_alive", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=UNIFI_DISCOVERY_DICT, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert mock_config.data[CONF_HOST] == "1.2.2.2" + + async def test_discovered_host_not_updated_if_existing_is_a_hostname( hass: HomeAssistant, mock_nvr: NVR ) -> None: