Deprecate IPv6 zeroconf setting in favor of the network integration (#51173)

pull/52311/head^2
J. Nick Koston 2021-06-29 17:13:31 -10:00 committed by GitHub
parent 3c20f2dd42
commit 9f16e390f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 195 additions and 177 deletions

View File

@ -15,10 +15,9 @@ from zeroconf import (
InterfaceChoice,
IPVersion,
NonUniqueNameException,
ServiceInfo,
ServiceStateChange,
Zeroconf,
)
from zeroconf.asyncio import AsyncServiceInfo
from homeassistant import config_entries, util
from homeassistant.components import network
@ -35,7 +34,7 @@ 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
from .models import HaAsyncZeroconf, HaServiceBrowser, HaZeroconf
from .models import HaAsyncServiceBrowser, HaAsyncZeroconf, HaZeroconf
from .usage import install_multiple_zeroconf_catcher
_LOGGER = logging.getLogger(__name__)
@ -70,6 +69,7 @@ CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
cv.deprecated(CONF_DEFAULT_INTERFACE),
cv.deprecated(CONF_IPV6),
vol.Schema(
{
vol.Optional(CONF_DEFAULT_INTERFACE): cv.boolean,
@ -119,16 +119,16 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
aio_zc = HaAsyncZeroconf(**zcargs)
zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
zeroconf = HaZeroconf(**zcargs)
aio_zc = HaAsyncZeroconf(zc=zeroconf)
install_multiple_zeroconf_catcher(zeroconf)
def _stop_zeroconf(_event: Event) -> None:
async def _async_stop_zeroconf(_event: Event) -> None:
"""Stop Zeroconf."""
zeroconf.ha_close()
await aio_zc.ha_async_close()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_zeroconf)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_zeroconf)
hass.data[DOMAIN] = aio_zc
return aio_zc
@ -143,7 +143,6 @@ def _async_use_default_interface(adapters: list[Adapter]) -> bool:
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up Zeroconf and make Home Assistant discoverable."""
zc_config = config.get(DOMAIN, {})
zc_args: dict = {}
adapters = await network.async_get_adapters(hass)
@ -158,16 +157,18 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
interfaces.append(ipv4s[0]["address"])
elif ipv6s := adapter["ipv6"]:
interfaces.append(ipv6s[0]["scope_id"])
if not zc_config.get(CONF_IPV6, DEFAULT_IPV6):
ipv6 = True
if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
ipv6 = False
zc_args["ip_version"] = IPVersion.V4Only
aio_zc = await _async_get_instance(hass, **zc_args)
zeroconf = aio_zc.zeroconf
zeroconf = cast(HaZeroconf, 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)
discovery = ZeroconfDiscovery(hass, zeroconf, zeroconf_types, homekit_models, ipv6)
await discovery.async_setup()
async def _async_zeroconf_hass_start(_event: Event) -> None:
@ -230,7 +231,7 @@ async def _async_register_hass_zc_service(
_suppress_invalid_properties(params)
info = ServiceInfo(
info = AsyncServiceInfo(
ZEROCONF_TYPE,
name=f"{valid_location_name}.{ZEROCONF_TYPE}",
server=f"{uuid}.local.",
@ -268,10 +269,10 @@ class FlowDispatcher:
self.hass.async_create_task(self._init_flow(flow))
self.pending_flows = []
def create(self, flow: ZeroconfFlow) -> None:
def async_create(self, flow: ZeroconfFlow) -> None:
"""Create and add or queue a flow."""
if self.started:
self.hass.create_task(self._init_flow(flow))
self.hass.async_create_task(self._init_flow(flow))
else:
self.pending_flows.append(flow)
@ -288,18 +289,20 @@ class ZeroconfDiscovery:
def __init__(
self,
hass: HomeAssistant,
zeroconf: Zeroconf,
zeroconf: HaZeroconf,
zeroconf_types: dict[str, list[dict[str, str]]],
homekit_models: dict[str, str],
ipv6: bool,
) -> None:
"""Init discovery."""
self.hass = hass
self.zeroconf = zeroconf
self.zeroconf_types = zeroconf_types
self.homekit_models = homekit_models
self.ipv6 = ipv6
self.flow_dispatcher: FlowDispatcher | None = None
self.service_browser: HaServiceBrowser | None = None
self.async_service_browser: HaAsyncServiceBrowser | None = None
async def async_setup(self) -> None:
"""Start discovery."""
@ -311,15 +314,15 @@ class ZeroconfDiscovery:
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]
_LOGGER.debug("Starting Zeroconf browser for: %s", types)
self.async_service_browser = HaAsyncServiceBrowser(
self.ipv6, self.zeroconf, types, handlers=[self.async_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)
if self.async_service_browser:
await self.async_service_browser.async_cancel()
@callback
def async_start(self) -> None:
@ -327,21 +330,35 @@ class ZeroconfDiscovery:
assert self.flow_dispatcher is not None
self.flow_dispatcher.async_start()
def service_update(
@callback
def async_service_update(
self,
zeroconf: Zeroconf,
zeroconf: HaZeroconf,
service_type: str,
name: str,
state_change: ServiceStateChange,
) -> None:
"""Service state changed."""
_LOGGER.debug(
"service_update: type=%s name=%s state_change=%s",
service_type,
name,
state_change,
)
if state_change == ServiceStateChange.Removed:
return
service_info = ServiceInfo(service_type, name)
service_info.load_from_cache(zeroconf)
asyncio.create_task(self._process_service_update(zeroconf, service_type, name))
info = info_from_service(service_info)
async def _process_service_update(
self, zeroconf: HaZeroconf, service_type: str, name: str
) -> None:
"""Process a zeroconf update."""
async_service_info = AsyncServiceInfo(service_type, name)
await async_service_info.async_request(zeroconf, 3000)
info = info_from_service(async_service_info)
if not info:
# Prevent the browser thread from collapsing
_LOGGER.debug("Failed to get addresses for device %s", name)
@ -353,7 +370,7 @@ class ZeroconfDiscovery:
# If we can handle it as a HomeKit discovery, we do that here.
if service_type in HOMEKIT_TYPES:
if pending_flow := handle_homekit(self.hass, self.homekit_models, info):
self.flow_dispatcher.create(pending_flow)
self.flow_dispatcher.async_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.
@ -415,7 +432,7 @@ class ZeroconfDiscovery:
"context": {"source": config_entries.SOURCE_ZEROCONF},
"data": info,
}
self.flow_dispatcher.create(flow)
self.flow_dispatcher.async_create(flow)
def handle_homekit(
@ -453,7 +470,7 @@ def handle_homekit(
return None
def info_from_service(service: ServiceInfo) -> HaServiceInfo | None:
def info_from_service(service: AsyncServiceInfo) -> HaServiceInfo | None:
"""Return prepared info from mDNS entries."""
properties: dict[str, Any] = {"_raw": {}}

View File

@ -2,7 +2,7 @@
"domain": "zeroconf",
"name": "Zero-configuration networking (zeroconf)",
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
"requirements": ["zeroconf==0.31.0"],
"requirements": ["zeroconf==0.32.0"],
"dependencies": ["network", "api"],
"codeowners": ["@bdraco"],
"quality_scale": "internal",

View File

@ -1,10 +1,11 @@
"""Models for Zeroconf."""
import asyncio
from typing import Any
from zeroconf import DNSPointer, DNSRecord, ServiceBrowser, Zeroconf
from zeroconf.asyncio import AsyncZeroconf
from zeroconf import DNSAddress, DNSRecord, Zeroconf
from zeroconf.asyncio import AsyncServiceBrowser, AsyncZeroconf
TYPE_AAAA = 28
class HaZeroconf(Zeroconf):
@ -19,33 +20,26 @@ class HaZeroconf(Zeroconf):
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."""
ha_async_close = AsyncZeroconf.async_close
class HaServiceBrowser(ServiceBrowser):
class HaAsyncServiceBrowser(AsyncServiceBrowser):
"""ServiceBrowser that only consumes DNSPointer records."""
def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
"""Pre-Filter update_record to DNSPointers for the configured type."""
def __init__(self, ipv6: bool, *args: Any, **kwargs: Any) -> None:
"""Create service browser that filters ipv6 if it is disabled."""
self.ipv6 = ipv6
super().__init__(*args, **kwargs)
#
# Each ServerBrowser currently runs in its own thread which
# processes every A or AAAA record update per instance.
#
# As the list of zeroconf names we watch for grows, each additional
# ServiceBrowser would process all the A and AAAA updates on the network.
#
# To avoid overwhelming the system we pre-filter here and only process
# DNSPointers for the configured record name (type)
#
if record.name not in self.types or not isinstance(record, DNSPointer):
def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
"""Pre-Filter AAAA records if IPv6 is not enabled."""
if (
not self.ipv6
and isinstance(record, DNSAddress)
and record.type == TYPE_AAAA
):
return
super().update_record(zc, now, record)

View File

@ -33,7 +33,7 @@ sqlalchemy==1.4.17
voluptuous-serialize==2.4.0
voluptuous==0.12.1
yarl==1.6.3
zeroconf==0.31.0
zeroconf==0.32.0
pycryptodome>=3.6.6

View File

@ -2425,7 +2425,7 @@ zeep[async]==4.0.0
zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.31.0
zeroconf==0.32.0
# homeassistant.components.zha
zha-quirks==0.0.57

View File

@ -1328,7 +1328,7 @@ yeelight==0.6.3
zeep[async]==4.0.0
# homeassistant.components.zeroconf
zeroconf==0.31.0
zeroconf==0.32.0
# homeassistant.components.zha
zha-quirks==0.0.57

View File

@ -0,0 +1,15 @@
"""Tests for the Zeroconf component."""
from unittest.mock import AsyncMock, patch
import pytest
@pytest.fixture
def mock_async_zeroconf():
"""Mock AsyncZeroconf."""
with patch("homeassistant.components.zeroconf.HaAsyncZeroconf") as mock_aiozc:
zc = mock_aiozc.return_value
zc.async_register_service = AsyncMock()
zc.zeroconf.async_wait_for_start = AsyncMock()
zc.ha_async_close = AsyncMock()
yield zc

View File

@ -1,7 +1,8 @@
"""Test Zeroconf component setup process."""
from unittest.mock import call, patch
from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange
from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange
from zeroconf.asyncio import AsyncServiceInfo
from homeassistant.components import zeroconf
from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6
@ -24,29 +25,8 @@ PROPERTIES = {
HOMEKIT_STATUS_UNPAIRED = b"1"
HOMEKIT_STATUS_PAIRED = b"0"
_ROUTE_NO_LOOPBACK = (
{
"attrs": [
("RTA_TABLE", 254),
("RTA_DST", "224.0.0.251"),
("RTA_OIF", 4),
("RTA_PREFSRC", "192.168.1.5"),
],
},
)
_ROUTE_LOOPBACK = (
{
"attrs": [
("RTA_TABLE", 254),
("RTA_DST", "224.0.0.251"),
("RTA_OIF", 4),
("RTA_PREFSRC", "127.0.0.1"),
],
},
)
def service_update_mock(zeroconf, services, handlers, *, limit_service=None):
def service_update_mock(ipv6, zeroconf, services, handlers, *, limit_service=None):
"""Call service update handler."""
for service in services:
if limit_service is not None and service != limit_service:
@ -56,7 +36,7 @@ def service_update_mock(zeroconf, services, handlers, *, limit_service=None):
def get_service_info_mock(service_type, name):
"""Return service info for get_service_info."""
return ServiceInfo(
return AsyncServiceInfo(
service_type,
name,
addresses=[b"\n\x00\x00\x14"],
@ -70,7 +50,7 @@ def get_service_info_mock(service_type, name):
def get_service_info_mock_without_an_address(service_type, name):
"""Return service info for get_service_info without any addresses."""
return ServiceInfo(
return AsyncServiceInfo(
service_type,
name,
addresses=[],
@ -86,7 +66,7 @@ def get_homekit_info_mock(model, pairing_status):
"""Return homekit info for get_service_info for an homekit device."""
def mock_homekit_info(service_type, name):
return ServiceInfo(
return AsyncServiceInfo(
service_type,
name,
addresses=[b"\n\x00\x00\x14"],
@ -104,7 +84,7 @@ def get_zeroconf_info_mock(macaddress):
"""Return info for get_service_info for an zeroconf device."""
def mock_zc_info(service_type, name):
return ServiceInfo(
return AsyncServiceInfo(
service_type,
name,
addresses=[b"\n\x00\x00\x14"],
@ -122,7 +102,7 @@ def get_zeroconf_info_mock_manufacturer(manufacturer):
"""Return info for get_service_info for an zeroconf device."""
def mock_zc_info(service_type, name):
return ServiceInfo(
return AsyncServiceInfo(
service_type,
name,
addresses=[b"\n\x00\x00\x14"],
@ -136,14 +116,14 @@ def get_zeroconf_info_mock_manufacturer(manufacturer):
return mock_zc_info
async def test_setup(hass, mock_zeroconf):
async def test_setup(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -162,13 +142,15 @@ async def test_setup(hass, mock_zeroconf):
# Test instance is set.
assert "zeroconf" in hass.data
assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf
assert (
await hass.components.zeroconf.async_get_async_instance() is mock_async_zeroconf
)
async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog):
async def test_setup_with_overly_long_url_and_name(hass, mock_async_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
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), 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",
@ -177,7 +159,7 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog):
"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",
"homeassistant.components.zeroconf.AsyncServiceInfo.request",
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
@ -187,12 +169,12 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog):
assert "German Umlaut" in caplog.text
async def test_setup_with_default_interface(hass, mock_zeroconf):
async def test_setup_with_default_interface(hass, mock_async_zeroconf):
"""Test default interface config."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(
@ -201,30 +183,30 @@ async def test_setup_with_default_interface(hass, mock_zeroconf):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.Default)
assert mock_async_zeroconf.called_with(interface_choice=InterfaceChoice.Default)
async def test_setup_without_default_interface(hass, mock_zeroconf):
async def test_setup_without_default_interface(hass, mock_async_zeroconf):
"""Test without default interface config."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(
hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: False}}
)
assert mock_zeroconf.called_with()
assert mock_async_zeroconf.called_with()
async def test_setup_without_ipv6(hass, mock_zeroconf):
async def test_setup_without_ipv6(hass, mock_async_zeroconf):
"""Test without ipv6."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(
@ -233,15 +215,15 @@ async def test_setup_without_ipv6(hass, mock_zeroconf):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert mock_zeroconf.called_with(ip_version=IPVersion.V4Only)
assert mock_async_zeroconf.called_with(ip_version=IPVersion.V4Only)
async def test_setup_with_ipv6(hass, mock_zeroconf):
async def test_setup_with_ipv6(hass, mock_async_zeroconf):
"""Test without ipv6."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(
@ -250,28 +232,28 @@ async def test_setup_with_ipv6(hass, mock_zeroconf):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert mock_zeroconf.called_with()
assert mock_async_zeroconf.called_with()
async def test_setup_with_ipv6_default(hass, mock_zeroconf):
async def test_setup_with_ipv6_default(hass, mock_async_zeroconf):
"""Test without ipv6 as default."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
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()
assert mock_zeroconf.called_with()
assert mock_async_zeroconf.called_with()
async def test_zeroconf_match_macaddress(hass, mock_zeroconf):
async def test_zeroconf_match_macaddress(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
def http_only_service_update_mock(zeroconf, services, handlers):
def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
@ -291,9 +273,9 @@ async def test_zeroconf_match_macaddress(hass, mock_zeroconf):
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -305,10 +287,10 @@ async def test_zeroconf_match_macaddress(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "shelly"
async def test_zeroconf_match_manufacturer(hass, mock_zeroconf):
async def test_zeroconf_match_manufacturer(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
def http_only_service_update_mock(zeroconf, services, handlers):
def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
@ -324,9 +306,9 @@ async def test_zeroconf_match_manufacturer(hass, mock_zeroconf):
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_zeroconf_info_mock_manufacturer("Samsung Electronics"),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -338,10 +320,10 @@ async def test_zeroconf_match_manufacturer(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "samsungtv"
async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf):
async def test_zeroconf_match_manufacturer_not_present(hass, mock_async_zeroconf):
"""Test matchers reject when a property is missing."""
def http_only_service_update_mock(zeroconf, services, handlers):
def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
@ -357,9 +339,9 @@ async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf):
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_zeroconf_info_mock("aabbccddeeff"),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -370,10 +352,10 @@ async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf):
assert len(mock_config_flow.mock_calls) == 0
async def test_zeroconf_no_match(hass, mock_zeroconf):
async def test_zeroconf_no_match(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
def http_only_service_update_mock(zeroconf, services, handlers):
def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
@ -389,9 +371,9 @@ async def test_zeroconf_no_match(hass, mock_zeroconf):
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -402,10 +384,10 @@ async def test_zeroconf_no_match(hass, mock_zeroconf):
assert len(mock_config_flow.mock_calls) == 0
async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf):
async def test_zeroconf_no_match_manufacturer(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
def http_only_service_update_mock(zeroconf, services, handlers):
def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
@ -421,9 +403,9 @@ async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf):
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_zeroconf_info_mock_manufacturer("Not Samsung Electronics"),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -434,7 +416,7 @@ async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf):
assert len(mock_config_flow.mock_calls) == 0
async def test_homekit_match_partial_space(hass, mock_zeroconf):
async def test_homekit_match_partial_space(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(
zc_gen.ZEROCONF,
@ -444,12 +426,12 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf):
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf,
"HaServiceBrowser",
"HaAsyncServiceBrowser",
side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._tcp.local."
),
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock("LIFX bulb", HOMEKIT_STATUS_UNPAIRED),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -461,7 +443,7 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "lifx"
async def test_homekit_match_partial_dash(hass, mock_zeroconf):
async def test_homekit_match_partial_dash(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(
zc_gen.ZEROCONF,
@ -471,12 +453,12 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf):
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf,
"HaServiceBrowser",
"HaAsyncServiceBrowser",
side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._udp.local."
),
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock("Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -488,7 +470,7 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "rachio"
async def test_homekit_match_partial_fnmatch(hass, mock_zeroconf):
async def test_homekit_match_partial_fnmatch(hass, mock_async_zeroconf):
"""Test matching homekit devices with fnmatch."""
with patch.dict(
zc_gen.ZEROCONF,
@ -498,12 +480,12 @@ async def test_homekit_match_partial_fnmatch(hass, mock_zeroconf):
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf,
"HaServiceBrowser",
"HaAsyncServiceBrowser",
side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._tcp.local."
),
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock("YLDP13YL", HOMEKIT_STATUS_UNPAIRED),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -515,7 +497,7 @@ async def test_homekit_match_partial_fnmatch(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "yeelight"
async def test_homekit_match_full(hass, mock_zeroconf):
async def test_homekit_match_full(hass, mock_async_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(
zc_gen.ZEROCONF,
@ -525,12 +507,12 @@ async def test_homekit_match_full(hass, mock_zeroconf):
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf,
"HaServiceBrowser",
"HaAsyncServiceBrowser",
side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._udp.local."
),
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -542,7 +524,7 @@ async def test_homekit_match_full(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "hue"
async def test_homekit_already_paired(hass, mock_zeroconf):
async def test_homekit_already_paired(hass, mock_async_zeroconf):
"""Test that an already paired device is sent to homekit_controller."""
with patch.dict(
zc_gen.ZEROCONF,
@ -552,12 +534,12 @@ async def test_homekit_already_paired(hass, mock_zeroconf):
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf,
"HaServiceBrowser",
"HaAsyncServiceBrowser",
side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._tcp.local."
),
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock("tado", HOMEKIT_STATUS_PAIRED),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -570,7 +552,7 @@ async def test_homekit_already_paired(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller"
async def test_homekit_invalid_paring_status(hass, mock_zeroconf):
async def test_homekit_invalid_paring_status(hass, mock_async_zeroconf):
"""Test that missing paring data is not sent to homekit_controller."""
with patch.dict(
zc_gen.ZEROCONF,
@ -580,12 +562,12 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf):
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf,
"HaServiceBrowser",
"HaAsyncServiceBrowser",
side_effect=lambda *args, **kwargs: service_update_mock(
*args, **kwargs, limit_service="_hap._tcp.local."
),
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock("tado", b"invalid"),
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -597,7 +579,7 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf):
assert mock_config_flow.mock_calls[0][1][0] == "tado"
async def test_homekit_not_paired(hass, mock_zeroconf):
async def test_homekit_not_paired(hass, mock_async_zeroconf):
"""Test that an not paired device is sent to homekit_controller."""
with patch.dict(
zc_gen.ZEROCONF,
@ -606,9 +588,9 @@ async def test_homekit_not_paired(hass, mock_zeroconf):
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_homekit_info_mock(
"this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED
),
@ -646,19 +628,21 @@ async def test_info_from_service_with_addresses(hass):
assert info is None
async def test_get_instance(hass, mock_zeroconf):
async def test_get_instance(hass, mock_async_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
assert (
await hass.components.zeroconf.async_get_async_instance() is mock_async_zeroconf
)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
assert len(mock_zeroconf.ha_close.mock_calls) == 1
assert len(mock_async_zeroconf.ha_async_close.mock_calls) == 1
async def test_removed_ignored(hass, mock_zeroconf):
async def test_removed_ignored(hass, mock_async_zeroconf):
"""Test we remove it when a zeroconf entry is removed."""
def service_update_mock(zeroconf, services, handlers):
def service_update_mock(ipv6, zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
@ -680,9 +664,9 @@ async def test_removed_ignored(hass, mock_zeroconf):
)
with patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
) as mock_service_info:
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
@ -709,24 +693,28 @@ _ADAPTER_WITH_DEFAULT_ENABLED = [
]
async def test_async_detect_interfaces_setting_non_loopback_route(hass):
async def test_async_detect_interfaces_setting_non_loopback_route(
hass, mock_async_zeroconf
):
"""Test without default interface config and the route returns a non-loopback address."""
with patch(
"homeassistant.components.zeroconf.models.HaZeroconf"
) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object(
hass.config_entries.flow, "async_init"
), patch.object(
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.network.async_get_adapters",
return_value=_ADAPTER_WITH_DEFAULT_ENABLED,
), patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
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()
assert mock_zc.mock_calls[0] == call(interfaces=InterfaceChoice.Default)
assert mock_zc.mock_calls[0] == call(
interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only
)
_ADAPTERS_WITH_MANUAL_CONFIG = [
@ -764,17 +752,17 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [
]
async def test_async_detect_interfaces_setting_empty_route(hass):
async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zeroconf):
"""Test without default interface config and the route returns nothing."""
with patch(
"homeassistant.components.zeroconf.models.HaZeroconf"
) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object(
hass.config_entries.flow, "async_init"
), patch.object(
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.network.async_get_adapters",
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
), patch(
"homeassistant.components.zeroconf.ServiceInfo",
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})

View File

@ -10,7 +10,9 @@ from homeassistant.setup import async_setup_component
DOMAIN = "zeroconf"
async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog):
async def test_multiple_zeroconf_instances(
hass, mock_async_zeroconf, mock_zeroconf, caplog
):
"""Test creating multiple zeroconf throws without an integration."""
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -24,7 +26,9 @@ async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog):
assert "Zeroconf" in caplog.text
async def test_multiple_zeroconf_instances_gives_shared(hass, mock_zeroconf, caplog):
async def test_multiple_zeroconf_instances_gives_shared(
hass, mock_async_zeroconf, mock_zeroconf, caplog
):
"""Test creating multiple zeroconf gives the shared instance to an integration."""
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})