Refactor zeroconf setup to be async (#39955)
* Refactor zeroconf setup to be async Most of the setup was calling back to async because we were setting up listeners. Since we only need to jump into the executor to create the zeroconf instance, its much faster to setup in async. In testing this cut the setup time in half or better. * partial revert to after_depspull/40048/head
parent
00acb180d6
commit
7b016063ca
|
@ -1,6 +1,6 @@
|
|||
"""Support for exposing Home Assistant via Zeroconf."""
|
||||
import asyncio
|
||||
import fnmatch
|
||||
from functools import partial
|
||||
import ipaddress
|
||||
import logging
|
||||
import socket
|
||||
|
@ -81,26 +81,21 @@ CONFIG_SCHEMA = vol.Schema(
|
|||
@singleton(DOMAIN)
|
||||
async def async_get_instance(hass):
|
||||
"""Zeroconf instance to be shared with other integrations that use it."""
|
||||
return await hass.async_add_executor_job(_get_instance, hass)
|
||||
return await _async_get_instance(hass)
|
||||
|
||||
|
||||
def _get_instance(hass, default_interface=False, ipv6=True):
|
||||
"""Create an instance."""
|
||||
async def _async_get_instance(hass, **zcargs):
|
||||
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
|
||||
|
||||
zc_args = {}
|
||||
if default_interface:
|
||||
zc_args["interfaces"] = InterfaceChoice.Default
|
||||
if not ipv6:
|
||||
zc_args["ip_version"] = IPVersion.V4Only
|
||||
zeroconf = await hass.async_add_executor_job(partial(HaZeroconf, **zcargs))
|
||||
|
||||
zeroconf = HaZeroconf(**zc_args)
|
||||
install_multiple_zeroconf_catcher(zeroconf)
|
||||
|
||||
def stop_zeroconf(_):
|
||||
def _stop_zeroconf(_):
|
||||
"""Stop Zeroconf."""
|
||||
zeroconf.ha_close()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_zeroconf)
|
||||
|
||||
return zeroconf
|
||||
|
||||
|
@ -135,24 +130,42 @@ class HaZeroconf(Zeroconf):
|
|||
ha_close = Zeroconf.close
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up Zeroconf and make Home Assistant discoverable."""
|
||||
zc_config = config.get(DOMAIN, {})
|
||||
zeroconf = hass.data[DOMAIN] = _get_instance(
|
||||
hass,
|
||||
default_interface=zc_config.get(
|
||||
CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE
|
||||
),
|
||||
ipv6=zc_config.get(CONF_IPV6, DEFAULT_IPV6),
|
||||
zc_args = {}
|
||||
if zc_config.get(CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE):
|
||||
zc_args["interfaces"] = InterfaceChoice.Default
|
||||
if not zc_config.get(CONF_IPV6, DEFAULT_IPV6):
|
||||
zc_args["ip_version"] = IPVersion.V4Only
|
||||
|
||||
zeroconf = hass.data[DOMAIN] = await _async_get_instance(hass, **zc_args)
|
||||
|
||||
async def _async_zeroconf_hass_start(_event):
|
||||
"""Expose Home Assistant on zeroconf when it starts.
|
||||
|
||||
Wait till started or otherwise HTTP is not up and running.
|
||||
"""
|
||||
uuid = await hass.helpers.instance_id.async_get()
|
||||
await hass.async_add_executor_job(
|
||||
_register_hass_zc_service, hass, zeroconf, uuid
|
||||
)
|
||||
|
||||
async def _async_zeroconf_hass_started(_event):
|
||||
"""Start the service browser."""
|
||||
|
||||
await _async_start_zeroconf_browser(hass, zeroconf)
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_zeroconf_hass_start)
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STARTED, _async_zeroconf_hass_started
|
||||
)
|
||||
|
||||
install_multiple_zeroconf_catcher(zeroconf)
|
||||
return True
|
||||
|
||||
|
||||
def _register_hass_zc_service(hass, zeroconf, uuid):
|
||||
# Get instance UUID
|
||||
uuid = asyncio.run_coroutine_threadsafe(
|
||||
hass.helpers.instance_id.async_get(), hass.loop
|
||||
).result()
|
||||
|
||||
valid_location_name = _truncate_location_name_to_valid(hass.config.location_name)
|
||||
|
||||
params = {
|
||||
|
@ -199,23 +212,25 @@ def setup(hass, config):
|
|||
properties=params,
|
||||
)
|
||||
|
||||
def zeroconf_hass_start(_event):
|
||||
"""Expose Home Assistant on zeroconf when it starts.
|
||||
_LOGGER.info("Starting Zeroconf broadcast")
|
||||
try:
|
||||
zeroconf.register_service(info)
|
||||
except NonUniqueNameException:
|
||||
_LOGGER.error(
|
||||
"Home Assistant instance with identical name present in the local network"
|
||||
)
|
||||
|
||||
Wait till started or otherwise HTTP is not up and running.
|
||||
"""
|
||||
_LOGGER.info("Starting Zeroconf broadcast")
|
||||
try:
|
||||
zeroconf.register_service(info)
|
||||
except NonUniqueNameException:
|
||||
_LOGGER.error(
|
||||
"Home Assistant instance with identical name present in the local network"
|
||||
)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, zeroconf_hass_start)
|
||||
async def _async_start_zeroconf_browser(hass, zeroconf):
|
||||
"""Start the zeroconf browser."""
|
||||
|
||||
zeroconf_types = {}
|
||||
homekit_models = {}
|
||||
zeroconf_types = await async_get_zeroconf(hass)
|
||||
homekit_models = await async_get_homekit(hass)
|
||||
|
||||
types = list(zeroconf_types)
|
||||
|
||||
if HOMEKIT_TYPE not in zeroconf_types:
|
||||
types.append(HOMEKIT_TYPE)
|
||||
|
||||
def service_update(zeroconf, service_type, name, state_change):
|
||||
"""Service state changed."""
|
||||
|
@ -292,25 +307,8 @@ def setup(hass, config):
|
|||
)
|
||||
)
|
||||
|
||||
async def zeroconf_hass_started(_event):
|
||||
"""Start the service browser."""
|
||||
nonlocal zeroconf_types
|
||||
nonlocal homekit_models
|
||||
|
||||
zeroconf_types = await async_get_zeroconf(hass)
|
||||
homekit_models = await async_get_homekit(hass)
|
||||
|
||||
types = list(zeroconf_types)
|
||||
|
||||
if HOMEKIT_TYPE not in zeroconf_types:
|
||||
types.append(HOMEKIT_TYPE)
|
||||
|
||||
_LOGGER.debug("Starting Zeroconf browser")
|
||||
HaServiceBrowser(zeroconf, types, handlers=[service_update])
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STARTED, zeroconf_hass_started)
|
||||
|
||||
return True
|
||||
_LOGGER.debug("Starting Zeroconf browser")
|
||||
HaServiceBrowser(zeroconf, types, handlers=[service_update])
|
||||
|
||||
|
||||
def handle_homekit(hass, homekit_models, info) -> bool:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Test Home Assistant Cast."""
|
||||
|
||||
from homeassistant.components.cast import home_assistant_cast
|
||||
from homeassistant.config import async_process_ha_core_config
|
||||
|
||||
|
@ -6,7 +7,7 @@ from tests.async_mock import patch
|
|||
from tests.common import MockConfigEntry, async_mock_signal
|
||||
|
||||
|
||||
async def test_service_show_view(hass):
|
||||
async def test_service_show_view(hass, mock_zeroconf):
|
||||
"""Test we don't set app id in prod."""
|
||||
await async_process_ha_core_config(
|
||||
hass,
|
||||
|
@ -33,7 +34,7 @@ async def test_service_show_view(hass):
|
|||
assert url_path is None
|
||||
|
||||
|
||||
async def test_service_show_view_dashboard(hass):
|
||||
async def test_service_show_view_dashboard(hass, mock_zeroconf):
|
||||
"""Test casting a specific dashboard."""
|
||||
await async_process_ha_core_config(
|
||||
hass,
|
||||
|
@ -60,7 +61,7 @@ async def test_service_show_view_dashboard(hass):
|
|||
assert url_path == "mock-dashboard"
|
||||
|
||||
|
||||
async def test_use_cloud_url(hass):
|
||||
async def test_use_cloud_url(hass, mock_zeroconf):
|
||||
"""Test that we fall back to cloud url."""
|
||||
await async_process_ha_core_config(
|
||||
hass,
|
||||
|
|
|
@ -6,13 +6,6 @@ from homeassistant.setup import async_setup_component
|
|||
from tests.async_mock import patch
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_zeroconf():
|
||||
"""Mock zeroconf."""
|
||||
with patch("homeassistant.components.zeroconf.HaZeroconf"):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_ssdp():
|
||||
"""Mock ssdp."""
|
||||
|
@ -34,6 +27,6 @@ def recorder_url_mock():
|
|||
yield
|
||||
|
||||
|
||||
async def test_setup(hass):
|
||||
async def test_setup(hass, mock_zeroconf):
|
||||
"""Test setup."""
|
||||
assert await async_setup_component(hass, "default_config", {"foo": "bar"})
|
||||
|
|
|
@ -37,7 +37,9 @@ def netdisco_mock():
|
|||
|
||||
async def mock_discovery(hass, discoveries, config=BASE_CONFIG):
|
||||
"""Mock discoveries."""
|
||||
with patch("homeassistant.components.zeroconf.async_get_instance"):
|
||||
with patch("homeassistant.components.zeroconf.async_get_instance"), patch(
|
||||
"homeassistant.components.zeroconf.async_setup", return_value=True
|
||||
):
|
||||
assert await async_setup_component(hass, "discovery", config)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
|
|
|
@ -29,10 +29,3 @@ def events(hass):
|
|||
EVENT_HOMEKIT_CHANGED, ha_callback(lambda e: events.append(e))
|
||||
)
|
||||
yield events
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_zeroconf():
|
||||
"""Mock zeroconf."""
|
||||
with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc:
|
||||
yield mock_zc.return_value
|
||||
|
|
|
@ -1018,6 +1018,7 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf):
|
|||
data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT},
|
||||
options={},
|
||||
)
|
||||
assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}})
|
||||
system_zc = await zeroconf.async_get_instance(hass)
|
||||
|
||||
with patch("pyhap.accessory_driver.AccessoryDriver.start_service"), patch(
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
"""conftest for zeroconf."""
|
||||
import pytest
|
||||
|
||||
from tests.async_mock import patch
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_zeroconf():
|
||||
"""Mock zeroconf."""
|
||||
with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc:
|
||||
yield mock_zc.return_value
|
|
@ -9,7 +9,11 @@ from zeroconf import (
|
|||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.generated import zeroconf as zc_gen
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
@ -128,7 +132,7 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog):
|
|||
"""Test we still setup with long urls and names."""
|
||||
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
|
||||
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
|
||||
) as mock_service_browser, patch(
|
||||
), patch(
|
||||
"homeassistant.components.zeroconf.get_url",
|
||||
return_value="https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a/bit/longer/than/the/maximum/length/that/we/allow/for/a/value",
|
||||
), patch.object(
|
||||
|
@ -138,10 +142,9 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog):
|
|||
):
|
||||
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)
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_service_browser.mock_calls) == 1
|
||||
assert "https://this.url.is.way.too.long" in caplog.text
|
||||
assert "German Umlaut" in caplog.text
|
||||
|
||||
|
@ -461,6 +464,7 @@ async def test_info_from_service_with_addresses(hass):
|
|||
|
||||
async def test_get_instance(hass, mock_zeroconf):
|
||||
"""Test we get an instance."""
|
||||
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
||||
assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||
await hass.async_block_till_done()
|
||||
|
|
|
@ -3,12 +3,16 @@ import zeroconf
|
|||
|
||||
from homeassistant.components.zeroconf import async_get_instance
|
||||
from homeassistant.components.zeroconf.usage import install_multiple_zeroconf_catcher
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.async_mock import Mock, patch
|
||||
|
||||
DOMAIN = "zeroconf"
|
||||
|
||||
|
||||
async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog):
|
||||
"""Test creating multiple zeroconf throws without an integration."""
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
|
||||
zeroconf_instance = await async_get_instance(hass)
|
||||
|
||||
|
@ -22,6 +26,7 @@ async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog):
|
|||
|
||||
async def test_multiple_zeroconf_instances_gives_shared(hass, mock_zeroconf, caplog):
|
||||
"""Test creating multiple zeroconf gives the shared instance to an integration."""
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
|
||||
zeroconf_instance = await async_get_instance(hass)
|
||||
|
||||
|
|
|
@ -395,6 +395,13 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config):
|
|||
return component
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_zeroconf():
|
||||
"""Mock zeroconf."""
|
||||
with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc:
|
||||
yield mock_zc.return_value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def legacy_patchable_time():
|
||||
"""Allow time to be patchable by using event listeners instead of asyncio loop."""
|
||||
|
|
Loading…
Reference in New Issue