mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 02:37:08 +00:00
Break apart zeroconf integration to prepare for WebSocket API (#143490)
This commit is contained in:
parent
65db3c1164
commit
36081c69e0
@ -2,26 +2,17 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from fnmatch import translate
|
from functools import partial
|
||||||
from functools import lru_cache, partial
|
|
||||||
from ipaddress import IPv4Address, IPv6Address
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, Any, Final, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from zeroconf import (
|
from zeroconf import InterfaceChoice, IPVersion
|
||||||
BadTypeInNameException,
|
from zeroconf.asyncio import AsyncServiceInfo
|
||||||
InterfaceChoice,
|
|
||||||
IPVersion,
|
|
||||||
ServiceStateChange,
|
|
||||||
)
|
|
||||||
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo
|
|
||||||
|
|
||||||
from homeassistant import config_entries
|
|
||||||
from homeassistant.components import network
|
from homeassistant.components import network
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_CLOSE,
|
EVENT_HOMEASSISTANT_CLOSE,
|
||||||
@ -29,55 +20,40 @@ from homeassistant.const import (
|
|||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
from homeassistant.core import Event, HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv, discovery_flow, instance_id
|
from homeassistant.helpers import config_validation as cv, instance_id
|
||||||
from homeassistant.helpers.deprecation import (
|
from homeassistant.helpers.deprecation import (
|
||||||
DeprecatedConstant,
|
DeprecatedConstant,
|
||||||
all_with_deprecated_constants,
|
all_with_deprecated_constants,
|
||||||
check_if_deprecated_constant,
|
check_if_deprecated_constant,
|
||||||
dir_with_deprecated_constants,
|
dir_with_deprecated_constants,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.discovery_flow import DiscoveryKey
|
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
||||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||||
from homeassistant.helpers.service_info.zeroconf import (
|
from homeassistant.helpers.service_info.zeroconf import (
|
||||||
ATTR_PROPERTIES_ID as _ATTR_PROPERTIES_ID,
|
ATTR_PROPERTIES_ID as _ATTR_PROPERTIES_ID,
|
||||||
ZeroconfServiceInfo as _ZeroconfServiceInfo,
|
ZeroconfServiceInfo as _ZeroconfServiceInfo,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.loader import (
|
from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass
|
||||||
HomeKitDiscoveredIntegration,
|
|
||||||
ZeroconfMatcher,
|
|
||||||
async_get_homekit,
|
|
||||||
async_get_zeroconf,
|
|
||||||
bind_hass,
|
|
||||||
)
|
|
||||||
from homeassistant.setup import async_when_setup_or_start
|
from homeassistant.setup import async_when_setup_or_start
|
||||||
|
|
||||||
|
from .const import DOMAIN, ZEROCONF_TYPE
|
||||||
|
from .discovery import ( # noqa: F401
|
||||||
|
DATA_DISCOVERY,
|
||||||
|
ZeroconfDiscovery,
|
||||||
|
build_homekit_model_lookups,
|
||||||
|
info_from_service,
|
||||||
|
)
|
||||||
from .models import HaAsyncZeroconf, HaZeroconf
|
from .models import HaAsyncZeroconf, HaZeroconf
|
||||||
from .usage import install_multiple_zeroconf_catcher
|
from .usage import install_multiple_zeroconf_catcher
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "zeroconf"
|
|
||||||
|
|
||||||
ZEROCONF_TYPE = "_home-assistant._tcp.local."
|
|
||||||
HOMEKIT_TYPES = [
|
|
||||||
"_hap._tcp.local.",
|
|
||||||
# Thread based devices
|
|
||||||
"_hap._udp.local.",
|
|
||||||
]
|
|
||||||
_HOMEKIT_MODEL_SPLITS = (None, " ", "-")
|
|
||||||
|
|
||||||
|
|
||||||
CONF_DEFAULT_INTERFACE = "default_interface"
|
CONF_DEFAULT_INTERFACE = "default_interface"
|
||||||
CONF_IPV6 = "ipv6"
|
CONF_IPV6 = "ipv6"
|
||||||
DEFAULT_DEFAULT_INTERFACE = True
|
DEFAULT_DEFAULT_INTERFACE = True
|
||||||
DEFAULT_IPV6 = True
|
DEFAULT_IPV6 = True
|
||||||
|
|
||||||
HOMEKIT_PAIRED_STATUS_FLAG = "sf"
|
|
||||||
HOMEKIT_MODEL_LOWER = "md"
|
|
||||||
HOMEKIT_MODEL_UPPER = "MD"
|
|
||||||
|
|
||||||
# Property key=value has a max length of 255
|
# Property key=value has a max length of 255
|
||||||
# so we use 230 to leave space for key=
|
# so we use 230 to leave space for key=
|
||||||
MAX_PROPERTY_VALUE_LEN = 230
|
MAX_PROPERTY_VALUE_LEN = 230
|
||||||
@ -85,10 +61,6 @@ MAX_PROPERTY_VALUE_LEN = 230
|
|||||||
# Dns label max length
|
# Dns label max length
|
||||||
MAX_NAME_LEN = 63
|
MAX_NAME_LEN = 63
|
||||||
|
|
||||||
ATTR_DOMAIN: Final = "domain"
|
|
||||||
ATTR_NAME: Final = "name"
|
|
||||||
ATTR_PROPERTIES: Final = "properties"
|
|
||||||
|
|
||||||
# Attributes for ZeroconfServiceInfo[ATTR_PROPERTIES]
|
# Attributes for ZeroconfServiceInfo[ATTR_PROPERTIES]
|
||||||
_DEPRECATED_ATTR_PROPERTIES_ID = DeprecatedConstant(
|
_DEPRECATED_ATTR_PROPERTIES_ID = DeprecatedConstant(
|
||||||
_ATTR_PROPERTIES_ID,
|
_ATTR_PROPERTIES_ID,
|
||||||
@ -214,7 +186,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
|
zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
|
||||||
zeroconf_types = await async_get_zeroconf(hass)
|
zeroconf_types = await async_get_zeroconf(hass)
|
||||||
homekit_models = await async_get_homekit(hass)
|
homekit_models = await async_get_homekit(hass)
|
||||||
homekit_model_lookup, homekit_model_matchers = _build_homekit_model_lookups(
|
homekit_model_lookup, homekit_model_matchers = build_homekit_model_lookups(
|
||||||
homekit_models
|
homekit_models
|
||||||
)
|
)
|
||||||
discovery = ZeroconfDiscovery(
|
discovery = ZeroconfDiscovery(
|
||||||
@ -225,6 +197,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
homekit_model_matchers,
|
homekit_model_matchers,
|
||||||
)
|
)
|
||||||
await discovery.async_setup()
|
await discovery.async_setup()
|
||||||
|
hass.data[DATA_DISCOVERY] = discovery
|
||||||
|
|
||||||
async def _async_zeroconf_hass_start(hass: HomeAssistant, comp: str) -> None:
|
async def _async_zeroconf_hass_start(hass: HomeAssistant, comp: str) -> None:
|
||||||
"""Expose Home Assistant on zeroconf when it starts.
|
"""Expose Home Assistant on zeroconf when it starts.
|
||||||
@ -243,25 +216,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _build_homekit_model_lookups(
|
|
||||||
homekit_models: dict[str, HomeKitDiscoveredIntegration],
|
|
||||||
) -> tuple[
|
|
||||||
dict[str, HomeKitDiscoveredIntegration],
|
|
||||||
dict[re.Pattern, HomeKitDiscoveredIntegration],
|
|
||||||
]:
|
|
||||||
"""Build lookups for homekit models."""
|
|
||||||
homekit_model_lookup: dict[str, HomeKitDiscoveredIntegration] = {}
|
|
||||||
homekit_model_matchers: dict[re.Pattern, HomeKitDiscoveredIntegration] = {}
|
|
||||||
|
|
||||||
for model, discovery in homekit_models.items():
|
|
||||||
if "*" in model or "?" in model or "[" in model:
|
|
||||||
homekit_model_matchers[_compile_fnmatch(model)] = discovery
|
|
||||||
else:
|
|
||||||
homekit_model_lookup[model] = discovery
|
|
||||||
|
|
||||||
return homekit_model_lookup, homekit_model_matchers
|
|
||||||
|
|
||||||
|
|
||||||
def _filter_disallowed_characters(name: str) -> str:
|
def _filter_disallowed_characters(name: str) -> str:
|
||||||
"""Filter disallowed characters from a string.
|
"""Filter disallowed characters from a string.
|
||||||
|
|
||||||
@ -315,299 +269,6 @@ async def _async_register_hass_zc_service(
|
|||||||
await aio_zc.async_register_service(info, allow_name_change=True)
|
await aio_zc.async_register_service(info, allow_name_change=True)
|
||||||
|
|
||||||
|
|
||||||
def _match_against_props(matcher: dict[str, str], props: dict[str, str | None]) -> bool:
|
|
||||||
"""Check a matcher to ensure all values in props."""
|
|
||||||
for key, value in matcher.items():
|
|
||||||
prop_val = props.get(key)
|
|
||||||
if prop_val is None or not _memorized_fnmatch(prop_val.lower(), value):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def is_homekit_paired(props: dict[str, Any]) -> bool:
|
|
||||||
"""Check properties to see if a device is homekit paired."""
|
|
||||||
if HOMEKIT_PAIRED_STATUS_FLAG not in props:
|
|
||||||
return False
|
|
||||||
with contextlib.suppress(ValueError):
|
|
||||||
# 0 means paired and not discoverable by iOS clients)
|
|
||||||
return int(props[HOMEKIT_PAIRED_STATUS_FLAG]) == 0
|
|
||||||
# If we cannot tell, we assume its not paired
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class ZeroconfDiscovery:
|
|
||||||
"""Discovery via zeroconf."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
zeroconf: HaZeroconf,
|
|
||||||
zeroconf_types: dict[str, list[ZeroconfMatcher]],
|
|
||||||
homekit_model_lookups: dict[str, HomeKitDiscoveredIntegration],
|
|
||||||
homekit_model_matchers: dict[re.Pattern, HomeKitDiscoveredIntegration],
|
|
||||||
) -> None:
|
|
||||||
"""Init discovery."""
|
|
||||||
self.hass = hass
|
|
||||||
self.zeroconf = zeroconf
|
|
||||||
self.zeroconf_types = zeroconf_types
|
|
||||||
self.homekit_model_lookups = homekit_model_lookups
|
|
||||||
self.homekit_model_matchers = homekit_model_matchers
|
|
||||||
self.async_service_browser: AsyncServiceBrowser | None = None
|
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
|
||||||
"""Start discovery."""
|
|
||||||
types = list(self.zeroconf_types)
|
|
||||||
# We want to make sure we know about other HomeAssistant
|
|
||||||
# instances as soon as possible to avoid name conflicts
|
|
||||||
# so we always browse for ZEROCONF_TYPE
|
|
||||||
types.extend(
|
|
||||||
hk_type
|
|
||||||
for hk_type in (ZEROCONF_TYPE, *HOMEKIT_TYPES)
|
|
||||||
if hk_type not in self.zeroconf_types
|
|
||||||
)
|
|
||||||
_LOGGER.debug("Starting Zeroconf browser for: %s", types)
|
|
||||||
self.async_service_browser = AsyncServiceBrowser(
|
|
||||||
self.zeroconf, types, handlers=[self.async_service_update]
|
|
||||||
)
|
|
||||||
|
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass,
|
|
||||||
config_entries.signal_discovered_config_entry_removed(DOMAIN),
|
|
||||||
self._handle_config_entry_removed,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_stop(self) -> None:
|
|
||||||
"""Cancel the service browser and stop processing the queue."""
|
|
||||||
if self.async_service_browser:
|
|
||||||
await self.async_service_browser.async_cancel()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _handle_config_entry_removed(
|
|
||||||
self,
|
|
||||||
entry: config_entries.ConfigEntry,
|
|
||||||
) -> None:
|
|
||||||
"""Handle config entry changes."""
|
|
||||||
for discovery_key in entry.discovery_keys[DOMAIN]:
|
|
||||||
if discovery_key.version != 1:
|
|
||||||
continue
|
|
||||||
_type = discovery_key.key[0]
|
|
||||||
name = discovery_key.key[1]
|
|
||||||
_LOGGER.debug("Rediscover service %s.%s", _type, name)
|
|
||||||
self._async_service_update(self.zeroconf, _type, name)
|
|
||||||
|
|
||||||
def _async_dismiss_discoveries(self, name: str) -> None:
|
|
||||||
"""Dismiss all discoveries for the given name."""
|
|
||||||
for flow in self.hass.config_entries.flow.async_progress_by_init_data_type(
|
|
||||||
_ZeroconfServiceInfo,
|
|
||||||
lambda service_info: bool(service_info.name == name),
|
|
||||||
):
|
|
||||||
self.hass.config_entries.flow.async_abort(flow["flow_id"])
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_service_update(
|
|
||||||
self,
|
|
||||||
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 is ServiceStateChange.Removed:
|
|
||||||
self._async_dismiss_discoveries(name)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._async_service_update(zeroconf, service_type, name)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_service_update(
|
|
||||||
self,
|
|
||||||
zeroconf: HaZeroconf,
|
|
||||||
service_type: str,
|
|
||||||
name: str,
|
|
||||||
) -> None:
|
|
||||||
"""Service state added or changed."""
|
|
||||||
try:
|
|
||||||
async_service_info = AsyncServiceInfo(service_type, name)
|
|
||||||
except BadTypeInNameException as ex:
|
|
||||||
# Some devices broadcast a name that is not a valid DNS name
|
|
||||||
# This is a bug in the device firmware and we should ignore it
|
|
||||||
_LOGGER.debug("Bad name in zeroconf record: %s: %s", name, ex)
|
|
||||||
return
|
|
||||||
|
|
||||||
if async_service_info.load_from_cache(zeroconf):
|
|
||||||
self._async_process_service_update(async_service_info, service_type, name)
|
|
||||||
else:
|
|
||||||
self.hass.async_create_background_task(
|
|
||||||
self._async_lookup_and_process_service_update(
|
|
||||||
zeroconf, async_service_info, service_type, name
|
|
||||||
),
|
|
||||||
name=f"zeroconf lookup {name}.{service_type}",
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _async_lookup_and_process_service_update(
|
|
||||||
self,
|
|
||||||
zeroconf: HaZeroconf,
|
|
||||||
async_service_info: AsyncServiceInfo,
|
|
||||||
service_type: str,
|
|
||||||
name: str,
|
|
||||||
) -> None:
|
|
||||||
"""Update and process a zeroconf update."""
|
|
||||||
await async_service_info.async_request(zeroconf, 3000)
|
|
||||||
self._async_process_service_update(async_service_info, service_type, name)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_process_service_update(
|
|
||||||
self, async_service_info: AsyncServiceInfo, service_type: str, name: str
|
|
||||||
) -> None:
|
|
||||||
"""Process a zeroconf update."""
|
|
||||||
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)
|
|
||||||
return
|
|
||||||
_LOGGER.debug("Discovered new device %s %s", name, info)
|
|
||||||
props: dict[str, str | None] = info.properties
|
|
||||||
discovery_key = DiscoveryKey(
|
|
||||||
domain=DOMAIN,
|
|
||||||
key=(info.type, info.name),
|
|
||||||
version=1,
|
|
||||||
)
|
|
||||||
domain = None
|
|
||||||
|
|
||||||
# If we can handle it as a HomeKit discovery, we do that here.
|
|
||||||
if service_type in HOMEKIT_TYPES and (
|
|
||||||
homekit_discovery := async_get_homekit_discovery(
|
|
||||||
self.homekit_model_lookups, self.homekit_model_matchers, props
|
|
||||||
)
|
|
||||||
):
|
|
||||||
domain = homekit_discovery.domain
|
|
||||||
discovery_flow.async_create_flow(
|
|
||||||
self.hass,
|
|
||||||
homekit_discovery.domain,
|
|
||||||
{"source": config_entries.SOURCE_HOMEKIT},
|
|
||||||
info,
|
|
||||||
discovery_key=discovery_key,
|
|
||||||
)
|
|
||||||
# Continue on here as homekit_controller
|
|
||||||
# still needs to get updates on devices
|
|
||||||
# so it can see when the 'c#' field is updated.
|
|
||||||
#
|
|
||||||
# We only send updates to homekit_controller
|
|
||||||
# if the device is already paired in order to avoid
|
|
||||||
# offering a second discovery for the same device
|
|
||||||
if not is_homekit_paired(props) and not homekit_discovery.always_discover:
|
|
||||||
# If the device is paired with HomeKit we must send on
|
|
||||||
# the update to homekit_controller so it can see when
|
|
||||||
# the 'c#' field is updated. This is used to detect
|
|
||||||
# when the device has been reset or updated.
|
|
||||||
#
|
|
||||||
# If the device is not paired and we should not always
|
|
||||||
# discover it, we can stop here.
|
|
||||||
return
|
|
||||||
|
|
||||||
if not (matchers := self.zeroconf_types.get(service_type)):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Not all homekit types are currently used for discovery
|
|
||||||
# so not all service type exist in zeroconf_types
|
|
||||||
for matcher in matchers:
|
|
||||||
if len(matcher) > 1:
|
|
||||||
if ATTR_NAME in matcher and not _memorized_fnmatch(
|
|
||||||
info.name.lower(), matcher[ATTR_NAME]
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
if ATTR_PROPERTIES in matcher and not _match_against_props(
|
|
||||||
matcher[ATTR_PROPERTIES], props
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
matcher_domain = matcher[ATTR_DOMAIN]
|
|
||||||
# Create a type annotated regular dict since this is a hot path and creating
|
|
||||||
# a regular dict is slightly cheaper than calling ConfigFlowContext
|
|
||||||
context: config_entries.ConfigFlowContext = {
|
|
||||||
"source": config_entries.SOURCE_ZEROCONF,
|
|
||||||
}
|
|
||||||
if domain:
|
|
||||||
# Domain of integration that offers alternative API to handle
|
|
||||||
# this device.
|
|
||||||
context["alternative_domain"] = domain
|
|
||||||
|
|
||||||
discovery_flow.async_create_flow(
|
|
||||||
self.hass,
|
|
||||||
matcher_domain,
|
|
||||||
context,
|
|
||||||
info,
|
|
||||||
discovery_key=discovery_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def async_get_homekit_discovery(
|
|
||||||
homekit_model_lookups: dict[str, HomeKitDiscoveredIntegration],
|
|
||||||
homekit_model_matchers: dict[re.Pattern, HomeKitDiscoveredIntegration],
|
|
||||||
props: dict[str, Any],
|
|
||||||
) -> HomeKitDiscoveredIntegration | None:
|
|
||||||
"""Handle a HomeKit discovery.
|
|
||||||
|
|
||||||
Return the domain to forward the discovery data to
|
|
||||||
"""
|
|
||||||
if not (
|
|
||||||
model := props.get(HOMEKIT_MODEL_LOWER) or props.get(HOMEKIT_MODEL_UPPER)
|
|
||||||
) or not isinstance(model, str):
|
|
||||||
return None
|
|
||||||
|
|
||||||
for split_str in _HOMEKIT_MODEL_SPLITS:
|
|
||||||
key = (model.split(split_str))[0] if split_str else model
|
|
||||||
if discovery := homekit_model_lookups.get(key):
|
|
||||||
return discovery
|
|
||||||
|
|
||||||
for pattern, discovery in homekit_model_matchers.items():
|
|
||||||
if pattern.match(model):
|
|
||||||
return discovery
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def info_from_service(service: AsyncServiceInfo) -> _ZeroconfServiceInfo | None:
|
|
||||||
"""Return prepared info from mDNS entries."""
|
|
||||||
# See https://ietf.org/rfc/rfc6763.html#section-6.4 and
|
|
||||||
# https://ietf.org/rfc/rfc6763.html#section-6.5 for expected encodings
|
|
||||||
# for property keys and values
|
|
||||||
if not (maybe_ip_addresses := service.ip_addresses_by_version(IPVersion.All)):
|
|
||||||
return None
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
ip_addresses = cast(list[IPv4Address | IPv6Address], maybe_ip_addresses)
|
|
||||||
else:
|
|
||||||
ip_addresses = maybe_ip_addresses
|
|
||||||
ip_address: IPv4Address | IPv6Address | None = None
|
|
||||||
for ip_addr in ip_addresses:
|
|
||||||
if not ip_addr.is_link_local and not ip_addr.is_unspecified:
|
|
||||||
ip_address = ip_addr
|
|
||||||
break
|
|
||||||
if not ip_address:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
assert service.server is not None, (
|
|
||||||
"server cannot be none if there are addresses"
|
|
||||||
)
|
|
||||||
return _ZeroconfServiceInfo(
|
|
||||||
ip_address=ip_address,
|
|
||||||
ip_addresses=ip_addresses,
|
|
||||||
port=service.port,
|
|
||||||
hostname=service.server,
|
|
||||||
type=service.type,
|
|
||||||
name=service.name,
|
|
||||||
properties=service.decoded_properties,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _suppress_invalid_properties(properties: dict) -> None:
|
def _suppress_invalid_properties(properties: dict) -> None:
|
||||||
"""Suppress any properties that will cause zeroconf to fail to startup."""
|
"""Suppress any properties that will cause zeroconf to fail to startup."""
|
||||||
|
|
||||||
@ -644,27 +305,6 @@ def _truncate_location_name_to_valid(location_name: str) -> str:
|
|||||||
return location_name.encode("utf-8")[:MAX_NAME_LEN].decode("utf-8", "ignore")
|
return location_name.encode("utf-8")[:MAX_NAME_LEN].decode("utf-8", "ignore")
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=4096, typed=True)
|
|
||||||
def _compile_fnmatch(pattern: str) -> re.Pattern:
|
|
||||||
"""Compile a fnmatch pattern."""
|
|
||||||
return re.compile(translate(pattern))
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1024, typed=True)
|
|
||||||
def _memorized_fnmatch(name: str, pattern: str) -> bool:
|
|
||||||
"""Memorized version of fnmatch that has a larger lru_cache.
|
|
||||||
|
|
||||||
The default version of fnmatch only has a lru_cache of 256 entries.
|
|
||||||
With many devices we quickly reach that limit and end up compiling
|
|
||||||
the same pattern over and over again.
|
|
||||||
|
|
||||||
Zeroconf has its own memorized fnmatch with its own lru_cache
|
|
||||||
since the data is going to be relatively the same
|
|
||||||
since the devices will not change frequently
|
|
||||||
"""
|
|
||||||
return bool(_compile_fnmatch(pattern).match(name))
|
|
||||||
|
|
||||||
|
|
||||||
# These can be removed if no deprecated constant are in this module anymore
|
# These can be removed if no deprecated constant are in this module anymore
|
||||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||||
__dir__ = partial(
|
__dir__ = partial(
|
||||||
|
5
homeassistant/components/zeroconf/const.py
Normal file
5
homeassistant/components/zeroconf/const.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Zeroconf constants."""
|
||||||
|
|
||||||
|
DOMAIN = "zeroconf"
|
||||||
|
|
||||||
|
ZEROCONF_TYPE = "_home-assistant._tcp.local."
|
385
homeassistant/components/zeroconf/discovery.py
Normal file
385
homeassistant/components/zeroconf/discovery.py
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
"""Zeroconf discovery for Home Assistant."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
from fnmatch import translate
|
||||||
|
from functools import lru_cache
|
||||||
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import TYPE_CHECKING, Any, Final, cast
|
||||||
|
|
||||||
|
from zeroconf import BadTypeInNameException, IPVersion, ServiceStateChange
|
||||||
|
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import discovery_flow
|
||||||
|
from homeassistant.helpers.discovery_flow import DiscoveryKey
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.helpers.service_info.zeroconf import (
|
||||||
|
ZeroconfServiceInfo as _ZeroconfServiceInfo,
|
||||||
|
)
|
||||||
|
from homeassistant.loader import HomeKitDiscoveredIntegration, ZeroconfMatcher
|
||||||
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .models import HaZeroconf
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ZEROCONF_TYPE = "_home-assistant._tcp.local."
|
||||||
|
HOMEKIT_TYPES = [
|
||||||
|
"_hap._tcp.local.",
|
||||||
|
# Thread based devices
|
||||||
|
"_hap._udp.local.",
|
||||||
|
]
|
||||||
|
_HOMEKIT_MODEL_SPLITS = (None, " ", "-")
|
||||||
|
|
||||||
|
|
||||||
|
HOMEKIT_PAIRED_STATUS_FLAG = "sf"
|
||||||
|
HOMEKIT_MODEL_LOWER = "md"
|
||||||
|
HOMEKIT_MODEL_UPPER = "MD"
|
||||||
|
|
||||||
|
ATTR_DOMAIN: Final = "domain"
|
||||||
|
ATTR_NAME: Final = "name"
|
||||||
|
ATTR_PROPERTIES: Final = "properties"
|
||||||
|
|
||||||
|
|
||||||
|
DATA_DISCOVERY: HassKey[ZeroconfDiscovery] = HassKey("zeroconf_discovery")
|
||||||
|
|
||||||
|
|
||||||
|
def build_homekit_model_lookups(
|
||||||
|
homekit_models: dict[str, HomeKitDiscoveredIntegration],
|
||||||
|
) -> tuple[
|
||||||
|
dict[str, HomeKitDiscoveredIntegration],
|
||||||
|
dict[re.Pattern, HomeKitDiscoveredIntegration],
|
||||||
|
]:
|
||||||
|
"""Build lookups for homekit models."""
|
||||||
|
homekit_model_lookup: dict[str, HomeKitDiscoveredIntegration] = {}
|
||||||
|
homekit_model_matchers: dict[re.Pattern, HomeKitDiscoveredIntegration] = {}
|
||||||
|
|
||||||
|
for model, discovery in homekit_models.items():
|
||||||
|
if "*" in model or "?" in model or "[" in model:
|
||||||
|
homekit_model_matchers[_compile_fnmatch(model)] = discovery
|
||||||
|
else:
|
||||||
|
homekit_model_lookup[model] = discovery
|
||||||
|
|
||||||
|
return homekit_model_lookup, homekit_model_matchers
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=4096, typed=True)
|
||||||
|
def _compile_fnmatch(pattern: str) -> re.Pattern:
|
||||||
|
"""Compile a fnmatch pattern."""
|
||||||
|
return re.compile(translate(pattern))
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1024, typed=True)
|
||||||
|
def _memorized_fnmatch(name: str, pattern: str) -> bool:
|
||||||
|
"""Memorized version of fnmatch that has a larger lru_cache.
|
||||||
|
|
||||||
|
The default version of fnmatch only has a lru_cache of 256 entries.
|
||||||
|
With many devices we quickly reach that limit and end up compiling
|
||||||
|
the same pattern over and over again.
|
||||||
|
|
||||||
|
Zeroconf has its own memorized fnmatch with its own lru_cache
|
||||||
|
since the data is going to be relatively the same
|
||||||
|
since the devices will not change frequently
|
||||||
|
"""
|
||||||
|
return bool(_compile_fnmatch(pattern).match(name))
|
||||||
|
|
||||||
|
|
||||||
|
def _match_against_props(matcher: dict[str, str], props: dict[str, str | None]) -> bool:
|
||||||
|
"""Check a matcher to ensure all values in props."""
|
||||||
|
for key, value in matcher.items():
|
||||||
|
prop_val = props.get(key)
|
||||||
|
if prop_val is None or not _memorized_fnmatch(prop_val.lower(), value):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_homekit_paired(props: dict[str, Any]) -> bool:
|
||||||
|
"""Check properties to see if a device is homekit paired."""
|
||||||
|
if HOMEKIT_PAIRED_STATUS_FLAG not in props:
|
||||||
|
return False
|
||||||
|
with contextlib.suppress(ValueError):
|
||||||
|
# 0 means paired and not discoverable by iOS clients)
|
||||||
|
return int(props[HOMEKIT_PAIRED_STATUS_FLAG]) == 0
|
||||||
|
# If we cannot tell, we assume its not paired
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def async_get_homekit_discovery(
|
||||||
|
homekit_model_lookups: dict[str, HomeKitDiscoveredIntegration],
|
||||||
|
homekit_model_matchers: dict[re.Pattern, HomeKitDiscoveredIntegration],
|
||||||
|
props: dict[str, Any],
|
||||||
|
) -> HomeKitDiscoveredIntegration | None:
|
||||||
|
"""Handle a HomeKit discovery.
|
||||||
|
|
||||||
|
Return the domain to forward the discovery data to
|
||||||
|
"""
|
||||||
|
if not (
|
||||||
|
model := props.get(HOMEKIT_MODEL_LOWER) or props.get(HOMEKIT_MODEL_UPPER)
|
||||||
|
) or not isinstance(model, str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
for split_str in _HOMEKIT_MODEL_SPLITS:
|
||||||
|
key = (model.split(split_str))[0] if split_str else model
|
||||||
|
if discovery := homekit_model_lookups.get(key):
|
||||||
|
return discovery
|
||||||
|
|
||||||
|
for pattern, discovery in homekit_model_matchers.items():
|
||||||
|
if pattern.match(model):
|
||||||
|
return discovery
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def info_from_service(service: AsyncServiceInfo) -> _ZeroconfServiceInfo | None:
|
||||||
|
"""Return prepared info from mDNS entries."""
|
||||||
|
# See https://ietf.org/rfc/rfc6763.html#section-6.4 and
|
||||||
|
# https://ietf.org/rfc/rfc6763.html#section-6.5 for expected encodings
|
||||||
|
# for property keys and values
|
||||||
|
if not (maybe_ip_addresses := service.ip_addresses_by_version(IPVersion.All)):
|
||||||
|
return None
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
ip_addresses = cast(list[IPv4Address | IPv6Address], maybe_ip_addresses)
|
||||||
|
else:
|
||||||
|
ip_addresses = maybe_ip_addresses
|
||||||
|
ip_address: IPv4Address | IPv6Address | None = None
|
||||||
|
for ip_addr in ip_addresses:
|
||||||
|
if not ip_addr.is_link_local and not ip_addr.is_unspecified:
|
||||||
|
ip_address = ip_addr
|
||||||
|
break
|
||||||
|
if not ip_address:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert service.server is not None, (
|
||||||
|
"server cannot be none if there are addresses"
|
||||||
|
)
|
||||||
|
return _ZeroconfServiceInfo(
|
||||||
|
ip_address=ip_address,
|
||||||
|
ip_addresses=ip_addresses,
|
||||||
|
port=service.port,
|
||||||
|
hostname=service.server,
|
||||||
|
type=service.type,
|
||||||
|
name=service.name,
|
||||||
|
properties=service.decoded_properties,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ZeroconfDiscovery:
|
||||||
|
"""Discovery via zeroconf."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
zeroconf: HaZeroconf,
|
||||||
|
zeroconf_types: dict[str, list[ZeroconfMatcher]],
|
||||||
|
homekit_model_lookups: dict[str, HomeKitDiscoveredIntegration],
|
||||||
|
homekit_model_matchers: dict[re.Pattern, HomeKitDiscoveredIntegration],
|
||||||
|
) -> None:
|
||||||
|
"""Init discovery."""
|
||||||
|
self.hass = hass
|
||||||
|
self.zeroconf = zeroconf
|
||||||
|
self.zeroconf_types = zeroconf_types
|
||||||
|
self.homekit_model_lookups = homekit_model_lookups
|
||||||
|
self.homekit_model_matchers = homekit_model_matchers
|
||||||
|
self.async_service_browser: AsyncServiceBrowser | None = None
|
||||||
|
|
||||||
|
async def async_setup(self) -> None:
|
||||||
|
"""Start discovery."""
|
||||||
|
types = list(self.zeroconf_types)
|
||||||
|
# We want to make sure we know about other HomeAssistant
|
||||||
|
# instances as soon as possible to avoid name conflicts
|
||||||
|
# so we always browse for ZEROCONF_TYPE
|
||||||
|
types.extend(
|
||||||
|
hk_type
|
||||||
|
for hk_type in (ZEROCONF_TYPE, *HOMEKIT_TYPES)
|
||||||
|
if hk_type not in self.zeroconf_types
|
||||||
|
)
|
||||||
|
_LOGGER.debug("Starting Zeroconf browser for: %s", types)
|
||||||
|
self.async_service_browser = AsyncServiceBrowser(
|
||||||
|
self.zeroconf, types, handlers=[self.async_service_update]
|
||||||
|
)
|
||||||
|
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass,
|
||||||
|
config_entries.signal_discovered_config_entry_removed(DOMAIN),
|
||||||
|
self._handle_config_entry_removed,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_stop(self) -> None:
|
||||||
|
"""Cancel the service browser and stop processing the queue."""
|
||||||
|
if self.async_service_browser:
|
||||||
|
await self.async_service_browser.async_cancel()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_config_entry_removed(
|
||||||
|
self,
|
||||||
|
entry: config_entries.ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Handle config entry changes."""
|
||||||
|
for discovery_key in entry.discovery_keys[DOMAIN]:
|
||||||
|
if discovery_key.version != 1:
|
||||||
|
continue
|
||||||
|
_type = discovery_key.key[0]
|
||||||
|
name = discovery_key.key[1]
|
||||||
|
_LOGGER.debug("Rediscover service %s.%s", _type, name)
|
||||||
|
self._async_service_update(self.zeroconf, _type, name)
|
||||||
|
|
||||||
|
def _async_dismiss_discoveries(self, name: str) -> None:
|
||||||
|
"""Dismiss all discoveries for the given name."""
|
||||||
|
for flow in self.hass.config_entries.flow.async_progress_by_init_data_type(
|
||||||
|
_ZeroconfServiceInfo,
|
||||||
|
lambda service_info: bool(service_info.name == name),
|
||||||
|
):
|
||||||
|
self.hass.config_entries.flow.async_abort(flow["flow_id"])
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_service_update(
|
||||||
|
self,
|
||||||
|
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 is ServiceStateChange.Removed:
|
||||||
|
self._async_dismiss_discoveries(name)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._async_service_update(zeroconf, service_type, name)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_service_update(
|
||||||
|
self,
|
||||||
|
zeroconf: HaZeroconf,
|
||||||
|
service_type: str,
|
||||||
|
name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Service state added or changed."""
|
||||||
|
try:
|
||||||
|
async_service_info = AsyncServiceInfo(service_type, name)
|
||||||
|
except BadTypeInNameException as ex:
|
||||||
|
# Some devices broadcast a name that is not a valid DNS name
|
||||||
|
# This is a bug in the device firmware and we should ignore it
|
||||||
|
_LOGGER.debug("Bad name in zeroconf record: %s: %s", name, ex)
|
||||||
|
return
|
||||||
|
|
||||||
|
if async_service_info.load_from_cache(zeroconf):
|
||||||
|
self._async_process_service_update(async_service_info, service_type, name)
|
||||||
|
else:
|
||||||
|
self.hass.async_create_background_task(
|
||||||
|
self._async_lookup_and_process_service_update(
|
||||||
|
zeroconf, async_service_info, service_type, name
|
||||||
|
),
|
||||||
|
name=f"zeroconf lookup {name}.{service_type}",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_lookup_and_process_service_update(
|
||||||
|
self,
|
||||||
|
zeroconf: HaZeroconf,
|
||||||
|
async_service_info: AsyncServiceInfo,
|
||||||
|
service_type: str,
|
||||||
|
name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Update and process a zeroconf update."""
|
||||||
|
await async_service_info.async_request(zeroconf, 3000)
|
||||||
|
self._async_process_service_update(async_service_info, service_type, name)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_process_service_update(
|
||||||
|
self, async_service_info: AsyncServiceInfo, service_type: str, name: str
|
||||||
|
) -> None:
|
||||||
|
"""Process a zeroconf update."""
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
_LOGGER.debug("Discovered new device %s %s", name, info)
|
||||||
|
props: dict[str, str | None] = info.properties
|
||||||
|
discovery_key = DiscoveryKey(
|
||||||
|
domain=DOMAIN,
|
||||||
|
key=(info.type, info.name),
|
||||||
|
version=1,
|
||||||
|
)
|
||||||
|
domain = None
|
||||||
|
|
||||||
|
# If we can handle it as a HomeKit discovery, we do that here.
|
||||||
|
if service_type in HOMEKIT_TYPES and (
|
||||||
|
homekit_discovery := async_get_homekit_discovery(
|
||||||
|
self.homekit_model_lookups, self.homekit_model_matchers, props
|
||||||
|
)
|
||||||
|
):
|
||||||
|
domain = homekit_discovery.domain
|
||||||
|
discovery_flow.async_create_flow(
|
||||||
|
self.hass,
|
||||||
|
homekit_discovery.domain,
|
||||||
|
{"source": config_entries.SOURCE_HOMEKIT},
|
||||||
|
info,
|
||||||
|
discovery_key=discovery_key,
|
||||||
|
)
|
||||||
|
# Continue on here as homekit_controller
|
||||||
|
# still needs to get updates on devices
|
||||||
|
# so it can see when the 'c#' field is updated.
|
||||||
|
#
|
||||||
|
# We only send updates to homekit_controller
|
||||||
|
# if the device is already paired in order to avoid
|
||||||
|
# offering a second discovery for the same device
|
||||||
|
if not is_homekit_paired(props) and not homekit_discovery.always_discover:
|
||||||
|
# If the device is paired with HomeKit we must send on
|
||||||
|
# the update to homekit_controller so it can see when
|
||||||
|
# the 'c#' field is updated. This is used to detect
|
||||||
|
# when the device has been reset or updated.
|
||||||
|
#
|
||||||
|
# If the device is not paired and we should not always
|
||||||
|
# discover it, we can stop here.
|
||||||
|
return
|
||||||
|
|
||||||
|
if not (matchers := self.zeroconf_types.get(service_type)):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Not all homekit types are currently used for discovery
|
||||||
|
# so not all service type exist in zeroconf_types
|
||||||
|
for matcher in matchers:
|
||||||
|
if len(matcher) > 1:
|
||||||
|
if ATTR_NAME in matcher and not _memorized_fnmatch(
|
||||||
|
info.name.lower(), matcher[ATTR_NAME]
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
if ATTR_PROPERTIES in matcher and not _match_against_props(
|
||||||
|
matcher[ATTR_PROPERTIES], props
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
matcher_domain = matcher[ATTR_DOMAIN]
|
||||||
|
# Create a type annotated regular dict since this is a hot path and creating
|
||||||
|
# a regular dict is slightly cheaper than calling ConfigFlowContext
|
||||||
|
context: config_entries.ConfigFlowContext = {
|
||||||
|
"source": config_entries.SOURCE_ZEROCONF,
|
||||||
|
}
|
||||||
|
if domain:
|
||||||
|
# Domain of integration that offers alternative API to handle
|
||||||
|
# this device.
|
||||||
|
context["alternative_domain"] = domain
|
||||||
|
|
||||||
|
discovery_flow.async_create_flow(
|
||||||
|
self.hass,
|
||||||
|
matcher_domain,
|
||||||
|
context,
|
||||||
|
info,
|
||||||
|
discovery_key=discovery_key,
|
||||||
|
)
|
@ -14,6 +14,7 @@ from zeroconf.asyncio import AsyncServiceInfo
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import zeroconf
|
from homeassistant.components import zeroconf
|
||||||
|
from homeassistant.components.zeroconf import discovery
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_COMPONENT_LOADED,
|
EVENT_COMPONENT_LOADED,
|
||||||
EVENT_HOMEASSISTANT_CLOSE,
|
EVENT_HOMEASSISTANT_CLOSE,
|
||||||
@ -181,10 +182,10 @@ async def test_setup(hass: HomeAssistant, mock_async_zeroconf: MagicMock) -> Non
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock
|
discovery, "AsyncServiceBrowser", side_effect=service_update_mock
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_service_info_mock,
|
side_effect=get_service_info_mock,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -214,7 +215,7 @@ async def test_setup_with_overly_long_url_and_name(
|
|||||||
"""Test we still setup with long urls and names."""
|
"""Test we still setup with long urls and names."""
|
||||||
with (
|
with (
|
||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.get_url",
|
"homeassistant.components.zeroconf.get_url",
|
||||||
return_value=(
|
return_value=(
|
||||||
@ -240,7 +241,7 @@ async def test_setup_with_overly_long_url_and_name(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo.async_request",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo.async_request",
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
||||||
@ -258,9 +259,9 @@ async def test_setup_with_defaults(
|
|||||||
"""Test default interface config."""
|
"""Test default interface config."""
|
||||||
with (
|
with (
|
||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_service_info_mock,
|
side_effect=get_service_info_mock,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -302,10 +303,10 @@ async def test_zeroconf_match_macaddress(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
discovery, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
|
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -351,10 +352,10 @@ async def test_zeroconf_match_manufacturer(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
discovery, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_zeroconf_info_mock_manufacturer("Samsung Electronics"),
|
side_effect=get_zeroconf_info_mock_manufacturer("Samsung Electronics"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -392,10 +393,10 @@ async def test_zeroconf_match_model(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
discovery, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_zeroconf_info_mock_model("appletv"),
|
side_effect=get_zeroconf_info_mock_model("appletv"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -433,10 +434,10 @@ async def test_zeroconf_match_manufacturer_not_present(hass: HomeAssistant) -> N
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
discovery, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_zeroconf_info_mock("aabbccddeeff"),
|
side_effect=get_zeroconf_info_mock("aabbccddeeff"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -469,10 +470,10 @@ async def test_zeroconf_no_match(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
discovery, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
|
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -509,10 +510,10 @@ async def test_zeroconf_no_match_manufacturer(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
discovery, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_zeroconf_info_mock_manufacturer("Not Samsung Electronics"),
|
side_effect=get_zeroconf_info_mock_manufacturer("Not Samsung Electronics"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -540,14 +541,14 @@ async def test_homekit_match_partial_space(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf,
|
discovery,
|
||||||
"AsyncServiceBrowser",
|
"AsyncServiceBrowser",
|
||||||
side_effect=lambda *args, **kwargs: service_update_mock(
|
side_effect=lambda *args, **kwargs: service_update_mock(
|
||||||
*args, **kwargs, limit_service="_hap._tcp.local."
|
*args, **kwargs, limit_service="_hap._tcp.local."
|
||||||
),
|
),
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_homekit_info_mock("LIFX bulb", HOMEKIT_STATUS_UNPAIRED),
|
side_effect=get_homekit_info_mock("LIFX bulb", HOMEKIT_STATUS_UNPAIRED),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -588,14 +589,14 @@ async def test_device_with_invalid_name(
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf,
|
discovery,
|
||||||
"AsyncServiceBrowser",
|
"AsyncServiceBrowser",
|
||||||
side_effect=lambda *args, **kwargs: service_update_mock(
|
side_effect=lambda *args, **kwargs: service_update_mock(
|
||||||
*args, **kwargs, limit_service="_hap._tcp.local."
|
*args, **kwargs, limit_service="_hap._tcp.local."
|
||||||
),
|
),
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=BadTypeInNameException,
|
side_effect=BadTypeInNameException,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -624,14 +625,14 @@ async def test_homekit_match_partial_dash(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf,
|
discovery,
|
||||||
"AsyncServiceBrowser",
|
"AsyncServiceBrowser",
|
||||||
side_effect=lambda *args, **kwargs: service_update_mock(
|
side_effect=lambda *args, **kwargs: service_update_mock(
|
||||||
*args, **kwargs, limit_service="_hap._udp.local."
|
*args, **kwargs, limit_service="_hap._udp.local."
|
||||||
),
|
),
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_homekit_info_mock(
|
side_effect=get_homekit_info_mock(
|
||||||
"Smart Bridge-001", HOMEKIT_STATUS_UNPAIRED
|
"Smart Bridge-001", HOMEKIT_STATUS_UNPAIRED
|
||||||
),
|
),
|
||||||
@ -662,14 +663,14 @@ async def test_homekit_match_partial_fnmatch(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf,
|
discovery,
|
||||||
"AsyncServiceBrowser",
|
"AsyncServiceBrowser",
|
||||||
side_effect=lambda *args, **kwargs: service_update_mock(
|
side_effect=lambda *args, **kwargs: service_update_mock(
|
||||||
*args, **kwargs, limit_service="_hap._tcp.local."
|
*args, **kwargs, limit_service="_hap._tcp.local."
|
||||||
),
|
),
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_homekit_info_mock("YLDP13YL", HOMEKIT_STATUS_UNPAIRED),
|
side_effect=get_homekit_info_mock("YLDP13YL", HOMEKIT_STATUS_UNPAIRED),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -698,14 +699,14 @@ async def test_homekit_match_full(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf,
|
discovery,
|
||||||
"AsyncServiceBrowser",
|
"AsyncServiceBrowser",
|
||||||
side_effect=lambda *args, **kwargs: service_update_mock(
|
side_effect=lambda *args, **kwargs: service_update_mock(
|
||||||
*args, **kwargs, limit_service="_hap._udp.local."
|
*args, **kwargs, limit_service="_hap._udp.local."
|
||||||
),
|
),
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED),
|
side_effect=get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -737,14 +738,14 @@ async def test_homekit_already_paired(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf,
|
discovery,
|
||||||
"AsyncServiceBrowser",
|
"AsyncServiceBrowser",
|
||||||
side_effect=lambda *args, **kwargs: service_update_mock(
|
side_effect=lambda *args, **kwargs: service_update_mock(
|
||||||
*args, **kwargs, limit_service="_hap._tcp.local."
|
*args, **kwargs, limit_service="_hap._tcp.local."
|
||||||
),
|
),
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_homekit_info_mock("tado", HOMEKIT_STATUS_PAIRED),
|
side_effect=get_homekit_info_mock("tado", HOMEKIT_STATUS_PAIRED),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -774,14 +775,14 @@ async def test_homekit_invalid_paring_status(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf,
|
discovery,
|
||||||
"AsyncServiceBrowser",
|
"AsyncServiceBrowser",
|
||||||
side_effect=lambda *args, **kwargs: service_update_mock(
|
side_effect=lambda *args, **kwargs: service_update_mock(
|
||||||
*args, **kwargs, limit_service="_hap._tcp.local."
|
*args, **kwargs, limit_service="_hap._tcp.local."
|
||||||
),
|
),
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_homekit_info_mock("Smart Bridge", b"invalid"),
|
side_effect=get_homekit_info_mock("Smart Bridge", b"invalid"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -805,10 +806,10 @@ async def test_homekit_not_paired(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock
|
discovery, "AsyncServiceBrowser", side_effect=service_update_mock
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_homekit_info_mock(
|
side_effect=get_homekit_info_mock(
|
||||||
"this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED
|
"this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED
|
||||||
),
|
),
|
||||||
@ -847,14 +848,14 @@ async def test_homekit_controller_still_discovered_unpaired_for_cloud(
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf,
|
discovery,
|
||||||
"AsyncServiceBrowser",
|
"AsyncServiceBrowser",
|
||||||
side_effect=lambda *args, **kwargs: service_update_mock(
|
side_effect=lambda *args, **kwargs: service_update_mock(
|
||||||
*args, **kwargs, limit_service="_hap._udp.local."
|
*args, **kwargs, limit_service="_hap._udp.local."
|
||||||
),
|
),
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_homekit_info_mock("Rachio-xyz", HOMEKIT_STATUS_UNPAIRED),
|
side_effect=get_homekit_info_mock("Rachio-xyz", HOMEKIT_STATUS_UNPAIRED),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -892,14 +893,14 @@ async def test_homekit_controller_still_discovered_unpaired_for_polling(
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf,
|
discovery,
|
||||||
"AsyncServiceBrowser",
|
"AsyncServiceBrowser",
|
||||||
side_effect=lambda *args, **kwargs: service_update_mock(
|
side_effect=lambda *args, **kwargs: service_update_mock(
|
||||||
*args, **kwargs, limit_service="_hap._udp.local."
|
*args, **kwargs, limit_service="_hap._udp.local."
|
||||||
),
|
),
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_homekit_info_mock("iSmartGate", HOMEKIT_STATUS_UNPAIRED),
|
side_effect=get_homekit_info_mock("iSmartGate", HOMEKIT_STATUS_UNPAIRED),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -1053,9 +1054,9 @@ async def test_removed_ignored(hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_service_info_mock,
|
side_effect=get_service_info_mock,
|
||||||
) as mock_service_info,
|
) as mock_service_info,
|
||||||
):
|
):
|
||||||
@ -1088,13 +1089,13 @@ async def test_async_detect_interfaces_setting_non_loopback_route(
|
|||||||
with (
|
with (
|
||||||
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||||
return_value=_ADAPTER_WITH_DEFAULT_ENABLED,
|
return_value=_ADAPTER_WITH_DEFAULT_ENABLED,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_service_info_mock,
|
side_effect=get_service_info_mock,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -1176,13 +1177,13 @@ async def test_async_detect_interfaces_setting_empty_route_linux(
|
|||||||
patch("homeassistant.components.zeroconf.sys.platform", "linux"),
|
patch("homeassistant.components.zeroconf.sys.platform", "linux"),
|
||||||
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||||
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_service_info_mock,
|
side_effect=get_service_info_mock,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -1210,13 +1211,13 @@ async def test_async_detect_interfaces_setting_empty_route_freebsd(
|
|||||||
patch("homeassistant.components.zeroconf.sys.platform", "freebsd"),
|
patch("homeassistant.components.zeroconf.sys.platform", "freebsd"),
|
||||||
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||||
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_service_info_mock,
|
side_effect=get_service_info_mock,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -1261,13 +1262,13 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_linux(
|
|||||||
patch("homeassistant.components.zeroconf.sys.platform", "linux"),
|
patch("homeassistant.components.zeroconf.sys.platform", "linux"),
|
||||||
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||||
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_service_info_mock,
|
side_effect=get_service_info_mock,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -1290,13 +1291,13 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_freebsd(
|
|||||||
patch("homeassistant.components.zeroconf.sys.platform", "freebsd"),
|
patch("homeassistant.components.zeroconf.sys.platform", "freebsd"),
|
||||||
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||||
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_service_info_mock,
|
side_effect=get_service_info_mock,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -1319,13 +1320,13 @@ async def test_async_detect_interfaces_explicitly_before_setup(
|
|||||||
patch("homeassistant.components.zeroconf.sys.platform", "linux"),
|
patch("homeassistant.components.zeroconf.sys.platform", "linux"),
|
||||||
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||||
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_service_info_mock,
|
side_effect=get_service_info_mock,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -1359,14 +1360,14 @@ async def test_setup_with_disallowed_characters_in_local_name(
|
|||||||
"""Test we still setup with disallowed characters in the location name."""
|
"""Test we still setup with disallowed characters in the location name."""
|
||||||
with (
|
with (
|
||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch.object(
|
patch.object(
|
||||||
hass.config,
|
hass.config,
|
||||||
"location_name",
|
"location_name",
|
||||||
"My.House",
|
"My.House",
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo.async_request",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo.async_request",
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
||||||
@ -1422,10 +1423,10 @@ async def test_zeroconf_removed(hass: HomeAssistant) -> None:
|
|||||||
) as mock_async_progress_by_init_data_type,
|
) as mock_async_progress_by_init_data_type,
|
||||||
patch.object(hass.config_entries.flow, "async_abort") as mock_async_abort,
|
patch.object(hass.config_entries.flow, "async_abort") as mock_async_abort,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf, "AsyncServiceBrowser", side_effect=_device_removed_mock
|
discovery, "AsyncServiceBrowser", side_effect=_device_removed_mock
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
|
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -1545,10 +1546,10 @@ async def test_zeroconf_rediscover(
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
discovery, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
|
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -1665,10 +1666,10 @@ async def test_zeroconf_rediscover_no_match(
|
|||||||
),
|
),
|
||||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||||
patch.object(
|
patch.object(
|
||||||
zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
discovery, "AsyncServiceBrowser", side_effect=http_only_service_update_mock
|
||||||
) as mock_service_browser,
|
) as mock_service_browser,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
||||||
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
|
side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
|
@ -1346,7 +1346,10 @@ def mock_zeroconf() -> Generator[MagicMock]:
|
|||||||
|
|
||||||
with (
|
with (
|
||||||
patch("homeassistant.components.zeroconf.HaZeroconf", autospec=True) as mock_zc,
|
patch("homeassistant.components.zeroconf.HaZeroconf", autospec=True) as mock_zc,
|
||||||
patch("homeassistant.components.zeroconf.AsyncServiceBrowser", autospec=True),
|
patch(
|
||||||
|
"homeassistant.components.zeroconf.discovery.AsyncServiceBrowser",
|
||||||
|
autospec=True,
|
||||||
|
),
|
||||||
):
|
):
|
||||||
zc = mock_zc.return_value
|
zc = mock_zc.return_value
|
||||||
# DNSCache has strong Cython type checks, and MagicMock does not work
|
# DNSCache has strong Cython type checks, and MagicMock does not work
|
||||||
|
Loading…
x
Reference in New Issue
Block a user