Skip to content

Load integration icons through the Home Assistant brands proxy - #949

Open
ReikanYsora wants to merge 1 commit into
hacs:mainfrom
ReikanYsora:brands-proxy-icons
Open

Load integration icons through the Home Assistant brands proxy#949
ReikanYsora wants to merge 1 commit into
hacs:mainfrom
ReikanYsora:brands-proxy-icons

Conversation

@ReikanYsora

@ReikanYsora ReikanYsora commented Sep 4, 2026

Copy link
Copy Markdown

Hi :)

Proposed change

Since Home Assistant 2026.3, brand images are served by a local, token-gated proxy at /api/brands/integration/{domain}/{image} (announcement). The proxy resolves custom_components/<domain>/brand/ first (core), which is what custom integrations rely on now that home-assistant/brands no longer accepts their images.

The dashboard still builds icon URLs against the public CDN through the pinned homeassistant-frontend (January 2025), so every integration that is not grandfathered into the CDN renders the placeholder, including integrations that are downloaded and whose icon core itself displays fine.

This adds src/tools/brands-url.ts, a port of the parts of upstream src/util/brands-url.ts that the pinned submodule predates, and wires it into the existing lifecycle:

  • The access token is requested (brands/access_token) in the same Promise.all as hacs/info and hacs/repositories/list, so the first render already has it and startup gains no extra round trip.
  • It is refreshed every 30 minutes, the cadence at which core rotates it while still honouring the previous token, and re-fetched with a short backoff when hass.connected comes back after a core restart.
  • brandsUrl() returns /api/brands/integration/{domain}/[dark_]icon.png?token=…. If no token could be obtained (core < 2026.3) it returns the exact CDN URL used today, so older cores see no change.
  • hacs-dashboard.ts only switches the import and drops useFallback, which the proxy implements itself.

No submodule bump, no new dependency, no change to the <img> markup.

Scope

This fixes the icon for every integration that is downloaded, which is the case core's proxy is built for and the one reported in hacs/integration#5171, hacs/integration#5179, hacs/integration#5223 and hacs/integration#5402. Repositories that are not downloaded go through the proxy's CDN cache and behave exactly as today: grandfathered icons keep showing, the rest keep the placeholder.

That remaining gap is the one raised on #937 and can only be closed by a HACS-side endpoint such as hacs/integration#5388 / #945. This PR is deliberately independent of that work: the token plumbing here is what any such endpoint needs as well, and brandsUrl() is the single place to redirect once it lands.

Compared with #929 and #937: no homeassistant-frontend bump, and the token is obtained and refreshed inside the HACS bundle, which is required because the upstream module's token is module-scoped and never populated from within a custom panel.

Side effects

On cores with the proxy the HACS panel no longer contacts brands.home-assistant.io from the browser, matching core's own behaviour since 2026.3.

Release note

hacs/integration pins the frontend release in scripts/install/frontend; a new frontend release and a pin bump are needed before this reaches users.

Verification

  • tsc --noEmit reports no errors in src/.
  • gulp build-hacs succeeds on Node 18 (the CI version). script/bootstrap on main currently needs Skip tsparticles deps that break yarn resolve #943 to resolve dependencies.
  • Contract checked against a live Home Assistant 2026.9.0 instance running HACS with two custom integrations that ship brand/icon.png and are not in home-assistant/brands: brands/access_token returns the token, /api/brands/integration/{domain}/icon.png?token=… serves the real icon (200, 10.6 KB and 14 KB), dark_icon.png falls back to it, and the same request without a token is rejected with 403. The CDN URL used today answers 200 with the 3 KB placeholder for both domains, so no client-side onerror fallback could ever have caught this. The URL and token contract is the one core's frontend uses for the same images (connection-mixin.ts, brands-url.ts).

Companion for the update entities: hacs/integration#5524.

Cheers ;)

ReikanYsora / Jérôme

Home Assistant 2026.3 moved brand images behind a local, token-gated
proxy at /api/brands/. That proxy also serves the brand/ assets custom
integrations now ship themselves, since home-assistant/brands stopped
accepting images for custom integrations.

The dashboard still built icon URLs against the public CDN through the
pinned homeassistant-frontend, so every integration that is not
grandfathered into the CDN rendered the placeholder image.

Add a small brands-url helper mirroring the upstream module: resolve the
brands access token over the WebSocket alongside the initial data, keep
it fresh on the same 30 minute cadence core rotates it, and pick up a
new one after a core restart. Integration icons are now requested from
the proxy with that token, and the CDN URL remains the fallback on
cores without the proxy.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant