diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 484d4404a66a6..ceaba3f02a12d 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,7 +1,8 @@ """Support for exposing Home Assistant via Zeroconf.""" from __future__ import annotations -from collections.abc import Iterable +import asyncio +from collections.abc import Coroutine, Iterable from contextlib import suppress import fnmatch import ipaddress @@ -13,7 +14,6 @@ from pyroute2 import IPRoute import voluptuous as vol from zeroconf import ( - Error as ZeroconfError, InterfaceChoice, IPVersion, NonUniqueNameException, @@ -29,7 +29,8 @@ EVENT_HOMEASSISTANT_STOP, __version__, ) -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass @@ -90,6 +91,14 @@ class HaServiceInfo(TypedDict): properties: dict[str, Any] +class ZeroconfFlow(TypedDict): + """A queued zeroconf discovery flow.""" + + domain: str + context: dict[str, Any] + data: HaServiceInfo + + @bind_hass async def async_get_instance(hass: HomeAssistant) -> HaZeroconf: """Zeroconf instance to be shared with other integrations that use it.""" @@ -183,6 +192,12 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: aio_zc = await _async_get_instance(hass, **zc_args) zeroconf = aio_zc.zeroconf + zeroconf_types, homekit_models = await asyncio.gather( + async_get_zeroconf(hass), async_get_homekit(hass) + ) + discovery = ZeroconfDiscovery(hass, zeroconf, zeroconf_types, homekit_models) + await discovery.async_setup() + async def _async_zeroconf_hass_start(_event: Event) -> None: """Expose Home Assistant on zeroconf when it starts. @@ -191,15 +206,17 @@ async def _async_zeroconf_hass_start(_event: Event) -> None: uuid = await hass.helpers.instance_id.async_get() await _async_register_hass_zc_service(hass, aio_zc, uuid) - async def _async_zeroconf_hass_started(_event: Event) -> None: - """Start the service browser.""" + @callback + def _async_start_discovery(_event: Event) -> None: + """Start processing flows.""" + discovery.async_start() - await _async_start_zeroconf_browser(hass, zeroconf) + async def _async_zeroconf_hass_stop(_event: Event) -> None: + await discovery.async_stop() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_zeroconf_hass_stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_zeroconf_hass_start) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, _async_zeroconf_hass_started - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_start_discovery) return True @@ -259,44 +276,98 @@ async def _async_register_hass_zc_service( ) -async def _async_start_zeroconf_browser( - hass: HomeAssistant, zeroconf: Zeroconf -) -> None: - """Start the zeroconf browser.""" +class FlowDispatcher: + """Dispatch discovery flows.""" + + def __init__(self, hass: HomeAssistant): + """Init the discovery dispatcher.""" + self.hass = hass + self.pending_flows: list[ZeroconfFlow] = [] + self.started = False + + @callback + def async_start(self) -> None: + """Start processing pending flows.""" + self.started = True + self.hass.loop.call_soon(self._async_process_pending_flows) + + def _async_process_pending_flows(self) -> None: + for flow in self.pending_flows: + self.hass.async_create_task(self._init_flow(flow)) + self.pending_flows = [] + + def create(self, flow: ZeroconfFlow) -> None: + """Create and add or queue a flow.""" + if self.started: + self.hass.create_task(self._init_flow(flow)) + else: + self.pending_flows.append(flow) + + def _init_flow(self, flow: ZeroconfFlow) -> Coroutine[None, None, FlowResult]: + """Create a flow.""" + return self.hass.config_entries.flow.async_init( + flow["domain"], context=flow["context"], data=flow["data"] + ) + - zeroconf_types = await async_get_zeroconf(hass) - homekit_models = await async_get_homekit(hass) +class ZeroconfDiscovery: + """Discovery via zeroconf.""" - types = list(zeroconf_types) + def __init__( + self, + hass: HomeAssistant, + zeroconf: Zeroconf, + zeroconf_types: dict[str, list[dict[str, str]]], + homekit_models: dict[str, str], + ) -> None: + """Init discovery.""" + self.hass = hass + self.zeroconf = zeroconf + self.zeroconf_types = zeroconf_types + self.homekit_models = homekit_models + + self.flow_dispatcher: FlowDispatcher | None = None + self.service_browser: HaServiceBrowser | None = None + + async def async_setup(self) -> None: + """Start discovery.""" + self.flow_dispatcher = FlowDispatcher(self.hass) + types = list(self.zeroconf_types) + # We want to make sure we know about other HomeAssistant + # instances as soon as possible to avoid name conflicts + # so we always browse for ZEROCONF_TYPE + for hk_type in (ZEROCONF_TYPE, *HOMEKIT_TYPES): + if hk_type not in self.zeroconf_types: + types.append(hk_type) + _LOGGER.debug("Starting Zeroconf browser") + self.service_browser = HaServiceBrowser( + self.zeroconf, types, handlers=[self.service_update] + ) + + async def async_stop(self) -> None: + """Cancel the service browser and stop processing the queue.""" + if self.service_browser: + await self.hass.async_add_executor_job(self.service_browser.cancel) - for hk_type in HOMEKIT_TYPES: - if hk_type not in zeroconf_types: - types.append(hk_type) + @callback + def async_start(self) -> None: + """Start processing discovery flows.""" + assert self.flow_dispatcher is not None + self.flow_dispatcher.async_start() def service_update( + self, zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange, ) -> None: """Service state changed.""" - nonlocal zeroconf_types - nonlocal homekit_models - if state_change == ServiceStateChange.Removed: return - try: - service_info = zeroconf.get_service_info(service_type, name) - except ZeroconfError: - _LOGGER.exception("Failed to get info for device %s", name) - return - - if not service_info: - # Prevent the browser thread from collapsing as - # service_info can be None - _LOGGER.debug("Failed to get info for device %s", name) - return + service_info = ServiceInfo(service_type, name) + service_info.load_from_cache(zeroconf) info = info_from_service(service_info) if not info: @@ -305,10 +376,12 @@ def service_update( return _LOGGER.debug("Discovered new device %s %s", name, info) + assert self.flow_dispatcher is not None # If we can handle it as a HomeKit discovery, we do that here. if service_type in HOMEKIT_TYPES: - discovery_was_forwarded = handle_homekit(hass, homekit_models, info) + if pending_flow := handle_homekit(self.hass, self.homekit_models, info): + self.flow_dispatcher.create(pending_flow) # Continue on here as homekit_controller # still needs to get updates on devices # so it can see when the 'c#' field is updated. @@ -316,10 +389,7 @@ def service_update( # We only send updates to homekit_controller # if the device is already paired in order to avoid # offering a second discovery for the same device - if ( - discovery_was_forwarded - and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"] - ): + if pending_flow and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"]: try: # 0 means paired and not discoverable by iOS clients) if int(info["properties"][HOMEKIT_PAIRED_STATUS_FLAG]): @@ -348,7 +418,7 @@ def service_update( # Not all homekit types are currently used for discovery # so not all service type exist in zeroconf_types - for matcher in zeroconf_types.get(service_type, []): + for matcher in self.zeroconf_types.get(service_type, []): if len(matcher) > 1: if "macaddress" in matcher and ( uppercase_mac is None @@ -368,19 +438,17 @@ def service_update( ): continue - hass.add_job( - hass.config_entries.flow.async_init( - matcher["domain"], context={"source": DOMAIN}, data=info - ) # type: ignore - ) - - _LOGGER.debug("Starting Zeroconf browser") - HaServiceBrowser(zeroconf, types, handlers=[service_update]) + flow: ZeroconfFlow = { + "domain": matcher["domain"], + "context": {"source": config_entries.SOURCE_ZEROCONF}, + "data": info, + } + self.flow_dispatcher.create(flow) def handle_homekit( hass: HomeAssistant, homekit_models: dict[str, str], info: HaServiceInfo -) -> bool: +) -> ZeroconfFlow | None: """Handle a HomeKit discovery. Return if discovery was forwarded. @@ -394,7 +462,7 @@ def handle_homekit( break if model is None: - return False + return None for test_model in homekit_models: if ( @@ -404,16 +472,13 @@ def handle_homekit( ): continue - hass.add_job( - hass.config_entries.flow.async_init( - homekit_models[test_model], - context={"source": config_entries.SOURCE_HOMEKIT}, - data=info, - ) # type: ignore - ) - return True + return { + "domain": homekit_models[test_model], + "context": {"source": config_entries.SOURCE_HOMEKIT}, + "data": info, + } - return False + return None def info_from_service(service: ServiceInfo) -> HaServiceInfo | None: diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index d76639f3b5b5e..3abd8824eba44 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.30.0","pyroute2==0.5.18"], + "requirements": ["zeroconf==0.31.0","pyroute2==0.5.18"], "dependencies": ["api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0d51642db3954..9755a5b2e5dc4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ sqlalchemy==1.4.13 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.30.0 +zeroconf==0.31.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 2c427851d015c..4a14eb63e6f05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2402,7 +2402,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.30.0 +zeroconf==0.31.0 # homeassistant.components.zha zha-quirks==0.0.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67a4b3f20cd6a..fd825852e90f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1290,7 +1290,7 @@ yeelight==0.6.2 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.30.0 +zeroconf==0.31.0 # homeassistant.components.zha zha-quirks==0.0.57 diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 809177b6089af..d8a9a94da96f5 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,14 +1,7 @@ """Test Zeroconf component setup process.""" from unittest.mock import patch -from zeroconf import ( - BadTypeInNameException, - Error as ZeroconfError, - InterfaceChoice, - IPVersion, - ServiceInfo, - ServiceStateChange, -) +from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange from homeassistant.components import zeroconf from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6 @@ -149,8 +142,10 @@ async def test_setup(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_service_info_mock + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -181,8 +176,9 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog): hass.config, "location_name", "\u00dcBER \u00dcber German Umlaut long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string", + ), patch( + "homeassistant.components.zeroconf.ServiceInfo.request", ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -195,8 +191,10 @@ async def test_setup_with_default_interface(hass, mock_zeroconf): """Test default interface config.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: True}} ) @@ -210,8 +208,10 @@ async def test_setup_without_default_interface(hass, mock_zeroconf): """Test without default interface config.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: False}} ) @@ -223,8 +223,10 @@ async def test_setup_without_ipv6(hass, mock_zeroconf): """Test without ipv6.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_IPV6: False}} ) @@ -238,8 +240,10 @@ async def test_setup_with_ipv6(hass, mock_zeroconf): """Test without ipv6.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_IPV6: True}} ) @@ -253,8 +257,10 @@ async def test_setup_with_ipv6_default(hass, mock_zeroconf): """Test without ipv6 as default.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -262,20 +268,6 @@ async def test_setup_with_ipv6_default(hass, mock_zeroconf): assert mock_zeroconf.called_with() -async def test_service_with_invalid_name(hass, mock_zeroconf, caplog): - """Test we do not crash on service with an invalid name.""" - with patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = BadTypeInNameException - assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - assert len(mock_service_browser.mock_calls) == 1 - assert "Failed to get info for device" in caplog.text - - async def test_zeroconf_match_macaddress(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" @@ -300,10 +292,10 @@ def http_only_service_update_mock(zeroconf, services, handlers): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( - "FFAADDCC11DD" - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + 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() @@ -333,10 +325,10 @@ def http_only_service_update_mock(zeroconf, services, handlers): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = ( - get_zeroconf_info_mock_manufacturer("Samsung Electronics") - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock_manufacturer("Samsung Electronics"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -366,10 +358,10 @@ def http_only_service_update_mock(zeroconf, services, handlers): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( - "aa:bb:cc:dd:ee:ff" - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock("aabbccddeeff"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -398,10 +390,10 @@ def http_only_service_update_mock(zeroconf, services, handlers): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( - "FFAADDCC11DD" - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + 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() @@ -430,10 +422,10 @@ def http_only_service_update_mock(zeroconf, services, handlers): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = ( - get_zeroconf_info_mock_manufacturer("Not Samsung Electronics") - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock_manufacturer("Not Samsung Electronics"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -456,10 +448,10 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "LIFX bulb", HOMEKIT_STATUS_UNPAIRED - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("LIFX bulb", HOMEKIT_STATUS_UNPAIRED), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -483,10 +475,10 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._udp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -510,10 +502,10 @@ async def test_homekit_match_full(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._udp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "BSB002", HOMEKIT_STATUS_UNPAIRED - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -537,10 +529,10 @@ async def test_homekit_already_paired(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "tado", HOMEKIT_STATUS_PAIRED - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("tado", HOMEKIT_STATUS_PAIRED), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -565,10 +557,10 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "tado", b"invalid" - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("tado", b"invalid"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -588,10 +580,12 @@ async def test_homekit_not_paired(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock( "this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED - ) + ), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -636,34 +630,44 @@ async def test_get_instance(hass, mock_zeroconf): async def test_removed_ignored(hass, mock_zeroconf): """Test we remove it when a zeroconf entry is removed.""" - mock_zeroconf.get_service_info.side_effect = ZeroconfError def service_update_mock(zeroconf, services, handlers): """Call service update handler.""" handlers[0]( - zeroconf, "_service.added", "name._service.added", ServiceStateChange.Added + zeroconf, + "_service.added.local.", + "name._service.added.local.", + ServiceStateChange.Added, ) handlers[0]( zeroconf, - "_service.updated", - "name._service.updated", + "_service.updated.local.", + "name._service.updated.local.", ServiceStateChange.Updated, ) handlers[0]( zeroconf, - "_service.removed", - "name._service.removed", + "_service.removed.local.", + "name._service.removed.local.", ServiceStateChange.Removed, ) - with patch.object(zeroconf, "HaServiceBrowser", side_effect=service_update_mock): + with patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, + ) as mock_service_info: assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert len(mock_zeroconf.get_service_info.mock_calls) == 2 - assert mock_zeroconf.get_service_info.mock_calls[0][1][0] == "_service.added" - assert mock_zeroconf.get_service_info.mock_calls[1][1][0] == "_service.updated" + assert len(mock_service_info.mock_calls) == 2 + import pprint + + pprint.pprint(mock_service_info.mock_calls[0][1]) + assert mock_service_info.mock_calls[0][1][0] == "_service.added.local." + assert mock_service_info.mock_calls[1][1][0] == "_service.updated.local." async def test_async_detect_interfaces_setting_non_loopback_route(hass, mock_zeroconf): @@ -673,8 +677,10 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, mock_zer ), patch( "homeassistant.components.zeroconf.IPRoute.route", return_value=_ROUTE_NO_LOOPBACK, + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -688,8 +694,10 @@ async def test_async_detect_interfaces_setting_loopback_route(hass, mock_zerocon zeroconf, "HaServiceBrowser", side_effect=service_update_mock ), patch( "homeassistant.components.zeroconf.IPRoute.route", return_value=_ROUTE_LOOPBACK + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -701,8 +709,10 @@ async def test_async_detect_interfaces_setting_empty_route(hass, mock_zeroconf): """Test without default interface config and the route returns nothing.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ), patch("homeassistant.components.zeroconf.IPRoute.route", return_value=[]): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock + ), patch("homeassistant.components.zeroconf.IPRoute.route", return_value=[]), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -716,8 +726,10 @@ async def test_async_detect_interfaces_setting_exception(hass, mock_zeroconf): zeroconf, "HaServiceBrowser", side_effect=service_update_mock ), patch( "homeassistant.components.zeroconf.IPRoute.route", side_effect=AttributeError + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done()