diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 8a773213a58..4c10a2328a2 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -139,6 +139,7 @@ CONF_ENABLE_QUIRKS = "enable_quirks" CONF_FLOWCONTROL = "flow_control" CONF_RADIO_TYPE = "radio_type" CONF_USB_PATH = "usb_path" +CONF_USE_THREAD = "use_thread" CONF_ZIGPY = "zigpy_config" CONF_CONSIDER_UNAVAILABLE_MAINS = "consider_unavailable_mains" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 128e3b145f1..2f1b22e0ea2 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -40,7 +40,9 @@ from .const import ( ATTR_SIGNATURE, ATTR_TYPE, CONF_DATABASE, + CONF_DEVICE_PATH, CONF_RADIO_TYPE, + CONF_USE_THREAD, CONF_ZIGPY, DATA_ZHA, DATA_ZHA_BRIDGE_ID, @@ -167,6 +169,15 @@ class ZHAGateway: app_config[CONF_DATABASE] = database app_config[CONF_DEVICE] = self.config_entry.data[CONF_DEVICE] + # The bellows UART thread sometimes propagates a cancellation into the main Core + # event loop, when a connection to a TCP coordinator fails in a specific way + if ( + CONF_USE_THREAD not in app_config + and RadioType[radio_type] is RadioType.ezsp + and app_config[CONF_DEVICE][CONF_DEVICE_PATH].startswith("socket://") + ): + app_config[CONF_USE_THREAD] = False + app_config = app_controller_cls.SCHEMA(app_config) for attempt in range(STARTUP_RETRIES): diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index b96acb29b10..adff43d377b 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -287,3 +287,39 @@ async def test_gateway_initialize_failure_transient( # Initialization immediately stops and is retried after TransientConnectionError assert mock_new.call_count == 2 + + +@patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_devices", + MagicMock(), +) +@patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_groups", + MagicMock(), +) +@pytest.mark.parametrize( + ("device_path", "thread_state", "config_override"), + [ + ("/dev/ttyUSB0", True, {}), + ("socket://192.168.1.123:9999", False, {}), + ("socket://192.168.1.123:9999", True, {"use_thread": True}), + ], +) +async def test_gateway_initialize_bellows_thread( + device_path, thread_state, config_override, hass, coordinator +): + """Test ZHA disabling the UART thread when connecting to a TCP coordinator.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + + zha_gateway.config_entry.data = dict(zha_gateway.config_entry.data) + zha_gateway.config_entry.data["device"]["path"] = device_path + zha_gateway._config.setdefault("zigpy_config", {}).update(config_override) + + with patch( + "bellows.zigbee.application.ControllerApplication.new", + new=AsyncMock(), + ) as mock_new: + await zha_gateway.async_initialize() + + assert mock_new.mock_calls[0].args[0]["use_thread"] is thread_state