From 0ae6fa07636f4d66c15e3f2169fc63e49acb272f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 5 Feb 2025 18:33:00 +0100 Subject: [PATCH 01/24] Fix area registry dialog field (#24090) --- src/panels/config/areas/dialog-area-registry-detail.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts index 26f98283b4..98367f9a0c 100644 --- a/src/panels/config/areas/dialog-area-registry-detail.ts +++ b/src/panels/config/areas/dialog-area-registry-detail.ts @@ -329,6 +329,9 @@ class DialogAreaDetail extends LitElement { return [ haStyleDialog, css` + ha-textfield { + display: block; + } ha-aliases-editor, ha-entity-picker, ha-floor-picker, From bf962b29afcb621233b82bf445e7b59d79f700a7 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 6 Feb 2025 10:36:58 +0200 Subject: [PATCH 02/24] Reduce padding in energy charts and align unit (#24095) --- .../cards/energy/common/energy-chart-options.ts | 13 ++++++++----- .../energy/hui-energy-devices-detail-graph-card.ts | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) 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 2e576c7c9e..a5c5f1ba9b 100644 --- a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts +++ b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts @@ -72,7 +72,10 @@ export function getCommonOptions( yAxis: { type: "value", name: unit, - nameGap: 5, + nameGap: 2, + nameTextStyle: { + align: "left", + }, axisLabel: { formatter: (value: number) => formatNumber(Math.abs(value), locale), }, @@ -81,10 +84,10 @@ export function getCommonOptions( }, }, grid: { - top: 35, - bottom: 10, - left: 10, - right: 10, + top: 15, + bottom: 0, + left: 1, + right: 1, containLabel: true, }, tooltip: { 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 a1501155b9..0e0b60b16a 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 @@ -193,9 +193,10 @@ export class HuiEnergyDevicesDetailGraphCard icon: "circle", }, grid: { + top: 45, bottom: 0, - left: 5, - right: 5, + left: 1, + right: 1, containLabel: true, }, }; From dcb04067b8de5572c8459db50f6d0b68d9a8e85c Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Thu, 6 Feb 2025 08:49:05 +0100 Subject: [PATCH 03/24] Add network adapter translations (#24096) --- src/components/ha-network.ts | 19 +++++++++++++------ src/translations/en.json | 7 ++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/ha-network.ts b/src/components/ha-network.ts index 0841010272..f46bf00ced 100644 --- a/src/components/ha-network.ts +++ b/src/components/ha-network.ts @@ -64,9 +64,13 @@ export class HaNetwork extends LitElement { > - Auto Configure + + ${this.hass.localize( + "ui.panel.config.network.adapter.auto_configure" + )} + - Detected: + ${this.hass.localize("ui.panel.config.network.adapter.detected")}: ${format_auto_detected_interfaces(this.networkConfig.adapters)} @@ -85,18 +89,21 @@ export class HaNetwork extends LitElement { - Adapter: ${adapter.name} + ${this.hass.localize( + "ui.panel.config.network.adapter.adapter" + )}: + ${adapter.name} ${adapter.default ? html` - (Default)` - : ""} + (${this.hass.localize("ui.common.default")})` + : nothing} ${format_addresses([...adapter.ipv4, ...adapter.ipv6])} ` ) - : ""} + : nothing} `; } diff --git a/src/translations/en.json b/src/translations/en.json index 8271cb33d7..80d7b4f6de 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6122,7 +6122,12 @@ }, "network_adapter": "Network adapter", "network_adapter_info": "Configure which network adapters integrations will use. Currently this setting only affects multicast traffic. A restart is required for these settings to apply.", - "ip_information": "IP Information" + "ip_information": "IP Information", + "adapter": { + "auto_configure": "Auto configure", + "detected": "Detected", + "adapter": "Adapter" + } }, "storage": { "caption": "Storage", From a30e501031ba944c698e3d5e114e89bdbe0342a2 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 6 Feb 2025 13:16:35 +0200 Subject: [PATCH 04/24] Set charts font to Roboto (#24097) --- src/components/chart/ha-chart-base.ts | 1 + src/util/text.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 4988ff75c8..81029851a0 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -328,6 +328,7 @@ export class HaChartBase extends LitElement { backgroundColor: "transparent", textStyle: { color: style.getPropertyValue("--primary-text-color"), + fontFamily: "Roboto, Noto, sans-serif", }, title: { textStyle: { diff --git a/src/util/text.ts b/src/util/text.ts index edd6afdd08..433f7b5fee 100644 --- a/src/util/text.ts +++ b/src/util/text.ts @@ -10,7 +10,7 @@ let textMeasureCanvas: HTMLCanvasElement | undefined; export function measureTextWidth( text: string, fontSize: number, - fontFamily = "sans-serif" + fontFamily = "Roboto, Noto, sans-serif" ): number { if (!textMeasureCanvas) { textMeasureCanvas = document.createElement("canvas"); From 9ab5be4730f23c654af807b209adb5b2fedacc96 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 6 Feb 2025 13:38:22 +0200 Subject: [PATCH 05/24] Fix energy dashboard data formatting (#24101) --- src/panels/lovelace/cards/energy/common/energy-chart-options.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts index a5c5f1ba9b..d26d47f96d 100644 --- a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts +++ b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts @@ -225,7 +225,7 @@ export function fillDataGapsAndRoundCaps(datasets: BarSeriesOption[]) { if (x === undefined) { continue; } - if (x !== bucket) { + if (Number(x) !== bucket) { datasets[i].data?.splice(index, 0, { value: [bucket, 0], itemStyle: { From 936f66c41cf5457fbd567a93b559540644462199 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 6 Feb 2025 21:44:39 -0800 Subject: [PATCH 06/24] Stack solar forecasts (#24113) --- src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts index e1ffa869f0..3119e53138 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts @@ -367,6 +367,7 @@ export class HuiEnergySolarGraphCard data.push({ id: "forecast-" + source.stat_energy_from, type: "line", + stack: "forecast", name: this.hass.localize( "ui.panel.lovelace.cards.energy.energy_solar_graph.forecast", { From 91e8750f4487efa3daf9d4de7ffee19453f4b60d Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 10 Feb 2025 11:12:42 +0200 Subject: [PATCH 07/24] Fix device energy card with `max_devices` (#24150) --- .../cards/energy/hui-energy-devices-detail-graph-card.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 0e0b60b16a..42edcdaacc 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 @@ -447,9 +447,9 @@ export class HuiEnergyDevicesDetailGraphCard stack: compare ? "devicesCompare" : "devices", }); }); - return sorted_devices.map( - (device) => data.find((d) => (d.id as string).includes(device))! - ); + return sorted_devices + .map((device) => data.find((d) => (d.id as string).includes(device))!) + .filter(Boolean); } static styles = css` From 090086995739251be33c562cab1791bed383d723 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 10 Feb 2025 11:12:12 +0200 Subject: [PATCH 08/24] Round log scale limits (#24151) --- src/components/chart/state-history-chart-line.ts | 4 ++-- src/components/chart/statistics-chart.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index d268bd5d7c..76e73a89e7 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -222,14 +222,14 @@ export class StateHistoryChartLine extends LitElement { minYAxis = ({ min }) => Math.min(min, this.minYAxis!); } } else if (this.logarithmicScale) { - minYAxis = ({ min }) => (min > 0 ? min * 0.95 : min * 1.05); + minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05); } if (typeof maxYAxis === "number") { if (this.fitYData) { maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!); } } else if (this.logarithmicScale) { - maxYAxis = ({ max }) => (max > 0 ? max * 1.05 : max * 0.95); + maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95); } this._chartOptions = { xAxis: { diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index ac9aabf85e..fd34fa5733 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -237,14 +237,14 @@ export class StatisticsChart extends LitElement { minYAxis = ({ min }) => Math.min(min, this.minYAxis!); } } else if (this.logarithmicScale) { - minYAxis = ({ min }) => (min > 0 ? min * 0.95 : min * 1.05); + minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05); } if (typeof maxYAxis === "number") { if (this.fitYData) { maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!); } } else if (this.logarithmicScale) { - maxYAxis = ({ max }) => (max > 0 ? max * 1.05 : max * 0.95); + maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95); } const endTime = this.endTime ?? new Date(); let startTime = this.startTime; @@ -290,7 +290,6 @@ export class StatisticsChart extends LitElement { align: "left", }, position: computeRTL(this.hass) ? "right" : "left", - // @ts-ignore scale: true, min: this._clampYAxis(minYAxis), max: this._clampYAxis(maxYAxis), From 2ca739573335514eff9c590fc52409c417e2076b Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 10 Feb 2025 16:54:20 +0200 Subject: [PATCH 09/24] Limit max label width of hui-energy-devices-graph-card (#24152) --- .../energy/hui-energy-devices-graph-card.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index 3c99584415..fc987b3300 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts @@ -27,6 +27,7 @@ import { hasConfigChanged } from "../../common/has-changed"; import type { ECOption } from "../../../../resources/echarts"; import "../../../../components/ha-card"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { measureTextWidth } from "../../../../util/text"; @customElement("hui-energy-devices-graph-card") export class HuiEnergyDevicesGraphCard @@ -109,8 +110,11 @@ export class HuiEnergyDevicesGraphCard return `${title}${params.marker} ${params.seriesName}: ${value}`; } - private _createOptions = memoizeOne( - (data: BarSeriesOption[]): ECOption => ({ + private _createOptions = memoizeOne((data: BarSeriesOption[]): ECOption => { + const isMobile = window.matchMedia( + "all and (max-width: 450px), all and (max-height: 500px)" + ).matches; + return { xAxis: { type: "value", name: "kWh", @@ -124,6 +128,17 @@ export class HuiEnergyDevicesGraphCard axisLabel: { formatter: this._getDeviceName.bind(this), overflow: "truncate", + fontSize: 12, + margin: 5, + width: Math.min( + isMobile ? 100 : 200, + Math.max( + ...(data[0]?.data?.map( + (d: any) => + measureTextWidth(this._getDeviceName(d.value[1]), 12) + 5 + ) || []) + ) + ), }, }, grid: { @@ -137,8 +152,8 @@ export class HuiEnergyDevicesGraphCard show: true, formatter: this._renderTooltip.bind(this), }, - }) - ); + }; + }); private _getDeviceName(statisticId: string): string { return ( From 76977b64fa2cfa0c142e6c659824428986d9cf2d Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 10 Feb 2025 16:53:36 +0200 Subject: [PATCH 10/24] Bring back energy usage graph order (#24156) --- .../cards/energy/hui-energy-usage-graph-card.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts index 9836c7f061..18e737696f 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts @@ -300,6 +300,8 @@ export class HuiEnergyUsageGraphCard type: "bar", stack: "usage", data: [], + // @ts-expect-error + order: 0, }); } @@ -315,6 +317,8 @@ export class HuiEnergyUsageGraphCard ) ); + // @ts-expect-error + datasets.sort((a, b) => a.order - b.order); fillDataGapsAndRoundCaps(datasets); this._chartData = datasets; } @@ -482,7 +486,7 @@ export class HuiEnergyUsageGraphCard this._compareStart! ); - Object.entries(combinedData).forEach(([type, sources]) => { + Object.entries(combinedData).forEach(([type, sources], idx) => { Object.entries(sources).forEach(([statId, source]) => { const points: BarSeriesOption["data"] = []; // Process chart data. @@ -513,6 +517,13 @@ export class HuiEnergyUsageGraphCard statId, statisticsMetaData[statId] ), + // @ts-expect-error + order: + type === "used_solar" + ? 1 + : type === "to_battery" + ? Object.keys(combinedData).length + : idx + 2, barMaxWidth: 50, itemStyle: { borderColor: getEnergyColor( From 4498747fb132baa151dd3adbcce99d1214698a4e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 10 Feb 2025 19:38:30 +0100 Subject: [PATCH 11/24] Bumped version to 20250210.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 193a84b4b7..a7effa1919 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250205.0" +version = "20250210.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" From 5d6fcaf6bb692008eb084c8c4c7e7fe80589e785 Mon Sep 17 00:00:00 2001 From: Brynley McDonald Date: Thu, 6 Feb 2025 22:59:58 +1300 Subject: [PATCH 12/24] Add Mastodon and Bluesky to help tip (#24099) Add Mastodon and Bluesky to socials tip --- src/panels/config/dashboard/ha-config-dashboard.ts | 12 ++++++++++++ src/translations/en.json | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 335d84a7ac..726434b6de 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -66,6 +66,18 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => { rel="noreferrer" >${hass.localize("ui.panel.config.tips.join_x")}`, + mastodon: html`${hass.localize("ui.panel.config.tips.join_mastodon")}`, + bluesky: html`${hass.localize("ui.panel.config.tips.join_bluesky")}`, discord: html` Date: Mon, 10 Feb 2025 15:53:05 +0100 Subject: [PATCH 13/24] Fix section border radius (#24159) --- src/panels/lovelace/sections/hui-grid-section.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/lovelace/sections/hui-grid-section.ts b/src/panels/lovelace/sections/hui-grid-section.ts index f50a5dc7b4..059fe80852 100644 --- a/src/panels/lovelace/sections/hui-grid-section.ts +++ b/src/panels/lovelace/sections/hui-grid-section.ts @@ -240,6 +240,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement { .container.edit-mode { padding: 8px; border-radius: var(--ha-card-border-radius, 12px); + border-start-end-radius: 0; border: 2px dashed var(--divider-color); min-height: var(--row-height); } From d9559b7f07010650726935cb63bb8eb2f044e0b4 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 14 Feb 2025 13:46:55 +0200 Subject: [PATCH 14/24] Optimize chart performance (#24215) * Stop listening to chart scroll events to improve performance * only set visualmap when needed * Reduce statistics detail for long periods * reduce calls to `setOption` * tweak zoom modifier code * always replace series * revert statistics detail change --- src/components/chart/ha-chart-base.ts | 64 ++++++------------ .../chart/state-history-chart-line.ts | 67 ++++++++++--------- 2 files changed, 57 insertions(+), 74 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 81029851a0..78902bc018 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -6,6 +6,7 @@ import type { DataZoomComponentOption } from "echarts/components"; import type { EChartsType } from "echarts/core"; import type { ECElementEvent, + SetOptionOpts, XAXisOption, YAXisOption, } from "echarts/types/dist/shared"; @@ -83,19 +84,19 @@ export class HaChartBase extends LitElement { this._listeners.push( listenMediaQuery("(prefers-reduced-motion)", (matches) => { - this._reducedMotion = matches; - this.chart?.setOption({ animation: !this._reducedMotion }); + if (this._reducedMotion !== matches) { + this._reducedMotion = matches; + this.chart?.setOption({ animation: !this._reducedMotion }); + } }) ); // Add keyboard event listeners const handleKeyDown = (ev: KeyboardEvent) => { - if ((isMac && ev.metaKey) || (!isMac && ev.ctrlKey)) { + if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) { this._modifierPressed = true; if (!this.options?.dataZoom) { - this.chart?.setOption({ - dataZoom: this._getDataZoomConfig(), - }); + this.chart?.setOption({ dataZoom: this._getDataZoomConfig() }); } } }; @@ -104,9 +105,7 @@ export class HaChartBase extends LitElement { if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) { this._modifierPressed = false; if (!this.options?.dataZoom) { - this.chart?.setOption({ - dataZoom: this._getDataZoomConfig(), - }); + this.chart?.setOption({ dataZoom: this._getDataZoomConfig() }); } } }; @@ -124,27 +123,26 @@ export class HaChartBase extends LitElement { } public willUpdate(changedProps: PropertyValues): void { - super.willUpdate(changedProps); - - if (!this.hasUpdated || !this.chart) { + if (!this.chart) { return; } if (changedProps.has("_themes")) { this._setupChart(); return; } + let chartOptions: ECOption = {}; + const chartUpdateParams: SetOptionOpts = { lazyUpdate: true }; if (changedProps.has("data")) { - this.chart.setOption( - { series: this.data }, - { lazyUpdate: true, replaceMerge: ["series"] } - ); + chartOptions.series = this.data; + chartUpdateParams.replaceMerge = ["series"]; } - if (changedProps.has("options") || changedProps.has("_isZoomed")) { - this.chart.setOption(this._createOptions(), { - lazyUpdate: true, - // if we replace the whole object, it will reset the dataZoom - replaceMerge: ["grid"], - }); + if (changedProps.has("options")) { + chartOptions = { ...chartOptions, ...this._createOptions() }; + } else if (this._isTouchDevice && changedProps.has("_isZoomed")) { + chartOptions.dataZoom = this._getDataZoomConfig(); + } + if (Object.keys(chartOptions).length > 0) { + this.chart.setOption(chartOptions, chartUpdateParams); } } @@ -158,7 +156,6 @@ export class HaChartBase extends LitElement { style=${styleMap({ height: this.height ?? `${this._getDefaultHeight()}px`, })} - @wheel=${this._handleWheel} >
${this._isZoomed @@ -240,8 +237,8 @@ export class HaChartBase extends LitElement { type: "inside", orient: "horizontal", filterMode: "none", - moveOnMouseMove: this._isZoomed, - preventDefaultMouseMove: this._isZoomed, + moveOnMouseMove: !this._isTouchDevice || this._isZoomed, + preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed, zoomLock: !this._isTouchDevice && !this._modifierPressed, }; } @@ -514,23 +511,6 @@ export class HaChartBase extends LitElement { private _handleZoomReset() { this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 }); - this._modifierPressed = false; - } - - private _handleWheel(e: WheelEvent) { - // if the window is not focused, we don't receive the keydown events but scroll still works - if (!this.options?.dataZoom) { - const modifierPressed = (isMac && e.metaKey) || (!isMac && e.ctrlKey); - if (modifierPressed) { - e.preventDefault(); - } - if (modifierPressed !== this._modifierPressed) { - this._modifierPressed = modifierPressed; - this.chart?.setOption({ - dataZoom: this._getDataZoomConfig(), - }); - } - } } static styles = css` diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 76e73a89e7..f7e60f7804 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -75,6 +75,8 @@ export class StateHistoryChartLine extends LitElement { @state() private _yWidth = 25; + @state() private _visualMap?: VisualMapComponentOption[]; + private _chartTime: Date = new Date(); protected render() { @@ -92,7 +94,7 @@ export class StateHistoryChartLine extends LitElement { `; } - private _renderTooltip(params: any) { + private _renderTooltip = (params: any) => { const time = params[0].axisValue; const title = formatDateTimeWithSeconds( @@ -115,7 +117,7 @@ export class StateHistoryChartLine extends LitElement { return; } // If the datapoint is not found, we need to find the last datapoint before the current time - let lastData; + let lastData: any; const data = dataset.data || []; for (let i = data.length - 1; i >= 0; i--) { const point = data[i]; @@ -175,7 +177,7 @@ export class StateHistoryChartLine extends LitElement { }) .join("
") ); - } + }; private _datasetHidden(ev: CustomEvent) { this._hiddenStats.add(ev.detail.name); @@ -208,8 +210,8 @@ export class StateHistoryChartLine extends LitElement { changedProps.has("minYAxis") || changedProps.has("maxYAxis") || changedProps.has("fitYData") || - changedProps.has("_chartData") || changedProps.has("paddingYAxis") || + changedProps.has("_visualMap") || changedProps.has("_yWidth") ) { const rtl = computeRTL(this.hass); @@ -280,37 +282,11 @@ export class StateHistoryChartLine extends LitElement { right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1, bottom: 30, }, - visualMap: this._chartData - .map((_, seriesIndex) => { - const dataIndex = this._datasetToDataIndex[seriesIndex]; - const data = this.data[dataIndex]; - if (!data.statistics || data.statistics.length === 0) { - return false; - } - // render stat data with a slightly transparent line - const firstStateTS = - data.states[0]?.last_changed ?? this.endTime.getTime(); - return { - show: false, - seriesIndex, - dimension: 0, - pieces: [ - { - max: firstStateTS - 0.01, - colorAlpha: 0.5, - }, - { - min: firstStateTS, - colorAlpha: 1, - }, - ], - }; - }) - .filter(Boolean) as VisualMapComponentOption[], + visualMap: this._visualMap, tooltip: { trigger: "axis", appendTo: document.body, - formatter: this._renderTooltip.bind(this), + formatter: this._renderTooltip, }, }; } @@ -725,6 +701,33 @@ export class StateHistoryChartLine extends LitElement { this._chartData = datasets; this._entityIds = entityIds; this._datasetToDataIndex = datasetToDataIndex; + const visualMap: VisualMapComponentOption[] = []; + this._chartData.forEach((_, seriesIndex) => { + const dataIndex = this._datasetToDataIndex[seriesIndex]; + const data = this.data[dataIndex]; + if (!data.statistics || data.statistics.length === 0) { + return; + } + // render stat data with a slightly transparent line + const firstStateTS = + data.states[0]?.last_changed ?? this.endTime.getTime(); + visualMap.push({ + show: false, + seriesIndex, + dimension: 0, + pieces: [ + { + max: firstStateTS - 0.01, + colorAlpha: 0.5, + }, + { + min: firstStateTS, + colorAlpha: 1, + }, + ], + }); + }); + this._visualMap = visualMap.length > 0 ? visualMap : undefined; } private _clampYAxis(value?: number | ((values: any) => number)) { From 37ee2bf308c9c76cf7aec8e7e18cb5a0da9edbe8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Feb 2025 08:47:03 -0500 Subject: [PATCH 15/24] Fix config flow URLs linking to device (#24223) --- src/panels/config/integrations/ha-config-flow-card.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/integrations/ha-config-flow-card.ts b/src/panels/config/integrations/ha-config-flow-card.ts index 54291d238a..4538dc3914 100644 --- a/src/panels/config/integrations/ha-config-flow-card.ts +++ b/src/panels/config/integrations/ha-config-flow-card.ts @@ -70,7 +70,7 @@ export class HaConfigFlowCard extends LitElement { ? html`
Date: Fri, 14 Feb 2025 13:53:48 +0200 Subject: [PATCH 16/24] Fix endTime of statistics-chart (#24233) --- src/components/chart/statistics-chart.ts | 5 +++-- src/panels/lovelace/cards/hui-statistics-graph-card.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index fd34fa5733..7954d76010 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -273,11 +273,13 @@ export class StatisticsChart extends LitElement { this._chartOptions = { xAxis: [ { + id: "xAxis", type: "time", min: startTime, - max: endTime, + max: this.endTime, }, { + id: "hiddenAxis", type: "time", show: false, }, @@ -368,7 +370,6 @@ export class StatisticsChart extends LitElement { if (endTime > new Date()) { endTime = new Date(); } - this.endTime = endTime; let unit: string | undefined | null; diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index 3754725c9d..a8402a8010 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -327,7 +327,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { ); const endDate = this._energyEnd; try { - let unitClass; + let unitClass: string | undefined | null; if (this._config!.unit && this._metadata) { const metadata = Object.values(this._metadata).find( (metaData) => From 64274d7355bc45aef51860c9a1357d0c37e78d26 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 14 Feb 2025 14:32:09 +0200 Subject: [PATCH 17/24] Fix inclusion dialog in ZwaveJS panel (#24234) --- .../zwave_js/zwave_js-config-dashboard.ts | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 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 96b91b41f2..2ddef810e7 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 @@ -76,6 +76,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { @state() private _statistics?: ZWaveJSControllerStatisticsUpdatedMessage; + private _dialogOpen = false; + protected async firstUpdated() { if (this.hass) { await this._fetchData(); @@ -104,11 +106,17 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { } ), subscribeS2Inclusion(this.hass, this.configEntryId, (message) => { - showZWaveJSAddNodeDialog(this, { - entry_id: this.configEntryId, - dsk: message.dsk, - onStop: () => setTimeout(() => this._fetchData(), 100), - }); + if (!this._dialogOpen) { + showZWaveJSAddNodeDialog(this, { + entry_id: this.configEntryId, + dsk: message.dsk, + onStop: () => { + setTimeout(() => this._fetchData(), 100); + this._dialogOpen = false; + }, + }); + this._dialogOpen = true; + } }), ]; } @@ -570,11 +578,17 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { } private async _addNodeClicked() { - showZWaveJSAddNodeDialog(this, { - entry_id: this.configEntryId!, - // refresh the data after the dialog is closed. add a small delay for the inclusion state to update - onStop: () => setTimeout(() => this._fetchData(), 100), - }); + if (!this._dialogOpen) { + showZWaveJSAddNodeDialog(this, { + entry_id: this.configEntryId!, + // refresh the data after the dialog is closed. add a small delay for the inclusion state to update + onStop: () => { + setTimeout(() => this._fetchData(), 100); + this._dialogOpen = false; + }, + }); + this._dialogOpen = true; + } } private async _removeNodeClicked() { From 8dbc203130db1683b423cebe28d3aca1be6e2e0a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 14 Feb 2025 13:33:35 +0100 Subject: [PATCH 18/24] Bumped version to 20250214.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a7effa1919..fc74110b4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250210.0" +version = "20250214.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" From 23f1925c8474e9ce056bade1f5a6f7c4b74f870d Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 18 Feb 2025 08:57:07 +0200 Subject: [PATCH 19/24] Make part of the chart rendering async for large datasets (#24260) --- src/components/chart/ha-chart-base.ts | 41 ++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 78902bc018..deb89d24d2 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -6,7 +6,6 @@ import type { DataZoomComponentOption } from "echarts/components"; import type { EChartsType } from "echarts/core"; import type { ECElementEvent, - SetOptionOpts, XAXisOption, YAXisOption, } from "echarts/types/dist/shared"; @@ -25,6 +24,7 @@ import type { HomeAssistant } from "../../types"; import { isMac } from "../../util/is_mac"; import "../ha-icon-button"; import { formatTimeLabel } from "./axis-label"; +import { ensureArray } from "../../common/array/ensure-array"; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; @@ -68,12 +68,16 @@ export class HaChartBase extends LitElement { private _listeners: (() => void)[] = []; + private _originalZrFlush?: () => void; + public disconnectedCallback() { super.disconnectedCallback(); while (this._listeners.length) { this._listeners.pop()!(); } this.chart?.dispose(); + this.chart = undefined; + this._originalZrFlush = undefined; } public connectedCallback() { @@ -86,7 +90,7 @@ export class HaChartBase extends LitElement { listenMediaQuery("(prefers-reduced-motion)", (matches) => { if (this._reducedMotion !== matches) { this._reducedMotion = matches; - this.chart?.setOption({ animation: !this._reducedMotion }); + this._setChartOptions({ animation: !this._reducedMotion }); } }) ); @@ -96,7 +100,7 @@ export class HaChartBase extends LitElement { if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) { this._modifierPressed = true; if (!this.options?.dataZoom) { - this.chart?.setOption({ dataZoom: this._getDataZoomConfig() }); + this._setChartOptions({ dataZoom: this._getDataZoomConfig() }); } } }; @@ -105,7 +109,7 @@ export class HaChartBase extends LitElement { if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) { this._modifierPressed = false; if (!this.options?.dataZoom) { - this.chart?.setOption({ dataZoom: this._getDataZoomConfig() }); + this._setChartOptions({ dataZoom: this._getDataZoomConfig() }); } } }; @@ -131,10 +135,8 @@ export class HaChartBase extends LitElement { return; } let chartOptions: ECOption = {}; - const chartUpdateParams: SetOptionOpts = { lazyUpdate: true }; if (changedProps.has("data")) { chartOptions.series = this.data; - chartUpdateParams.replaceMerge = ["series"]; } if (changedProps.has("options")) { chartOptions = { ...chartOptions, ...this._createOptions() }; @@ -142,7 +144,7 @@ export class HaChartBase extends LitElement { chartOptions.dataZoom = this._getDataZoomConfig(); } if (Object.keys(chartOptions).length > 0) { - this.chart.setOption(chartOptions, chartUpdateParams); + this._setChartOptions(chartOptions); } } @@ -509,6 +511,31 @@ export class HaChartBase extends LitElement { return Math.max(this.clientWidth / 2, 200); } + private _setChartOptions(options: ECOption) { + if (!this.chart) { + return; + } + if (!this._originalZrFlush) { + const dataSize = ensureArray(this.data).reduce( + (acc, series) => acc + (series.data as any[]).length, + 0 + ); + if (dataSize > 10000) { + // for large datasets zr.flush takes 30-40% of the render time + // so we delay it a bit to avoid blocking the main thread + const zr = this.chart.getZr(); + this._originalZrFlush = zr.flush.bind(zr); + zr.flush = () => { + setTimeout(() => { + this._originalZrFlush?.(); + }, 10); + }; + } + } + const replaceMerge = options.series ? ["series"] : []; + this.chart.setOption(options, { replaceMerge }); + } + private _handleZoomReset() { this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 }); } From 5c14afd94416e252635cfea8c6351533d8bda2f0 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Sun, 16 Feb 2025 05:06:01 -0800 Subject: [PATCH 20/24] Fix duplicate id in energy-devices-detail-graph-card (#24261) * Fix duplicate id in energy-devices-detail-graph-card * address compare * Update src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts Co-authored-by: Petar Petrov * prettier --------- Co-authored-by: Petar Petrov --- .../energy/hui-energy-devices-detail-graph-card.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 42edcdaacc..82ae3d31a4 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 @@ -448,7 +448,15 @@ export class HuiEnergyDevicesDetailGraphCard }); }); return sorted_devices - .map((device) => data.find((d) => (d.id as string).includes(device))!) + .map( + (device) => + data.find((d) => { + const id = (d.id as string) + .replace(/^compare-/, "") // Remove compare- prefix + .replace(/-\d+$/, ""); // Remove numeric suffix + return id === device; + })! + ) .filter(Boolean); } From 753fe719e3a1f3c9f9300febacd5490ab3068152 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:05:04 +0100 Subject: [PATCH 21/24] Fix backup forever retention settings (#24299) Fix forever retention settings --- .../config/ha-backup-config-schedule.ts | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/panels/config/backup/components/config/ha-backup-config-schedule.ts b/src/panels/config/backup/components/config/ha-backup-config-schedule.ts index d57389a001..c19d334487 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-schedule.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-schedule.ts @@ -46,7 +46,7 @@ enum BackupScheduleTime { } interface RetentionData { - type: "copies" | "days"; + type: "copies" | "days" | "forever"; value: number; } @@ -55,7 +55,7 @@ const RETENTION_PRESETS: Record< RetentionData > = { copies_3: { type: "copies", value: 3 }, - forever: { type: "days", value: 0 }, + forever: { type: "forever", value: 0 }, }; const SCHEDULE_OPTIONS = [ @@ -79,7 +79,10 @@ const computeRetentionPreset = ( data: RetentionData ): RetentionPreset | undefined => { for (const [key, value] of Object.entries(RETENTION_PRESETS)) { - if (value.type === data.type && value.value === data.value) { + if ( + value.type === data.type && + (value.type === RetentionPreset.FOREVER || value.value === data.value) + ) { return key as RetentionPreset; } } @@ -92,7 +95,7 @@ interface FormData { time?: string | null; days: BackupDay[]; retention: { - type: "copies" | "days"; + type: "copies" | "days" | "forever"; value: number; }; } @@ -142,7 +145,12 @@ class HaBackupConfigSchedule extends LitElement { ? config.schedule.days : [], retention: { - type: config.retention.days != null ? "days" : "copies", + type: + config.retention.days === null && config.retention.copies === null + ? "forever" + : config.retention.days != null + ? "days" + : "copies", value: config.retention.days ?? config.retention.copies ?? 3, }, }; @@ -160,9 +168,11 @@ class HaBackupConfigSchedule extends LitElement { : [], }, retention: - data.retention.type === "days" - ? { days: data.retention.value, copies: null } - : { copies: data.retention.value, days: null }, + data.retention.type === "forever" + ? { days: null, copies: null } + : data.retention.type === "days" + ? { days: data.retention.value, copies: null } + : { copies: data.retention.value, days: null }, }; fireEvent(this, "value-changed", { value: this.value }); @@ -481,9 +491,19 @@ class HaBackupConfigSchedule extends LitElement { private _retentionPresetChanged(ev) { ev.stopPropagation(); const target = ev.currentTarget as HaMdSelect; - const value = target.value as RetentionPreset; + let value = target.value as RetentionPreset; + + // custom needs to have a type of days or copies, set it to default copies 3 + if ( + value === RetentionPreset.CUSTOM && + this._retentionPreset === RetentionPreset.FOREVER + ) { + this._retentionPreset = value; + value = RetentionPreset.COPIES_3; + } else { + this._retentionPreset = value; + } - this._retentionPreset = value; if (value !== RetentionPreset.CUSTOM) { const data = this._getData(this.value); const retention = RETENTION_PRESETS[value]; @@ -493,7 +513,7 @@ class HaBackupConfigSchedule extends LitElement { } this._setData({ ...data, - retention: RETENTION_PRESETS[value], + retention, }); } } @@ -504,6 +524,7 @@ class HaBackupConfigSchedule extends LitElement { const value = parseInt(target.value); const clamped = clamp(value, MIN_VALUE, MAX_VALUE); const data = this._getData(this.value); + target.value = clamped.toString(); this._setData({ ...data, retention: { From 99559ff716c70cdc6a89d92df35069ffcd11e53c Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 19 Feb 2025 16:05:32 +0200 Subject: [PATCH 22/24] Enable downsampling in echarts (#24311) * Enable downsampling in echarts * remove unneeded symbol set --- src/components/chart/ha-chart-base.ts | 10 +++++----- src/components/chart/state-history-chart-line.ts | 5 +++-- src/components/chart/statistics-chart.ts | 5 ++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index deb89d24d2..a87b7aa3a2 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -521,14 +521,14 @@ export class HaChartBase extends LitElement { 0 ); if (dataSize > 10000) { - // for large datasets zr.flush takes 30-40% of the render time - // so we delay it a bit to avoid blocking the main thread + // delay the last bit of the render to avoid blocking the main thread + // this is not that impactful with sampling enabled but it doesn't hurt to have it const zr = this.chart.getZr(); - this._originalZrFlush = zr.flush.bind(zr); + this._originalZrFlush = zr.flush; zr.flush = () => { setTimeout(() => { - this._originalZrFlush?.(); - }, 10); + this._originalZrFlush?.call(zr); + }, 5); }; } } diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index f7e60f7804..27de961e41 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -354,9 +354,10 @@ export class StateHistoryChartLine extends LitElement { name: nameY, color, symbol: "circle", - step: "end", - animationDurationUpdate: 0, symbolSize: 1, + step: "end", + sampling: "minmax", + animationDurationUpdate: 0, lineStyle: { width: fill ? 0 : 1.5, }, diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 7954d76010..5c2372826c 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -492,8 +492,8 @@ export class StatisticsChart extends LitElement { : this.hass.localize( `ui.components.statistics_charts.statistic_types.${type}` ), - symbol: "circle", - symbolSize: 0, + symbol: "none", + sampling: "minmax", animationDurationUpdate: 0, lineStyle: { width: 1.5, @@ -511,7 +511,6 @@ export class StatisticsChart extends LitElement { if (band && this.chartType === "line") { series.stack = `band-${statistic_id}`; series.stackStrategy = "all"; - (series as LineSeriesOption).symbol = "none"; if (drawBands && type === "max") { (series as LineSeriesOption).areaStyle = { color: color + "3F", From 41c93f5f7e4a6aa45d6c3c680811d3e24fd3e46d Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:32:15 +0100 Subject: [PATCH 23/24] Fix hassio backup restore url (#24313) --- src/data/hassio/backup.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/data/hassio/backup.ts b/src/data/hassio/backup.ts index d9937e1dce..ba54ea2352 100644 --- a/src/data/hassio/backup.ts +++ b/src/data/hassio/backup.ts @@ -244,20 +244,23 @@ export const restoreBackup = async ( type: HassioBackupDetail["type"], backupSlug: string, backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams, - useSnapshotUrl: boolean + useBackupUrl: boolean ): Promise => { if (hass) { await hass.callApi>( "POST", - `hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`, + `hassio/${useBackupUrl ? "backups" : "snapshots"}/${backupSlug}/restore/${type}`, backupDetails ); } else { await handleFetchPromise( - fetch(`/api/hassio/backups/${backupSlug}/restore/${type}`, { - method: "POST", - body: JSON.stringify(backupDetails), - }) + fetch( + `/api/hassio/${useBackupUrl ? "backups" : "snapshots"}/${backupSlug}/restore/${type}`, + { + method: "POST", + body: JSON.stringify(backupDetails), + } + ) ); } }; From 75fadcca422fe9db8df852e0a5747a9ea1b1c8a9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 21 Feb 2025 19:31:09 +0100 Subject: [PATCH 24/24] Bumped version to 20250221.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fc74110b4b..f973be014b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250214.0" +version = "20250221.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md"