Allow ZHA startup to fail instead of raising ConfigEntryNotReady (#77417)

* Retry startup within ZHA instead of raising `ConfigEntryNotReady`

* Add unit tests

* Disable pylint warning for intentional broad except
This commit is contained in:
puddly 2022-08-29 11:42:01 -04:00 committed by GitHub
parent 795691038f
commit 2e8d598795
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 14 deletions

View File

@ -408,3 +408,7 @@ class Strobe(t.enum8):
No_Strobe = 0x00 No_Strobe = 0x00
Strobe = 0x01 Strobe = 0x01
STARTUP_FAILURE_DELAY_S = 3
STARTUP_RETRIES = 3

View File

@ -13,7 +13,6 @@ import time
import traceback import traceback
from typing import TYPE_CHECKING, Any, NamedTuple, Union from typing import TYPE_CHECKING, Any, NamedTuple, Union
from serial import SerialException
from zigpy.application import ControllerApplication from zigpy.application import ControllerApplication
from zigpy.config import CONF_DEVICE from zigpy.config import CONF_DEVICE
import zigpy.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.components.system_log import LogEntry, _figure_out_source
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback 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 import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
@ -62,6 +60,8 @@ from .const import (
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_GROUP_MEMBERSHIP_CHANGE, SIGNAL_GROUP_MEMBERSHIP_CHANGE,
SIGNAL_REMOVE, SIGNAL_REMOVE,
STARTUP_FAILURE_DELAY_S,
STARTUP_RETRIES,
UNKNOWN_MANUFACTURER, UNKNOWN_MANUFACTURER,
UNKNOWN_MODEL, UNKNOWN_MODEL,
ZHA_GW_MSG, ZHA_GW_MSG,
@ -166,17 +166,27 @@ class ZHAGateway:
app_config[CONF_DEVICE] = self.config_entry.data[CONF_DEVICE] app_config[CONF_DEVICE] = self.config_entry.data[CONF_DEVICE]
app_config = app_controller_cls.SCHEMA(app_config) app_config = app_controller_cls.SCHEMA(app_config)
try:
self.application_controller = await app_controller_cls.new( for attempt in range(STARTUP_RETRIES):
app_config, auto_form=True, start_radio=True try:
) self.application_controller = await app_controller_cls.new(
except (asyncio.TimeoutError, SerialException, OSError) as exception: app_config, auto_form=True, start_radio=True
_LOGGER.error( )
"Couldn't start %s coordinator", except Exception as exc: # pylint: disable=broad-except
self.radio_description, _LOGGER.warning(
exc_info=exception, "Couldn't start %s coordinator (attempt %s of %s)",
) self.radio_description,
raise ConfigEntryNotReady from exception 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.add_listener(self)
self.application_controller.groups.add_listener(self) self.application_controller.groups.add_listener(self)

View File

@ -1,6 +1,6 @@
"""Test ZHA Gateway.""" """Test ZHA Gateway."""
import asyncio import asyncio
from unittest.mock import patch from unittest.mock import AsyncMock, MagicMock, patch
import pytest import pytest
import zigpy.profiles.zha as zha 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 len(zha_group.members) == 1
assert zha_group.members[0].device is device_light_1 assert zha_group.members[0].device is device_light_1
assert zha_group.group_id == 0x1234 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