diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index dfa5f608cfe..4a48c254b40 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -408,3 +408,7 @@ class Strobe(t.enum8): No_Strobe = 0x00 Strobe = 0x01 + + +STARTUP_FAILURE_DELAY_S = 3 +STARTUP_RETRIES = 3 diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 14fbf2cf701..5261396c794 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -13,7 +13,6 @@ import time import traceback from typing import TYPE_CHECKING, Any, NamedTuple, Union -from serial import SerialException from zigpy.application import ControllerApplication from zigpy.config import CONF_DEVICE import zigpy.device @@ -25,7 +24,6 @@ from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.system_log import LogEntry, _figure_out_source from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo @@ -62,6 +60,8 @@ from .const import ( SIGNAL_ADD_ENTITIES, SIGNAL_GROUP_MEMBERSHIP_CHANGE, SIGNAL_REMOVE, + STARTUP_FAILURE_DELAY_S, + STARTUP_RETRIES, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZHA_GW_MSG, @@ -166,17 +166,27 @@ class ZHAGateway: app_config[CONF_DEVICE] = self.config_entry.data[CONF_DEVICE] app_config = app_controller_cls.SCHEMA(app_config) - try: - self.application_controller = await app_controller_cls.new( - app_config, auto_form=True, start_radio=True - ) - except (asyncio.TimeoutError, SerialException, OSError) as exception: - _LOGGER.error( - "Couldn't start %s coordinator", - self.radio_description, - exc_info=exception, - ) - raise ConfigEntryNotReady from exception + + for attempt in range(STARTUP_RETRIES): + try: + self.application_controller = await app_controller_cls.new( + app_config, auto_form=True, start_radio=True + ) + except Exception as exc: # pylint: disable=broad-except + _LOGGER.warning( + "Couldn't start %s coordinator (attempt %s of %s)", + self.radio_description, + attempt + 1, + STARTUP_RETRIES, + exc_info=exc, + ) + + if attempt == STARTUP_RETRIES - 1: + raise exc + + await asyncio.sleep(STARTUP_FAILURE_DELAY_S) + else: + break self.application_controller.add_listener(self) self.application_controller.groups.add_listener(self) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 3c8c3e78c0e..bc49b04d86a 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,6 +1,6 @@ """Test ZHA Gateway.""" import asyncio -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest import zigpy.profiles.zha as zha @@ -211,3 +211,51 @@ async def test_gateway_create_group_with_id(hass, device_light_1, coordinator): assert len(zha_group.members) == 1 assert zha_group.members[0].device is device_light_1 assert zha_group.group_id == 0x1234 + + +@patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_devices", + MagicMock(), +) +@patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_groups", + MagicMock(), +) +@patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) +@pytest.mark.parametrize( + "startup", + [ + [asyncio.TimeoutError(), FileNotFoundError(), MagicMock()], + [asyncio.TimeoutError(), MagicMock()], + [MagicMock()], + ], +) +async def test_gateway_initialize_success(startup, hass, device_light_1, coordinator): + """Test ZHA initializing the gateway successfully.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + + zha_gateway.shutdown = AsyncMock() + + with patch( + "bellows.zigbee.application.ControllerApplication.new", side_effect=startup + ) as mock_new: + await zha_gateway.async_initialize() + + assert mock_new.call_count == len(startup) + + +@patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) +async def test_gateway_initialize_failure(hass, device_light_1, coordinator): + """Test ZHA failing to initialize the gateway.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + + with patch( + "bellows.zigbee.application.ControllerApplication.new", + side_effect=[asyncio.TimeoutError(), FileNotFoundError(), RuntimeError()], + ) as mock_new: + with pytest.raises(RuntimeError): + await zha_gateway.async_initialize() + + assert mock_new.call_count == 3