diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index a5015e9fc8c..b0a78a1ff88 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -398,14 +398,12 @@ class ZeroconfDiscovery: entry: config_entries.ConfigEntry, ) -> None: """Handle config entry changes.""" - if entry.source != config_entries.SOURCE_IGNORE: - return for discovery_key in entry.discovery_keys[DOMAIN]: if discovery_key.version != 1: continue _type = discovery_key.key[0] name = discovery_key.key[1] - _LOGGER.debug("Rediscover unignored service %s.%s", _type, name) + _LOGGER.debug("Rediscover service %s.%s", _type, name) self._async_service_update(self.zeroconf, _type, name) def _async_dismiss_discoveries(self, name: str) -> None: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 5df7e9b9cb0..be7f74582eb 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1388,7 +1388,6 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): result["handler"], unique_id ) ) - and entry.source == SOURCE_IGNORE and discovery_key not in ( known_discovery_keys := entry.discovery_keys.get( diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 935af9a339e..8dd8d118d72 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1456,10 +1456,12 @@ async def test_zeroconf_removed(hass: HomeAssistant) -> None: ), ], ) +@pytest.mark.parametrize("entry_source", [config_entries.SOURCE_IGNORE]) async def test_zeroconf_rediscover( hass: HomeAssistant, entry_domain: str, entry_discovery_keys: tuple, + entry_source: str, ) -> None: """Test we reinitiate flows when an ignored config entry is removed.""" @@ -1477,7 +1479,7 @@ async def test_zeroconf_rediscover( discovery_keys=entry_discovery_keys, unique_id="mock-unique-id", state=config_entries.ConfigEntryState.LOADED, - source=config_entries.SOURCE_IGNORE, + source=entry_source, ) entry.add_to_hass(hass) @@ -1534,6 +1536,145 @@ async def test_zeroconf_rediscover( assert mock_config_flow.mock_calls[2][2]["context"] == expected_context +@pytest.mark.usefixtures("mock_async_zeroconf") +@pytest.mark.parametrize( + ( + "entry_domain", + "entry_discovery_keys", + ), + [ + # Matching discovery key + ( + "shelly", + { + "zeroconf": ( + DiscoveryKey( + domain="zeroconf", + key=("_http._tcp.local.", "Shelly108._http._tcp.local."), + version=1, + ), + ) + }, + ), + # Matching discovery key + ( + "shelly", + { + "zeroconf": ( + DiscoveryKey( + domain="zeroconf", + key=("_http._tcp.local.", "Shelly108._http._tcp.local."), + version=1, + ), + ), + "other": ( + DiscoveryKey( + domain="other", + key="blah", + version=1, + ), + ), + }, + ), + # Matching discovery key, other domain + # Note: Rediscovery is not currently restricted to the domain of the removed + # entry. Such a check can be added if needed. + ( + "comp", + { + "zeroconf": ( + DiscoveryKey( + domain="zeroconf", + key=("_http._tcp.local.", "Shelly108._http._tcp.local."), + version=1, + ), + ) + }, + ), + ], +) +@pytest.mark.parametrize( + "entry_source", [config_entries.SOURCE_USER, config_entries.SOURCE_ZEROCONF] +) +async def test_zeroconf_rediscover_2( + hass: HomeAssistant, + entry_domain: str, + entry_discovery_keys: tuple, + entry_source: str, +) -> None: + """Test we reinitiate flows when an ignored config entry is removed. + + This test can be merged with test_zeroconf_rediscover when + async_step_unignore has been removed from the ConfigFlow base class. + """ + + def http_only_service_update_mock(zeroconf, services, handlers): + """Call service update handler.""" + handlers[0]( + zeroconf, + "_http._tcp.local.", + "Shelly108._http._tcp.local.", + ServiceStateChange.Added, + ) + + entry = MockConfigEntry( + domain=entry_domain, + discovery_keys=entry_discovery_keys, + unique_id="mock-unique-id", + state=config_entries.ConfigEntryState.LOADED, + source=entry_source, + ) + entry.add_to_hass(hass) + + with ( + patch.dict( + zc_gen.ZEROCONF, + { + "_http._tcp.local.": [ + { + "domain": "shelly", + "name": "shelly*", + "properties": {"macaddress": "ffaadd*"}, + } + ] + }, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), + ), + ): + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + expected_context = { + "discovery_key": DiscoveryKey( + domain="zeroconf", + key=("_http._tcp.local.", "Shelly108._http._tcp.local."), + version=1, + ), + "source": "zeroconf", + } + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "shelly" + assert mock_config_flow.mock_calls[0][2]["context"] == expected_context + + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 2 + assert mock_config_flow.mock_calls[1][1][0] == "shelly" + assert mock_config_flow.mock_calls[1][2]["context"] == expected_context + + @pytest.mark.usefixtures("mock_async_zeroconf") @pytest.mark.parametrize( ( @@ -1654,110 +1795,3 @@ async def test_zeroconf_rediscover_no_match( assert mock_config_flow.mock_calls[1][2]["context"] == { "source": "unignore", } - - -@pytest.mark.usefixtures("mock_async_zeroconf") -@pytest.mark.parametrize( - ( - "entry_domain", - "entry_discovery_keys", - "entry_source", - "entry_unique_id", - ), - [ - # Source not SOURCE_IGNORE - ( - "shelly", - { - "zeroconf": ( - DiscoveryKey( - domain="zeroconf", - key=("_http._tcp.local.", "Shelly108._http._tcp.local."), - version=1, - ), - ) - }, - config_entries.SOURCE_ZEROCONF, - "mock-unique-id", - ), - ], -) -async def test_zeroconf_rediscover_no_match_2( - hass: HomeAssistant, - entry_domain: str, - entry_discovery_keys: tuple, - entry_source: str, - entry_unique_id: str, -) -> None: - """Test we don't reinitiate flows when a non matching config entry is removed. - - This test can be merged with test_zeroconf_rediscover_no_match when - async_step_unignore has been removed from the ConfigFlow base class. - """ - - def http_only_service_update_mock(zeroconf, services, handlers): - """Call service update handler.""" - handlers[0]( - zeroconf, - "_http._tcp.local.", - "Shelly108._http._tcp.local.", - ServiceStateChange.Added, - ) - - hass.config.components.add(entry_domain) - mock_integration(hass, MockModule(entry_domain)) - - entry = MockConfigEntry( - domain=entry_domain, - discovery_keys=entry_discovery_keys, - unique_id=entry_unique_id, - state=config_entries.ConfigEntryState.LOADED, - source=entry_source, - ) - entry.add_to_hass(hass) - - with ( - patch.dict( - zc_gen.ZEROCONF, - { - "_http._tcp.local.": [ - { - "domain": "shelly", - "name": "shelly*", - "properties": {"macaddress": "ffaadd*"}, - } - ] - }, - clear=True, - ), - patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, - patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser, - patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), - ), - ): - assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - expected_context = { - "discovery_key": DiscoveryKey( - domain="zeroconf", - key=("_http._tcp.local.", "Shelly108._http._tcp.local."), - version=1, - ), - "source": "zeroconf", - } - assert len(mock_service_browser.mock_calls) == 1 - assert len(mock_config_flow.mock_calls) == 1 - assert mock_config_flow.mock_calls[0][1][0] == "shelly" - assert mock_config_flow.mock_calls[0][2]["context"] == expected_context - - await hass.config_entries.async_remove(entry.entry_id) - await hass.async_block_till_done() - - assert len(mock_service_browser.mock_calls) == 1 - assert len(mock_config_flow.mock_calls) == 1 diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 57730a9f014..53bcb459c60 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2911,7 +2911,6 @@ async def test_manual_add_overrides_ignored_entry_singleton( @pytest.mark.parametrize( ( "discovery_keys", - "entry_source", "entry_unique_id", "flow_context", "flow_source", @@ -2922,7 +2921,6 @@ async def test_manual_add_overrides_ignored_entry_singleton( # No discovery key ( {}, - config_entries.SOURCE_IGNORE, "mock-unique-id", {}, config_entries.SOURCE_ZEROCONF, @@ -2932,7 +2930,6 @@ async def test_manual_add_overrides_ignored_entry_singleton( # Discovery key added to ignored entry data ( {}, - config_entries.SOURCE_IGNORE, "mock-unique-id", {"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)}, config_entries.SOURCE_ZEROCONF, @@ -2942,7 +2939,6 @@ async def test_manual_add_overrides_ignored_entry_singleton( # Discovery key added to ignored entry data ( {"test": (DiscoveryKey(domain="test", key="bleh", version=1),)}, - config_entries.SOURCE_IGNORE, "mock-unique-id", {"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)}, config_entries.SOURCE_ZEROCONF, @@ -2970,7 +2966,6 @@ async def test_manual_add_overrides_ignored_entry_singleton( DiscoveryKey(domain="test", key="10", version=1), ) }, - config_entries.SOURCE_IGNORE, "mock-unique-id", {"discovery_key": DiscoveryKey(domain="test", key="11", version=1)}, config_entries.SOURCE_ZEROCONF, @@ -2993,33 +2988,102 @@ async def test_manual_add_overrides_ignored_entry_singleton( # Discovery key already in ignored entry data ( {"test": (DiscoveryKey(domain="test", key="blah", version=1),)}, - config_entries.SOURCE_IGNORE, "mock-unique-id", {"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)}, config_entries.SOURCE_ZEROCONF, data_entry_flow.FlowResultType.ABORT, {"test": (DiscoveryKey(domain="test", key="blah", version=1),)}, ), - # Discovery key not added to user entry data - ( - {}, - config_entries.SOURCE_USER, - "mock-unique-id", - {"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)}, - config_entries.SOURCE_ZEROCONF, - data_entry_flow.FlowResultType.ABORT, - {}, - ), # Flow not aborted when unique id is not matching ( {}, - config_entries.SOURCE_IGNORE, "mock-unique-id-2", {"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)}, config_entries.SOURCE_ZEROCONF, data_entry_flow.FlowResultType.FORM, {}, ), + ], +) +@pytest.mark.parametrize( + "entry_source", + [ + config_entries.SOURCE_IGNORE, + config_entries.SOURCE_USER, + config_entries.SOURCE_ZEROCONF, + ], +) +async def test_update_discovery_keys( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + discovery_keys: tuple, + entry_source: str, + entry_unique_id: str, + flow_context: dict, + flow_source: str, + flow_result: data_entry_flow.FlowResultType, + updated_discovery_keys: tuple, +) -> None: + """Test that discovery keys of an entry can be updated.""" + hass.config.components.add("comp") + entry = MockConfigEntry( + domain="comp", + discovery_keys=discovery_keys, + unique_id=entry_unique_id, + state=config_entries.ConfigEntryState.LOADED, + source=entry_source, + ) + entry.add_to_hass(hass) + + mock_integration(hass, MockModule("comp")) + mock_platform(hass, "comp.config_flow", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + await self.async_set_unique_id("mock-unique-id") + self._abort_if_unique_id_configured(reload_on_update=False) + return self.async_show_form(step_id="step2") + + async def async_step_step2(self, user_input=None): + raise NotImplementedError + + async def async_step_zeroconf(self, discovery_info=None): + """Test zeroconf step.""" + return await self.async_step_user(discovery_info) + + with ( + mock_config_flow("comp", TestFlow), + patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload, + ): + result = await manager.flow.async_init( + "comp", context={"source": flow_source} | flow_context + ) + await hass.async_block_till_done() + + assert result["type"] == flow_result + assert entry.data == {} + assert entry.discovery_keys == updated_discovery_keys + assert len(async_reload.mock_calls) == 0 + + +@pytest.mark.parametrize( + ( + "discovery_keys", + "entry_source", + "entry_unique_id", + "flow_context", + "flow_source", + "flow_result", + "updated_discovery_keys", + ), + [ # Flow not aborted when user initiated flow ( {}, @@ -3032,7 +3096,7 @@ async def test_manual_add_overrides_ignored_entry_singleton( ), ], ) -async def test_ignored_entry_update_discovery_keys( +async def test_update_discovery_keys_2( hass: HomeAssistant, manager: config_entries.ConfigEntries, discovery_keys: tuple, @@ -3043,7 +3107,7 @@ async def test_ignored_entry_update_discovery_keys( flow_result: data_entry_flow.FlowResultType, updated_discovery_keys: tuple, ) -> None: - """Test that discovery keys of an ignored entry can be updated.""" + """Test that discovery keys of an entry can be updated.""" hass.config.components.add("comp") entry = MockConfigEntry( domain="comp",