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_deps
This commit is contained in:
parent
00acb180d6
commit
7b016063ca
10 changed files with 84 additions and 91 deletions
|
@ -1,6 +1,6 @@
|
||||||
"""Support for exposing Home Assistant via Zeroconf."""
|
"""Support for exposing Home Assistant via Zeroconf."""
|
||||||
import asyncio
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
from functools import partial
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
|
@ -81,26 +81,21 @@ CONFIG_SCHEMA = vol.Schema(
|
||||||
@singleton(DOMAIN)
|
@singleton(DOMAIN)
|
||||||
async def async_get_instance(hass):
|
async def async_get_instance(hass):
|
||||||
"""Zeroconf instance to be shared with other integrations that use it."""
|
"""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):
|
async def _async_get_instance(hass, **zcargs):
|
||||||
"""Create an instance."""
|
|
||||||
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
|
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
|
||||||
|
|
||||||
zc_args = {}
|
zeroconf = await hass.async_add_executor_job(partial(HaZeroconf, **zcargs))
|
||||||
if default_interface:
|
|
||||||
zc_args["interfaces"] = InterfaceChoice.Default
|
|
||||||
if not ipv6:
|
|
||||||
zc_args["ip_version"] = IPVersion.V4Only
|
|
||||||
|
|
||||||
zeroconf = HaZeroconf(**zc_args)
|
install_multiple_zeroconf_catcher(zeroconf)
|
||||||
|
|
||||||
def stop_zeroconf(_):
|
def _stop_zeroconf(_):
|
||||||
"""Stop Zeroconf."""
|
"""Stop Zeroconf."""
|
||||||
zeroconf.ha_close()
|
zeroconf.ha_close()
|
||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_zeroconf)
|
||||||
|
|
||||||
return zeroconf
|
return zeroconf
|
||||||
|
|
||||||
|
@ -135,24 +130,42 @@ class HaZeroconf(Zeroconf):
|
||||||
ha_close = Zeroconf.close
|
ha_close = Zeroconf.close
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up Zeroconf and make Home Assistant discoverable."""
|
"""Set up Zeroconf and make Home Assistant discoverable."""
|
||||||
zc_config = config.get(DOMAIN, {})
|
zc_config = config.get(DOMAIN, {})
|
||||||
zeroconf = hass.data[DOMAIN] = _get_instance(
|
zc_args = {}
|
||||||
hass,
|
if zc_config.get(CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE):
|
||||||
default_interface=zc_config.get(
|
zc_args["interfaces"] = InterfaceChoice.Default
|
||||||
CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE
|
if not zc_config.get(CONF_IPV6, DEFAULT_IPV6):
|
||||||
),
|
zc_args["ip_version"] = IPVersion.V4Only
|
||||||
ipv6=zc_config.get(CONF_IPV6, DEFAULT_IPV6),
|
|
||||||
|
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
|
# 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)
|
valid_location_name = _truncate_location_name_to_valid(hass.config.location_name)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
|
@ -199,23 +212,25 @@ def setup(hass, config):
|
||||||
properties=params,
|
properties=params,
|
||||||
)
|
)
|
||||||
|
|
||||||
def zeroconf_hass_start(_event):
|
_LOGGER.info("Starting Zeroconf broadcast")
|
||||||
"""Expose Home Assistant on zeroconf when it starts.
|
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 = {}
|
zeroconf_types = await async_get_zeroconf(hass)
|
||||||
homekit_models = {}
|
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):
|
def service_update(zeroconf, service_type, name, state_change):
|
||||||
"""Service state changed."""
|
"""Service state changed."""
|
||||||
|
@ -292,25 +307,8 @@ def setup(hass, config):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def zeroconf_hass_started(_event):
|
_LOGGER.debug("Starting Zeroconf browser")
|
||||||
"""Start the service browser."""
|
HaServiceBrowser(zeroconf, types, handlers=[service_update])
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def handle_homekit(hass, homekit_models, info) -> bool:
|
def handle_homekit(hass, homekit_models, info) -> bool:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""Test Home Assistant Cast."""
|
"""Test Home Assistant Cast."""
|
||||||
|
|
||||||
from homeassistant.components.cast import home_assistant_cast
|
from homeassistant.components.cast import home_assistant_cast
|
||||||
from homeassistant.config import async_process_ha_core_config
|
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
|
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."""
|
"""Test we don't set app id in prod."""
|
||||||
await async_process_ha_core_config(
|
await async_process_ha_core_config(
|
||||||
hass,
|
hass,
|
||||||
|
@ -33,7 +34,7 @@ async def test_service_show_view(hass):
|
||||||
assert url_path is None
|
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."""
|
"""Test casting a specific dashboard."""
|
||||||
await async_process_ha_core_config(
|
await async_process_ha_core_config(
|
||||||
hass,
|
hass,
|
||||||
|
@ -60,7 +61,7 @@ async def test_service_show_view_dashboard(hass):
|
||||||
assert url_path == "mock-dashboard"
|
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."""
|
"""Test that we fall back to cloud url."""
|
||||||
await async_process_ha_core_config(
|
await async_process_ha_core_config(
|
||||||
hass,
|
hass,
|
||||||
|
|
|
@ -6,13 +6,6 @@ from homeassistant.setup import async_setup_component
|
||||||
from tests.async_mock import patch
|
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)
|
@pytest.fixture(autouse=True)
|
||||||
def mock_ssdp():
|
def mock_ssdp():
|
||||||
"""Mock ssdp."""
|
"""Mock ssdp."""
|
||||||
|
@ -34,6 +27,6 @@ def recorder_url_mock():
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
async def test_setup(hass):
|
async def test_setup(hass, mock_zeroconf):
|
||||||
"""Test setup."""
|
"""Test setup."""
|
||||||
assert await async_setup_component(hass, "default_config", {"foo": "bar"})
|
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):
|
async def mock_discovery(hass, discoveries, config=BASE_CONFIG):
|
||||||
"""Mock discoveries."""
|
"""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)
|
assert await async_setup_component(hass, "discovery", config)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await hass.async_start()
|
await hass.async_start()
|
||||||
|
|
|
@ -29,10 +29,3 @@ def events(hass):
|
||||||
EVENT_HOMEKIT_CHANGED, ha_callback(lambda e: events.append(e))
|
EVENT_HOMEKIT_CHANGED, ha_callback(lambda e: events.append(e))
|
||||||
)
|
)
|
||||||
yield events
|
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},
|
data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT},
|
||||||
options={},
|
options={},
|
||||||
)
|
)
|
||||||
|
assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}})
|
||||||
system_zc = await zeroconf.async_get_instance(hass)
|
system_zc = await zeroconf.async_get_instance(hass)
|
||||||
|
|
||||||
with patch("pyhap.accessory_driver.AccessoryDriver.start_service"), patch(
|
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 import zeroconf
|
||||||
from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6
|
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.generated import zeroconf as zc_gen
|
||||||
from homeassistant.setup import async_setup_component
|
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."""
|
"""Test we still setup with long urls and names."""
|
||||||
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
|
||||||
) as mock_service_browser, patch(
|
), patch(
|
||||||
"homeassistant.components.zeroconf.get_url",
|
"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",
|
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(
|
), 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
|
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_START)
|
||||||
await hass.async_block_till_done()
|
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 "https://this.url.is.way.too.long" in caplog.text
|
||||||
assert "German Umlaut" 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):
|
async def test_get_instance(hass, mock_zeroconf):
|
||||||
"""Test we get an instance."""
|
"""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
|
assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
|
@ -3,12 +3,16 @@ import zeroconf
|
||||||
|
|
||||||
from homeassistant.components.zeroconf import async_get_instance
|
from homeassistant.components.zeroconf import async_get_instance
|
||||||
from homeassistant.components.zeroconf.usage import install_multiple_zeroconf_catcher
|
from homeassistant.components.zeroconf.usage import install_multiple_zeroconf_catcher
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.async_mock import Mock, patch
|
from tests.async_mock import Mock, patch
|
||||||
|
|
||||||
|
DOMAIN = "zeroconf"
|
||||||
|
|
||||||
|
|
||||||
async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog):
|
async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog):
|
||||||
"""Test creating multiple zeroconf throws without an integration."""
|
"""Test creating multiple zeroconf throws without an integration."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
zeroconf_instance = await async_get_instance(hass)
|
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):
|
async def test_multiple_zeroconf_instances_gives_shared(hass, mock_zeroconf, caplog):
|
||||||
"""Test creating multiple zeroconf gives the shared instance to an integration."""
|
"""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)
|
zeroconf_instance = await async_get_instance(hass)
|
||||||
|
|
||||||
|
|
|
@ -395,6 +395,13 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config):
|
||||||
return component
|
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
|
@pytest.fixture
|
||||||
def legacy_patchable_time():
|
def legacy_patchable_time():
|
||||||
"""Allow time to be patchable by using event listeners instead of asyncio loop."""
|
"""Allow time to be patchable by using event listeners instead of asyncio loop."""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue