
diff --git a/src/panels/config/automation/add-automation-element/ha-automation-add-from-target.ts b/src/panels/config/automation/add-automation-element/ha-automation-add-from-target.ts
index e2cf97b4a2..b7f6cf1c12 100644
--- a/src/panels/config/automation/add-automation-element/ha-automation-add-from-target.ts
+++ b/src/panels/config/automation/add-automation-element/ha-automation-add-from-target.ts
@@ -769,11 +769,14 @@ export default class HaAutomationAddFromTarget extends LitElement {
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
- src=${brandsUrl({
- domain,
- type: "icon",
- darkOptimized: this.hass.themes?.darkMode,
- })}
+ src=${brandsUrl(
+ {
+ domain,
+ type: "icon",
+ darkOptimized: this.hass.themes?.darkMode,
+ },
+ this.hass.auth.data.hassUrl
+ )}
/>
`;
diff --git a/src/panels/config/backup/components/config/ha-backup-config-agents.ts b/src/panels/config/backup/components/config/ha-backup-config-agents.ts
index 8e44f4695f..e587321abe 100644
--- a/src/panels/config/backup/components/config/ha-backup-config-agents.ts
+++ b/src/panels/config/backup/components/config/ha-backup-config-agents.ts
@@ -149,11 +149,14 @@ class HaBackupConfigAgents extends LitElement {
return html`
![]()
`
: html`
![${name}]()
`}
>
diff --git a/src/panels/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts
index 7562c51c2d..8d37d9b871 100644
--- a/src/panels/config/hardware/ha-config-hardware.ts
+++ b/src/panels/config/hardware/ha-config-hardware.ts
@@ -229,12 +229,15 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
boardId = boardData.board!.hassio_board_id;
boardName = boardData.name;
documentationURL = boardData.url;
- imageURL = hardwareBrandsUrl({
- category: "boards",
- manufacturer: boardData.board!.manufacturer,
- model: boardData.board!.model,
- darkOptimized: this.hass.themes?.darkMode,
- });
+ imageURL = hardwareBrandsUrl(
+ {
+ category: "boards",
+ manufacturer: boardData.board!.manufacturer,
+ model: boardData.board!.model,
+ darkOptimized: this.hass.themes?.darkMode,
+ },
+ this.hass.auth.data.hassUrl
+ );
} else if (this._OSData?.board) {
boardId = this._OSData.board;
boardName = BOARD_NAMES[this._OSData.board];
diff --git a/src/panels/config/helpers/dialog-helper-detail.ts b/src/panels/config/helpers/dialog-helper-detail.ts
index 3b3e2b2525..85bec0e9b4 100644
--- a/src/panels/config/helpers/dialog-helper-detail.ts
+++ b/src/panels/config/helpers/dialog-helper-detail.ts
@@ -252,11 +252,14 @@ export class DialogHelperDetail extends LitElement {
slot="graphic"
loading="lazy"
alt=""
- src=${brandsUrl({
- domain,
- type: "icon",
- darkOptimized: this.hass.themes?.darkMode,
- })}
+ src=${brandsUrl(
+ {
+ domain,
+ type: "icon",
+ darkOptimized: this.hass.themes?.darkMode,
+ },
+ this.hass.auth.data.hassUrl
+ )}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
diff --git a/src/panels/config/integrations/ha-config-integration-page.ts b/src/panels/config/integrations/ha-config-integration-page.ts
index 6ff3aedf7a..a8bc30620e 100644
--- a/src/panels/config/integrations/ha-config-integration-page.ts
+++ b/src/panels/config/integrations/ha-config-integration-page.ts
@@ -376,11 +376,14 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {

@@ -112,11 +115,14 @@ class HaDomainIntegrations extends LitElement {
slot="graphic"
loading="lazy"
alt=""
- src=${brandsUrl({
- domain,
- type: "icon",
- darkOptimized: this.hass.themes?.darkMode,
- })}
+ src=${brandsUrl(
+ {
+ domain,
+ type: "icon",
+ darkOptimized: this.hass.themes?.darkMode,
+ },
+ this.hass.auth.data.hassUrl
+ )}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
@@ -175,11 +181,14 @@ class HaDomainIntegrations extends LitElement {
slot="graphic"
loading="lazy"
alt=""
- src=${brandsUrl({
- domain: this.domain,
- type: "icon",
- darkOptimized: this.hass.themes?.darkMode,
- })}
+ src=${brandsUrl(
+ {
+ domain: this.domain,
+ type: "icon",
+ darkOptimized: this.hass.themes?.darkMode,
+ },
+ this.hass.auth.data.hassUrl
+ )}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
diff --git a/src/panels/config/integrations/ha-integration-action-card.ts b/src/panels/config/integrations/ha-integration-action-card.ts
index ae7302ad17..81430bcb02 100644
--- a/src/panels/config/integrations/ha-integration-action-card.ts
+++ b/src/panels/config/integrations/ha-integration-action-card.ts
@@ -31,11 +31,14 @@ export class HaIntegrationActionCard extends LitElement {

`}
diff --git a/src/panels/config/integrations/integration-panels/matter/dialog-matter-open-commissioning-window.ts b/src/panels/config/integrations/integration-panels/matter/dialog-matter-open-commissioning-window.ts
index a416a3e13e..bc92c6c1de 100644
--- a/src/panels/config/integrations/integration-panels/matter/dialog-matter-open-commissioning-window.ts
+++ b/src/panels/config/integrations/integration-panels/matter/dialog-matter-open-commissioning-window.ts
@@ -67,11 +67,14 @@ class DialogMatterOpenCommissioningWindow extends LitElement {
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt=${domainToName(this.hass.localize, "matter")}
- src=${brandsUrl({
- domain: "matter",
- type: "logo",
- darkOptimized: this.hass.themes?.darkMode,
- })}
+ src=${brandsUrl(
+ {
+ domain: "matter",
+ type: "logo",
+ darkOptimized: this.hass.themes?.darkMode,
+ },
+ this.hass.auth.data.hassUrl
+ )}
/>
diff --git a/src/panels/config/repairs/ha-config-repairs.ts b/src/panels/config/repairs/ha-config-repairs.ts
index 778ec31955..c856368974 100644
--- a/src/panels/config/repairs/ha-config-repairs.ts
+++ b/src/panels/config/repairs/ha-config-repairs.ts
@@ -74,11 +74,14 @@ class HaConfigRepairs extends LitElement {
slot="start"
alt=${domainName}
loading="lazy"
- src=${brandsUrl({
- domain: issue.issue_domain || issue.domain,
- type: "icon",
- darkOptimized: this.hass.themes?.darkMode,
- })}
+ src=${brandsUrl(
+ {
+ domain: issue.issue_domain || issue.domain,
+ type: "icon",
+ darkOptimized: this.hass.themes?.darkMode,
+ },
+ this.hass.auth.data.hassUrl
+ )}
.title=${domainName}
crossorigin="anonymous"
referrerpolicy="no-referrer"
diff --git a/src/panels/config/repairs/integrations-startup-time.ts b/src/panels/config/repairs/integrations-startup-time.ts
index f737804bef..6946b74096 100644
--- a/src/panels/config/repairs/integrations-startup-time.ts
+++ b/src/panels/config/repairs/integrations-startup-time.ts
@@ -55,11 +55,14 @@ class IntegrationsStartupTime extends LitElement {
{
}
};
-export const brandsUrl = (options: BrandsOptions): string => {
+export const brandsUrl = (options: BrandsOptions, hassUrl?: string): string => {
+ hassUrl = hassUrl ?? location.origin;
const base = `/api/brands/integration/${options.domain}/${
options.darkOptimized ? "dark_" : ""
}${options.type}.png`;
+
+ const url = new URL(base, hassUrl);
if (_brandsAccessToken) {
- return `${base}?token=${_brandsAccessToken}`;
+ url.searchParams.set("token", _brandsAccessToken);
}
- return base;
+ return url.toString();
};
-export const hardwareBrandsUrl = (options: HardwareBrandsOptions): string => {
+export const hardwareBrandsUrl = (
+ options: HardwareBrandsOptions,
+ hassUrl?: string
+): string => {
+ hassUrl = hassUrl ?? location.origin;
const base = `/api/brands/hardware/${options.category}/${
options.darkOptimized ? "dark_" : ""
}${options.manufacturer}${options.model ? `_${options.model}` : ""}.png`;
+
+ const url = new URL(base, hassUrl);
if (_brandsAccessToken) {
- return `${base}?token=${_brandsAccessToken}`;
+ url.searchParams.set("token", _brandsAccessToken);
}
- return base;
+ return url.toString();
};
-export const addBrandsAuth = (url: string): string => {
- if (!_brandsAccessToken || !url.startsWith("/api/brands/")) {
+export const addBrandsAuth = (url: string, hassUrl?: string): string => {
+ hassUrl = hassUrl ?? location.origin;
+ if (!_brandsAccessToken) {
+ return url;
+ }
+
+ try {
+ const parsedUrl = new URL(url, hassUrl);
+ if (!parsedUrl.pathname.startsWith("/api/brands/")) {
+ return url;
+ }
+ parsedUrl.searchParams.set("token", _brandsAccessToken);
+ return parsedUrl.toString();
+ } catch {
return url;
}
- const fullUrl = new URL(url, location.origin);
- fullUrl.searchParams.set("token", _brandsAccessToken);
- return `${fullUrl.pathname}${fullUrl.search}`;
};
export const extractDomainFromBrandUrl = (url: string): string => {
// Handle both new local API paths (/api/brands/integration/{domain}/...)
// and legacy CDN URLs (https://brands.home-assistant.io/_/{domain}/...)
- if (url.startsWith("/api/brands/")) {
+ const parsed = new URL(url, location.origin);
+ if (parsed.pathname.startsWith("/api/brands/")) {
// /api/brands/integration/{domain}/... -> ["" ,"api", "brands", "integration", "{domain}", ...]
- return url.split("/")[4];
+ return parsed.pathname.split("/")[4];
}
// https://brands.home-assistant.io/_/{domain}/... -> ["", "_", "{domain}", ...]
- const parsed = new URL(url);
const segments = parsed.pathname.split("/").filter((s) => s.length > 0);
const underscoreIdx = segments.indexOf("_");
if (underscoreIdx !== -1 && underscoreIdx + 1 < segments.length) {
@@ -101,6 +119,14 @@ export const extractDomainFromBrandUrl = (url: string): string => {
return segments[1] ?? "";
};
-export const isBrandUrl = (thumbnail: string | ""): boolean =>
- thumbnail.startsWith("/api/brands/") ||
- thumbnail.startsWith("https://brands.home-assistant.io/");
+export const isBrandUrl = (thumbnail: string | ""): boolean => {
+ try {
+ const url = new URL(thumbnail, location.origin);
+ return (
+ url.pathname.startsWith("/api/brands/") ||
+ thumbnail.startsWith("https://brands.home-assistant.io/")
+ );
+ } catch {
+ return false;
+ }
+};
diff --git a/test/util/generate-brands-url.test.ts b/test/util/generate-brands-url.test.ts
index 527bded700..9aa89d8d5b 100644
--- a/test/util/generate-brands-url.test.ts
+++ b/test/util/generate-brands-url.test.ts
@@ -11,21 +11,30 @@ import {
describe("Generate brands Url", () => {
it("Generate logo brands url for cloud component", () => {
assert.strictEqual(
- brandsUrl({ domain: "cloud", type: "logo" }),
- "/api/brands/integration/cloud/logo.png"
+ brandsUrl(
+ { domain: "cloud", type: "logo" },
+ "http://homeassistant.local:8123"
+ ),
+ "http://homeassistant.local:8123/api/brands/integration/cloud/logo.png"
);
});
it("Generate icon brands url for cloud component", () => {
assert.strictEqual(
- brandsUrl({ domain: "cloud", type: "icon" }),
- "/api/brands/integration/cloud/icon.png"
+ brandsUrl(
+ { domain: "cloud", type: "icon" },
+ "http://homeassistant.local:8123"
+ ),
+ "http://homeassistant.local:8123/api/brands/integration/cloud/icon.png"
);
});
it("Generate dark theme optimized logo brands url for cloud component", () => {
assert.strictEqual(
- brandsUrl({ domain: "cloud", type: "logo", darkOptimized: true }),
- "/api/brands/integration/cloud/dark_logo.png"
+ brandsUrl(
+ { domain: "cloud", type: "logo", darkOptimized: true },
+ "http://homeassistant.local:8123"
+ ),
+ "http://homeassistant.local:8123/api/brands/integration/cloud/dark_logo.png"
);
});
});
@@ -33,14 +42,20 @@ describe("Generate brands Url", () => {
describe("addBrandsAuth", () => {
it("Returns non-brands URLs unchanged", () => {
assert.strictEqual(
- addBrandsAuth("/api/camera_proxy/camera.foo?token=abc"),
+ addBrandsAuth(
+ "/api/camera_proxy/camera.foo?token=abc",
+ "http://homeassistant.local:8123"
+ ),
"/api/camera_proxy/camera.foo?token=abc"
);
});
it("Returns brands URL unchanged when no token is available", () => {
assert.strictEqual(
- addBrandsAuth("/api/brands/integration/demo/icon.png"),
+ addBrandsAuth(
+ "/api/brands/integration/demo/icon.png",
+ "http://homeassistant.local:8123"
+ ),
"/api/brands/integration/demo/icon.png"
);
});
@@ -52,8 +67,11 @@ describe("addBrandsAuth", () => {
await fetchBrandsAccessToken(mockHass);
assert.strictEqual(
- addBrandsAuth("/api/brands/integration/demo/icon.png"),
- "/api/brands/integration/demo/icon.png?token=test-token-123"
+ addBrandsAuth(
+ "/api/brands/integration/demo/icon.png",
+ "http://homeassistant.local:8123"
+ ),
+ "http://homeassistant.local:8123/api/brands/integration/demo/icon.png?token=test-token-123"
);
});
@@ -64,8 +82,11 @@ describe("addBrandsAuth", () => {
await fetchBrandsAccessToken(mockHass);
assert.strictEqual(
- addBrandsAuth("/api/brands/integration/demo/icon.png?token=old-token"),
- "/api/brands/integration/demo/icon.png?token=new-token"
+ addBrandsAuth(
+ "/api/brands/integration/demo/icon.png?token=old-token",
+ "http://homeassistant.local:8123"
+ ),
+ "http://homeassistant.local:8123/api/brands/integration/demo/icon.png?token=new-token"
);
});
});
@@ -90,8 +111,11 @@ describe("scheduleBrandsTokenRefresh", () => {
await fetchBrandsAccessToken(mockHass);
assert.strictEqual(callCount, 1);
assert.strictEqual(
- brandsUrl({ domain: "test", type: "icon" }),
- "/api/brands/integration/test/icon.png?token=token-1"
+ brandsUrl(
+ { domain: "test", type: "icon" },
+ "http://homeassistant.local:8123"
+ ),
+ "http://homeassistant.local:8123/api/brands/integration/test/icon.png?token=token-1"
);
scheduleBrandsTokenRefresh(mockHass);
@@ -100,8 +124,11 @@ describe("scheduleBrandsTokenRefresh", () => {
await vi.advanceTimersByTimeAsync(30 * 60 * 1000);
assert.strictEqual(callCount, 2);
assert.strictEqual(
- brandsUrl({ domain: "test", type: "icon" }),
- "/api/brands/integration/test/icon.png?token=token-2"
+ brandsUrl(
+ { domain: "test", type: "icon" },
+ "http://homeassistant.local:8123"
+ ),
+ "http://homeassistant.local:8123/api/brands/integration/test/icon.png?token=token-2"
);
});