Fix Reolink reconfigure and reauth for a device that moved to its other network interface (#182641)

This commit is contained in:
Franck Nijhof
2026-09-29 09:39:05 +02:00
committed by GitHub
parent 813bf055ab
commit 0ffab56bc2
2 changed files with 110 additions and 72 deletions
@@ -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()
+91 -64
View File
@@ -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)