From b213eb23c83d96df18d282168569a09ad90bae24 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:48:15 +0200 Subject: [PATCH] Reduce context switching in Tuya initialisation (#168830) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/tuya/__init__.py | 31 ++-------- homeassistant/components/tuya/coordinator.py | 63 +++++++++++++++----- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index dd9b38ec1515..0e6b195eb277 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -3,13 +3,10 @@ from __future__ import annotations import logging -from pathlib import Path -from tuya_device_handlers.devices import register_tuya_quirks from tuya_sharing import Manager from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType @@ -23,7 +20,7 @@ from .const import ( PLATFORMS, TUYA_CLIENT_ID, ) -from .coordinator import DeviceListener, TokenListener, TuyaConfigEntry, create_manager +from .coordinator import DeviceListener, TuyaConfigEntry from .services import async_setup_services CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -41,32 +38,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool: """Async setup hass config entry.""" - await hass.async_add_executor_job( - register_tuya_quirks, str(Path(hass.config.config_dir, "tuya_quirks")) - ) - - token_listener = TokenListener(hass, entry) - - # Move to executor as it makes blocking call to import_module - # with args ('.system', 'urllib3.contrib.resolver') - manager = await hass.async_add_executor_job(create_manager, entry, token_listener) - - listener = DeviceListener(hass, manager) - manager.add_device_listener(listener) - - # Get all devices from Tuya - try: - await hass.async_add_executor_job(manager.update_device_cache) - except Exception as exc: - # While in general, we should avoid catching broad exceptions, - # we have no other way of detecting this case. - if "sign invalid" in str(exc): - msg = "Authentication failed. Please re-authenticate" - raise ConfigEntryAuthFailed(msg) from exc - raise + listener = DeviceListener(hass, entry) + await hass.async_add_executor_job(listener.initialize) # Connection is successful, store the listener in runtime_data entry.runtime_data = listener + manager = listener.manager # Cleanup device registry await cleanup_device_registry(hass, manager, entry) diff --git a/homeassistant/components/tuya/coordinator.py b/homeassistant/components/tuya/coordinator.py index 75436a41ecba..bb9edb6b5b13 100644 --- a/homeassistant/components/tuya/coordinator.py +++ b/homeassistant/components/tuya/coordinator.py @@ -1,7 +1,9 @@ """Support for Tuya Smart devices.""" +from pathlib import Path from typing import Any +from tuya_device_handlers.devices import register_tuya_quirks from tuya_sharing import ( CustomerDevice, Manager, @@ -11,6 +13,7 @@ from tuya_sharing import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send @@ -29,28 +32,60 @@ from .const import ( type TuyaConfigEntry = ConfigEntry[DeviceListener] -def create_manager(entry: TuyaConfigEntry, token_listener: TokenListener) -> Manager: - """Create a Tuya Manager instance.""" - return Manager( - TUYA_CLIENT_ID, - entry.data[CONF_USER_CODE], - entry.data[CONF_TERMINAL_ID], - entry.data[CONF_ENDPOINT], - entry.data[CONF_TOKEN_INFO], - token_listener, - ) - - class DeviceListener(SharingDeviceListener): """Device Update Listener.""" + manager: Manager + def __init__( self, hass: HomeAssistant, - manager: Manager, + entry: TuyaConfigEntry, ) -> None: """Init DeviceListener.""" self.hass = hass + self._entry = entry + + def initialize(self) -> None: + """Initialize device listener. + + Needs to be called in executor as these make blocking calls: + - `register_tuya_quirks` + - `Manager` initialization + - `manager.update_device_cache` + """ + entry = self._entry + hass = self.hass + + # Makes blocking call to load files from disk + register_tuya_quirks(str(Path(hass.config.config_dir, "tuya_quirks"))) + + token_listener = _TokenListener(hass, entry) + + # Makes blocking call to import_module + # with args ('.system', 'urllib3.contrib.resolver') + manager = Manager( + TUYA_CLIENT_ID, + entry.data[CONF_USER_CODE], + entry.data[CONF_TERMINAL_ID], + entry.data[CONF_ENDPOINT], + entry.data[CONF_TOKEN_INFO], + token_listener, + ) + + manager.add_device_listener(self) + + # Get all devices from Tuya, makes blocking web calls + try: + manager.update_device_cache() + except Exception as exc: + # While in general, we should avoid catching broad exceptions, + # we have no other way of detecting this case. + if "sign invalid" in str(exc): + msg = "Authentication failed. Please re-authenticate" + raise ConfigEntryAuthFailed(msg) from exc + raise + self.manager = manager def update_device( @@ -112,7 +147,7 @@ class DeviceListener(SharingDeviceListener): device_registry.async_remove_device(device_entry.id) -class TokenListener(SharingTokenListener): +class _TokenListener(SharingTokenListener): """Token listener for upstream token updates.""" def __init__(