From de56c3376e257fe92b8a34b438de579be1a32be8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 26 May 2025 18:32:32 +0200 Subject: [PATCH 01/56] Bumped version to 20250526.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 169b86dbe8..9bc7f1e919 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250430.0" +version = "20250526.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 1611423ca5fbdd14d07d866065af1fc9c5e3b3e1 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 26 May 2025 19:21:44 +0200 Subject: [PATCH 02/56] Fix duplicated items in strategy editor (#25600) --- src/components/ha-items-display-editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-items-display-editor.ts b/src/components/ha-items-display-editor.ts index 77e1dfff35..e87ecfaef0 100644 --- a/src/components/ha-items-display-editor.ts +++ b/src/components/ha-items-display-editor.ts @@ -262,7 +262,7 @@ export class HaItemDisplayEditor extends LitElement { ]; } - return items.sort((a, b) => + return visibleItems.sort((a, b) => a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value) ); } From 9131bf6dfd234b553a7f6b2c951431669ee6ba2c Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 27 May 2025 10:08:58 +0300 Subject: [PATCH 03/56] Fix double history graphs for a disabled entity (#25604) --- src/data/history.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/data/history.ts b/src/data/history.ts index fb784b54f4..553421d404 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -640,6 +640,12 @@ export const mergeHistoryResults = ( } for (const item of ltsResult.line) { + if (item.unit === BLANK_UNIT) { + // disabled entities have no unit, so we need to find the unit from the history result + item.unit = + historyResult.line.find((line) => line.identifier === item.identifier) + ?.unit ?? BLANK_UNIT; + } const key = computeGroupKey( item.unit, item.device_class, From 1a57eeddde9034b01fad9738eb1b432fa158e710 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Tue, 27 May 2025 10:22:04 +0200 Subject: [PATCH 04/56] Fix sidebar loading and demo (#25606) --- demo/src/stubs/frontend.ts | 23 +++++++++++++++++++++++ src/components/ha-sidebar.ts | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/demo/src/stubs/frontend.ts b/demo/src/stubs/frontend.ts index ae4ac073fd..70a4d5a0d2 100644 --- a/demo/src/stubs/frontend.ts +++ b/demo/src/stubs/frontend.ts @@ -1,7 +1,30 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; +let changeFunction; + export const mockFrontend = (hass: MockHomeAssistant) => { hass.mockWS("frontend/get_user_data", () => ({ value: null, })); + hass.mockWS("frontend/set_user_data", ({ key, value }) => { + if (key === "sidebar") { + changeFunction?.({ + value: { + panelOrder: value.panelOrder || [], + hiddenPanels: value.hiddenPanels || [], + }, + }); + } + }); + hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => { + changeFunction = onChange; + onChange?.({ + value: { + panelOrder: [], + hiddenPanels: [], + }, + }); + // eslint-disable-next-line @typescript-eslint/no-empty-function + return () => {}; + }); }; diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index d2f77a9770..ea303ab4c8 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -368,7 +368,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { if (!this._panelOrder || !this._hiddenPanels) { return html` `; } From 77ee69b64d1cf61ff653fce96adfa8c1102fcece Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Tue, 27 May 2025 12:55:20 +0200 Subject: [PATCH 05/56] Fix font settings for button card (#25607) --- src/panels/lovelace/cards/hui-button-card.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts index f6cc9b0bcd..957c8a868b 100644 --- a/src/panels/lovelace/cards/hui-button-card.ts +++ b/src/panels/lovelace/cards/hui-button-card.ts @@ -224,19 +224,19 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { filter: colored ? stateColorBrightness(stateObj) : undefined, height: this._config.icon_height ? this._config.icon_height - : "", + : undefined, })} > ` - : ""} + : nothing} ${this._config.show_name ? html`${name}` - : ""} + : nothing} ${this._config.show_state && stateObj ? html` ${this.hass.formatEntityState(stateObj)} ` - : ""} + : nothing} `; } @@ -282,7 +282,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { align-items: center; text-align: center; padding: 4% 0; - font-size: 16.8px; + font-size: var(--ha-font-size-l); + line-height: var(--ha-line-height-condensed); height: 100%; box-sizing: border-box; justify-content: center; From 116716c51da210677e7313e4a82589d94be40c7f Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 27 May 2025 21:43:32 +0300 Subject: [PATCH 06/56] Fix duplicate legend items when comparing energy data (#25610) --- src/components/chart/ha-chart-base.ts | 40 ++++++++++++------- .../hui-energy-devices-detail-graph-card.ts | 18 +++++++++ 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 64db12b3df..2ecad752a3 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -220,11 +220,11 @@ export class HaChartBase extends LitElement { return nothing; } const datasets = ensureArray(this.data); - const items = (legend.data || - datasets + const items: LegendComponentOption["data"] = + legend.data || + ((datasets .filter((d) => (d.data as any[])?.length && (d.id || d.name)) - .map((d) => d.name ?? d.id) || - []) as string[]; + .map((d) => d.name ?? d.id) || []) as string[]); const isMobile = window.matchMedia( "all and (max-width: 450px), all and (max-height: 500px)" @@ -239,20 +239,32 @@ export class HaChartBase extends LitElement { })} >
    - ${items.map((item: string, index: number) => { + ${items.map((item, index) => { if (!this.expandLegend && index >= overflowLimit) { return nothing; } - const dataset = datasets.find( - (d) => d.id === item || d.name === item - ); - const color = dataset?.color as string; - const borderColor = dataset?.itemStyle?.borderColor as string; + let itemStyle: Record = {}; + let name = ""; + if (typeof item === "string") { + name = item; + const dataset = datasets.find( + (d) => d.id === item || d.name === item + ); + itemStyle = { + color: dataset?.color as string, + ...(dataset?.itemStyle as { borderColor?: string }), + }; + } else { + name = item.name ?? ""; + itemStyle = item.itemStyle ?? {}; + } + const color = itemStyle?.color as string; + const borderColor = itemStyle?.borderColor as string; return html`
  • -
    ${item}
    +
    ${name}
  • `; })} ${items.length > overflowLimit diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts index c3d9c5cc23..917d9e5950 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts @@ -6,6 +6,7 @@ import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import type { BarSeriesOption } from "echarts/charts"; +import type { LegendComponentOption } from "echarts/components"; import { getGraphColorByIndex } from "../../../../common/color/colors"; import { getEnergyColor } from "./common/color"; import "../../../../components/ha-card"; @@ -54,6 +55,8 @@ export class HuiEnergyDevicesDetailGraphCard @state() private _data?: EnergyData; + @state() private _legendData?: LegendComponentOption["data"]; + @state() private _start = startOfToday(); @state() private _end = endOfToday(); @@ -185,6 +188,7 @@ export class HuiEnergyDevicesDetailGraphCard legend: { show: true, type: "custom", + data: this._legendData, selected: this._hiddenStats.reduce((acc, stat) => { acc[stat] = false; return acc; @@ -310,6 +314,13 @@ export class HuiEnergyDevicesDetailGraphCard ); datasets.push(...processedData); + this._legendData = processedData.map((d) => ({ + name: d.name as string, + itemStyle: { + color: d.color as string, + borderColor: d.itemStyle?.borderColor as string, + }, + })); if (showUntracked) { const untrackedData = this._processUntracked( @@ -319,6 +330,13 @@ export class HuiEnergyDevicesDetailGraphCard false ); datasets.push(untrackedData); + this._legendData.push({ + name: untrackedData.name as string, + itemStyle: { + color: untrackedData.color as string, + borderColor: untrackedData.itemStyle?.borderColor as string, + }, + }); } fillDataGapsAndRoundCaps(datasets); From 6abdeeae209afe51b26d232f934c4098a8143a01 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 27 May 2025 21:46:03 +0300 Subject: [PATCH 07/56] Fix for history graph with tiny values (#25612) --- .../chart/state-history-chart-line.ts | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 5e11621b60..53cc3b926b 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -82,6 +82,8 @@ export class StateHistoryChartLine extends LitElement { private _chartTime: Date = new Date(); + private _previousYAxisLabelValue = 0; + protected render() { return html` { - const formatOptions = - value >= 1 || value <= -1 - ? undefined - : { - // show the first significant digit for tiny values - maximumFractionDigits: Math.max( - 2, - -Math.floor(Math.log10(Math.abs(value % 1 || 1))) - ), - }; - const label = formatNumber( - value, - this.hass.locale, - formatOptions - ); - const width = measureTextWidth(label, 12) + 5; - if (width > this._yWidth) { - this._yWidth = width; - fireEvent(this, "y-width-changed", { - value: this._yWidth, - chartIndex: this.chartIndex, - }); - } - return label; - }, + formatter: this._formatYAxisLabel, }, } as YAXisOption, legend: { @@ -745,6 +722,33 @@ export class StateHistoryChartLine extends LitElement { this._visualMap = visualMap.length > 0 ? visualMap : undefined; } + private _formatYAxisLabel = (value: number) => { + const formatOptions = + value >= 1 || value <= -1 + ? undefined + : { + // show the first significant digit for tiny values + maximumFractionDigits: Math.max( + 2, + // use the difference to the previous value to determine the number of significant digits #25526 + -Math.floor( + Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1)) + ) + ), + }; + const label = formatNumber(value, this.hass.locale, formatOptions); + const width = measureTextWidth(label, 12) + 5; + if (width > this._yWidth) { + this._yWidth = width; + fireEvent(this, "y-width-changed", { + value: this._yWidth, + chartIndex: this.chartIndex, + }); + } + this._previousYAxisLabelValue = value; + return label; + }; + private _clampYAxis(value?: number | ((values: any) => number)) { if (this.logarithmicScale) { // log(0) is -Infinity, so we need to set a minimum value From ae49de8e71cf9454fb9c17d9019725a7a1344840 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Tue, 27 May 2025 19:56:55 +0200 Subject: [PATCH 08/56] Fix typo in restore_entity_id_selected::confirm_text (#25615) --- src/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/translations/en.json b/src/translations/en.json index 02dc75f9d2..c24fbd49b8 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5131,7 +5131,7 @@ "restore_entity_id_selected": { "button": "Recreate entity IDs of selected", "confirm_title": "Recreate entity IDs?", - "confirm_text": "Are you sure you want to change the entity IDs of these entities? You will have to change you dashboards, automations and scripts to use the new entity IDs.", + "confirm_text": "Are you sure you want to change the entity IDs of these entities? You will have to change your dashboards, automations and scripts to use the new entity IDs.", "changes": "The following entity IDs will be updated:" }, "delete_selected": { From 06270c771f0f6562d1720a1adc97308fe71664e3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 27 May 2025 21:43:42 +0200 Subject: [PATCH 09/56] Bumped version to 20250527.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9bc7f1e919..839caacc93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250526.0" +version = "20250527.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 61d9b0d2a3b27d57afb6a4d93e255e05aa5e82f5 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 28 May 2025 15:03:17 +0200 Subject: [PATCH 10/56] Improve action picker UI and search (#25525) --- src/common/entity/valid_service_id.ts | 4 + src/components/entity/ha-entity-picker.ts | 9 +- src/components/ha-service-control.ts | 8 +- src/components/ha-service-picker.ts | 239 +++++++++++------- .../action/developer-tools-action.ts | 2 + .../state/developer-tools-state.ts | 1 + src/translations/en.json | 3 +- 7 files changed, 177 insertions(+), 89 deletions(-) create mode 100644 src/common/entity/valid_service_id.ts diff --git a/src/common/entity/valid_service_id.ts b/src/common/entity/valid_service_id.ts new file mode 100644 index 0000000000..97c88f6907 --- /dev/null +++ b/src/common/entity/valid_service_id.ts @@ -0,0 +1,4 @@ +const validServiceId = /^(\w+)\.(\w+)$/; + +export const isValidServiceId = (actionId: string) => + validServiceId.test(actionId); diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index ef39a4ef14..8d29711e52 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -51,6 +51,9 @@ export class HaEntityPicker extends LitElement { @property({ type: Boolean, attribute: "allow-custom-entity" }) public allowCustomEntity; + @property({ type: Boolean, attribute: "show-entity-id" }) + public showEntityId = false; + @property() public label?: string; @property() public value?: string; @@ -166,11 +169,15 @@ export class HaEntityPicker extends LitElement { `; }; + private get _showEntityId() { + return this.showEntityId || this.hass.userData?.showEntityIdPicker; + } + private _rowRenderer: ComboBoxLitRenderer = ( item, { index } ) => { - const showEntityId = this.hass.userData?.showEntityIdPicker; + const showEntityId = this._showEntityId; return html` diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts index 82f7ae547e..a31382ac2f 100644 --- a/src/components/ha-service-control.ts +++ b/src/components/ha-service-control.ts @@ -85,8 +85,11 @@ export class HaServiceControl extends LitElement { @property({ type: Boolean }) public narrow = false; - @property({ attribute: "show-advanced", type: Boolean }) public showAdvanced = - false; + @property({ attribute: "show-advanced", type: Boolean }) + public showAdvanced = false; + + @property({ attribute: "show-service-id", type: Boolean }) + public showServiceId = false; @property({ attribute: "hide-picker", type: Boolean, reflect: true }) public hidePicker = false; @@ -435,6 +438,7 @@ export class HaServiceControl extends LitElement { .value=${this._value?.action} .disabled=${this.disabled} @value-changed=${this._serviceChanged} + .showServiceId=${this.showServiceId} >`} ${this.hideDescription ? nothing diff --git a/src/components/ha-service-picker.ts b/src/components/ha-service-picker.ts index a72a1a463c..2d9c6c0853 100644 --- a/src/components/ha-service-picker.ts +++ b/src/components/ha-service-picker.ts @@ -1,15 +1,25 @@ +import { mdiRoomService } from "@mdi/js"; import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { html, LitElement, nothing, type TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; +import { isValidServiceId } from "../common/entity/valid_service_id"; import type { LocalizeFunc } from "../common/translations/localize"; -import { domainToName } from "../data/integration"; -import type { HomeAssistant } from "../types"; -import "./ha-combo-box"; -import "./ha-combo-box-item"; -import "./ha-service-icon"; import { getServiceIcons } from "../data/icons"; +import { domainToName } from "../data/integration"; +import type { HomeAssistant, ValueChangedEvent } from "../types"; +import "./ha-combo-box-item"; +import "./ha-generic-picker"; +import type { HaGenericPicker } from "./ha-generic-picker"; +import type { PickerComboBoxItem } from "./ha-picker-combo-box"; +import type { PickerValueRenderer } from "./ha-picker-field"; +import "./ha-service-icon"; + +interface ServiceComboBoxItem extends PickerComboBoxItem { + domain_name?: string; + service_id?: string; +} @customElement("ha-service-picker") class HaServicePicker extends LitElement { @@ -17,66 +27,121 @@ class HaServicePicker extends LitElement { @property({ type: Boolean }) public disabled = false; + @property() public label?: string; + + @property() public placeholder?: string; + @property() public value?: string; - @state() private _filter?: string; + @property({ attribute: "show-service-id", type: Boolean }) + public showServiceId = false; - protected willUpdate() { - if (!this.hasUpdated) { - this.hass.loadBackendTranslation("services"); - getServiceIcons(this.hass); - } + @query("ha-generic-picker") private _picker?: HaGenericPicker; + + public async open() { + await this.updateComplete; + await this._picker?.open(); } - private _rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = - (item) => html` - - - ${item.name} - ${item.name === item.service ? "" : item.service} - - `; + protected firstUpdated(props) { + super.firstUpdated(props); + this.hass.loadBackendTranslation("services"); + getServiceIcons(this.hass); + } - protected render() { - return html` - = ( + item, + { index } + ) => html` + + + ${item.primary} + ${item.secondary} + ${item.service_id && this.showServiceId + ? html` + ${item.service_id} + ` + : nothing} + ${item.domain_name + ? html` +
    + ${item.domain_name} +
    + ` + : nothing} +
    + `; + + private _valueRenderer: PickerValueRenderer = (value) => { + const serviceId = value; + const [domain, service] = serviceId.split("."); + + if (!this.hass.services[domain]?.[service]) { + return html` + + ${value} + `; + } + + const serviceName = + this.hass.localize(`component.${domain}.services.${service}.name`) || + this.hass.services[domain][service].name || + service; + + return html` + + ${serviceName} + ${this.showServiceId + ? html`${serviceId}` + : nothing} + `; + }; + + protected render(): TemplateResult { + const placeholder = + this.placeholder ?? + this.hass.localize("ui.components.service-picker.action"); + + return html` +
    + > + `; } + private _getItems = () => + this._services(this.hass.localize, this.hass.services); + private _services = memoizeOne( ( localize: LocalizeFunc, services: HomeAssistant["services"] - ): { - service: string; - name: string; - }[] => { + ): ServiceComboBoxItem[] => { if (!services) { return []; } - const result: { service: string; name: string }[] = []; + const items: ServiceComboBoxItem[] = []; Object.keys(services) .sort() @@ -84,56 +149,60 @@ class HaServicePicker extends LitElement { const services_keys = Object.keys(services[domain]).sort(); for (const service of services_keys) { - result.push({ - service: `${domain}.${service}`, - name: `${domainToName(localize, domain)}: ${ - this.hass.localize( - `component.${domain}.services.${service}.name` - ) || - services[domain][service].name || - service - }`, + const serviceId = `${domain}.${service}`; + const domainName = domainToName(localize, domain); + + const name = + this.hass.localize( + `component.${domain}.services.${service}.name` + ) || + services[domain][service].name || + service; + + const description = + this.hass.localize( + `component.${domain}.services.${service}.description` + ) || services[domain][service].description; + + items.push({ + id: serviceId, + primary: name, + secondary: description, + domain_name: domainName, + service_id: serviceId, + search_labels: [serviceId, domainName, name, description].filter( + Boolean + ), + sorting_label: serviceId, }); } }); - return result; + return items; } ); - private _filteredServices = memoizeOne( - ( - localize: LocalizeFunc, - services: HomeAssistant["services"], - filter?: string - ) => { - if (!services) { - return []; - } - const processedServices = this._services(localize, services); + private _valueChanged(ev: ValueChangedEvent) { + ev.stopPropagation(); + const value = ev.detail.value; - if (!filter) { - return processedServices; - } - const split_filter = filter.split(" "); - return processedServices.filter((service) => { - const lower_service_name = service.name.toLowerCase(); - const lower_service = service.service.toLowerCase(); - return split_filter.every( - (f) => lower_service_name.includes(f) || lower_service.includes(f) - ); - }); + if (!value) { + this._setValue(undefined); + return; } - ); - private _filterChanged(ev: CustomEvent): void { - this._filter = ev.detail.value.toLowerCase(); + if (!isValidServiceId(value)) { + return; + } + + this._setValue(value); } - private _valueChanged(ev) { - this.value = ev.detail.value; + private _setValue(value: string | undefined) { + this.value = value; + + fireEvent(this, "value-changed", { value }); fireEvent(this, "change"); - fireEvent(this, "value-changed", { value: this.value }); } } diff --git a/src/panels/developer-tools/action/developer-tools-action.ts b/src/panels/developer-tools/action/developer-tools-action.ts index 610b53be71..d964555e27 100644 --- a/src/panels/developer-tools/action/developer-tools-action.ts +++ b/src/panels/developer-tools/action/developer-tools-action.ts @@ -142,6 +142,7 @@ class HaPanelDevAction extends LitElement { .hass=${this.hass} .value=${this._serviceData?.action} @value-changed=${this._serviceChanged} + show-service-id > diff --git a/src/panels/developer-tools/state/developer-tools-state.ts b/src/panels/developer-tools/state/developer-tools-state.ts index 5b9596b3a0..30c76c952e 100644 --- a/src/panels/developer-tools/state/developer-tools-state.ts +++ b/src/panels/developer-tools/state/developer-tools-state.ts @@ -130,6 +130,7 @@ class HaPanelDevState extends LitElement { .value=${this._entityId} @value-changed=${this._entityIdChanged} allow-custom-entity + show-entity-id > ${this._entityId ? html` diff --git a/src/translations/en.json b/src/translations/en.json index c24fbd49b8..7fab97a767 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -873,7 +873,8 @@ } }, "service-picker": { - "action": "Action" + "action": "Action", + "no_match": "No matching actions found" }, "service-control": { "required": "This field is required", From 5371fd649c418ba033f5b32420fa7ce5ab8b025d Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Wed, 28 May 2025 10:04:54 +0200 Subject: [PATCH 11/56] Set markdown code line-height (#25618) --- src/components/ha-markdown.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-markdown.ts b/src/components/ha-markdown.ts index 51d4fdd49e..9e684e01ca 100644 --- a/src/components/ha-markdown.ts +++ b/src/components/ha-markdown.ts @@ -77,7 +77,7 @@ export class HaMarkdown extends LitElement { pre { padding: 16px; overflow: auto; - line-height: 1.45; + line-height: var(--ha-line-height-condensed); font-family: var(--ha-font-family-code); } h1, From b907dbefad278aae5cab03791a3fe69c5b39ff06 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 28 May 2025 12:04:54 +0200 Subject: [PATCH 12/56] Force narrow style for action, condition and trigger in flows (#25619) --- src/components/ha-form/ha-form.ts | 3 +++ src/components/ha-selector/ha-selector-action.ts | 3 +++ src/components/ha-selector/ha-selector-condition.ts | 3 +++ src/components/ha-selector/ha-selector-trigger.ts | 3 +++ src/components/ha-selector/ha-selector.ts | 3 +++ src/dialogs/config-flow/dialog-data-entry-flow.ts | 1 + src/dialogs/config-flow/step-flow-form.ts | 3 +++ 7 files changed, 19 insertions(+) diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index cbca7aca3c..fbc79a56dd 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -34,6 +34,8 @@ const getWarning = (obj, item) => (obj && item.name ? obj[item.name] : null); export class HaForm extends LitElement implements HaFormElement { @property({ attribute: false }) public hass?: HomeAssistant; + @property({ type: Boolean }) public narrow = false; + @property({ attribute: false }) public data!: HaFormDataContainer; @property({ attribute: false }) public schema!: readonly HaFormSchema[]; @@ -135,6 +137,7 @@ export class HaForm extends LitElement implements HaFormElement { ? html` `; } diff --git a/src/components/ha-selector/ha-selector-condition.ts b/src/components/ha-selector/ha-selector-condition.ts index 512b9657be..b96bd2894a 100644 --- a/src/components/ha-selector/ha-selector-condition.ts +++ b/src/components/ha-selector/ha-selector-condition.ts @@ -9,6 +9,8 @@ import type { HomeAssistant } from "../../types"; export class HaConditionSelector extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Boolean }) public narrow = false; + @property({ attribute: false }) public selector!: ConditionSelector; @property({ attribute: false }) public value?: Condition; @@ -24,6 +26,7 @@ export class HaConditionSelector extends LitElement { .disabled=${this.disabled} .conditions=${this.value || []} .hass=${this.hass} + .narrow=${this.narrow} > `; } diff --git a/src/components/ha-selector/ha-selector-trigger.ts b/src/components/ha-selector/ha-selector-trigger.ts index 9a580d8d5a..110a744b6e 100644 --- a/src/components/ha-selector/ha-selector-trigger.ts +++ b/src/components/ha-selector/ha-selector-trigger.ts @@ -11,6 +11,8 @@ import type { HomeAssistant } from "../../types"; export class HaTriggerSelector extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Boolean }) public narrow = false; + @property({ attribute: false }) public selector!: TriggerSelector; @property({ attribute: false }) public value?: Trigger; @@ -33,6 +35,7 @@ export class HaTriggerSelector extends LitElement { .disabled=${this.disabled} .triggers=${this._triggers(this.value)} .hass=${this.hass} + .narrow=${this.narrow} > `; } diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 69a5b59a25..47c2bdd8dd 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -69,6 +69,8 @@ const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]); export class HaSelector extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Boolean }) public narrow = false; + @property() public name?: string; @property({ attribute: false }) public selector!: Selector; @@ -127,6 +129,7 @@ export class HaSelector extends LitElement { return html` ${dynamicElement(`ha-selector-${this._type}`, { hass: this.hass, + narrow: this.narrow, name: this.name, selector: this._handleLegacySelector(this.selector), value: this.value, diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 5f141c3572..298f5742e2 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -349,6 +349,7 @@ class DataEntryFlowDialog extends LitElement { ${this._step.type === "form" ? html` Date: Wed, 28 May 2025 13:06:45 +0200 Subject: [PATCH 13/56] Fix missing helper for entity picker (#25622) --- src/components/entity/ha-entity-picker.ts | 1 + src/components/ha-generic-picker.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 8d29711e52..7803f079f7 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -397,6 +397,7 @@ export class HaEntityPicker extends LitElement { .autofocus=${this.autofocus} .allowCustomValue=${this.allowCustomEntity} .label=${this.label} + .helper=${this.helper} .searchLabel=${this.searchLabel} .notFoundLabel=${notFoundLabel} .placeholder=${placeholder} diff --git a/src/components/ha-generic-picker.ts b/src/components/ha-generic-picker.ts index 2c699382fd..687a548ede 100644 --- a/src/components/ha-generic-picker.ts +++ b/src/components/ha-generic-picker.ts @@ -104,8 +104,8 @@ export class HaGenericPicker extends LitElement { .getAdditionalItems=${this.getAdditionalItems} > `} - ${this._renderHelper()} + ${this._renderHelper()} `; } @@ -164,6 +164,10 @@ export class HaGenericPicker extends LitElement { display: block; margin: 0 0 8px; } + ha-input-helper-text { + display: block; + margin: 8px 0 0; + } `, ]; } From 139c8b3702eaac89967fb1e15c8750a7a0d93c6e Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 28 May 2025 13:07:22 +0200 Subject: [PATCH 14/56] Fix picker field height (#25623) --- src/components/ha-picker-field.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ha-picker-field.ts b/src/components/ha-picker-field.ts index 62663d2dfd..0d62e084bc 100644 --- a/src/components/ha-picker-field.ts +++ b/src/components/ha-picker-field.ts @@ -95,8 +95,8 @@ export class HaPickerField extends LitElement { border-end-start-radius: 0; --md-list-item-one-line-container-height: 56px; --md-list-item-two-line-container-height: 56px; - --md-list-item-top-space: 8px; - --md-list-item-bottom-space: 8px; + --md-list-item-top-space: 0px; + --md-list-item-bottom-space: 0px; --md-list-item-leading-space: 8px; --md-list-item-trailing-space: 8px; --ha-md-list-item-gap: 8px; From 48a3e1fd63811a6809a0d1ec90f4f32b25900cfc Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 28 May 2025 22:33:42 +0200 Subject: [PATCH 15/56] Put item at the top of picker result if there is an exact match with entity id (#25625) --- src/components/entity/ha-entity-picker.ts | 23 ++++++++++++++++++- src/components/entity/ha-statistic-picker.ts | 24 +++++++++++++++++++- src/components/ha-generic-picker.ts | 5 ++++ src/components/ha-picker-combo-box.ts | 20 +++++++++++++--- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 7803f079f7..ae7aa71088 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -23,7 +23,10 @@ import type { HomeAssistant } from "../../types"; import "../ha-combo-box-item"; import "../ha-generic-picker"; import type { HaGenericPicker } from "../ha-generic-picker"; -import type { PickerComboBoxItem } from "../ha-picker-combo-box"; +import type { + PickerComboBoxItem, + PickerComboBoxSearchFn, +} from "../ha-picker-combo-box"; import type { PickerValueRenderer } from "../ha-picker-field"; import "../ha-svg-icon"; import "./state-badge"; @@ -406,6 +409,7 @@ export class HaEntityPicker extends LitElement { .getItems=${this._getItems} .getAdditionalItems=${this._getAdditionalItems} .hideClearIcon=${this.hideClearIcon} + .searchFn=${this._searchFn} .valueRenderer=${this._valueRenderer} @value-changed=${this._valueChanged} > @@ -413,6 +417,23 @@ export class HaEntityPicker extends LitElement { `; } + private _searchFn: PickerComboBoxSearchFn = ( + search, + filteredItems + ) => { + // If there is exact match for entity id, put it first + const index = filteredItems.findIndex( + (item) => item.stateObj?.entity_id === search + ); + if (index === -1) { + return filteredItems; + } + + const [exactMatch] = filteredItems.splice(index, 1); + filteredItems.unshift(exactMatch); + return filteredItems; + }; + public async open() { await this.updateComplete; await this._picker?.open(); diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index 955071a8f2..3d20ab7a43 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -25,7 +25,10 @@ import "../ha-generic-picker"; import type { HaGenericPicker } from "../ha-generic-picker"; import "../ha-icon-button"; import "../ha-input-helper-text"; -import type { PickerComboBoxItem } from "../ha-picker-combo-box"; +import type { + PickerComboBoxItem, + PickerComboBoxSearchFn, +} from "../ha-picker-combo-box"; import type { PickerValueRenderer } from "../ha-picker-field"; import "../ha-svg-icon"; import "./state-badge"; @@ -470,6 +473,7 @@ export class HaStatisticPicker extends LitElement { .getItems=${this._getItems} .getAdditionalItems=${this._getAdditionalItems} .hideClearIcon=${this.hideClearIcon} + .searchFn=${this._searchFn} .valueRenderer=${this._valueRenderer} @value-changed=${this._valueChanged} > @@ -477,6 +481,24 @@ export class HaStatisticPicker extends LitElement { `; } + private _searchFn: PickerComboBoxSearchFn = ( + search, + filteredItems + ) => { + // If there is exact match for entity id or statistic id, put it first + const index = filteredItems.findIndex( + (item) => + item.stateObj?.entity_id === search || item.statistic_id === search + ); + if (index === -1) { + return filteredItems; + } + + const [exactMatch] = filteredItems.splice(index, 1); + filteredItems.unshift(exactMatch); + return filteredItems; + }; + private _valueChanged(ev: ValueChangedEvent) { ev.stopPropagation(); const value = ev.detail.value; diff --git a/src/components/ha-generic-picker.ts b/src/components/ha-generic-picker.ts index 687a548ede..92631333cf 100644 --- a/src/components/ha-generic-picker.ts +++ b/src/components/ha-generic-picker.ts @@ -12,6 +12,7 @@ import "./ha-picker-combo-box"; import type { HaPickerComboBox, PickerComboBoxItem, + PickerComboBoxSearchFn, } from "./ha-picker-combo-box"; import "./ha-picker-field"; import type { HaPickerField, PickerValueRenderer } from "./ha-picker-field"; @@ -57,6 +58,9 @@ export class HaGenericPicker extends LitElement { @property({ attribute: false }) public valueRenderer?: PickerValueRenderer; + @property({ attribute: false }) + public searchFn?: PickerComboBoxSearchFn; + @property({ attribute: "not-found-label", type: String }) public notFoundLabel?: string; @@ -102,6 +106,7 @@ export class HaGenericPicker extends LitElement { .notFoundLabel=${this.notFoundLabel} .getItems=${this.getItems} .getAdditionalItems=${this.getAdditionalItems} + .searchFn=${this.searchFn} > `} diff --git a/src/components/ha-picker-combo-box.ts b/src/components/ha-picker-combo-box.ts index 5280909ef8..9ea33ee20c 100644 --- a/src/components/ha-picker-combo-box.ts +++ b/src/components/ha-picker-combo-box.ts @@ -49,6 +49,12 @@ const DEFAULT_ROW_RENDERER: ComboBoxLitRenderer = (
    `; +export type PickerComboBoxSearchFn = ( + search: string, + filteredItems: T[], + allItems: T[] +) => T[]; + @customElement("ha-picker-combo-box") export class HaPickerComboBox extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -84,6 +90,9 @@ export class HaPickerComboBox extends LitElement { @property({ attribute: "not-found-label", type: String }) public notFoundLabel?: string; + @property({ attribute: false }) + public searchFn?: PickerComboBoxSearchFn; + @state() private _opened = false; @query("ha-combo-box", true) public comboBox!: HaComboBox; @@ -237,6 +246,7 @@ export class HaPickerComboBox extends LitElement { const fuse = new HaFuse(this._items, { shouldSort: false }, index); const results = fuse.multiTermsSearch(searchString); + let filteredItems = this._items as PickerComboBoxItem[]; if (results) { const items = results.map((result) => result.item); if (items.length === 0) { @@ -246,10 +256,14 @@ export class HaPickerComboBox extends LitElement { } const additionalItems = this._getAdditionalItems(searchString); items.push(...additionalItems); - target.filteredItems = items; - } else { - target.filteredItems = this._items; + filteredItems = items; } + + if (this.searchFn) { + filteredItems = this.searchFn(searchString, filteredItems, this._items); + } + + target.filteredItems = filteredItems; } private _setValue(value: string | undefined) { From a1819d6189e10dab5f7aeccd0e7c0e0789214366 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 28 May 2025 19:38:33 +0200 Subject: [PATCH 16/56] Improve search in add automation element dialog (#25626) * Improve search in add automation element dialog * Remove unexpected import * Take min character search into account --- .../add-automation-element-dialog.ts | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index 86dd1ee02e..729df0bba3 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -1,5 +1,4 @@ import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js"; -import type { IFuseOptions } from "fuse.js"; import Fuse from "fuse.js"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; @@ -46,6 +45,7 @@ import { haStyle, haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog"; import { PASTE_VALUE } from "./show-add-automation-element-dialog"; +import { HaFuse } from "../../../resources/fuse"; const TYPES = { trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS }, @@ -175,6 +175,40 @@ class DialogAddAutomationElement extends LitElement implements HassDialog { type: AddAutomationElementDialogParams["type"], group: string | undefined, filter: string, + domains: Set | undefined, + localize: LocalizeFunc, + services: HomeAssistant["services"], + manifests?: DomainManifestLookup + ): ListItem[] => { + const items = this._items(type, group, localize, services, manifests); + + const index = this._fuseIndex(items); + + const fuse = new HaFuse( + items, + { ignoreLocation: true, includeScore: true }, + index + ); + + const results = fuse.multiTermsSearch(filter); + if (results) { + return results.map((result) => result.item); + } + return this._getGroupItems( + type, + group, + domains, + localize, + services, + manifests + ); + } + ); + + private _items = memoizeOne( + ( + type: AddAutomationElementDialogParams["type"], + group: string | undefined, localize: LocalizeFunc, services: HomeAssistant["services"], manifests?: DomainManifestLookup @@ -189,24 +223,17 @@ class DialogAddAutomationElement extends LitElement implements HassDialog { ); const items = flattenGroups(groups).flat(); - if (type === "action") { items.push(...this._services(localize, services, manifests, group)); } - - const options: IFuseOptions = { - keys: ["key", "name", "description"], - isCaseSensitive: false, - ignoreLocation: true, - minMatchCharLength: Math.min(filter.length, 2), - threshold: 0.2, - ignoreDiacritics: true, - }; - const fuse = new Fuse(items, options); - return fuse.search(filter).map((result) => result.item); + return items; } ); + private _fuseIndex = memoizeOne((items: ListItem[]) => + Fuse.createIndex(["key", "name", "description"], items) + ); + private _getGroupItems = memoizeOne( ( type: AddAutomationElementDialogParams["type"], @@ -449,6 +476,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog { this._params.type, this._group, this._filter, + this._domains, this.hass.localize, this.hass.services, this._manifests From 46e05f10d116872fcf4171d59ada8b872e945c9c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 28 May 2025 22:35:50 +0200 Subject: [PATCH 17/56] Bumped version to 20250528.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 839caacc93..0fc881abf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250527.0" +version = "20250528.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 9f69347e1d0889d4feffb92ecf284e0f8714836f Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 28 May 2025 22:53:30 -0700 Subject: [PATCH 18/56] Cleanup some styling on disabled entity picker (#25632) --- src/components/ha-generic-picker.ts | 11 +++++++++-- src/components/ha-input-helper-text.ts | 7 ++++++- src/components/ha-multi-textfield.ts | 4 +++- src/components/ha-picker-field.ts | 12 ++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/components/ha-generic-picker.ts b/src/components/ha-generic-picker.ts index 92631333cf..98496a4b40 100644 --- a/src/components/ha-generic-picker.ts +++ b/src/components/ha-generic-picker.ts @@ -72,7 +72,9 @@ export class HaGenericPicker extends LitElement { protected render() { return html` - ${this.label ? html`` : nothing} + ${this.label + ? html`` + : nothing}
    ${!this._opened ? html` @@ -116,7 +118,9 @@ export class HaGenericPicker extends LitElement { private _renderHelper() { return this.helper - ? html`${this.helper}` + ? html`${this.helper}` : nothing; } @@ -165,6 +169,9 @@ export class HaGenericPicker extends LitElement { position: relative; display: block; } + label[disabled] { + color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.6)); + } label { display: block; margin: 0 0 8px; diff --git a/src/components/ha-input-helper-text.ts b/src/components/ha-input-helper-text.ts index 4af842ee74..6d817d1bf6 100644 --- a/src/components/ha-input-helper-text.ts +++ b/src/components/ha-input-helper-text.ts @@ -1,9 +1,11 @@ import type { TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; -import { customElement } from "lit/decorators"; +import { customElement, property } from "lit/decorators"; @customElement("ha-input-helper-text") class InputHelperText extends LitElement { + @property({ type: Boolean, reflect: true }) disabled = false; + protected render(): TemplateResult { return html``; } @@ -18,6 +20,9 @@ class InputHelperText extends LitElement { padding-inline-start: 16px; padding-inline-end: 16px; } + :host([disabled]) { + color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.6)); + } `; } diff --git a/src/components/ha-multi-textfield.ts b/src/components/ha-multi-textfield.ts index dd1d46b8b2..c40b6033b6 100644 --- a/src/components/ha-multi-textfield.ts +++ b/src/components/ha-multi-textfield.ts @@ -85,7 +85,9 @@ class HaMultiTextField extends LitElement {
    ${this.helper - ? html`${this.helper}` + ? html`${this.helper}` : nothing} `; } diff --git a/src/components/ha-picker-field.ts b/src/components/ha-picker-field.ts index 0d62e084bc..ba8513794f 100644 --- a/src/components/ha-picker-field.ts +++ b/src/components/ha-picker-field.ts @@ -88,6 +88,12 @@ export class HaPickerField extends LitElement { static get styles(): CSSResultGroup { return [ css` + ha-combo-box-item[disabled] { + background-color: var( + --mdc-text-field-disabled-fill-color, + whitesmoke + ); + } ha-combo-box-item { background-color: var(--mdc-text-field-fill-color, whitesmoke); border-radius: 4px; @@ -106,6 +112,12 @@ export class HaPickerField extends LitElement { } /* Add Similar focus style as the text field */ + ha-combo-box-item[disabled]:after { + background-color: var( + --mdc-text-field-disabled-line-color, + rgba(0, 0, 0, 0.42) + ); + } ha-combo-box-item:after { display: block; content: ""; From 228860a1ee268e0d1c11810159382345388c64d9 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 29 May 2025 11:05:26 +0300 Subject: [PATCH 19/56] Use theme variables for network graph labels (#25634) --- src/components/chart/ha-chart-base.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 2ecad752a3..37a12f07e6 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -494,6 +494,13 @@ export class HaChartBase extends LitElement { smooth: false, }, bar: { itemStyle: { barBorderWidth: 1.5 } }, + graph: { + label: { + color: style.getPropertyValue("--primary-text-color"), + textBorderColor: style.getPropertyValue("--primary-background-color"), + textBorderWidth: 2, + }, + }, categoryAxis: { axisLine: { show: false }, axisTick: { show: false }, From cd61725cf51f2ed78a190982cb4602744b0540c9 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 29 May 2025 15:29:24 +0300 Subject: [PATCH 20/56] Fix Z-WaveJS device count in dashboard (#25635) --- .../zwave_js/zwave_js-config-dashboard.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 15d2cf7926..aa4830a785 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 @@ -133,8 +133,11 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { if (ERROR_STATES.includes(this._configEntry.state)) { return this._renderErrorScreen(); } + const provisioningDevices = + this._provisioningEntries?.filter((entry) => !entry.nodeId).length ?? 0; const notReadyDevices = - this._network?.controller.nodes.filter((node) => !node.ready).length ?? 0; + (this._network?.controller.nodes.filter((node) => !node.ready).length ?? + 0) + provisioningDevices; return html` 0 From 7ceba218faba6177160823dda2c2c53520cab45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 29 May 2025 10:59:58 +0300 Subject: [PATCH 21/56] Fix Zigbee capitalization in manage device button (#25637) --- src/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/translations/en.json b/src/translations/en.json index 7fab97a767..316f78d7ec 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1782,7 +1782,7 @@ "buttons": { "add": "Add devices via this device", "remove": "Remove", - "manage": "Manage zigbee device", + "manage": "Manage Zigbee device", "reconfigure": "Reconfigure", "view_network": "View network" }, From e448268feb0799cc3c2015c4239a03fa2c2aaafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 29 May 2025 11:48:50 +0300 Subject: [PATCH 22/56] Spelling fixes (#25638) --- src/translations/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/translations/en.json b/src/translations/en.json index 316f78d7ec..0d2a4cc0e9 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1579,7 +1579,7 @@ "upload_in_progress": "A backup upload is currently in progress. The action will automatically proceed once the upload process is complete.", "restore_in_progress": "A backup restore is currently in progress. The action will automatically proceed once the restore process is complete.", "wait_for_backup": "Wait for the backup creation to finish", - "error_backup_state": "An error occured while getting the current backup state. Error: {error}", + "error_backup_state": "An error occurred while getting the current backup state. Error: {error}", "wait_for_upload": "Wait for backup upload to finish", "wait_for_restore": "Wait for backup restore to finish", "reload": { @@ -2378,7 +2378,7 @@ }, "generate": { "sync": { - "title": "Synchonization", + "title": "Synchronization", "name": "Backup name", "locations": "Locations", "locations_description": "What locations you want to automatically backup to.", From 560e3890b9158881c12773410e299e03b2d1cfc3 Mon Sep 17 00:00:00 2001 From: ildar170975 <71872483+ildar170975@users.noreply.github.com> Date: Fri, 30 May 2025 10:59:03 +0300 Subject: [PATCH 23/56] Revert #25027 "more-info-camera: disable download_snapshot if idle" (#25643) remove a check for "idle" --- src/dialogs/more-info/controls/more-info-camera.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dialogs/more-info/controls/more-info-camera.ts b/src/dialogs/more-info/controls/more-info-camera.ts index e3842dc053..e45fe3a633 100644 --- a/src/dialogs/more-info/controls/more-info-camera.ts +++ b/src/dialogs/more-info/controls/more-info-camera.ts @@ -45,8 +45,7 @@ class MoreInfoCamera extends LitElement { ${this.hass.localize( "ui.dialogs.more_info_control.camera.download_snapshot" From 4c78eb4797776fb05dfeed0d9becfe68bf26dc05 Mon Sep 17 00:00:00 2001 From: ildar170975 <71872483+ildar170975@users.noreply.github.com> Date: Fri, 30 May 2025 08:38:45 +0300 Subject: [PATCH 24/56] Add cyrilic letters to slugify() (#25647) * add cyrilic * Update src/common/string/slugify.ts --------- Co-authored-by: Petar Petrov --- src/common/string/slugify.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/common/string/slugify.ts b/src/common/string/slugify.ts index b7ddfed77c..fe5e0f537c 100644 --- a/src/common/string/slugify.ts +++ b/src/common/string/slugify.ts @@ -1,9 +1,19 @@ // https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1 export const slugify = (value: string, delimiter = "_") => { const a = - "àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·"; - const b = `aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}`; + "àáâäæãåāăąабçćčđďдèéêëēėęěеёэфğǵгḧхîïíīįìıİийкłлḿмñńǹňнôöòóœøōõőоṕпŕřрßśšşșсťțтûüùúūǘůűųувẃẍÿýыžźżз·"; + const b = `aaaaaaaaaaabcccdddeeeeeeeeeeefggghhiiiiiiiiijkllmmnnnnnoooooooooopprrrsssssstttuuuuuuuuuuvwxyyyzzzz${delimiter}`; const p = new RegExp(a.split("").join("|"), "g"); + const complex_cyrillic = { + ж: "zh", + х: "kh", + ц: "ts", + ч: "ch", + ш: "sh", + щ: "shch", + ю: "iu", + я: "ia", + }; let slugified; @@ -14,6 +24,7 @@ export const slugify = (value: string, delimiter = "_") => { .toString() .toLowerCase() .replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters + .replace(/[а-я]/g, (c) => complex_cyrillic[c] || "") // Replace some cyrillic characters .replace(/(\d),(?=\d)/g, "$1") // Remove Commas between numbers .replace(/[^a-z0-9]+/g, delimiter) // Replace all non-word characters .replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter From aebd6350c0cadeb114f46b0ec8750828dd50a26b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 May 2025 15:34:03 +0200 Subject: [PATCH 25/56] fix line height entity card (#25652) --- src/panels/lovelace/cards/hui-entity-card.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts index ae51480f36..3dbe834aee 100644 --- a/src/panels/lovelace/cards/hui-entity-card.ts +++ b/src/panels/lovelace/cards/hui-entity-card.ts @@ -301,7 +301,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - line-height: var(--ha-line-height-expanded); + line-height: var(--ha-line-height-condensed); } .value { From 74f9c1551ec0e2208044eb53ec42f0076009797f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 May 2025 16:47:54 +0200 Subject: [PATCH 26/56] Add label to collapse button in data table groups (#25653) --- src/components/data-table/ha-data-table.ts | 8 +++++--- src/translations/en.json | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 2c3cfc28c7..b0cf4bab88 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -740,6 +740,7 @@ export class HaDataTable extends LitElement { }, {}); const groupedItems: DataTableRowData[] = []; Object.entries(sorted).forEach(([groupName, rows]) => { + const collapsed = collapsedGroups.includes(groupName); groupedItems.push({ append: true, selectable: false, @@ -751,9 +752,10 @@ export class HaDataTable extends LitElement { > ${groupName === UNDEFINED_GROUP_KEY diff --git a/src/translations/en.json b/src/translations/en.json index 0d2a4cc0e9..4068dc710c 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -902,6 +902,8 @@ "hidden": "{number} hidden", "clear": "Clear", "ungrouped": "Ungrouped", + "collapse": "Collapse", + "expand": "Expand", "settings": { "header": "Customize", "hide": "Hide column {title}", From 84ac0cd41e3a429490e230f00b4b91857627b52a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 May 2025 16:48:15 +0200 Subject: [PATCH 27/56] Add css variables for start and end padding of tabs (#25654) --- src/panels/lovelace/hui-root.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 44ca521c0a..962a366767 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -1109,6 +1109,16 @@ class HUIRoot extends LitElement { sl-tab[aria-selected="true"] .edit-icon { display: inline-flex; } + sl-tab::part(base) { + padding-inline-start: var( + --ha-tab-padding-start, + var(--sl-spacing-large) + ); + padding-inline-end: var( + --ha-tab-padding-end, + var(--sl-spacing-large) + ); + } sl-tab::part(base) { padding-top: calc((var(--header-height) - 20px) / 2); padding-bottom: calc((var(--header-height) - 20px) / 2 - 2px); From 77eae6044d13683821d563cd89df175872e7c725 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 31 May 2025 16:15:54 +0200 Subject: [PATCH 28/56] Bumped version to 20250531.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0fc881abf4..6caa907986 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250528.0" +version = "20250531.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 5daec6bbc5e1c8cde33324a23cefe76b26383f12 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 1 Jun 2025 08:39:10 +0200 Subject: [PATCH 29/56] Calendar add event button gap alignment (#25662) Calendar gap alignment --- src/panels/calendar/ha-full-calendar.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/panels/calendar/ha-full-calendar.ts b/src/panels/calendar/ha-full-calendar.ts index 410aa4bdfd..17d83f1239 100644 --- a/src/panels/calendar/ha-full-calendar.ts +++ b/src/panels/calendar/ha-full-calendar.ts @@ -497,9 +497,9 @@ export class HAFullCalendar extends LitElement { ha-fab { position: absolute; - bottom: 32px; - right: 32px; - inset-inline-end: 32px; + bottom: 16px; + right: 16px; + inset-inline-end: 16px; inset-inline-start: initial; z-index: 1; } From 1483e8e38fd9fa21e678ab2d57092f616c9cd7a2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 2 Jun 2025 16:02:06 +0200 Subject: [PATCH 30/56] Set answers to yes and no for cloud pipeline confirm (#25674) --- src/panels/config/cloud/login/cloud-login.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/panels/config/cloud/login/cloud-login.ts b/src/panels/config/cloud/login/cloud-login.ts index 8c42063569..456f96b34b 100644 --- a/src/panels/config/cloud/login/cloud-login.ts +++ b/src/panels/config/cloud/login/cloud-login.ts @@ -233,6 +233,8 @@ export class CloudLogin extends LitElement { text: this.hass.localize( "ui.panel.config.cloud.login.cloud_pipeline_text" ), + confirmText: this.hass.localize("ui.common.yes"), + dismissText: this.hass.localize("ui.common.no"), }) ) { setAssistPipelinePreferred(this.hass, result.cloud_pipeline); From 7dc4d555ae33c1f4e7786f25218d7cf8f244e2d1 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:41:27 -0700 Subject: [PATCH 31/56] Hoist integration card tooltips (#25679) --- src/panels/config/integrations/ha-integration-card.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index 9ac636c354..82d30ee3c1 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -163,6 +163,7 @@ export class HaIntegrationCard extends LitElement { : "custom"}" > itm.source === "system") ? html`
    Date: Thu, 5 Jun 2025 06:35:32 +0200 Subject: [PATCH 32/56] =?UTF-8?q?Adjust=20tooltip=20positioning=20in=20ha-?= =?UTF-8?q?sidebar=20for=20not=20first=20lis=E2=80=A6=20(#25696)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(tooltip): fix tooltip positioning in ha-sidebar for not first listbox --- src/components/ha-sidebar.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index ea303ab4c8..8ccaad0d95 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -626,12 +626,15 @@ class HaSidebar extends SubscribeMixin(LitElement) { this._tooltipHideTimeout = undefined; } const tooltip = this._tooltip; - const listbox = this.shadowRoot!.querySelector("ha-md-list")!; - let top = item.offsetTop + 11; - if (listbox.contains(item)) { - top += listbox.offsetTop; - top -= listbox.scrollTop; - } + const allListbox = this.shadowRoot!.querySelectorAll("ha-md-list")!; + const listbox = [...allListbox].find((lb) => lb.contains(item)); + + const top = + item.offsetTop + + 11 + + (listbox?.offsetTop ?? 0) - + (listbox?.scrollTop ?? 0); + tooltip.innerText = ( item.querySelector(".item-text") as HTMLElement ).innerText; From 89ce6870f6dab6c9c64a46645e7a13dfa49503a3 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 9 Jun 2025 14:45:23 +0300 Subject: [PATCH 33/56] Handle tiny values in a log chart (#25727) --- src/components/chart/state-history-chart-line.ts | 14 ++++++++++---- src/components/chart/statistics-chart.ts | 14 ++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 53cc3b926b..f8f7689539 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -229,14 +229,20 @@ export class StateHistoryChartLine extends LitElement { minYAxis = ({ min }) => Math.min(min, this.minYAxis!); } } else if (this.logarithmicScale) { - minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05); + minYAxis = ({ min }) => { + const value = min > 0 ? min * 0.95 : min * 1.05; + return Math.abs(value) < 1 ? value : Math.floor(value); + }; } if (typeof maxYAxis === "number") { if (this.fitYData) { maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!); } } else if (this.logarithmicScale) { - maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95); + maxYAxis = ({ max }) => { + const value = max > 0 ? max * 1.05 : max * 0.95; + return Math.abs(value) < 1 ? value : Math.ceil(value); + }; } this._chartOptions = { xAxis: { @@ -753,10 +759,10 @@ export class StateHistoryChartLine extends LitElement { if (this.logarithmicScale) { // log(0) is -Infinity, so we need to set a minimum value if (typeof value === "number") { - return Math.max(value, 0.1); + return Math.max(value, Number.EPSILON); } if (typeof value === "function") { - return (values: any) => Math.max(value(values), 0.1); + return (values: any) => Math.max(value(values), Number.EPSILON); } } return value; diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index ea8469653a..a1b7e30dc9 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -241,14 +241,20 @@ export class StatisticsChart extends LitElement { minYAxis = ({ min }) => Math.min(min, this.minYAxis!); } } else if (this.logarithmicScale) { - minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05); + minYAxis = ({ min }) => { + const value = min > 0 ? min * 0.95 : min * 1.05; + return Math.abs(value) < 1 ? value : Math.floor(value); + }; } if (typeof maxYAxis === "number") { if (this.fitYData) { maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!); } } else if (this.logarithmicScale) { - maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95); + maxYAxis = ({ max }) => { + const value = max > 0 ? max * 1.05 : max * 0.95; + return Math.abs(value) < 1 ? value : Math.ceil(value); + }; } const endTime = this.endTime ?? new Date(); let startTime = this.startTime; @@ -619,10 +625,10 @@ export class StatisticsChart extends LitElement { if (this.logarithmicScale) { // log(0) is -Infinity, so we need to set a minimum value if (typeof value === "number") { - return Math.max(value, 0.1); + return Math.max(value, Number.EPSILON); } if (typeof value === "function") { - return (values: any) => Math.max(value(values), 0.1); + return (values: any) => Math.max(value(values), Number.EPSILON); } } return value; From 8149ee60cb888ad2dd188bbec0ce026f190b811f Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 10 Jun 2025 11:28:51 +0300 Subject: [PATCH 34/56] Fix period boundaries in Energy dashboard (#25728) --- src/data/energy.ts | 4 +++- .../cards/energy/common/energy-chart-options.ts | 6 ++++++ .../lovelace/components/hui-energy-period-selector.ts | 11 ++--------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/data/energy.ts b/src/data/energy.ts index 904e39728d..0ee60fde90 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -679,7 +679,9 @@ export const getEnergyDataCollection = ( const period = preferredPeriod === "today" && hour === "0" ? "yesterday" : preferredPeriod; - [collection.start, collection.end] = calcDateRange(hass, period); + const [start, end] = calcDateRange(hass, period); + collection.start = calcDate(start, startOfDay, hass.locale, hass.config); + collection.end = calcDate(end, endOfDay, hass.locale, hass.config); const scheduleUpdatePeriod = () => { collection._updatePeriodTimeout = window.setTimeout( 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 d26d47f96d..ff423acec7 100644 --- a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts +++ b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts @@ -10,6 +10,8 @@ import { addYears, addMonths, addHours, + startOfDay, + addDays, } from "date-fns"; import type { BarSeriesOption, @@ -282,6 +284,10 @@ export function getCompareTransform(start: Date, compareStart?: Date) { ) { return (ts: Date) => addMonths(ts, compareMonthDiff); } + const compareDayDiff = differenceInDays(start, compareStart); + if (compareDayDiff !== 0 && start.getTime() === startOfDay(start).getTime()) { + return (ts: Date) => addDays(ts, compareDayDiff); + } const compareOffset = start.getTime() - compareStart.getTime(); return (ts: Date) => addMilliseconds(ts, compareOffset); } diff --git a/src/panels/lovelace/components/hui-energy-period-selector.ts b/src/panels/lovelace/components/hui-energy-period-selector.ts index 6f1742d031..22bf863e34 100644 --- a/src/panels/lovelace/components/hui-energy-period-selector.ts +++ b/src/panels/lovelace/components/hui-energy-period-selector.ts @@ -297,24 +297,17 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { } private _dateRangeChanged(ev) { - const weekStartsOn = firstWeekdayIndex(this.hass.locale); this._startDate = calcDate( ev.detail.value.startDate, startOfDay, this.hass.locale, - this.hass.config, - { - weekStartsOn, - } + this.hass.config ); this._endDate = calcDate( ev.detail.value.endDate, endOfDay, this.hass.locale, - this.hass.config, - { - weekStartsOn, - } + this.hass.config ); this._updateCollectionPeriod(); From 2ab20aef69ecdc8aa153b69320180a4330d522bc Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 10 Jun 2025 12:20:10 +0200 Subject: [PATCH 35/56] Allow to open more info using query params (#25733) --- src/panels/lovelace/hui-root.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 962a366767..6f3d727d4d 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -57,6 +57,7 @@ import { showAlertDialog, showConfirmationDialog, } from "../../dialogs/generic/show-dialog-box"; +import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog"; import { QuickBarMode, showQuickBar, @@ -75,9 +76,9 @@ import { getLovelaceStrategy } from "./strategies/get-strategy"; import { isLegacyStrategyConfig } from "./strategies/legacy-strategy"; import type { Lovelace } from "./types"; import "./views/hui-view"; -import "./views/hui-view-container"; import type { HUIView } from "./views/hui-view"; import "./views/hui-view-background"; +import "./views/hui-view-container"; @customElement("hui-root") class HUIRoot extends LitElement { @@ -490,7 +491,16 @@ class HUIRoot extends LitElement { } else if (searchParams.conversation === "1") { this._clearParam("conversation"); this._showVoiceCommandDialog(); + } else if (searchParams["more-info-entity-id"]) { + const entityId = searchParams["more-info-entity-id"]; + this._clearParam("more-info-entity-id"); + // Wait for the next render to ensure the view is fully loaded + // because the more info dialog is closed when the url changes + afterNextRender(() => { + this._showMoreInfoDialog(entityId); + }); } + window.addEventListener("scroll", this._handleWindowScroll, { passive: true, }); @@ -730,6 +740,10 @@ class HUIRoot extends LitElement { showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" }); } + private _showMoreInfoDialog(entityId: string): void { + showMoreInfoDialog(this, { entityId }); + } + private _handleEnableEditMode(ev: CustomEvent): void { if (!shouldHandleRequestSelectedEvent(ev)) { return; From a26ca7d06529ec057750b8e24a7b1252c4def33d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 10 Jun 2025 16:46:09 +0200 Subject: [PATCH 36/56] Fix custom value selected when clicking item in combo box (#25734) --- src/components/ha-combo-box.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts index 58ed2e1f51..ab5ecfe4c3 100644 --- a/src/components/ha-combo-box.ts +++ b/src/components/ha-combo-box.ts @@ -345,8 +345,10 @@ export class HaComboBox extends LitElement { // @ts-ignore this._comboBox._closeOnBlurIsPrevented = true; } + if (!this.opened) { + return; + } const newValue = ev.detail.value; - if (newValue !== this.value) { fireEvent(this, "value-changed", { value: newValue || undefined }); } From 28214aebc579dd2c67d34bd0d97bba0c342ec7c5 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 10 Jun 2025 12:16:06 +0200 Subject: [PATCH 37/56] Reduce keypad gap and margin in alarm panel card (#25735) --- src/panels/lovelace/cards/hui-alarm-panel-card.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/panels/lovelace/cards/hui-alarm-panel-card.ts b/src/panels/lovelace/cards/hui-alarm-panel-card.ts index 17e55d852f..fe21e26c71 100644 --- a/src/panels/lovelace/cards/hui-alarm-panel-card.ts +++ b/src/panels/lovelace/cards/hui-alarm-panel-card.ts @@ -418,12 +418,11 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { .keypad { --keypad-columns: 3; - margin-top: 12px; padding: 12px; display: grid; grid-template-columns: repeat(var(--keypad-columns), auto); grid-auto-rows: auto; - grid-gap: 24px; + grid-gap: 16px; justify-items: center; align-items: center; } From 4c5015e178f40799de2f204f3f6bd7b8b32355a5 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 11 Jun 2025 12:04:54 +0200 Subject: [PATCH 38/56] Display full error for card preview mode (#25747) --- src/panels/lovelace/cards/hui-card.ts | 29 +++++++++++-- src/panels/lovelace/cards/hui-error-card.ts | 47 +++++++++++++-------- src/panels/lovelace/types.ts | 2 + 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/panels/lovelace/cards/hui-card.ts b/src/panels/lovelace/cards/hui-card.ts index dd3bcbeb30..cba0e15aea 100644 --- a/src/panels/lovelace/cards/hui-card.ts +++ b/src/panels/lovelace/cards/hui-card.ts @@ -12,7 +12,8 @@ import { attachConditionMediaQueriesListeners, checkConditionsMet, } from "../common/validate-condition"; -import { createCardElement } from "../create-element/create-card-element"; +import { tryCreateCardElement } from "../create-element/create-card-element"; +import { createErrorCardElement } from "../create-element/create-element-base"; import type { LovelaceCard, LovelaceGridOptions } from "../types"; declare global { @@ -71,10 +72,23 @@ export class HuiCard extends ReactiveElement { public getGridOptions(): LovelaceGridOptions { const elementOptions = this.getElementGridOptions(); const configOptions = this.getConfigGridOptions(); - return { + const mergedConfig = { ...elementOptions, ...configOptions, }; + + // If the element has fixed rows or columns, we use the values from the element + if (elementOptions.fixed_rows) { + mergedConfig.rows = elementOptions.rows; + delete mergedConfig.min_rows; + delete mergedConfig.max_rows; + } + if (elementOptions.fixed_columns) { + mergedConfig.columns = elementOptions.columns; + delete mergedConfig.min_columns; + delete mergedConfig.max_columns; + } + return mergedConfig; } // options provided by the element @@ -119,7 +133,15 @@ export class HuiCard extends ReactiveElement { } private _loadElement(config: LovelaceCardConfig) { - this._element = createCardElement(config); + try { + this._element = tryCreateCardElement(config); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : undefined; + this._element = createErrorCardElement({ + type: "error", + message: errorMessage, + }); + } this._elementConfig = config; if (this.hass) { this._element.hass = this.hass; @@ -200,6 +222,7 @@ export class HuiCard extends ReactiveElement { this._element.preview = this.preview; // For backwards compatibility (this._element as any).editMode = this.preview; + fireEvent(this, "card-updated"); } catch (e: any) { // eslint-disable-next-line no-console console.error(this.config?.type, e); diff --git a/src/panels/lovelace/cards/hui-error-card.ts b/src/panels/lovelace/cards/hui-error-card.ts index 837f78e367..26d72284d6 100644 --- a/src/panels/lovelace/cards/hui-error-card.ts +++ b/src/panels/lovelace/cards/hui-error-card.ts @@ -1,11 +1,11 @@ +import { mdiAlertCircleOutline, mdiAlertOutline } from "@mdi/js"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { mdiAlertCircleOutline, mdiAlertOutline } from "@mdi/js"; +import "../../../components/ha-card"; +import "../../../components/ha-svg-icon"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCard, LovelaceGridOptions } from "../types"; import type { ErrorCardConfig } from "./types"; -import "../../../components/ha-card"; -import "../../../components/ha-svg-icon"; const ERROR_ICONS = { warning: mdiAlertOutline, @@ -30,9 +30,10 @@ export class HuiErrorCard extends LitElement implements LovelaceCard { public getGridOptions(): LovelaceGridOptions { return { columns: 6, - rows: 1, + rows: this.preview ? "auto" : 1, min_rows: 1, min_columns: 6, + fixed_rows: this.preview, }; } @@ -45,17 +46,24 @@ export class HuiErrorCard extends LitElement implements LovelaceCard { const error = this._config?.error || this.hass?.localize("ui.errors.config.configuration_error"); - const showTitle = this.hass === undefined || this.hass?.user?.is_admin; + const showTitle = + this.hass === undefined || this.hass?.user?.is_admin || this.preview; + const showMessage = this.preview; return html` -
    - - - +
    +
    + + + +
    + ${showTitle + ? html`
    ${error}
    ` + : nothing}
    - ${showTitle - ? html`
    ${error}
    ` + ${showMessage && this._config?.message + ? html`
    ${this._config.message}
    ` : nothing} `; @@ -65,10 +73,6 @@ export class HuiErrorCard extends LitElement implements LovelaceCard { ha-card { height: 100%; border-width: 0; - display: flex; - align-items: center; - column-gap: 16px; - padding: 16px; } ha-card::after { position: absolute; @@ -81,6 +85,15 @@ export class HuiErrorCard extends LitElement implements LovelaceCard { content: ""; border-radius: var(--ha-card-border-radius, 12px); } + .header { + display: flex; + align-items: center; + gap: 8px; + padding: 16px; + } + .message { + padding: 0 16px 16px 16px; + } .no-title { justify-content: center; } @@ -90,13 +103,13 @@ export class HuiErrorCard extends LitElement implements LovelaceCard { text-overflow: ellipsis; font-weight: var(--ha-font-weight-bold); } - ha-card.warning > .icon { + ha-card.warning .icon { color: var(--warning-color); } ha-card.warning::after { background-color: var(--warning-color); } - ha-card.error > .icon { + ha-card.error .icon { color: var(--error-color); } ha-card.error::after { diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index 8da041bb27..40534349bf 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -62,6 +62,8 @@ export interface LovelaceGridOptions { min_columns?: number; min_rows?: number; max_rows?: number; + fixed_rows?: boolean; + fixed_columns?: boolean; } export interface LovelaceCard extends HTMLElement { From 0d979625783264879cf67a99eef75a177a7d75ed Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 11 Jun 2025 14:30:59 +0200 Subject: [PATCH 39/56] Bumped version to 20250531.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6caa907986..762f3468b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250531.0" +version = "20250531.1" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 89d0746c7cdd864269ef28cf8aaa1b81da55f3c5 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 11 Jun 2025 16:15:50 +0200 Subject: [PATCH 40/56] Fix edit card not working in chrome after editing (#25751) --- src/panels/lovelace/components/hui-badge-edit-mode.ts | 2 +- src/panels/lovelace/components/hui-card-edit-mode.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/components/hui-badge-edit-mode.ts b/src/panels/lovelace/components/hui-badge-edit-mode.ts index c765b3e8a5..521c47fff9 100644 --- a/src/panels/lovelace/components/hui-badge-edit-mode.ts +++ b/src/panels/lovelace/components/hui-badge-edit-mode.ts @@ -85,7 +85,7 @@ export class HuiBadgeEditMode extends LitElement { if (this._touchStarted) return; this._hover = true; }); - this.addEventListener("mouseout", () => { + this.addEventListener("mouseleave", () => { this._hover = false; }); this.addEventListener("click", () => { diff --git a/src/panels/lovelace/components/hui-card-edit-mode.ts b/src/panels/lovelace/components/hui-card-edit-mode.ts index 0711cf5849..d42fc1590d 100644 --- a/src/panels/lovelace/components/hui-card-edit-mode.ts +++ b/src/panels/lovelace/components/hui-card-edit-mode.ts @@ -71,7 +71,7 @@ export class HuiCardEditMode extends LitElement { if (this._touchStarted) return; this._hover = true; }); - this.addEventListener("mouseout", () => { + this.addEventListener("mouseleave", () => { this._hover = false; }); this.addEventListener("click", () => { From bffe38c827a737e46bd8b3a7c8bb9bbb264c842b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 11 Jun 2025 16:26:37 +0200 Subject: [PATCH 41/56] Bumped version to 20250531.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 762f3468b9..c7a4e50fc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250531.1" +version = "20250531.2" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 7f68447a4fae7e9c1ad43a05367aa4b93c4a73ed Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 12 Jun 2025 10:40:36 +0300 Subject: [PATCH 42/56] Fix alerts refresh on device page (#25748) * Fix alerts refresh on device page * don't reset actions periodically * reset stuff only on deviceId change --- .../config/devices/ha-config-device-page.ts | 76 ++++++++----------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index e396c7072b..44530ab333 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -127,16 +127,15 @@ export class HaConfigDevicePage extends LitElement { @state() private _related?: RelatedResult; - // If a number, it's the request ID so we make sure we don't show older info - @state() private _diagnosticDownloadLinks?: number | DeviceAction[]; + @state() private _diagnosticDownloadLinks: DeviceAction[] = []; - @state() private _deleteButtons?: DeviceAction[]; + @state() private _deleteButtons: DeviceAction[] = []; - @state() private _deviceActions?: DeviceAction[]; + @state() private _deviceActions: DeviceAction[] = []; - @state() private _deviceAlerts?: DeviceAlert[]; + @state() private _deviceAlerts: DeviceAlert[] = []; - private _deviceAlertsTimeout?: number; + private _deviceAlertsActionsTimeout?: number; @state() @consume({ context: fullEntitiesContext, subscribe: true }) @@ -255,42 +254,19 @@ export class HaConfigDevicePage extends LitElement { public willUpdate(changedProps) { super.willUpdate(changedProps); - if ( - changedProps.has("deviceId") || - changedProps.has("devices") || - changedProps.has("entries") - ) { - this._diagnosticDownloadLinks = undefined; - this._deleteButtons = undefined; - this._deviceActions = undefined; - this._deviceAlerts = undefined; + if (changedProps.has("deviceId") || changedProps.has("entries")) { + this._deviceActions = []; + this._deviceAlerts = []; + this._deleteButtons = []; + this._diagnosticDownloadLinks = []; + this._fetchData(); } - - if ( - (this._diagnosticDownloadLinks && - this._deleteButtons && - this._deviceActions && - this._deviceAlerts) || - !this.deviceId || - !this.entries - ) { - return; - } - - this._diagnosticDownloadLinks = Math.random(); - this._deleteButtons = []; // To prevent re-rendering if no delete buttons - this._deviceActions = []; - this._deviceAlerts = []; - this._getDiagnosticButtons(this._diagnosticDownloadLinks); - this._getDeleteActions(); - this._getDeviceActions(); - clearTimeout(this._deviceAlertsTimeout); - this._getDeviceAlerts(); } protected firstUpdated(changedProps) { super.firstUpdated(changedProps); loadDeviceRegistryDetailDialog(); + this._fetchData(); } protected updated(changedProps) { @@ -302,7 +278,7 @@ export class HaConfigDevicePage extends LitElement { public disconnectedCallback() { super.disconnectedCallback(); - clearTimeout(this._deviceAlertsTimeout); + clearTimeout(this._deviceAlertsActionsTimeout); } protected render() { @@ -909,7 +885,18 @@ export class HaConfigDevicePage extends LitElement { `; } - private async _getDiagnosticButtons(requestId: number): Promise { + private _fetchData() { + if (this.deviceId && this.entries.length) { + this._getDiagnosticButtons(); + this._getDeleteActions(); + clearTimeout(this._deviceAlertsActionsTimeout); + this._getDeviceActions(); + this._getDeviceAlerts(); + } + } + + private async _getDiagnosticButtons(): Promise { + const deviceId = this.deviceId; if (!isComponentLoaded(this.hass, "diagnostics")) { return; } @@ -951,7 +938,8 @@ export class HaConfigDevicePage extends LitElement { links = links.filter(Boolean); - if (this._diagnosticDownloadLinks !== requestId) { + if (this.deviceId !== deviceId) { + // abort if the device has changed return; } if (links.length > 0) { @@ -1176,12 +1164,12 @@ export class HaConfigDevicePage extends LitElement { deviceAlerts.push(...alerts); } + this._deviceAlerts = deviceAlerts; if (deviceAlerts.length) { - this._deviceAlerts = deviceAlerts; - this._deviceAlertsTimeout = window.setTimeout( - () => this._getDeviceAlerts(), - DEVICE_ALERTS_INTERVAL - ); + this._deviceAlertsActionsTimeout = window.setTimeout(() => { + this._getDeviceAlerts(); + this._getDeviceActions(); + }, DEVICE_ALERTS_INTERVAL); } } From 8cead75087a93b39e45805da5561c4de75cc3458 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 12 Jun 2025 09:29:58 +0200 Subject: [PATCH 43/56] Change backup type order (#25759) --- src/data/backup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/backup.ts b/src/data/backup.ts index 12261eb810..fd0324a229 100644 --- a/src/data/backup.ts +++ b/src/data/backup.ts @@ -339,7 +339,7 @@ export const computeBackupSize = (backup: BackupContent) => export type BackupType = "automatic" | "manual" | "addon_update"; -const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "manual", "addon_update"]; +const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "addon_update", "manual"]; export const getBackupTypes = memoize((isHassio: boolean) => isHassio From 38545a01dd202e6c51561e28f4d1c4798ba0358c Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 12 Jun 2025 09:44:20 +0200 Subject: [PATCH 44/56] Ensure grid options always return an object (#25760) --- src/panels/lovelace/cards/hui-card.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/cards/hui-card.ts b/src/panels/lovelace/cards/hui-card.ts index cba0e15aea..271e2cd027 100644 --- a/src/panels/lovelace/cards/hui-card.ts +++ b/src/panels/lovelace/cards/hui-card.ts @@ -96,7 +96,9 @@ export class HuiCard extends ReactiveElement { if (!this._element) return {}; if (this._element.getGridOptions) { - return this._element.getGridOptions(); + const options = this._element.getGridOptions(); + // Some custom cards might return undefined, so we ensure we return an object + return options || {}; } if (this._element.getLayoutOptions) { // Disabled for now to avoid spamming the console, need to be re-enabled when hui-card performance are fixed From 359460b570ca717a73f54e97d8fbd68d3acb7bf2 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 12 Jun 2025 14:22:11 +0200 Subject: [PATCH 45/56] Bumped version to 20250531.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c7a4e50fc4..59884f9957 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250531.2" +version = "20250531.3" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 77c458a0e5289c73ac7fc8bfcac8cae20540c38d Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 17 Jun 2025 16:07:57 +0300 Subject: [PATCH 46/56] Reduce reset-zoom button size on timeline charts (#25796) --- src/components/chart/ha-chart-base.ts | 14 +++++++++++++- .../chart/state-history-chart-timeline.ts | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 37a12f07e6..b40256b60b 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -49,6 +49,9 @@ export class HaChartBase extends LitElement { @property({ attribute: "expand-legend", type: Boolean }) public expandLegend?: boolean; + @property({ attribute: "small-controls", type: Boolean }) + public smallControls?: boolean; + // extraComponents is not reactive and should not trigger updates public extraComponents?: any[]; @@ -194,7 +197,7 @@ export class HaChartBase extends LitElement {
    ${this._renderLegend()} -
    +
    ${this._isZoomed ? html` `; From 6738b7d708c68014a06a452f83e1e617876afd17 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 17 Jun 2025 16:14:33 +0300 Subject: [PATCH 47/56] Fix sankey total calculation to account for `included_in_stat` (#25805) --- src/panels/lovelace/cards/energy/hui-energy-sankey-card.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-sankey-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sankey-card.ts index da162673e3..5f1a9cf146 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-sankey-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-sankey-card.ts @@ -238,7 +238,6 @@ class HuiEnergySankeyCard if (value < 0.01) { return; } - untrackedConsumption -= value; const node = { id: device.stat_consumption, label: @@ -260,6 +259,8 @@ class HuiEnergySankeyCard source: node.parent, target: node.id, }); + } else { + untrackedConsumption -= value; } deviceNodes.push(node); }); From d764187e8c8bb0abbb06aead2d3953d693014ae4 Mon Sep 17 00:00:00 2001 From: Anthony Relle Date: Tue, 17 Jun 2025 10:54:34 +0100 Subject: [PATCH 48/56] Update ElectricityMaps URL in Energy Dashboard (#25816) fix: update electricitymap domain --- .../lovelace/cards/energy/hui-energy-distribution-card.ts | 2 +- 1 file changed, 1 insertion(+), 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 a84858d13e..1d7d7d4a14 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -204,7 +204,7 @@ class HuiEnergyDistrubutionCard let homeHighCarbonCircumference: number | undefined; // This fallback is used in the demo - let electricityMapUrl = "https://app.electricitymap.org"; + let electricityMapUrl = "https://app.electricitymaps.com"; if (this._data.co2SignalEntity && this._data.fossilEnergyConsumption) { // Calculate high carbon consumption From 51246f119c5c11ed33d823b0045160272e316889 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 17 Jun 2025 15:14:07 +0200 Subject: [PATCH 49/56] Fix disabled color in dark mode in production (#25818) --- src/common/style/derived-css-vars.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/common/style/derived-css-vars.ts b/src/common/style/derived-css-vars.ts index 65e6f21d06..3b2add7e1b 100644 --- a/src/common/style/derived-css-vars.ts +++ b/src/common/style/derived-css-vars.ts @@ -9,7 +9,9 @@ const _extractCssVars = ( cssString.split(";").forEach((rawLine) => { const line = rawLine.substring(rawLine.indexOf("--")).trim(); if (line.startsWith("--") && condition(line)) { - const [name, value] = line.split(":").map((part) => part.trim()); + const [name, value] = line + .split(":") + .map((part) => part.replaceAll("}", "").trim()); variables[name.substring(2, name.length)] = value; } }); @@ -25,7 +27,10 @@ export const extractVar = (css: CSSResult, varName: string) => { } const endIndex = cssString.indexOf(";", startIndex + search.length); - return cssString.substring(startIndex + search.length, endIndex).trim(); + return cssString + .substring(startIndex + search.length, endIndex) + .replaceAll("}", "") + .trim(); }; export const extractVars = (css: CSSResult) => { From 7f7e6935475272ad1b7e29f082a56487fba2276c Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 19 Jun 2025 18:49:58 +0300 Subject: [PATCH 50/56] Fix bar chart data order when using the legend (#25832) * Fix bar chart data order when using the legend * type fix --- src/components/chart/ha-chart-base.ts | 77 ++++++++++--------- .../hui-energy-devices-detail-graph-card.ts | 11 ++- 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index b40256b60b..fdaad85c83 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -9,6 +9,7 @@ import type { LegendComponentOption, XAXisOption, YAXisOption, + LineSeriesOption, } from "echarts/types/dist/shared"; import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; @@ -642,44 +643,46 @@ export class HaChartBase extends LitElement { const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as | YAXisOption | undefined; - const series = ensureArray(this.data) - .filter((d) => !this._hiddenDatasets.has(String(d.name ?? d.id))) - .map((s) => { - if (s.type === "line") { - if (yAxis?.type === "log") { - // set <=0 values to null so they render as gaps on a log graph - return { - ...s, - data: s.data?.map((v) => - Array.isArray(v) - ? [ - v[0], - typeof v[1] !== "number" || v[1] > 0 ? v[1] : null, - ...v.slice(2), - ] - : v - ), - }; - } - 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; - return { - ...s, - sampling: undefined, - data: downSampleLineData(s.data, this.clientWidth, minX, maxX), - }; - } + const series = ensureArray(this.data).map((s) => { + const data = this._hiddenDatasets.has(String(s.name ?? s.id)) + ? undefined + : s.data; + if (data && s.type === "line") { + if (yAxis?.type === "log") { + // set <=0 values to null so they render as gaps on a log graph + return { + ...s, + data: (data as LineSeriesOption["data"])!.map((v) => + Array.isArray(v) + ? [ + v[0], + typeof v[1] !== "number" || v[1] > 0 ? v[1] : null, + ...v.slice(2), + ] + : v + ), + }; } - return s; - }); - return series; + 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; + return { + ...s, + sampling: undefined, + data: downSampleLineData( + data as LineSeriesOption["data"], + this.clientWidth, + minX, + maxX + ), + }; + } + } + return { ...s, data }; + }); + return series as ECOption["series"]; } private _getDefaultHeight() { diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts index 917d9e5950..c4ef01c62a 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts @@ -81,7 +81,6 @@ export class HuiEnergyDevicesDetailGraphCard key: this._config?.collection_key, }).subscribe((data) => { this._data = data; - this._processStatistics(); }), ]; } @@ -103,10 +102,7 @@ export class HuiEnergyDevicesDetailGraphCard } protected willUpdate(changedProps: PropertyValues) { - if ( - (changedProps.has("_hiddenStats") || changedProps.has("_config")) && - this._data - ) { + if (changedProps.has("_config") || changedProps.has("_data")) { this._processStatistics(); } } @@ -206,7 +202,10 @@ export class HuiEnergyDevicesDetailGraphCard ); private _processStatistics() { - const energyData = this._data!; + if (!this._data) { + return; + } + const energyData = this._data; this._start = energyData.start; this._end = energyData.end || endOfToday(); From 5ac6781f7dc7b14e8ca46a9a092e26e0a59b59ea Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 18 Jun 2025 17:44:27 +0200 Subject: [PATCH 51/56] Remove debug type in secondary line in statistic picker (#25835) --- src/components/entity/ha-statistic-picker.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index 3d20ab7a43..24f96b0eb2 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -438,10 +438,8 @@ export class HaStatisticPicker extends LitElement { ` : nothing} ${item.primary} - ${item.secondary || item.type - ? html`${item.secondary} - ${item.type}` + ${item.secondary + ? html`${item.secondary}` : nothing} ${item.statistic_id && showEntityId ? html` From e6fbe0d5387975268a3094dbff300104b9565bd8 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 23 Jun 2025 08:42:54 +0300 Subject: [PATCH 52/56] Round chart limits with fit_y_data (#25851) --- src/components/chart/state-history-chart-line.ts | 14 ++++++++++---- src/components/chart/statistics-chart.ts | 14 ++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index f8f7689539..e87ae2ac0e 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -226,22 +226,24 @@ export class StateHistoryChartLine extends LitElement { this.maxYAxis; if (typeof minYAxis === "number") { if (this.fitYData) { - minYAxis = ({ min }) => Math.min(min, this.minYAxis!); + minYAxis = ({ min }) => + Math.min(this._roundYAxis(min, Math.floor), this.minYAxis!); } } else if (this.logarithmicScale) { minYAxis = ({ min }) => { const value = min > 0 ? min * 0.95 : min * 1.05; - return Math.abs(value) < 1 ? value : Math.floor(value); + return this._roundYAxis(value, Math.floor); }; } if (typeof maxYAxis === "number") { if (this.fitYData) { - maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!); + maxYAxis = ({ max }) => + Math.max(this._roundYAxis(max, Math.ceil), this.maxYAxis!); } } else if (this.logarithmicScale) { maxYAxis = ({ max }) => { const value = max > 0 ? max * 1.05 : max * 0.95; - return Math.abs(value) < 1 ? value : Math.ceil(value); + return this._roundYAxis(value, Math.ceil); }; } this._chartOptions = { @@ -767,6 +769,10 @@ export class StateHistoryChartLine extends LitElement { } return value; } + + private _roundYAxis(value: number, roundingFn: (value: number) => number) { + return Math.abs(value) < 1 ? value : roundingFn(value); + } } customElements.define("state-history-chart-line", StateHistoryChartLine); diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index a1b7e30dc9..dd60599bc2 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -238,22 +238,24 @@ export class StatisticsChart extends LitElement { this.maxYAxis; if (typeof minYAxis === "number") { if (this.fitYData) { - minYAxis = ({ min }) => Math.min(min, this.minYAxis!); + minYAxis = ({ min }) => + Math.min(this._roundYAxis(min, Math.floor), this.minYAxis!); } } else if (this.logarithmicScale) { minYAxis = ({ min }) => { const value = min > 0 ? min * 0.95 : min * 1.05; - return Math.abs(value) < 1 ? value : Math.floor(value); + return this._roundYAxis(value, Math.floor); }; } if (typeof maxYAxis === "number") { if (this.fitYData) { - maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!); + maxYAxis = ({ max }) => + Math.max(this._roundYAxis(max, Math.ceil), this.maxYAxis!); } } else if (this.logarithmicScale) { maxYAxis = ({ max }) => { const value = max > 0 ? max * 1.05 : max * 0.95; - return Math.abs(value) < 1 ? value : Math.ceil(value); + return this._roundYAxis(value, Math.ceil); }; } const endTime = this.endTime ?? new Date(); @@ -634,6 +636,10 @@ export class StatisticsChart extends LitElement { return value; } + private _roundYAxis(value: number, roundingFn: (value: number) => number) { + return Math.abs(value) < 1 ? value : roundingFn(value); + } + static styles = css` :host { display: block; From 1a8ff83e2d75ba1b8aeb1d82c317156b97d44c70 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Sun, 22 Jun 2025 13:11:29 +0300 Subject: [PATCH 53/56] Another fix for history chart axis rounding (#25852) --- .../chart/state-history-chart-line.ts | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index e87ae2ac0e..362d7ab579 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -731,20 +731,17 @@ export class StateHistoryChartLine extends LitElement { } private _formatYAxisLabel = (value: number) => { - const formatOptions = - value >= 1 || value <= -1 - ? undefined - : { - // show the first significant digit for tiny values - maximumFractionDigits: Math.max( - 2, - // use the difference to the previous value to determine the number of significant digits #25526 - -Math.floor( - Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1)) - ) - ), - }; - const label = formatNumber(value, this.hass.locale, formatOptions); + // show the first significant digit for tiny values + const maximumFractionDigits = Math.max( + 1, + // use the difference to the previous value to determine the number of significant digits #25526 + -Math.floor( + Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1)) + ) + ); + const label = formatNumber(value, this.hass.locale, { + maximumFractionDigits, + }); const width = measureTextWidth(label, 12) + 5; if (width > this._yWidth) { this._yWidth = width; From 73cd1e8e9d59bd074e45d9a9c6bd90dee7ba5d42 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 23 Jun 2025 12:25:46 +0300 Subject: [PATCH 54/56] Fix duplicated requests in statistics-graph (#25878) --- .../cards/hui-statistics-graph-card.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index d7be2a06d2..7d493f1575 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -97,8 +97,8 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { } if (this._config?.energy_date_selection) { this._subscribeEnergy(); - } else { - this._setFetchStatisticsTimer(); + } else if (this._interval === undefined) { + this._setFetchStatisticsTimer(true); } } @@ -213,9 +213,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { changedProps.has("_config") && oldConfig?.entities !== this._config.entities ) { - this._getStatisticsMetaData(this._entities).then(() => { - this._setFetchStatisticsTimer(); - }); + this._setFetchStatisticsTimer(true); return; } @@ -230,10 +228,14 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { } } - private _setFetchStatisticsTimer() { - this._getStatistics(); - // statistics are created every hour + private async _setFetchStatisticsTimer(fetchMetadata = false) { clearInterval(this._interval); + this._interval = 0; // block concurrent calls + if (fetchMetadata) { + await this._getStatisticsMetaData(this._entities); + } + await this._getStatistics(); + // statistics are created every hour if (!this._config?.energy_date_selection) { this._interval = window.setInterval( () => this._getStatistics(), From 45e66d8b6e3c6530e6aaeffb6886fe64b7808dec Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 23 Jun 2025 14:10:27 +0200 Subject: [PATCH 55/56] Don't send double card updated event when rendering the card (#25883) --- src/panels/lovelace/cards/hui-card.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/cards/hui-card.ts b/src/panels/lovelace/cards/hui-card.ts index 271e2cd027..12be541b86 100644 --- a/src/panels/lovelace/cards/hui-card.ts +++ b/src/panels/lovelace/cards/hui-card.ts @@ -224,7 +224,9 @@ export class HuiCard extends ReactiveElement { this._element.preview = this.preview; // For backwards compatibility (this._element as any).editMode = this.preview; - fireEvent(this, "card-updated"); + if (this.hasUpdated) { + fireEvent(this, "card-updated"); + } } catch (e: any) { // eslint-disable-next-line no-console console.error(this.config?.type, e); From 47b90feffa149cf18ee8f9772a9b1c6f8beb8baa Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 24 Jun 2025 09:58:11 +0200 Subject: [PATCH 56/56] Bumped version to 20250531.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 59884f9957..17bcc756c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250531.3" +version = "20250531.4" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend"