Merge branch 'rc'

This commit is contained in:
Bram Kragten 2025-02-14 13:33:47 +01:00
commit c68002214f
10 changed files with 103 additions and 90 deletions

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20250210.0" version = "20250214.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"

View File

@ -6,6 +6,7 @@ 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";
@ -83,19 +84,19 @@ export class HaChartBase extends LitElement {
this._listeners.push( this._listeners.push(
listenMediaQuery("(prefers-reduced-motion)", (matches) => { listenMediaQuery("(prefers-reduced-motion)", (matches) => {
this._reducedMotion = matches; if (this._reducedMotion !== matches) {
this.chart?.setOption({ animation: !this._reducedMotion }); this._reducedMotion = matches;
this.chart?.setOption({ animation: !this._reducedMotion });
}
}) })
); );
// Add keyboard event listeners // Add keyboard event listeners
const handleKeyDown = (ev: KeyboardEvent) => { const handleKeyDown = (ev: KeyboardEvent) => {
if ((isMac && ev.metaKey) || (!isMac && ev.ctrlKey)) { 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({ this.chart?.setOption({ dataZoom: this._getDataZoomConfig() });
dataZoom: this._getDataZoomConfig(),
});
} }
} }
}; };
@ -104,9 +105,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({ this.chart?.setOption({ dataZoom: this._getDataZoomConfig() });
dataZoom: this._getDataZoomConfig(),
});
} }
} }
}; };
@ -124,27 +123,26 @@ export class HaChartBase extends LitElement {
} }
public willUpdate(changedProps: PropertyValues): void { public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps); if (!this.chart) {
if (!this.hasUpdated || !this.chart) {
return; return;
} }
if (changedProps.has("_themes")) { if (changedProps.has("_themes")) {
this._setupChart(); this._setupChart();
return; return;
} }
let chartOptions: ECOption = {};
const chartUpdateParams: SetOptionOpts = { lazyUpdate: true };
if (changedProps.has("data")) { if (changedProps.has("data")) {
this.chart.setOption( chartOptions.series = this.data;
{ series: this.data }, chartUpdateParams.replaceMerge = ["series"];
{ lazyUpdate: true, replaceMerge: ["series"] }
);
} }
if (changedProps.has("options") || changedProps.has("_isZoomed")) { if (changedProps.has("options")) {
this.chart.setOption(this._createOptions(), { chartOptions = { ...chartOptions, ...this._createOptions() };
lazyUpdate: true, } else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
// if we replace the whole object, it will reset the dataZoom chartOptions.dataZoom = this._getDataZoomConfig();
replaceMerge: ["grid"], }
}); if (Object.keys(chartOptions).length > 0) {
this.chart.setOption(chartOptions, chartUpdateParams);
} }
} }
@ -158,7 +156,6 @@ export class HaChartBase extends LitElement {
style=${styleMap({ style=${styleMap({
height: this.height ?? `${this._getDefaultHeight()}px`, height: this.height ?? `${this._getDefaultHeight()}px`,
})} })}
@wheel=${this._handleWheel}
> >
<div class="chart"></div> <div class="chart"></div>
${this._isZoomed ${this._isZoomed
@ -240,8 +237,8 @@ export class HaChartBase extends LitElement {
type: "inside", type: "inside",
orient: "horizontal", orient: "horizontal",
filterMode: "none", filterMode: "none",
moveOnMouseMove: this._isZoomed, moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
preventDefaultMouseMove: this._isZoomed, preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
zoomLock: !this._isTouchDevice && !this._modifierPressed, zoomLock: !this._isTouchDevice && !this._modifierPressed,
}; };
} }
@ -514,23 +511,6 @@ export class HaChartBase extends LitElement {
private _handleZoomReset() { private _handleZoomReset() {
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 }); 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` static styles = css`

View File

@ -75,6 +75,8 @@ export class StateHistoryChartLine extends LitElement {
@state() private _yWidth = 25; @state() private _yWidth = 25;
@state() private _visualMap?: VisualMapComponentOption[];
private _chartTime: Date = new Date(); private _chartTime: Date = new Date();
protected render() { 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 time = params[0].axisValue;
const title = const title =
formatDateTimeWithSeconds( formatDateTimeWithSeconds(
@ -115,7 +117,7 @@ export class StateHistoryChartLine extends LitElement {
return; return;
} }
// If the datapoint is not found, we need to find the last datapoint before the current time // 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 || []; const data = dataset.data || [];
for (let i = data.length - 1; i >= 0; i--) { for (let i = data.length - 1; i >= 0; i--) {
const point = data[i]; const point = data[i];
@ -175,7 +177,7 @@ export class StateHistoryChartLine extends LitElement {
}) })
.join("<br>") .join("<br>")
); );
} };
private _datasetHidden(ev: CustomEvent) { private _datasetHidden(ev: CustomEvent) {
this._hiddenStats.add(ev.detail.name); this._hiddenStats.add(ev.detail.name);
@ -208,8 +210,8 @@ export class StateHistoryChartLine extends LitElement {
changedProps.has("minYAxis") || changedProps.has("minYAxis") ||
changedProps.has("maxYAxis") || changedProps.has("maxYAxis") ||
changedProps.has("fitYData") || changedProps.has("fitYData") ||
changedProps.has("_chartData") ||
changedProps.has("paddingYAxis") || changedProps.has("paddingYAxis") ||
changedProps.has("_visualMap") ||
changedProps.has("_yWidth") changedProps.has("_yWidth")
) { ) {
const rtl = computeRTL(this.hass); const rtl = computeRTL(this.hass);
@ -280,37 +282,11 @@ export class StateHistoryChartLine extends LitElement {
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1, right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
bottom: 30, bottom: 30,
}, },
visualMap: this._chartData visualMap: this._visualMap,
.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[],
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
appendTo: document.body, appendTo: document.body,
formatter: this._renderTooltip.bind(this), formatter: this._renderTooltip,
}, },
}; };
} }
@ -725,6 +701,33 @@ export class StateHistoryChartLine extends LitElement {
this._chartData = datasets; this._chartData = datasets;
this._entityIds = entityIds; this._entityIds = entityIds;
this._datasetToDataIndex = datasetToDataIndex; 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)) { private _clampYAxis(value?: number | ((values: any) => number)) {

View File

@ -273,11 +273,13 @@ export class StatisticsChart extends LitElement {
this._chartOptions = { this._chartOptions = {
xAxis: [ xAxis: [
{ {
id: "xAxis",
type: "time", type: "time",
min: startTime, min: startTime,
max: endTime, max: this.endTime,
}, },
{ {
id: "hiddenAxis",
type: "time", type: "time",
show: false, show: false,
}, },
@ -368,7 +370,6 @@ export class StatisticsChart extends LitElement {
if (endTime > new Date()) { if (endTime > new Date()) {
endTime = new Date(); endTime = new Date();
} }
this.endTime = endTime;
let unit: string | undefined | null; let unit: string | undefined | null;

View File

@ -66,6 +66,18 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => {
rel="noreferrer" rel="noreferrer"
>${hass.localize("ui.panel.config.tips.join_x")}</a >${hass.localize("ui.panel.config.tips.join_x")}</a
>`, >`,
mastodon: html`<a
href=${documentationUrl(hass, `/mastodon`)}
target="_blank"
rel="noreferrer"
>${hass.localize("ui.panel.config.tips.join_mastodon")}</a
>`,
bluesky: html`<a
href=${documentationUrl(hass, `/bluesky`)}
target="_blank"
rel="noreferrer"
>${hass.localize("ui.panel.config.tips.join_bluesky")}</a
>`,
discord: html`<a discord: html`<a
href=${documentationUrl(hass, `/join-chat`)} href=${documentationUrl(hass, `/join-chat`)}
target="_blank" target="_blank"

View File

@ -70,7 +70,7 @@ export class HaConfigFlowCard extends LitElement {
? html`<a ? html`<a
href=${this.flow.context.configuration_url.replace( href=${this.flow.context.configuration_url.replace(
/^homeassistant:\/\//, /^homeassistant:\/\//,
"" "/"
)} )}
rel="noreferrer" rel="noreferrer"
target=${this.flow.context.configuration_url.startsWith( target=${this.flow.context.configuration_url.startsWith(

View File

@ -76,6 +76,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
@state() @state()
private _statistics?: ZWaveJSControllerStatisticsUpdatedMessage; private _statistics?: ZWaveJSControllerStatisticsUpdatedMessage;
private _dialogOpen = false;
protected async firstUpdated() { protected async firstUpdated() {
if (this.hass) { if (this.hass) {
await this._fetchData(); await this._fetchData();
@ -104,11 +106,17 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
} }
), ),
subscribeS2Inclusion(this.hass, this.configEntryId, (message) => { subscribeS2Inclusion(this.hass, this.configEntryId, (message) => {
showZWaveJSAddNodeDialog(this, { if (!this._dialogOpen) {
entry_id: this.configEntryId, showZWaveJSAddNodeDialog(this, {
dsk: message.dsk, entry_id: this.configEntryId,
onStop: () => setTimeout(() => this._fetchData(), 100), 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() { private async _addNodeClicked() {
showZWaveJSAddNodeDialog(this, { if (!this._dialogOpen) {
entry_id: this.configEntryId!, showZWaveJSAddNodeDialog(this, {
// refresh the data after the dialog is closed. add a small delay for the inclusion state to update entry_id: this.configEntryId!,
onStop: () => setTimeout(() => this._fetchData(), 100), // 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() { private async _removeNodeClicked() {

View File

@ -327,7 +327,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
); );
const endDate = this._energyEnd; const endDate = this._energyEnd;
try { try {
let unitClass; let unitClass: string | undefined | null;
if (this._config!.unit && this._metadata) { if (this._config!.unit && this._metadata) {
const metadata = Object.values(this._metadata).find( const metadata = Object.values(this._metadata).find(
(metaData) => (metaData) =>

View File

@ -240,6 +240,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
.container.edit-mode { .container.edit-mode {
padding: 8px; padding: 8px;
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, 12px);
border-start-end-radius: 0;
border: 2px dashed var(--divider-color); border: 2px dashed var(--divider-color);
min-height: var(--row-height); min-height: var(--row-height);
} }

View File

@ -6052,8 +6052,10 @@
}, },
"tips": { "tips": {
"tip": "Tip!", "tip": "Tip!",
"join": "Join the community on our {forums}, {twitter}, {discord}, {blog} or {newsletter}", "join": "Join the community on our {forums}, {mastodon}, {bluesky}, {twitter}, {discord}, {blog} or {newsletter}",
"join_x": "X (formerly Twitter)", "join_x": "X (formerly Twitter)",
"join_mastodon": "Mastodon",
"join_bluesky": "Bluesky",
"join_forums": "Forums", "join_forums": "Forums",
"join_chat": "Chat", "join_chat": "Chat",
"join_blog": "Blog", "join_blog": "Blog",