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" diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 78902bc018..a87b7aa3a2 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) { + // 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; + zr.flush = () => { + setTimeout(() => { + this._originalZrFlush?.call(zr); + }, 5); + }; + } + } + const replaceMerge = options.series ? ["series"] : []; + this.chart.setOption(options, { replaceMerge }); + } + private _handleZoomReset() { this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 }); } 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", 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), + } + ) ); } }; 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: { 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); }