diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 51da3638a9e..68300adbcfe 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -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: diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 2842ddc23f0..2fff760bb70 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -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, diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 7edf1dc7d60..6bf9cc44c56 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -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"}) diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 26f79e3e62f..8003f83d996 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -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() diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 79c15344b17..0cb31e1b701 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -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 diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 757281af1e9..c22d6286e76 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -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( diff --git a/tests/components/zeroconf/conftest.py b/tests/components/zeroconf/conftest.py deleted file mode 100644 index e7d7b030aaf..00000000000 --- a/tests/components/zeroconf/conftest.py +++ /dev/null @@ -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 diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index ae1f6d5fd98..8767953b363 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -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() diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py index e45a93a38b3..9cf953f4c8d 100644 --- a/tests/components/zeroconf/test_usage.py +++ b/tests/components/zeroconf/test_usage.py @@ -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) diff --git a/tests/conftest.py b/tests/conftest.py index 9008359e539..d6da979bb5e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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."""