Reinitialize zeroconf discovery flow on config entry removal (#126595)

This commit is contained in:
Erik Montnemery 2024-09-24 13:37:28 +02:00 committed by GitHub
parent 004941cc57
commit 589910b49b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 226 additions and 131 deletions

View File

@ -398,14 +398,12 @@ class ZeroconfDiscovery:
entry: config_entries.ConfigEntry, entry: config_entries.ConfigEntry,
) -> None: ) -> None:
"""Handle config entry changes.""" """Handle config entry changes."""
if entry.source != config_entries.SOURCE_IGNORE:
return
for discovery_key in entry.discovery_keys[DOMAIN]: for discovery_key in entry.discovery_keys[DOMAIN]:
if discovery_key.version != 1: if discovery_key.version != 1:
continue continue
_type = discovery_key.key[0] _type = discovery_key.key[0]
name = discovery_key.key[1] 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) self._async_service_update(self.zeroconf, _type, name)
def _async_dismiss_discoveries(self, name: str) -> None: def _async_dismiss_discoveries(self, name: str) -> None:

View File

@ -1388,7 +1388,6 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]):
result["handler"], unique_id result["handler"], unique_id
) )
) )
and entry.source == SOURCE_IGNORE
and discovery_key and discovery_key
not in ( not in (
known_discovery_keys := entry.discovery_keys.get( known_discovery_keys := entry.discovery_keys.get(

View File

@ -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( async def test_zeroconf_rediscover(
hass: HomeAssistant, hass: HomeAssistant,
entry_domain: str, entry_domain: str,
entry_discovery_keys: tuple, entry_discovery_keys: tuple,
entry_source: str,
) -> None: ) -> None:
"""Test we reinitiate flows when an ignored config entry is removed.""" """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, discovery_keys=entry_discovery_keys,
unique_id="mock-unique-id", unique_id="mock-unique-id",
state=config_entries.ConfigEntryState.LOADED, state=config_entries.ConfigEntryState.LOADED,
source=config_entries.SOURCE_IGNORE, source=entry_source,
) )
entry.add_to_hass(hass) 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 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.usefixtures("mock_async_zeroconf")
@pytest.mark.parametrize( @pytest.mark.parametrize(
( (
@ -1654,110 +1795,3 @@ async def test_zeroconf_rediscover_no_match(
assert mock_config_flow.mock_calls[1][2]["context"] == { assert mock_config_flow.mock_calls[1][2]["context"] == {
"source": "unignore", "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

View File

@ -2911,7 +2911,6 @@ async def test_manual_add_overrides_ignored_entry_singleton(
@pytest.mark.parametrize( @pytest.mark.parametrize(
( (
"discovery_keys", "discovery_keys",
"entry_source",
"entry_unique_id", "entry_unique_id",
"flow_context", "flow_context",
"flow_source", "flow_source",
@ -2922,7 +2921,6 @@ async def test_manual_add_overrides_ignored_entry_singleton(
# No discovery key # No discovery key
( (
{}, {},
config_entries.SOURCE_IGNORE,
"mock-unique-id", "mock-unique-id",
{}, {},
config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_ZEROCONF,
@ -2932,7 +2930,6 @@ async def test_manual_add_overrides_ignored_entry_singleton(
# Discovery key added to ignored entry data # Discovery key added to ignored entry data
( (
{}, {},
config_entries.SOURCE_IGNORE,
"mock-unique-id", "mock-unique-id",
{"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)}, {"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)},
config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_ZEROCONF,
@ -2942,7 +2939,6 @@ async def test_manual_add_overrides_ignored_entry_singleton(
# Discovery key added to ignored entry data # Discovery key added to ignored entry data
( (
{"test": (DiscoveryKey(domain="test", key="bleh", version=1),)}, {"test": (DiscoveryKey(domain="test", key="bleh", version=1),)},
config_entries.SOURCE_IGNORE,
"mock-unique-id", "mock-unique-id",
{"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)}, {"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)},
config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_ZEROCONF,
@ -2970,7 +2966,6 @@ async def test_manual_add_overrides_ignored_entry_singleton(
DiscoveryKey(domain="test", key="10", version=1), DiscoveryKey(domain="test", key="10", version=1),
) )
}, },
config_entries.SOURCE_IGNORE,
"mock-unique-id", "mock-unique-id",
{"discovery_key": DiscoveryKey(domain="test", key="11", version=1)}, {"discovery_key": DiscoveryKey(domain="test", key="11", version=1)},
config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_ZEROCONF,
@ -2993,33 +2988,102 @@ async def test_manual_add_overrides_ignored_entry_singleton(
# Discovery key already in ignored entry data # Discovery key already in ignored entry data
( (
{"test": (DiscoveryKey(domain="test", key="blah", version=1),)}, {"test": (DiscoveryKey(domain="test", key="blah", version=1),)},
config_entries.SOURCE_IGNORE,
"mock-unique-id", "mock-unique-id",
{"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)}, {"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)},
config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_ZEROCONF,
data_entry_flow.FlowResultType.ABORT, data_entry_flow.FlowResultType.ABORT,
{"test": (DiscoveryKey(domain="test", key="blah", version=1),)}, {"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 # Flow not aborted when unique id is not matching
( (
{}, {},
config_entries.SOURCE_IGNORE,
"mock-unique-id-2", "mock-unique-id-2",
{"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)}, {"discovery_key": DiscoveryKey(domain="test", key="blah", version=1)},
config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_ZEROCONF,
data_entry_flow.FlowResultType.FORM, 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 # 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, hass: HomeAssistant,
manager: config_entries.ConfigEntries, manager: config_entries.ConfigEntries,
discovery_keys: tuple, discovery_keys: tuple,
@ -3043,7 +3107,7 @@ async def test_ignored_entry_update_discovery_keys(
flow_result: data_entry_flow.FlowResultType, flow_result: data_entry_flow.FlowResultType,
updated_discovery_keys: tuple, updated_discovery_keys: tuple,
) -> None: ) -> 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") hass.config.components.add("comp")
entry = MockConfigEntry( entry = MockConfigEntry(
domain="comp", domain="comp",