From a584ccb8f731377fdbe36cd5febb8f07ae8d3ead Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 24 Apr 2025 22:14:46 +0200 Subject: [PATCH] Remove add-on changelog from cached information (#143526) --- homeassistant/components/hassio/__init__.py | 1 - homeassistant/components/hassio/const.py | 5 +-- .../components/hassio/coordinator.py | 40 ++++--------------- homeassistant/components/hassio/update.py | 40 +++++++------------ tests/components/hassio/test_init.py | 30 +++++++------- tests/components/hassio/test_update.py | 39 ++++++++++++------ 6 files changed, 65 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index bc0f819fde9..3eef1c14dd0 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -104,7 +104,6 @@ from .const import ( ) from .coordinator import ( HassioDataUpdateCoordinator, - get_addons_changelogs, # noqa: F401 get_addons_info, get_addons_stats, # noqa: F401 get_core_info, # noqa: F401 diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 562669f674a..563b271c578 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -85,7 +85,6 @@ DATA_OS_INFO = "hassio_os_info" DATA_NETWORK_INFO = "hassio_network_info" DATA_SUPERVISOR_INFO = "hassio_supervisor_info" DATA_SUPERVISOR_STATS = "hassio_supervisor_stats" -DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs" DATA_ADDONS_INFO = "hassio_addons_info" DATA_ADDONS_STATS = "hassio_addons_stats" HASSIO_UPDATE_INTERVAL = timedelta(minutes=5) @@ -94,7 +93,6 @@ ATTR_AUTO_UPDATE = "auto_update" ATTR_VERSION = "version" ATTR_VERSION_LATEST = "version_latest" ATTR_CPU_PERCENT = "cpu_percent" -ATTR_CHANGELOG = "changelog" ATTR_LOCATION = "location" ATTR_MEMORY_PERCENT = "memory_percent" ATTR_SLUG = "slug" @@ -124,14 +122,13 @@ CORE_CONTAINER = "homeassistant" SUPERVISOR_CONTAINER = "hassio_supervisor" CONTAINER_STATS = "stats" -CONTAINER_CHANGELOG = "changelog" CONTAINER_INFO = "info" # This is a mapping of which endpoint the key in the addon data # is obtained from so we know which endpoint to update when the # coordinator polls for updates. KEY_TO_UPDATE_TYPES: dict[str, set[str]] = { - ATTR_VERSION_LATEST: {CONTAINER_INFO, CONTAINER_CHANGELOG}, + ATTR_VERSION_LATEST: {CONTAINER_INFO}, ATTR_MEMORY_PERCENT: {CONTAINER_STATS}, ATTR_CPU_PERCENT: {CONTAINER_STATS}, ATTR_VERSION: {CONTAINER_INFO}, diff --git a/homeassistant/components/hassio/coordinator.py b/homeassistant/components/hassio/coordinator.py index 833068a713c..25a0e1dd6b2 100644 --- a/homeassistant/components/hassio/coordinator.py +++ b/homeassistant/components/hassio/coordinator.py @@ -7,7 +7,7 @@ from collections import defaultdict import logging from typing import TYPE_CHECKING, Any -from aiohasupervisor import SupervisorError +from aiohasupervisor import SupervisorError, SupervisorNotFoundError from aiohasupervisor.models import StoreInfo from homeassistant.config_entries import ConfigEntry @@ -21,18 +21,15 @@ from homeassistant.loader import bind_hass from .const import ( ATTR_AUTO_UPDATE, - ATTR_CHANGELOG, ATTR_REPOSITORY, ATTR_SLUG, ATTR_STARTED, ATTR_STATE, ATTR_URL, ATTR_VERSION, - CONTAINER_CHANGELOG, CONTAINER_INFO, CONTAINER_STATS, CORE_CONTAINER, - DATA_ADDONS_CHANGELOGS, DATA_ADDONS_INFO, DATA_ADDONS_STATS, DATA_COMPONENT, @@ -155,16 +152,6 @@ def get_supervisor_stats(hass: HomeAssistant) -> dict[str, Any]: return hass.data.get(DATA_SUPERVISOR_STATS) or {} -@callback -@bind_hass -def get_addons_changelogs(hass: HomeAssistant): - """Return Addons changelogs. - - Async friendly. - """ - return hass.data.get(DATA_ADDONS_CHANGELOGS) - - @callback @bind_hass def get_os_info(hass: HomeAssistant) -> dict[str, Any] | None: @@ -337,7 +324,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): supervisor_info = get_supervisor_info(self.hass) or {} addons_info = get_addons_info(self.hass) or {} addons_stats = get_addons_stats(self.hass) - addons_changelogs = get_addons_changelogs(self.hass) store_data = get_store(self.hass) if store_data: @@ -355,7 +341,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): ATTR_AUTO_UPDATE: (addons_info.get(addon[ATTR_SLUG]) or {}).get( ATTR_AUTO_UPDATE, False ), - ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]), ATTR_REPOSITORY: repositories.get( addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "") ), @@ -427,6 +412,13 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): self.hass.data[DATA_SUPERVISOR_INFO] = await self.hassio.get_supervisor_info() await self.async_refresh() + async def get_changelog(self, addon_slug: str) -> str | None: + """Get the changelog for an add-on.""" + try: + return await self.supervisor_client.store.addon_changelog(addon_slug) + except SupervisorNotFoundError: + return None + async def force_data_refresh(self, first_update: bool) -> None: """Force update of the addon info.""" container_updates = self._container_updates @@ -475,13 +467,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): started_addons, False, ), - ( - DATA_ADDONS_CHANGELOGS, - self._update_addon_changelog, - CONTAINER_CHANGELOG, - all_addons, - True, - ), ( DATA_ADDONS_INFO, self._update_addon_info, @@ -513,15 +498,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): return (slug, None) return (slug, stats.to_dict()) - async def _update_addon_changelog(self, slug: str) -> tuple[str, str | None]: - """Return the changelog for an add-on.""" - try: - changelog = await self.supervisor_client.store.addon_changelog(slug) - except SupervisorError as err: - _LOGGER.warning("Could not fetch changelog for %s: %s", slug, err) - return (slug, None) - return (slug, changelog) - async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]: """Return the info for an add-on.""" try: diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index 2c325979210..bb1d3f8bd50 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from typing import Any from aiohasupervisor import SupervisorError @@ -21,7 +22,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ( ADDONS_COORDINATOR, ATTR_AUTO_UPDATE, - ATTR_CHANGELOG, ATTR_VERSION, ATTR_VERSION_LATEST, DATA_KEY_ADDONS, @@ -116,11 +116,6 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity): """Version installed and in use.""" return self._addon_data[ATTR_VERSION] - @property - def release_summary(self) -> str | None: - """Release summary for the add-on.""" - return self._strip_release_notes() - @property def entity_picture(self) -> str | None: """Return the icon of the add-on if any.""" @@ -130,27 +125,22 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity): return f"/api/hassio/addons/{self._addon_slug}/icon" return None - def _strip_release_notes(self) -> str | None: - """Strip the release notes to contain the needed sections.""" - if (notes := self._addon_data[ATTR_CHANGELOG]) is None: - return None - - if ( - f"# {self.latest_version}" in notes - and f"# {self.installed_version}" in notes - ): - # Split the release notes to only what is between the versions if we can - new_notes = notes.split(f"# {self.installed_version}")[0] - if f"# {self.latest_version}" in new_notes: - # Make sure the latest version is still there. - # This can be False if the order of the release notes are not correct - # In that case we just return the whole release notes - return new_notes - return notes - async def async_release_notes(self) -> str | None: """Return the release notes for the update.""" - return self._strip_release_notes() + if ( + changelog := await self.coordinator.get_changelog(self._addon_slug) + ) is None: + return None + + if self.latest_version is None or self.installed_version is None: + return changelog + + regex_pattern = re.compile( + rf"^#* {re.escape(self.latest_version)}\n(?:^(?!#* {re.escape(self.installed_version)}).*\n)*", + re.MULTILINE, + ) + match = regex_pattern.search(changelog) + return match.group(0) if match else changelog async def async_install( self, diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index e6699cfe68e..2ac06b46fca 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -228,7 +228,7 @@ async def test_setup_api_ping( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18 assert get_core_info(hass)["version_latest"] == "1.0.0" assert is_hassio(hass) @@ -275,7 +275,7 @@ async def test_setup_api_push_api_data( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18 assert not aioclient_mock.mock_calls[0][2]["ssl"] assert aioclient_mock.mock_calls[0][2]["port"] == 9999 assert "watchdog" not in aioclient_mock.mock_calls[0][2] @@ -296,7 +296,7 @@ async def test_setup_api_push_api_data_server_host( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18 assert not aioclient_mock.mock_calls[0][2]["ssl"] assert aioclient_mock.mock_calls[0][2]["port"] == 9999 assert not aioclient_mock.mock_calls[0][2]["watchdog"] @@ -317,7 +317,7 @@ async def test_setup_api_push_api_data_default( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18 assert not aioclient_mock.mock_calls[0][2]["ssl"] assert aioclient_mock.mock_calls[0][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[0][2]["refresh_token"] @@ -398,7 +398,7 @@ async def test_setup_api_existing_hassio_user( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18 assert not aioclient_mock.mock_calls[0][2]["ssl"] assert aioclient_mock.mock_calls[0][2]["port"] == 8123 assert aioclient_mock.mock_calls[0][2]["refresh_token"] == token.token @@ -417,7 +417,7 @@ async def test_setup_core_push_timezone( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18 assert aioclient_mock.mock_calls[1][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -440,7 +440,7 @@ async def test_setup_hassio_no_additional_data( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18 assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456" @@ -522,14 +522,14 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 24 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 22 assert aioclient_mock.mock_calls[-1][2] == "test" await hass.services.async_call("hassio", "host_shutdown", {}) await hass.services.async_call("hassio", "host_reboot", {}) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 26 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 24 await hass.services.async_call("hassio", "backup_full", {}) await hass.services.async_call( @@ -544,7 +544,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 28 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 26 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 03:48:00", "homeassistant": True, @@ -569,7 +569,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 30 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 28 assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], @@ -588,7 +588,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 31 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 29 assert aioclient_mock.mock_calls[-1][2] == { "name": "backup_name", "location": "backup_share", @@ -604,7 +604,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 32 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 30 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 03:48:00", "location": None, @@ -623,7 +623,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 34 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 32 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 11:48:00", "location": None, @@ -1069,7 +1069,7 @@ async def test_setup_hardware_integration( await hass.async_block_till_done(wait_background_tasks=True) assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index b5f6dc96bef..6ecc2b44244 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -5,7 +5,11 @@ import os from typing import Any from unittest.mock import AsyncMock, MagicMock, patch -from aiohasupervisor import SupervisorBadRequestError, SupervisorError +from aiohasupervisor import ( + SupervisorBadRequestError, + SupervisorError, + SupervisorNotFoundError, +) from aiohasupervisor.models import ( HomeAssistantUpdateOptions, OSUpdate, @@ -987,6 +991,7 @@ async def test_update_core_with_backup_and_error( async def test_release_notes_between_versions( hass: HomeAssistant, + addon_changelog: AsyncMock, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, ) -> None: @@ -994,12 +999,10 @@ async def test_release_notes_between_versions( config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) + addon_changelog.return_value = "# 2.0.1\nNew updates\n# 2.0.0\nOld updates" + with ( patch.dict(os.environ, MOCK_ENVIRON), - patch( - "homeassistant.components.hassio.coordinator.get_addons_changelogs", - return_value={"test": "# 2.0.1\nNew updates\n# 2.0.0\nOld updates"}, - ), ): result = await async_setup_component( hass, @@ -1026,6 +1029,7 @@ async def test_release_notes_between_versions( async def test_release_notes_full( hass: HomeAssistant, + addon_changelog: AsyncMock, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, ) -> None: @@ -1033,12 +1037,11 @@ async def test_release_notes_full( config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) + full_changelog = "# 2.0.0\nNew updates\n# 2.0.0\nOld updates" + addon_changelog.return_value = full_changelog + with ( patch.dict(os.environ, MOCK_ENVIRON), - patch( - "homeassistant.components.hassio.coordinator.get_addons_changelogs", - return_value={"test": "# 2.0.0\nNew updates\n# 2.0.0\nOld updates"}, - ), ): result = await async_setup_component( hass, @@ -1062,9 +1065,21 @@ async def test_release_notes_full( assert "Old updates" in result["result"] assert "New updates" in result["result"] + # Update entity without update should returns full changelog + await client.send_json( + { + "id": 2, + "type": "update/release_notes", + "entity_id": "update.test2_update", + } + ) + result = await client.receive_json() + assert result["result"] == full_changelog + async def test_not_release_notes( hass: HomeAssistant, + addon_changelog: AsyncMock, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, ) -> None: @@ -1072,12 +1087,10 @@ async def test_not_release_notes( config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) + addon_changelog.side_effect = SupervisorNotFoundError() + with ( patch.dict(os.environ, MOCK_ENVIRON), - patch( - "homeassistant.components.hassio.coordinator.get_addons_changelogs", - return_value={"test": None}, - ), ): result = await async_setup_component( hass,