Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for exposing Home Assistant via Zeroconf."""
2 
3 from __future__ import annotations
4 
5 import contextlib
6 from contextlib import suppress
7 from dataclasses import dataclass
8 from fnmatch import translate
9 from functools import lru_cache
10 from ipaddress import IPv4Address, IPv6Address
11 import logging
12 import re
13 import sys
14 from typing import TYPE_CHECKING, Any, Final, cast
15 
16 import voluptuous as vol
17 from zeroconf import (
18  BadTypeInNameException,
19  InterfaceChoice,
20  IPVersion,
21  ServiceStateChange,
22 )
23 from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo
24 
25 from homeassistant import config_entries
26 from homeassistant.components import network
27 from homeassistant.const import (
28  EVENT_HOMEASSISTANT_CLOSE,
29  EVENT_HOMEASSISTANT_STOP,
30  __version__,
31 )
32 from homeassistant.core import Event, HomeAssistant, callback
33 from homeassistant.data_entry_flow import BaseServiceInfo
34 from homeassistant.helpers import discovery_flow, instance_id
36 from homeassistant.helpers.discovery_flow import DiscoveryKey
37 from homeassistant.helpers.dispatcher import async_dispatcher_connect
38 from homeassistant.helpers.network import NoURLAvailableError, get_url
39 from homeassistant.helpers.typing import ConfigType
40 from homeassistant.loader import (
41  HomeKitDiscoveredIntegration,
42  ZeroconfMatcher,
43  async_get_homekit,
44  async_get_zeroconf,
45  bind_hass,
46 )
47 from homeassistant.setup import async_when_setup_or_start
48 
49 from .models import HaAsyncZeroconf, HaZeroconf
50 from .usage import install_multiple_zeroconf_catcher
51 
52 _LOGGER = logging.getLogger(__name__)
53 
54 DOMAIN = "zeroconf"
55 
56 ZEROCONF_TYPE = "_home-assistant._tcp.local."
57 HOMEKIT_TYPES = [
58  "_hap._tcp.local.",
59  # Thread based devices
60  "_hap._udp.local.",
61 ]
62 _HOMEKIT_MODEL_SPLITS = (None, " ", "-")
63 
64 
65 CONF_DEFAULT_INTERFACE = "default_interface"
66 CONF_IPV6 = "ipv6"
67 DEFAULT_DEFAULT_INTERFACE = True
68 DEFAULT_IPV6 = True
69 
70 HOMEKIT_PAIRED_STATUS_FLAG = "sf"
71 HOMEKIT_MODEL_LOWER = "md"
72 HOMEKIT_MODEL_UPPER = "MD"
73 
74 # Property key=value has a max length of 255
75 # so we use 230 to leave space for key=
76 MAX_PROPERTY_VALUE_LEN = 230
77 
78 # Dns label max length
79 MAX_NAME_LEN = 63
80 
81 ATTR_DOMAIN: Final = "domain"
82 ATTR_NAME: Final = "name"
83 ATTR_PROPERTIES: Final = "properties"
84 
85 # Attributes for ZeroconfServiceInfo[ATTR_PROPERTIES]
86 ATTR_PROPERTIES_ID: Final = "id"
87 
88 CONFIG_SCHEMA = vol.Schema(
89  {
90  DOMAIN: vol.All(
91  cv.deprecated(CONF_DEFAULT_INTERFACE),
92  cv.deprecated(CONF_IPV6),
93  vol.Schema(
94  {
95  vol.Optional(CONF_DEFAULT_INTERFACE): cv.boolean,
96  vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean,
97  }
98  ),
99  )
100  },
101  extra=vol.ALLOW_EXTRA,
102 )
103 
104 
105 @dataclass(slots=True)
107  """Prepared info from mDNS entries.
108 
109  The ip_address is the most recently updated address
110  that is not a link local or unspecified address.
111 
112  The ip_addresses are all addresses in order of most
113  recently updated to least recently updated.
114 
115  The host is the string representation of the ip_address.
116 
117  The addresses are the string representations of the
118  ip_addresses.
119 
120  It is recommended to use the ip_address to determine
121  the address to connect to as it will be the most
122  recently updated address that is not a link local
123  or unspecified address.
124  """
125 
126  ip_address: IPv4Address | IPv6Address
127  ip_addresses: list[IPv4Address | IPv6Address]
128  port: int | None
129  hostname: str
130  type: str
131  name: str
132  properties: dict[str, Any]
133 
134  @property
135  def host(self) -> str:
136  """Return the host."""
137  return str(self.ip_address)
138 
139  @property
140  def addresses(self) -> list[str]:
141  """Return the addresses."""
142  return [str(ip_address) for ip_address in self.ip_addresses]
143 
144 
145 @bind_hass
146 async def async_get_instance(hass: HomeAssistant) -> HaZeroconf:
147  """Zeroconf instance to be shared with other integrations that use it."""
148  return cast(HaZeroconf, (await _async_get_instance(hass)).zeroconf)
149 
150 
151 @bind_hass
152 async def async_get_async_instance(hass: HomeAssistant) -> HaAsyncZeroconf:
153  """Zeroconf instance to be shared with other integrations that use it."""
154  return await _async_get_instance(hass)
155 
156 
157 async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZeroconf:
158  if DOMAIN in hass.data:
159  return cast(HaAsyncZeroconf, hass.data[DOMAIN])
160 
161  logging.getLogger("zeroconf").setLevel(logging.NOTSET)
162 
163  zeroconf = HaZeroconf(**zcargs)
164  aio_zc = HaAsyncZeroconf(zc=zeroconf)
165 
167 
168  async def _async_stop_zeroconf(_event: Event) -> None:
169  """Stop Zeroconf."""
170  await aio_zc.ha_async_close()
171 
172  # Wait to the close event to shutdown zeroconf to give
173  # integrations time to send a good bye message
174  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_zeroconf)
175  hass.data[DOMAIN] = aio_zc
176 
177  return aio_zc
178 
179 
180 @callback
182  """Return true for platforms not supporting IP_ADD_MEMBERSHIP on an AF_INET6 socket.
183 
184  Zeroconf only supports a single listen socket at this time.
185  """
186  return not sys.platform.startswith("freebsd") and not sys.platform.startswith(
187  "darwin"
188  )
189 
190 
191 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
192  """Set up Zeroconf and make Home Assistant discoverable."""
193  zc_args: dict = {"ip_version": IPVersion.V4Only}
194 
195  adapters = await network.async_get_adapters(hass)
196 
197  ipv6 = False
199  if any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
200  ipv6 = True
201  zc_args["ip_version"] = IPVersion.All
202  elif not any(adapter["enabled"] and adapter["ipv4"] for adapter in adapters):
203  zc_args["ip_version"] = IPVersion.V6Only
204  ipv6 = True
205 
206  if not ipv6 and network.async_only_default_interface_enabled(adapters):
207  zc_args["interfaces"] = InterfaceChoice.Default
208  else:
209  zc_args["interfaces"] = [
210  str(source_ip)
211  for source_ip in await network.async_get_enabled_source_ips(hass)
212  if not source_ip.is_loopback
213  and not (isinstance(source_ip, IPv6Address) and source_ip.is_global)
214  and not (
215  isinstance(source_ip, IPv6Address)
216  and zc_args["ip_version"] == IPVersion.V4Only
217  )
218  and not (
219  isinstance(source_ip, IPv4Address)
220  and zc_args["ip_version"] == IPVersion.V6Only
221  )
222  ]
223 
224  aio_zc = await _async_get_instance(hass, **zc_args)
225  zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
226  zeroconf_types = await async_get_zeroconf(hass)
227  homekit_models = await async_get_homekit(hass)
228  homekit_model_lookup, homekit_model_matchers = _build_homekit_model_lookups(
229  homekit_models
230  )
231  discovery = ZeroconfDiscovery(
232  hass,
233  zeroconf,
234  zeroconf_types,
235  homekit_model_lookup,
236  homekit_model_matchers,
237  )
238  await discovery.async_setup()
239 
240  async def _async_zeroconf_hass_start(hass: HomeAssistant, comp: str) -> None:
241  """Expose Home Assistant on zeroconf when it starts.
242 
243  Wait till started or otherwise HTTP is not up and running.
244  """
245  uuid = await instance_id.async_get(hass)
246  await _async_register_hass_zc_service(hass, aio_zc, uuid)
247 
248  async def _async_zeroconf_hass_stop(_event: Event) -> None:
249  await discovery.async_stop()
250 
251  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_zeroconf_hass_stop)
252  async_when_setup_or_start(hass, "frontend", _async_zeroconf_hass_start)
253 
254  return True
255 
256 
258  homekit_models: dict[str, HomeKitDiscoveredIntegration],
259 ) -> tuple[
260  dict[str, HomeKitDiscoveredIntegration],
261  dict[re.Pattern, HomeKitDiscoveredIntegration],
262 ]:
263  """Build lookups for homekit models."""
264  homekit_model_lookup: dict[str, HomeKitDiscoveredIntegration] = {}
265  homekit_model_matchers: dict[re.Pattern, HomeKitDiscoveredIntegration] = {}
266 
267  for model, discovery in homekit_models.items():
268  if "*" in model or "?" in model or "[" in model:
269  homekit_model_matchers[_compile_fnmatch(model)] = discovery
270  else:
271  homekit_model_lookup[model] = discovery
272 
273  return homekit_model_lookup, homekit_model_matchers
274 
275 
276 def _filter_disallowed_characters(name: str) -> str:
277  """Filter disallowed characters from a string.
278 
279  . is a reversed character for zeroconf.
280  """
281  return name.replace(".", " ")
282 
283 
285  hass: HomeAssistant, aio_zc: HaAsyncZeroconf, uuid: str
286 ) -> None:
287  # Get instance UUID
288  valid_location_name = _truncate_location_name_to_valid(
289  _filter_disallowed_characters(hass.config.location_name or "Home")
290  )
291 
292  params = {
293  "location_name": valid_location_name,
294  "uuid": uuid,
295  "version": __version__,
296  "external_url": "",
297  "internal_url": "",
298  # Old base URL, for backward compatibility
299  "base_url": "",
300  # Always needs authentication
301  "requires_api_password": True,
302  }
303 
304  # Get instance URL's
305  with suppress(NoURLAvailableError):
306  params["external_url"] = get_url(hass, allow_internal=False)
307 
308  with suppress(NoURLAvailableError):
309  params["internal_url"] = get_url(hass, allow_external=False)
310 
311  # Set old base URL based on external or internal
312  params["base_url"] = params["external_url"] or params["internal_url"]
313 
315 
316  info = AsyncServiceInfo(
317  ZEROCONF_TYPE,
318  name=f"{valid_location_name}.{ZEROCONF_TYPE}",
319  server=f"{uuid}.local.",
320  parsed_addresses=await network.async_get_announce_addresses(hass),
321  port=hass.http.server_port,
322  properties=params,
323  )
324 
325  _LOGGER.info("Starting Zeroconf broadcast")
326  await aio_zc.async_register_service(info, allow_name_change=True)
327 
328 
329 def _match_against_props(matcher: dict[str, str], props: dict[str, str | None]) -> bool:
330  """Check a matcher to ensure all values in props."""
331  for key, value in matcher.items():
332  prop_val = props.get(key)
333  if prop_val is None or not _memorized_fnmatch(prop_val.lower(), value):
334  return False
335  return True
336 
337 
338 def is_homekit_paired(props: dict[str, Any]) -> bool:
339  """Check properties to see if a device is homekit paired."""
340  if HOMEKIT_PAIRED_STATUS_FLAG not in props:
341  return False
342  with contextlib.suppress(ValueError):
343  # 0 means paired and not discoverable by iOS clients)
344  return int(props[HOMEKIT_PAIRED_STATUS_FLAG]) == 0
345  # If we cannot tell, we assume its not paired
346  return False
347 
348 
350  """Discovery via zeroconf."""
351 
352  def __init__(
353  self,
354  hass: HomeAssistant,
355  zeroconf: HaZeroconf,
356  zeroconf_types: dict[str, list[ZeroconfMatcher]],
357  homekit_model_lookups: dict[str, HomeKitDiscoveredIntegration],
358  homekit_model_matchers: dict[re.Pattern, HomeKitDiscoveredIntegration],
359  ) -> None:
360  """Init discovery."""
361  self.hasshass = hass
362  self.zeroconfzeroconf = zeroconf
363  self.zeroconf_typeszeroconf_types = zeroconf_types
364  self.homekit_model_lookupshomekit_model_lookups = homekit_model_lookups
365  self.homekit_model_matchershomekit_model_matchers = homekit_model_matchers
366  self.async_service_browserasync_service_browser: AsyncServiceBrowser | None = None
367 
368  async def async_setup(self) -> None:
369  """Start discovery."""
370  types = list(self.zeroconf_typeszeroconf_types)
371  # We want to make sure we know about other HomeAssistant
372  # instances as soon as possible to avoid name conflicts
373  # so we always browse for ZEROCONF_TYPE
374  types.extend(
375  hk_type
376  for hk_type in (ZEROCONF_TYPE, *HOMEKIT_TYPES)
377  if hk_type not in self.zeroconf_typeszeroconf_types
378  )
379  _LOGGER.debug("Starting Zeroconf browser for: %s", types)
380  self.async_service_browserasync_service_browser = AsyncServiceBrowser(
381  self.zeroconfzeroconf, types, handlers=[self.async_service_updateasync_service_update]
382  )
383 
385  self.hasshass,
386  config_entries.signal_discovered_config_entry_removed(DOMAIN),
387  self._handle_config_entry_removed_handle_config_entry_removed,
388  )
389 
390  async def async_stop(self) -> None:
391  """Cancel the service browser and stop processing the queue."""
392  if self.async_service_browserasync_service_browser:
393  await self.async_service_browserasync_service_browser.async_cancel()
394 
395  @callback
397  self,
398  entry: config_entries.ConfigEntry,
399  ) -> None:
400  """Handle config entry changes."""
401  for discovery_key in entry.discovery_keys[DOMAIN]:
402  if discovery_key.version != 1:
403  continue
404  _type = discovery_key.key[0]
405  name = discovery_key.key[1]
406  _LOGGER.debug("Rediscover service %s.%s", _type, name)
407  self._async_service_update_async_service_update(self.zeroconfzeroconf, _type, name)
408 
409  def _async_dismiss_discoveries(self, name: str) -> None:
410  """Dismiss all discoveries for the given name."""
411  for flow in self.hasshass.config_entries.flow.async_progress_by_init_data_type(
412  ZeroconfServiceInfo,
413  lambda service_info: bool(service_info.name == name),
414  ):
415  self.hasshass.config_entries.flow.async_abort(flow["flow_id"])
416 
417  @callback
419  self,
420  zeroconf: HaZeroconf,
421  service_type: str,
422  name: str,
423  state_change: ServiceStateChange,
424  ) -> None:
425  """Service state changed."""
426  _LOGGER.debug(
427  "service_update: type=%s name=%s state_change=%s",
428  service_type,
429  name,
430  state_change,
431  )
432 
433  if state_change is ServiceStateChange.Removed:
434  self._async_dismiss_discoveries_async_dismiss_discoveries(name)
435  return
436 
437  self._async_service_update_async_service_update(zeroconf, service_type, name)
438 
439  @callback
441  self,
442  zeroconf: HaZeroconf,
443  service_type: str,
444  name: str,
445  ) -> None:
446  """Service state added or changed."""
447  try:
448  async_service_info = AsyncServiceInfo(service_type, name)
449  except BadTypeInNameException as ex:
450  # Some devices broadcast a name that is not a valid DNS name
451  # This is a bug in the device firmware and we should ignore it
452  _LOGGER.debug("Bad name in zeroconf record: %s: %s", name, ex)
453  return
454 
455  if async_service_info.load_from_cache(zeroconf):
456  self._async_process_service_update_async_process_service_update(async_service_info, service_type, name)
457  else:
458  self.hasshass.async_create_background_task(
459  self._async_lookup_and_process_service_update_async_lookup_and_process_service_update(
460  zeroconf, async_service_info, service_type, name
461  ),
462  name=f"zeroconf lookup {name}.{service_type}",
463  )
464 
466  self,
467  zeroconf: HaZeroconf,
468  async_service_info: AsyncServiceInfo,
469  service_type: str,
470  name: str,
471  ) -> None:
472  """Update and process a zeroconf update."""
473  await async_service_info.async_request(zeroconf, 3000)
474  self._async_process_service_update_async_process_service_update(async_service_info, service_type, name)
475 
476  @callback
478  self, async_service_info: AsyncServiceInfo, service_type: str, name: str
479  ) -> None:
480  """Process a zeroconf update."""
481  info = info_from_service(async_service_info)
482  if not info:
483  # Prevent the browser thread from collapsing
484  _LOGGER.debug("Failed to get addresses for device %s", name)
485  return
486  _LOGGER.debug("Discovered new device %s %s", name, info)
487  props: dict[str, str | None] = info.properties
488  discovery_key = DiscoveryKey(
489  domain=DOMAIN,
490  key=(info.type, info.name),
491  version=1,
492  )
493  domain = None
494 
495  # If we can handle it as a HomeKit discovery, we do that here.
496  if service_type in HOMEKIT_TYPES and (
497  homekit_discovery := async_get_homekit_discovery(
498  self.homekit_model_lookupshomekit_model_lookups, self.homekit_model_matchershomekit_model_matchers, props
499  )
500  ):
501  domain = homekit_discovery.domain
502  discovery_flow.async_create_flow(
503  self.hasshass,
504  homekit_discovery.domain,
505  {"source": config_entries.SOURCE_HOMEKIT},
506  info,
507  discovery_key=discovery_key,
508  )
509  # Continue on here as homekit_controller
510  # still needs to get updates on devices
511  # so it can see when the 'c#' field is updated.
512  #
513  # We only send updates to homekit_controller
514  # if the device is already paired in order to avoid
515  # offering a second discovery for the same device
516  if not is_homekit_paired(props) and not homekit_discovery.always_discover:
517  # If the device is paired with HomeKit we must send on
518  # the update to homekit_controller so it can see when
519  # the 'c#' field is updated. This is used to detect
520  # when the device has been reset or updated.
521  #
522  # If the device is not paired and we should not always
523  # discover it, we can stop here.
524  return
525 
526  if not (matchers := self.zeroconf_typeszeroconf_types.get(service_type)):
527  return
528 
529  # Not all homekit types are currently used for discovery
530  # so not all service type exist in zeroconf_types
531  for matcher in matchers:
532  if len(matcher) > 1:
533  if ATTR_NAME in matcher and not _memorized_fnmatch(
534  info.name.lower(), matcher[ATTR_NAME]
535  ):
536  continue
537  if ATTR_PROPERTIES in matcher and not _match_against_props(
538  matcher[ATTR_PROPERTIES], props
539  ):
540  continue
541 
542  matcher_domain = matcher[ATTR_DOMAIN]
543  # Create a type annotated regular dict since this is a hot path and creating
544  # a regular dict is slightly cheaper than calling ConfigFlowContext
546  "source": config_entries.SOURCE_ZEROCONF,
547  }
548  if domain:
549  # Domain of integration that offers alternative API to handle
550  # this device.
551  context["alternative_domain"] = domain
552 
553  discovery_flow.async_create_flow(
554  self.hasshass,
555  matcher_domain,
556  context,
557  info,
558  discovery_key=discovery_key,
559  )
560 
561 
563  homekit_model_lookups: dict[str, HomeKitDiscoveredIntegration],
564  homekit_model_matchers: dict[re.Pattern, HomeKitDiscoveredIntegration],
565  props: dict[str, Any],
566 ) -> HomeKitDiscoveredIntegration | None:
567  """Handle a HomeKit discovery.
568 
569  Return the domain to forward the discovery data to
570  """
571  if not (
572  model := props.get(HOMEKIT_MODEL_LOWER) or props.get(HOMEKIT_MODEL_UPPER)
573  ) or not isinstance(model, str):
574  return None
575 
576  for split_str in _HOMEKIT_MODEL_SPLITS:
577  key = (model.split(split_str))[0] if split_str else model
578  if discovery := homekit_model_lookups.get(key):
579  return discovery
580 
581  for pattern, discovery in homekit_model_matchers.items():
582  if pattern.match(model):
583  return discovery
584 
585  return None
586 
587 
588 def info_from_service(service: AsyncServiceInfo) -> ZeroconfServiceInfo | None:
589  """Return prepared info from mDNS entries."""
590  # See https://ietf.org/rfc/rfc6763.html#section-6.4 and
591  # https://ietf.org/rfc/rfc6763.html#section-6.5 for expected encodings
592  # for property keys and values
593  if not (maybe_ip_addresses := service.ip_addresses_by_version(IPVersion.All)):
594  return None
595  if TYPE_CHECKING:
596  ip_addresses = cast(list[IPv4Address | IPv6Address], maybe_ip_addresses)
597  else:
598  ip_addresses = maybe_ip_addresses
599  ip_address: IPv4Address | IPv6Address | None = None
600  for ip_addr in ip_addresses:
601  if not ip_addr.is_link_local and not ip_addr.is_unspecified:
602  ip_address = ip_addr
603  break
604  if not ip_address:
605  return None
606 
607  if TYPE_CHECKING:
608  assert (
609  service.server is not None
610  ), "server cannot be none if there are addresses"
611  return ZeroconfServiceInfo(
612  ip_address=ip_address,
613  ip_addresses=ip_addresses,
614  port=service.port,
615  hostname=service.server,
616  type=service.type,
617  name=service.name,
618  properties=service.decoded_properties,
619  )
620 
621 
622 def _suppress_invalid_properties(properties: dict) -> None:
623  """Suppress any properties that will cause zeroconf to fail to startup."""
624 
625  for prop, prop_value in properties.items():
626  if not isinstance(prop_value, str):
627  continue
628 
629  if len(prop_value.encode("utf-8")) > MAX_PROPERTY_VALUE_LEN:
630  _LOGGER.error(
631  (
632  "The property '%s' was suppressed because it is longer than the"
633  " maximum length of %d bytes: %s"
634  ),
635  prop,
636  MAX_PROPERTY_VALUE_LEN,
637  prop_value,
638  )
639  properties[prop] = ""
640 
641 
642 def _truncate_location_name_to_valid(location_name: str) -> str:
643  """Truncate or return the location name usable for zeroconf."""
644  if len(location_name.encode("utf-8")) < MAX_NAME_LEN:
645  return location_name
646 
647  _LOGGER.warning(
648  (
649  "The location name was truncated because it is longer than the maximum"
650  " length of %d bytes: %s"
651  ),
652  MAX_NAME_LEN,
653  location_name,
654  )
655  return location_name.encode("utf-8")[:MAX_NAME_LEN].decode("utf-8", "ignore")
656 
657 
658 @lru_cache(maxsize=4096, typed=True)
659 def _compile_fnmatch(pattern: str) -> re.Pattern:
660  """Compile a fnmatch pattern."""
661  return re.compile(translate(pattern))
662 
663 
664 @lru_cache(maxsize=1024, typed=True)
665 def _memorized_fnmatch(name: str, pattern: str) -> bool:
666  """Memorized version of fnmatch that has a larger lru_cache.
667 
668  The default version of fnmatch only has a lru_cache of 256 entries.
669  With many devices we quickly reach that limit and end up compiling
670  the same pattern over and over again.
671 
672  Zeroconf has its own memorized fnmatch with its own lru_cache
673  since the data is going to be relatively the same
674  since the devices will not change frequently
675  """
676  return bool(_compile_fnmatch(pattern).match(name))
None _async_lookup_and_process_service_update(self, HaZeroconf zeroconf, AsyncServiceInfo async_service_info, str service_type, str name)
Definition: __init__.py:471
None __init__(self, HomeAssistant hass, HaZeroconf zeroconf, dict[str, list[ZeroconfMatcher]] zeroconf_types, dict[str, HomeKitDiscoveredIntegration] homekit_model_lookups, dict[re.Pattern, HomeKitDiscoveredIntegration] homekit_model_matchers)
Definition: __init__.py:359
None async_service_update(self, HaZeroconf zeroconf, str service_type, str name, ServiceStateChange state_change)
Definition: __init__.py:424
None _async_process_service_update(self, AsyncServiceInfo async_service_info, str service_type, str name)
Definition: __init__.py:479
None _handle_config_entry_removed(self, config_entries.ConfigEntry entry)
Definition: __init__.py:399
None _async_service_update(self, HaZeroconf zeroconf, str service_type, str name)
Definition: __init__.py:445
list[_T] match(self, BluetoothServiceInfoBleak service_info)
Definition: match.py:246
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None install_multiple_zeroconf_catcher(HaZeroconf hass_zc)
Definition: usage.py:12
bool _memorized_fnmatch(str name, str pattern)
Definition: __init__.py:665
re.Pattern _compile_fnmatch(str pattern)
Definition: __init__.py:659
str _truncate_location_name_to_valid(str location_name)
Definition: __init__.py:642
None _suppress_invalid_properties(dict properties)
Definition: __init__.py:622
None _async_register_hass_zc_service(HomeAssistant hass, HaAsyncZeroconf aio_zc, str uuid)
Definition: __init__.py:286
HaZeroconf async_get_instance(HomeAssistant hass)
Definition: __init__.py:146
HaAsyncZeroconf async_get_async_instance(HomeAssistant hass)
Definition: __init__.py:152
HaAsyncZeroconf _async_get_instance(HomeAssistant hass, **Any zcargs)
Definition: __init__.py:157
str _filter_disallowed_characters(str name)
Definition: __init__.py:276
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:191
bool is_homekit_paired(dict[str, Any] props)
Definition: __init__.py:338
HomeKitDiscoveredIntegration|None async_get_homekit_discovery(dict[str, HomeKitDiscoveredIntegration] homekit_model_lookups, dict[re.Pattern, HomeKitDiscoveredIntegration] homekit_model_matchers, dict[str, Any] props)
Definition: __init__.py:566
ZeroconfServiceInfo|None info_from_service(AsyncServiceInfo service)
Definition: __init__.py:588
tuple[ dict[str, HomeKitDiscoveredIntegration], dict[re.Pattern, HomeKitDiscoveredIntegration],] _build_homekit_model_lookups(dict[str, HomeKitDiscoveredIntegration] homekit_models)
Definition: __init__.py:262
bool _match_against_props(dict[str, str] matcher, dict[str, str|None] props)
Definition: __init__.py:329
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
str get_url(HomeAssistant hass, *bool require_current_request=False, bool require_ssl=False, bool require_standard_port=False, bool require_cloud=False, bool allow_internal=True, bool allow_external=True, bool allow_cloud=True, bool|None allow_ip=None, bool|None prefer_external=None, bool prefer_cloud=False)
Definition: network.py:131
dict[str, list[ZeroconfMatcher]] async_get_zeroconf(HomeAssistant hass)
Definition: loader.py:494
dict[str, HomeKitDiscoveredIntegration] async_get_homekit(HomeAssistant hass)
Definition: loader.py:583
None async_when_setup_or_start(core.HomeAssistant hass, str component, Callable[[core.HomeAssistant, str], Awaitable[None]] when_setup_cb)
Definition: setup.py:597