mirror of
https://github.com/home-assistant/core.git
synced 2025-08-11 00:25:12 +02:00
Refactor zeroconf task handling
- Avoid the need to create tasks for most callbacks - Fixes the untracked task that could get unexpectedly GCed
This commit is contained in:
@@ -35,9 +35,8 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.loader import (
|
from homeassistant.loader import (
|
||||||
Integration,
|
HomeKitModel,
|
||||||
async_get_homekit,
|
async_get_homekit,
|
||||||
async_get_integration,
|
|
||||||
async_get_zeroconf,
|
async_get_zeroconf,
|
||||||
bind_hass,
|
bind_hass,
|
||||||
)
|
)
|
||||||
@@ -348,7 +347,7 @@ class ZeroconfDiscovery:
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
zeroconf: HaZeroconf,
|
zeroconf: HaZeroconf,
|
||||||
zeroconf_types: dict[str, list[dict[str, str | dict[str, str]]]],
|
zeroconf_types: dict[str, list[dict[str, str | dict[str, str]]]],
|
||||||
homekit_models: dict[str, str],
|
homekit_models: dict[str, HomeKitModel],
|
||||||
ipv6: bool,
|
ipv6: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init discovery."""
|
"""Init discovery."""
|
||||||
@@ -398,12 +397,6 @@ class ZeroconfDiscovery:
|
|||||||
if state_change == ServiceStateChange.Removed:
|
if state_change == ServiceStateChange.Removed:
|
||||||
return
|
return
|
||||||
|
|
||||||
asyncio.create_task(self._process_service_update(zeroconf, service_type, name))
|
|
||||||
|
|
||||||
async def _process_service_update(
|
|
||||||
self, zeroconf: HaZeroconf, service_type: str, name: str
|
|
||||||
) -> None:
|
|
||||||
"""Process a zeroconf update."""
|
|
||||||
try:
|
try:
|
||||||
async_service_info = AsyncServiceInfo(service_type, name)
|
async_service_info = AsyncServiceInfo(service_type, name)
|
||||||
except BadTypeInNameException as ex:
|
except BadTypeInNameException as ex:
|
||||||
@@ -411,24 +404,53 @@ class ZeroconfDiscovery:
|
|||||||
# This is a bug in the device firmware and we should ignore it
|
# This is a bug in the device firmware and we should ignore it
|
||||||
_LOGGER.debug("Bad name in zeroconf record: %s: %s", name, ex)
|
_LOGGER.debug("Bad name in zeroconf record: %s: %s", name, ex)
|
||||||
return
|
return
|
||||||
await async_service_info.async_request(zeroconf, 3000)
|
|
||||||
|
|
||||||
|
if async_service_info.load_from_cache(zeroconf):
|
||||||
|
self._async_process_service_update(async_service_info, service_type, name)
|
||||||
|
else:
|
||||||
|
self.hass.async_create_task(
|
||||||
|
self._async_lookup_and_process_service_update(
|
||||||
|
zeroconf, async_service_info, service_type, name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
info = info_from_service(async_service_info)
|
||||||
if not info:
|
if not info:
|
||||||
# Prevent the browser thread from collapsing
|
# Prevent the browser thread from collapsing
|
||||||
_LOGGER.debug("Failed to get addresses for device %s", name)
|
_LOGGER.debug("Failed to get addresses for device %s", name)
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.debug("Discovered new device %s %s", name, info)
|
_LOGGER.debug("Discovered new device %s %s", name, info)
|
||||||
props: dict[str, str] = info.properties
|
props: dict[str, str] = info.properties
|
||||||
domain = None
|
domain = None
|
||||||
|
|
||||||
# If we can handle it as a HomeKit discovery, we do that here.
|
# If we can handle it as a HomeKit discovery, we do that here.
|
||||||
if service_type in HOMEKIT_TYPES and (
|
if service_type in HOMEKIT_TYPES and (
|
||||||
domain := async_get_homekit_discovery_domain(self.homekit_models, props)
|
homekit_model := async_get_homekit_discovery_domain(
|
||||||
|
self.homekit_models, props
|
||||||
|
)
|
||||||
):
|
):
|
||||||
|
domain = homekit_model.domain
|
||||||
discovery_flow.async_create_flow(
|
discovery_flow.async_create_flow(
|
||||||
self.hass, domain, {"source": config_entries.SOURCE_HOMEKIT}, info
|
self.hass,
|
||||||
|
homekit_model.domain,
|
||||||
|
{"source": config_entries.SOURCE_HOMEKIT},
|
||||||
|
info,
|
||||||
)
|
)
|
||||||
# Continue on here as homekit_controller
|
# Continue on here as homekit_controller
|
||||||
# still needs to get updates on devices
|
# still needs to get updates on devices
|
||||||
@@ -438,9 +460,6 @@ class ZeroconfDiscovery:
|
|||||||
# if the device is already paired in order to avoid
|
# if the device is already paired in order to avoid
|
||||||
# offering a second discovery for the same device
|
# offering a second discovery for the same device
|
||||||
if not is_homekit_paired(props):
|
if not is_homekit_paired(props):
|
||||||
integration: Integration = await async_get_integration(
|
|
||||||
self.hass, domain
|
|
||||||
)
|
|
||||||
# Since we prefer local control, if the integration that is being
|
# Since we prefer local control, if the integration that is being
|
||||||
# discovered is cloud AND the homekit device is UNPAIRED we still
|
# discovered is cloud AND the homekit device is UNPAIRED we still
|
||||||
# want to discovery it.
|
# want to discovery it.
|
||||||
@@ -453,9 +472,9 @@ class ZeroconfDiscovery:
|
|||||||
# dismissed in the event the user does not want to pair
|
# dismissed in the event the user does not want to pair
|
||||||
# with Home Assistant.
|
# with Home Assistant.
|
||||||
#
|
#
|
||||||
if not integration.iot_class or (
|
if not homekit_model.iot_class or (
|
||||||
not integration.iot_class.startswith("cloud")
|
not homekit_model.iot_class.startswith("cloud")
|
||||||
and "polling" not in integration.iot_class
|
and "polling" not in homekit_model.iot_class
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -495,8 +514,8 @@ class ZeroconfDiscovery:
|
|||||||
|
|
||||||
|
|
||||||
def async_get_homekit_discovery_domain(
|
def async_get_homekit_discovery_domain(
|
||||||
homekit_models: dict[str, str], props: dict[str, Any]
|
homekit_models: dict[str, HomeKitModel], props: dict[str, Any]
|
||||||
) -> str | None:
|
) -> HomeKitModel | None:
|
||||||
"""Handle a HomeKit discovery.
|
"""Handle a HomeKit discovery.
|
||||||
|
|
||||||
Return the domain to forward the discovery data to
|
Return the domain to forward the discovery data to
|
||||||
|
@@ -4,65 +4,242 @@ To update, run python3 -m script.hassfest
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
HOMEKIT = {
|
HOMEKIT = {
|
||||||
"3810X": "roku",
|
"3810X": {
|
||||||
"3820X": "roku",
|
"integration": "roku",
|
||||||
"4660X": "roku",
|
"iot_class": "local_polling",
|
||||||
"7820X": "roku",
|
},
|
||||||
"819LMB": "myq",
|
"3820X": {
|
||||||
"AC02": "tado",
|
"integration": "roku",
|
||||||
"Abode": "abode",
|
"iot_class": "local_polling",
|
||||||
"BSB002": "hue",
|
},
|
||||||
"C105X": "roku",
|
"4660X": {
|
||||||
"C135X": "roku",
|
"integration": "roku",
|
||||||
"EB-*": "ecobee",
|
"iot_class": "local_polling",
|
||||||
"Escea": "escea",
|
},
|
||||||
"HHKBridge*": "hive",
|
"7820X": {
|
||||||
"Healty Home Coach": "netatmo",
|
"integration": "roku",
|
||||||
"Iota": "abode",
|
"iot_class": "local_polling",
|
||||||
"LIFX A19": "lifx",
|
},
|
||||||
"LIFX BR30": "lifx",
|
"819LMB": {
|
||||||
"LIFX Beam": "lifx",
|
"integration": "myq",
|
||||||
"LIFX Candle": "lifx",
|
"iot_class": "cloud_polling",
|
||||||
"LIFX Clean": "lifx",
|
},
|
||||||
"LIFX Color": "lifx",
|
"AC02": {
|
||||||
"LIFX DLCOL": "lifx",
|
"integration": "tado",
|
||||||
"LIFX DLWW": "lifx",
|
"iot_class": "cloud_polling",
|
||||||
"LIFX Dlight": "lifx",
|
},
|
||||||
"LIFX Downlight": "lifx",
|
"Abode": {
|
||||||
"LIFX Filament": "lifx",
|
"integration": "abode",
|
||||||
"LIFX GU10": "lifx",
|
"iot_class": "cloud_push",
|
||||||
"LIFX Lightstrip": "lifx",
|
},
|
||||||
"LIFX Mini": "lifx",
|
"BSB002": {
|
||||||
"LIFX Nightvision": "lifx",
|
"integration": "hue",
|
||||||
"LIFX Pls": "lifx",
|
"iot_class": "local_push",
|
||||||
"LIFX Plus": "lifx",
|
},
|
||||||
"LIFX Tile": "lifx",
|
"C105X": {
|
||||||
"LIFX White": "lifx",
|
"integration": "roku",
|
||||||
"LIFX Z": "lifx",
|
"iot_class": "local_polling",
|
||||||
"MYQ": "myq",
|
},
|
||||||
"NL29": "nanoleaf",
|
"C135X": {
|
||||||
"NL42": "nanoleaf",
|
"integration": "roku",
|
||||||
"NL47": "nanoleaf",
|
"iot_class": "local_polling",
|
||||||
"NL48": "nanoleaf",
|
},
|
||||||
"NL52": "nanoleaf",
|
"EB-*": {
|
||||||
"NL59": "nanoleaf",
|
"integration": "ecobee",
|
||||||
"Netatmo Relay": "netatmo",
|
"iot_class": "cloud_polling",
|
||||||
"PowerView": "hunterdouglas_powerview",
|
},
|
||||||
"Presence": "netatmo",
|
"Escea": {
|
||||||
"Rachio": "rachio",
|
"integration": "escea",
|
||||||
"SPK5": "rainmachine",
|
"iot_class": "local_push",
|
||||||
"Sensibo": "sensibo",
|
},
|
||||||
"Smart Bridge": "lutron_caseta",
|
"HHKBridge*": {
|
||||||
"Socket": "wemo",
|
"integration": "hive",
|
||||||
"TRADFRI": "tradfri",
|
"iot_class": "cloud_polling",
|
||||||
"Touch HD": "rainmachine",
|
},
|
||||||
"Welcome": "netatmo",
|
"Healty Home Coach": {
|
||||||
"Wemo": "wemo",
|
"integration": "netatmo",
|
||||||
"YL*": "yeelight",
|
"iot_class": "cloud_polling",
|
||||||
"ecobee*": "ecobee",
|
},
|
||||||
"iSmartGate": "gogogate2",
|
"Iota": {
|
||||||
"iZone": "izone",
|
"integration": "abode",
|
||||||
"tado": "tado",
|
"iot_class": "cloud_push",
|
||||||
|
},
|
||||||
|
"LIFX A19": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX BR30": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Beam": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Candle": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Clean": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Color": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX DLCOL": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX DLWW": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Dlight": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Downlight": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Filament": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX GU10": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Lightstrip": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Mini": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Nightvision": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Pls": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Plus": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Tile": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX White": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"LIFX Z": {
|
||||||
|
"integration": "lifx",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"MYQ": {
|
||||||
|
"integration": "myq",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
},
|
||||||
|
"NL29": {
|
||||||
|
"integration": "nanoleaf",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
},
|
||||||
|
"NL42": {
|
||||||
|
"integration": "nanoleaf",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
},
|
||||||
|
"NL47": {
|
||||||
|
"integration": "nanoleaf",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
},
|
||||||
|
"NL48": {
|
||||||
|
"integration": "nanoleaf",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
},
|
||||||
|
"NL52": {
|
||||||
|
"integration": "nanoleaf",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
},
|
||||||
|
"NL59": {
|
||||||
|
"integration": "nanoleaf",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
},
|
||||||
|
"Netatmo Relay": {
|
||||||
|
"integration": "netatmo",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
},
|
||||||
|
"PowerView": {
|
||||||
|
"integration": "hunterdouglas_powerview",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"Presence": {
|
||||||
|
"integration": "netatmo",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
},
|
||||||
|
"Rachio": {
|
||||||
|
"integration": "rachio",
|
||||||
|
"iot_class": "cloud_push",
|
||||||
|
},
|
||||||
|
"SPK5": {
|
||||||
|
"integration": "rainmachine",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"Sensibo": {
|
||||||
|
"integration": "sensibo",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
},
|
||||||
|
"Smart Bridge": {
|
||||||
|
"integration": "lutron_caseta",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
},
|
||||||
|
"Socket": {
|
||||||
|
"integration": "wemo",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
},
|
||||||
|
"TRADFRI": {
|
||||||
|
"integration": "tradfri",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"Touch HD": {
|
||||||
|
"integration": "rainmachine",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"Welcome": {
|
||||||
|
"integration": "netatmo",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
},
|
||||||
|
"Wemo": {
|
||||||
|
"integration": "wemo",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
},
|
||||||
|
"YL*": {
|
||||||
|
"integration": "yeelight",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
},
|
||||||
|
"ecobee*": {
|
||||||
|
"integration": "ecobee",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
},
|
||||||
|
"iSmartGate": {
|
||||||
|
"integration": "gogogate2",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"iZone": {
|
||||||
|
"integration": "izone",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
},
|
||||||
|
"tado": {
|
||||||
|
"integration": "tado",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ZEROCONF = {
|
ZEROCONF = {
|
||||||
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from dataclasses import dataclass
|
||||||
import functools as ft
|
import functools as ft
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
@@ -118,6 +119,14 @@ class USBMatcher(USBMatcherRequired, USBMatcherOptional):
|
|||||||
"""Matcher for the bluetooth integration."""
|
"""Matcher for the bluetooth integration."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HomeKitModel:
|
||||||
|
"""HomeKit model."""
|
||||||
|
|
||||||
|
domain: str
|
||||||
|
iot_class: str | None
|
||||||
|
|
||||||
|
|
||||||
class Manifest(TypedDict, total=False):
|
class Manifest(TypedDict, total=False):
|
||||||
"""Integration manifest.
|
"""Integration manifest.
|
||||||
|
|
||||||
@@ -410,10 +419,13 @@ async def async_get_usb(hass: HomeAssistant) -> list[USBMatcher]:
|
|||||||
return usb
|
return usb
|
||||||
|
|
||||||
|
|
||||||
async def async_get_homekit(hass: HomeAssistant) -> dict[str, str]:
|
async def async_get_homekit(hass: HomeAssistant) -> dict[str, HomeKitModel]:
|
||||||
"""Return cached list of homekit models."""
|
"""Return cached list of homekit models."""
|
||||||
|
|
||||||
homekit: dict[str, str] = HOMEKIT.copy()
|
homekit: dict[str, HomeKitModel] = {
|
||||||
|
model: HomeKitModel(details["domain"], details["iot_class"])
|
||||||
|
for model, details in HOMEKIT.items()
|
||||||
|
}
|
||||||
|
|
||||||
integrations = await async_get_custom_components(hass)
|
integrations = await async_get_custom_components(hass)
|
||||||
for integration in integrations.values():
|
for integration in integrations.values():
|
||||||
@@ -424,7 +436,7 @@ async def async_get_homekit(hass: HomeAssistant) -> dict[str, str]:
|
|||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
for model in integration.homekit["models"]:
|
for model in integration.homekit["models"]:
|
||||||
homekit[model] = integration.domain
|
homekit[model] = HomeKitModel(integration.domain, integration.iot_class)
|
||||||
|
|
||||||
return homekit
|
return homekit
|
||||||
|
|
||||||
|
@@ -12,7 +12,7 @@ from .serializer import format_python_namespace
|
|||||||
def generate_and_validate(integrations: dict[str, Integration]) -> str:
|
def generate_and_validate(integrations: dict[str, Integration]) -> str:
|
||||||
"""Validate and generate zeroconf data."""
|
"""Validate and generate zeroconf data."""
|
||||||
service_type_dict = defaultdict(list)
|
service_type_dict = defaultdict(list)
|
||||||
homekit_dict: dict[str, str] = {}
|
homekit_dict: dict[str, dict[str, str]] = {}
|
||||||
|
|
||||||
for domain in sorted(integrations):
|
for domain in sorted(integrations):
|
||||||
integration = integrations[domain]
|
integration = integrations[domain]
|
||||||
@@ -42,7 +42,10 @@ def generate_and_validate(integrations: dict[str, Integration]) -> str:
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
homekit_dict[model] = domain
|
homekit_dict[model] = {
|
||||||
|
"integration": domain,
|
||||||
|
"iot_class": integration.manifest["iot_class"],
|
||||||
|
}
|
||||||
|
|
||||||
# HomeKit models are matched on starting string, make sure none overlap.
|
# HomeKit models are matched on starting string, make sure none overlap.
|
||||||
warned = set()
|
warned = set()
|
||||||
|
Reference in New Issue
Block a user