From 909a20b36d4df6724c955c2ae28cb82fe6d50c2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 11:03:36 -0500 Subject: [PATCH] Use async zeroconf registration functions (#50168) --- homeassistant/components/zeroconf/__init__.py | 40 +++++++++++-------- homeassistant/components/zeroconf/models.py | 18 +++++++++ .../components/homekit_controller/conftest.py | 2 +- tests/conftest.py | 2 +- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 58d8ad21094..bf717141f11 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Iterable from contextlib import suppress import fnmatch -from functools import partial import ipaddress from ipaddress import ip_address import logging @@ -33,11 +32,10 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url -from homeassistant.helpers.singleton import singleton -from homeassistant.loader import async_get_homekit, async_get_zeroconf +from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass from homeassistant.util.network import is_loopback -from .models import HaServiceBrowser, HaZeroconf +from .models import HaAsyncZeroconf, HaServiceBrowser, HaZeroconf from .usage import install_multiple_zeroconf_catcher _LOGGER = logging.getLogger(__name__) @@ -92,16 +90,26 @@ class HaServiceInfo(TypedDict): properties: dict[str, Any] -@singleton(DOMAIN) +@bind_hass async def async_get_instance(hass: HomeAssistant) -> HaZeroconf: + """Zeroconf instance to be shared with other integrations that use it.""" + return cast(HaZeroconf, (await _async_get_instance(hass)).zeroconf) + + +@bind_hass +async def async_get_async_instance(hass: HomeAssistant) -> HaAsyncZeroconf: """Zeroconf instance to be shared with other integrations that use it.""" return await _async_get_instance(hass) -async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaZeroconf: +async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZeroconf: + if DOMAIN in hass.data: + return cast(HaAsyncZeroconf, hass.data[DOMAIN]) + logging.getLogger("zeroconf").setLevel(logging.NOTSET) - zeroconf = await hass.async_add_executor_job(partial(HaZeroconf, **zcargs)) + aio_zc = HaAsyncZeroconf(**zcargs) + zeroconf = cast(HaZeroconf, aio_zc.zeroconf) install_multiple_zeroconf_catcher(zeroconf) @@ -110,8 +118,9 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaZeroconf: zeroconf.ha_close() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_zeroconf) + hass.data[DOMAIN] = aio_zc - return zeroconf + return aio_zc def _get_ip_route(dst_ip: str) -> Any: @@ -171,7 +180,8 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: 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) + aio_zc = await _async_get_instance(hass, **zc_args) + zeroconf = aio_zc.zeroconf async def _async_zeroconf_hass_start(_event: Event) -> None: """Expose Home Assistant on zeroconf when it starts. @@ -179,9 +189,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: 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 - ) + await _async_register_hass_zc_service(hass, aio_zc, uuid) async def _async_zeroconf_hass_started(_event: Event) -> None: """Start the service browser.""" @@ -196,8 +204,8 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: return True -def _register_hass_zc_service( - hass: HomeAssistant, zeroconf: HaZeroconf, uuid: str +async def _async_register_hass_zc_service( + hass: HomeAssistant, aio_zc: HaAsyncZeroconf, uuid: str ) -> None: # Get instance UUID valid_location_name = _truncate_location_name_to_valid(hass.config.location_name) @@ -244,7 +252,7 @@ def _register_hass_zc_service( _LOGGER.info("Starting Zeroconf broadcast") try: - zeroconf.register_service(info) + await aio_zc.async_register_service(info) except NonUniqueNameException: _LOGGER.error( "Home Assistant instance with identical name present in the local network" @@ -252,7 +260,7 @@ def _register_hass_zc_service( async def _async_start_zeroconf_browser( - hass: HomeAssistant, zeroconf: HaZeroconf + hass: HomeAssistant, zeroconf: Zeroconf ) -> None: """Start the zeroconf browser.""" diff --git a/homeassistant/components/zeroconf/models.py b/homeassistant/components/zeroconf/models.py index 02a6fc7cdaa..c09e6428f2a 100644 --- a/homeassistant/components/zeroconf/models.py +++ b/homeassistant/components/zeroconf/models.py @@ -1,6 +1,10 @@ """Models for Zeroconf.""" +import asyncio +from typing import Any + from zeroconf import DNSPointer, DNSRecord, ServiceBrowser, Zeroconf +from zeroconf.asyncio import AsyncZeroconf class HaZeroconf(Zeroconf): @@ -12,6 +16,20 @@ class HaZeroconf(Zeroconf): ha_close = Zeroconf.close +class HaAsyncZeroconf(AsyncZeroconf): + """Home Assistant version of AsyncZeroconf.""" + + def __init__( # pylint: disable=super-init-not-called + self, *args: Any, **kwargs: Any + ) -> None: + """Wrap AsyncZeroconf.""" + self.zeroconf = HaZeroconf(*args, **kwargs) + self.loop = asyncio.get_running_loop() + + async def async_close(self) -> None: + """Fake method to avoid integrations closing it.""" + + class HaServiceBrowser(ServiceBrowser): """ServiceBrowser that only consumes DNSPointer records.""" diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 3cde3912709..266fa177fb2 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -14,7 +14,7 @@ from tests.components.light.conftest import mock_light_profiles # noqa: F401 @pytest.fixture(autouse=True) def mock_zeroconf(): """Mock zeroconf.""" - with mock.patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc: + with mock.patch("homeassistant.components.zeroconf.models.HaZeroconf") as mock_zc: yield mock_zc.return_value diff --git a/tests/conftest.py b/tests/conftest.py index 3fc2dc748cb..2a453a8dad1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -478,7 +478,7 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): @pytest.fixture def mock_zeroconf(): """Mock zeroconf.""" - with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc: + with patch("homeassistant.components.zeroconf.models.HaZeroconf") as mock_zc: yield mock_zc.return_value