From 17d9cd192f31507ec7a16d52ca8b9302f95a5b8d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 25 Feb 2026 17:14:36 +0100 Subject: [PATCH 01/69] Bumped version to 20260225.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6328907890..b920ab0258 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20260128.0" +version = "20260225.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From e07194027abe23237acd56ba381df8e9747ae0a3 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 26 Feb 2026 11:38:01 +0100 Subject: [PATCH 02/69] Convert Energy Now tiles to badges (#29845) --- .../energy/strategies/power-view-strategy.ts | 49 +++------- .../energy/hui-gas-total-badge.ts} | 67 ++++---------- .../energy/hui-power-total-badge.ts} | 90 ++++++------------- .../energy/hui-water-total-badge.ts} | 67 ++++---------- src/panels/lovelace/badges/types.ts | 17 ++++ src/panels/lovelace/cards/types.ts | 15 ---- .../create-element/create-badge-element.ts | 3 + .../create-element/create-card-element.ts | 3 - 8 files changed, 98 insertions(+), 213 deletions(-) rename src/panels/lovelace/{cards/energy/hui-gas-total-card.ts => badges/energy/hui-gas-total-badge.ts} (61%) rename src/panels/lovelace/{cards/energy/hui-power-total-card.ts => badges/energy/hui-power-total-badge.ts} (59%) rename src/panels/lovelace/{cards/energy/hui-water-total-card.ts => badges/energy/hui-water-total-badge.ts} (61%) diff --git a/src/panels/energy/strategies/power-view-strategy.ts b/src/panels/energy/strategies/power-view-strategy.ts index 9ea1aea5ee..f223fa8e16 100644 --- a/src/panels/energy/strategies/power-view-strategy.ts +++ b/src/panels/energy/strategies/power-view-strategy.ts @@ -7,11 +7,7 @@ import type { HomeAssistant } from "../../../types"; import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy"; import { shouldShowFloorsAndAreas } from "./show-floors-and-areas"; import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section"; -import { - LARGE_SCREEN_CONDITION, - SMALL_SCREEN_CONDITION, -} from "../../lovelace/strategies/helpers/screen-conditions"; -import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; +import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; @customElement("power-view-strategy") export class PowerViewStrategy extends ReactiveElement { @@ -49,22 +45,15 @@ export class PowerViewStrategy extends ReactiveElement { (source) => source.type === "gas" && source.stat_rate ); - const tileSection: LovelaceSectionConfig = { - type: "grid", - cards: [], - column_span: 2, - }; const chartsSection: LovelaceSectionConfig = { type: "grid", cards: [], - column_span: 2, }; - const tiles: LovelaceCardConfig[] = []; + const badges: LovelaceBadgeConfig[] = []; const view: LovelaceViewConfig = { type: "sections", - sections: [tileSection, chartsSection], - max_columns: 2, + sections: [chartsSection], }; // No sources configured @@ -80,11 +69,10 @@ export class PowerViewStrategy extends ReactiveElement { } if (hasPowerSources) { - const card = { + badges.push({ type: "power-total", collection_key: collectionKey, - }; - tiles.push(card); + }); chartsSection.cards!.push({ title: hass.localize("ui.panel.energy.cards.power_sources_graph_title"), @@ -97,19 +85,17 @@ export class PowerViewStrategy extends ReactiveElement { } if (hasGasSources) { - const card = { + badges.push({ type: "gas-total", collection_key: collectionKey, - }; - tiles.push({ ...card }); + }); } if (hasWaterSources) { - const card = { + badges.push({ type: "water-total", collection_key: collectionKey, - }; - tiles.push({ ...card }); + }); } if (hasPowerDevices) { @@ -148,21 +134,8 @@ export class PowerViewStrategy extends ReactiveElement { }); } - tiles.forEach((card) => { - tileSection.cards!.push({ - ...card, - grid_options: { columns: 24 / tiles.length }, - }); - }); - - if (tiles.length > 2) { - // On small screens with 3 tiles, show them in 1 column - tileSection.visibility = [LARGE_SCREEN_CONDITION]; - view.sections!.unshift({ - type: "grid", - cards: tiles, - visibility: [SMALL_SCREEN_CONDITION], - }); + if (badges.length) { + view.badges = badges; } return view; diff --git a/src/panels/lovelace/cards/energy/hui-gas-total-card.ts b/src/panels/lovelace/badges/energy/hui-gas-total-badge.ts similarity index 61% rename from src/panels/lovelace/cards/energy/hui-gas-total-card.ts rename to src/panels/lovelace/badges/energy/hui-gas-total-badge.ts index cca6c4b737..87f212e0ce 100644 --- a/src/panels/lovelace/cards/energy/hui-gas-total-card.ts +++ b/src/panels/lovelace/badges/energy/hui-gas-total-badge.ts @@ -3,11 +3,8 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "../../../../components/ha-card"; +import "../../../../components/ha-badge"; import "../../../../components/ha-svg-icon"; -import "../../../../components/tile/ha-tile-container"; -import "../../../../components/tile/ha-tile-icon"; -import "../../../../components/tile/ha-tile-info"; import type { EnergyData, EnergyPreferences } from "../../../../data/energy"; import { formatFlowRateShort, @@ -16,18 +13,17 @@ import { } from "../../../../data/energy"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; -import type { LovelaceCard, LovelaceGridOptions } from "../../types"; -import { tileCardStyle } from "../tile/tile-card-style"; -import type { GasTotalCardConfig } from "../types"; +import type { LovelaceBadge } from "../../types"; +import type { GasTotalBadgeConfig } from "../types"; -@customElement("hui-gas-total-card") -export class HuiGasTotalCard +@customElement("hui-gas-total-badge") +export class HuiGasTotalBadge extends SubscribeMixin(LitElement) - implements LovelaceCard + implements LovelaceBadge { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _config?: GasTotalCardConfig; + @state() private _config?: GasTotalBadgeConfig; @state() private _data?: EnergyData; @@ -35,7 +31,7 @@ export class HuiGasTotalCard protected hassSubscribeRequiredHostProps = ["_config"]; - public setConfig(config: GasTotalCardConfig): void { + public setConfig(config: GasTotalBadgeConfig): void { this._config = config; } @@ -49,34 +45,19 @@ export class HuiGasTotalCard ]; } - public getCardSize(): Promise | number { - return 1; - } - - getGridOptions(): LovelaceGridOptions { - return { - columns: 12, - min_columns: 6, - rows: 1, - min_rows: 1, - }; - } - protected shouldUpdate(changedProps: PropertyValues): boolean { if (changedProps.has("_config") || changedProps.has("_data")) { return true; } - // Check if any of the tracked entity states have changed if (changedProps.has("hass")) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; if (!oldHass || !this._entities.size) { return true; } - // Only update if one of our tracked entities changed for (const entityId of this._entities) { - if (oldHass.states[entityId] !== this.hass.states[entityId]) { + if (oldHass.states[entityId] !== this.hass?.states[entityId]) { return true; } } @@ -122,32 +103,22 @@ export class HuiGasTotalCard this.hass.localize("ui.panel.lovelace.cards.energy.gas_total_title"); return html` - - - - - - - ${name} - ${displayValue} - - - + + + ${displayValue} + `; } - static styles = [ - tileCardStyle, - css` - :host { - --tile-color: var(--energy-gas-color); - } - `, - ]; + static styles = css` + ha-badge { + --badge-color: var(--energy-gas-color); + } + `; } declare global { interface HTMLElementTagNameMap { - "hui-gas-total-card": HuiGasTotalCard; + "hui-gas-total-badge": HuiGasTotalBadge; } } diff --git a/src/panels/lovelace/cards/energy/hui-power-total-card.ts b/src/panels/lovelace/badges/energy/hui-power-total-badge.ts similarity index 59% rename from src/panels/lovelace/cards/energy/hui-power-total-card.ts rename to src/panels/lovelace/badges/energy/hui-power-total-badge.ts index d94161cd08..7d106cc6f5 100644 --- a/src/panels/lovelace/cards/energy/hui-power-total-card.ts +++ b/src/panels/lovelace/badges/energy/hui-power-total-badge.ts @@ -4,11 +4,8 @@ import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { formatNumber } from "../../../../common/number/format_number"; -import "../../../../components/ha-card"; +import "../../../../components/ha-badge"; import "../../../../components/ha-svg-icon"; -import "../../../../components/tile/ha-tile-container"; -import "../../../../components/tile/ha-tile-icon"; -import "../../../../components/tile/ha-tile-info"; import type { EnergyData, EnergyPreferences } from "../../../../data/energy"; import { getEnergyDataCollection, @@ -16,18 +13,17 @@ import { } from "../../../../data/energy"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; -import type { LovelaceCard, LovelaceGridOptions } from "../../types"; -import { tileCardStyle } from "../tile/tile-card-style"; -import type { PowerTotalCardConfig } from "../types"; +import type { LovelaceBadge } from "../../types"; +import type { PowerTotalBadgeConfig } from "../types"; -@customElement("hui-power-total-card") -export class HuiPowerTotalCard +@customElement("hui-power-total-badge") +export class HuiPowerTotalBadge extends SubscribeMixin(LitElement) - implements LovelaceCard + implements LovelaceBadge { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _config?: PowerTotalCardConfig; + @state() private _config?: PowerTotalBadgeConfig; @state() private _data?: EnergyData; @@ -35,7 +31,7 @@ export class HuiPowerTotalCard protected hassSubscribeRequiredHostProps = ["_config"]; - public setConfig(config: PowerTotalCardConfig): void { + public setConfig(config: PowerTotalBadgeConfig): void { this._config = config; } @@ -49,34 +45,19 @@ export class HuiPowerTotalCard ]; } - public getCardSize(): Promise | number { - return 1; - } - - getGridOptions(): LovelaceGridOptions { - return { - columns: 12, - min_columns: 6, - rows: 1, - min_rows: 1, - }; - } - protected shouldUpdate(changedProps: PropertyValues): boolean { if (changedProps.has("_config") || changedProps.has("_data")) { return true; } - // Check if any of the tracked entity states have changed if (changedProps.has("hass")) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; if (!oldHass || !this._entities.size) { return true; } - // Only update if one of our tracked entities changed for (const entityId of this._entities) { - if (oldHass.states[entityId] !== this.hass.states[entityId]) { + if (oldHass.states[entityId] !== this.hass?.states[entityId]) { return true; } } @@ -94,10 +75,10 @@ export class HuiPowerTotalCard this._entities.clear(); let solar = 0; - let from_grid = 0; - let to_grid = 0; - let from_battery = 0; - let to_battery = 0; + let fromGrid = 0; + let toGrid = 0; + let fromBattery = 0; + let toBattery = 0; prefs.energy_sources.forEach((source) => { if (source.type === "solar" && source.stat_rate) { @@ -105,17 +86,17 @@ export class HuiPowerTotalCard if (value > 0) solar += value; } else if (source.type === "grid" && source.stat_rate) { const value = this._getCurrentPower(source.stat_rate); - if (value > 0) from_grid += value; - else if (value < 0) to_grid += Math.abs(value); + if (value > 0) fromGrid += value; + else if (value < 0) toGrid += Math.abs(value); } else if (source.type === "battery" && source.stat_rate) { const value = this._getCurrentPower(source.stat_rate); - if (value > 0) from_battery += value; - else if (value < 0) to_battery += Math.abs(value); + if (value > 0) fromBattery += value; + else if (value < 0) toBattery += Math.abs(value); } }); - const used_total = from_grid + solar + from_battery - to_grid - to_battery; - return Math.max(0, used_total); + const usedTotal = fromGrid + solar + fromBattery - toGrid - toBattery; + return Math.max(0, usedTotal); } protected render() { @@ -141,35 +122,22 @@ export class HuiPowerTotalCard this.hass.localize("ui.panel.lovelace.cards.energy.power_total_title"); return html` - - - - - - - ${name} - ${displayValue} - - - + + + ${displayValue} + `; } - static styles = [ - tileCardStyle, - css` - :host { - --tile-color: var(--primary-color); - } - `, - ]; + static styles = css` + ha-badge { + --badge-color: var(--primary-color); + } + `; } declare global { interface HTMLElementTagNameMap { - "hui-power-total-card": HuiPowerTotalCard; + "hui-power-total-badge": HuiPowerTotalBadge; } } diff --git a/src/panels/lovelace/cards/energy/hui-water-total-card.ts b/src/panels/lovelace/badges/energy/hui-water-total-badge.ts similarity index 61% rename from src/panels/lovelace/cards/energy/hui-water-total-card.ts rename to src/panels/lovelace/badges/energy/hui-water-total-badge.ts index 23f9c972fb..2c3b694898 100644 --- a/src/panels/lovelace/cards/energy/hui-water-total-card.ts +++ b/src/panels/lovelace/badges/energy/hui-water-total-badge.ts @@ -3,11 +3,8 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "../../../../components/ha-card"; +import "../../../../components/ha-badge"; import "../../../../components/ha-svg-icon"; -import "../../../../components/tile/ha-tile-container"; -import "../../../../components/tile/ha-tile-icon"; -import "../../../../components/tile/ha-tile-info"; import type { EnergyData, EnergyPreferences } from "../../../../data/energy"; import { formatFlowRateShort, @@ -16,18 +13,17 @@ import { } from "../../../../data/energy"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; -import type { LovelaceCard, LovelaceGridOptions } from "../../types"; -import { tileCardStyle } from "../tile/tile-card-style"; -import type { WaterTotalCardConfig } from "../types"; +import type { LovelaceBadge } from "../../types"; +import type { WaterTotalBadgeConfig } from "../types"; -@customElement("hui-water-total-card") -export class HuiWaterTotalCard +@customElement("hui-water-total-badge") +export class HuiWaterTotalBadge extends SubscribeMixin(LitElement) - implements LovelaceCard + implements LovelaceBadge { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _config?: WaterTotalCardConfig; + @state() private _config?: WaterTotalBadgeConfig; @state() private _data?: EnergyData; @@ -35,7 +31,7 @@ export class HuiWaterTotalCard protected hassSubscribeRequiredHostProps = ["_config"]; - public setConfig(config: WaterTotalCardConfig): void { + public setConfig(config: WaterTotalBadgeConfig): void { this._config = config; } @@ -49,34 +45,19 @@ export class HuiWaterTotalCard ]; } - public getCardSize(): Promise | number { - return 1; - } - - getGridOptions(): LovelaceGridOptions { - return { - columns: 12, - min_columns: 6, - rows: 1, - min_rows: 1, - }; - } - protected shouldUpdate(changedProps: PropertyValues): boolean { if (changedProps.has("_config") || changedProps.has("_data")) { return true; } - // Check if any of the tracked entity states have changed if (changedProps.has("hass")) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; if (!oldHass || !this._entities.size) { return true; } - // Only update if one of our tracked entities changed for (const entityId of this._entities) { - if (oldHass.states[entityId] !== this.hass.states[entityId]) { + if (oldHass.states[entityId] !== this.hass?.states[entityId]) { return true; } } @@ -122,32 +103,22 @@ export class HuiWaterTotalCard this.hass.localize("ui.panel.lovelace.cards.energy.water_total_title"); return html` - - - - - - - ${name} - ${displayValue} - - - + + + ${displayValue} + `; } - static styles = [ - tileCardStyle, - css` - :host { - --tile-color: var(--energy-water-color); - } - `, - ]; + static styles = css` + ha-badge { + --badge-color: var(--energy-water-color); + } + `; } declare global { interface HTMLElementTagNameMap { - "hui-water-total-card": HuiWaterTotalCard; + "hui-water-total-badge": HuiWaterTotalBadge; } } diff --git a/src/panels/lovelace/badges/types.ts b/src/panels/lovelace/badges/types.ts index b46145ee72..3c553a2108 100644 --- a/src/panels/lovelace/badges/types.ts +++ b/src/panels/lovelace/badges/types.ts @@ -48,3 +48,20 @@ export interface EntityBadgeConfig extends LovelaceBadgeConfig { */ display_type?: DisplayType; } + +interface EnergyTotalBadgeConfig extends LovelaceBadgeConfig { + title?: string; + collection_key?: string; +} + +export interface PowerTotalBadgeConfig extends EnergyTotalBadgeConfig { + type: "power-total"; +} + +export interface WaterTotalBadgeConfig extends EnergyTotalBadgeConfig { + type: "water-total"; +} + +export interface GasTotalBadgeConfig extends EnergyTotalBadgeConfig { + type: "gas-total"; +} diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index cea8c6becb..f70e7815be 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -265,21 +265,6 @@ export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig { show_legend?: boolean; } -export interface PowerTotalCardConfig extends EnergyCardBaseConfig { - type: "power-total"; - title?: string; -} - -export interface WaterTotalCardConfig extends EnergyCardBaseConfig { - type: "water-total"; - title?: string; -} - -export interface GasTotalCardConfig extends EnergyCardBaseConfig { - type: "gas-total"; - title?: string; -} - export interface PowerSankeyCardConfig extends EnergyCardBaseConfig { type: "power-sankey"; title?: string; diff --git a/src/panels/lovelace/create-element/create-badge-element.ts b/src/panels/lovelace/create-element/create-badge-element.ts index 15d7b45cdd..34b80d4418 100644 --- a/src/panels/lovelace/create-element/create-badge-element.ts +++ b/src/panels/lovelace/create-element/create-badge-element.ts @@ -10,6 +10,9 @@ const ALWAYS_LOADED_TYPES = new Set(["error", "entity"]); const LAZY_LOAD_TYPES = { "entity-filter": () => import("../badges/hui-entity-filter-badge"), "state-label": () => import("../badges/hui-state-label-badge"), + "power-total": () => import("../badges/energy/hui-power-total-badge"), + "gas-total": () => import("../badges/energy/hui-gas-total-badge"), + "water-total": () => import("../badges/energy/hui-water-total-badge"), }; // This will not return an error card but will throw the error diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index 54d89aa391..26d2266a67 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -71,9 +71,6 @@ const LAZY_LOAD_TYPES = { import("../cards/water/hui-water-flow-sankey-card"), "power-sources-graph": () => import("../cards/energy/hui-power-sources-graph-card"), - "power-total": () => import("../cards/energy/hui-power-total-card"), - "water-total": () => import("../cards/energy/hui-water-total-card"), - "gas-total": () => import("../cards/energy/hui-gas-total-card"), "power-sankey": () => import("../cards/energy/hui-power-sankey-card"), "entity-filter": () => import("../cards/hui-entity-filter-card"), error: () => import("../cards/hui-error-card"), From be430931cc3d37b3ea0867a3e0a217248ab1cefe Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:31:50 +0100 Subject: [PATCH 03/69] Fix protocols dashboards fab padding (#29847) --- .../integration-panels/matter/matter-config-dashboard.ts | 5 +++-- .../integration-panels/zha/zha-config-dashboard.ts | 5 +++-- .../integration-panels/zwave_js/zwave_js-config-dashboard.ts | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/matter/matter-config-dashboard.ts b/src/panels/config/integrations/integration-panels/matter/matter-config-dashboard.ts index 54133ea94e..862f68f816 100644 --- a/src/panels/config/integrations/integration-panels/matter/matter-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/matter/matter-config-dashboard.ts @@ -20,9 +20,9 @@ import "../../../../../components/ha-md-list-item"; import "../../../../../components/ha-svg-icon"; import type { ConfigEntry } from "../../../../../data/config_entries"; import { getConfigEntries } from "../../../../../data/config_entries"; -import type { HomeAssistant } from "../../../../../types"; import "../../../../../layouts/hass-subpage"; import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant } from "../../../../../types"; const THREAD_ICON = "m 17.126982,8.0730792 c 0,-0.7297242 -0.593746,-1.32357 -1.323637,-1.32357 -0.729454,0 -1.323199,0.5938458 -1.323199,1.32357 v 1.3234242 l 1.323199,1.458e-4 c 0.729891,0 1.323637,-0.5937006 1.323637,-1.32357 z M 11.999709,0 C 5.3829818,0 0,5.3838955 0,12.001455 0,18.574352 5.3105455,23.927406 11.865164,24 V 12.012075 l -3.9275642,-2.91e-4 c -1.1669814,0 -2.1169453,0.949979 -2.1169453,2.118323 0,1.16718 0.9499639,2.116868 2.1169453,2.116868 v 2.615717 c -2.6093089,0 -4.732218,-2.12327 -4.732218,-4.732585 0,-2.61048 2.1229091,-4.7343308 4.732218,-4.7343308 l 3.9275642,5.82e-4 v -1.323279 c 0,-2.172296 1.766691,-3.9395777 3.938181,-3.9395777 2.171928,0 3.9392,1.7672817 3.9392,3.9395777 0,2.1721498 -1.767272,3.9395768 -3.9392,3.9395768 l -1.323199,-1.45e-4 V 23.744102 C 19.911127,22.597726 24,17.768833 24,12.001455 24,5.3838955 18.616727,0 11.999709,0 Z"; @@ -312,7 +312,8 @@ export class MatterConfigDashboard extends LitElement { } .container { - padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4); + padding: var(--ha-space-2) var(--ha-space-4) + calc(var(--ha-space-16) + var(--safe-area-inset-bottom, 0px)); } a[slot="fab"] { diff --git a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts index 2430a379fc..e3ebe417bc 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts @@ -37,10 +37,10 @@ import { } from "../../../../../data/zha"; import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow"; import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box"; -import { fileDownload } from "../../../../../util/file_download"; import "../../../../../layouts/hass-subpage"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant, Route } from "../../../../../types"; +import { fileDownload } from "../../../../../util/file_download"; @customElement("zha-config-dashboard") class ZHAConfigDashboard extends LitElement { @@ -520,7 +520,8 @@ class ZHAConfigDashboard extends LitElement { } .container { - padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4); + padding: var(--ha-space-2) var(--ha-space-4) + calc(var(--ha-space-20) + var(--safe-area-inset-bottom, 0px)); } `, ]; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts index 02e7d3f01b..2e2e6ed389 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts @@ -18,6 +18,7 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { goBack } from "../../../../../common/navigate"; import "../../../../../components/ha-button"; import "../../../../../components/ha-card"; import "../../../../../components/ha-fab"; @@ -28,7 +29,6 @@ import "../../../../../components/ha-md-list-item"; import "../../../../../components/ha-progress-ring"; import "../../../../../components/ha-spinner"; import "../../../../../components/ha-svg-icon"; -import { goBack } from "../../../../../common/navigate"; import type { ConfigEntry } from "../../../../../data/config_entries"; import { ERROR_STATES, @@ -968,7 +968,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { } .container { - padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4); + padding: var(--ha-space-2) var(--ha-space-4) + calc(var(--ha-space-16) + var(--safe-area-inset-bottom, 0px)); } `, ]; From 81feea1109558c4992af62fdba54254ffa4c50a3 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 26 Feb 2026 10:33:34 +0100 Subject: [PATCH 04/69] Dynamically calculate the date range picker's vertical opening direction (#29850) --- src/components/ha-date-range-picker.ts | 33 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/components/ha-date-range-picker.ts b/src/components/ha-date-range-picker.ts index edf9f018ca..1b4f11b88a 100644 --- a/src/components/ha-date-range-picker.ts +++ b/src/components/ha-date-range-picker.ts @@ -93,6 +93,8 @@ export class HaDateRangePicker extends LitElement { | "center" | "inline"; + @state() private _calcedVerticalOpeningDirection?: "up" | "down"; + protected willUpdate(changedProps: PropertyValues) { if ( (!this.hasUpdated && this.ranges === undefined) || @@ -134,7 +136,9 @@ export class HaDateRangePicker extends LitElement { opening-direction=${ifDefined( this.openingDirection || this._calcedOpeningDirection )} - opens-vertical=${ifDefined(this.verticalOpeningDirection)} + opens-vertical=${ifDefined( + this.verticalOpeningDirection || this._calcedVerticalOpeningDirection + )} first-day=${firstWeekdayIndex(this.hass.locale)} language=${this.hass.locale.language} @change=${this._handleChange} @@ -328,17 +332,24 @@ export class HaDateRangePicker extends LitElement { private _handleClick() { // calculate opening direction if not set - if (!this._dateRangePicker.open && !this.openingDirection) { - const datePickerPosition = this.getBoundingClientRect().x; - let opens: "right" | "left" | "center" | "inline"; - if (datePickerPosition > (2 * window.innerWidth) / 3) { - opens = "left"; - } else if (datePickerPosition < window.innerWidth / 3) { - opens = "right"; - } else { - opens = "center"; + if (!this._dateRangePicker.open) { + if (!this.openingDirection) { + const datePickerPosition = this.getBoundingClientRect().x; + let opens: "right" | "left" | "center" | "inline"; + if (datePickerPosition > (2 * window.innerWidth) / 3) { + opens = "left"; + } else if (datePickerPosition < window.innerWidth / 3) { + opens = "right"; + } else { + opens = "center"; + } + this._calcedOpeningDirection = opens; + } + if (!this.verticalOpeningDirection) { + const rect = this.getBoundingClientRect(); + this._calcedVerticalOpeningDirection = + rect.top > window.innerHeight / 2 ? "up" : "down"; } - this._calcedOpeningDirection = opens; } } From 6190ba18eac4c2870617cac6ecdbd3f9dddf09b4 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 26 Feb 2026 11:20:05 +0000 Subject: [PATCH 05/69] Fix esc closing dialogs with prevent scrim close (#29851) --- src/components/ha-bottom-sheet.ts | 3 +++ src/components/ha-dialog.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/components/ha-bottom-sheet.ts b/src/components/ha-bottom-sheet.ts index 87785a0126..0996dc099e 100644 --- a/src/components/ha-bottom-sheet.ts +++ b/src/components/ha-bottom-sheet.ts @@ -141,6 +141,9 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) { private _handleKeyDown = (ev: KeyboardEvent) => { if (ev.key === "Escape") { this._escapePressed = true; + if (this.preventScrimClose) { + ev.preventDefault(); + } ev.stopPropagation(); (ev.currentTarget as WaDrawer).open = false; } diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index 1f56aa066f..2eb0d99b0b 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -239,6 +239,9 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) { private _handleKeyDown(ev: KeyboardEvent) { if (ev.key === "Escape") { this._escapePressed = true; + if (this.preventScrimClose) { + ev.preventDefault(); + } ev.stopPropagation(); (ev.currentTarget as WaDialog).open = false; } From 9ebfa4029b5fa820375fd0656c3824e360cc029d Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:02:12 +0100 Subject: [PATCH 06/69] Fix `ha-icon-button-toggle` selected style (#29856) --- src/components/ha-icon-button-toggle.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ha-icon-button-toggle.ts b/src/components/ha-icon-button-toggle.ts index 38458dceae..abcd9401c2 100644 --- a/src/components/ha-icon-button-toggle.ts +++ b/src/components/ha-icon-button-toggle.ts @@ -37,6 +37,9 @@ export class HaIconButtonToggle extends HaIconButton { background-color: transparent; border: 2px solid var(--primary-text-color); } + :host([selected]) ha-button::after { + opacity: 0; + } :host([selected]) ha-button::part(base) { color: var(--primary-background-color); background-color: unset; From 2813ed79389342f6cdb6c342f285ea85f8b37280 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 26 Feb 2026 15:43:20 +0000 Subject: [PATCH 07/69] Add missing theming variable support to dialog and bottom sheet (#29857) --- .../pages/components/ha-adaptive-dialog.ts | 36 +++++++++++++ gallery/src/pages/components/ha-dialog.ts | 20 ++++++- src/components/ha-adaptive-dialog.ts | 9 ++++ src/components/ha-bottom-sheet.ts | 54 ++++++++++++++++++- src/components/ha-dialog.ts | 27 ++++++++-- 5 files changed, 139 insertions(+), 7 deletions(-) diff --git a/gallery/src/pages/components/ha-adaptive-dialog.ts b/gallery/src/pages/components/ha-adaptive-dialog.ts index be0f829030..df27822ec1 100644 --- a/gallery/src/pages/components/ha-adaptive-dialog.ts +++ b/gallery/src/pages/components/ha-adaptive-dialog.ts @@ -515,6 +515,14 @@ export class DemoHaAdaptiveDialog extends LitElement { --ha-dialog-surface-background Dialog/sheet background color. + + --ha-dialog-surface-backdrop-filter + Dialog/sheet surface backdrop filter. + + + --dialog-box-shadow + Dialog surface box shadow (dialog mode only). + --ha-dialog-border-radius Border radius of the dialog surface (dialog mode only). @@ -527,6 +535,34 @@ export class DemoHaAdaptiveDialog extends LitElement { --ha-dialog-hide-duration Hide animation duration (dialog mode only). + + --ha-dialog-scrim-backdrop-filter + Dialog/sheet scrim backdrop filter. + + + --dialog-backdrop-filter + Dialog/sheet scrim backdrop filter (legacy fallback). + + + --mdc-dialog-scrim-color + Dialog/sheet scrim color (legacy compatibility). + + + --ha-bottom-sheet-surface-background + Bottom sheet background color (sheet mode only). + + + --ha-bottom-sheet-surface-backdrop-filter + Bottom sheet surface backdrop filter (sheet mode only). + + + --ha-bottom-sheet-scrim-backdrop-filter + Bottom sheet scrim backdrop filter (sheet mode only). + + + --ha-bottom-sheet-scrim-color + Bottom sheet scrim color (sheet mode only). + diff --git a/gallery/src/pages/components/ha-dialog.ts b/gallery/src/pages/components/ha-dialog.ts index 7e555e5905..734c60148c 100644 --- a/gallery/src/pages/components/ha-dialog.ts +++ b/gallery/src/pages/components/ha-dialog.ts @@ -380,13 +380,29 @@ export class DemoHaDialog extends LitElement { --ha-dialog-surface-background Dialog background color. + + --ha-dialog-surface-backdrop-filter + Backdrop filter applied to the dialog surface. + + + --dialog-box-shadow + Dialog surface box shadow. + --ha-dialog-border-radius Border radius of the dialog surface. - --dialog-z-index - Z-index for the dialog. + --ha-dialog-scrim-backdrop-filter + Backdrop filter applied to the dialog scrim. + + + --dialog-backdrop-filter + Legacy fallback for the dialog scrim backdrop filter. + + + --mdc-dialog-scrim-color + Dialog scrim color (legacy compatibility). --dialog-surface-margin-top diff --git a/src/components/ha-adaptive-dialog.ts b/src/components/ha-adaptive-dialog.ts index b0de1951d0..999f9f21db 100644 --- a/src/components/ha-adaptive-dialog.ts +++ b/src/components/ha-adaptive-dialog.ts @@ -31,9 +31,18 @@ type DialogSheetMode = "dialog" | "bottom-sheet"; * @slot footer - Dialog/sheet footer content. * * @cssprop --ha-dialog-surface-background - Dialog/sheet background color. + * @cssprop --ha-dialog-surface-backdrop-filter - Dialog/sheet backdrop filter. + * @cssprop --dialog-box-shadow - Dialog box shadow (dialog mode only). * @cssprop --ha-dialog-border-radius - Border radius of the dialog surface (dialog mode only). * @cssprop --ha-dialog-show-duration - Show animation duration (dialog mode only). * @cssprop --ha-dialog-hide-duration - Hide animation duration (dialog mode only). + * @cssprop --ha-dialog-scrim-backdrop-filter - Dialog/sheet scrim backdrop filter. + * @cssprop --dialog-backdrop-filter - Dialog/sheet scrim backdrop filter (legacy). + * @cssprop --mdc-dialog-scrim-color - Dialog/sheet scrim color (legacy). + * @cssprop --ha-bottom-sheet-surface-background - Bottom sheet background color (sheet mode only). + * @cssprop --ha-bottom-sheet-surface-backdrop-filter - Bottom sheet backdrop filter (sheet mode only). + * @cssprop --ha-bottom-sheet-scrim-backdrop-filter - Bottom sheet scrim backdrop filter (sheet mode only). + * @cssprop --ha-bottom-sheet-scrim-color - Bottom sheet scrim color (sheet mode only). * * @attr {boolean} open - Controls the dialog/sheet open state. * @attr {("alert"|"standard")} type - Dialog type (dialog mode only). Defaults to "standard". diff --git a/src/components/ha-bottom-sheet.ts b/src/components/ha-bottom-sheet.ts index 0996dc099e..af37604d94 100644 --- a/src/components/ha-bottom-sheet.ts +++ b/src/components/ha-bottom-sheet.ts @@ -25,6 +25,27 @@ const SWIPE_LOCKED_COMPONENTS = new Set([ const SWIPE_LOCKED_CLASSES = new Set(["volume-slider-container", "forecast"]); +/** + * Home Assistant bottom sheet component. + * + * @element ha-bottom-sheet + * @extends {LitElement} + * + * @cssprop --ha-bottom-sheet-height - Preferred height of the bottom sheet. + * @cssprop --ha-bottom-sheet-max-height - Maximum height of the bottom sheet. + * @cssprop --ha-bottom-sheet-max-width - Maximum width of the bottom sheet. + * @cssprop --ha-bottom-sheet-border-radius - Top border radius of the bottom sheet. + * @cssprop --ha-bottom-sheet-surface-background - Bottom sheet background color. + * @cssprop --ha-bottom-sheet-surface-backdrop-filter - Bottom sheet surface backdrop filter. + * @cssprop --ha-bottom-sheet-scrim-backdrop-filter - Bottom sheet scrim backdrop filter. + * @cssprop --ha-bottom-sheet-scrim-color - Bottom sheet scrim color. + * + * @cssprop --ha-dialog-surface-background - Bottom sheet background color fallback. + * @cssprop --ha-dialog-surface-backdrop-filter - Bottom sheet surface backdrop filter fallback. + * @cssprop --ha-dialog-scrim-backdrop-filter - Bottom sheet scrim backdrop filter fallback. + * @cssprop --dialog-backdrop-filter - Bottom sheet scrim backdrop filter legacy fallback. + * @cssprop --mdc-dialog-scrim-color - Bottom sheet scrim color legacy fallback. + */ @customElement("ha-bottom-sheet") export class HaBottomSheet extends ScrollableFadeMixin(LitElement) { @property({ attribute: false }) public hass?: HomeAssistant; @@ -385,6 +406,26 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) { transform: var(--dialog-transform); transition: var(--dialog-transition); } + wa-drawer::part(dialog)::backdrop { + -webkit-backdrop-filter: var( + --ha-bottom-sheet-scrim-backdrop-filter, + var( + --ha-dialog-scrim-backdrop-filter, + var(--dialog-backdrop-filter, none) + ) + ); + backdrop-filter: var( + --ha-bottom-sheet-scrim-backdrop-filter, + var( + --ha-dialog-scrim-backdrop-filter, + var(--dialog-backdrop-filter, none) + ) + ); + background-color: var( + --ha-bottom-sheet-scrim-color, + var(--mdc-dialog-scrim-color, none) + ); + } wa-drawer::part(body) { max-width: var(--ha-bottom-sheet-max-width); width: 100%; @@ -399,7 +440,18 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) { ); background-color: var( --ha-bottom-sheet-surface-background, - var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)), + var( + --ha-dialog-surface-background, + var(--card-background-color, var(--ha-color-surface-default)) + ) + ); + -webkit-backdrop-filter: var( + --ha-bottom-sheet-surface-backdrop-filter, + var(--ha-dialog-surface-backdrop-filter, none) + ); + backdrop-filter: var( + --ha-bottom-sheet-surface-backdrop-filter, + var(--ha-dialog-surface-backdrop-filter, none) ); padding: var( --ha-bottom-sheet-padding, diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index 2eb0d99b0b..3c0aabd1e3 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -52,7 +52,12 @@ type DialogHideEvent = CustomEvent<{ source?: Element }>; * @cssprop --ha-dialog-show-duration - Show animation duration. * @cssprop --ha-dialog-hide-duration - Hide animation duration. * @cssprop --ha-dialog-surface-background - Dialog background color. + * @cssprop --ha-dialog-surface-backdrop-filter - Dialog backdrop filter. + * @cssprop --dialog-box-shadow - Dialog box shadow. * @cssprop --ha-dialog-border-radius - Border radius of the dialog surface. + * @cssprop --ha-dialog-scrim-backdrop-filter - Dialog scrim backdrop filter. + * @cssprop --dialog-backdrop-filter - Dialog scrim backdrop filter (legacy). + * @cssprop --mdc-dialog-scrim-color - Dialog scrim color (legacy). * @cssprop --dialog-surface-margin-top - Top margin for the dialog surface. * * @attr {boolean} open - Controls the dialog open state. @@ -271,10 +276,6 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) { --spacing: var(--dialog-content-padding, var(--ha-space-6)); --show-duration: var(--ha-dialog-show-duration, 200ms); --hide-duration: var(--ha-dialog-hide-duration, 200ms); - --ha-dialog-surface-background: var( - --card-background-color, - var(--ha-color-surface-default) - ); --wa-color-surface-raised: var( --ha-dialog-surface-background, var(--card-background-color, var(--ha-color-surface-default)) @@ -305,6 +306,12 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) { } wa-dialog::part(dialog) { + -webkit-backdrop-filter: var( + --ha-dialog-surface-backdrop-filter, + none + ); + backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none); + box-shadow: var(--dialog-box-shadow, var(--wa-shadow-l)); color: var(--primary-text-color); min-width: var(--width, var(--full-width)); max-width: var(--width, var(--full-width)); @@ -334,6 +341,18 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) { overflow: hidden; } + wa-dialog::part(dialog)::backdrop { + -webkit-backdrop-filter: var( + --ha-dialog-scrim-backdrop-filter, + var(--dialog-backdrop-filter, none) + ); + backdrop-filter: var( + --ha-dialog-scrim-backdrop-filter, + var(--dialog-backdrop-filter, none) + ); + background-color: var(--mdc-dialog-scrim-color, none); + } + @media all and (max-width: 450px), all and (max-height: 500px) { :host([type="standard"]) { --ha-dialog-border-radius: 0; From bb7f441d8d179265f8b3550060c28a9863fe780b Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:59:27 +0100 Subject: [PATCH 08/69] Fix quick search icon size (#29858) --- src/dialogs/quick-bar/ha-quick-bar.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 27ac70df6f..c5cbd9b93e 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -288,7 +288,7 @@ export class QuickBar extends LitElement { ${"stateObj" in item && item.stateObj ? html` @@ -302,6 +302,7 @@ export class QuickBar extends LitElement { ? html` ` : item.icon - ? html`` + ? html`` : "iconColor" in item && item.iconColor ? html`
` : html` - + `} ${item.primary} ${item.secondary From 0793af68463ca7ac0b09767927c2c70db443ceb6 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 26 Feb 2026 14:41:43 +0000 Subject: [PATCH 09/69] Add matter configuration my link (#29859) --- src/panels/my/ha-panel-my.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index 98549eb0ab..d6efc1b13c 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -102,6 +102,10 @@ export const getMyRedirects = (): Redirects => ({ component: "zwave_js", redirect: "/config/zwave_js/dashboard", }, + config_matter: { + component: "matter", + redirect: "/config/matter/dashboard", + }, add_zigbee_device: { component: "zha", redirect: "/config/zha/add", From 9ca1cfbf4a20d4d9af7573a9aeb88cf53058cc61 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 26 Feb 2026 15:06:46 +0000 Subject: [PATCH 10/69] Add thread configuration my link (#29861) --- src/panels/my/ha-panel-my.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index d6efc1b13c..b3d123120e 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -106,6 +106,10 @@ export const getMyRedirects = (): Redirects => ({ component: "matter", redirect: "/config/matter/dashboard", }, + config_thread: { + component: "thread", + redirect: "/config/thread", + }, add_zigbee_device: { component: "zha", redirect: "/config/zha/add", From 2685a007e78581333d8b3469bcd600341ec6d009 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 26 Feb 2026 16:44:12 +0100 Subject: [PATCH 11/69] Fix scrollbar in 2026.3 (#29865) --- .../config/dashboard/ha-config-dashboard.ts | 193 ++++++++---------- src/panels/lovelace/hui-root.ts | 46 ++--- .../lovelace/views/hui-sections-view.ts | 10 +- .../lovelace/views/hui-view-container.ts | 17 +- src/resources/styles.ts | 9 +- src/resources/theme/color/color.globals.ts | 2 + src/resources/theme/main.globals.ts | 15 ++ 7 files changed, 123 insertions(+), 169 deletions(-) diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 0f1d3c14a8..ae036e45cd 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -36,7 +36,7 @@ import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar"; import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"; import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; -import { haStyle, haStyleScrollbar } from "../../../resources/styles"; +import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { isMac } from "../../../util/is_mac"; @@ -255,90 +255,88 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) { -
- - ${repairsIssues.length || canInstallUpdates.length - ? html` - ${repairsIssues.length - ? html` - - ${totalRepairIssues > repairsIssues.length - ? html` - - - ` - : ""} - ` - : ""} - ${repairsIssues.length && canInstallUpdates.length - ? html`
` - : ""} - ${canInstallUpdates.length - ? html` - - ${totalUpdates > canInstallUpdates.length - ? html` - - - ` - : ""} - ` - : ""} -
` - : ""} - ${this._pages( - this.cloudStatus, - isComponentLoaded(this.hass, "cloud"), - this.hass.auth.external?.config.hasSettingsScreen - ).map((categoryPages) => - categoryPages.length === 0 - ? nothing - : html` - - + ${repairsIssues.length || canInstallUpdates.length + ? html` + ${repairsIssues.length + ? html` + - - ` - )} - ${this._tip} -
-
+ .total=${totalRepairIssues} + .repairsIssues=${repairsIssues} + > + ${totalRepairIssues > repairsIssues.length + ? html` + + + ` + : ""} + ` + : ""} + ${repairsIssues.length && canInstallUpdates.length + ? html`
` + : ""} + ${canInstallUpdates.length + ? html` + + ${totalUpdates > canInstallUpdates.length + ? html` + + + ` + : ""} + ` + : ""} + ` + : ""} + ${this._pages( + this.cloudStatus, + isComponentLoaded(this.hass, "cloud"), + this.hass.auth.external?.config.hasSettingsScreen + ).map((categoryPages) => + categoryPages.length === 0 + ? nothing + : html` + + + + ` + )} + ${this._tip} + `; } @@ -394,36 +392,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) { static get styles(): CSSResultGroup { return [ haStyle, - haStyleScrollbar, css` - :host { - display: block; - height: 100%; - } - - ha-top-app-bar-fixed { - height: 100%; - overflow: hidden; - } - - .content { - height: calc( - 100vh - var(--header-height, 0px) - var( - --safe-area-inset-top, - 0px - ) - var(--safe-area-inset-bottom, 0px) - ); - height: calc( - 100dvh - var(--header-height, 0px) - var( - --safe-area-inset-top, - 0px - ) - var(--safe-area-inset-bottom, 0px) - ); - padding-bottom: var(--ha-space-5); - box-sizing: border-box; - overflow-x: hidden; - } - :host(:not([narrow])) ha-card:last-child { margin-bottom: 24px; } diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index ea7b442449..47f60d026d 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -631,11 +631,8 @@ class HUIRoot extends LitElement { `; } - private _handleContainerScroll = () => { - this.toggleAttribute( - "scrolled", - this._viewRoot ? this._viewRoot.scrollTop !== 0 : false - ); + private _handleWindowScroll = () => { + this.toggleAttribute("scrolled", window.scrollY !== 0); }; private _locationChanged = () => { @@ -666,7 +663,7 @@ class HUIRoot extends LitElement { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); - this._viewRoot?.addEventListener("scroll", this._handleContainerScroll, { + window.addEventListener("scroll", this._handleWindowScroll, { passive: true, }); this._handleUrlChanged(); @@ -677,7 +674,7 @@ class HUIRoot extends LitElement { public connectedCallback(): void { super.connectedCallback(); - this._viewRoot?.addEventListener("scroll", this._handleContainerScroll, { + window.addEventListener("scroll", this._handleWindowScroll, { passive: true, }); window.addEventListener("popstate", this._handlePopState); @@ -688,13 +685,10 @@ class HUIRoot extends LitElement { public disconnectedCallback(): void { super.disconnectedCallback(); - this._viewRoot?.removeEventListener("scroll", this._handleContainerScroll); + window.removeEventListener("scroll", this._handleWindowScroll); window.removeEventListener("popstate", this._handlePopState); window.removeEventListener("location-changed", this._locationChanged); - this.toggleAttribute( - "scrolled", - this._viewRoot ? this._viewRoot.scrollTop !== 0 : false - ); + this.toggleAttribute("scrolled", window.scrollY !== 0); // Re-enable history scroll restoration when leaving the page window.history.scrollRestoration = "auto"; } @@ -827,11 +821,9 @@ class HUIRoot extends LitElement { (this._restoreScroll && this._viewScrollPositions[newSelectView]) || 0; this._restoreScroll = false; - requestAnimationFrame(() => { - if (this._viewRoot) { - this._viewRoot.scrollTo({ behavior: "auto", top: position }); - } - }); + requestAnimationFrame(() => + scrollTo({ behavior: "auto", top: position }) + ); } this._selectView(newSelectView, force); }); @@ -1156,7 +1148,7 @@ class HUIRoot extends LitElement { const path = this.config.views[viewIndex].path || viewIndex; this._navigateToView(path); } else if (!this._editMode) { - this._viewRoot?.scrollTo({ behavior: "smooth", top: 0 }); + scrollTo({ behavior: "smooth", top: 0 }); } } @@ -1167,7 +1159,7 @@ class HUIRoot extends LitElement { // Save scroll position of current view if (this._curView != null) { - this._viewScrollPositions[this._curView] = this._viewRoot?.scrollTop ?? 0; + this._viewScrollPositions[this._curView] = window.scrollY; } viewIndex = viewIndex === undefined ? 0 : viewIndex; @@ -1475,14 +1467,9 @@ class HUIRoot extends LitElement { hui-view-container { position: relative; display: flex; - height: calc( - 100vh - var(--header-height) - var(--safe-area-inset-top) - var( - --view-container-padding-top, - 0px - ) - ); + min-height: 100vh; box-sizing: border-box; - margin-top: calc( + padding-top: calc( var(--header-height) + var(--safe-area-inset-top) + var(--view-container-padding-top, 0px) ); @@ -1505,12 +1492,7 @@ class HUIRoot extends LitElement { * In edit mode we have the tab bar on a new line * */ hui-view-container.has-tab-bar { - height: calc( - 100vh - var(--header-height, 56px) - calc( - var(--tab-bar-height, 56px) - 2px - ) - var(--safe-area-inset-top, 0px) - ); - margin-top: calc( + padding-top: calc( var(--header-height, 56px) + calc(var(--tab-bar-height, 56px) - 2px) + var(--safe-area-inset-top, 0px) diff --git a/src/panels/lovelace/views/hui-sections-view.ts b/src/panels/lovelace/views/hui-sections-view.ts index 30c77c8826..8cd23946cc 100644 --- a/src/panels/lovelace/views/hui-sections-view.ts +++ b/src/panels/lovelace/views/hui-sections-view.ts @@ -418,15 +418,11 @@ export class SectionsView extends LitElement implements LovelaceViewElement { } private _toggleView() { - // The scroll container is the hui-view-container parent - const scrollContainer = this.closest("hui-view-container"); - const scrollTop = scrollContainer?.scrollTop ?? 0; - // Save current scroll position if (this._sidebarTabActive) { - this._sidebarScrollTop = scrollTop; + this._sidebarScrollTop = window.scrollY; } else { - this._contentScrollTop = scrollTop; + this._contentScrollTop = window.scrollY; } this._sidebarTabActive = !this._sidebarTabActive; @@ -442,7 +438,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement { const scrollY = this._sidebarTabActive ? this._sidebarScrollTop : this._contentScrollTop; - scrollContainer?.scrollTo(0, scrollY); + window.scrollTo(0, scrollY); }); } diff --git a/src/panels/lovelace/views/hui-view-container.ts b/src/panels/lovelace/views/hui-view-container.ts index bb14605c45..8746d44c0d 100644 --- a/src/panels/lovelace/views/hui-view-container.ts +++ b/src/panels/lovelace/views/hui-view-container.ts @@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { listenMediaQuery } from "../../../common/dom/media_query"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; -import { haStyleScrollbar } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; type BackgroundConfig = LovelaceViewConfig["background"]; @@ -23,7 +22,6 @@ class HuiViewContainer extends LitElement { public connectedCallback(): void { super.connectedCallback(); - this.classList.add("ha-scrollbar"); this._setUpMediaQuery(); this._applyTheme(); } @@ -76,16 +74,11 @@ class HuiViewContainer extends LitElement { } } - static styles = [ - haStyleScrollbar, - css` - :host { - display: block; - height: 100%; - -webkit-overflow-scrolling: touch; - } - `, - ]; + static styles = css` + :host { + display: relative; + } + `; } declare global { diff --git a/src/resources/styles.ts b/src/resources/styles.ts index ff74bb6fba..00fa07185e 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -258,20 +258,17 @@ export const haStyleDialogFixedTop = css` `; export const haStyleScrollbar = css` - .ha-scrollbar::-webkit-scrollbar, - :host(.ha-scrollbar)::-webkit-scrollbar { + .ha-scrollbar::-webkit-scrollbar { width: 0.4rem; height: 0.4rem; } - .ha-scrollbar::-webkit-scrollbar-thumb, - :host(.ha-scrollbar)::-webkit-scrollbar-thumb { + .ha-scrollbar::-webkit-scrollbar-thumb { border-radius: var(--ha-border-radius-sm); background: var(--scrollbar-thumb-color); } - .ha-scrollbar, - :host(.ha-scrollbar) { + .ha-scrollbar { overflow-y: auto; scrollbar-color: var(--scrollbar-thumb-color) transparent; scrollbar-width: thin; diff --git a/src/resources/theme/color/color.globals.ts b/src/resources/theme/color/color.globals.ts index be1e2e85bb..374fa7bfd0 100644 --- a/src/resources/theme/color/color.globals.ts +++ b/src/resources/theme/color/color.globals.ts @@ -359,6 +359,8 @@ export const darkColorStyles = css` --outline-hover-color: rgba(225, 225, 225, 0.24); --shadow-color: rgba(0, 0, 0, 0.48); + --scrollbar-thumb-color: rgb(110, 110, 110); + --mdc-ripple-color: #aaaaaa; --mdc-linear-progress-buffer-color: rgba(255, 255, 255, 0.1); diff --git a/src/resources/theme/main.globals.ts b/src/resources/theme/main.globals.ts index 4e8ddb67e7..b79023396d 100644 --- a/src/resources/theme/main.globals.ts +++ b/src/resources/theme/main.globals.ts @@ -51,6 +51,21 @@ export const mainStyles = css` /* dialog backdrop filter */ --ha-dialog-scrim-backdrop-filter: brightness(68%); + + /* scrollbar */ + scrollbar-color: var(--scrollbar-thumb-color) transparent; + scrollbar-width: thin; + } + + html::-webkit-scrollbar { + width: 0.4rem; + height: 0.4rem; + } + + html::-webkit-scrollbar-thumb { + border-radius: var(--ha-border-radius-sm); + background: var(--scrollbar-thumb-color); + border: 3px solid transparent; } `; From 030a9a492c00b219c1c7b905e00575943cdf1b4c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 26 Feb 2026 16:56:33 +0100 Subject: [PATCH 12/69] Bumped version to 20260226.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b920ab0258..2ed349cca7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20260225.0" +version = "20260226.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 519d3d0e53315bebda0c5dd4f502d9b0d5a86510 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 2 Mar 2026 17:02:31 +0100 Subject: [PATCH 13/69] Fix data-table content bottom margin (#29805) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/data-table/ha-data-table.ts | 20 +++------ src/layouts/hass-tabs-subpage-data-table.ts | 46 +++++++++++++++++++-- src/layouts/hass-tabs-subpage.ts | 38 +++++++++-------- 3 files changed, 68 insertions(+), 36 deletions(-) diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 0846c1eb5d..3e350edf8a 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -118,8 +118,6 @@ export class HaDataTable extends LitElement { @property({ type: Boolean }) public clickable = false; - @property({ attribute: "has-fab", type: Boolean }) public hasFab = false; - /** * Add an extra row at the bottom of the data table * @type {TemplateResult} @@ -519,7 +517,6 @@ export class HaDataTable extends LitElement { this._filteredData, localize, this.appendRow, - this.hasFab, this.groupColumn, this.groupOrder, this._collapsedGroups, @@ -716,14 +713,13 @@ export class HaDataTable extends LitElement { data: DataTableRowData[], localize: LocalizeFunc, appendRow, - hasFab: boolean, groupColumn: string | undefined, groupOrder: string[] | undefined, collapsedGroups: string[], sortColumn: string | undefined, sortDirection: SortingDirection ) => { - if (appendRow || hasFab || groupColumn) { + if (appendRow || groupColumn) { let items = [...data]; if (groupColumn) { @@ -813,13 +809,11 @@ export class HaDataTable extends LitElement { items.push({ append: true, selectable: false, content: appendRow }); } - if (hasFab) { - items.push({ empty: true }); - } + items.push({ empty: true }); return items; } - return data; + return [...data, { empty: true }]; } ); @@ -871,7 +865,6 @@ export class HaDataTable extends LitElement { this._filteredData, this.localizeFunc || this.hass.localize, this.appendRow, - this.hasFab, this.groupColumn, this.groupOrder, this._collapsedGroups, @@ -1089,11 +1082,8 @@ export class HaDataTable extends LitElement { } .mdc-data-table__row.empty-row { - height: max( - var( - --data-table-empty-row-height, - var(--data-table-row-height, 52px) - ), + height: var( + --data-table-empty-row-height, var(--safe-area-inset-bottom, 0px) ); } diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index 76f494ba9f..27fec4832e 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -12,10 +12,11 @@ import { mdiUnfoldLessHorizontal, mdiUnfoldMoreHorizontal, } from "@mdi/js"; -import type { TemplateResult } from "lit"; +import type { TemplateResult, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import { canShowPage } from "../common/config/can_show_page"; import { fireEvent } from "../common/dom/fire_event"; import type { LocalizeFunc } from "../common/translations/localize"; import "../components/chips/ha-assist-chip"; @@ -83,7 +84,15 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { * Do we need to add padding for a fab. * @type {Boolean} */ - @property({ attribute: "has-fab", type: Boolean }) public hasFab = false; + @property({ attribute: "has-fab", type: Boolean, reflect: true }) + public hasFab = false; + + /** + * Show tabs on top or at bottom (narrow) of the page. + * @type {Boolean} + */ + @property({ attribute: "show-tabs", type: Boolean, reflect: true }) + public showTabs = false; /** * Add an extra row at the bottom of the data table @@ -200,7 +209,19 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { this._dataTable.clearSelection(); } - protected willUpdate() { + protected willUpdate(changedProperties: PropertyValues) { + if ( + changedProperties.has("tabs") || + (changedProperties.has("hass") && + (this.hass?.config.components !== + changedProperties.get("hass")?.config.components || + this.hass?.userData?.showAdvanced !== + changedProperties.get("hass")?.userData?.showAdvanced)) + ) { + this.showTabs = + this.tabs.filter((page) => canShowPage(this.hass, page)).length > 1; + } + if (this.hasUpdated) { return; } @@ -491,7 +512,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { .noDataText=${this.noDataText} .filter=${this.filter} .selectable=${this._selectMode} - .hasFab=${this.hasFab} .id=${this.id} .clickable=${this.clickable} .appendRow=${this.appendRow} @@ -713,6 +733,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { width: 100%; height: 100%; --data-table-border-width: 0; + --data-table-empty-row-height: var(--safe-area-inset-bottom, 0px); } :host(:not([narrow])) ha-data-table, .pane { @@ -725,6 +746,23 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { ); display: block; } + /* Last content row should keep the same padding above the fab as the fab + has to the bottom (16px standard fab bottom padding) + the safe-area inset. */ + :host([has-fab]) ha-data-table { + --data-table-empty-row-height: calc( + 48px + 16px * 2 + var(--safe-area-inset-bottom, 0px) + ); + } + /* In narrow view with tabs shown at the bottom, the tab bar already + accounts for safe-area-inset-bottom. No extra empty-row height is needed. */ + :host([narrow][show-tabs]:not([has-fab])) ha-data-table { + --data-table-empty-row-height: 0px; + } + /* Reserve space for fab + doubled narrow-mode bottom padding (28px * 2) + when using narrow layout with bottom tabs. */ + :host([narrow][show-tabs][has-fab]) ha-data-table { + --data-table-empty-row-height: calc(48px + 28px * 2); + } .pane-content { height: calc( diff --git a/src/layouts/hass-tabs-subpage.ts b/src/layouts/hass-tabs-subpage.ts index 88aba938f6..c917604d33 100644 --- a/src/layouts/hass-tabs-subpage.ts +++ b/src/layouts/hass-tabs-subpage.ts @@ -37,7 +37,7 @@ export interface PageNavigation { } @customElement("hass-tabs-subpage") -class HassTabsSubpage extends LitElement { +export class HassTabsSubpage extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public localizeFunc?: LocalizeFunc; @@ -65,6 +65,14 @@ class HassTabsSubpage extends LitElement { */ @property({ type: Boolean, attribute: "has-fab" }) public hasFab = false; + /** + * Whether tabs are shown (2 or more tabs visible). + * When both, show-tabs and narrow are true, tabs are shown as bottom bar. + * @type {Boolean} + */ + @property({ type: Boolean, attribute: "show-tabs", reflect: true }) + public showTabs = false; + @state() private _activeTab?: PageNavigation; // @ts-ignore @@ -83,6 +91,7 @@ class HassTabsSubpage extends LitElement { const shownTabs = tabs.filter((page) => canShowPage(this.hass, page)); if (shownTabs.length < 2) { + this.showTabs = false; if (shownTabs.length === 1) { const page = shownTabs[0]; return [ @@ -92,6 +101,7 @@ class HassTabsSubpage extends LitElement { return [""]; } + this.showTabs = true; return shownTabs.map( (page) => html` @@ -135,7 +145,6 @@ class HassTabsSubpage extends LitElement { this.narrow, this.localizeFunc || this.hass.localize ); - const showTabs = tabs.length > 1; return html`
@@ -160,12 +169,12 @@ class HassTabsSubpage extends LitElement { @click=${this._backTapped} > `} - ${this.narrow || !showTabs + ${this.narrow || !this.showTabs ? html`
- ${!showTabs ? tabs[0] : ""} + ${!this.showTabs ? tabs[0] : ""}
` : ""} - ${showTabs && !this.narrow + ${this.showTabs && !this.narrow ? html`
${tabs}
` : ""}
@@ -173,13 +182,11 @@ class HassTabsSubpage extends LitElement {
- ${showTabs && this.narrow + ${this.showTabs && this.narrow ? html`
${tabs}
` : ""}
-
+
${this.pane ? html`
@@ -188,15 +195,12 @@ class HassTabsSubpage extends LitElement {
` : nothing} -
+
${this.hasFab ? html`
` : nothing}
-
+
`; @@ -373,7 +377,7 @@ class HassTabsSubpage extends LitElement { margin-left: var(--safe-area-inset-left); margin-inline-start: var(--safe-area-inset-left); } - :host([narrow]) .content.tabs { + :host([narrow][show-tabs]) .content { /* Bottom bar reuses header height */ margin-bottom: calc( var(--header-height, 0px) + var(--safe-area-inset-bottom, 0px) @@ -384,7 +388,7 @@ class HassTabsSubpage extends LitElement { height: calc(64px + var(--safe-area-inset-bottom, 0px)); } - :host([narrow]) .content.tabs .fab-bottom-space { + :host([narrow][show-tabs]) .content .fab-bottom-space { height: calc(80px + var(--safe-area-inset-bottom, 0px)); } @@ -400,7 +404,7 @@ class HassTabsSubpage extends LitElement { justify-content: flex-end; gap: var(--ha-space-2); } - :host([narrow]) #fab.tabs { + :host([narrow][show-tabs]) #fab { bottom: calc(84px + var(--safe-area-inset-bottom, 0px)); } #fab[is-wide] { From 29ede122a12b7169288dce665e6ddd5b5daaae4b Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 27 Feb 2026 13:45:55 +0000 Subject: [PATCH 14/69] Add audits and yaml mode to more info details (#29854) * Add audits and yaml mode to more info details * Reset yaml mode on back * Use mapped array for state entries * Typo Co-authored-by: Bram Kragten * Memoize * Rename * Fix * Format audits in normal mode * Refactor, dont pass hass --------- Co-authored-by: Bram Kragten --- src/dialogs/more-info/ha-more-info-details.ts | 169 +++++++++++++----- src/dialogs/more-info/ha-more-info-dialog.ts | 24 ++- src/translations/en.json | 1 + 3 files changed, 148 insertions(+), 46 deletions(-) diff --git a/src/dialogs/more-info/ha-more-info-details.ts b/src/dialogs/more-info/ha-more-info-details.ts index 7022c0d7b6..7175fa97cd 100644 --- a/src/dialogs/more-info/ha-more-info-details.ts +++ b/src/dialogs/more-info/ha-more-info-details.ts @@ -2,17 +2,27 @@ import type { HassEntity } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display"; +import checkValidDate from "../../common/datetime/check_valid_date"; +import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import "../../components/ha-attribute-value"; import "../../components/ha-card"; +import type { LocalizeKeys } from "../../common/translations/localize"; import { computeShownAttributes } from "../../data/entity/entity_attributes"; import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry"; import type { HomeAssistant } from "../../types"; +import "../../components/ha-yaml-editor"; interface DetailsViewParams { entityId: string; } +interface DetailEntry { + translationKey: LocalizeKeys; + value: string; +} + @customElement("ha-more-info-details") class HaMoreInfoDetails extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -21,6 +31,8 @@ class HaMoreInfoDetails extends LitElement { @property({ attribute: false }) public params?: DetailsViewParams; + @property({ attribute: false }) public yamlMode = false; + @state() private _stateObj?: HassEntity; protected willUpdate(changedProps: PropertyValues): void { @@ -37,60 +49,127 @@ class HaMoreInfoDetails extends LitElement { return nothing; } - const translatedState = this.hass.formatEntityState(this._stateObj); - const detailsAttributes = computeShownAttributes(this._stateObj); - const detailsAttributeSet = new Set(detailsAttributes); - const builtInAttributes = Object.keys(this._stateObj.attributes).filter( - (attribute) => !detailsAttributeSet.has(attribute) + const { stateEntries, attributes, yamlData } = this._getDetailData( + this._stateObj ); - const allAttributes = [...detailsAttributes, ...builtInAttributes]; return html`
-
-

- ${this.hass.localize( - "ui.components.entity.entity-state-picker.state" - )} -

- -
-
-
-
- ${this.hass.localize( - "ui.dialogs.more_info_control.translated" - )} + ${this.yamlMode + ? html`` + : html` +
+

+ ${this.hass.localize( + "ui.components.entity.entity-state-picker.state" + )} +

+ +
+
+ ${stateEntries.map( + (entry) => + html`
+
+ ${this.hass.localize(entry.translationKey)} +
+
${entry.value}
+
` + )} +
-
${translatedState}
-
-
-
- ${this.hass.localize("ui.dialogs.more_info_control.raw")} -
-
${this._stateObj.state}
-
-
-
- -
+ + -
-

- ${this.hass.localize("ui.dialogs.more_info_control.attributes")} -

- -
-
- ${this._renderAttributes(allAttributes)} -
-
-
-
+
+

+ ${this.hass.localize( + "ui.dialogs.more_info_control.attributes" + )} +

+ +
+
+ ${this._renderAttributes(attributes)} +
+
+
+
+ `}
`; } + private _getDetailData = memoizeOne( + ( + stateObj: HassEntity + ): { + stateEntries: DetailEntry[]; + attributes: string[]; + yamlData: { + state: { + translated: string; + raw: string; + last_changed: string; + last_updated: string; + }; + attributes: Record; + }; + } => { + const translatedState = this.hass.formatEntityState(stateObj); + + const detailsAttributes = computeShownAttributes(stateObj); + const detailsAttributeSet = new Set(detailsAttributes); + const builtInAttributes = Object.keys(stateObj.attributes).filter( + (attribute) => !detailsAttributeSet.has(attribute) + ); + + return { + stateEntries: [ + { + translationKey: "ui.dialogs.more_info_control.translated", + value: translatedState, + }, + { + translationKey: "ui.dialogs.more_info_control.raw", + value: stateObj.state, + }, + { + translationKey: "ui.dialogs.more_info_control.last_changed", + value: this._formatTimestamp(stateObj.last_changed), + }, + { + translationKey: "ui.dialogs.more_info_control.last_updated", + value: this._formatTimestamp(stateObj.last_updated), + }, + ], + attributes: [...detailsAttributes, ...builtInAttributes], + yamlData: { + state: { + translated: translatedState, + raw: stateObj.state, + last_changed: stateObj.last_changed, + last_updated: stateObj.last_updated, + }, + attributes: stateObj.attributes, + }, + }; + } + ); + + private _formatTimestamp(value: string): string { + const date = new Date(value); + + return checkValidDate(date) + ? formatDateTimeWithSeconds(date, this.hass.locale, this.hass.config) + : value; + } + private _renderAttributes(attributes: string[]) { if (attributes.length === 0) { return html`
@@ -159,7 +238,7 @@ class HaMoreInfoDetails extends LitElement { border-bottom: 1px solid var(--divider-color); } - .attribute-group .data-entry:last-of-type { + .data-group .data-entry:last-of-type { border-bottom: none; } diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index cc461370c6..6b63bd81ee 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -1,6 +1,7 @@ import { mdiChartBoxOutline, mdiClose, + mdiCodeBraces, mdiCogOutline, mdiDevices, mdiDotsVertical, @@ -132,6 +133,8 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { @state() private _infoEditMode = false; + @state() private _detailsYamlMode = false; + @state() private _isEscapeEnabled = true; @state() private _sensorNumericDeviceClasses?: string[] = []; @@ -182,6 +185,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { this._parentEntityIds = []; this._entry = undefined; this._infoEditMode = false; + this._detailsYamlMode = false; this._initialView = DEFAULT_VIEW; this._currView = DEFAULT_VIEW; this._childView = undefined; @@ -251,6 +255,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { private _goBack() { if (this._childView) { this._childView = undefined; + this._detailsYamlMode = false; return; } if (this._initialView !== this._currView) { @@ -314,6 +319,10 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { this._infoEditMode = !this._infoEditMode; } + private _toggleDetailsYamlMode() { + this._detailsYamlMode = !this._detailsYamlMode; + } + private _handleToggleInfoEditModeEvent(ev) { this._infoEditMode = ev.detail; } @@ -637,7 +646,18 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { ` - : nothing} + : this._childView?.viewTag === "ha-more-info-details" + ? html` + + ` + : nothing}
` @@ -731,6 +752,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { if (changedProps.has("_currView")) { this._childView = undefined; this._infoEditMode = false; + this._detailsYamlMode = false; } } diff --git a/src/translations/en.json b/src/translations/en.json index 1f3764ed6f..3cdb0f8c62 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1524,6 +1524,7 @@ "settings": "Settings", "edit": "Edit entity", "details": "Details", + "toggle_yaml_mode": "Toggle YAML mode", "translated": "Translated", "raw": "Raw", "back_to_info": "Back to info", From fdbeb12622fa6b9b1f7e12515d23e89372ad593e Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 2 Mar 2026 17:00:34 +0100 Subject: [PATCH 15/69] Migrate Energy date selector to new footer (#29867) --- src/panels/energy/ha-panel-energy.ts | 79 +--------- .../energy-overview-view-strategy.ts | 15 +- .../energy/strategies/energy-view-strategy.ts | 145 +++++++++++------- .../energy/strategies/gas-view-strategy.ts | 24 ++- .../energy/strategies/water-view-strategy.ts | 27 +++- .../components/hui-energy-period-selector.ts | 2 +- src/panels/lovelace/views/hui-view-footer.ts | 5 +- 7 files changed, 145 insertions(+), 152 deletions(-) diff --git a/src/panels/energy/ha-panel-energy.ts b/src/panels/energy/ha-panel-energy.ts index 6f11aab754..8b1dd50ae2 100644 --- a/src/panels/energy/ha-panel-energy.ts +++ b/src/panels/energy/ha-panel-energy.ts @@ -1,7 +1,6 @@ import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; import { navigate } from "../../common/navigate"; import type { LocalizeKeys } from "../../common/translations/localize"; import "../../components/ha-alert"; @@ -11,13 +10,9 @@ import "../../components/ha-top-app-bar-fixed"; import type { EnergyPreferences } from "../../data/energy"; import { getEnergyDataCollection } from "../../data/energy"; import type { LovelaceConfig } from "../../data/lovelace/config/types"; -import { - isStrategyView, - type LovelaceViewConfig, -} from "../../data/lovelace/config/view"; +import type { LovelaceViewConfig } from "../../data/lovelace/config/view"; import { haStyle } from "../../resources/styles"; import type { HomeAssistant, PanelInfo } from "../../types"; -import "../lovelace/components/hui-energy-period-selector"; import "../lovelace/hui-root"; import type { Lovelace } from "../lovelace/types"; import "../lovelace/views/hui-view"; @@ -37,7 +32,6 @@ const OVERVIEW_VIEW = { strategy: { type: "energy-overview", collection_key: DEFAULT_ENERGY_COLLECTION_KEY, - show_period_selector: true, }, } as LovelaceViewConfig; @@ -46,7 +40,6 @@ const ENERGY_VIEW = { strategy: { type: "energy", collection_key: DEFAULT_ENERGY_COLLECTION_KEY, - show_period_selector: true, }, } as LovelaceViewConfig; @@ -55,7 +48,6 @@ const WATER_VIEW = { strategy: { type: "water", collection_key: DEFAULT_ENERGY_COLLECTION_KEY, - show_period_selector: true, }, } as LovelaceViewConfig; @@ -64,7 +56,6 @@ const GAS_VIEW = { strategy: { type: "gas", collection_key: DEFAULT_ENERGY_COLLECTION_KEY, - show_period_selector: true, }, } as LovelaceViewConfig; @@ -208,16 +199,6 @@ class PanelEnergy extends LitElement { return nothing; } - const routePath = this.route?.path?.split("/")[1] || ""; - const currentView = this._lovelace.config.views.find( - (view) => view.path === routePath - ); - - const showEnergySelector = - currentView && - isStrategyView(currentView) && - currentView.strategy?.show_period_selector; - return html` - ${showEnergySelector - ? html` - - - - ` - : nothing} `; } @@ -354,50 +321,6 @@ class PanelEnergy extends LitElement { align-items: center; justify-content: center; } - hui-root.has-period-selector { - --view-container-padding-bottom: var(--ha-space-18); - } - .period-selector { - position: fixed; - z-index: 4; - bottom: max(var(--ha-space-4), var(--safe-area-inset-bottom, 0px)); - left: max( - var(--mdc-drawer-width, 0px), - var(--safe-area-inset-left, 0px) - ); - right: var(--safe-area-inset-right, 0); - inset-inline-start: max( - var(--mdc-drawer-width, 0px), - var(--safe-area-inset-left, 0px) - ); - inset-inline-end: var(--safe-area-inset-right, 0); - transition: - left var(--ha-animation-duration-normal) ease, - right var(--ha-animation-duration-normal) ease, - inset-inline-start var(--ha-animation-duration-normal) ease, - inset-inline-end var(--ha-animation-duration-normal) ease; - margin: 0 auto; - max-width: calc(min(470px, 100% - var(--ha-space-4))); - box-sizing: border-box; - padding-left: var(--ha-space-2); - padding-right: 0; - padding-inline-start: var(--ha-space-4); - padding-inline-end: 0; - --ha-card-box-shadow: - 0px 3px 5px -1px rgba(0, 0, 0, 0.2), - 0px 6px 10px 0px rgba(0, 0, 0, 0.14), - 0px 1px 18px 0px rgba(0, 0, 0, 0.12); - --ha-card-border-color: var(--divider-color); - --ha-card-border-width: var(--ha-card-border-width, 1px); - } - @media all and (max-width: 450px), all and (max-height: 500px) { - hui-root.has-period-selector { - --view-container-padding-bottom: var(--ha-space-14); - } - .period-selector { - bottom: max(var(--ha-space-2), var(--safe-area-inset-bottom, 0px)); - } - } `, ]; } diff --git a/src/panels/energy/strategies/energy-overview-view-strategy.ts b/src/panels/energy/strategies/energy-overview-view-strategy.ts index ff4be6179f..74e4d7aa76 100644 --- a/src/panels/energy/strategies/energy-overview-view-strategy.ts +++ b/src/panels/energy/strategies/energy-overview-view-strategy.ts @@ -13,16 +13,23 @@ export class EnergyOverviewViewStrategy extends ReactiveElement { _config: LovelaceStrategyConfig, hass: HomeAssistant ): Promise { + const collectionKey = + _config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY; + const view: LovelaceViewConfig = { type: "sections", sections: [], dense_section_placement: true, - max_columns: 2, + max_columns: 3, + footer: { + column_span: 1.1, + card: { + type: "energy-date-selection", + collection_key: collectionKey, + }, + }, }; - const collectionKey = - _config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY; - const energyCollection = getEnergyDataCollection(hass, { key: collectionKey, }); diff --git a/src/panels/energy/strategies/energy-view-strategy.ts b/src/panels/energy/strategies/energy-view-strategy.ts index a602534f75..5b518c34bc 100644 --- a/src/panels/energy/strategies/energy-view-strategy.ts +++ b/src/panels/energy/strategies/energy-view-strategy.ts @@ -2,6 +2,7 @@ import { ReactiveElement } from "lit"; import { customElement } from "lit/decorators"; import type { GridSourceTypeEnergyPreference } from "../../../data/energy"; import { getEnergyDataCollection } from "../../../data/energy"; +import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; @@ -14,11 +15,22 @@ export class EnergyViewStrategy extends ReactiveElement { _config: LovelaceStrategyConfig, hass: HomeAssistant ): Promise { - const view: LovelaceViewConfig = { cards: [] }; - const collectionKey = _config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY; + const view: LovelaceViewConfig = { + type: "sections", + max_columns: 3, + sections: [], + footer: { + column_span: 1.1, + card: { + type: "energy-date-selection", + collection_key: collectionKey, + }, + }, + }; + const energyCollection = getEnergyDataCollection(hass, { key: collectionKey, }); @@ -36,8 +48,6 @@ export class EnergyViewStrategy extends ReactiveElement { return view; } - view.type = "sidebar"; - const hasGrid = prefs.energy_sources.find( (source): source is GridSourceTypeEnergyPreference => source.type === "grid" && @@ -50,83 +60,95 @@ export class EnergyViewStrategy extends ReactiveElement { const hasBattery = prefs.energy_sources.some( (source) => source.type === "battery" ); - view.cards!.push({ + + const mainCards: LovelaceCardConfig[] = []; + const gaugeCards: LovelaceCardConfig[] = []; + + // Only include if we have a grid source & return. + if (hasReturn) { + const card = { + type: "energy-grid-neutrality-gauge", + collection_key: collectionKey, + }; + gaugeCards.push(card); + } + + // Only include if we have a solar source. + if (hasSolar) { + if (hasReturn) { + const card = { + type: "energy-solar-consumed-gauge", + collection_key: collectionKey, + }; + gaugeCards.push(card); + } + if (hasGrid) { + const card = { + type: "energy-self-sufficiency-gauge", + collection_key: collectionKey, + }; + gaugeCards.push(card); + } + } + + // Only include if we have a grid + if (hasGrid) { + const card = { + type: "energy-carbon-consumed-gauge", + collection_key: collectionKey, + }; + gaugeCards.push(card); + } + + if (gaugeCards.length) { + view.sections!.push({ + type: "grid", + column_span: 3, + cards: + gaugeCards.length === 1 + ? [gaugeCards[0]] + : gaugeCards.map((card) => ({ + ...card, + grid_options: { columns: 6 }, + })), + }); + } + + mainCards.push({ type: "energy-compare", collection_key: collectionKey, + grid_options: { columns: 36 }, }); // Only include if we have a grid or battery. if (hasGrid || hasBattery) { - view.cards!.push({ + mainCards.push({ title: hass.localize("ui.panel.energy.cards.energy_usage_graph_title"), type: "energy-usage-graph", collection_key: collectionKey, + grid_options: { columns: 36 }, }); } // Only include if we have a solar source. if (hasSolar) { - view.cards!.push({ + mainCards.push({ title: hass.localize("ui.panel.energy.cards.energy_solar_graph_title"), type: "energy-solar-graph", collection_key: collectionKey, - }); - } - - // Only include if we have a grid or battery. - if (hasGrid || hasBattery) { - view.cards!.push({ - title: hass.localize("ui.panel.energy.cards.energy_distribution_title"), - type: "energy-distribution", - view_layout: { position: "sidebar" }, - collection_key: collectionKey, + grid_options: { columns: 36 }, }); } if (hasGrid || hasSolar || hasBattery) { - view.cards!.push({ + mainCards.push({ title: hass.localize( "ui.panel.energy.cards.energy_sources_table_title" ), type: "energy-sources-table", collection_key: collectionKey, types: ["grid", "solar", "battery"], - }); - } - - // Only include if we have a grid source & return. - if (hasReturn) { - view.cards!.push({ - type: "energy-grid-neutrality-gauge", - view_layout: { position: "sidebar" }, - collection_key: collectionKey, - }); - } - - // Only include if we have a solar source. - if (hasSolar) { - if (hasReturn) { - view.cards!.push({ - type: "energy-solar-consumed-gauge", - view_layout: { position: "sidebar" }, - collection_key: collectionKey, - }); - } - if (hasGrid) { - view.cards!.push({ - type: "energy-self-sufficiency-gauge", - view_layout: { position: "sidebar" }, - collection_key: collectionKey, - }); - } - } - - // Only include if we have a grid - if (hasGrid) { - view.cards!.push({ - type: "energy-carbon-consumed-gauge", - view_layout: { position: "sidebar" }, - collection_key: collectionKey, + grid_options: { columns: 36 }, }); } @@ -137,29 +159,38 @@ export class EnergyViewStrategy extends ReactiveElement { hass, (d) => d.stat_consumption ); - view.cards!.push({ + mainCards.push({ title: hass.localize( "ui.panel.energy.cards.energy_devices_detail_graph_title" ), type: "energy-devices-detail-graph", collection_key: collectionKey, + grid_options: { columns: 36 }, }); - view.cards!.push({ + mainCards.push({ title: hass.localize( "ui.panel.energy.cards.energy_devices_graph_title" ), type: "energy-devices-graph", collection_key: collectionKey, + grid_options: { columns: 36 }, }); - view.cards!.push({ + mainCards.push({ title: hass.localize("ui.panel.energy.cards.energy_sankey_title"), type: "energy-sankey", collection_key: collectionKey, group_by_floor: showFloorsAndAreas, group_by_area: showFloorsAndAreas, + grid_options: { columns: 36 }, }); } + view.sections!.push({ + type: "grid", + column_span: 3, + cards: mainCards, + }); + return view; } } diff --git a/src/panels/energy/strategies/gas-view-strategy.ts b/src/panels/energy/strategies/gas-view-strategy.ts index 754018170d..1c97b5a677 100644 --- a/src/panels/energy/strategies/gas-view-strategy.ts +++ b/src/panels/energy/strategies/gas-view-strategy.ts @@ -13,14 +13,22 @@ export class GasViewStrategy extends ReactiveElement { _config: LovelaceStrategyConfig, hass: HomeAssistant ): Promise { - const view: LovelaceViewConfig = { - type: "sections", - sections: [{ type: "grid", cards: [] }], - }; - const collectionKey = _config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY; + const view: LovelaceViewConfig = { + type: "sections", + max_columns: 3, + sections: [{ type: "grid", cards: [], column_span: 3 }], + footer: { + column_span: 1.1, + card: { + type: "energy-date-selection", + collection_key: collectionKey, + }, + }, + }; + const energyCollection = getEnergyDataCollection(hass, { key: collectionKey, }); @@ -49,6 +57,9 @@ export class GasViewStrategy extends ReactiveElement { title: hass.localize("ui.panel.energy.cards.energy_gas_graph_title"), type: "energy-gas-graph", collection_key: collectionKey, + grid_options: { + columns: 24, + }, }); section.cards!.push({ @@ -56,6 +67,9 @@ export class GasViewStrategy extends ReactiveElement { type: "energy-sources-table", collection_key: collectionKey, types: ["gas"], + grid_options: { + columns: 12, + }, }); return view; diff --git a/src/panels/energy/strategies/water-view-strategy.ts b/src/panels/energy/strategies/water-view-strategy.ts index 058a5e01a4..7579e169d3 100644 --- a/src/panels/energy/strategies/water-view-strategy.ts +++ b/src/panels/energy/strategies/water-view-strategy.ts @@ -14,14 +14,22 @@ export class WaterViewStrategy extends ReactiveElement { _config: LovelaceStrategyConfig, hass: HomeAssistant ): Promise { - const view: LovelaceViewConfig = { - type: "sections", - sections: [{ type: "grid", cards: [] }], - }; - const collectionKey = _config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY; + const view: LovelaceViewConfig = { + type: "sections", + max_columns: 3, + sections: [{ type: "grid", cards: [], column_span: 3 }], + footer: { + column_span: 1.1, + card: { + type: "energy-date-selection", + collection_key: collectionKey, + }, + }, + }; + const energyCollection = getEnergyDataCollection(hass, { key: collectionKey, }); @@ -52,6 +60,9 @@ export class WaterViewStrategy extends ReactiveElement { title: hass.localize("ui.panel.energy.cards.energy_water_graph_title"), type: "energy-water-graph", collection_key: collectionKey, + grid_options: { + columns: 24, + }, }); section.cards!.push({ title: hass.localize( @@ -60,6 +71,9 @@ export class WaterViewStrategy extends ReactiveElement { type: "energy-sources-table", collection_key: collectionKey, types: ["water"], + grid_options: { + columns: 12, + }, }); } @@ -76,6 +90,9 @@ export class WaterViewStrategy extends ReactiveElement { collection_key: collectionKey, group_by_floor: showFloorsAndAreas, group_by_area: showFloorsAndAreas, + grid_options: { + columns: 24, + }, }); } diff --git a/src/panels/lovelace/components/hui-energy-period-selector.ts b/src/panels/lovelace/components/hui-energy-period-selector.ts index 4580a9550e..ce45c682ec 100644 --- a/src/panels/lovelace/components/hui-energy-period-selector.ts +++ b/src/panels/lovelace/components/hui-energy-period-selector.ts @@ -125,7 +125,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { } private _measure() { - this.narrow = this.offsetWidth < 450; + this.narrow = this.offsetWidth < 425; this._collapseButtons = this.offsetWidth < 320; } diff --git a/src/panels/lovelace/views/hui-view-footer.ts b/src/panels/lovelace/views/hui-view-footer.ts index 952c702355..02d2a447dc 100644 --- a/src/panels/lovelace/views/hui-view-footer.ts +++ b/src/panels/lovelace/views/hui-view-footer.ts @@ -241,8 +241,9 @@ export class HuiViewFooter extends LitElement { box-sizing: content-box; margin: 0 auto; max-width: calc( - var(--footer-column-span, 1) * var(--column-max-width, 500px) + - (var(--footer-column-span, 1) - 1) * var(--column-gap, 32px) + var(--footer-column-span, 1) / var(--column-count, 1) * 100% + + (var(--footer-column-span, 1) - var(--column-count, 1)) / + var(--column-count, 1) * var(--column-gap, 32px) ); } From d695c4c845b69239e05429e00a71379fe49a7306 Mon Sep 17 00:00:00 2001 From: Brandon Chen Date: Fri, 27 Feb 2026 16:11:28 +0800 Subject: [PATCH 16/69] =?UTF-8?q?Fix=20YAML=20content=20invisible=20in=20d?= =?UTF-8?q?ark=20mode=20for=20conversation=20debug=20result=E2=80=A6=20(#2?= =?UTF-8?q?9874)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voice-assistants/debug/assist-render-pipeline-run.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/panels/config/voice-assistants/debug/assist-render-pipeline-run.ts b/src/panels/config/voice-assistants/debug/assist-render-pipeline-run.ts index 9a6d798390..506b1d18cf 100644 --- a/src/panels/config/voice-assistants/debug/assist-render-pipeline-run.ts +++ b/src/panels/config/voice-assistants/debug/assist-render-pipeline-run.ts @@ -670,10 +670,10 @@ export class AssistPipelineDebug extends LitElement { background-color: var(--light-primary-color); color: var(--text-light-primary-color, var(--primary-text-color)); direction: var(--direction); - --primary-text-color: var( - --text-light-primary-color, - var(--primary-text-color) - ); + } + + .tool_result [slot="header"] { + color: var(--text-light-primary-color, var(--primary-text-color)); } .message.user, From b1ceece2245643ac43d089a3ffddd24eb5eca8f2 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 27 Feb 2026 11:18:49 +0100 Subject: [PATCH 17/69] Revert "Add vacuum mapping not configured issue" (#29876) --- src/panels/config/repairs/ha-config-repairs.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/panels/config/repairs/ha-config-repairs.ts b/src/panels/config/repairs/ha-config-repairs.ts index c64d7e980a..778ec31955 100644 --- a/src/panels/config/repairs/ha-config-repairs.ts +++ b/src/panels/config/repairs/ha-config-repairs.ts @@ -143,8 +143,7 @@ class HaConfigRepairs extends LitElement { } } else if ( issue.domain === "vacuum" && - (issue.translation_key === "segments_changed" || - issue.translation_key === "segments_mapping_not_configured") + issue.translation_key === "segments_changed" ) { const data = await fetchRepairsIssueData( this.hass.connection, From 616c3d4657357e529fdb1d43044c12fab915326f Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 27 Feb 2026 11:46:10 +0000 Subject: [PATCH 18/69] Use large width on system log dialogs (#29879) --- src/panels/config/logs/dialog-system-log-detail.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/config/logs/dialog-system-log-detail.ts b/src/panels/config/logs/dialog-system-log-detail.ts index 532eac2760..f1f65886e7 100644 --- a/src/panels/config/logs/dialog-system-log-detail.ts +++ b/src/panels/config/logs/dialog-system-log-detail.ts @@ -86,6 +86,7 @@ class DialogSystemLogDetail extends LitElement { ${title} From c9f96bbe69129221f3c2e712da67eb7beb4ba01d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 27 Feb 2026 16:23:58 +0100 Subject: [PATCH 19/69] Add render icon property to ha-control-select-menu (#29881) --- src/components/ha-control-select-menu.ts | 48 ++++++---------- .../more-info/controls/more-info-climate.ts | 57 ++++++++++++------- .../more-info/controls/more-info-fan.ts | 32 ++++++----- .../controls/more-info-humidifier.ts | 16 +++--- .../more-info/controls/more-info-light.ts | 16 +++--- .../controls/more-info-water_heater.ts | 14 +++-- .../hui-climate-fan-modes-card-feature.ts | 18 +++--- .../hui-climate-preset-modes-card-feature.ts | 18 +++--- ...ate-swing-horizontal-modes-card-feature.ts | 18 +++--- .../hui-climate-swing-modes-card-feature.ts | 18 +++--- .../hui-fan-preset-modes-card-feature.ts | 18 +++--- .../hui-humidifier-modes-card-feature.ts | 18 +++--- ...ter-heater-operation-modes-card-feature.ts | 18 +++--- 13 files changed, 168 insertions(+), 141 deletions(-) diff --git a/src/components/ha-control-select-menu.ts b/src/components/ha-control-select-menu.ts index 3dc78ef957..7659675391 100644 --- a/src/components/ha-control-select-menu.ts +++ b/src/components/ha-control-select-menu.ts @@ -1,11 +1,9 @@ import { mdiMenuDown } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; +import type { TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; -import type { HomeAssistant } from "../types"; -import "./ha-attribute-icon"; import "./ha-dropdown"; import "./ha-dropdown-item"; import "./ha-icon"; @@ -16,17 +14,10 @@ export interface SelectOption { value: string; iconPath?: string; icon?: string; - attributeIcon?: { - stateObj: HassEntity; - attribute: string; - attributeValue?: string; - }; } @customElement("ha-control-select-menu") export class HaControlSelectMenu extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean, attribute: "show-arrow" }) public showArrow = false; @@ -47,6 +38,9 @@ export class HaControlSelectMenu extends LitElement { @property({ attribute: false }) public options: SelectOption[] = []; + @property({ attribute: false }) + public renderIcon?: (value: string) => TemplateResult<1> | typeof nothing; + @query("button") private _triggerButton!: HTMLButtonElement; public override render() { @@ -94,14 +88,8 @@ export class HaControlSelectMenu extends LitElement { ? html`` : option.icon ? html`` - : option.attributeIcon - ? html`` + : this.renderIcon + ? html`${this.renderIcon(option.value)}` : nothing} ${option.label}`; @@ -119,24 +107,20 @@ export class HaControlSelectMenu extends LitElement { } private _renderIcon() { - const { iconPath, icon, attributeIcon } = - this.getValueObject(this.options, this.value) ?? {}; + const value = this.getValueObject(this.options, this.value); const defaultIcon = this.querySelector("[slot='icon']"); return html`
- ${iconPath - ? html`` - : icon - ? html`` - : attributeIcon - ? html`` + ${value?.iconPath + ? html`` + : value?.icon + ? html`` + : this.renderIcon && this.value + ? this.renderIcon(this.value) : defaultIcon ? html`` : nothing} diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts index 79a153e773..01552bc7fd 100644 --- a/src/dialogs/more-info/controls/more-info-climate.ts +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -9,6 +9,7 @@ import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { supportsFeature } from "../../../common/entity/supports-feature"; +import "../../../components/ha-attribute-icon"; import "../../../components/ha-control-select-menu"; import "../../../components/ha-icon-button-group"; import "../../../components/ha-icon-button-toggle"; @@ -39,6 +40,38 @@ class MoreInfoClimate extends LitElement { @state() private _mainControl: MainControl = "temperature"; + private _renderPresetModeIcon = (value: string) => + html``; + + private _renderFanModeIcon = (value: string) => + html``; + + private _renderSwingModeIcon = (value: string) => + html``; + + private _renderSwingHorizontalModeIcon = (value: string) => + html``; + protected willUpdate(changedProps: PropertyValues): void { if ( changedProps.has("stateObj") && @@ -205,12 +238,8 @@ class MoreInfoClimate extends LitElement { "preset_mode", mode ), - attributeIcon: { - stateObj, - attribute: "preset_mode", - attributeValue: mode, - }, }))} + .renderIcon=${this._renderPresetModeIcon} > @@ -234,12 +263,8 @@ class MoreInfoClimate extends LitElement { "fan_mode", mode ), - attributeIcon: { - stateObj, - attribute: "fan_mode", - attributeValue: mode, - }, }))} + .renderIcon=${this._renderFanModeIcon} > @@ -263,12 +288,8 @@ class MoreInfoClimate extends LitElement { "swing_mode", mode ), - attributeIcon: { - stateObj, - attribute: "swing_mode", - attributeValue: mode, - }, }))} + .renderIcon=${this._renderSwingModeIcon} > + html``; + + private _renderDirectionIcon = (value: string) => + html``; + private _toggle = () => { const service = this.stateObj?.state === "on" ? "turn_off" : "turn_on"; forwardHaptic(this, "light"); @@ -192,15 +208,9 @@ class MoreInfoFan extends LitElement { "preset_mode", mode ), - attributeIcon: this.stateObj - ? { - stateObj: this.stateObj, - attribute: "preset_mode", - attributeValue: mode, - } - : undefined, }) )} + .renderIcon=${this._renderPresetModeIcon} > @@ -226,14 +236,8 @@ class MoreInfoFan extends LitElement { direction ) : direction, - attributeIcon: this.stateObj - ? { - stateObj: this.stateObj, - attribute: "direction", - attributeValue: direction, - } - : undefined, }))} + .renderIcon=${this._renderDirectionIcon} > + html``; + protected willUpdate(changedProps: PropertyValues): void { super.willUpdate(changedProps); if (changedProps.has("stateObj")) { @@ -106,14 +114,8 @@ class MoreInfoHumidifier extends LitElement { mode ) : mode, - attributeIcon: stateObj - ? { - stateObj, - attribute: "mode", - attributeValue: mode, - } - : undefined, })) || []} + .renderIcon=${this._renderModeIcon} > diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index 9379c86a24..3035829276 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -55,6 +55,14 @@ class MoreInfoLight extends LitElement { @state() private _mainControl: MainControl = "brightness"; + private _renderEffectIcon = (value: string) => + html``; + protected updated(changedProps: PropertyValues): void { if (changedProps.has("stateObj")) { this._effect = this.stateObj?.attributes.effect; @@ -271,15 +279,9 @@ class MoreInfoLight extends LitElement { effect ) : effect, - attributeIcon: this.stateObj - ? { - stateObj: this.stateObj, - attribute: "effect", - attributeValue: effect, - } - : undefined, }) )} + .renderIcon=${this._renderEffectIcon} > diff --git a/src/dialogs/more-info/controls/more-info-water_heater.ts b/src/dialogs/more-info/controls/more-info-water_heater.ts index d9d1a0543e..09ba21a381 100644 --- a/src/dialogs/more-info/controls/more-info-water_heater.ts +++ b/src/dialogs/more-info/controls/more-info-water_heater.ts @@ -24,6 +24,14 @@ class MoreInfoWaterHeater extends LitElement { @property({ attribute: false }) public stateObj?: WaterHeaterEntity; + private _renderOperationModeIcon = (value: string) => + html``; + protected render() { if (!this.stateObj) { return nothing; @@ -85,12 +93,8 @@ class MoreInfoWaterHeater extends LitElement { .map((mode) => ({ value: mode, label: this.hass.formatEntityState(stateObj, mode), - attributeIcon: { - stateObj, - attribute: "operation_mode", - attributeValue: mode, - }, }))} + .renderIcon=${this._renderOperationModeIcon} > diff --git a/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts index 1a7259f12f..fc03ed3fbd 100644 --- a/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts @@ -49,6 +49,14 @@ class HuiClimateFanModesCardFeature @state() _currentFanMode?: string; + private _renderFanModeIcon = (value: string) => + html``; + private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -175,14 +183,8 @@ class HuiClimateFanModesCardFeature .value=${this._currentFanMode} .disabled=${this._stateObj.state === UNAVAILABLE} @wa-select=${this._valueChanged} - .options=${options.map((option) => ({ - ...option, - attributeIcon: { - stateObj: stateObj, - attribute: "fan_mode", - attributeValue: option.value, - }, - }))} + .options=${options} + .renderIcon=${this._renderFanModeIcon} > `; diff --git a/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts index d13941c40d..df92172317 100644 --- a/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts @@ -48,6 +48,14 @@ class HuiClimatePresetModesCardFeature @state() _currentPresetMode?: string; + private _renderPresetModeIcon = (value: string) => + html``; + private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -179,14 +187,8 @@ class HuiClimatePresetModesCardFeature .value=${this._currentPresetMode} .disabled=${this._stateObj.state === UNAVAILABLE} @wa-select=${this._valueChanged} - .options=${options.map((option) => ({ - ...option, - attributeIcon: { - stateObj: stateObj, - attribute: "preset_mode", - attributeValue: option.value, - }, - }))} + .options=${options} + .renderIcon=${this._renderPresetModeIcon} > diff --git a/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts index cfc0386575..946dd32f95 100644 --- a/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts @@ -48,6 +48,14 @@ class HuiClimateSwingHorizontalModesCardFeature @state() _currentSwingHorizontalMode?: string; + private _renderSwingHorizontalModeIcon = (value: string) => + html``; + private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -187,14 +195,8 @@ class HuiClimateSwingHorizontalModesCardFeature .value=${this._currentSwingHorizontalMode} .disabled=${this._stateObj.state === UNAVAILABLE} @wa-select=${this._valueChanged} - .options=${options.map((option) => ({ - ...option, - attributeIcon: { - stateObj: stateObj, - attribute: "swing_horizontal_mode", - attributeValue: option.value, - }, - }))} + .options=${options} + .renderIcon=${this._renderSwingHorizontalModeIcon} > diff --git a/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts index 5cc902591c..4b2c9d5ded 100644 --- a/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts @@ -48,6 +48,14 @@ class HuiClimateSwingModesCardFeature @state() _currentSwingMode?: string; + private _renderSwingModeIcon = (value: string) => + html``; + private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -179,14 +187,8 @@ class HuiClimateSwingModesCardFeature .value=${this._currentSwingMode} .disabled=${this._stateObj.state === UNAVAILABLE} @wa-select=${this._valueChanged} - .options=${options.map((option) => ({ - ...option, - attributeIcon: { - stateObj, - attribute: "swing_mode", - attributeValue: option.value, - }, - }))} + .options=${options} + .renderIcon=${this._renderSwingModeIcon} > `; diff --git a/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts index 2f71cadec3..b63acb8aae 100644 --- a/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts @@ -47,6 +47,14 @@ class HuiFanPresetModesCardFeature @state() _currentPresetMode?: string; + private _renderPresetModeIcon = (value: string) => + html``; + private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -173,14 +181,8 @@ class HuiFanPresetModesCardFeature .value=${this._currentPresetMode} .disabled=${this._stateObj.state === UNAVAILABLE} @wa-select=${this._valueChanged} - .options=${options.map((option) => ({ - ...option, - attributeIcon: { - stateObj: stateObj, - attribute: "preset_mode", - attributeValue: option.value, - }, - }))} + .options=${options} + .renderIcon=${this._renderPresetModeIcon} > diff --git a/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts index 07a10e7848..f65b7582b9 100644 --- a/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts @@ -48,6 +48,14 @@ class HuiHumidifierModesCardFeature @state() _currentMode?: string; + private _renderModeIcon = (value: string) => + html``; + private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -174,14 +182,8 @@ class HuiHumidifierModesCardFeature .value=${this._currentMode} .disabled=${this._stateObj.state === UNAVAILABLE} @wa-select=${this._valueChanged} - .options=${options.map((option) => ({ - ...option, - attributeIcon: { - stateObj, - attribute: "mode", - attributeValue: option.value, - }, - }))} + .options=${options} + .renderIcon=${this._renderModeIcon} > diff --git a/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts index 8a9f554b88..7b007ab28e 100644 --- a/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts @@ -49,6 +49,14 @@ class HuiWaterHeaterOperationModeCardFeature @state() _currentOperationMode?: OperationMode; + private _renderOperationModeIcon = (value: string) => + html``; + private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -153,14 +161,8 @@ class HuiWaterHeaterOperationModeCardFeature .value=${this._currentOperationMode} .disabled=${this._stateObj.state === UNAVAILABLE} @wa-select=${this._valueChanged} - .options=${options.map((option) => ({ - ...option, - attributeIcon: { - stateObj: this._stateObj, - attribute: "operation_mode", - attributeValue: option.value, - }, - }))} + .options=${options} + .renderIcon=${this._renderOperationModeIcon} > From 1e72ad1411f9c3673e1355d38fa7880982416bd5 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 2 Mar 2026 16:01:13 +0000 Subject: [PATCH 20/69] Code editor fullscreen in dialogs (#29882) Co-authored-by: Bram Kragten --- src/components/ha-code-editor.ts | 36 +++++++++++-- src/components/ha-dialog.ts | 51 ++++++++++++++++++- src/components/ha-yaml-editor.ts | 11 ++++ src/dialogs/more-info/ha-more-info-details.ts | 1 + src/dialogs/more-info/ha-more-info-dialog.ts | 30 +++++++++++ .../badge-editor/hui-dialog-edit-badge.ts | 5 +- .../badge-editor/hui-dialog-suggest-badge.ts | 1 + .../card-editor/hui-dialog-edit-card.ts | 5 +- .../card-editor/hui-dialog-suggest-card.ts | 1 + .../lovelace/editor/hui-element-editor.ts | 7 +++ .../section-editor/hui-dialog-edit-section.ts | 1 + .../view-editor/hui-dialog-edit-view.ts | 1 + .../hui-dialog-edit-view-header.ts | 1 + 13 files changed, 144 insertions(+), 7 deletions(-) diff --git a/src/components/ha-code-editor.ts b/src/components/ha-code-editor.ts index 052f01a1a3..988f32856b 100644 --- a/src/components/ha-code-editor.ts +++ b/src/components/ha-code-editor.ts @@ -84,6 +84,9 @@ export class HaCodeEditor extends ReactiveElement { @property({ type: Boolean, attribute: "disable-fullscreen" }) public disableFullscreen = false; + @property({ type: Boolean, attribute: "in-dialog" }) + public inDialog = false; + @property({ type: Boolean, attribute: "has-toolbar" }) public hasToolbar = true; @@ -132,6 +135,7 @@ export class HaCodeEditor extends ReactiveElement { public connectedCallback() { super.connectedCallback(); + this.classList.toggle("in-dialog", this.inDialog); // Force update on reconnection so editor is recreated if (this.hasUpdated) { this.requestUpdate(); @@ -150,6 +154,7 @@ export class HaCodeEditor extends ReactiveElement { } public disconnectedCallback() { + fireEvent(this, "dialog-set-fullscreen", false); super.disconnectedCallback(); this.removeEventListener("keydown", stopPropagation); this.removeEventListener("keydown", this._handleKeyDown); @@ -216,6 +221,9 @@ export class HaCodeEditor extends ReactiveElement { if (changedProps.has("error")) { this.classList.toggle("error-state", this.error); } + if (changedProps.has("inDialog")) { + this.classList.toggle("in-dialog", this.inDialog); + } if (changedProps.has("_isFullscreen")) { this.classList.toggle("fullscreen", this._isFullscreen); this._updateToolbarButtons(); @@ -434,10 +442,19 @@ export class HaCodeEditor extends ReactiveElement { private _updateFullscreenState( fullscreen: boolean = this._isFullscreen ): boolean { + const previousFullscreen = this._isFullscreen; + + this.classList.toggle("in-dialog", this.inDialog); + // Update the current fullscreen state based on selected value. If fullscreen // is disabled, or we have no toolbar, ensure we are not in fullscreen mode. this._isFullscreen = fullscreen && !this.disableFullscreen && this.hasToolbar; + + if (previousFullscreen !== this._isFullscreen) { + fireEvent(this, "dialog-set-fullscreen", this._isFullscreen); + } + // Return whether successfully in requested state return this._isFullscreen === fullscreen; } @@ -846,10 +863,10 @@ export class HaCodeEditor extends ReactiveElement { :host(.fullscreen) { position: fixed !important; - top: calc(var(--header-height, 56px) + 8px) !important; - left: 8px !important; - right: 8px !important; - bottom: 8px !important; + top: calc(var(--header-height, 56px) + var(--ha-space-2)) !important; + left: var(--ha-space-2) !important; + right: var(--ha-space-2) !important; + bottom: var(--ha-space-2) !important; z-index: 6; border-radius: var(--ha-border-radius-lg) !important; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important; @@ -867,6 +884,17 @@ export class HaCodeEditor extends ReactiveElement { display: block !important; } + :host(.in-dialog.fullscreen) { + position: absolute !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + padding: 0 !important; + } + :host(.hasToolbar) .cm-editor { padding-top: var(--code-editor-toolbar-height); } diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index 3c0aabd1e3..9755e49e62 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -10,7 +10,9 @@ import { state, } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; +import type { HASSDomEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event"; +import { withViewTransition } from "../common/util/view-transition"; import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin"; import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant } from "../types"; @@ -127,6 +129,14 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) { private _escapePressed = false; + public connectedCallback(): void { + super.connectedCallback(); + this.addEventListener( + "dialog-set-fullscreen", + this._handleFullscreenChanged as EventListener + ); + } + protected get scrollableElement(): HTMLElement | null { return this.bodyContainer; } @@ -227,15 +237,36 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) { private _handleAfterHide = (ev: DialogHideEvent) => { if (ev.eventPhase === Event.AT_TARGET) { this._open = false; + this._setFullscreen(false); fireEvent(this, "closed"); } }; public disconnectedCallback(): void { + this.removeEventListener( + "dialog-set-fullscreen", + this._handleFullscreenChanged as EventListener + ); + this._setFullscreen(false); super.disconnectedCallback(); this._open = false; } + private _handleFullscreenChanged(ev: HASSDomEvent): void { + if (!this._open) { + this._setFullscreen(ev.detail); + return; + } + + withViewTransition(() => { + this._setFullscreen(ev.detail); + }); + } + + private _setFullscreen(fullscreen: boolean): void { + this.toggleAttribute("fullscreen", fullscreen); + } + @eventOptions({ passive: true }) private _handleBodyScroll(ev: Event) { this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0; @@ -301,10 +332,27 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) { --width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width)); } - :host([width="full"]) wa-dialog { + :host([width="full"]) wa-dialog, + :host([fullscreen]) wa-dialog { --width: var(--full-width); } + :host([fullscreen]) wa-dialog::part(dialog) { + min-height: var(--safe-height); + max-height: var(--safe-height); + margin-top: 0; + transform: none; + } + + :host([fullscreen]) .content-wrapper { + overflow: hidden; + } + + :host([fullscreen]) .body { + overflow: hidden; + padding: 0; + } + wa-dialog::part(dialog) { -webkit-backdrop-filter: var( --ha-dialog-surface-backdrop-filter, @@ -465,6 +513,7 @@ declare global { } interface HASSDomEvents { + "dialog-set-fullscreen": boolean; opened: undefined; "after-show": undefined; closed: undefined; diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts index 4d922ad7e5..72336bdb30 100644 --- a/src/components/ha-yaml-editor.ts +++ b/src/components/ha-yaml-editor.ts @@ -47,6 +47,9 @@ export class HaYamlEditor extends LitElement { @property({ type: Boolean, attribute: "disable-fullscreen" }) public disableFullscreen = false; + @property({ type: Boolean, attribute: "in-dialog" }) + public inDialog = false; + @property({ type: Boolean }) public required = false; @property({ attribute: "copy-clipboard", type: Boolean }) @@ -101,6 +104,13 @@ export class HaYamlEditor extends LitElement { } } + public disableCodeEditorFullscreen(): void { + this.disableFullscreen = true; + if (this._codeEditor) { + this._codeEditor.disableFullscreen = true; + } + } + protected render() { if (this._yaml === undefined) { return nothing; @@ -114,6 +124,7 @@ export class HaYamlEditor extends LitElement { .value=${this._yaml} .readOnly=${this.readOnly} .disableFullscreen=${this.disableFullscreen} + .inDialog=${this.inDialog} mode="yaml" autocomplete-entities autocomplete-icons diff --git a/src/dialogs/more-info/ha-more-info-details.ts b/src/dialogs/more-info/ha-more-info-details.ts index 7175fa97cd..fbe6d7fc1b 100644 --- a/src/dialogs/more-info/ha-more-info-details.ts +++ b/src/dialogs/more-info/ha-more-info-details.ts @@ -61,6 +61,7 @@ class HaMoreInfoDetails extends LitElement { .value=${yamlData} read-only auto-update + in-dialog >` : html`
diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index 6b63bd81ee..da8a41b210 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -119,6 +119,8 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { @query(".content") private _contentElement?: HTMLDivElement; + @query("ha-adaptive-dialog") private _dialogElement?: HTMLElement; + @state() private _entityId?: string | null; @state() private _data?: Record; @@ -177,6 +179,10 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { } public closeDialog() { + const dialog = this._dialogElement?.shadowRoot?.querySelector("ha-dialog"); + if (dialog) { + fireEvent(dialog as HTMLElement, "dialog-set-fullscreen", false); + } this._open = false; } @@ -254,6 +260,11 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { private _goBack() { if (this._childView) { + const dialog = + this._dialogElement?.shadowRoot?.querySelector("ha-dialog"); + if (dialog) { + fireEvent(dialog as HTMLElement, "dialog-set-fullscreen", false); + } this._childView = undefined; this._detailsYamlMode = false; return; @@ -320,6 +331,10 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { } private _toggleDetailsYamlMode() { + const dialog = this._dialogElement?.shadowRoot?.querySelector("ha-dialog"); + if (dialog) { + fireEvent(dialog as HTMLElement, "dialog-set-fullscreen", false); + } this._detailsYamlMode = !this._detailsYamlMode; } @@ -749,6 +764,21 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { protected updated(changedProps: PropertyValues) { super.updated(changedProps); + const previousChildView = changedProps.get("_childView") as + | ChildView + | undefined; + + if ( + previousChildView?.viewTag === "ha-more-info-details" && + this._childView?.viewTag !== "ha-more-info-details" + ) { + const dialog = + this._dialogElement?.shadowRoot?.querySelector("ha-dialog"); + if (dialog) { + fireEvent(dialog as HTMLElement, "dialog-set-fullscreen", false); + } + } + if (changedProps.has("_currView")) { this._childView = undefined; this._infoEditMode = false; diff --git a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts index 37b42605b2..1c7bf0f22d 100644 --- a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts +++ b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts @@ -226,6 +226,7 @@ export class HuiDialogEditBadge .hass=${this.hass} .lovelace=${this._params.lovelaceConfig} .value=${this._badgeConfig} + in-dialog @config-changed=${this._handleConfigChanged} @GUImode-changed=${this._handleGUIModeChanged} @editor-save=${this._save} @@ -314,7 +315,9 @@ export class HuiDialogEditBadge } private _toggleMode(): void { - this._badgeEditorEl?.toggleMode(); + withViewTransition(() => { + this._badgeEditorEl?.toggleMode(); + }); } private _opened() { diff --git a/src/panels/lovelace/editor/badge-editor/hui-dialog-suggest-badge.ts b/src/panels/lovelace/editor/badge-editor/hui-dialog-suggest-badge.ts index 9d8b8df370..20cfffd388 100644 --- a/src/panels/lovelace/editor/badge-editor/hui-dialog-suggest-badge.ts +++ b/src/panels/lovelace/editor/badge-editor/hui-dialog-suggest-badge.ts @@ -97,6 +97,7 @@ export class HuiDialogSuggestBadge extends LitElement {
` diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts index e081c0e7a3..2449842c72 100644 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts @@ -203,6 +203,7 @@ export class HuiDialogEditCard .hass=${this.hass} .lovelace=${this._params.lovelaceConfig} .value=${this._cardConfig} + in-dialog @config-changed=${this._handleConfigChanged} @GUImode-changed=${this._handleGUIModeChanged} @editor-save=${this._save} @@ -297,7 +298,9 @@ export class HuiDialogEditCard } private _toggleMode(): void { - this._cardEditorEl?.toggleMode(); + withViewTransition(() => { + this._cardEditorEl?.toggleMode(); + }); } private _opened() { diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts index d990c78b25..7cd20d08be 100644 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts @@ -133,6 +133,7 @@ export class HuiDialogSuggestCard extends LitElement {
` diff --git a/src/panels/lovelace/editor/hui-element-editor.ts b/src/panels/lovelace/editor/hui-element-editor.ts index 28b53e7924..a725e5c401 100644 --- a/src/panels/lovelace/editor/hui-element-editor.ts +++ b/src/panels/lovelace/editor/hui-element-editor.ts @@ -57,6 +57,9 @@ export abstract class HuiElementEditor< @property({ attribute: false }) public context?: C; + @property({ type: Boolean, attribute: "in-dialog" }) + public inDialog = false; + @state() private _config?: T; @state() private _configElement?: LovelaceGenericElementEditor; @@ -150,6 +153,9 @@ export abstract class HuiElementEditor< } public toggleMode() { + if (!this.GUImode) { + this._yamlEditor?.disableCodeEditorFullscreen(); + } this.GUImode = !this.GUImode; } @@ -243,6 +249,7 @@ export abstract class HuiElementEditor< .defaultValue=${this._config} autofocus .hass=${this.hass} + .inDialog=${this.inDialog} @value-changed=${this._handleYAMLChanged} @blur=${this._onBlurYaml} @keydown=${this._ignoreKeydown} diff --git a/src/panels/lovelace/editor/section-editor/hui-dialog-edit-section.ts b/src/panels/lovelace/editor/section-editor/hui-dialog-edit-section.ts index fd7c7fa185..c0b4d7c6d5 100644 --- a/src/panels/lovelace/editor/section-editor/hui-dialog-edit-section.ts +++ b/src/panels/lovelace/editor/section-editor/hui-dialog-edit-section.ts @@ -127,6 +127,7 @@ export class HuiDialogEditSection `; diff --git a/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts index bec99e4999..d17c948715 100644 --- a/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts +++ b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts @@ -162,6 +162,7 @@ export class HuiDialogEditView extends LitElement { `; diff --git a/src/panels/lovelace/editor/view-header/hui-dialog-edit-view-header.ts b/src/panels/lovelace/editor/view-header/hui-dialog-edit-view-header.ts index 2129a18a32..a93c03d16d 100644 --- a/src/panels/lovelace/editor/view-header/hui-dialog-edit-view-header.ts +++ b/src/panels/lovelace/editor/view-header/hui-dialog-edit-view-header.ts @@ -84,6 +84,7 @@ export class HuiDialogEditViewHeader extends LitElement { `; From 8f5059c24a9f4c3e48b3dc75808f84b1a398abad Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 27 Feb 2026 14:37:52 +0100 Subject: [PATCH 21/69] Fix energy compare tooltip showing wrong year (#29885) --- .../lovelace/cards/energy/common/energy-chart-options.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts index ebb83472d0..745a9d2e19 100644 --- a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts +++ b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts @@ -218,7 +218,9 @@ function formatTooltip( } // when comparing the first value is offset to match the main period // and the real date is in the third value - const date = new Date(params[0].value?.[2] ?? params[0].value?.[0]); + // find the first param with the real date to handle gap-filled entries + const origDate = params.find((p) => p.value?.[2] != null)?.value?.[2]; + const date = new Date(origDate ?? params[0].value?.[0]); let period: string; if (suggestedPeriod === "month") { From aff1fedc9d7ca76574588f1c8bfbd1fb07dae313 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 27 Feb 2026 14:59:14 +0100 Subject: [PATCH 22/69] Fix monetary device class state display with non-ISO 4217 currency symbols (#29887) --- src/common/entity/compute_state_display.ts | 43 +++++++++++----------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 16ab18ff4a..d875dc4b48 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -133,33 +133,34 @@ const computeStateToPartsFromEntityAttributes = ( ), }); } catch (_err) { - // fallback to default + // fallback to default numeric formatting below } - const TYPE_MAP: Record = { - integer: "value", - group: "value", - decimal: "value", - fraction: "value", - literal: "literal", - currency: "unit", - }; + if (parts.length) { + const TYPE_MAP: Record = { + integer: "value", + group: "value", + decimal: "value", + fraction: "value", + literal: "literal", + currency: "unit", + }; - const valueParts: ValuePart[] = []; + const valueParts: ValuePart[] = []; - for (const part of parts) { - const type = TYPE_MAP[part.type]; - if (!type) continue; - const last = valueParts[valueParts.length - 1]; - // Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56") - if (type === "value" && last?.type === "value") { - last.value += part.value; - } else { - valueParts.push({ type, value: part.value }); + for (const part of parts) { + const type = TYPE_MAP[part.type]; + if (!type) continue; + const last = valueParts[valueParts.length - 1]; + // Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56") + if (type === "value" && last?.type === "value") { + last.value += part.value; + } else { + valueParts.push({ type, value: part.value }); + } } + return valueParts; } - - return valueParts; } // default processing of numeric values From 7b8884f0fd7d4efe0ab1356051beebf433df6dcc Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 27 Feb 2026 16:41:54 +0100 Subject: [PATCH 23/69] Fix sensor card graph not updating when value is unchanged (#29889) --- .../header-footer/hui-graph-header-footer.ts | 83 ++++++++++++++----- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts index b289f1a343..543b8aec4f 100644 --- a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts +++ b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts @@ -6,6 +6,7 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeDomain } from "../../../common/entity/compute_domain"; import "../../../components/ha-spinner"; +import type { HistoryStates } from "../../../data/history"; import { subscribeHistoryStatesTimeWindow } from "../../../data/history"; import type { HomeAssistant } from "../../../types"; import { findEntities } from "../common/find-entities"; @@ -66,6 +67,8 @@ export class HuiGraphHeaderFooter private _error?: string; + private _history?: HistoryStates; + private _interval?: number; private _subscribed?: Promise<(() => Promise) | undefined>; @@ -161,24 +164,8 @@ export class HuiGraphHeaderFooter // Message came in before we had a chance to unload return; } - const width = this.clientWidth || this.offsetWidth; - // sample to 1 point per hour or 1 point per 5 pixels - const maxDetails = Math.max( - 10, - this._config.detail! > 1 - ? Math.max(width / 5, this._config.hours_to_show!) - : this._config.hours_to_show! - ); - const useMean = this._config.detail !== 2; - const { points } = coordinatesMinimalResponseCompressedState( - combinedHistory[this._config.entity], - width, - width / 5, - maxDetails, - { minY: this._config.limits?.min, maxY: this._config.limits?.max }, - useMean - ); - this._coordinates = points; + this._history = combinedHistory; + this._computeCoordinates(); }, this._config.hours_to_show!, [this._config.entity] @@ -190,10 +177,63 @@ export class HuiGraphHeaderFooter this._setRedrawTimer(); } - private _redrawGraph() { - if (this._coordinates) { - this._coordinates = [...this._coordinates]; + private _computeCoordinates() { + if (!this._history || !this._config) { + return; } + const entityHistory = this._history[this._config.entity]; + if (!entityHistory?.length) { + return; + } + const width = this.clientWidth || this.offsetWidth; + // sample to 1 point per hour or 1 point per 5 pixels + const maxDetails = Math.max( + 10, + this._config.detail! > 1 + ? Math.max(width / 5, this._config.hours_to_show!) + : this._config.hours_to_show! + ); + const useMean = this._config.detail !== 2; + const { points } = coordinatesMinimalResponseCompressedState( + entityHistory, + width, + width / 5, + maxDetails, + { minY: this._config.limits?.min, maxY: this._config.limits?.max }, + useMean + ); + this._coordinates = points; + } + + private _redrawGraph() { + if (!this._history || !this._config?.hours_to_show) { + return; + } + const entityId = this._config.entity; + const entityHistory = this._history[entityId]; + if (entityHistory?.length) { + const purgeBeforeTimestamp = + (Date.now() - this._config.hours_to_show * 60 * 60 * 1000) / 1000; + let purgedHistory = entityHistory.filter( + (entry) => entry.lu >= purgeBeforeTimestamp + ); + if (purgedHistory.length !== entityHistory.length) { + if ( + !purgedHistory.length || + purgedHistory[0].lu !== purgeBeforeTimestamp + ) { + // Preserve the last expired state as the start boundary + const lastExpiredState = { + ...entityHistory[entityHistory.length - purgedHistory.length - 1], + }; + lastExpiredState.lu = purgeBeforeTimestamp; + delete lastExpiredState.lc; + purgedHistory = [lastExpiredState, ...purgedHistory]; + } + this._history = { ...this._history, [entityId]: purgedHistory }; + } + } + this._computeCoordinates(); } private _setRedrawTimer() { @@ -211,6 +251,7 @@ export class HuiGraphHeaderFooter this._subscribed.then((unsub) => unsub?.()); this._subscribed = undefined; } + this._history = undefined; } protected updated(changedProps: PropertyValues) { From b2eb8ec968c58d2a9d0647378d7889cbac3a8736 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 27 Feb 2026 16:39:21 +0100 Subject: [PATCH 24/69] Make hui-sections-view always fill the screen so footer is at the bottom (#29890) --- src/panels/lovelace/hui-root.ts | 2 ++ src/panels/lovelace/views/hui-sections-view.ts | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 47f60d026d..ab5689fba0 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -1485,6 +1485,8 @@ class HUIRoot extends LitElement { padding-inline-start: var(--safe-area-inset-left); } hui-view-container > * { + display: flex; + flex-direction: column; flex: 1 1 100%; max-width: 100%; } diff --git a/src/panels/lovelace/views/hui-sections-view.ts b/src/panels/lovelace/views/hui-sections-view.ts index 8cd23946cc..a763285183 100644 --- a/src/panels/lovelace/views/hui-sections-view.ts +++ b/src/panels/lovelace/views/hui-sections-view.ts @@ -461,6 +461,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement { --column-min-width: var(--ha-view-sections-column-min-width, 320px); --top-margin: var(--ha-view-sections-extra-top-margin, 80px); display: block; + flex: 1; } @media (max-width: 600px) { @@ -470,7 +471,9 @@ export class SectionsView extends LitElement implements LovelaceViewElement { } .wrapper { - display: block; + display: flex; + flex-direction: column; + min-height: calc(100% - 2 * var(--row-gap)); padding: var(--row-gap) var(--column-gap); box-sizing: content-box; margin: 0 auto; @@ -503,6 +506,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement { gap: var(--row-gap) var(--column-gap); padding: var(--row-gap) 0; align-items: flex-start; + flex: 1 0 auto; } .wrapper.has-sidebar .container { From ad1d1e2260df52bbcf0966afcefa970a2f54f21f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 27 Feb 2026 16:44:21 +0100 Subject: [PATCH 25/69] Fix overflow for icon buttons (#29891) --- src/components/ha-button.ts | 2 +- src/components/ha-icon-button.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ha-button.ts b/src/components/ha-button.ts index 186f9be1e9..d9a6e82505 100644 --- a/src/components/ha-button.ts +++ b/src/components/ha-button.ts @@ -245,7 +245,7 @@ export class HaButton extends Button { } .label { - overflow: hidden; + overflow: var(--ha-button-label-overflow, hidden); text-overflow: ellipsis; padding: var(--ha-space-1) 0; } diff --git a/src/components/ha-icon-button.ts b/src/components/ha-icon-button.ts index 1624c53be8..4dbc22eb34 100644 --- a/src/components/ha-icon-button.ts +++ b/src/components/ha-icon-button.ts @@ -74,6 +74,7 @@ export class HaIconButton extends LitElement { ); --wa-color-on-normal: currentColor; --wa-color-fill-quiet: transparent; + --ha-button-label-overflow: visible; } ha-button::after { content: ""; From 38d02a3f304e95bc44b0a04c36a67888c50ecdab Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 27 Feb 2026 17:26:04 +0100 Subject: [PATCH 26/69] Fix control select menu color in ios (#29892) --- src/components/ha-control-select-menu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-control-select-menu.ts b/src/components/ha-control-select-menu.ts index 7659675391..86bb5eeb8e 100644 --- a/src/components/ha-control-select-menu.ts +++ b/src/components/ha-control-select-menu.ts @@ -156,12 +156,12 @@ export class HaControlSelectMenu extends LitElement { font-size: var(--ha-font-size-m); line-height: 1.4; width: auto; - color: var(--primary-text-color); -webkit-tap-highlight-color: transparent; } .select-anchor { border: none; text-align: left; + color: var(--primary-text-color); height: var(--control-select-menu-height); padding: var(--control-select-menu-padding); overflow: hidden; From c3cc566fe3e2ddce52c772bce5ae2d6125b7465f Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:06:10 -0800 Subject: [PATCH 27/69] Fix distribution card stub error (#29915) * Fix distribution card stub error * unit check not required --- src/panels/lovelace/cards/hui-distribution-card.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/cards/hui-distribution-card.ts b/src/panels/lovelace/cards/hui-distribution-card.ts index 5b2e43e520..30cf512ed4 100644 --- a/src/panels/lovelace/cards/hui-distribution-card.ts +++ b/src/panels/lovelace/cards/hui-distribution-card.ts @@ -68,9 +68,9 @@ export class HuiDistributionCard // Strategy 1: Try to find power sensors (W, kW) - most common use case const powerFilter = (stateObj: HassEntity): boolean => { - const unit = stateObj.attributes.unit_of_measurement; const stateValue = Number(stateObj.state); - return (unit === "W" || unit === "kW") && !isNaN(stateValue); + const deviceClass = stateObj.attributes.device_class; + return deviceClass === "power" && !isNaN(stateValue); }; let foundEntities = findEntities( From 67ccfa0f6e23cfd67ba83ea428bace0ec89993d6 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:49:46 +0100 Subject: [PATCH 28/69] Add error translation for loading energy preferences (#29924) --- src/panels/energy/ha-panel-energy.ts | 4 +++- src/translations/en.json | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/panels/energy/ha-panel-energy.ts b/src/panels/energy/ha-panel-energy.ts index 8b1dd50ae2..eca00a476d 100644 --- a/src/panels/energy/ha-panel-energy.ts +++ b/src/panels/energy/ha-panel-energy.ts @@ -180,7 +180,9 @@ class PanelEnergy extends LitElement { return html`
- An error occurred loading energy preferences: ${this._error} + ${this.hass.localize("ui.panel.energy.error_loading_preferences", { + error: this._error, + })}
`; diff --git a/src/translations/en.json b/src/translations/en.json index 3cdb0f8c62..6c1fc050f6 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -10337,6 +10337,7 @@ } }, "energy": { + "error_loading_preferences": "An error occurred loading energy preferences: {error}", "title": { "overview": "Summary", "electricity": "Electricity", From 852caa32be7d6af8e7ee451d9f747bd6a980b1f1 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 2 Mar 2026 09:49:19 +0000 Subject: [PATCH 29/69] Remove cache to fix re-add repo issue (#29926) Remove cache to fix readd repo issue --- .../config/apps/dialogs/repositories/dialog-repositories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/apps/dialogs/repositories/dialog-repositories.ts b/src/panels/config/apps/dialogs/repositories/dialog-repositories.ts index 2a20ced557..af7d191855 100644 --- a/src/panels/config/apps/dialogs/repositories/dialog-repositories.ts +++ b/src/panels/config/apps/dialogs/repositories/dialog-repositories.ts @@ -35,7 +35,7 @@ import type { RepositoryDialogParams } from "./show-dialog-repositories"; class AppsRepositoriesDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @query("#repository_input", true) private _optionInput?: HaTextField; + @query("#repository_input") private _optionInput?: HaTextField; @state() private _repositories?: HassioAddonRepository[]; From 640f2b9245e8e6b4c3bb68dd91ec279d213d8221 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:41:52 +0100 Subject: [PATCH 30/69] Dialog: Add show event target check (#29927) Add event phase check in _handleShow and _handleAfterShow methods --- src/components/ha-dialog.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index 9755e49e62..f094ab1b04 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -204,7 +204,10 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) { `; } - private _handleShow = async () => { + private _handleShow = async (ev: Event) => { + if (ev.eventPhase !== Event.AT_TARGET) { + return; + } this._open = true; fireEvent(this, "opened"); @@ -230,7 +233,10 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) { }); }; - private _handleAfterShow = () => { + private _handleAfterShow = (ev: Event) => { + if (ev.eventPhase !== Event.AT_TARGET) { + return; + } fireEvent(this, "after-show"); }; From 457c51cf58cc333345330f137949c8432039fd40 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 2 Mar 2026 14:19:26 +0100 Subject: [PATCH 31/69] Fix sidebar not closing when reduced motion is enabled (#29934) --- src/components/ha-drawer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/ha-drawer.ts b/src/components/ha-drawer.ts index 72b69ca8b5..bfd2ac3d9e 100644 --- a/src/components/ha-drawer.ts +++ b/src/components/ha-drawer.ts @@ -186,9 +186,11 @@ export class HaDrawer extends DrawerBase { padding-inline-start var(--ha-animation-duration-normal) ease; } @media (prefers-reduced-motion: reduce) { + /* Use 1ms instead of "none" so the transitionend event still fires. + The MDC drawer foundation relies on it to complete the close cycle. */ .mdc-drawer, .mdc-drawer-app-content { - transition: none; + transition: 1ms; } } `, From 465c10b945c397853a185fc405c2eaaae234c3d0 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 2 Mar 2026 14:50:42 +0100 Subject: [PATCH 32/69] Fix updates, discovered devices and repairs cards flickering (#29935) --- src/panels/lovelace/cards/hui-discovered-devices-card.ts | 5 +++-- src/panels/lovelace/cards/hui-repairs-card.ts | 5 +++-- src/panels/lovelace/cards/hui-updates-card.ts | 7 ++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/panels/lovelace/cards/hui-discovered-devices-card.ts b/src/panels/lovelace/cards/hui-discovered-devices-card.ts index 4236958124..0579eecaad 100644 --- a/src/panels/lovelace/cards/hui-discovered-devices-card.ts +++ b/src/panels/lovelace/cards/hui-discovered-devices-card.ts @@ -131,9 +131,10 @@ export class HuiDiscoveredDevicesCard } // Update visibility based on admin status and discovered devices count - const shouldBeHidden = + const shouldBeHidden = Boolean( !this.hass.user?.is_admin || - (this._config.hide_empty && this._discoveredFlows.length === 0); + (this._config.hide_empty && this._discoveredFlows.length === 0) + ); if (shouldBeHidden !== this.hidden) { this.style.display = shouldBeHidden ? "none" : ""; diff --git a/src/panels/lovelace/cards/hui-repairs-card.ts b/src/panels/lovelace/cards/hui-repairs-card.ts index 3c9607540e..a5ee3ab9df 100644 --- a/src/panels/lovelace/cards/hui-repairs-card.ts +++ b/src/panels/lovelace/cards/hui-repairs-card.ts @@ -97,9 +97,10 @@ export class HuiRepairsCard } // Update visibility based on admin status and repairs count - const shouldBeHidden = + const shouldBeHidden = Boolean( !this.hass.user?.is_admin || - (this._config.hide_empty && this._repairsIssues.length === 0); + (this._config.hide_empty && this._repairsIssues.length === 0) + ); if (shouldBeHidden !== this.hidden) { this.style.display = shouldBeHidden ? "none" : ""; diff --git a/src/panels/lovelace/cards/hui-updates-card.ts b/src/panels/lovelace/cards/hui-updates-card.ts index 6231dc5dae..a9aad310b2 100644 --- a/src/panels/lovelace/cards/hui-updates-card.ts +++ b/src/panels/lovelace/cards/hui-updates-card.ts @@ -91,9 +91,10 @@ export class HuiUpdatesCard extends LitElement implements LovelaceCard { const updateEntities = this._getUpdateEntities(); // Update visibility based on admin status and updates count - const shouldBeHidden = + const shouldBeHidden = Boolean( !this.hass.user?.is_admin || - (this._config.hide_empty && updateEntities.length === 0); + (this._config.hide_empty && updateEntities.length === 0) + ); if (shouldBeHidden !== this.hidden) { this.style.display = shouldBeHidden ? "none" : ""; @@ -103,7 +104,7 @@ export class HuiUpdatesCard extends LitElement implements LovelaceCard { } protected render(): TemplateResult | typeof nothing { - if (!this._config || !this.hass || this.hidden) { + if (!this._config || !this.hass) { return nothing; } From 15de13759102847133e65a9da23f633b2037a516 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 2 Mar 2026 17:08:01 +0100 Subject: [PATCH 33/69] Bumped version to 20260302.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2ed349cca7..d7c62e9217 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20260226.0" +version = "20260302.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From ab4c3a431615c675a4d8e768a4c49f987b258fd9 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:22:06 +0100 Subject: [PATCH 34/69] ha-authorize fix rtl check (#29937) Add RTL direction handling in updated lifecycle method --- src/mixins/lit-localize-lite-mixin.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/mixins/lit-localize-lite-mixin.ts b/src/mixins/lit-localize-lite-mixin.ts index 2f2b4b9094..7e4225e726 100644 --- a/src/mixins/lit-localize-lite-mixin.ts +++ b/src/mixins/lit-localize-lite-mixin.ts @@ -2,10 +2,10 @@ import type { LitElement, PropertyValues } from "lit"; import { property, state } from "lit/decorators"; import type { LocalizeFunc } from "../common/translations/localize"; import { computeLocalize } from "../common/translations/localize"; +import { computeDirectionStyles } from "../common/util/compute_rtl"; +import { translationMetadata } from "../resources/translations-metadata"; import type { Constructor, Resources } from "../types"; import { getLocalLanguage, getTranslation } from "../util/common-translation"; -import { translationMetadata } from "../resources/translations-metadata"; -import { computeDirectionStyles } from "../common/util/compute_rtl"; const empty = () => ""; @@ -28,16 +28,16 @@ export const litLocalizeLiteMixin = >( this._initializeLocalizeLite(); } - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - computeDirectionStyles( - translationMetadata.translations[this.language!].isRTL, - this - ); - } - protected willUpdate(changedProperties: PropertyValues) { super.willUpdate(changedProperties); + + if (!this.updated || changedProperties.has("language")) { + computeDirectionStyles( + translationMetadata.translations[this.language!].isRTL, + this + ); + } + if (changedProperties.get("language")) { this._resources = undefined; this._initializeLocalizeLite(); From b74b02c09fb9697cdbe8036a84d4627ab916857c Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 3 Mar 2026 11:53:54 +0100 Subject: [PATCH 35/69] Use net battery power in power sankey card (#29940) --- .../cards/energy/hui-power-sankey-card.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts b/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts index a375015844..997461d6c7 100644 --- a/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts +++ b/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts @@ -610,18 +610,21 @@ class HuiPowerSankeyCard }); // Collect battery power (positive = discharge, negative = charge) + // Sum all battery values first, then determine net direction. + // Momentary power should only flow in one direction across all batteries. + let net_battery = 0; prefs.energy_sources .filter((source) => source.type === "battery") .forEach((source) => { if (source.type === "battery" && source.stat_rate) { - const value = this._getCurrentPower(source.stat_rate); - if (value > 0) { - from_battery += value; - } else if (value < 0) { - to_battery += Math.abs(value); - } + net_battery += this._getCurrentPower(source.stat_rate); } }); + if (net_battery > 0) { + from_battery = net_battery; + } else if (net_battery < 0) { + to_battery = Math.abs(net_battery); + } // Calculate total consumption const used_total = from_grid + solar + from_battery - to_grid - to_battery; From 2f2e64bb1d94a9a94ac4982464ceac9d0709832d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 3 Mar 2026 14:57:57 +0100 Subject: [PATCH 36/69] Use max width for dashboard footer (#29947) --- src/data/lovelace/config/view.ts | 4 +- .../energy-overview-view-strategy.ts | 1 - .../energy/strategies/energy-view-strategy.ts | 1 - .../energy/strategies/gas-view-strategy.ts | 1 - .../energy/strategies/water-view-strategy.ts | 1 - .../hui-dialog-edit-view-footer.ts | 3 +- .../hui-view-footer-settings-editor.ts | 56 +++++++------------ .../show-edit-view-footer-dialog.ts | 1 - src/panels/lovelace/views/hui-view-footer.ts | 34 ++++------- src/translations/en.json | 3 +- 10 files changed, 37 insertions(+), 68 deletions(-) diff --git a/src/data/lovelace/config/view.ts b/src/data/lovelace/config/view.ts index b4ef3dfc4e..0c82d4755b 100644 --- a/src/data/lovelace/config/view.ts +++ b/src/data/lovelace/config/view.ts @@ -37,9 +37,11 @@ export interface LovelaceViewHeaderConfig { badges_wrap?: "wrap" | "scroll"; } +export const DEFAULT_FOOTER_MAX_WIDTH_PX = 600; + export interface LovelaceViewFooterConfig { card?: LovelaceCardConfig; - column_span?: number; + max_width?: number; } export interface LovelaceViewSidebarConfig { diff --git a/src/panels/energy/strategies/energy-overview-view-strategy.ts b/src/panels/energy/strategies/energy-overview-view-strategy.ts index 74e4d7aa76..42011fb031 100644 --- a/src/panels/energy/strategies/energy-overview-view-strategy.ts +++ b/src/panels/energy/strategies/energy-overview-view-strategy.ts @@ -22,7 +22,6 @@ export class EnergyOverviewViewStrategy extends ReactiveElement { dense_section_placement: true, max_columns: 3, footer: { - column_span: 1.1, card: { type: "energy-date-selection", collection_key: collectionKey, diff --git a/src/panels/energy/strategies/energy-view-strategy.ts b/src/panels/energy/strategies/energy-view-strategy.ts index 5b518c34bc..ce5abfd721 100644 --- a/src/panels/energy/strategies/energy-view-strategy.ts +++ b/src/panels/energy/strategies/energy-view-strategy.ts @@ -23,7 +23,6 @@ export class EnergyViewStrategy extends ReactiveElement { max_columns: 3, sections: [], footer: { - column_span: 1.1, card: { type: "energy-date-selection", collection_key: collectionKey, diff --git a/src/panels/energy/strategies/gas-view-strategy.ts b/src/panels/energy/strategies/gas-view-strategy.ts index 1c97b5a677..7c6001f4b3 100644 --- a/src/panels/energy/strategies/gas-view-strategy.ts +++ b/src/panels/energy/strategies/gas-view-strategy.ts @@ -21,7 +21,6 @@ export class GasViewStrategy extends ReactiveElement { max_columns: 3, sections: [{ type: "grid", cards: [], column_span: 3 }], footer: { - column_span: 1.1, card: { type: "energy-date-selection", collection_key: collectionKey, diff --git a/src/panels/energy/strategies/water-view-strategy.ts b/src/panels/energy/strategies/water-view-strategy.ts index 7579e169d3..c524fdb92a 100644 --- a/src/panels/energy/strategies/water-view-strategy.ts +++ b/src/panels/energy/strategies/water-view-strategy.ts @@ -22,7 +22,6 @@ export class WaterViewStrategy extends ReactiveElement { max_columns: 3, sections: [{ type: "grid", cards: [], column_span: 3 }], footer: { - column_span: 1.1, card: { type: "energy-date-selection", collection_key: collectionKey, diff --git a/src/panels/lovelace/editor/view-footer/hui-dialog-edit-view-footer.ts b/src/panels/lovelace/editor/view-footer/hui-dialog-edit-view-footer.ts index d391cf5b08..93f1161bd0 100644 --- a/src/panels/lovelace/editor/view-footer/hui-dialog-edit-view-footer.ts +++ b/src/panels/lovelace/editor/view-footer/hui-dialog-edit-view-footer.ts @@ -91,7 +91,6 @@ export class HuiDialogEditViewFooter extends LitElement { `; @@ -106,7 +105,7 @@ export class HuiDialogEditViewFooter extends LitElement { .hass=${this.hass} .open=${this._open} header-title=${title} - .width=${this._yamlMode ? "full" : "large"} + width="medium" @closed=${this._dialogClosed} class=${this._yamlMode ? "yaml-mode" : ""} > diff --git a/src/panels/lovelace/editor/view-footer/hui-view-footer-settings-editor.ts b/src/panels/lovelace/editor/view-footer/hui-view-footer-settings-editor.ts index 9f459b07f1..98a1b45675 100644 --- a/src/panels/lovelace/editor/view-footer/hui-view-footer-settings-editor.ts +++ b/src/panels/lovelace/editor/view-footer/hui-view-footer-settings-editor.ts @@ -1,53 +1,48 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-form/ha-form"; import type { HaFormSchema, SchemaUnion, } from "../../../../components/ha-form/types"; -import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/view"; +import { + DEFAULT_FOOTER_MAX_WIDTH_PX, + type LovelaceViewFooterConfig, +} from "../../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../../types"; +const SCHEMA = [ + { + name: "max_width", + selector: { + number: { + min: 100, + max: 1600, + step: 10, + unit_of_measurement: "px", + }, + }, + }, +] as const satisfies HaFormSchema[]; + @customElement("hui-view-footer-settings-editor") export class HuiViewFooterSettingsEditor extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public config?: LovelaceViewFooterConfig; - @property({ attribute: false }) public maxColumns = 4; - - private _schema = memoizeOne( - (maxColumns: number) => - [ - { - name: "column_span", - selector: { - number: { - min: 1, - max: maxColumns, - slider_ticks: true, - }, - }, - }, - ] as const satisfies HaFormSchema[] - ); - protected render() { const data = { - column_span: this.config?.column_span || 1, + max_width: this.config?.max_width || DEFAULT_FOOTER_MAX_WIDTH_PX, }; - const schema = this._schema(this.maxColumns); - return html` `; @@ -65,19 +60,10 @@ export class HuiViewFooterSettingsEditor extends LitElement { fireEvent(this, "config-changed", { config }); } - private _computeLabel = ( - schema: SchemaUnion> - ) => + private _computeLabel = (schema: SchemaUnion) => this.hass.localize( `ui.panel.lovelace.editor.edit_view_footer.settings.${schema.name}` ); - - private _computeHelper = ( - schema: SchemaUnion> - ) => - this.hass.localize( - `ui.panel.lovelace.editor.edit_view_footer.settings.${schema.name}_helper` - ) || ""; } declare global { diff --git a/src/panels/lovelace/editor/view-footer/show-edit-view-footer-dialog.ts b/src/panels/lovelace/editor/view-footer/show-edit-view-footer-dialog.ts index a7310a746a..f310b91458 100644 --- a/src/panels/lovelace/editor/view-footer/show-edit-view-footer-dialog.ts +++ b/src/panels/lovelace/editor/view-footer/show-edit-view-footer-dialog.ts @@ -4,7 +4,6 @@ import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/ export interface EditViewFooterDialogParams { saveConfig: (config: LovelaceViewFooterConfig) => void; config: LovelaceViewFooterConfig; - maxColumns: number; } export const showEditViewFooterDialog = ( diff --git a/src/panels/lovelace/views/hui-view-footer.ts b/src/panels/lovelace/views/hui-view-footer.ts index 02d2a447dc..4ee2308ab1 100644 --- a/src/panels/lovelace/views/hui-view-footer.ts +++ b/src/panels/lovelace/views/hui-view-footer.ts @@ -7,9 +7,10 @@ import { styleMap } from "lit/directives/style-map"; import "../../../components/ha-ripple"; import "../../../components/ha-svg-icon"; import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; -import type { - LovelaceViewConfig, - LovelaceViewFooterConfig, +import { + DEFAULT_FOOTER_MAX_WIDTH_PX, + type LovelaceViewConfig, + type LovelaceViewFooterConfig, } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; import type { HuiCard } from "../cards/hui-card"; @@ -19,7 +20,6 @@ import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog" import { replaceView } from "../editor/config-util"; import { showEditViewFooterDialog } from "../editor/view-footer/show-edit-view-footer-dialog"; import type { Lovelace } from "../types"; -import { DEFAULT_MAX_COLUMNS } from "./hui-sections-view"; @customElement("hui-view-footer") export class HuiViewFooter extends LitElement { @@ -97,13 +97,8 @@ export class HuiViewFooter extends LitElement { } private _configure() { - const viewConfig = this.lovelace.config.views[ - this.viewIndex - ] as LovelaceViewConfig; - showEditViewFooterDialog(this, { config: this.config || {}, - maxColumns: viewConfig.max_columns || DEFAULT_MAX_COLUMNS, saveConfig: (newConfig: LovelaceViewFooterConfig) => { this._saveFooterConfig(newConfig); }, @@ -180,13 +175,11 @@ export class HuiViewFooter extends LitElement { if (!card && !editMode) return nothing; - const columnSpan = this.config?.column_span || 1; - return html`
${editMode @@ -228,23 +221,18 @@ export class HuiViewFooter extends LitElement { :host([sticky]) { position: sticky; - bottom: 0; + bottom: var(--row-gap); z-index: 4; } .wrapper { - padding: var(--ha-space-4) 0; - padding-bottom: max( - var(--ha-space-4), - var(--safe-area-inset-bottom, 0px) + padding: var(--ha-space-2) 0; + padding-bottom: calc( + max(var(--ha-space-2), var(--safe-area-inset-bottom, 0px)) ); box-sizing: content-box; margin: 0 auto; - max-width: calc( - var(--footer-column-span, 1) / var(--column-count, 1) * 100% + - (var(--footer-column-span, 1) - var(--column-count, 1)) / - var(--column-count, 1) * var(--column-gap, 32px) - ); + max-width: var(--footer-max-width, 600px); } .wrapper:not(.edit-mode) { @@ -315,7 +303,7 @@ export class HuiViewFooter extends LitElement { border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; background: var(--secondary-background-color); - --mdc-icon-button-size: 36px; + --ha-icon-button-size: 36px; --mdc-icon-size: 20px; color: var(--primary-text-color); } diff --git a/src/translations/en.json b/src/translations/en.json index 6c1fc050f6..b8975d9f7b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -8495,8 +8495,7 @@ "edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]", "saving_failed": "[%key:ui::panel::lovelace::editor::edit_view::saving_failed%]", "settings": { - "column_span": "Width", - "column_span_helper": "[%key:ui::panel::lovelace::editor::edit_section::settings::column_span_helper%]" + "max_width": "Max width" } }, "edit_badges": { From 043d4eed852ea4fc96773c7a9d1bdc6fd3c29361 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 3 Mar 2026 13:34:05 +0100 Subject: [PATCH 37/69] Add label for toggle button in area strategy (#29949) --- src/components/ha-control-button.ts | 3 ++- src/components/ha-heading-badge.ts | 2 +- src/panels/light/strategies/light-view-strategy.ts | 9 ++++++--- .../lovelace/heading-badges/hui-button-heading-badge.ts | 4 +++- src/translations/en.json | 4 +++- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/components/ha-control-button.ts b/src/components/ha-control-button.ts index 8165d7b6ba..470b310177 100644 --- a/src/components/ha-control-button.ts +++ b/src/components/ha-control-button.ts @@ -33,6 +33,7 @@ export class HaControlButton extends LitElement { --control-button-background-color: var(--disabled-color); --control-button-background-opacity: 0.2; --control-button-border-radius: var(--ha-border-radius-md); + --control-button-font-weight: var(--ha-font-weight-medium); --control-button-padding: 8px; --mdc-icon-size: 20px; --ha-ripple-color: var(--secondary-text-color); @@ -59,7 +60,7 @@ export class HaControlButton extends LitElement { box-sizing: border-box; line-height: inherit; font-family: var(--ha-font-family-body); - font-weight: var(--ha-font-weight-medium); + font-weight: var(--control-button-font-weight); outline: none; overflow: hidden; background: none; diff --git a/src/components/ha-heading-badge.ts b/src/components/ha-heading-badge.ts index 27def2d750..db9ef679d7 100644 --- a/src/components/ha-heading-badge.ts +++ b/src/components/ha-heading-badge.ts @@ -38,7 +38,7 @@ export class HaBadge extends LitElement { font-weight: var(--ha-heading-badge-font-weight, 400); line-height: var(--ha-heading-badge-line-height, 20px); letter-spacing: 0.1px; - --mdc-icon-size: 14px; + --mdc-icon-size: 16px; } ::slotted([slot="icon"]) { --ha-icon-display: block; diff --git a/src/panels/light/strategies/light-view-strategy.ts b/src/panels/light/strategies/light-view-strategy.ts index 29aa9ea20a..c499920ce7 100644 --- a/src/panels/light/strategies/light-view-strategy.ts +++ b/src/panels/light/strategies/light-view-strategy.ts @@ -17,6 +17,7 @@ import { SMALL_SCREEN_CONDITION, } from "../../lovelace/strategies/helpers/screen-conditions"; import type { ToggleGroupCardConfig } from "../../lovelace/cards/types"; +import type { ButtonHeadingBadgeConfig } from "../../lovelace/heading-badges/types"; export interface LightViewStrategyConfig { type: "light"; @@ -75,6 +76,7 @@ const processAreasForLight = ( { type: "button", icon: "mdi:power", + text: hass.localize("ui.panel.lovelace.strategy.light.off"), tap_action: { action: "perform-action", perform_action: "light.turn_on", @@ -89,11 +91,12 @@ const processAreasForLight = ( conditions: [anyOnCondition], }, ], - }, + } satisfies ButtonHeadingBadgeConfig, { type: "button", icon: "mdi:power", - color: "amber", + color: "orange", + text: hass.localize("ui.panel.lovelace.strategy.light.on"), tap_action: { action: "perform-action", perform_action: "light.turn_off", @@ -102,7 +105,7 @@ const processAreasForLight = ( }, }, visibility: [SMALL_SCREEN_CONDITION, anyOnCondition], - }, + } satisfies ButtonHeadingBadgeConfig, ] satisfies LovelaceCardConfig[], }); diff --git a/src/panels/lovelace/heading-badges/hui-button-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-button-heading-badge.ts index bffb00172c..d3a405afdd 100644 --- a/src/panels/lovelace/heading-badges/hui-button-heading-badge.ts +++ b/src/panels/lovelace/heading-badges/hui-button-heading-badge.ts @@ -108,7 +108,7 @@ export class HuiButtonHeadingBadge var(--ha-border-radius-pill) ); --control-button-padding: 0; - --mdc-icon-size: var(--ha-heading-badge-icon-size, 14px); + --mdc-icon-size: var(--ha-heading-badge-icon-size, 16px); width: auto; height: var(--ha-heading-badge-size, 26px); min-width: var(--ha-heading-badge-size, 26px); @@ -121,6 +121,8 @@ export class HuiButtonHeadingBadge --control-button-icon-color: var(--color); --control-button-background-color: var(--color); --control-button-focus-color: var(--color); + --control-button-background-opacity: 0.2; + --control-button-font-weight: var(--ha-font-weight-bold); --ha-ripple-color: var(--color); } .content { diff --git a/src/translations/en.json b/src/translations/en.json index b8975d9f7b..76cbe7f4d7 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -8089,7 +8089,9 @@ }, "light": { "lights": "Lights", - "other_lights": "Other lights" + "other_lights": "Other lights", + "on": "On", + "off": "Off" }, "security": { "devices": "Devices", From 64749350ef28e9b1dcf8fdf962b1e500c12dbe3b Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:06:23 +0100 Subject: [PATCH 38/69] ha-bottom-sheet reduce motion support (#29950) --- src/components/ha-bottom-sheet.ts | 12 ++++++++++++ src/components/ha-sidebar.ts | 4 ++-- src/components/ha-top-app-bar-fixed.ts | 2 +- src/components/ha-two-pane-top-app-bar-fixed.ts | 2 +- src/panels/media-browser/ha-bar-media-player.ts | 2 +- src/resources/theme/core.globals.ts | 6 +++--- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/components/ha-bottom-sheet.ts b/src/components/ha-bottom-sheet.ts index af37604d94..71fd53e9d5 100644 --- a/src/components/ha-bottom-sheet.ts +++ b/src/components/ha-bottom-sheet.ts @@ -406,6 +406,18 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) { transform: var(--dialog-transform); transition: var(--dialog-transition); } + @media (prefers-reduced-motion: reduce) { + wa-drawer { + --wa-color-surface-raised: transparent; + --spacing: 0; + --size: var(--ha-bottom-sheet-height, auto); + --show-duration: 1ms; + --hide-duration: 1ms; + } + wa-drawer::part(dialog) { + transition: 1ms; + } + } wa-drawer::part(dialog)::backdrop { -webkit-backdrop-filter: var( --ha-bottom-sheet-scrim-backdrop-filter, diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 2116991208..17735bab75 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -37,8 +37,8 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs"; import type { UpdateEntity } from "../data/update"; import { updateCanInstall } from "../data/update"; import { showEditSidebarDialog } from "../dialogs/sidebar/show-dialog-edit-sidebar"; -import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin"; +import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant, PanelInfo, Route } from "../types"; @@ -981,7 +981,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) { ha-md-list-item, ha-md-list-item .item-text, .title { - transition: none; + transition: 1ms; } } `, diff --git a/src/components/ha-top-app-bar-fixed.ts b/src/components/ha-top-app-bar-fixed.ts index 1f7a650dd9..dc451b6751 100644 --- a/src/components/ha-top-app-bar-fixed.ts +++ b/src/components/ha-top-app-bar-fixed.ts @@ -46,7 +46,7 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase { } @media (prefers-reduced-motion: reduce) { .mdc-top-app-bar { - transition: none; + transition: 1ms; } } .mdc-top-app-bar__title { diff --git a/src/components/ha-two-pane-top-app-bar-fixed.ts b/src/components/ha-two-pane-top-app-bar-fixed.ts index 52a560b5d1..706278f95d 100644 --- a/src/components/ha-two-pane-top-app-bar-fixed.ts +++ b/src/components/ha-two-pane-top-app-bar-fixed.ts @@ -298,7 +298,7 @@ export class TopAppBarBaseBase extends BaseElement { } @media (prefers-reduced-motion: reduce) { .mdc-top-app-bar { - transition: none; + transition: 1ms; } } .mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled { diff --git a/src/panels/media-browser/ha-bar-media-player.ts b/src/panels/media-browser/ha-bar-media-player.ts index 42d460082e..cb9d72dc1b 100644 --- a/src/panels/media-browser/ha-bar-media-player.ts +++ b/src/panels/media-browser/ha-bar-media-player.ts @@ -679,7 +679,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { } @media (prefers-reduced-motion: reduce) { :host { - transition: none; + transition: 1ms; } } diff --git a/src/resources/theme/core.globals.ts b/src/resources/theme/core.globals.ts index b746d79656..997731db57 100644 --- a/src/resources/theme/core.globals.ts +++ b/src/resources/theme/core.globals.ts @@ -62,9 +62,9 @@ export const coreStyles = css` @media (prefers-reduced-motion: reduce) { html { - --ha-animation-duration-fast: 0ms; - --ha-animation-duration-normal: 0ms; - --ha-animation-duration-slow: 0ms; + --ha-animation-duration-fast: 1ms; + --ha-animation-duration-normal: 1ms; + --ha-animation-duration-slow: 1ms; } } `; From 2b2bb77a2b72212e832758748640f79e5f401992 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Tue, 3 Mar 2026 12:09:30 +0000 Subject: [PATCH 39/69] Fix copy to clipboard for wa dialogs (#29951) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com> --- src/common/util/copy-clipboard.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/common/util/copy-clipboard.ts b/src/common/util/copy-clipboard.ts index c50ad02418..e6017e3871 100644 --- a/src/common/util/copy-clipboard.ts +++ b/src/common/util/copy-clipboard.ts @@ -1,3 +1,24 @@ +import { deepActiveElement } from "../dom/deep-active-element"; + +const getClipboardFallbackRoot = (): HTMLElement => { + const activeElement = deepActiveElement(); + if (activeElement instanceof HTMLElement) { + let root: Node = activeElement.getRootNode(); + let host: HTMLElement | null = null; + + while (root instanceof ShadowRoot && root.host instanceof HTMLElement) { + host = root.host; + root = root.host.getRootNode(); + } + + if (host) { + return host; + } + } + + return document.body; +}; + export const copyToClipboard = async (str, rootEl?: HTMLElement) => { if (navigator.clipboard) { try { @@ -8,10 +29,15 @@ export const copyToClipboard = async (str, rootEl?: HTMLElement) => { } } - const root = rootEl ?? document.body; + const root = rootEl || getClipboardFallbackRoot(); const el = document.createElement("textarea"); el.value = str; + el.setAttribute("readonly", ""); + el.style.position = "fixed"; + el.style.top = "0"; + el.style.left = "0"; + el.style.opacity = "0"; root.appendChild(el); el.select(); document.execCommand("copy"); From 1b8211db6d243e068716f111963289ac78577504 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 4 Mar 2026 07:08:22 +0100 Subject: [PATCH 40/69] Align heading button font-size with other heading entity badge (#29958) --- .../lovelace/heading-badges/hui-button-heading-badge.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/heading-badges/hui-button-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-button-heading-badge.ts index d3a405afdd..6a43fd4f77 100644 --- a/src/panels/lovelace/heading-badges/hui-button-heading-badge.ts +++ b/src/panels/lovelace/heading-badges/hui-button-heading-badge.ts @@ -112,7 +112,8 @@ export class HuiButtonHeadingBadge width: auto; height: var(--ha-heading-badge-size, 26px); min-width: var(--ha-heading-badge-size, 26px); - font-size: var(--ha-font-size-s); + font-size: var(--ha-heading-badge-font-size, var(--ha-font-size-m)); + font-weight: var(--ha-font-weight-medium); } ha-control-button.with-text { --control-button-padding: 0 var(--ha-space-2); @@ -122,7 +123,6 @@ export class HuiButtonHeadingBadge --control-button-background-color: var(--color); --control-button-focus-color: var(--color); --control-button-background-opacity: 0.2; - --control-button-font-weight: var(--ha-font-weight-bold); --ha-ripple-color: var(--color); } .content { From 17c6dc52a8299c195acec0d5320e7e5a90ef43ec Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 3 Mar 2026 21:00:43 +0100 Subject: [PATCH 41/69] Add hass url to brand images (#29961) --- src/components/device/ha-device-picker.ts | 26 ++++---- src/components/entity/state-badge.ts | 2 +- src/components/ha-domain-icon.ts | 13 ++-- src/components/ha-related-items.ts | 26 ++++---- .../ha-selector/ha-selector-media.ts | 13 ++-- src/components/ha-target-picker.ts | 13 ++-- .../media-player/ha-media-player-browse.ts | 13 ++-- .../ha-target-picker-item-row.ts | 13 ++-- .../ha-target-picker-value-chip.ts | 13 ++-- src/components/voice-assistant-brand-icon.ts | 13 ++-- .../config-flow/step-flow-create-entry.ts | 14 +++-- src/onboarding/integration-badge.ts | 13 ++-- .../ha-automation-add-from-target.ts | 13 ++-- .../config/ha-backup-config-agents.ts | 13 ++-- .../components/ha-backup-agents-picker.ts | 13 ++-- .../config/backup/ha-config-backup-backups.ts | 13 ++-- .../config/backup/ha-config-backup-details.ts | 15 +++-- .../backup/ha-config-backup-settings.ts | 13 ++-- src/panels/config/core/ai-task-pref.ts | 13 ++-- .../config/devices/ha-config-device-page.ts | 26 ++++---- .../devices/ha-config-devices-dashboard.ts | 13 ++-- .../components/ha-energy-grid-settings.ts | 13 ++-- .../dialogs/dialog-energy-solar-settings.ts | 13 ++-- .../config/hardware/ha-config-hardware.ts | 15 +++-- .../config/helpers/dialog-helper-detail.ts | 13 ++-- .../ha-config-integration-page.ts | 13 ++-- .../integrations/ha-domain-integrations.ts | 39 +++++++----- .../ha-integration-action-card.ts | 13 ++-- .../integrations/ha-integration-header.ts | 13 ++-- .../integrations/ha-integration-list-item.ts | 13 ++-- ...dialog-matter-open-commissioning-window.ts | 13 ++-- .../thread/thread-config-panel.ts | 13 ++-- src/panels/config/labs/ha-config-labs.ts | 13 ++-- .../config/repairs/ha-config-repairs.ts | 13 ++-- .../repairs/integrations-startup-time.ts | 13 ++-- src/panels/logbook/ha-logbook-renderer.ts | 13 ++-- .../lovelace/badges/hui-entity-badge.ts | 5 +- src/panels/lovelace/cards/hui-tile-card.ts | 5 +- src/util/brands-url.ts | 60 +++++++++++++------ test/util/generate-brands-url.test.ts | 59 +++++++++++++----- 40 files changed, 418 insertions(+), 238 deletions(-) diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index a68d73b165..451762c2c6 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -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} ${primary} @@ -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} diff --git a/src/components/entity/state-badge.ts b/src/components/entity/state-badge.ts index 71b6a7d74e..d1233c72e0 100644 --- a/src/components/entity/state-badge.ts +++ b/src/components/entity/state-badge.ts @@ -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); } diff --git a/src/components/ha-domain-icon.ts b/src/components/ha-domain-icon.ts index d41a0799de..48a81284db 100644 --- a/src/components/ha-domain-icon.ts +++ b/src/components/ha-domain-icon.ts @@ -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` ${entry.domain} ${integration} ` : type === "floor" diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts index d60ec2cabf..26077cc187 100644 --- a/src/components/media-player/ha-media-player-browse.ts +++ b/src/components/media-player/ha-media-player-browse.ts @@ -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("/")) { diff --git a/src/components/target-picker/ha-target-picker-item-row.ts b/src/components/target-picker/ha-target-picker-item-row.ts index b52bbb13b3..4c6db12d03 100644 --- a/src/components/target-picker/ha-target-picker-item-row.ts +++ b/src/components/target-picker/ha-target-picker-item-row.ts @@ -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 { diff --git a/src/components/target-picker/ha-target-picker-value-chip.ts b/src/components/target-picker/ha-target-picker-value-chip.ts index 4c58c28502..b25a385fdf 100644 --- a/src/components/target-picker/ha-target-picker-value-chip.ts +++ b/src/components/target-picker/ha-target-picker-value-chip.ts @@ -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 { diff --git a/src/components/voice-assistant-brand-icon.ts b/src/components/voice-assistant-brand-icon.ts index c40ad9d64e..6a4032f401 100644 --- a/src/components/voice-assistant-brand-icon.ts +++ b/src/components/voice-assistant-brand-icon.ts @@ -17,11 +17,14 @@ export class VoiceAssistantBrandicon extends LitElement { diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index 79a8e73725..c4ea0982b8 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -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" />` diff --git a/src/onboarding/integration-badge.ts b/src/onboarding/integration-badge.ts index 8112e4b900..0edf2538be 100644 --- a/src/onboarding/integration-badge.ts +++ b/src/onboarding/integration-badge.ts @@ -21,11 +21,14 @@ class IntegrationBadge extends LitElement {
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`
Nabu Casa logo ${this.hass.localize("ui.panel.config.ai_task.header")} diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 74cf314320..f353b0b82b 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -364,11 +364,14 @@ export class HaConfigDevicePage extends LitElement { ${domainToName(this.hass.localize,` : "", }, diff --git a/src/panels/config/energy/components/ha-energy-grid-settings.ts b/src/panels/config/energy/components/ha-energy-grid-settings.ts index 4a272ba489..e37cb6a7b4 100644 --- a/src/panels/config/energy/components/ha-energy-grid-settings.ts +++ b/src/panels/config/energy/components/ha-energy-grid-settings.ts @@ -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 + )} /> ${this._co2ConfigEntry.title} ${entry.title}
`} > 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) {
${domainToName(this.hass.localize, @@ -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 + )} /> ${router.brand} 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" ); }); From bb16cc8c006c595487735f7a0f6ca6df9ac6bec2 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:13:13 +0100 Subject: [PATCH 42/69] Open quick search quicker (#29967) --- src/dialogs/quick-bar/ha-quick-bar.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index c5cbd9b93e..d450728733 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -855,6 +855,7 @@ export class QuickBar extends LitElement { ); --dialog-content-padding: 0; --safe-area-inset-bottom: 0px; + --ha-dialog-show-duration: var(--ha-animation-duration-instant); } ha-tip { From 5709af57def2dde1953702180300a821bc1c7423 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 Mar 2026 12:42:58 +0100 Subject: [PATCH 43/69] Bumped version to 20260304.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d7c62e9217..0cf2945aa4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20260302.0" +version = "20260304.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 1859d35f7b2b81aac7407eef3467698772fa1c69 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 4 Mar 2026 14:58:34 +0100 Subject: [PATCH 44/69] Add arrow and fix footer for vacuum segment mapper (#29975) --- .../ha-vacuum-segment-area-mapper.ts | 46 +++++++++++++++---- ...a-more-info-view-vacuum-segment-mapping.ts | 9 +++- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/components/ha-vacuum-segment-area-mapper.ts b/src/components/ha-vacuum-segment-area-mapper.ts index 09ee6432e0..28f5caead0 100644 --- a/src/components/ha-vacuum-segment-area-mapper.ts +++ b/src/components/ha-vacuum-segment-area-mapper.ts @@ -1,6 +1,7 @@ import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { mdiArrowRightThin } from "@mdi/js"; import { fireEvent } from "../common/dom/fire_event"; import type { Segment } from "../data/vacuum"; import { getVacuumSegments } from "../data/vacuum"; @@ -8,8 +9,7 @@ import { haStyle } from "../resources/styles"; import type { HomeAssistant } from "../types"; import "./ha-alert"; import "./ha-area-picker"; -import "./ha-md-list"; -import "./ha-md-list-item"; +import "./ha-svg-icon"; type AreaSegmentMapping = Record; // area ID -> segment IDs @@ -80,9 +80,7 @@ export class HaVacuumSegmentAreaMapper extends LitElement { ${Object.entries(groupedSegments).map( ([groupName, segments]) => html` ${groupName ? html`

${groupName}

` : nothing} - - ${segments.map((segment) => this._renderSegment(segment))} - + ${segments.map((segment) => this._renderSegment(segment))} ` )} `; @@ -106,10 +104,10 @@ export class HaVacuumSegmentAreaMapper extends LitElement { const mappedAreas = this._getSegmentAreas(segment.id); return html` - - ${segment.name} +
+ ${segment.name} + - +
`; } @@ -179,8 +177,36 @@ export class HaVacuumSegmentAreaMapper extends LitElement { display: block; } - ha-area-picker { + .segment-row { + display: flex; + align-items: center; + gap: var(--ha-space-4); + padding: var(--ha-space-2) var(--ha-space-4); + } + + .segment-name { flex: 1; + font: var(--ha-font-body-l); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .arrow { + flex-shrink: 0; + color: var(--secondary-text-color); + } + + @media (max-width: 600px) { + .arrow { + display: none; + } + } + + ha-area-picker { + flex: 2; + min-width: 0; + max-width: 300px; } h2 { diff --git a/src/dialogs/more-info/components/vacuum/ha-more-info-view-vacuum-segment-mapping.ts b/src/dialogs/more-info/components/vacuum/ha-more-info-view-vacuum-segment-mapping.ts index 2b3b611b5b..c7227b7c13 100644 --- a/src/dialogs/more-info/components/vacuum/ha-more-info-view-vacuum-segment-mapping.ts +++ b/src/dialogs/more-info/components/vacuum/ha-more-info-view-vacuum-segment-mapping.ts @@ -117,13 +117,11 @@ export class HaMoreInfoViewVacuumSegmentMapping extends LitElement { static styles: CSSResultGroup = css` :host { display: block; - height: 100%; } .content { display: flex; flex-direction: column; - height: 100%; } ha-spinner { @@ -142,6 +140,13 @@ export class HaMoreInfoViewVacuumSegmentMapping extends LitElement { justify-content: flex-end; padding: var(--ha-space-4); border-top: 1px solid var(--divider-color); + background: var( + --ha-dialog-surface-background, + var(--mdc-theme-surface, #fff) + ); + position: sticky; + bottom: 0; + z-index: 10; } `; } From b286b07cfddcaeb183cd15231c920c5948a28c14 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 5 Mar 2026 15:13:05 +0100 Subject: [PATCH 45/69] Fix sensor card graph time axis not progressing when value is unchanged (#29976) --- .../lovelace/header-footer/hui-graph-header-footer.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts index 543b8aec4f..44e4d6b613 100644 --- a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts +++ b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts @@ -193,13 +193,19 @@ export class HuiGraphHeaderFooter ? Math.max(width / 5, this._config.hours_to_show!) : this._config.hours_to_show! ); + const now = Date.now(); const useMean = this._config.detail !== 2; const { points } = coordinatesMinimalResponseCompressedState( entityHistory, width, width / 5, maxDetails, - { minY: this._config.limits?.min, maxY: this._config.limits?.max }, + { + minX: now - this._config.hours_to_show! * HOUR, + maxX: now, + minY: this._config.limits?.min, + maxY: this._config.limits?.max, + }, useMean ); this._coordinates = points; From 1a6d46a7ffe2d25aadcc12a830daf9f911c06de9 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 4 Mar 2026 16:02:09 +0100 Subject: [PATCH 46/69] Refactor tooltip CSS tokens to use ha- prefix (#29978) Co-authored-by: Claude Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com> --- gallery/src/pages/components/ha-tooltip.markdown | 13 ++++++++----- src/components/ha-slider.ts | 14 ++++++++++---- src/components/ha-tooltip.ts | 14 ++++++++++---- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/gallery/src/pages/components/ha-tooltip.markdown b/gallery/src/pages/components/ha-tooltip.markdown index c51856ba41..bd5713a39a 100644 --- a/gallery/src/pages/components/ha-tooltip.markdown +++ b/gallery/src/pages/components/ha-tooltip.markdown @@ -28,9 +28,12 @@ This element is based on webawesome `wa-tooltip` it only sets some css tokens an In your theme settings use this without the prefixed `--`. -- `--ha-tooltip-border-radius` (Default: 4px) -- `--ha-tooltip-arrow-size` (Default: 8px) -- `--wa-tooltip-font-family` (Default: `var(--ha-font-family-body)`) +- `--ha-tooltip-background-color` (Default: `var(--secondary-background-color)`) +- `--ha-tooltip-text-color` (Default: `var(--primary-text-color)`) +- `--ha-tooltip-font-family` (Default: `var(--ha-font-family-body)`) - `--ha-tooltip-font-size` (Default: `var(--ha-font-size-s)`) -- `--wa-tooltip-font-weight` (Default: `var(--ha-font-weight-normal)`) -- `--wa-tooltip-line-height` (Default: `var(--ha-line-height-condensed)`) +- `--ha-tooltip-font-weight` (Default: `var(--ha-font-weight-normal)`) +- `--ha-tooltip-line-height` (Default: `var(--ha-line-height-condensed)`) +- `--ha-tooltip-padding` (Default: 8px) +- `--ha-tooltip-border-radius` (Default: `var(--ha-border-radius-sm)`) +- `--ha-tooltip-arrow-size` (Default: 8px) diff --git a/src/components/ha-slider.ts b/src/components/ha-slider.ts index 57d35a28e0..1b55319338 100644 --- a/src/components/ha-slider.ts +++ b/src/components/ha-slider.ts @@ -24,8 +24,14 @@ export class HaSlider extends Slider { --marker-width: calc(var(--ha-slider-track-size, 4px) / 2); --wa-color-surface-default: var(--card-background-color); --wa-color-neutral-fill-normal: var(--disabled-color); - --wa-tooltip-background-color: var(--secondary-background-color); - --wa-tooltip-color: var(--primary-text-color); + --wa-tooltip-background-color: var( + --ha-tooltip-background-color, + var(--secondary-background-color) + ); + --wa-tooltip-content-color: var( + --ha-tooltip-text-color, + var(--primary-text-color) + ); --wa-tooltip-font-family: var( --ha-tooltip-font-family, var(--ha-font-family-body) @@ -42,13 +48,13 @@ export class HaSlider extends Slider { --ha-tooltip-line-height, var(--ha-line-height-condensed) ); - --wa-tooltip-padding: 8px; + --wa-tooltip-padding: var(--ha-tooltip-padding, var(--ha-space-2)); --wa-tooltip-border-radius: var( --ha-tooltip-border-radius, var(--ha-border-radius-sm) ); --wa-tooltip-arrow-size: var(--ha-tooltip-arrow-size, 8px); - --wa-z-index-tooltip: var(--ha-tooltip-z-index, 1000); + --wa-z-index-tooltip: 1000; min-width: 100px; min-inline-size: 100px; width: 200px; diff --git a/src/components/ha-tooltip.ts b/src/components/ha-tooltip.ts index 6635da1361..c3bf0c7249 100644 --- a/src/components/ha-tooltip.ts +++ b/src/components/ha-tooltip.ts @@ -16,8 +16,14 @@ export class HaTooltip extends Tooltip { Tooltip.styles, css` :host { - --wa-tooltip-background-color: var(--secondary-background-color); - --wa-tooltip-content-color: var(--primary-text-color); + --wa-tooltip-background-color: var( + --ha-tooltip-background-color, + var(--secondary-background-color) + ); + --wa-tooltip-content-color: var( + --ha-tooltip-text-color, + var(--primary-text-color) + ); --wa-tooltip-font-family: var( --ha-tooltip-font-family, var(--ha-font-family-body) @@ -34,13 +40,13 @@ export class HaTooltip extends Tooltip { --ha-tooltip-line-height, var(--ha-line-height-condensed) ); - --wa-tooltip-padding: 8px; + --wa-tooltip-padding: var(--ha-tooltip-padding, var(--ha-space-2)); --wa-tooltip-border-radius: var( --ha-tooltip-border-radius, var(--ha-border-radius-sm) ); --wa-tooltip-arrow-size: var(--ha-tooltip-arrow-size, 8px); - --wa-z-index-tooltip: var(--ha-tooltip-z-index, 1000); + --wa-z-index-tooltip: 1000; } `, ]; From 8d42395938006e16aafea9c19c84fea684e2b235 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 6 Mar 2026 19:42:52 +0100 Subject: [PATCH 47/69] Fix stale data point in history-graph cards with sub-hour windows (#29998) Skip fetching hourly statistics when hours_to_show < 1 since hourly aggregates produce stale outlier points in sub-hour chart windows (e.g. hours_to_show: 0.1 or 0.05). Also fix Date object handling in ha-chart-base downsampling bounds extraction. --- src/components/chart/ha-chart-base.ts | 18 ++++++++++++++---- .../lovelace/cards/hui-history-graph-card.ts | 4 ++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index e0c759cdfc..19f1d1bbe3 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -877,10 +877,20 @@ export class HaChartBase extends LitElement { }; } if (s.sampling === "minmax") { - const minX = - xAxis?.min && typeof xAxis.min === "number" ? xAxis.min : undefined; - const maxX = - xAxis?.max && typeof xAxis.max === "number" ? xAxis.max : undefined; + const minX = xAxis?.min + ? xAxis.min instanceof Date + ? xAxis.min.getTime() + : typeof xAxis.min === "number" + ? xAxis.min + : undefined + : undefined; + const maxX = xAxis?.max + ? xAxis.max instanceof Date + ? xAxis.max.getTime() + : typeof xAxis.max === "number" + ? xAxis.max + : undefined + : undefined; return { ...s, sampling: undefined, diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index b570839860..23e40041f8 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -185,6 +185,10 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { } private async _fetchStatistics(sensorNumericDeviceClasses: string[]) { + if (this._hoursToShow < 1) { + // Statistics are hourly aggregates, not useful for sub-hour windows + return; + } const now = new Date(); const start = new Date(); start.setHours(start.getHours() - this._hoursToShow - 1); From f24c009dd75e5f051721e163e2ea0dec62880a79 Mon Sep 17 00:00:00 2001 From: Yosi Levy <37745463+yosilevy@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:42:21 +0200 Subject: [PATCH 48/69] RTL textfield fixes for quick search (#30013) textfield fixes for quick search --- src/components/ha-base-time-input.ts | 6 +++++- src/components/ha-picker-combo-box.ts | 5 ++++- .../ha-selector/ha-selector-color-rgb.ts | 5 ++++- src/components/ha-textfield.ts | 15 ++++++++------- src/panels/profile/ha-pick-theme-row.ts | 5 ++++- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/components/ha-base-time-input.ts b/src/components/ha-base-time-input.ts index e06923f2ad..95d8ad351d 100644 --- a/src/components/ha-base-time-input.ts +++ b/src/components/ha-base-time-input.ts @@ -349,7 +349,11 @@ export class HaBaseTimeInput extends LitElement { text-align: center; --mdc-shape-small: 0; --text-field-appearance: none; - --text-field-padding: 0 4px; + --text-field-padding-top: 0; + --text-field-padding-bottom: 0; + --text-field-padding-start: 4px; + --text-field-padding-end: 4px; + --text-field-suffix-padding-left: 2px; --text-field-suffix-padding-right: 0; --text-field-text-align: center; diff --git a/src/components/ha-picker-combo-box.ts b/src/components/ha-picker-combo-box.ts index 5f4f5f0831..d5a896545a 100644 --- a/src/components/ha-picker-combo-box.ts +++ b/src/components/ha-picker-combo-box.ts @@ -801,7 +801,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { } :host([clearable]) { - --text-field-padding: 0 0 0 var(--ha-space-4); + --text-field-padding-top: 0; + --text-field-padding-bottom: 0; + --text-field-padding-start: var(--ha-space-4); + --text-field-padding-end: 0; } ha-textfield { diff --git a/src/components/ha-selector/ha-selector-color-rgb.ts b/src/components/ha-selector/ha-selector-color-rgb.ts index d59406c9e5..40b0c1e455 100644 --- a/src/components/ha-selector/ha-selector-color-rgb.ts +++ b/src/components/ha-selector/ha-selector-color-rgb.ts @@ -51,7 +51,10 @@ export class HaColorRGBSelector extends LitElement { align-items: center; } ha-textfield { - --text-field-padding: 8px; + --text-field-padding-top: 8px; + --text-field-padding-bottom: 8px; + --text-field-padding-start: 8px; + --text-field-padding-end: 8px; min-width: 75px; flex-grow: 1; margin: 0 4px; diff --git a/src/components/ha-textfield.ts b/src/components/ha-textfield.ts index c4b84d0271..c59c26ea5d 100644 --- a/src/components/ha-textfield.ts +++ b/src/components/ha-textfield.ts @@ -95,11 +95,12 @@ export class HaTextField extends TextFieldBase { width: var(--ha-textfield-input-width, 100%); } .mdc-text-field:not(.mdc-text-field--with-leading-icon) { - padding: var(--text-field-padding, 0px 16px); + padding-top: var(--text-field-padding-top, 0px); + padding-bottom: var(--text-field-padding-bottom, 0px); + padding-inline-start: var(--text-field-padding-start, 16px); + padding-inline-end: var(--text-field-padding-end, 16px); } .mdc-text-field__affix--suffix { - padding-left: var(--text-field-suffix-padding-left, 12px); - padding-right: var(--text-field-suffix-padding-right, 0px); padding-inline-start: var(--text-field-suffix-padding-left, 12px); padding-inline-end: var(--text-field-suffix-padding-right, 0px); direction: ltr; @@ -110,12 +111,12 @@ export class HaTextField extends TextFieldBase { direction: var(--direction); } - .mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon { - padding-left: var(--text-field-suffix-padding-left, 0px); - padding-right: var(--text-field-suffix-padding-right, 0px); - padding-inline-start: var(--text-field-suffix-padding-left, 0px); + .mdc-text-field--with-trailing-icon { + padding-inline-start: var(--text-field-suffix-padding-left, 16px); padding-inline-end: var(--text-field-suffix-padding-right, 0px); + direction: var(--direction); } + .mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__affix--suffix { color: var(--secondary-text-color); diff --git a/src/panels/profile/ha-pick-theme-row.ts b/src/panels/profile/ha-pick-theme-row.ts index d6384de0ca..a4f07951e8 100644 --- a/src/panels/profile/ha-pick-theme-row.ts +++ b/src/panels/profile/ha-pick-theme-row.ts @@ -328,7 +328,10 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) { flex-grow: 1; } ha-textfield { - --text-field-padding: 8px; + --text-field-padding-top: 8px; + --text-field-padding-bottom: 8px; + --text-field-padding-start: 8px; + --text-field-padding-end: 8px; min-width: 75px; flex-grow: 1; margin: 0 4px; From c790d2356ca3414590a4282b9b30922c2d1a791c Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Sun, 8 Mar 2026 09:34:28 +0100 Subject: [PATCH 49/69] Add back energy distribution card to electricity tab (#30049) --- .../energy/strategies/energy-view-strategy.ts | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/panels/energy/strategies/energy-view-strategy.ts b/src/panels/energy/strategies/energy-view-strategy.ts index ce5abfd721..1cdf5dd7c7 100644 --- a/src/panels/energy/strategies/energy-view-strategy.ts +++ b/src/panels/energy/strategies/energy-view-strategy.ts @@ -8,6 +8,10 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy"; import { shouldShowFloorsAndAreas } from "./show-floors-and-areas"; +import { + LARGE_SCREEN_CONDITION, + SMALL_SCREEN_CONDITION, +} from "../../lovelace/strategies/helpers/screen-conditions"; @customElement("energy-view-strategy") export class EnergyViewStrategy extends ReactiveElement { @@ -20,8 +24,11 @@ export class EnergyViewStrategy extends ReactiveElement { const view: LovelaceViewConfig = { type: "sections", - max_columns: 3, sections: [], + sidebar: { + sections: [{ cards: [] }], + visibility: [LARGE_SCREEN_CONDITION], + }, footer: { card: { type: "energy-date-selection", @@ -62,6 +69,22 @@ export class EnergyViewStrategy extends ReactiveElement { const mainCards: LovelaceCardConfig[] = []; const gaugeCards: LovelaceCardConfig[] = []; + const sidebarSection = view.sidebar!.sections![0]; + + if (hasGrid || hasBattery || hasSolar) { + const distributionCard = { + title: hass.localize("ui.panel.energy.cards.energy_distribution_title"), + type: "energy-distribution", + collection_key: collectionKey, + }; + sidebarSection.cards!.push(distributionCard); + view.sections!.push({ + type: "grid", + column_span: 1, + cards: [distributionCard], + visibility: [SMALL_SCREEN_CONDITION], + }); + } // Only include if we have a grid source & return. if (hasReturn) { @@ -100,9 +123,15 @@ export class EnergyViewStrategy extends ReactiveElement { } if (gaugeCards.length) { + sidebarSection.cards!.push({ + type: "grid", + columns: gaugeCards.length === 1 ? 1 : 2, + cards: gaugeCards, + }); view.sections!.push({ type: "grid", - column_span: 3, + column_span: 1, + visibility: [SMALL_SCREEN_CONDITION], cards: gaugeCards.length === 1 ? [gaugeCards[0]] From 52667b3266086829d0d11be9ea504a756358d83e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 9 Mar 2026 09:24:20 +0100 Subject: [PATCH 50/69] Add reorder support to area selector (#30056) --- src/components/ha-areas-picker.ts | 84 ++++++++++++++----- .../ha-selector/ha-selector-area.ts | 1 + src/data/selector.ts | 1 + 3 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/components/ha-areas-picker.ts b/src/components/ha-areas-picker.ts index 088a9dcde3..9f3dfd0d51 100644 --- a/src/components/ha-areas-picker.ts +++ b/src/components/ha-areas-picker.ts @@ -1,3 +1,4 @@ +import { mdiDragHorizontalVariant } from "@mdi/js"; import type { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; @@ -6,6 +7,7 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin"; import type { HomeAssistant } from "../types"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import "./ha-area-picker"; +import "./ha-sortable"; @customElement("ha-areas-picker") export class HaAreasPicker extends SubscribeMixin(LitElement) { @@ -62,6 +64,8 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public required = false; + @property({ type: Boolean }) public reorder = false; + protected render() { if (!this.hass) { return nothing; @@ -69,26 +73,42 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) { const currentAreas = this._currentAreas; return html` - ${currentAreas.map( - (area) => html` -
- -
- ` - )} + +
+ ${currentAreas.map( + (area) => html` +
+ + ${this.reorder + ? html` + + ` + : nothing} +
+ ` + )} +
+
`; } diff --git a/src/data/selector.ts b/src/data/selector.ts index aeca3d04cb..c77b722fd9 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -100,6 +100,7 @@ export interface AreaSelector { entity?: EntitySelectorFilter | readonly EntitySelectorFilter[]; device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[]; multiple?: boolean; + reorder?: boolean; } | null; } From 1f46f477c7f3fdc3253241152612cb95438849c2 Mon Sep 17 00:00:00 2001 From: Tom Carpenter Date: Mon, 9 Mar 2026 07:25:21 +0000 Subject: [PATCH 51/69] Add missing webawesome tooltip CSS variable (#30057) * Correct missing ha-tooltip CSS variable We were missing a default for the `--wa-tooltip-border-width` variable which meant the arrow from the tooltip disappeared in WA 3.3.1. * Fix tooltip in ha-slider --- src/components/ha-slider.ts | 1 + src/components/ha-tooltip.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/ha-slider.ts b/src/components/ha-slider.ts index 1b55319338..c16687c3dc 100644 --- a/src/components/ha-slider.ts +++ b/src/components/ha-slider.ts @@ -54,6 +54,7 @@ export class HaSlider extends Slider { var(--ha-border-radius-sm) ); --wa-tooltip-arrow-size: var(--ha-tooltip-arrow-size, 8px); + --wa-tooltip-border-width: 0px; --wa-z-index-tooltip: 1000; min-width: 100px; min-inline-size: 100px; diff --git a/src/components/ha-tooltip.ts b/src/components/ha-tooltip.ts index c3bf0c7249..52da759d90 100644 --- a/src/components/ha-tooltip.ts +++ b/src/components/ha-tooltip.ts @@ -46,6 +46,7 @@ export class HaTooltip extends Tooltip { var(--ha-border-radius-sm) ); --wa-tooltip-arrow-size: var(--ha-tooltip-arrow-size, 8px); + --wa-tooltip-border-width: 0px; --wa-z-index-tooltip: 1000; } `, From d9d2d6aa030173b17d2f911d1a2cd96d2d49955b Mon Sep 17 00:00:00 2001 From: Tom Carpenter Date: Mon, 9 Mar 2026 07:02:00 +0000 Subject: [PATCH 52/69] Don't include "null" data point in stat graph (#30058) When displaying the "now" value on statistics graphs, don't include a "null" data point for sum/change type graphs, just skip entirely. Otherwise for you get a messy null data point in the tooltip. --- src/components/chart/statistics-chart.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index dbe2ada387..4caaa5f8dd 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -640,11 +640,12 @@ export class StatisticsChart extends LitElement { ) { // Then push the current state at now statTypes.forEach((type, i) => { - const val: (number | null)[] = []; if (type === "sum" || type === "change") { - // Skip cumulative types - need special calculation - val.push(null); - } else if ( + // Skip cumulative types - need special calculation. + return; + } + const val: (number | null)[] = []; + if ( type === bandTop && this.chartType === "line" && drawBands && From cfa8eb5370036f86af7efc9955797da8bd52d00a Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 9 Mar 2026 11:51:28 +0100 Subject: [PATCH 53/69] Fix hasReturn check to scan all grid sources in energy view strategy (#30062) --- src/panels/energy/strategies/energy-view-strategy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/panels/energy/strategies/energy-view-strategy.ts b/src/panels/energy/strategies/energy-view-strategy.ts index 1cdf5dd7c7..075cd0b394 100644 --- a/src/panels/energy/strategies/energy-view-strategy.ts +++ b/src/panels/energy/strategies/energy-view-strategy.ts @@ -59,7 +59,9 @@ export class EnergyViewStrategy extends ReactiveElement { source.type === "grid" && (!!source.stat_energy_from || !!source.stat_energy_to) ); - const hasReturn = hasGrid && !!hasGrid.stat_energy_to; + const hasReturn = prefs.energy_sources.some( + (source) => source.type === "grid" && !!source.stat_energy_to + ); const hasSolar = prefs.energy_sources.some( (source) => source.type === "solar" ); From ee77619da302f6a4126793183ab4a6728e4ec8e6 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Tue, 10 Mar 2026 16:20:40 +0000 Subject: [PATCH 54/69] Fix code editor autocomplete using wa popup (#30081) --- src/components/ha-code-editor.ts | 203 ++++++++++++++++++++++++++++++- src/resources/codemirror.ts | 24 ++-- 2 files changed, 214 insertions(+), 13 deletions(-) diff --git a/src/components/ha-code-editor.ts b/src/components/ha-code-editor.ts index 988f32856b..f30960b4ce 100644 --- a/src/components/ha-code-editor.ts +++ b/src/components/ha-code-editor.ts @@ -1,3 +1,5 @@ +import "@home-assistant/webawesome/dist/components/popup/popup"; +import type WaPopup from "@home-assistant/webawesome/dist/components/popup/popup"; import type { Completion, CompletionContext, @@ -110,6 +112,18 @@ export class HaCodeEditor extends ReactiveElement { // eslint-disable-next-line @typescript-eslint/consistent-type-imports private _loadedCodeMirror?: typeof import("../resources/codemirror"); + private _completionInfoPopover?: WaPopup; + + private _completionInfoContainer?: HTMLDivElement; + + private _completionInfoDestroy?: () => void; + + private _completionInfoRequest = 0; + + private _completionInfoKey?: string; + + private _completionInfoFrame?: number; + private _editorToolbar?: HaIconButtonToolbar; private _iconList?: Completion[]; @@ -155,6 +169,14 @@ export class HaCodeEditor extends ReactiveElement { public disconnectedCallback() { fireEvent(this, "dialog-set-fullscreen", false); + this._clearCompletionInfo(); + if (this._completionInfoFrame !== undefined) { + cancelAnimationFrame(this._completionInfoFrame); + this._completionInfoFrame = undefined; + } + this._completionInfoPopover?.remove(); + this._completionInfoPopover = undefined; + this._completionInfoContainer = undefined; super.disconnectedCallback(); this.removeEventListener("keydown", stopPropagation); this.removeEventListener("keydown", this._handleKeyDown); @@ -288,6 +310,9 @@ export class HaCodeEditor extends ReactiveElement { this._loadedCodeMirror.foldingCompartment.of( this._getFoldingExtensions() ), + this._loadedCodeMirror.tooltips({ + position: "absolute", + }), ...(this.placeholder ? [placeholder(this.placeholder)] : []), ]; @@ -595,6 +620,157 @@ export class HaCodeEditor extends ReactiveElement { return completionInfo; }; + private _getCompletionInfo = ( + completion: Completion + ): CompletionInfo | Promise | null => { + if (this.hass && completion.label in this.hass.states) { + return this._renderInfo(completion); + } + + if (completion.label.startsWith("mdi:")) { + return renderIcon(completion); + } + + return null; + }; + + private _ensureCompletionInfoPopover(): WaPopup { + if (!this._completionInfoPopover) { + this._completionInfoPopover = document.createElement( + "wa-popup" + ) as WaPopup; + this._completionInfoPopover.classList.add("completion-info-popover"); + this._completionInfoPopover.placement = "right-start"; + this._completionInfoPopover.distance = 4; + this._completionInfoPopover.flip = true; + this._completionInfoPopover.flipFallbackPlacements = + "left-start bottom-start top-start"; + this._completionInfoPopover.shift = true; + this._completionInfoPopover.shiftPadding = 8; + this._completionInfoPopover.autoSize = "both"; + this._completionInfoPopover.autoSizePadding = 8; + + this._completionInfoContainer = document.createElement("div"); + this._completionInfoPopover.appendChild(this._completionInfoContainer); + this.renderRoot.appendChild(this._completionInfoPopover); + } + + return this._completionInfoPopover; + } + + private _clearCompletionInfo() { + this._completionInfoRequest += 1; + this._completionInfoKey = undefined; + this._completionInfoDestroy?.(); + this._completionInfoDestroy = undefined; + this._completionInfoContainer?.replaceChildren(); + + if (this._completionInfoPopover?.active) { + this._completionInfoPopover.active = false; + } + } + + private _renderCompletionInfoContent(info: CompletionInfo) { + this._completionInfoDestroy?.(); + this._completionInfoDestroy = undefined; + + if (!this._completionInfoContainer) { + return; + } + + if (info === null) { + this._completionInfoContainer.replaceChildren(); + return; + } + + if ("nodeType" in info) { + this._completionInfoContainer.replaceChildren(info); + return; + } + + this._completionInfoContainer.replaceChildren(info.dom); + this._completionInfoDestroy = info.destroy; + } + + private _syncCompletionInfoPopover = () => { + if (this._completionInfoFrame !== undefined) { + cancelAnimationFrame(this._completionInfoFrame); + } + + this._completionInfoFrame = requestAnimationFrame(() => { + this._completionInfoFrame = undefined; + this._syncCompletionInfoPopoverNow(); + }); + }; + + private _syncCompletionInfoPopoverNow = () => { + if (!this.codemirror || !this._loadedCodeMirror) { + return; + } + + if (window.matchMedia("(max-width: 600px)").matches) { + this._clearCompletionInfo(); + return; + } + + const completion = this._loadedCodeMirror.selectedCompletion( + this.codemirror.state + ); + const selectedOption = this.codemirror.dom.querySelector( + ".cm-tooltip-autocomplete li[aria-selected]" + ) as HTMLElement | null; + + if (!completion || !selectedOption) { + this._clearCompletionInfo(); + return; + } + + const infoResult = this._getCompletionInfo(completion); + + if (!infoResult) { + this._clearCompletionInfo(); + return; + } + + const requestId = ++this._completionInfoRequest; + const infoKey = completion.label; + const popover = this._ensureCompletionInfoPopover(); + popover.anchor = selectedOption; + + const showPopover = async (info: CompletionInfo) => { + if (requestId !== this._completionInfoRequest) { + if (info && typeof info === "object" && "destroy" in info) { + info.destroy?.(); + } + return; + } + + if (infoKey !== this._completionInfoKey) { + this._renderCompletionInfoContent(info); + this._completionInfoKey = infoKey; + } + + await popover.updateComplete; + popover.active = true; + popover.reposition(); + }; + + if ("then" in infoResult) { + infoResult.then(showPopover).catch(() => { + if (requestId === this._completionInfoRequest) { + this._clearCompletionInfo(); + } + }); + return; + } + + showPopover(infoResult).catch(() => { + if (requestId === this._completionInfoRequest) { + this._clearCompletionInfo(); + } + }); + }; + private _getStates = memoizeOne((states: HassEntities): Completion[] => { if (!states) { return []; @@ -604,7 +780,6 @@ export class HaCodeEditor extends ReactiveElement { type: "variable", label: key, detail: states[key].attributes.friendly_name, - info: this._renderInfo, })); return options; @@ -778,7 +953,6 @@ export class HaCodeEditor extends ReactiveElement { type: "variable", label: `mdi:${icon.name}`, detail: icon.keywords.join(", "), - info: renderIcon, })); } @@ -806,6 +980,7 @@ export class HaCodeEditor extends ReactiveElement { private _onUpdate = (update: ViewUpdate): void => { this._canUndo = !this.readOnly && undoDepth(update.state) > 0; this._canRedo = !this.readOnly && redoDepth(update.state) > 0; + this._syncCompletionInfoPopover(); if (!update.docChanged) { return; } @@ -925,9 +1100,31 @@ export class HaCodeEditor extends ReactiveElement { padding: 8px; } + wa-popup.completion-info-popover { + --auto-size-available-width: min( + 420px, + calc(var(--safe-width) - var(--ha-space-8)) + ); + } + + wa-popup.completion-info-popover::part(popup) { + padding: 0; + color: var(--primary-text-color); + background-color: var( + --code-editor-background-color, + var(--card-background-color) + ); + border: 1px solid var(--divider-color); + border-radius: var(--mdc-shape-medium, 4px); + box-shadow: + 0px 5px 5px -3px rgb(0 0 0 / 20%), + 0px 8px 10px 1px rgb(0 0 0 / 14%), + 0px 3px 14px 2px rgb(0 0 0 / 12%); + } + /* Hide completion info on narrow screens */ @media (max-width: 600px) { - .cm-completionInfo, + wa-popup.completion-info-popover, .completion-info { display: none; } diff --git a/src/resources/codemirror.ts b/src/resources/codemirror.ts index 3e199d79cf..1d8893ae1f 100644 --- a/src/resources/codemirror.ts +++ b/src/resources/codemirror.ts @@ -12,7 +12,7 @@ import type { KeyBinding } from "@codemirror/view"; import { EditorView } from "@codemirror/view"; import { tags } from "@lezer/highlight"; -export { autocompletion } from "@codemirror/autocomplete"; +export { autocompletion, selectedCompletion } from "@codemirror/autocomplete"; export { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; export { highlightingFor, foldGutter } from "@codemirror/language"; export { @@ -32,6 +32,7 @@ export { lineNumbers, rectangularSelection, dropCursor, + tooltips, } from "@codemirror/view"; export { indentationMarkers } from "@replit/codemirror-indentation-markers"; export { tags } from "@lezer/highlight"; @@ -151,10 +152,22 @@ export const haTheme = EditorView.theme({ "var(--code-editor-background-color, var(--card-background-color))", border: "1px solid var(--divider-color)", borderRadius: "var(--mdc-shape-medium, 4px)", + maxWidth: "min(420px, calc(var(--safe-width) - var(--ha-space-8)))", + boxSizing: "border-box", boxShadow: "0px 5px 5px -3px rgb(0 0 0 / 20%), 0px 8px 10px 1px rgb(0 0 0 / 14%), 0px 3px 14px 2px rgb(0 0 0 / 12%)", }, + ".cm-tooltip.cm-tooltip-autocomplete": { + maxWidth: + "min(420px, calc(var(--safe-width) - var(--ha-space-8)), calc(100% - var(--ha-space-2)))", + }, + + ".cm-tooltip-autocomplete > ul": { + maxWidth: "100%", + boxSizing: "border-box", + }, + "& .cm-tooltip.cm-tooltip-autocomplete > ul > li": { padding: "4px 8px", }, @@ -177,15 +190,6 @@ export const haTheme = EditorView.theme({ color: "var(--text-primary-color)", }, - "& .cm-completionInfo.cm-completionInfo-right": { - left: "calc(100% + 4px)", - }, - - "& .cm-tooltip.cm-completionInfo": { - padding: "4px 8px", - marginTop: "-5px", - }, - ".cm-selectionMatch": { backgroundColor: "rgba(var(--rgb-primary-color), 0.1)", }, From 7a310812e0dc852a2865c944f245bf768fe0a64a Mon Sep 17 00:00:00 2001 From: Tom Carpenter Date: Wed, 11 Mar 2026 06:12:22 +0000 Subject: [PATCH 55/69] Fix energy dashboard date picker opening direction (#30090) * Add Opening Direction to Date Picker Config * Force date picker opening direction on energy dash --- .../energy-overview-view-strategy.ts | 2 ++ .../energy/strategies/energy-view-strategy.ts | 2 ++ .../energy/strategies/gas-view-strategy.ts | 2 ++ .../energy/strategies/water-view-strategy.ts | 2 ++ .../energy/hui-energy-date-selection-card.ts | 19 ++++++++++++++++--- src/panels/lovelace/cards/types.ts | 6 ++++++ 6 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/panels/energy/strategies/energy-overview-view-strategy.ts b/src/panels/energy/strategies/energy-overview-view-strategy.ts index 42011fb031..2905ce0cfe 100644 --- a/src/panels/energy/strategies/energy-overview-view-strategy.ts +++ b/src/panels/energy/strategies/energy-overview-view-strategy.ts @@ -25,6 +25,8 @@ export class EnergyOverviewViewStrategy extends ReactiveElement { card: { type: "energy-date-selection", collection_key: collectionKey, + opening_direction: "right", + vertical_opening_direction: "up", }, }, }; diff --git a/src/panels/energy/strategies/energy-view-strategy.ts b/src/panels/energy/strategies/energy-view-strategy.ts index 075cd0b394..dea22e7a3f 100644 --- a/src/panels/energy/strategies/energy-view-strategy.ts +++ b/src/panels/energy/strategies/energy-view-strategy.ts @@ -33,6 +33,8 @@ export class EnergyViewStrategy extends ReactiveElement { card: { type: "energy-date-selection", collection_key: collectionKey, + opening_direction: "right", + vertical_opening_direction: "up", }, }, }; diff --git a/src/panels/energy/strategies/gas-view-strategy.ts b/src/panels/energy/strategies/gas-view-strategy.ts index 7c6001f4b3..59a754daa7 100644 --- a/src/panels/energy/strategies/gas-view-strategy.ts +++ b/src/panels/energy/strategies/gas-view-strategy.ts @@ -24,6 +24,8 @@ export class GasViewStrategy extends ReactiveElement { card: { type: "energy-date-selection", collection_key: collectionKey, + opening_direction: "right", + vertical_opening_direction: "up", }, }, }; diff --git a/src/panels/energy/strategies/water-view-strategy.ts b/src/panels/energy/strategies/water-view-strategy.ts index c524fdb92a..64209107b3 100644 --- a/src/panels/energy/strategies/water-view-strategy.ts +++ b/src/panels/energy/strategies/water-view-strategy.ts @@ -25,6 +25,8 @@ export class WaterViewStrategy extends ReactiveElement { card: { type: "energy-date-selection", collection_key: collectionKey, + opening_direction: "right", + vertical_opening_direction: "up", }, }, }; diff --git a/src/panels/lovelace/cards/energy/hui-energy-date-selection-card.ts b/src/panels/lovelace/cards/energy/hui-energy-date-selection-card.ts index ca7440b93c..35394cd96c 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-date-selection-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-date-selection-card.ts @@ -6,7 +6,7 @@ import type { HomeAssistant } from "../../../../types"; import { hasConfigChanged } from "../../common/has-changed"; import "../../components/hui-energy-period-selector"; import type { LovelaceCard, LovelaceGridOptions } from "../../types"; -import type { EnergyCardBaseConfig } from "../types"; +import type { EnergyDateSelectorCardConfig } from "../types"; @customElement("hui-energy-date-selection-card") export class HuiEnergyDateSelectionCard @@ -15,7 +15,7 @@ export class HuiEnergyDateSelectionCard { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _config?: EnergyCardBaseConfig; + @state() private _config?: EnergyDateSelectorCardConfig; public getCardSize(): Promise | number { return 1; @@ -28,7 +28,7 @@ export class HuiEnergyDateSelectionCard }; } - public setConfig(config: EnergyCardBaseConfig): void { + public setConfig(config: EnergyDateSelectorCardConfig): void { this._config = config; } @@ -45,12 +45,25 @@ export class HuiEnergyDateSelectionCard return nothing; } + const verticalOpeningDirection = + this._config.vertical_opening_direction === "auto" + ? undefined + : this._config.vertical_opening_direction; + + const openingDirection = + this._config.opening_direction === "auto" + ? undefined + : this._config.opening_direction; + return html`
diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index f70e7815be..18a51ca538 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -169,6 +169,12 @@ export interface EnergyCardBaseConfig extends LovelaceCardConfig { collection_key?: string; } +export interface EnergyDateSelectorCardConfig extends EnergyCardBaseConfig { + vertical_opening_direction?: "auto" | "up" | "down"; + opening_direction?: "auto" | "right" | "left" | "center" | "inline"; + disable_compare?: boolean; +} + export interface EnergyDistributionCardConfig extends EnergyCardBaseConfig { type: "energy-distribution"; title?: string; From 3feb40a8f4abbc30e38f551137ef6719ac00adf5 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 12 Mar 2026 11:27:43 +0100 Subject: [PATCH 56/69] Add token for brands url in hassUrl helper (#30111) --- src/components/entity/state-badge.ts | 2 -- src/panels/lovelace/badges/hui-entity-badge.ts | 6 +----- src/panels/lovelace/cards/hui-tile-card.ts | 6 +----- src/state/connection-mixin.ts | 7 ++++++- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/components/entity/state-badge.ts b/src/components/entity/state-badge.ts index d1233c72e0..f15f36178f 100644 --- a/src/components/entity/state-badge.ts +++ b/src/components/entity/state-badge.ts @@ -15,7 +15,6 @@ import { iconColorCSS } from "../../common/style/icon_color_css"; import { cameraUrlWithWidthHeight } from "../../data/camera"; import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate"; import type { HomeAssistant } from "../../types"; -import { addBrandsAuth } from "../../util/brands-url"; import "../ha-state-icon"; @customElement("state-badge") @@ -141,7 +140,6 @@ export class StateBadge extends LitElement { if (this.hass) { imageUrl = this.hass.hassUrl(imageUrl); } - imageUrl = addBrandsAuth(imageUrl, this.hass?.auth.data.hassUrl); if (domain === "camera") { imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80); } diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index d9fbb82094..db38a3cfd5 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -18,7 +18,6 @@ import "../../../components/ha-svg-icon"; import { cameraUrlWithWidthHeight } from "../../../data/camera"; import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import type { HomeAssistant } from "../../../types"; -import { addBrandsAuth } from "../../../util/brands-url"; import { actionHandler } from "../common/directives/action-handler-directive"; import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; import { findEntities } from "../common/find-entities"; @@ -144,10 +143,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { if (!entityPicture) return undefined; - let imageUrl = addBrandsAuth( - this.hass!.hassUrl(entityPicture), - this.hass?.auth.data.hassUrl - ); + let imageUrl = this.hass!.hassUrl(entityPicture); if (computeStateDomain(stateObj) === "camera") { imageUrl = cameraUrlWithWidthHeight(imageUrl, 32, 32); } diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index 90d2c1ed5c..71f2510831 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -21,7 +21,6 @@ import { cameraUrlWithWidthHeight } from "../../../data/camera"; import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import "../../../state-display/state-display"; import type { HomeAssistant } from "../../../types"; -import { addBrandsAuth } from "../../../util/brands-url"; import "../card-features/hui-card-features"; import type { LovelaceCardFeatureContext } from "../card-features/types"; import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; @@ -159,10 +158,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { if (!entityPicture) return undefined; - let imageUrl = addBrandsAuth( - this.hass!.hassUrl(entityPicture), - this.hass?.auth.data.hassUrl - ); + let imageUrl = this.hass!.hassUrl(entityPicture); if (computeDomain(entity.entity_id) === "camera") { imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80); } diff --git a/src/state/connection-mixin.ts b/src/state/connection-mixin.ts index 2239d57b44..9b41ffba29 100644 --- a/src/state/connection-mixin.ts +++ b/src/state/connection-mixin.ts @@ -31,6 +31,7 @@ import { subscribeFloorRegistry } from "../data/ws-floor_registry"; import { subscribePanels } from "../data/ws-panels"; import { translationMetadata } from "../resources/translations-metadata"; import { + addBrandsAuth, clearBrandsTokenRefresh, fetchAndScheduleBrandsAccessToken, } from "../util/brands-url"; @@ -88,7 +89,11 @@ export const connectionMixin = >( suspendWhenHidden: true, enableShortcuts: true, moreInfoEntityId: null, - hassUrl: (path = "") => new URL(path, auth.data.hassUrl).toString(), + hassUrl: (path = "") => + addBrandsAuth( + new URL(path, auth.data.hassUrl).toString(), + auth.data.hassUrl + ), callService: async ( domain, service, From 9c4aacdb1fbe7b944f1fa3e6b8076fa7894da8c9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 12 Mar 2026 22:08:30 +0100 Subject: [PATCH 57/69] Bumped version to 20260312.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0cf2945aa4..67752912e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20260304.0" +version = "20260312.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 6b6ad8dd2c1d5dd9614eba988b0729e32e261451 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 18 Mar 2026 13:00:17 +0100 Subject: [PATCH 58/69] Preserve entity unit in gas and water flow rate badges (#30116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Preserve entity unit_of_measurement in gas and water flow rate badges The gas and water total badges on the energy dashboard Now tab previously converted all flow rate values to L/min and then formatted them as either L/min or gal/min based on the unit system. This meant entities reporting in m³/h or other units always displayed incorrectly. Now the badges preserve the unit_of_measurement from the entities. If all entities share the same unit, the raw values are summed directly. If they differ, values are converted through L/min as an intermediate and displayed in the first entity's unit. * Extract shared computeTotalFlowRate to energy.ts --- src/data/energy.ts | 67 ++++++++++++++++++- .../badges/energy/hui-gas-total-badge.ts | 37 +++------- .../badges/energy/hui-water-total-badge.ts | 37 +++------- 3 files changed, 84 insertions(+), 57 deletions(-) diff --git a/src/data/energy.ts b/src/data/energy.ts index 3cafae12a6..dc0f7c29c9 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -1421,7 +1421,7 @@ export const calculateSolarConsumedGauge = ( /** Exact number of liters in one US gallon */ const LITERS_PER_GALLON = 3.785411784; -const FLOW_RATE_TO_LMIN: Record = { +export const FLOW_RATE_TO_LMIN: Record = { "m³/h": 1000 / 60, "m³/min": 1000, "m³/s": 60000, @@ -1458,6 +1458,71 @@ export const getFlowRateFromState = ( return value * factor; }; +/** + * Compute the total flow rate across all energy sources of a given type. + * Used by gas and water total badges. + */ +export const computeTotalFlowRate = ( + sourceType: "gas" | "water", + prefs: EnergyPreferences, + states: HomeAssistant["states"], + entities: Set +): { value: number; unit: string } => { + entities.clear(); + + let targetUnit: string | undefined; + let totalFlow = 0; + + prefs.energy_sources.forEach((source) => { + if (source.type !== sourceType || !source.stat_rate) { + return; + } + + const entityId = source.stat_rate; + entities.add(entityId); + + const stateObj = states[entityId]; + if (!stateObj) { + return; + } + + const rawValue = parseFloat(stateObj.state); + if (isNaN(rawValue) || rawValue <= 0) { + return; + } + + const entityUnit = stateObj.attributes.unit_of_measurement; + if (!entityUnit) { + return; + } + + if (targetUnit === undefined) { + targetUnit = entityUnit; + totalFlow += rawValue; + return; + } + + if (entityUnit === targetUnit) { + totalFlow += rawValue; + return; + } + + const sourceFactor = FLOW_RATE_TO_LMIN[entityUnit]; + const targetFactor = FLOW_RATE_TO_LMIN[targetUnit]; + + if (sourceFactor !== undefined && targetFactor !== undefined) { + totalFlow += (rawValue * sourceFactor) / targetFactor; + } else { + totalFlow += rawValue; + } + }); + + return { + value: Math.max(0, totalFlow), + unit: targetUnit ?? "", + }; +}; + /** * Format a flow rate value (in L/min) to a human-readable string using * the preferred unit system: metric → L/min, imperial → gal/min. diff --git a/src/panels/lovelace/badges/energy/hui-gas-total-badge.ts b/src/panels/lovelace/badges/energy/hui-gas-total-badge.ts index 87f212e0ce..3ff3f77d8d 100644 --- a/src/panels/lovelace/badges/energy/hui-gas-total-badge.ts +++ b/src/panels/lovelace/badges/energy/hui-gas-total-badge.ts @@ -5,11 +5,11 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../../../components/ha-badge"; import "../../../../components/ha-svg-icon"; -import type { EnergyData, EnergyPreferences } from "../../../../data/energy"; +import { formatNumber } from "../../../../common/number/format_number"; +import type { EnergyData } from "../../../../data/energy"; import { - formatFlowRateShort, + computeTotalFlowRate, getEnergyDataCollection, - getFlowRateFromState, } from "../../../../data/energy"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; @@ -66,37 +66,18 @@ export class HuiGasTotalBadge return false; } - private _getCurrentFlowRate(entityId: string): number { - this._entities.add(entityId); - return getFlowRateFromState(this.hass.states[entityId]) ?? 0; - } - - private _computeTotalFlowRate(prefs: EnergyPreferences): number { - this._entities.clear(); - - let totalFlow = 0; - - prefs.energy_sources.forEach((source) => { - if (source.type === "gas" && source.stat_rate) { - const value = this._getCurrentFlowRate(source.stat_rate); - if (value > 0) totalFlow += value; - } - }); - - return Math.max(0, totalFlow); - } - protected render() { if (!this._config || !this._data) { return nothing; } - const flowRate = this._computeTotalFlowRate(this._data.prefs); - const displayValue = formatFlowRateShort( - this.hass.locale, - this.hass.config.unit_system.length, - flowRate + const { value, unit } = computeTotalFlowRate( + "gas", + this._data.prefs, + this.hass.states, + this._entities ); + const displayValue = `${formatNumber(value, this.hass.locale, { maximumFractionDigits: 1 })} ${unit}`; const name = this._config.title || diff --git a/src/panels/lovelace/badges/energy/hui-water-total-badge.ts b/src/panels/lovelace/badges/energy/hui-water-total-badge.ts index 2c3b694898..08b47dcdcd 100644 --- a/src/panels/lovelace/badges/energy/hui-water-total-badge.ts +++ b/src/panels/lovelace/badges/energy/hui-water-total-badge.ts @@ -5,11 +5,11 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../../../components/ha-badge"; import "../../../../components/ha-svg-icon"; -import type { EnergyData, EnergyPreferences } from "../../../../data/energy"; +import { formatNumber } from "../../../../common/number/format_number"; +import type { EnergyData } from "../../../../data/energy"; import { - formatFlowRateShort, + computeTotalFlowRate, getEnergyDataCollection, - getFlowRateFromState, } from "../../../../data/energy"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; @@ -66,37 +66,18 @@ export class HuiWaterTotalBadge return false; } - private _getCurrentFlowRate(entityId: string): number { - this._entities.add(entityId); - return getFlowRateFromState(this.hass.states[entityId]) ?? 0; - } - - private _computeTotalFlowRate(prefs: EnergyPreferences): number { - this._entities.clear(); - - let totalFlow = 0; - - prefs.energy_sources.forEach((source) => { - if (source.type === "water" && source.stat_rate) { - const value = this._getCurrentFlowRate(source.stat_rate); - if (value > 0) totalFlow += value; - } - }); - - return Math.max(0, totalFlow); - } - protected render() { if (!this._config || !this._data) { return nothing; } - const flowRate = this._computeTotalFlowRate(this._data.prefs); - const displayValue = formatFlowRateShort( - this.hass.locale, - this.hass.config.unit_system.length, - flowRate + const { value, unit } = computeTotalFlowRate( + "water", + this._data.prefs, + this.hass.states, + this._entities ); + const displayValue = `${formatNumber(value, this.hass.locale, { maximumFractionDigits: 1 })} ${unit}`; const name = this._config.title || From 22c0035e60697e58e4ff6f37549f621bcd684090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 13 Mar 2026 14:47:32 +0100 Subject: [PATCH 59/69] Fix formatting of ha-switch in cloud remote preferences panel (#30143) --- src/panels/config/cloud/account/cloud-remote-pref.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/panels/config/cloud/account/cloud-remote-pref.ts b/src/panels/config/cloud/account/cloud-remote-pref.ts index fc9e990505..303fec6d73 100644 --- a/src/panels/config/cloud/account/cloud-remote-pref.ts +++ b/src/panels/config/cloud/account/cloud-remote-pref.ts @@ -154,9 +154,11 @@ export class CloudRemotePref extends LitElement { "ui.panel.config.cloud.account.remote.external_activation_secondary" )} - +
From 4020bcec424a7abc6ab896f9c2f267c0d9f9a8d4 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 16 Mar 2026 12:32:50 +0000 Subject: [PATCH 60/69] Fix event entity row propagation (#30163) * Stop event entity row value propagation * Catch interaction --- src/panels/lovelace/entity-rows/hui-event-entity-row.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/entity-rows/hui-event-entity-row.ts b/src/panels/lovelace/entity-rows/hui-event-entity-row.ts index 143cb72a58..90dd5e95ed 100644 --- a/src/panels/lovelace/entity-rows/hui-event-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-event-entity-row.ts @@ -52,7 +52,11 @@ class HuiEventEntityRow extends LitElement implements LovelaceRow { } return html` - +
Date: Mon, 16 Mar 2026 14:27:44 +0100 Subject: [PATCH 61/69] Fix passing click handler to ha-switch in cloudhooks section (#30166) Fix passing clickhandler to ha-switch in cloudhooks section --- src/panels/config/cloud/account/cloud-webhooks.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/panels/config/cloud/account/cloud-webhooks.ts b/src/panels/config/cloud/account/cloud-webhooks.ts index 3ad296d0ed..bfe98895f8 100644 --- a/src/panels/config/cloud/account/cloud-webhooks.ts +++ b/src/panels/config/cloud/account/cloud-webhooks.ts @@ -103,8 +103,10 @@ export class CloudWebhooks extends LitElement { )} ` - : html` + : html` `} ` From 4f916abcbf67eed6a8752d3a5ae033d451f461bf Mon Sep 17 00:00:00 2001 From: Tom Carpenter Date: Tue, 17 Mar 2026 06:25:23 +0000 Subject: [PATCH 62/69] Remove duplicate final point in bar statistics-chart (#30175) For bar charts, we don't need to close out the final segment. All this does is produce a duplicate final bar. --- src/components/chart/statistics-chart.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 4caaa5f8dd..e85e9c31fc 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -613,10 +613,10 @@ export class StatisticsChart extends LitElement { } }); - // Close out the last stat segment at prevEndTime + // For line charts, close out the last stat segment at prevEndTime const lastEndTime = prevEndTime; const lastValues = prevValues; - if (lastEndTime && lastValues) { + if (this.chartType === "line" && lastEndTime && lastValues) { statDataSets.forEach((d, i) => { d.data!.push( this._transformDataValue([lastEndTime, ...lastValues[i]!]) From d3e1d556869b376dbcd1807ef2e50abf3375217b Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 17 Mar 2026 09:11:15 +0100 Subject: [PATCH 63/69] Fix negative monetary values displayed as positive (#30178) --- src/common/entity/compute_state_display.ts | 1 + .../entity/compute_state_display.test.ts | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index d875dc4b48..48044a160a 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -144,6 +144,7 @@ const computeStateToPartsFromEntityAttributes = ( fraction: "value", literal: "literal", currency: "unit", + minusSign: "value", }; const valueParts: ValuePart[] = []; diff --git a/test/common/entity/compute_state_display.test.ts b/test/common/entity/compute_state_display.test.ts index 8d9f59f207..707b6a5010 100644 --- a/test/common/entity/compute_state_display.test.ts +++ b/test/common/entity/compute_state_display.test.ts @@ -684,6 +684,26 @@ describe("computeStateDisplayFromEntityAttributes with numeric device classes", ); expect(result).toBe("$12.00"); }); + + it("Should format negative monetary device_class", () => { + const result = computeStateDisplayFromEntityAttributes( + // eslint-disable-next-line @typescript-eslint/no-empty-function + (() => {}) as any, + { + language: "en", + } as FrontendLocaleData, + [], + {} as HassConfig, + undefined, + "number.test", + { + device_class: "monetary", + unit_of_measurement: "USD", + }, + "-12" + ); + expect(result).toBe("-$12.00"); + }); }); describe("computeStateDisplayFromEntityAttributes datetime device calss", () => { From ccdd71dd6452579f5a66c1e827629849e099827f Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:15:35 -0700 Subject: [PATCH 64/69] Fix tag dialog (#30191) --- src/panels/config/tags/dialog-tag-detail.ts | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/panels/config/tags/dialog-tag-detail.ts b/src/panels/config/tags/dialog-tag-detail.ts index 83e4fa5b30..a68c73d603 100644 --- a/src/panels/config/tags/dialog-tag-detail.ts +++ b/src/panels/config/tags/dialog-tag-detail.ts @@ -35,6 +35,8 @@ class DialogTagDetail @state() private _open = false; + @state() private _qrReady = false; + public showDialog(params: TagDetailDialogParams): void { this._params = params; this._error = undefined; @@ -45,6 +47,11 @@ class DialogTagDetail this._id = ""; this._name = ""; } + + // Defer QR until dialog has had a chance to apply styles + requestAnimationFrame(() => { + this._qrReady = true; + }); } public closeDialog(): boolean { @@ -54,6 +61,7 @@ class DialogTagDetail private _dialogClosed() { this._params = undefined; + this._qrReady = false; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -125,13 +133,17 @@ class DialogTagDetail

- - + ${this._qrReady + ? html` + + + ` + : nothing}
` : ``} From e1a8616ab01eb31148f485e08e6508bc9c39cee2 Mon Sep 17 00:00:00 2001 From: Qusai Ismael Date: Wed, 18 Mar 2026 07:56:04 +0000 Subject: [PATCH 65/69] Fix missing conversation language picker in new pipeline dialog (#30194) --- .../ha-conversation-agent-picker.ts | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/components/ha-conversation-agent-picker.ts b/src/components/ha-conversation-agent-picker.ts index 4013016883..40b94fb817 100644 --- a/src/components/ha-conversation-agent-picker.ts +++ b/src/components/ha-conversation-agent-picker.ts @@ -43,30 +43,6 @@ export class HaConversationAgentPicker extends LitElement { return nothing; } let value = this.value; - if (!value && this.required) { - // Select Home Assistant conversation agent if it supports the language - for (const agent of this._agents) { - if ( - agent.id === "conversation.home_assistant" && - agent.supported_languages.includes(this.language!) - ) { - value = agent.id; - break; - } - } - if (!value) { - // Select the first agent that supports the language - for (const agent of this._agents) { - if ( - agent.supported_languages === "*" && - agent.supported_languages.includes(this.language!) - ) { - value = agent.id; - break; - } - } - } - } if (!value) { value = NONE; } @@ -170,6 +146,39 @@ export class HaConversationAgentPicker extends LitElement { this._agents = agents; + if (!this.value && this.required) { + let defaultValue: string | undefined; + // Select Home Assistant conversation agent if it supports the language + for (const agent of this._agents) { + if ( + agent.id === "conversation.home_assistant" && + (!this.language || + agent.supported_languages === "*" || + agent.supported_languages.includes(this.language)) + ) { + defaultValue = agent.id; + break; + } + } + if (!defaultValue) { + // Select the first agent that supports the language + for (const agent of this._agents) { + if ( + agent.supported_languages === "*" || + !this.language || + agent.supported_languages.includes(this.language) + ) { + defaultValue = agent.id; + break; + } + } + } + if (defaultValue) { + this.value = defaultValue; + fireEvent(this, "value-changed", { value: this.value }); + } + } + if (!this.value) { return; } From 10e8c2a148489d796a9036e84ac30e7a972505f2 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:56:35 +0100 Subject: [PATCH 66/69] Fix copy-to-clipboard in unsecure context (#30204) --- src/common/util/copy-clipboard.ts | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/common/util/copy-clipboard.ts b/src/common/util/copy-clipboard.ts index e6017e3871..6755c184b0 100644 --- a/src/common/util/copy-clipboard.ts +++ b/src/common/util/copy-clipboard.ts @@ -1,24 +1,5 @@ import { deepActiveElement } from "../dom/deep-active-element"; -const getClipboardFallbackRoot = (): HTMLElement => { - const activeElement = deepActiveElement(); - if (activeElement instanceof HTMLElement) { - let root: Node = activeElement.getRootNode(); - let host: HTMLElement | null = null; - - while (root instanceof ShadowRoot && root.host instanceof HTMLElement) { - host = root.host; - root = root.host.getRootNode(); - } - - if (host) { - return host; - } - } - - return document.body; -}; - export const copyToClipboard = async (str, rootEl?: HTMLElement) => { if (navigator.clipboard) { try { @@ -29,7 +10,7 @@ export const copyToClipboard = async (str, rootEl?: HTMLElement) => { } } - const root = rootEl || getClipboardFallbackRoot(); + const root = rootEl || deepActiveElement()?.getRootNode() || document.body; const el = document.createElement("textarea"); el.value = str; From eb8b2a9d17c28d7453ff7a81425a12d848184eec Mon Sep 17 00:00:00 2001 From: Tom Carpenter Date: Thu, 19 Mar 2026 09:33:51 +0000 Subject: [PATCH 67/69] Skip plotting state value on statistic graph if units mismatch (#30214) * Use isExternalStatistic helper for consistency * Remove redundant if condition We have `band = drawBands && ...`, so there is no point checking if `drawBands` is true inside `if (band && ...)`. * Skip plotting state value on statistic graph if units mismatch For example plotting a *F sensor on a *C chart - statistic data will be converted to *C, but the state value will still be in *F so the displayed point is wrong. Similarly if plotting a kW sensor on a W chart, the same is true - statistics get converted to W by recorder, but the state value would still be in kW. In other words the plotted state point is complete nonsense. If the units of the statistic state don't match the units of the graph, we should not be displaying the value on the graph. * Remove redundant this.unit check Co-authored-by: Petar Petrov --------- Co-authored-by: Petar Petrov --- src/components/chart/statistics-chart.ts | 55 +++++++++++++----------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index e85e9c31fc..8adf5eaffe 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -27,6 +27,7 @@ import { getDisplayUnit, getStatisticLabel, getStatisticMetadata, + isExternalStatistic, statisticsHaveType, } from "../../data/recorder"; import type { ECOption } from "../../resources/echarts/echarts"; @@ -398,7 +399,23 @@ export class StatisticsChart extends LitElement { endTime = new Date(); } - let unit: string | undefined | null; + // Try to determine chart unit if it has not already been set explicitly + if (!this.unit) { + let unit: string | undefined | null; + statisticsData.forEach(([statistic_id, _stats]) => { + const meta = statisticsMetaData?.[statistic_id]; + const statisticUnit = getDisplayUnit(this.hass, statistic_id, meta); + if (unit === undefined) { + unit = statisticUnit; + } else if (unit !== null && unit !== statisticUnit) { + // Clear unit if not all statistics have same unit + unit = null; + } + }); + if (unit) { + this.unit = unit; + } + } const names = this.names || {}; statisticsData.forEach(([statistic_id, stats]) => { @@ -408,18 +425,6 @@ export class StatisticsChart extends LitElement { name = getStatisticLabel(this.hass, statistic_id, meta); } - if (!this.unit) { - if (unit === undefined) { - unit = getDisplayUnit(this.hass, statistic_id, meta); - } else if ( - unit !== null && - unit !== getDisplayUnit(this.hass, statistic_id, meta) - ) { - // Clear unit if not all statistics have same unit - unit = null; - } - } - // array containing [value1, value2, etc] let prevValues: (number | null)[][] | null = null; let prevEndTime: Date | undefined; @@ -544,7 +549,7 @@ export class StatisticsChart extends LitElement { (series as LineSeriesOption).areaStyle = undefined; } else { series.stackOrder = "seriesAsc"; - if (drawBands && type === bandTop) { + if (type === bandTop) { (series as LineSeriesOption).areaStyle = { color: color + "3F", }; @@ -624,13 +629,19 @@ export class StatisticsChart extends LitElement { }); } - // Append current state if viewing recent data + // Check if we need to display most recent data. Allow 10m of leeway for "now", + // because stats are 5 minute aggregated const now = new Date(); - // allow 10m of leeway for "now", because stats are 5 minute aggregated - const isUpToNow = now.getTime() - endTime.getTime() <= 600000; - if (isUpToNow) { - // Skip external statistics (they have ":" in the ID) - if (!statistic_id.includes(":")) { + const displayCurrentState = now.getTime() - endTime.getTime() <= 600000; + + // Show current state if required, and units match (or are unknown) + const statisticUnit = getDisplayUnit(this.hass, statistic_id, meta); + if ( + displayCurrentState && + (!this.unit || !statisticUnit || this.unit === statisticUnit) + ) { + // Skip external statistics + if (!isExternalStatistic(statistic_id)) { const stateObj = this.hass.states[statistic_id]; if (stateObj) { const currentValue = parseFloat(stateObj.state); @@ -671,10 +682,6 @@ export class StatisticsChart extends LitElement { Array.prototype.push.apply(legendData, statLegendData); }); - if (unit) { - this.unit = unit; - } - legendData.forEach(({ id, name, color, borderColor }) => { // Add an empty series for the legend totalDataSets.push({ From 88c063ba2a010540dd83a07a95a72236502ec22f Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 23 Mar 2026 09:55:22 +0100 Subject: [PATCH 68/69] Fix hasReturnToGrid only checking first grid source in energy distribution card (#30273) --- .../lovelace/cards/energy/hui-energy-distribution-card.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts index 45e429825c..9d5f6b94d1 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -118,7 +118,8 @@ class HuiEnergyDistrubutionCard const hasBattery = types.battery !== undefined; const hasGas = types.gas !== undefined; const hasWater = types.water !== undefined; - const hasReturnToGrid = !!types.grid?.[0] && !!types.grid[0].stat_energy_to; + const hasReturnToGrid = + types.grid?.some((source) => !!source.stat_energy_to) ?? false; const { summedData, compareSummedData: _ } = getSummedData(this._data); const { consumption, compareConsumption: __ } = computeConsumptionData( From a2a38e1da7b9ba707c1a36c538f5dc6780f74e20 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 23 Mar 2026 12:40:43 +0100 Subject: [PATCH 69/69] Bumped version to 20260312.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 67752912e6..ad8ec1f120 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20260312.0" +version = "20260312.1" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend"