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."""
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 typing import Any, TypedDict, cast
from pyroute2 import IPRoute
import voluptuous as vol
from zeroconf import (
Error as ZeroconfError,
InterfaceChoice,
IPVersion,
NonUniqueNameException,
@ -29,7 +29,8 @@ from homeassistant.const import (
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_setup(hass: HomeAssistant, config: dict) -> bool:
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."""
zeroconf_types = await async_get_zeroconf(hass)
homekit_models = await async_get_homekit(hass)
def __init__(self, hass: HomeAssistant):
"""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:
if hk_type not in zeroconf_types:
types.append(hk_type)
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"]
)
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(
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 @@ async def _async_start_zeroconf_browser(
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 @@ async def _async_start_zeroconf_browser(
# 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 @@ async def _async_start_zeroconf_browser(
# 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 @@ async def _async_start_zeroconf_browser(
):
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:

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 @@ async def test_zeroconf_match_macaddress(hass, mock_zeroconf):
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 @@ async def test_zeroconf_match_manufacturer(hass, mock_zeroconf):
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 @@ async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf):
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 @@ async def test_zeroconf_no_match(hass, mock_zeroconf):
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 @@ async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf):
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()