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:
J. Nick Koston
2023-02-15 14:46:45 -06:00
parent a1efd59760
commit 01144578c0
4 changed files with 296 additions and 85 deletions

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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()