Start ServiceBrowser as soon as possible in zeroconf (#50784)

Co-authored-by: Ruslan Sayfutdinov <ruslan@sayfutdinov.com>
This commit is contained in:
J. Nick Koston 2021-05-17 22:51:05 -05:00 committed by GitHub
parent 7a60d0eae4
commit 3cc3cacd08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 227 additions and 150 deletions

View file

@ -1,7 +1,8 @@
"""Support for exposing Home Assistant via Zeroconf.""" """Support for exposing Home Assistant via Zeroconf."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterable import asyncio
from collections.abc import Coroutine, Iterable
from contextlib import suppress from contextlib import suppress
import fnmatch import fnmatch
import ipaddress import ipaddress
@ -13,7 +14,6 @@ from typing import Any, TypedDict, cast
from pyroute2 import IPRoute from pyroute2 import IPRoute
import voluptuous as vol import voluptuous as vol
from zeroconf import ( from zeroconf import (
Error as ZeroconfError,
InterfaceChoice, InterfaceChoice,
IPVersion, IPVersion,
NonUniqueNameException, NonUniqueNameException,
@ -29,7 +29,8 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
__version__, __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 import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass
@ -90,6 +91,14 @@ class HaServiceInfo(TypedDict):
properties: dict[str, Any] properties: dict[str, Any]
class ZeroconfFlow(TypedDict):
"""A queued zeroconf discovery flow."""
domain: str
context: dict[str, Any]
data: HaServiceInfo
@bind_hass @bind_hass
async def async_get_instance(hass: HomeAssistant) -> HaZeroconf: async def async_get_instance(hass: HomeAssistant) -> HaZeroconf:
"""Zeroconf instance to be shared with other integrations that use it.""" """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) aio_zc = await _async_get_instance(hass, **zc_args)
zeroconf = aio_zc.zeroconf 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: async def _async_zeroconf_hass_start(_event: Event) -> None:
"""Expose Home Assistant on zeroconf when it starts. """Expose Home Assistant on zeroconf when it starts.
@ -191,15 +206,17 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
uuid = await hass.helpers.instance_id.async_get() uuid = await hass.helpers.instance_id.async_get()
await _async_register_hass_zc_service(hass, aio_zc, uuid) await _async_register_hass_zc_service(hass, aio_zc, uuid)
async def _async_zeroconf_hass_started(_event: Event) -> None: @callback
"""Start the service browser.""" 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_START, _async_zeroconf_hass_start)
hass.bus.async_listen_once( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_start_discovery)
EVENT_HOMEASSISTANT_STARTED, _async_zeroconf_hass_started
)
return True return True
@ -259,44 +276,98 @@ async def _async_register_hass_zc_service(
) )
async def _async_start_zeroconf_browser( class FlowDispatcher:
hass: HomeAssistant, zeroconf: Zeroconf """Dispatch discovery flows."""
) -> None:
"""Start the zeroconf browser."""
zeroconf_types = await async_get_zeroconf(hass) def __init__(self, hass: HomeAssistant):
homekit_models = await async_get_homekit(hass) """Init the discovery dispatcher."""
self.hass = hass
self.pending_flows: list[ZeroconfFlow] = []
self.started = False
types = list(zeroconf_types) @callback
def async_start(self) -> None:
"""Start processing pending flows."""
self.started = True
self.hass.loop.call_soon(self._async_process_pending_flows)
for hk_type in HOMEKIT_TYPES: def _async_process_pending_flows(self) -> None:
if hk_type not in zeroconf_types: for flow in self.pending_flows:
types.append(hk_type) 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"]
)
class ZeroconfDiscovery:
"""Discovery via zeroconf."""
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)
@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( def service_update(
self,
zeroconf: Zeroconf, zeroconf: Zeroconf,
service_type: str, service_type: str,
name: str, name: str,
state_change: ServiceStateChange, state_change: ServiceStateChange,
) -> None: ) -> None:
"""Service state changed.""" """Service state changed."""
nonlocal zeroconf_types
nonlocal homekit_models
if state_change == ServiceStateChange.Removed: if state_change == ServiceStateChange.Removed:
return return
try: service_info = ServiceInfo(service_type, name)
service_info = zeroconf.get_service_info(service_type, name) service_info.load_from_cache(zeroconf)
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
info = info_from_service(service_info) info = info_from_service(service_info)
if not info: if not info:
@ -305,10 +376,12 @@ async def _async_start_zeroconf_browser(
return return
_LOGGER.debug("Discovered new device %s %s", name, info) _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 we can handle it as a HomeKit discovery, we do that here.
if service_type in HOMEKIT_TYPES: 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 # Continue on here as homekit_controller
# still needs to get updates on devices # still needs to get updates on devices
# so it can see when the 'c#' field is updated. # so it can see when the 'c#' field is updated.
@ -316,10 +389,7 @@ async def _async_start_zeroconf_browser(
# We only send updates to homekit_controller # We only send updates to homekit_controller
# if the device is already paired in order to avoid # if the device is already paired in order to avoid
# offering a second discovery for the same device # offering a second discovery for the same device
if ( if pending_flow and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"]:
discovery_was_forwarded
and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"]
):
try: try:
# 0 means paired and not discoverable by iOS clients) # 0 means paired and not discoverable by iOS clients)
if int(info["properties"][HOMEKIT_PAIRED_STATUS_FLAG]): if int(info["properties"][HOMEKIT_PAIRED_STATUS_FLAG]):
@ -348,7 +418,7 @@ async def _async_start_zeroconf_browser(
# Not all homekit types are currently used for discovery # Not all homekit types are currently used for discovery
# so not all service type exist in zeroconf_types # 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 len(matcher) > 1:
if "macaddress" in matcher and ( if "macaddress" in matcher and (
uppercase_mac is None uppercase_mac is None
@ -368,19 +438,17 @@ async def _async_start_zeroconf_browser(
): ):
continue continue
hass.add_job( flow: ZeroconfFlow = {
hass.config_entries.flow.async_init( "domain": matcher["domain"],
matcher["domain"], context={"source": DOMAIN}, data=info "context": {"source": config_entries.SOURCE_ZEROCONF},
) # type: ignore "data": info,
) }
self.flow_dispatcher.create(flow)
_LOGGER.debug("Starting Zeroconf browser")
HaServiceBrowser(zeroconf, types, handlers=[service_update])
def handle_homekit( def handle_homekit(
hass: HomeAssistant, homekit_models: dict[str, str], info: HaServiceInfo hass: HomeAssistant, homekit_models: dict[str, str], info: HaServiceInfo
) -> bool: ) -> ZeroconfFlow | None:
"""Handle a HomeKit discovery. """Handle a HomeKit discovery.
Return if discovery was forwarded. Return if discovery was forwarded.
@ -394,7 +462,7 @@ def handle_homekit(
break break
if model is None: if model is None:
return False return None
for test_model in homekit_models: for test_model in homekit_models:
if ( if (
@ -404,16 +472,13 @@ def handle_homekit(
): ):
continue continue
hass.add_job( return {
hass.config_entries.flow.async_init( "domain": homekit_models[test_model],
homekit_models[test_model], "context": {"source": config_entries.SOURCE_HOMEKIT},
context={"source": config_entries.SOURCE_HOMEKIT}, "data": info,
data=info, }
) # type: ignore
)
return True
return False return None
def info_from_service(service: ServiceInfo) -> HaServiceInfo | None: def info_from_service(service: ServiceInfo) -> HaServiceInfo | None:

View file

@ -2,7 +2,7 @@
"domain": "zeroconf", "domain": "zeroconf",
"name": "Zero-configuration networking (zeroconf)", "name": "Zero-configuration networking (zeroconf)",
"documentation": "https://www.home-assistant.io/integrations/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"], "dependencies": ["api"],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],
"quality_scale": "internal", "quality_scale": "internal",

View file

@ -34,7 +34,7 @@ sqlalchemy==1.4.13
voluptuous-serialize==2.4.0 voluptuous-serialize==2.4.0
voluptuous==0.12.1 voluptuous==0.12.1
yarl==1.6.3 yarl==1.6.3
zeroconf==0.30.0 zeroconf==0.31.0
pycryptodome>=3.6.6 pycryptodome>=3.6.6

View file

@ -2402,7 +2402,7 @@ zeep[async]==4.0.0
zengge==0.2 zengge==0.2
# homeassistant.components.zeroconf # homeassistant.components.zeroconf
zeroconf==0.30.0 zeroconf==0.31.0
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.57 zha-quirks==0.0.57

View file

@ -1290,7 +1290,7 @@ yeelight==0.6.2
zeep[async]==4.0.0 zeep[async]==4.0.0
# homeassistant.components.zeroconf # homeassistant.components.zeroconf
zeroconf==0.30.0 zeroconf==0.31.0
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.57 zha-quirks==0.0.57

View file

@ -1,14 +1,7 @@
"""Test Zeroconf component setup process.""" """Test Zeroconf component setup process."""
from unittest.mock import patch from unittest.mock import patch
from zeroconf import ( from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange
BadTypeInNameException,
Error as ZeroconfError,
InterfaceChoice,
IPVersion,
ServiceInfo,
ServiceStateChange,
)
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6 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" hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object( ) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock zeroconf, "HaServiceBrowser", side_effect=service_update_mock
) as mock_service_browser: ) as mock_service_browser, patch(
mock_zeroconf.get_service_info.side_effect = get_service_info_mock "homeassistant.components.zeroconf.ServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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, hass.config,
"location_name", "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", "\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: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_START) hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -195,8 +191,10 @@ async def test_setup_with_default_interface(hass, mock_zeroconf):
"""Test default interface config.""" """Test default interface config."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object( with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock 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( assert await async_setup_component(
hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: True}} 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.""" """Test without default interface config."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object( with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock 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( assert await async_setup_component(
hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: False}} 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.""" """Test without ipv6."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object( with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock 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( assert await async_setup_component(
hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_IPV6: False}} hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_IPV6: False}}
) )
@ -238,8 +240,10 @@ async def test_setup_with_ipv6(hass, mock_zeroconf):
"""Test without ipv6.""" """Test without ipv6."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object( with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock 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( assert await async_setup_component(
hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_IPV6: True}} 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.""" """Test without ipv6 as default."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object( with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock 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: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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() 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): async def test_zeroconf_match_macaddress(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry.""" """Test configured options for a device are loaded via config entry."""
@ -300,10 +292,10 @@ async def test_zeroconf_match_macaddress(hass, mock_zeroconf):
hass.config_entries.flow, "async_init" hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object( ) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser: ) as mock_service_browser, patch(
mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( "homeassistant.components.zeroconf.ServiceInfo",
"FFAADDCC11DD" side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
) ):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -333,10 +325,10 @@ async def test_zeroconf_match_manufacturer(hass, mock_zeroconf):
hass.config_entries.flow, "async_init" hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object( ) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser: ) as mock_service_browser, patch(
mock_zeroconf.get_service_info.side_effect = ( "homeassistant.components.zeroconf.ServiceInfo",
get_zeroconf_info_mock_manufacturer("Samsung Electronics") side_effect=get_zeroconf_info_mock_manufacturer("Samsung Electronics"),
) ):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -366,10 +358,10 @@ async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf):
hass.config_entries.flow, "async_init" hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object( ) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser: ) as mock_service_browser, patch(
mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( "homeassistant.components.zeroconf.ServiceInfo",
"aa:bb:cc:dd:ee:ff" side_effect=get_zeroconf_info_mock("aabbccddeeff"),
) ):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -398,10 +390,10 @@ async def test_zeroconf_no_match(hass, mock_zeroconf):
hass.config_entries.flow, "async_init" hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object( ) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser: ) as mock_service_browser, patch(
mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( "homeassistant.components.zeroconf.ServiceInfo",
"FFAADDCC11DD" side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
) ):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -430,10 +422,10 @@ async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf):
hass.config_entries.flow, "async_init" hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object( ) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser: ) as mock_service_browser, patch(
mock_zeroconf.get_service_info.side_effect = ( "homeassistant.components.zeroconf.ServiceInfo",
get_zeroconf_info_mock_manufacturer("Not Samsung Electronics") side_effect=get_zeroconf_info_mock_manufacturer("Not Samsung Electronics"),
) ):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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( side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._tcp.local." *args, **kwargs, limit_service="_hap._tcp.local."
), ),
) as mock_service_browser: ) as mock_service_browser, patch(
mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "homeassistant.components.zeroconf.ServiceInfo",
"LIFX bulb", HOMEKIT_STATUS_UNPAIRED side_effect=get_homekit_info_mock("LIFX bulb", HOMEKIT_STATUS_UNPAIRED),
) ):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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( side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._udp.local." *args, **kwargs, limit_service="_hap._udp.local."
), ),
) as mock_service_browser: ) as mock_service_browser, patch(
mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "homeassistant.components.zeroconf.ServiceInfo",
"Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED side_effect=get_homekit_info_mock("Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED),
) ):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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( side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._udp.local." *args, **kwargs, limit_service="_hap._udp.local."
), ),
) as mock_service_browser: ) as mock_service_browser, patch(
mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "homeassistant.components.zeroconf.ServiceInfo",
"BSB002", HOMEKIT_STATUS_UNPAIRED side_effect=get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED),
) ):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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( side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._tcp.local." *args, **kwargs, limit_service="_hap._tcp.local."
), ),
) as mock_service_browser: ) as mock_service_browser, patch(
mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "homeassistant.components.zeroconf.ServiceInfo",
"tado", HOMEKIT_STATUS_PAIRED side_effect=get_homekit_info_mock("tado", HOMEKIT_STATUS_PAIRED),
) ):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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( side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._tcp.local." *args, **kwargs, limit_service="_hap._tcp.local."
), ),
) as mock_service_browser: ) as mock_service_browser, patch(
mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "homeassistant.components.zeroconf.ServiceInfo",
"tado", b"invalid" side_effect=get_homekit_info_mock("tado", b"invalid"),
) ):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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" hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object( ) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock zeroconf, "HaServiceBrowser", side_effect=service_update_mock
) as mock_service_browser: ) as mock_service_browser, patch(
mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "homeassistant.components.zeroconf.ServiceInfo",
side_effect=get_homekit_info_mock(
"this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED "this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED
) ),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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): async def test_removed_ignored(hass, mock_zeroconf):
"""Test we remove it when a zeroconf entry is removed.""" """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): def service_update_mock(zeroconf, services, handlers):
"""Call service update handler.""" """Call service update handler."""
handlers[0]( handlers[0](
zeroconf, "_service.added", "name._service.added", ServiceStateChange.Added zeroconf,
"_service.added.local.",
"name._service.added.local.",
ServiceStateChange.Added,
) )
handlers[0]( handlers[0](
zeroconf, zeroconf,
"_service.updated", "_service.updated.local.",
"name._service.updated", "name._service.updated.local.",
ServiceStateChange.Updated, ServiceStateChange.Updated,
) )
handlers[0]( handlers[0](
zeroconf, zeroconf,
"_service.removed", "_service.removed.local.",
"name._service.removed", "name._service.removed.local.",
ServiceStateChange.Removed, 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: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_zeroconf.get_service_info.mock_calls) == 2 assert len(mock_service_info.mock_calls) == 2
assert mock_zeroconf.get_service_info.mock_calls[0][1][0] == "_service.added" import pprint
assert mock_zeroconf.get_service_info.mock_calls[1][1][0] == "_service.updated"
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): 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( ), patch(
"homeassistant.components.zeroconf.IPRoute.route", "homeassistant.components.zeroconf.IPRoute.route",
return_value=_ROUTE_NO_LOOPBACK, 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: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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 zeroconf, "HaServiceBrowser", side_effect=service_update_mock
), patch( ), patch(
"homeassistant.components.zeroconf.IPRoute.route", return_value=_ROUTE_LOOPBACK "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: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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.""" """Test without default interface config and the route returns nothing."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object( with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock zeroconf, "HaServiceBrowser", side_effect=service_update_mock
), patch("homeassistant.components.zeroconf.IPRoute.route", return_value=[]): ), patch("homeassistant.components.zeroconf.IPRoute.route", return_value=[]), patch(
mock_zeroconf.get_service_info.side_effect = get_service_info_mock "homeassistant.components.zeroconf.ServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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 zeroconf, "HaServiceBrowser", side_effect=service_update_mock
), patch( ), patch(
"homeassistant.components.zeroconf.IPRoute.route", side_effect=AttributeError "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: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() await hass.async_block_till_done()