Add DHCP discovery support to Bond (#142372)

* Add DHCP discovery support to Bond

* fixes

* unique ids are always upper

* raise_on_progress=False for user

* Update tests/components/bond/test_config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* assert unique id

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
J. Nick Koston 2025-04-05 14:22:23 -10:00 committed by GitHub
parent 0a7b4d18dc
commit dcef86a30d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 206 additions and 5 deletions

View File

@ -16,6 +16,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import DOMAIN
@ -91,11 +92,22 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovered[CONF_ACCESS_TOKEN] = token
try:
_, hub_name = await _validate_input(self.hass, self._discovered)
bond_id, hub_name = await _validate_input(self.hass, self._discovered)
except InputValidationError:
return
await self.async_set_unique_id(bond_id)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
self._discovered[CONF_NAME] = hub_name
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a flow initialized by dhcp discovery."""
host = discovery_info.ip
bond_id = discovery_info.hostname.partition("-")[2].upper()
await self.async_set_unique_id(bond_id)
return await self.async_step_any_discovery(bond_id, host)
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
@ -104,11 +116,17 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
host: str = discovery_info.host
bond_id = name.partition(".")[0]
await self.async_set_unique_id(bond_id)
return await self.async_step_any_discovery(bond_id, host)
async def async_step_any_discovery(
self, bond_id: str, host: str
) -> ConfigFlowResult:
"""Handle a flow initialized by discovery."""
for entry in self._async_current_entries():
if entry.unique_id != bond_id:
continue
updates = {CONF_HOST: host}
if entry.state == ConfigEntryState.SETUP_ERROR and (
if entry.state is ConfigEntryState.SETUP_ERROR and (
token := await async_get_token(self.hass, host)
):
updates[CONF_ACCESS_TOKEN] = token
@ -153,10 +171,14 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_HOST: self._discovered[CONF_HOST],
}
try:
_, hub_name = await _validate_input(self.hass, data)
bond_id, hub_name = await _validate_input(self.hass, data)
except InputValidationError as error:
errors["base"] = error.base
else:
await self.async_set_unique_id(bond_id)
self._abort_if_unique_id_configured(
updates={CONF_HOST: self._discovered[CONF_HOST]}
)
return self.async_create_entry(
title=hub_name,
data=data,
@ -185,8 +207,10 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
except InputValidationError as error:
errors["base"] = error.base
else:
await self.async_set_unique_id(bond_id)
self._abort_if_unique_id_configured()
await self.async_set_unique_id(bond_id, raise_on_progress=False)
self._abort_if_unique_id_configured(
updates={CONF_HOST: user_input[CONF_HOST]}
)
return self.async_create_entry(title=hub_name, data=user_input)
return self.async_show_form(

View File

@ -3,6 +3,16 @@
"name": "Bond",
"codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"],
"config_flow": true,
"dhcp": [
{
"hostname": "bond-*",
"macaddress": "3C6A2C1*"
},
{
"hostname": "bond-*",
"macaddress": "F44E38*"
}
],
"documentation": "https://www.home-assistant.io/integrations/bond",
"iot_class": "local_push",
"loggers": ["bond_async"],

View File

@ -84,6 +84,16 @@ DHCP: Final[list[dict[str, str | bool]]] = [
"hostname": "blink*",
"macaddress": "20A171*",
},
{
"domain": "bond",
"hostname": "bond-*",
"macaddress": "3C6A2C1*",
},
{
"domain": "bond",
"hostname": "bond-*",
"macaddress": "F44E38*",
},
{
"domain": "broadlink",
"registered_devices": True,

View File

@ -15,6 +15,8 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .common import (
@ -63,6 +65,59 @@ async def test_user_form(hass: HomeAssistant) -> None:
assert len(mock_setup_entry.mock_calls) == 1
async def test_user_form_can_create_when_already_discovered(
hass: HomeAssistant,
) -> None:
"""Test we get the user initiated form can create when already discovered."""
with patch_bond_version(), patch_bond_token():
zc_result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address=ip_address("127.0.0.1"),
ip_addresses=[ip_address("127.0.0.1")],
hostname="mock_hostname",
name="ZXXX12345.some-other-tail-info",
port=None,
properties={},
type="mock_type",
),
)
assert zc_result["type"] is FlowResultType.FORM
assert zc_result["errors"] == {}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
with (
patch_bond_version(return_value={"bondid": "ZXXX12345"}),
patch_bond_device_ids(return_value=["f6776c11", "f6776c12"]),
patch_bond_bridge(),
patch_bond_device_properties(),
patch_bond_device(),
patch_bond_device_state(),
_patch_async_setup_entry() as mock_setup_entry,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == "bond-name"
assert result2["data"] == {
CONF_HOST: "some host",
CONF_ACCESS_TOKEN: "test-token",
}
assert result2["result"].unique_id == "ZXXX12345"
assert len(mock_setup_entry.mock_calls) == 1
async def test_user_form_with_non_bridge(hass: HomeAssistant) -> None:
"""Test setup a smart by bond fan."""
@ -97,6 +152,7 @@ async def test_user_form_with_non_bridge(hass: HomeAssistant) -> None:
CONF_HOST: "some host",
CONF_ACCESS_TOKEN: "test-token",
}
assert result2["result"].unique_id == "KXXX12345"
assert len(mock_setup_entry.mock_calls) == 1
@ -253,6 +309,107 @@ async def test_zeroconf_form(hass: HomeAssistant) -> None:
assert len(mock_setup_entry.mock_calls) == 1
async def test_dhcp_discovery(hass: HomeAssistant) -> None:
"""Test DHCP discovery."""
with patch_bond_version(), patch_bond_token():
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=DhcpServiceInfo(
ip="127.0.0.1",
hostname="Bond-KVPRBDJ45842",
macaddress=format_mac("3c:6a:2c:1c:8c:80"),
),
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
with (
patch_bond_version(return_value={"bondid": "KVPRBDJ45842"}),
patch_bond_bridge(),
patch_bond_device_ids(),
_patch_async_setup_entry() as mock_setup_entry,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_ACCESS_TOKEN: "test-token"},
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == "bond-name"
assert result2["data"] == {
CONF_HOST: "127.0.0.1",
CONF_ACCESS_TOKEN: "test-token",
}
assert result2["result"].unique_id == "KVPRBDJ45842"
assert len(mock_setup_entry.mock_calls) == 1
async def test_dhcp_discovery_already_exists(hass: HomeAssistant) -> None:
"""Test DHCP discovery for an already existing entry."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="KVPRBDJ45842",
)
entry.add_to_hass(hass)
with (
patch_bond_version(return_value={"bondid": "KVPRBDJ45842"}),
patch_bond_token(),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=DhcpServiceInfo(
ip="127.0.0.1",
hostname="Bond-KVPRBDJ45842".lower(),
macaddress=format_mac("3c:6a:2c:1c:8c:80"),
),
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_dhcp_discovery_short_name(hass: HomeAssistant) -> None:
"""Test DHCP discovery with the name cut off."""
with patch_bond_version(), patch_bond_token():
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=DhcpServiceInfo(
ip="127.0.0.1",
hostname="Bond-KVPRBDJ",
macaddress=format_mac("3c:6a:2c:1c:8c:80"),
),
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
with (
patch_bond_version(return_value={"bondid": "KVPRBDJ45842"}),
patch_bond_bridge(),
patch_bond_device_ids(),
_patch_async_setup_entry() as mock_setup_entry,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_ACCESS_TOKEN: "test-token"},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == "bond-name"
assert result2["data"] == {
CONF_HOST: "127.0.0.1",
CONF_ACCESS_TOKEN: "test-token",
}
assert result2["result"].unique_id == "KVPRBDJ45842"
assert len(mock_setup_entry.mock_calls) == 1
async def test_zeroconf_form_token_unavailable(hass: HomeAssistant) -> None:
"""Test we get the discovery form and we handle the token being unavailable."""