mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Merge branch 'rc'
This commit is contained in:
commit
4a1b7d46ca
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20250214.0"
|
version = "20250221.0"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -6,7 +6,6 @@ import type { DataZoomComponentOption } from "echarts/components";
|
|||||||
import type { EChartsType } from "echarts/core";
|
import type { EChartsType } from "echarts/core";
|
||||||
import type {
|
import type {
|
||||||
ECElementEvent,
|
ECElementEvent,
|
||||||
SetOptionOpts,
|
|
||||||
XAXisOption,
|
XAXisOption,
|
||||||
YAXisOption,
|
YAXisOption,
|
||||||
} from "echarts/types/dist/shared";
|
} from "echarts/types/dist/shared";
|
||||||
@ -25,6 +24,7 @@ import type { HomeAssistant } from "../../types";
|
|||||||
import { isMac } from "../../util/is_mac";
|
import { isMac } from "../../util/is_mac";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
import { formatTimeLabel } from "./axis-label";
|
import { formatTimeLabel } from "./axis-label";
|
||||||
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
|
|
||||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||||
|
|
||||||
@ -68,12 +68,16 @@ export class HaChartBase extends LitElement {
|
|||||||
|
|
||||||
private _listeners: (() => void)[] = [];
|
private _listeners: (() => void)[] = [];
|
||||||
|
|
||||||
|
private _originalZrFlush?: () => void;
|
||||||
|
|
||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
while (this._listeners.length) {
|
while (this._listeners.length) {
|
||||||
this._listeners.pop()!();
|
this._listeners.pop()!();
|
||||||
}
|
}
|
||||||
this.chart?.dispose();
|
this.chart?.dispose();
|
||||||
|
this.chart = undefined;
|
||||||
|
this._originalZrFlush = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
@ -86,7 +90,7 @@ export class HaChartBase extends LitElement {
|
|||||||
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
||||||
if (this._reducedMotion !== matches) {
|
if (this._reducedMotion !== matches) {
|
||||||
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")) {
|
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||||
this._modifierPressed = true;
|
this._modifierPressed = true;
|
||||||
if (!this.options?.dataZoom) {
|
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")) {
|
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||||
this._modifierPressed = false;
|
this._modifierPressed = false;
|
||||||
if (!this.options?.dataZoom) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
let chartOptions: ECOption = {};
|
let chartOptions: ECOption = {};
|
||||||
const chartUpdateParams: SetOptionOpts = { lazyUpdate: true };
|
|
||||||
if (changedProps.has("data")) {
|
if (changedProps.has("data")) {
|
||||||
chartOptions.series = this.data;
|
chartOptions.series = this.data;
|
||||||
chartUpdateParams.replaceMerge = ["series"];
|
|
||||||
}
|
}
|
||||||
if (changedProps.has("options")) {
|
if (changedProps.has("options")) {
|
||||||
chartOptions = { ...chartOptions, ...this._createOptions() };
|
chartOptions = { ...chartOptions, ...this._createOptions() };
|
||||||
@ -142,7 +144,7 @@ export class HaChartBase extends LitElement {
|
|||||||
chartOptions.dataZoom = this._getDataZoomConfig();
|
chartOptions.dataZoom = this._getDataZoomConfig();
|
||||||
}
|
}
|
||||||
if (Object.keys(chartOptions).length > 0) {
|
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);
|
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() {
|
private _handleZoomReset() {
|
||||||
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
|
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
|
||||||
}
|
}
|
||||||
|
@ -354,9 +354,10 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
name: nameY,
|
name: nameY,
|
||||||
color,
|
color,
|
||||||
symbol: "circle",
|
symbol: "circle",
|
||||||
step: "end",
|
|
||||||
animationDurationUpdate: 0,
|
|
||||||
symbolSize: 1,
|
symbolSize: 1,
|
||||||
|
step: "end",
|
||||||
|
sampling: "minmax",
|
||||||
|
animationDurationUpdate: 0,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: fill ? 0 : 1.5,
|
width: fill ? 0 : 1.5,
|
||||||
},
|
},
|
||||||
|
@ -492,8 +492,8 @@ export class StatisticsChart extends LitElement {
|
|||||||
: this.hass.localize(
|
: this.hass.localize(
|
||||||
`ui.components.statistics_charts.statistic_types.${type}`
|
`ui.components.statistics_charts.statistic_types.${type}`
|
||||||
),
|
),
|
||||||
symbol: "circle",
|
symbol: "none",
|
||||||
symbolSize: 0,
|
sampling: "minmax",
|
||||||
animationDurationUpdate: 0,
|
animationDurationUpdate: 0,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
@ -511,7 +511,6 @@ export class StatisticsChart extends LitElement {
|
|||||||
if (band && this.chartType === "line") {
|
if (band && this.chartType === "line") {
|
||||||
series.stack = `band-${statistic_id}`;
|
series.stack = `band-${statistic_id}`;
|
||||||
series.stackStrategy = "all";
|
series.stackStrategy = "all";
|
||||||
(series as LineSeriesOption).symbol = "none";
|
|
||||||
if (drawBands && type === "max") {
|
if (drawBands && type === "max") {
|
||||||
(series as LineSeriesOption).areaStyle = {
|
(series as LineSeriesOption).areaStyle = {
|
||||||
color: color + "3F",
|
color: color + "3F",
|
||||||
|
@ -244,20 +244,23 @@ export const restoreBackup = async (
|
|||||||
type: HassioBackupDetail["type"],
|
type: HassioBackupDetail["type"],
|
||||||
backupSlug: string,
|
backupSlug: string,
|
||||||
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
||||||
useSnapshotUrl: boolean
|
useBackupUrl: boolean
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (hass) {
|
if (hass) {
|
||||||
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
|
`hassio/${useBackupUrl ? "backups" : "snapshots"}/${backupSlug}/restore/${type}`,
|
||||||
backupDetails
|
backupDetails
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await handleFetchPromise(
|
await handleFetchPromise(
|
||||||
fetch(`/api/hassio/backups/${backupSlug}/restore/${type}`, {
|
fetch(
|
||||||
method: "POST",
|
`/api/hassio/${useBackupUrl ? "backups" : "snapshots"}/${backupSlug}/restore/${type}`,
|
||||||
body: JSON.stringify(backupDetails),
|
{
|
||||||
})
|
method: "POST",
|
||||||
|
body: JSON.stringify(backupDetails),
|
||||||
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -46,7 +46,7 @@ enum BackupScheduleTime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface RetentionData {
|
interface RetentionData {
|
||||||
type: "copies" | "days";
|
type: "copies" | "days" | "forever";
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ const RETENTION_PRESETS: Record<
|
|||||||
RetentionData
|
RetentionData
|
||||||
> = {
|
> = {
|
||||||
copies_3: { type: "copies", value: 3 },
|
copies_3: { type: "copies", value: 3 },
|
||||||
forever: { type: "days", value: 0 },
|
forever: { type: "forever", value: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCHEDULE_OPTIONS = [
|
const SCHEDULE_OPTIONS = [
|
||||||
@ -79,7 +79,10 @@ const computeRetentionPreset = (
|
|||||||
data: RetentionData
|
data: RetentionData
|
||||||
): RetentionPreset | undefined => {
|
): RetentionPreset | undefined => {
|
||||||
for (const [key, value] of Object.entries(RETENTION_PRESETS)) {
|
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;
|
return key as RetentionPreset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,7 +95,7 @@ interface FormData {
|
|||||||
time?: string | null;
|
time?: string | null;
|
||||||
days: BackupDay[];
|
days: BackupDay[];
|
||||||
retention: {
|
retention: {
|
||||||
type: "copies" | "days";
|
type: "copies" | "days" | "forever";
|
||||||
value: number;
|
value: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -142,7 +145,12 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
? config.schedule.days
|
? config.schedule.days
|
||||||
: [],
|
: [],
|
||||||
retention: {
|
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,
|
value: config.retention.days ?? config.retention.copies ?? 3,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -160,9 +168,11 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
: [],
|
: [],
|
||||||
},
|
},
|
||||||
retention:
|
retention:
|
||||||
data.retention.type === "days"
|
data.retention.type === "forever"
|
||||||
? { days: data.retention.value, copies: null }
|
? { days: null, copies: null }
|
||||||
: { copies: data.retention.value, days: null },
|
: data.retention.type === "days"
|
||||||
|
? { days: data.retention.value, copies: null }
|
||||||
|
: { copies: data.retention.value, days: null },
|
||||||
};
|
};
|
||||||
|
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
@ -481,9 +491,19 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
private _retentionPresetChanged(ev) {
|
private _retentionPresetChanged(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.currentTarget as HaMdSelect;
|
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) {
|
if (value !== RetentionPreset.CUSTOM) {
|
||||||
const data = this._getData(this.value);
|
const data = this._getData(this.value);
|
||||||
const retention = RETENTION_PRESETS[value];
|
const retention = RETENTION_PRESETS[value];
|
||||||
@ -493,7 +513,7 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
}
|
}
|
||||||
this._setData({
|
this._setData({
|
||||||
...data,
|
...data,
|
||||||
retention: RETENTION_PRESETS[value],
|
retention,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -504,6 +524,7 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
const value = parseInt(target.value);
|
const value = parseInt(target.value);
|
||||||
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
|
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
|
||||||
const data = this._getData(this.value);
|
const data = this._getData(this.value);
|
||||||
|
target.value = clamped.toString();
|
||||||
this._setData({
|
this._setData({
|
||||||
...data,
|
...data,
|
||||||
retention: {
|
retention: {
|
||||||
|
@ -448,7 +448,15 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
return sorted_devices
|
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);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user