Skip to content

Commit 9a165a6

Browse files
bdracoCopilot
authored andcommitted
Fix DoorBird being updated with wrong IP addresses during discovery (home-assistant#152088)
Co-authored-by: Copilot <[email protected]>
1 parent 9c749a6 commit 9a165a6

File tree

3 files changed

+140
-2
lines changed

3 files changed

+140
-2
lines changed

homeassistant/components/doorbird/config_flow.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
)
2020
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
2121
from homeassistant.core import HomeAssistant, callback
22+
from homeassistant.data_entry_flow import AbortFlow
2223
from homeassistant.exceptions import HomeAssistantError
2324
from homeassistant.helpers.aiohttp_client import async_get_clientsession
25+
from homeassistant.helpers.device_registry import format_mac
2426
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
2527
from homeassistant.helpers.typing import VolDictType
2628

@@ -103,6 +105,43 @@ def __init__(self) -> None:
103105
"""Initialize the DoorBird config flow."""
104106
self.discovery_schema: vol.Schema | None = None
105107

108+
async def _async_verify_existing_device_for_discovery(
109+
self,
110+
existing_entry: ConfigEntry,
111+
host: str,
112+
macaddress: str,
113+
) -> None:
114+
"""Verify discovered device matches existing entry before updating IP.
115+
116+
This method performs the following verification steps:
117+
1. Ensures that the stored credentials work before updating the entry.
118+
2. Verifies that the device at the discovered IP address has the expected MAC address.
119+
"""
120+
info, errors = await self._async_validate_or_error(
121+
{
122+
**existing_entry.data,
123+
CONF_HOST: host,
124+
}
125+
)
126+
127+
if errors:
128+
_LOGGER.debug(
129+
"Cannot validate DoorBird at %s with existing credentials: %s",
130+
host,
131+
errors,
132+
)
133+
raise AbortFlow("cannot_connect")
134+
135+
# Verify the MAC address matches what was advertised
136+
if format_mac(info["mac_addr"]) != format_mac(macaddress):
137+
_LOGGER.debug(
138+
"DoorBird at %s reports MAC %s but zeroconf advertised %s, ignoring",
139+
host,
140+
info["mac_addr"],
141+
macaddress,
142+
)
143+
raise AbortFlow("wrong_device")
144+
106145
async def async_step_reauth(
107146
self, entry_data: Mapping[str, Any]
108147
) -> ConfigFlowResult:
@@ -172,7 +211,22 @@ async def async_step_zeroconf(
172211

173212
await self.async_set_unique_id(macaddress)
174213
host = discovery_info.host
175-
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
214+
215+
# Check if we have an existing entry for this MAC
216+
existing_entry = self.hass.config_entries.async_entry_for_domain_unique_id(
217+
DOMAIN, macaddress
218+
)
219+
220+
if existing_entry:
221+
# Check if the host is actually changing
222+
if existing_entry.data.get(CONF_HOST) != host:
223+
await self._async_verify_existing_device_for_discovery(
224+
existing_entry, host, macaddress
225+
)
226+
227+
# All checks passed or no change needed, abort
228+
# if already configured with potential IP update
229+
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
176230

177231
self._async_abort_entries_match({CONF_HOST: host})
178232

homeassistant/components/doorbird/strings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
5050
"link_local_address": "Link local addresses are not supported",
5151
"not_doorbird_device": "This device is not a DoorBird",
52+
"not_ipv4_address": "Only IPv4 addresses are supported",
53+
"wrong_device": "Device MAC address does not match",
5254
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
5355
},
5456
"flow_title": "{name} ({host})",

tests/components/doorbird/test_config_flow.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ async def test_form_zeroconf_link_local_ignored(hass: HomeAssistant) -> None:
108108
assert result["reason"] == "link_local_address"
109109

110110

111-
async def test_form_zeroconf_ipv4_address(hass: HomeAssistant) -> None:
111+
async def test_form_zeroconf_ipv4_address(
112+
hass: HomeAssistant, doorbird_api: DoorBird
113+
) -> None:
112114
"""Test we abort and update the ip address from zeroconf with an ipv4 address."""
113115

114116
config_entry = MockConfigEntry(
@@ -118,6 +120,13 @@ async def test_form_zeroconf_ipv4_address(hass: HomeAssistant) -> None:
118120
options={CONF_EVENTS: ["event1", "event2", "event3"]},
119121
)
120122
config_entry.add_to_hass(hass)
123+
124+
# Mock the API to return the correct MAC when validating
125+
doorbird_api.info.return_value = {
126+
"PRIMARY_MAC_ADDR": "1CCAE3AAAAAA",
127+
"WIFI_MAC_ADDR": "1CCAE3BBBBBB",
128+
}
129+
121130
result = await hass.config_entries.flow.async_init(
122131
DOMAIN,
123132
context={"source": config_entries.SOURCE_ZEROCONF},
@@ -136,6 +145,79 @@ async def test_form_zeroconf_ipv4_address(hass: HomeAssistant) -> None:
136145
assert config_entry.data[CONF_HOST] == "4.4.4.4"
137146

138147

148+
async def test_form_zeroconf_ipv4_address_wrong_device(
149+
hass: HomeAssistant, doorbird_api: DoorBird
150+
) -> None:
151+
"""Test we abort when the device MAC doesn't match during zeroconf update."""
152+
153+
config_entry = MockConfigEntry(
154+
domain=DOMAIN,
155+
unique_id="1CCAE3AAAAAA",
156+
data=VALID_CONFIG,
157+
options={CONF_EVENTS: ["event1", "event2", "event3"]},
158+
)
159+
config_entry.add_to_hass(hass)
160+
161+
# Mock the API to return a different MAC (wrong device)
162+
doorbird_api.info.return_value = {
163+
"PRIMARY_MAC_ADDR": "1CCAE3DIFFERENT", # Different MAC!
164+
"WIFI_MAC_ADDR": "1CCAE3BBBBBB",
165+
}
166+
167+
result = await hass.config_entries.flow.async_init(
168+
DOMAIN,
169+
context={"source": config_entries.SOURCE_ZEROCONF},
170+
data=ZeroconfServiceInfo(
171+
ip_address=ip_address("4.4.4.4"),
172+
ip_addresses=[ip_address("4.4.4.4")],
173+
hostname="mock_hostname",
174+
name="Doorstation - abc123._axis-video._tcp.local.",
175+
port=None,
176+
properties={"macaddress": "1CCAE3AAAAAA"},
177+
type="mock_type",
178+
),
179+
)
180+
assert result["type"] is FlowResultType.ABORT
181+
assert result["reason"] == "wrong_device"
182+
# Host should not be updated since it's the wrong device
183+
assert config_entry.data[CONF_HOST] == "1.2.3.4"
184+
185+
186+
async def test_form_zeroconf_ipv4_address_cannot_connect(
187+
hass: HomeAssistant, doorbird_api: DoorBird
188+
) -> None:
189+
"""Test we abort when we cannot connect to validate during zeroconf update."""
190+
191+
config_entry = MockConfigEntry(
192+
domain=DOMAIN,
193+
unique_id="1CCAE3AAAAAA",
194+
data=VALID_CONFIG,
195+
options={CONF_EVENTS: ["event1", "event2", "event3"]},
196+
)
197+
config_entry.add_to_hass(hass)
198+
199+
# Mock the API to fail connection (e.g., wrong credentials or network error)
200+
doorbird_api.info.side_effect = mock_unauthorized_exception()
201+
202+
result = await hass.config_entries.flow.async_init(
203+
DOMAIN,
204+
context={"source": config_entries.SOURCE_ZEROCONF},
205+
data=ZeroconfServiceInfo(
206+
ip_address=ip_address("4.4.4.4"),
207+
ip_addresses=[ip_address("4.4.4.4")],
208+
hostname="mock_hostname",
209+
name="Doorstation - abc123._axis-video._tcp.local.",
210+
port=None,
211+
properties={"macaddress": "1CCAE3AAAAAA"},
212+
type="mock_type",
213+
),
214+
)
215+
assert result["type"] is FlowResultType.ABORT
216+
assert result["reason"] == "cannot_connect"
217+
# Host should not be updated since we couldn't validate
218+
assert config_entry.data[CONF_HOST] == "1.2.3.4"
219+
220+
139221
async def test_form_zeroconf_non_ipv4_ignored(hass: HomeAssistant) -> None:
140222
"""Test we abort when we get a non ipv4 address via zeroconf."""
141223

0 commit comments

Comments
 (0)