1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 00:27:49 +01:00

Add hass url to brand images (#29961)

This commit is contained in:
Bram Kragten
2026-03-03 21:00:43 +01:00
committed by GitHub
parent 23af40743b
commit 76aef60c05
40 changed files with 418 additions and 238 deletions

View File

@@ -173,11 +173,14 @@ export class HaDevicePicker extends LitElement {
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: configEntry.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: configEntry.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
/>`
: nothing}
<span slot="headline">${primary}</span>
@@ -195,11 +198,14 @@ export class HaDevicePicker extends LitElement {
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: item.domain,
type: "icon",
darkOptimized: this.hass.themes.darkMode,
})}
src=${brandsUrl(
{
domain: item.domain,
type: "icon",
darkOptimized: this.hass.themes.darkMode,
},
this.hass.auth.data.hassUrl
)}
/>
`
: nothing}

View File

@@ -138,10 +138,10 @@ export class StateBadge extends LitElement {
let imageUrl =
stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture;
imageUrl = addBrandsAuth(imageUrl);
if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl);
}
imageUrl = addBrandsAuth(imageUrl, this.hass?.auth.data.hassUrl);
if (domain === "camera") {
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
}

View File

@@ -61,11 +61,14 @@ export class HaDomainIcon extends LitElement {
`;
}
if (this.brandFallback) {
const image = brandsUrl({
domain: this.domain!,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
});
const image = brandsUrl(
{
domain: this.domain!,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
);
return html`
<img
alt=""

View File

@@ -220,11 +220,14 @@ export class HaRelatedItems extends LitElement {
>
<ha-list-item hasMeta graphic="icon">
<img
.src=${brandsUrl({
domain: entry.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
.src=${brandsUrl(
{
domain: entry.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt=${entry.domain}
@@ -245,11 +248,14 @@ export class HaRelatedItems extends LitElement {
>
<ha-list-item hasMeta graphic="icon">
<img
.src=${brandsUrl({
domain: integration,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
.src=${brandsUrl(
{
domain: integration,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt=${integration}

View File

@@ -79,11 +79,14 @@ export class HaMediaSelector extends LitElement {
if (thumbnail && isBrandUrl(thumbnail)) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
this._thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnail),
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
});
this._thumbnailUrl = brandsUrl(
{
domain: extractDomainFromBrandUrl(thumbnail),
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
);
} else if (thumbnail && thumbnail.startsWith("/")) {
this._thumbnailUrl = undefined;
// Thumbnails served by local API require authentication

View File

@@ -1022,11 +1022,14 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: (item as DevicePickerItem).domain!,
type: "icon",
darkOptimized: this.hass.themes.darkMode,
})}
src=${brandsUrl(
{
domain: (item as DevicePickerItem).domain!,
type: "icon",
darkOptimized: this.hass.themes.darkMode,
},
this.hass.auth.data.hassUrl
)}
/>
`
: type === "floor"

View File

@@ -768,11 +768,14 @@ export class HaMediaPlayerBrowse extends LitElement {
if (isBrandUrl(thumbnailUrl)) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
return brandsUrl({
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
});
return brandsUrl(
{
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
);
}
if (thumbnailUrl.startsWith("/")) {

View File

@@ -577,11 +577,14 @@ export class HaTargetPickerItemRow extends LitElement {
try {
const data = await getConfigEntry(this.hass, configEntryId);
const domain = data.config_entry.domain;
this._iconImg = brandsUrl({
domain: domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
});
this._iconImg = brandsUrl(
{
domain: domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
);
this._setDomainName(domain);
} catch {

View File

@@ -203,11 +203,14 @@ export class HaTargetPickerValueChip extends LitElement {
try {
const data = await getConfigEntry(this.hass, configEntryId);
const domain = data.config_entry.domain;
this._iconImg = brandsUrl({
domain: domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
});
this._iconImg = brandsUrl(
{
domain: domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
);
this._setDomainName(domain);
} catch {

View File

@@ -17,11 +17,14 @@ export class VoiceAssistantBrandicon extends LitElement {
<img
class="logo"
alt=${voiceAssistants[this.voiceAssistantId].name}
src=${brandsUrl({
domain: voiceAssistants[this.voiceAssistantId].domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: voiceAssistants[this.voiceAssistantId].domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>

View File

@@ -140,11 +140,15 @@ class StepFlowCreateEntry extends LitElement {
this.hass.localize,
domains[device.primary_config_entry]
)}
src=${brandsUrl({
domain: domains[device.primary_config_entry],
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain:
domains[device.primary_config_entry],
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>`

View File

@@ -21,11 +21,14 @@ class IntegrationBadge extends LitElement {
<div class="icon">
<img
alt=""
src=${brandsUrl({
domain: this.domain,
type: "icon",
darkOptimized: this.darkOptimizedIcon,
})}
src=${brandsUrl(
{
domain: this.domain,
type: "icon",
darkOptimized: this.darkOptimizedIcon,
},
location.origin
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>

View File

@@ -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
)}
/>
`;

View File

@@ -149,11 +149,14 @@ class HaBackupConfigAgents extends LitElement {
return html`
<img
.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"
alt=""

View File

@@ -63,11 +63,14 @@ class HaBackupAgentsPicker extends LitElement {
? html` <ha-svg-icon .path=${mdiNas} slot="start"></ha-svg-icon> `
: html`
<img
.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"
alt=""

View File

@@ -227,11 +227,14 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
return html`
<img
title=${name}
.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
)}
height="24"
crossorigin="anonymous"
referrerpolicy="no-referrer"

View File

@@ -204,12 +204,15 @@ class HaConfigBackupDetails extends LitElement {
`
: html`
<img
.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"
alt=${`${domain} logo`}

View File

@@ -246,11 +246,14 @@ class HaConfigBackupSettings extends LitElement {
? html`<ha-card class="cloud-info">
<div class="cloud-header">
<img
.src=${brandsUrl({
domain: "cloud",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
.src=${brandsUrl(
{
domain: "cloud",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt="Nabu Casa logo"

View File

@@ -55,11 +55,14 @@ export class AITaskPref extends LitElement {
<h1 class="card-header">
<img
alt=""
src=${brandsUrl({
domain: "ai_task",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: "ai_task",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>${this.hass.localize("ui.panel.config.ai_task.header")}

View File

@@ -364,11 +364,14 @@ export class HaConfigDevicePage extends LitElement {
<img
slot="graphic"
alt=${domainToName(this.hass.localize, integration.domain)}
src=${brandsUrl({
domain: integration.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: integration.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@@ -763,11 +766,14 @@ export class HaConfigDevicePage extends LitElement {
this.hass.localize,
integrations[0].domain
)}
src=${brandsUrl({
domain: integrations[0].domain,
type: "logo",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: integrations[0].domain,
type: "logo",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
@load=${this._onImageLoad}

View File

@@ -535,11 +535,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: device.domains[0],
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: device.domains[0],
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
/>`
: "",
},

View File

@@ -186,11 +186,14 @@ export class EnergyGridSettings extends LitElement {
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: "co2signal",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: "co2signal",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
/>
<span class="content">${this._co2ConfigEntry.title}</span>
<ha-icon-button

View File

@@ -193,11 +193,14 @@ export class DialogEnergySolarSettings
crossorigin="anonymous"
referrerpolicy="no-referrer"
style="height: 24px; margin-right: 16px; margin-inline-end: 16px; margin-inline-start: initial;"
src=${brandsUrl({
domain: entry.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: entry.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
/>${entry.title}
</div>`}
>

View File

@@ -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];

View File

@@ -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"
/>

View File

@@ -376,11 +376,14 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
<div class="logo-container">
<img
alt=${domainToName(this.hass.localize, this.domain)}
src=${brandsUrl({
domain: this.domain,
type: "icon@2x",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: this.domain,
type: "icon@2x",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
@load=${this._onImageLoad}

View File

@@ -64,11 +64,14 @@ class HaDomainIntegrations extends LitElement {
alt=""
slot="graphic"
loading="lazy"
src=${brandsUrl({
domain: flow.handler,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: flow.handler,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
@@ -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"
/>

View File

@@ -31,11 +31,14 @@ export class HaIntegrationActionCard extends LitElement {
<div class="card-content">
<img
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"
@error=${this._onImageError}

View File

@@ -32,11 +32,14 @@ export class HaIntegrationHeader extends LitElement {
<div class="header">
<img
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"
@error=${this._onImageError}

View File

@@ -62,11 +62,14 @@ export class HaIntegrationListItem extends ListItemBase {
: html`<img
alt=""
loading="lazy"
src=${brandsUrl({
domain: this.integration.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: this.integration.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>`}

View File

@@ -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
)}
/>
<ha-qr-code
.data=${this._commissionParams.setup_qr_code}

View File

@@ -222,11 +222,14 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
>
<img
slot="graphic"
.src=${brandsUrl({
domain: router.brand,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
.src=${brandsUrl(
{
domain: router.brand,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
alt=${router.brand}
crossorigin="anonymous"
referrerpolicy="no-referrer"

View File

@@ -208,11 +208,14 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
<div class="card-header">
<img
alt=""
src=${brandsUrl({
domain: preview_feature.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: preview_feature.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>

View File

@@ -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"

View File

@@ -55,11 +55,14 @@ class IntegrationsStartupTime extends LitElement {
<img
alt=""
loading="lazy"
src=${brandsUrl({
domain: setup.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
src=${brandsUrl(
{
domain: setup.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
slot="start"

View File

@@ -157,11 +157,14 @@ class HaLogbookRenderer extends LitElement {
!item.state &&
domain &&
isComponentLoaded(this.hass, domain)
? brandsUrl({
domain: domain!,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})
? brandsUrl(
{
domain: domain!,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)
: undefined;
const traceContext =

View File

@@ -144,7 +144,10 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
if (!entityPicture) return undefined;
let imageUrl = this.hass!.hassUrl(addBrandsAuth(entityPicture));
let imageUrl = addBrandsAuth(
this.hass!.hassUrl(entityPicture),
this.hass?.auth.data.hassUrl
);
if (computeStateDomain(stateObj) === "camera") {
imageUrl = cameraUrlWithWidthHeight(imageUrl, 32, 32);
}

View File

@@ -159,7 +159,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
if (!entityPicture) return undefined;
let imageUrl = this.hass!.hassUrl(addBrandsAuth(entityPicture));
let imageUrl = addBrandsAuth(
this.hass!.hassUrl(entityPicture),
this.hass?.auth.data.hassUrl
);
if (computeDomain(entity.entity_id) === "camera") {
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
}

View File

@@ -55,44 +55,62 @@ export const clearBrandsTokenRefresh = (): void => {
}
};
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;
}
};

View File

@@ -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"
);
});