From 3de8df4cdedc5e3553a4106ad43c022e377ecc12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20CREMOUX?= Date: Fri, 4 Sep 2026 22:44:45 +0200 Subject: [PATCH] Use the brands proxy for update entity pictures when available Home Assistant 2026.3 serves brand images through a local, token-gated proxy at /api/brands/, which also serves the brand/ assets custom integrations ship themselves now that home-assistant/brands no longer accepts images for them. The update entities still pointed entity_picture at the public CDN, so integrations that are not grandfathered into the CDN showed the placeholder image in the updates list. Return the proxy path when the brands integration is loaded, which is the case on every core with the proxy, and keep the CDN URL otherwise so older cores are unaffected. The frontend appends the access token to /api/brands/ paths itself, exactly as it does for core's own update entities. --- custom_components/hacs/update.py | 6 +++ ...proxy-hacs-test-org-integration-basic.json | 17 +++++++++ ...e-cdn-hacs-test-org-integration-basic.json | 17 +++++++++ tests/test_update.py | 38 +++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 tests/snapshots/api-usage/tests/test_updatetest-update-entity-picture-brands-proxy-hacs-test-org-integration-basic.json create mode 100644 tests/snapshots/api-usage/tests/test_updatetest-update-entity-picture-cdn-hacs-test-org-integration-basic.json diff --git a/custom_components/hacs/update.py b/custom_components/hacs/update.py index ec4014302db..65526b132a3 100644 --- a/custom_components/hacs/update.py +++ b/custom_components/hacs/update.py @@ -76,6 +76,12 @@ def entity_picture(self) -> str | None: ): return None + if "brands" in self.hass.config.components: + # Home Assistant 2026.3+ proxies brand images locally, which also serves + # the assets custom integrations ship in their own brand/ directory. + # The frontend adds the required access token to /api/brands/ paths. + return f"/api/brands/integration/{self.repository.data.domain}/icon.png" + return f"https://brands.home-assistant.io/_/{self.repository.data.domain}/icon.png" async def async_install(self, version: str | None, backup: bool, **kwargs: Any) -> None: diff --git a/tests/snapshots/api-usage/tests/test_updatetest-update-entity-picture-brands-proxy-hacs-test-org-integration-basic.json b/tests/snapshots/api-usage/tests/test_updatetest-update-entity-picture-brands-proxy-hacs-test-org-integration-basic.json new file mode 100644 index 00000000000..c28939ca8e9 --- /dev/null +++ b/tests/snapshots/api-usage/tests/test_updatetest-update-entity-picture-brands-proxy-hacs-test-org-integration-basic.json @@ -0,0 +1,17 @@ +{ + "tests/test_update.py::test_update_entity_picture[brands_proxy-hacs-test-org/integration-basic]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://data-v2.hacs.xyz/appdaemon/data.json": 1, + "https://data-v2.hacs.xyz/critical/data.json": 1, + "https://data-v2.hacs.xyz/integration/data.json": 1, + "https://data-v2.hacs.xyz/plugin/data.json": 1, + "https://data-v2.hacs.xyz/python_script/data.json": 1, + "https://data-v2.hacs.xyz/removed/data.json": 1, + "https://data-v2.hacs.xyz/template/data.json": 1, + "https://data-v2.hacs.xyz/theme/data.json": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/test_updatetest-update-entity-picture-cdn-hacs-test-org-integration-basic.json b/tests/snapshots/api-usage/tests/test_updatetest-update-entity-picture-cdn-hacs-test-org-integration-basic.json new file mode 100644 index 00000000000..29ede252dc1 --- /dev/null +++ b/tests/snapshots/api-usage/tests/test_updatetest-update-entity-picture-cdn-hacs-test-org-integration-basic.json @@ -0,0 +1,17 @@ +{ + "tests/test_update.py::test_update_entity_picture[cdn-hacs-test-org/integration-basic]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://data-v2.hacs.xyz/appdaemon/data.json": 1, + "https://data-v2.hacs.xyz/critical/data.json": 1, + "https://data-v2.hacs.xyz/integration/data.json": 1, + "https://data-v2.hacs.xyz/plugin/data.json": 1, + "https://data-v2.hacs.xyz/python_script/data.json": 1, + "https://data-v2.hacs.xyz/removed/data.json": 1, + "https://data-v2.hacs.xyz/template/data.json": 1, + "https://data-v2.hacs.xyz/theme/data.json": 1 + } +} \ No newline at end of file diff --git a/tests/test_update.py b/tests/test_update.py index 7896ca107d5..8261f58f342 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -10,6 +10,7 @@ import pytest from custom_components.hacs.const import DOMAIN +from custom_components.hacs.enums import HacsCategory from tests.common import ( CategoryTestData, @@ -96,3 +97,40 @@ async def test_update_entity_state( "updated_state": updated_state}), f"{category_test_data['repository']}/test_update_entity_state.json", ) + + +@pytest.mark.parametrize( + "category_test_data", + category_test_data_parametrized(categories=[HacsCategory.INTEGRATION]), +) +@pytest.mark.parametrize("brands_loaded", [False, True], ids=["cdn", "brands_proxy"]) +async def test_update_entity_picture( + hass: HomeAssistant, + setup_integration: Generator, + category_test_data: CategoryTestData, + brands_loaded: bool, +): + """Test the entity picture follows the availability of the brands proxy.""" + hacs = get_hacs(hass) + repo = hacs.repositories.get_by_full_name(category_test_data["repository"]) + + assert repo is not None + + repo.data.installed = True + repo.data.installed_version = category_test_data["version_base"] + + if brands_loaded: + hass.config.components.add("brands") + + await hass.config_entries.async_reload(hacs.configuration.config_entry.entry_id) + await hass.async_block_till_done() + + er = async_get_entity_registry(hass) + entity_id = er.async_get_entity_id("update", DOMAIN, repo.data.id) + state = hass.states.get(entity_id) + + assert state.attributes["entity_picture"] == ( + f"/api/brands/integration/{repo.data.domain}/icon.png" + if brands_loaded + else f"https://brands.home-assistant.io/_/{repo.data.domain}/icon.png" + )