1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-22 08:20:04 +01:00

Drop auto_discovered config in nobo_hub (#169558)

This commit is contained in:
Øyvind Matheson Wergeland
2026-04-30 18:29:13 +02:00
committed by GitHub
parent b57e2814a7
commit 6c08950995
7 changed files with 56 additions and 120 deletions
+15 -14
View File
@@ -19,7 +19,6 @@ from homeassistant.util import dt as dt_util
from .const import (
ATTR_HARDWARE_VERSION,
ATTR_SOFTWARE_VERSION,
CONF_AUTO_DISCOVERED,
CONF_OVERRIDE_TYPE,
CONF_SERIAL,
DOMAIN,
@@ -36,7 +35,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: NoboHubConfigEntry) -> b
serial = entry.data[CONF_SERIAL]
stored_ip = entry.data[CONF_IP_ADDRESS]
auto_discovered = entry.data[CONF_AUTO_DISCOVERED]
async def _connect(ip: str) -> nobo:
hub = nobo(
@@ -52,20 +50,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: NoboHubConfigEntry) -> b
try:
hub = await _connect(stored_ip)
except OSError as err:
if not auto_discovered:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect_manual",
translation_placeholders={"serial": serial, "ip": stored_ip},
) from err
# Stored IP may be stale for an auto-discovered entry - try UDP
# rediscovery to pick up a new DHCP lease.
# Stored IP may be stale - try UDP rediscovery to pick up a new
# DHCP lease (or a hub that's been moved).
discovered = await nobo.async_discover_hubs(serial=serial)
if not discovered:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="hub_not_found",
translation_placeholders={"serial": serial},
translation_key="cannot_connect",
translation_placeholders={"serial": serial, "ip": stored_ip},
) from err
new_ip, _ = next(iter(discovered))
try:
@@ -73,8 +65,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: NoboHubConfigEntry) -> b
except OSError as rediscover_err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect_rediscovered",
translation_placeholders={"ip": new_ip},
translation_key="cannot_connect",
translation_placeholders={"serial": serial, "ip": new_ip},
) from rediscover_err
if new_ip != stored_ip:
hass.config_entries.async_update_entry(
@@ -129,4 +121,13 @@ async def async_migrate_entry(hass: HomeAssistant, entry: NoboHubConfigEntry) ->
entry, options=new_options, version=1, minor_version=2
)
if entry.version == 1 and entry.minor_version < 3:
# auto_discovered no longer affects behaviour; rediscovery is now
# the unconditional fallback on connection failure.
new_data = dict(entry.data)
new_data.pop("auto_discovered", None)
hass.config_entries.async_update_entry(
entry, data=new_data, version=1, minor_version=3
)
return True
@@ -20,7 +20,6 @@ from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
from . import NoboHubConfigEntry
from .const import (
CONF_AUTO_DISCOVERED,
CONF_OVERRIDE_TYPE,
CONF_SERIAL,
DOMAIN,
@@ -36,7 +35,7 @@ class NoboHubConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Nobø Ecohub."""
VERSION = 1
MINOR_VERSION = 2
MINOR_VERSION = 3
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -85,7 +84,7 @@ class NoboHubConfigFlow(ConfigFlow, domain=DOMAIN):
serial_suffix = user_input["serial_suffix"]
serial = f"{serial_prefix}{serial_suffix}"
try:
return await self._create_configuration(serial, self._hub, True)
return await self._create_configuration(serial, self._hub)
except NoboHubConnectError as error:
errors["base"] = error.msg
@@ -114,7 +113,7 @@ class NoboHubConfigFlow(ConfigFlow, domain=DOMAIN):
serial = user_input[CONF_SERIAL]
ip_address = user_input[CONF_IP_ADDRESS]
try:
return await self._create_configuration(serial, ip_address, False)
return await self._create_configuration(serial, ip_address)
except NoboHubConnectError as error:
errors["base"] = error.msg
@@ -133,7 +132,7 @@ class NoboHubConfigFlow(ConfigFlow, domain=DOMAIN):
)
async def _create_configuration(
self, serial: str, ip_address: str, auto_discovered: bool
self, serial: str, ip_address: str
) -> ConfigFlowResult:
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured()
@@ -143,7 +142,6 @@ class NoboHubConfigFlow(ConfigFlow, domain=DOMAIN):
data={
CONF_SERIAL: serial,
CONF_IP_ADDRESS: ip_address,
CONF_AUTO_DISCOVERED: auto_discovered,
},
)
@@ -2,7 +2,6 @@
DOMAIN = "nobo_hub"
CONF_AUTO_DISCOVERED = "auto_discovered"
CONF_SERIAL = "serial"
CONF_OVERRIDE_TYPE = "override_type"
OVERRIDE_TYPE_CONSTANT = "constant"
@@ -58,14 +58,8 @@
}
},
"exceptions": {
"cannot_connect_manual": {
"message": "Unable to connect to Nobø Ecohub with serial {serial} at {ip}. If the hub has moved to a new IP address, remove and re-add the integration."
},
"cannot_connect_rediscovered": {
"message": "Unable to connect to Nobø Ecohub at rediscovered IP {ip}; will retry."
},
"hub_not_found": {
"message": "Nobø Ecohub with serial {serial} not found on the network. The hub may be offline or on a different subnet; will retry."
"cannot_connect": {
"message": "Unable to connect to Nobø Ecohub with serial {serial} at {ip}; will retry. If the hub is on a different network from Home Assistant and has changed IP address, remove and re-add the integration."
}
},
"options": {
+1 -13
View File
@@ -8,11 +8,7 @@ from pynobo import nobo as pynobo_nobo
import pytest
from homeassistant.components.nobo_hub import PLATFORMS
from homeassistant.components.nobo_hub.const import (
CONF_AUTO_DISCOVERED,
CONF_SERIAL,
DOMAIN,
)
from homeassistant.components.nobo_hub.const import CONF_SERIAL, DOMAIN
from homeassistant.const import CONF_IP_ADDRESS, Platform
from homeassistant.core import HomeAssistant
@@ -46,12 +42,6 @@ def ip_address() -> str:
return STORED_IP
@pytest.fixture
def auto_discovered() -> bool:
"""Return whether the config entry was auto-discovered."""
return False
@pytest.fixture
def connect_exc() -> BaseException | None:
"""Exception to raise from hub.connect(), or None for success."""
@@ -67,7 +57,6 @@ def config_entry_options() -> dict[str, Any]:
@pytest.fixture
def mock_config_entry(
ip_address: str,
auto_discovered: bool,
config_entry_options: dict[str, Any],
) -> MockConfigEntry:
"""Return a mock Nobø Ecohub config entry."""
@@ -78,7 +67,6 @@ def mock_config_entry(
data={
CONF_SERIAL: SERIAL,
CONF_IP_ADDRESS: ip_address,
CONF_AUTO_DISCOVERED: auto_discovered,
},
options=config_entry_options,
)
@@ -57,7 +57,6 @@ async def test_configure_with_discover(
assert result3["data"] == {
"ip_address": "1.1.1.1",
"serial": "123456789012",
"auto_discovered": True,
}
mock_connect.assert_awaited_once_with("1.1.1.1", "123456789012")
mock_setup_entry.assert_awaited_once()
@@ -102,7 +101,6 @@ async def test_configure_manual(
assert result2["data"] == {
"serial": "123456789012",
"ip_address": "1.1.1.1",
"auto_discovered": False,
}
mock_connect.assert_awaited_once_with("1.1.1.1", "123456789012")
mock_setup_entry.assert_awaited_once()
@@ -154,7 +152,6 @@ async def test_configure_user_selected_manual(
assert result2["data"] == {
"serial": "123456789012",
"ip_address": "1.1.1.1",
"auto_discovered": False,
}
mock_connect.assert_awaited_once_with("1.1.1.1", "123456789012")
mock_setup_entry.assert_awaited_once()
+34 -75
View File
@@ -7,7 +7,6 @@ import pytest
from homeassistant.components.nobo_hub import async_setup_entry
from homeassistant.components.nobo_hub.const import (
CONF_AUTO_DISCOVERED,
CONF_OVERRIDE_TYPE,
CONF_SERIAL,
DOMAIN,
@@ -44,12 +43,12 @@ def _spec_hub(connect_exc: BaseException | None = None) -> MagicMock:
return hub
async def test_setup_manual_entry_uses_stored_ip(
async def test_setup_uses_stored_ip(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_nobo_class: MagicMock,
) -> None:
"""Manual entry connects using the stored IP without rediscovery."""
"""Setup connects using the stored IP without invoking rediscovery."""
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
@@ -60,49 +59,11 @@ async def test_setup_manual_entry_uses_stored_ip(
mock_nobo_class.async_discover_hubs.assert_not_called()
@pytest.mark.parametrize("auto_discovered", [True])
async def test_setup_autodiscovered_entry_uses_stored_ip(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_nobo_class: MagicMock,
) -> None:
"""Auto-discovered entry with a working stored IP does not rediscover."""
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_nobo_class.async_discover_hubs.assert_not_called()
@pytest.mark.parametrize(
"connect_exc",
[OSError("Unreachable"), TimeoutError("Handshake timed out")],
)
async def test_setup_manual_entry_connection_fails(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_nobo_class: MagicMock,
) -> None:
"""Manual entry raises ConfigEntryNotReady on socket errors or timeouts."""
mock_config_entry.add_to_hass(hass)
with pytest.raises(ConfigEntryNotReady) as exc_info:
await async_setup_entry(hass, mock_config_entry)
assert exc_info.value.translation_key == "cannot_connect_manual"
assert exc_info.value.translation_placeholders == {
"serial": SERIAL,
"ip": STORED_IP,
}
mock_nobo_class.async_discover_hubs.assert_not_called()
@pytest.mark.parametrize("auto_discovered", [True])
async def test_setup_autodiscovered_rediscovery_updates_ip(
async def test_setup_rediscovery_updates_ip(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Auto-discovered entry recovers via rediscovery and persists the new IP."""
"""A failed direct connect falls back to rediscovery and persists the new IP."""
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.nobo_hub.nobo", autospec=True) as mock_cls:
mock_cls.side_effect = [
@@ -121,30 +82,26 @@ async def test_setup_autodiscovered_rediscovery_updates_ip(
@pytest.mark.parametrize(
(
"discovered_hubs",
"rediscovered_connect_fails",
"expected_key",
"expected_placeholders",
"auto_discovered",
),
("discovered_hubs", "second_exc", "expected_placeholders"),
[
(set(), False, "hub_not_found", {"serial": SERIAL}, True),
({(NEW_IP, SERIAL)}, True, "cannot_connect_rediscovered", {"ip": NEW_IP}, True),
(set(), None, {"serial": SERIAL, "ip": STORED_IP}),
(
{(NEW_IP, SERIAL)},
OSError("Unreachable"),
{"serial": SERIAL, "ip": NEW_IP},
),
],
ids=["rediscovery_empty", "rediscovered_ip_fails"],
)
async def test_setup_autodiscovered_rediscovery_failure(
async def test_setup_rediscovery_failure(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
discovered_hubs: set[tuple[str, str]],
rediscovered_connect_fails: bool,
expected_key: str,
second_exc: BaseException | None,
expected_placeholders: dict[str, str],
) -> None:
"""Auto-discovered entry raises the right error when rediscovery can't recover."""
"""Setup raises cannot_connect when rediscovery can't recover."""
mock_config_entry.add_to_hass(hass)
second_exc = OSError("Unreachable") if rediscovered_connect_fails else None
with patch("homeassistant.components.nobo_hub.nobo", autospec=True) as mock_cls:
mock_cls.side_effect = [
_spec_hub(connect_exc=OSError("Unreachable")),
@@ -154,25 +111,27 @@ async def test_setup_autodiscovered_rediscovery_failure(
with pytest.raises(ConfigEntryNotReady) as exc_info:
await async_setup_entry(hass, mock_config_entry)
assert exc_info.value.translation_key == expected_key
assert exc_info.value.translation_key == "cannot_connect"
assert exc_info.value.translation_placeholders == expected_placeholders
@pytest.mark.parametrize(
("stored_value", "expected_value"),
("stored_options", "expected_options"),
[
("Constant", "constant"),
("Now", "now"),
("constant", "constant"),
({CONF_OVERRIDE_TYPE: "Constant"}, {CONF_OVERRIDE_TYPE: "constant"}),
({CONF_OVERRIDE_TYPE: "Now"}, {CONF_OVERRIDE_TYPE: "now"}),
({CONF_OVERRIDE_TYPE: "constant"}, {CONF_OVERRIDE_TYPE: "constant"}),
({}, {}),
],
ids=["Constant", "Now", "already_lowercase", "no_options"],
)
async def test_migrate_options_lowercases_override_type(
async def test_migrate_options(
hass: HomeAssistant,
mock_nobo_class: MagicMock,
stored_value: str,
expected_value: str,
stored_options: dict[str, str],
expected_options: dict[str, str],
) -> None:
"""Legacy capitalized override_type values are lowercased on migration."""
"""Migrating from minor_version 1 lowercases override_type and bumps version."""
entry = MockConfigEntry(
domain=DOMAIN,
title="My Eco Hub",
@@ -180,9 +139,8 @@ async def test_migrate_options_lowercases_override_type(
data={
CONF_SERIAL: SERIAL,
CONF_IP_ADDRESS: STORED_IP,
CONF_AUTO_DISCOVERED: False,
},
options={CONF_OVERRIDE_TYPE: stored_value},
options=stored_options,
version=1,
minor_version=1,
)
@@ -190,15 +148,15 @@ async def test_migrate_options_lowercases_override_type(
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.minor_version == 2
assert entry.options == {CONF_OVERRIDE_TYPE: expected_value}
assert entry.minor_version == 3
assert entry.options == expected_options
async def test_migrate_options_without_override_type(
async def test_migrate_data_drops_auto_discovered(
hass: HomeAssistant,
mock_nobo_class: MagicMock,
) -> None:
"""Migration still bumps the version when no override_type is stored."""
"""The auto_discovered key is stripped from entry.data on migration."""
entry = MockConfigEntry(
domain=DOMAIN,
title="My Eco Hub",
@@ -206,16 +164,17 @@ async def test_migrate_options_without_override_type(
data={
CONF_SERIAL: SERIAL,
CONF_IP_ADDRESS: STORED_IP,
CONF_AUTO_DISCOVERED: False,
"auto_discovered": True,
},
version=1,
minor_version=1,
minor_version=2,
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.minor_version == 2
assert entry.minor_version == 3
assert entry.data == {CONF_SERIAL: SERIAL, CONF_IP_ADDRESS: STORED_IP}
assert entry.options == {}