diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index afe6f1abba8..75bf923c66a 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError + from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.const import EVENT_HOMEASSISTANT_STOP, UnitOfTemperature from homeassistant.core import Event, HomeAssistant @@ -57,7 +59,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzboxConfigEntry) -> async def async_unload_entry(hass: HomeAssistant, entry: FritzboxConfigEntry) -> bool: """Unloading the AVM FRITZ!SmartHome platforms.""" - await hass.async_add_executor_job(entry.runtime_data.fritz.logout) + try: + await hass.async_add_executor_job(entry.runtime_data.fritz.logout) + except (RequestConnectionError, HTTPError) as ex: + LOGGER.debug("logout failed with '%s', anyway continue with unload", ex) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index 926a9873ad5..fcbea2d0265 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -121,26 +121,11 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat def _update_fritz_devices(self) -> FritzboxCoordinatorData: """Update all fritzbox device data.""" - try: - self.fritz.update_devices(ignore_removed=False) - if self.has_templates: - self.fritz.update_templates(ignore_removed=False) - if self.has_triggers: - self.fritz.update_triggers(ignore_removed=False) - - except RequestConnectionError as ex: - raise UpdateFailed from ex - except HTTPError: - # If the device rebooted, login again - try: - self.fritz.login() - except LoginError as ex: - raise ConfigEntryAuthFailed from ex - self.fritz.update_devices(ignore_removed=False) - if self.has_templates: - self.fritz.update_templates(ignore_removed=False) - if self.has_triggers: - self.fritz.update_triggers(ignore_removed=False) + self.fritz.update_devices(ignore_removed=False) + if self.has_templates: + self.fritz.update_templates(ignore_removed=False) + if self.has_triggers: + self.fritz.update_triggers(ignore_removed=False) devices = self.fritz.get_devices() device_data = {} @@ -193,7 +178,18 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat async def _async_update_data(self) -> FritzboxCoordinatorData: """Fetch all device data.""" - new_data = await self.hass.async_add_executor_job(self._update_fritz_devices) + try: + new_data = await self.hass.async_add_executor_job( + self._update_fritz_devices + ) + except (RequestConnectionError, HTTPError) as ex: + LOGGER.debug( + "Reload %s due to error '%s' to ensure proper re-login", + self.config_entry.title, + ex, + ) + self.hass.config_entries.async_schedule_reload(self.config_entry.entry_id) + raise UpdateFailed from ex for device in new_data.devices.values(): # create device registry entry for new main devices diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 09b69b16e79..f2a4bbc0680 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -155,21 +155,21 @@ async def test_automatic_offset(hass: HomeAssistant, fritz: Mock) -> None: async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: """Test update with error.""" device = FritzDeviceClimateMock() - fritz().update_devices.side_effect = HTTPError("Boom") + fritz().update_devices.side_effect = ["", HTTPError("Boom"), ""] entry = await setup_config_entry( hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) - assert entry.state is ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.LOADED - assert fritz().update_devices.call_count == 2 - assert fritz().login.call_count == 2 + assert fritz().update_devices.call_count == 1 + assert fritz().login.call_count == 1 - next_update = dt_util.utcnow() + timedelta(seconds=200) + next_update = dt_util.utcnow() + timedelta(seconds=35) async_fire_time_changed(hass, next_update) await hass.async_block_till_done(wait_background_tasks=True) - assert fritz().update_devices.call_count == 4 - assert fritz().login.call_count == 4 + assert fritz().update_devices.call_count == 3 + assert fritz().login.call_count == 2 @pytest.mark.parametrize( diff --git a/tests/components/fritzbox/test_coordinator.py b/tests/components/fritzbox/test_coordinator.py index 37ac5f40440..5f9bec774a5 100644 --- a/tests/components/fritzbox/test_coordinator.py +++ b/tests/components/fritzbox/test_coordinator.py @@ -40,14 +40,19 @@ async def test_coordinator_update_after_reboot( unique_id="any", ) entry.add_to_hass(hass) - fritz().update_devices.side_effect = [HTTPError(), ""] + fritz().update_devices.side_effect = ["", HTTPError()] assert await hass.config_entries.async_setup(entry.entry_id) - assert fritz().update_devices.call_count == 2 + assert fritz().update_devices.call_count == 1 assert fritz().update_templates.call_count == 1 assert fritz().get_devices.call_count == 1 assert fritz().get_templates.call_count == 1 - assert fritz().login.call_count == 2 + assert fritz().login.call_count == 1 + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=35)) + await hass.async_block_till_done(wait_background_tasks=True) + + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_coordinator_update_after_password_change( @@ -60,14 +65,10 @@ async def test_coordinator_update_after_password_change( unique_id="any", ) entry.add_to_hass(hass) - fritz().update_devices.side_effect = HTTPError() - fritz().login.side_effect = ["", LoginError("some_user")] + fritz().login.side_effect = [LoginError("some_user")] assert not await hass.config_entries.async_setup(entry.entry_id) - assert fritz().update_devices.call_count == 1 - assert fritz().get_devices.call_count == 0 - assert fritz().get_templates.call_count == 0 - assert fritz().login.call_count == 2 + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_coordinator_update_when_unreachable( @@ -80,9 +81,10 @@ async def test_coordinator_update_when_unreachable( unique_id="any", ) entry.add_to_hass(hass) - fritz().update_devices.side_effect = [ConnectionError(), ""] + fritz().update_devices.side_effect = [ConnectionError()] assert not await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/fritzbox/test_light.py b/tests/components/fritzbox/test_light.py index db4fa4f0ae1..334773efac6 100644 --- a/tests/components/fritzbox/test_light.py +++ b/tests/components/fritzbox/test_light.py @@ -248,20 +248,21 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: device.get_colors.return_value = { "Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")] } - fritz().update_devices.side_effect = HTTPError("Boom") + fritz().update_devices.side_effect = ["", HTTPError("Boom"), ""] entry = await setup_config_entry( - hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], device=device, fritz=fritz ) - assert entry.state is ConfigEntryState.SETUP_RETRY - assert fritz().update_devices.call_count == 2 - assert fritz().login.call_count == 2 + assert entry.state is ConfigEntryState.LOADED - next_update = dt_util.utcnow() + timedelta(seconds=200) + assert fritz().update_devices.call_count == 1 + assert fritz().login.call_count == 1 + + next_update = dt_util.utcnow() + timedelta(seconds=35) async_fire_time_changed(hass, next_update) await hass.async_block_till_done(wait_background_tasks=True) - assert fritz().update_devices.call_count == 4 - assert fritz().login.call_count == 4 + assert fritz().update_devices.call_count == 3 + assert fritz().login.call_count == 2 async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index fe966a7643c..067d733118c 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -80,20 +80,21 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: """Test update with error.""" device = FritzDeviceSensorMock() - fritz().update_devices.side_effect = HTTPError("Boom") + fritz().update_devices.side_effect = ["", HTTPError("Boom"), ""] entry = await setup_config_entry( - hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], device=device, fritz=fritz ) - assert entry.state is ConfigEntryState.SETUP_RETRY - assert fritz().update_devices.call_count == 2 - assert fritz().login.call_count == 2 + assert entry.state is ConfigEntryState.LOADED - next_update = dt_util.utcnow() + timedelta(seconds=200) + assert fritz().update_devices.call_count == 1 + assert fritz().login.call_count == 1 + + next_update = dt_util.utcnow() + timedelta(seconds=35) async_fire_time_changed(hass, next_update) await hass.async_block_till_done(wait_background_tasks=True) - assert fritz().update_devices.call_count == 4 - assert fritz().login.call_count == 4 + assert fritz().update_devices.call_count == 3 + assert fritz().login.call_count == 2 async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index ec2ea48f521..d8e343caa05 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -136,20 +136,21 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: """Test update with error.""" device = FritzDeviceSwitchMock() - fritz().update_devices.side_effect = HTTPError("Boom") + fritz().update_devices.side_effect = ["", HTTPError("Boom"), ""] entry = await setup_config_entry( hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], device=device, fritz=fritz ) - assert entry.state is ConfigEntryState.SETUP_RETRY - assert fritz().update_devices.call_count == 2 - assert fritz().login.call_count == 2 + assert entry.state is ConfigEntryState.LOADED - next_update = dt_util.utcnow() + timedelta(seconds=200) + assert fritz().update_devices.call_count == 1 + assert fritz().login.call_count == 1 + + next_update = dt_util.utcnow() + timedelta(seconds=35) async_fire_time_changed(hass, next_update) await hass.async_block_till_done(wait_background_tasks=True) - assert fritz().update_devices.call_count == 4 - assert fritz().login.call_count == 4 + assert fritz().update_devices.call_count == 3 + assert fritz().login.call_count == 2 async def test_assume_device_unavailable(hass: HomeAssistant, fritz: Mock) -> None: