diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index d3d7b86de11..e321f393ba3 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -154,24 +154,27 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): ) self.device_id = device_entry.id - async def _async_device_connect(self) -> None: - """Connect to a Shelly Block device.""" + async def _async_device_connect_task(self) -> bool: + """Connect to a Shelly device task.""" LOGGER.debug("Connecting to Shelly Device - %s", self.name) try: await self.device.initialize() update_device_fw_info(self.hass, self.device, self.entry) except DeviceConnectionError as err: - raise UpdateFailed(f"Device disconnected: {repr(err)}") from err + LOGGER.debug( + "Error connecting to Shelly device %s, error: %r", self.name, err + ) + return False except InvalidAuthError: self.entry.async_start_reauth(self.hass) - return + return False if not self.device.firmware_supported: async_create_issue_unsupported_firmware(self.hass, self.entry) - return + return False if not self._pending_platforms: - return + return True LOGGER.debug("Device %s is online, resuming setup", self.entry.title) platforms = self._pending_platforms @@ -193,6 +196,8 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): # Resume platform setup await self.hass.config_entries.async_forward_entry_setups(self.entry, platforms) + return True + async def _async_reload_entry(self) -> None: """Reload entry.""" self._debounced_reload.async_cancel() @@ -363,7 +368,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): if update_type is BlockUpdateType.ONLINE: self.entry.async_create_background_task( self.hass, - self._async_device_connect(), + self._async_device_connect_task(), "block device online", eager_start=True, ) @@ -591,7 +596,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if self.device.connected: return - await self._async_device_connect() + if not await self._async_device_connect_task(): + raise UpdateFailed("Device reconnect error") async def _async_disconnected(self) -> None: """Handle device disconnected.""" @@ -661,7 +667,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if update_type is RpcUpdateType.ONLINE: self.entry.async_create_background_task( self.hass, - self._async_device_connect(), + self._async_device_connect_task(), "rpc device online", eager_start=True, ) diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 1e581e156c5..1dc45a98c44 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -24,10 +24,11 @@ from homeassistant.components.shelly.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, STATE_ON, STATE_UNAVAILABLE -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, HomeAssistant, State from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, + DeviceRegistry, async_entries_for_config_entry, async_get as async_get_dev_reg, format_mac, @@ -40,10 +41,11 @@ from . import ( inject_rpc_device_event, mock_polling_rpc_update, mock_rest_update, + register_device, register_entity, ) -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, mock_restore_cache RELAY_BLOCK_ID = 0 LIGHT_BLOCK_ID = 2 @@ -806,3 +808,93 @@ async def test_rpc_runs_connected_events_when_initialized( # BLE script list is called during connected events assert call.script_list() in mock_rpc_device.mock_calls + + +async def test_block_sleeping_device_connection_error( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_block_device: Mock, + device_reg: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test block sleeping device connection error during initialize.""" + sleep_period = 1000 + entry = await init_integration(hass, 1, sleep_period=sleep_period, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_name_motion", "sensor_0-motion", entry + ) + mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert get_entity_state(hass, entity_id) == STATE_ON + + # Make device online event with connection error + monkeypatch.setattr( + mock_block_device, + "initialize", + AsyncMock( + side_effect=DeviceConnectionError, + ), + ) + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Error connecting to Shelly device" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_ON + + # Move time to generate sleep period update + freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Sleeping device did not update" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_UNAVAILABLE + + +async def test_rpc_sleeping_device_connection_error( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_rpc_device: Mock, + device_reg: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test RPC sleeping device connection error during initialize.""" + sleep_period = 1000 + entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry + ) + mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_rpc_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert get_entity_state(hass, entity_id) == STATE_ON + + # Make device online event with connection error + monkeypatch.setattr( + mock_rpc_device, + "initialize", + AsyncMock( + side_effect=DeviceConnectionError, + ), + ) + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Error connecting to Shelly device" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_ON + + # Move time to generate sleep period update + freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Sleeping device did not update" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_UNAVAILABLE