From 6c0895099536207517c028edd903b00a07ccb6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Thu, 30 Apr 2026 18:29:13 +0200 Subject: [PATCH] Drop auto_discovered config in nobo_hub (#169558) --- homeassistant/components/nobo_hub/__init__.py | 29 ++--- .../components/nobo_hub/config_flow.py | 10 +- homeassistant/components/nobo_hub/const.py | 1 - .../components/nobo_hub/strings.json | 10 +- tests/components/nobo_hub/conftest.py | 14 +-- tests/components/nobo_hub/test_config_flow.py | 3 - tests/components/nobo_hub/test_init.py | 109 ++++++------------ 7 files changed, 56 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/nobo_hub/__init__.py b/homeassistant/components/nobo_hub/__init__.py index cff8f29149c..9790eb6fc1a 100644 --- a/homeassistant/components/nobo_hub/__init__.py +++ b/homeassistant/components/nobo_hub/__init__.py @@ -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 diff --git a/homeassistant/components/nobo_hub/config_flow.py b/homeassistant/components/nobo_hub/config_flow.py index 2839d4280e9..8d52925d255 100644 --- a/homeassistant/components/nobo_hub/config_flow.py +++ b/homeassistant/components/nobo_hub/config_flow.py @@ -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, }, ) diff --git a/homeassistant/components/nobo_hub/const.py b/homeassistant/components/nobo_hub/const.py index bf7fe018f50..8b231f76a7a 100644 --- a/homeassistant/components/nobo_hub/const.py +++ b/homeassistant/components/nobo_hub/const.py @@ -2,7 +2,6 @@ DOMAIN = "nobo_hub" -CONF_AUTO_DISCOVERED = "auto_discovered" CONF_SERIAL = "serial" CONF_OVERRIDE_TYPE = "override_type" OVERRIDE_TYPE_CONSTANT = "constant" diff --git a/homeassistant/components/nobo_hub/strings.json b/homeassistant/components/nobo_hub/strings.json index 791ccc4cbda..6abe2a4a29d 100644 --- a/homeassistant/components/nobo_hub/strings.json +++ b/homeassistant/components/nobo_hub/strings.json @@ -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": { diff --git a/tests/components/nobo_hub/conftest.py b/tests/components/nobo_hub/conftest.py index d3df30ec001..7df91c55130 100644 --- a/tests/components/nobo_hub/conftest.py +++ b/tests/components/nobo_hub/conftest.py @@ -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, ) diff --git a/tests/components/nobo_hub/test_config_flow.py b/tests/components/nobo_hub/test_config_flow.py index b087be516de..b92de2865b0 100644 --- a/tests/components/nobo_hub/test_config_flow.py +++ b/tests/components/nobo_hub/test_config_flow.py @@ -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() diff --git a/tests/components/nobo_hub/test_init.py b/tests/components/nobo_hub/test_init.py index b4d54485e97..a3681aad61d 100644 --- a/tests/components/nobo_hub/test_init.py +++ b/tests/components/nobo_hub/test_init.py @@ -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 == {}