diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index acc676bdf9f..7b8608a7fad 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -42,7 +42,7 @@ from homeassistant.helpers.device_registry import ( ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow @@ -609,21 +609,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: DOMAIN, service, async_service_handler, schema=settings.schema ) - async def update_addon_stats(slug): - """Update single addon stats.""" - stats = await hassio.get_addon_stats(slug) - return (slug, stats) - - async def update_addon_changelog(slug): - """Return the changelog for an add-on.""" - changelog = await hassio.get_addon_changelog(slug) - return (slug, changelog) - - async def update_addon_info(slug): - """Return the info for an add-on.""" - info = await hassio.get_addon_info(slug) - return (slug, info) - async def update_info_data(now): """Update last available supervisor information.""" @@ -644,28 +629,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: hassio.get_os_info(), ) - addons = [ - addon - for addon in hass.data[DATA_SUPERVISOR_INFO].get("addons", []) - if addon[ATTR_STATE] == ATTR_STARTED - ] - stats_data = await asyncio.gather( - *[update_addon_stats(addon[ATTR_SLUG]) for addon in addons] - ) - hass.data[DATA_ADDONS_STATS] = dict(stats_data) - hass.data[DATA_ADDONS_CHANGELOGS] = dict( - await asyncio.gather( - *[update_addon_changelog(addon[ATTR_SLUG]) for addon in addons] - ) - ) - hass.data[DATA_ADDONS_INFO] = dict( - await asyncio.gather( - *[update_addon_info(addon[ATTR_SLUG]) for addon in addons] - ) - ) - - if ADDONS_COORDINATOR in hass.data: - await hass.data[ADDONS_COORDINATOR].async_refresh() except HassioAPIError as err: _LOGGER.warning("Can't read Supervisor data: %s", err) @@ -855,7 +818,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, name=DOMAIN, - update_method=self._async_update_data, + update_interval=HASSIO_UPDATE_INTERVAL, ) self.hassio: HassIO = hass.data[DOMAIN] self.data = {} @@ -865,6 +828,11 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" + try: + await self.force_data_refresh() + except HassioAPIError as err: + raise UpdateFailed(f"Error on Supervisor API: {err}") from err + new_data = {} supervisor_info = get_supervisor_info(self.hass) addons_info = get_addons_info(self.hass) @@ -940,3 +908,53 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): """Force update of the supervisor info.""" self.hass.data[DATA_SUPERVISOR_INFO] = await self.hassio.get_supervisor_info() await self.async_refresh() + + async def force_data_refresh(self) -> None: + """Force update of the addon info.""" + await self.hassio.refresh_updates() + ( + self.hass.data[DATA_INFO], + self.hass.data[DATA_CORE_INFO], + self.hass.data[DATA_SUPERVISOR_INFO], + self.hass.data[DATA_OS_INFO], + ) = await asyncio.gather( + self.hassio.get_info(), + self.hassio.get_core_info(), + self.hassio.get_supervisor_info(), + self.hassio.get_os_info(), + ) + + addons = [ + addon + for addon in self.hass.data[DATA_SUPERVISOR_INFO].get("addons", []) + if addon[ATTR_STATE] == ATTR_STARTED + ] + stats_data = await asyncio.gather( + *[self._update_addon_stats(addon[ATTR_SLUG]) for addon in addons] + ) + self.hass.data[DATA_ADDONS_STATS] = dict(stats_data) + self.hass.data[DATA_ADDONS_CHANGELOGS] = dict( + await asyncio.gather( + *[self._update_addon_changelog(addon[ATTR_SLUG]) for addon in addons] + ) + ) + self.hass.data[DATA_ADDONS_INFO] = dict( + await asyncio.gather( + *[self._update_addon_info(addon[ATTR_SLUG]) for addon in addons] + ) + ) + + async def _update_addon_stats(self, slug): + """Update single addon stats.""" + stats = await self.hassio.get_addon_stats(slug) + return (slug, stats) + + async def _update_addon_changelog(self, slug): + """Return the changelog for an add-on.""" + changelog = await self.hassio.get_addon_changelog(slug) + return (slug, changelog) + + async def _update_addon_info(self, slug): + """Return the info for an add-on.""" + info = await self.hassio.get_addon_info(slug) + return (slug, info) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 66395b49400..4146753b753 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -168,6 +168,14 @@ class HassIO: """ return self.send_command("/homeassistant/stop") + @_api_bool + def refresh_updates(self): + """Refresh available updates. + + This method return a coroutine. + """ + return self.send_command("/refresh_updates", timeout=None) + @api_data def retrieve_discovery_messages(self): """Return all discovery data from Hass.io API. diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index ef114b90771..a49b27ba4e7 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -127,6 +127,7 @@ def mock_all(aioclient_mock, request): aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) + aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 1a50d9c3036..527c98b615e 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -9,6 +9,7 @@ from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import frontend from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.hassio import ADDONS_COORDINATOR, DOMAIN, STORAGE_KEY +from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers.device_registry import async_get from homeassistant.setup import async_setup_component @@ -151,6 +152,7 @@ def mock_all(aioclient_mock, request): aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) + aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) async def test_setup_api_ping(hass, aioclient_mock): @@ -159,7 +161,7 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -198,7 +200,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -214,7 +216,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -226,7 +228,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -293,7 +295,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -307,7 +309,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -324,7 +326,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" @@ -630,3 +632,27 @@ async def test_device_registry_calls(hass): async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3)) await hass.async_block_till_done() assert len(dev_reg.devices) == 5 + + +async def test_coordinator_updates(hass, caplog): + """Test coordinator.""" + with patch.dict(os.environ, MOCK_ENVIRON), patch( + "homeassistant.components.hassio.HassIO.refresh_updates" + ) as refresh_updates_mock: + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert refresh_updates_mock.call_count == 1 + + with patch( + "homeassistant.components.hassio.HassIO.refresh_updates", + side_effect=HassioAPIError("Unknown"), + ) as refresh_updates_mock: + async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=5)) + await hass.async_block_till_done() + assert refresh_updates_mock.call_count == 1 + assert ( + "Error fetching hassio data: Error on Supervisor API: Unknown" + in caplog.text + ) diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index e6a35c4744d..9dc620ba94f 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -120,6 +120,7 @@ def mock_all(aioclient_mock, request): aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) + aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index f9b7c201ba3..e682562297d 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -133,6 +133,7 @@ def mock_all(aioclient_mock, request): aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) + aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) @pytest.mark.parametrize(