1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-03 12:46:09 +01:00

Filter already-configured hubs from nobo_hub config flow (#169593)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Øyvind Matheson Wergeland
2026-06-27 14:18:53 +02:00
committed by GitHub
parent e8e5eedd7e
commit 97844d7ee0
3 changed files with 124 additions and 3 deletions
@@ -25,6 +25,8 @@ from .const import (
DOMAIN,
OVERRIDE_TYPE_CONSTANT,
OVERRIDE_TYPE_NOW,
SERIAL_LENGTH,
SERIAL_PREFIX_LENGTH,
)
DATA_NOBO_HUB_IMPL = "nobo_hub_flow_implementation"
@@ -49,7 +51,20 @@ class NoboHubConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle the initial step."""
if self._discovered_hubs is None:
self._discovered_hubs = dict(await nobo.async_discover_hubs())
# Wait 5s — real-world gaps up to ~4s have been observed.
discovered = dict(await nobo.async_discover_hubs(autodiscover_wait=5.0))
# Hide hubs that already have a config entry. Include matching on IP
# as serial prefix is not unique.
configured = {
(entry.data[CONF_IP_ADDRESS], entry.unique_id[:SERIAL_PREFIX_LENGTH])
for entry in self._async_current_entries(include_ignore=False)
if entry.unique_id
}
self._discovered_hubs = {
ip: prefix
for ip, prefix in discovered.items()
if (ip, prefix) not in configured
}
if not self._discovered_hubs:
# No hubs auto discovered
@@ -227,7 +242,7 @@ class NoboHubConfigFlow(ConfigFlow, domain=DOMAIN):
)
async def _test_connection(self, serial: str, ip_address: str) -> str:
if not len(serial) == 12 or not serial.isdigit():
if len(serial) != SERIAL_LENGTH or not serial.isdigit():
raise NoboHubConnectError("invalid_serial")
try:
socket.inet_aton(ip_address)
@@ -9,6 +9,11 @@ CONF_OVERRIDE_TYPE = "override_type"
OVERRIDE_TYPE_CONSTANT = "constant"
OVERRIDE_TYPE_NOW = "now"
# Hub serial: 9-digit batch prefix + 3-digit per-hub suffix. Discovery
# broadcasts only the prefix; the user supplies the suffix.
SERIAL_PREFIX_LENGTH = 9
SERIAL_LENGTH = SERIAL_PREFIX_LENGTH + 3
NOBO_MANUFACTURER = "Glen Dimplex Nordic AS"
ATTR_HARDWARE_VERSION: Final = "hardware_version"
ATTR_SOFTWARE_VERSION: Final = "software_version"
+102 -1
View File
@@ -32,12 +32,13 @@ async def test_configure_with_discover(
with patch(
"homeassistant.components.nobo_hub.config_flow.nobo.async_discover_hubs",
return_value=[("1.1.1.1", "123456789")],
):
) as mock_discover:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
mock_discover.assert_awaited_once_with(autodiscover_wait=5.0)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -76,6 +77,106 @@ async def test_configure_with_discover(
mock_setup_entry.assert_awaited_once()
@pytest.mark.parametrize(
("discovered", "expected_devices", "selected_device"),
[
# Same IP+prefix hidden; sibling with same prefix at a different IP shown.
(
[("1.1.1.1", "111111111"), ("2.2.2.2", "111111111")],
{"2.2.2.2", "manual"},
"2.2.2.2",
),
# Same IP, different prefix → different hub (e.g. replacement), shown.
([("1.1.1.1", "222222222")], {"1.1.1.1", "manual"}, "1.1.1.1"),
],
ids=["sibling_different_ip", "replaced_hub"],
)
async def test_configure_filters_configured_hubs(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
discovered: list[tuple[str, str]],
expected_devices: set[str],
selected_device: str,
) -> None:
"""Configured (IP, prefix) pairs are hidden; the user can pick a remaining one."""
MockConfigEntry(
domain=DOMAIN,
unique_id="111111111012",
data={CONF_SERIAL: "111111111012", CONF_IP_ADDRESS: "1.1.1.1"},
).add_to_hass(hass)
with patch("pynobo.nobo.async_discover_hubs", return_value=discovered):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert set(result["data_schema"].schema["device"].container) == expected_devices
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"device": selected_device},
)
assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "selected"
with (
patch("pynobo.nobo.async_connect_hub", return_value=True),
patch(
"pynobo.nobo.hub_info",
new_callable=PropertyMock,
create=True,
return_value={"name": "My Nobø Ecohub"},
),
):
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{"serial_suffix": "999"},
)
assert result3["type"] is FlowResultType.CREATE_ENTRY
async def test_configure_skips_user_step_when_all_configured(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
) -> None:
"""Flow falls through to manual when every discovered hub matches a configured pair."""
MockConfigEntry(
domain=DOMAIN,
unique_id="111111111012",
data={CONF_SERIAL: "111111111012", CONF_IP_ADDRESS: "1.1.1.1"},
).add_to_hass(hass)
with patch(
"pynobo.nobo.async_discover_hubs",
return_value=[("1.1.1.1", "111111111")],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "manual"
with (
patch("pynobo.nobo.async_connect_hub", return_value=True),
patch(
"pynobo.nobo.hub_info",
new_callable=PropertyMock,
create=True,
return_value={"name": "My Nobø Ecohub"},
),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"serial": "999999999999", "ip_address": "9.9.9.9"},
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
async def test_configure_manual(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,