diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 36b079595990..b10fa6d0f6e3 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -322,16 +322,27 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): ) mac_address = format_mac(host.api.mac_address) - await self.async_set_unique_id(mac_address, raise_on_progress=False) - if self.source == SOURCE_REAUTH: - self._abort_if_unique_id_mismatch() - return self.async_update_reload_and_abort( - entry=self._get_reauth_entry(), data=user_input + existing_entry = await self.async_set_unique_id( + mac_address, raise_on_progress=False + ) + if self.source in (SOURCE_REAUTH, SOURCE_RECONFIGURE): + entry = ( + self._get_reauth_entry() + if self.source == SOURCE_REAUTH + else self._get_reconfigure_entry() ) - if self.source == SOURCE_RECONFIGURE: - self._abort_if_unique_id_mismatch() + # a device moved to its other network interface reports a new + # MAC address, its UID tells it is still the same device + same_device = ( + host.api.supported(None, "UID") + and entry.data.get(CONF_UID) == host.api.uid + ) + if not same_device: + self._abort_if_unique_id_mismatch() + if existing_entry is not None and existing_entry is not entry: + return self.async_abort(reason="already_configured") return self.async_update_reload_and_abort( - entry=self._get_reconfigure_entry(), data=user_input + entry=entry, data=user_input, unique_id=mac_address ) self._abort_if_unique_id_configured() diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 17a79187f319..dd014219740e 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -50,6 +50,8 @@ from .conftest import ( TEST_HOST, TEST_HOST2, TEST_MAC, + TEST_MAC2, + TEST_MAC_CAM, TEST_NVR_NAME, TEST_PASSWORD, TEST_PASSWORD2, @@ -461,58 +463,6 @@ async def test_reauth(hass: HomeAssistant) -> None: assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 -@pytest.mark.usefixtures("mock_setup_entry") -async def test_reauth_abort_unique_id_mismatch( - hass: HomeAssistant, reolink_host: MagicMock -) -> None: - """Test a reauth flow.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id=format_mac(TEST_MAC), - data={ - CONF_HOST: TEST_HOST, - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - CONF_PORT: TEST_PORT, - CONF_USE_HTTPS: TEST_USE_HTTPS, - CONF_BC_PORT: TEST_BC_PORT, - CONF_BC_CONNECT: TEST_BC_CON, - CONF_BC_ONLY: False, - CONF_UID: TEST_UID, - }, - options={ - CONF_PROTOCOL: DEFAULT_PROTOCOL, - }, - title=TEST_NVR_NAME, - ) - config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - reolink_host.mac_address = "aa:aa:aa:aa:aa:aa" - - result = await config_entry.start_reauth_flow(hass) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_USERNAME: TEST_USERNAME2, - CONF_PASSWORD: TEST_PASSWORD2, - }, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "unique_id_mismatch" - assert config_entry.data[CONF_HOST] == TEST_HOST - assert config_entry.data[CONF_USERNAME] == TEST_USERNAME - assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD - - @pytest.mark.usefixtures("mock_setup_entry") async def test_dhcp_flow(hass: HomeAssistant) -> None: """Successful flow from DHCP discovery.""" @@ -885,10 +835,72 @@ async def test_reconfig(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("mock_setup_entry") -async def test_reconfig_abort_unique_id_mismatch( - hass: HomeAssistant, reolink_host: MagicMock +@pytest.mark.parametrize( + ("source", "user_input", "success_reason"), + [ + pytest.param( + config_entries.SOURCE_REAUTH, + {CONF_USERNAME: TEST_USERNAME2, CONF_PASSWORD: TEST_PASSWORD2}, + "reauth_successful", + id="reauth", + ), + pytest.param( + config_entries.SOURCE_RECONFIGURE, + { + CONF_HOST: TEST_HOST2, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + "reconfigure_successful", + id="reconfigure", + ), + ], +) +@pytest.mark.parametrize( + ("uid_supported", "device_uid", "other_entry_mac", "abort_reason"), + [ + pytest.param( + True, + TEST_UID, + TEST_MAC_CAM, + None, + id="same_device_on_other_interface", + ), + pytest.param( + True, + "OTHER0123456789A", + TEST_MAC_CAM, + "unique_id_mismatch", + id="different_device", + ), + pytest.param( + False, + TEST_UID, + TEST_MAC_CAM, + "unique_id_mismatch", + id="uid_not_supported", + ), + pytest.param( + True, + TEST_UID, + TEST_MAC2, + "already_configured", + id="other_interface_has_its_own_entry", + ), + ], +) +async def test_reauth_reconfig_mac_change( + hass: HomeAssistant, + reolink_host: MagicMock, + source: str, + user_input: dict[str, str], + success_reason: str, + uid_supported: bool, + device_uid: str, + other_entry_mac: str, + abort_reason: str | None, ) -> None: - """Test a reconfiguration flow aborts if the unique id does not match.""" + """Test a reauth or reconfigure flow when the device reports another MAC address.""" config_entry = MockConfigEntry( domain=DOMAIN, unique_id=format_mac(TEST_MAC), @@ -909,29 +921,44 @@ async def test_reconfig_abort_unique_id_mismatch( title=TEST_NVR_NAME, ) config_entry.add_to_hass(hass) + MockConfigEntry(domain=DOMAIN, unique_id=format_mac(other_entry_mac)).add_to_hass( + hass + ) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - reolink_host.mac_address = "aa:aa:aa:aa:aa:aa" + reolink_host.mac_address = TEST_MAC2 + reolink_host.uid = device_uid + reolink_host.supported.side_effect = lambda channel, capability: ( + capability != "UID" or uid_supported + ) - result = await config_entry.start_reconfigure_flow(hass) + if source == config_entries.SOURCE_REAUTH: + result = await config_entry.start_reauth_flow(hass) + else: + result = await config_entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOST: TEST_HOST2, - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - }, + result["flow_id"], user_input ) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "unique_id_mismatch" + if abort_reason is None: + # the entry follows the device to its new MAC address + assert result["reason"] == success_reason + assert config_entry.data[CONF_HOST] == user_input.get(CONF_HOST, TEST_HOST) + assert config_entry.data[CONF_USERNAME] == user_input[CONF_USERNAME] + assert config_entry.data[CONF_PASSWORD] == user_input[CONF_PASSWORD] + assert config_entry.unique_id == format_mac(TEST_MAC2) + return + + assert result["reason"] == abort_reason assert config_entry.data[CONF_HOST] == TEST_HOST assert config_entry.data[CONF_USERNAME] == TEST_USERNAME assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD + assert config_entry.unique_id == format_mac(TEST_MAC)