From 0dff538298cb6a4fd5d28b38f2f84615d4707f14 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Thu, 30 Jan 2025 18:06:57 +0100 Subject: [PATCH 01/64] Backup location translations improvements (#23940) * Backup location translations improvements * Apply better translations --- src/translations/en.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/translations/en.json b/src/translations/en.json index 70bace21f2..65531f1388 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2678,19 +2678,19 @@ "encryption": { "title": "Encryption", "description": "All your backups are encrypted by default to keep your data private and secure.", - "location_encrypted": "This location is encrypted", - "location_unencrypted": "This location is unencrypted", - "location_encrypted_description": "Your data private and secure by securing it with your encryption key.", + "location_encrypted": "Backups made to this location will be encrypted", + "location_unencrypted": "Backups made to this location will be unencrypted", + "location_encrypted_description": "Your data is private and secure by encrypting backups with your encryption key.", "location_encrypted_cloud_description": "Home Assistant Cloud is the privacy-focused cloud. This is why it will only accept encrypted backups and why we don’t store your encryption key.", "location_encrypted_cloud_learn_more": "Learn more", "location_unencrypted_description": "Please keep your backups private and secure.", "encryption_turn_on": "Turn on", "encryption_turn_off": "Turn off", "encryption_turn_off_confirm_title": "Turn encryption off?", - "encryption_turn_off_confirm_text": "All your next backups will not be encrypted for this location. Please keep your backups private and secure.", + "encryption_turn_off_confirm_text": "After confirming, backups created will be unencrypted for this location. Please ensure your backups remain private and secure.", "encryption_turn_off_confirm_action": "Turn encryption off", "warning_encryption_turn_off": "Encryption turned off", - "warning_encryption_turn_off_description": "All your next backups will not be encrypted." + "warning_encryption_turn_off_description": "Backups will be unencrypted." } } }, From 91319be855b519e5f90a49634580dd890ed7ae32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Jan 2025 09:01:47 -1000 Subject: [PATCH 02/64] Reduce size of address column on Bluetooth Advertisement monitor (#23942) --- .../bluetooth/bluetooth-advertisement-monitor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts index e3d9122a4b..07f7f98a1c 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts @@ -84,7 +84,7 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement { hideable: false, moveable: false, direction: "asc", - flex: 2, + flex: 1, }, name: { title: localize("ui.panel.config.bluetooth.name"), From 8577b0721c7d9b15ede58edd3761e9a2deaaa9c4 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 30 Jan 2025 00:57:54 -0800 Subject: [PATCH 03/64] Fix untracked energy in compare (#23949) --- .../cards/energy/hui-energy-devices-detail-graph-card.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 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 943a3b6457..c56c31f23b 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 @@ -314,8 +314,9 @@ export class HuiEnergyDevicesDetailGraphCard processedData.forEach((device) => { device.data.forEach((datapoint) => { - totalDeviceConsumption[datapoint[0]] = - (totalDeviceConsumption[datapoint[0]] || 0) + datapoint[1]; + totalDeviceConsumption[datapoint[compare ? 2 : 0]] = + (totalDeviceConsumption[datapoint[compare ? 2 : 0]] || 0) + + datapoint[1]; }); }); const compareOffset = compare From 59b2582fe34611045340cd114ab2657d6b095c4b Mon Sep 17 00:00:00 2001 From: ildar170975 <71872483+ildar170975@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:26:11 +0300 Subject: [PATCH 04/64] Fix for "Increase generic entity row touch target" (#23953) fix for "touch target" --- src/panels/lovelace/components/hui-generic-entity-row.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts index dfa86f40f5..95d286d063 100644 --- a/src/panels/lovelace/components/hui-generic-entity-row.ts +++ b/src/panels/lovelace/components/hui-generic-entity-row.ts @@ -201,9 +201,7 @@ export class HuiGenericEntityRow extends LitElement { padding-inline-end: 8px; flex: 1 1 30%; min-height: 40px; - display: flex; - flex-direction: column; - justify-content: center; + line-height: 40px; } .info, .info > * { @@ -238,8 +236,7 @@ export class HuiGenericEntityRow extends LitElement { .value { direction: ltr; min-height: 40px; - display: flex; - align-items: center; + line-height: 40px; } `; } From e7931ce0499d496564fc81fdde67f7738cfc6f1a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 30 Jan 2025 09:56:52 +0100 Subject: [PATCH 05/64] Restore scroll position go back to backup settings page (#23955) --- src/panels/config/backup/ha-config-backup-settings.ts | 4 ++-- src/panels/config/backup/ha-config-backup.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/panels/config/backup/ha-config-backup-settings.ts b/src/panels/config/backup/ha-config-backup-settings.ts index 6b800b3574..79e0ead4e0 100644 --- a/src/panels/config/backup/ha-config-backup-settings.ts +++ b/src/panels/config/backup/ha-config-backup-settings.ts @@ -50,8 +50,8 @@ class HaConfigBackupSettings extends LitElement { } } - protected firstUpdated(_changedProperties: PropertyValues): void { - super.firstUpdated(_changedProperties); + public connectedCallback(): void { + super.connectedCallback(); this._scrollToSection(); } diff --git a/src/panels/config/backup/ha-config-backup.ts b/src/panels/config/backup/ha-config-backup.ts index 245927dffe..b488058b58 100644 --- a/src/panels/config/backup/ha-config-backup.ts +++ b/src/panels/config/backup/ha-config-backup.ts @@ -119,6 +119,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) { settings: { tag: "ha-config-backup-settings", load: () => import("./ha-config-backup-settings"), + cache: true, }, location: { tag: "ha-config-backup-location", From dc68aaa80381607614c24abbfc9151383b4cdfe7 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 30 Jan 2025 18:05:18 +0100 Subject: [PATCH 06/64] Add localizable "Actions" label to OAuth credentials picker (#23958) * Add localizable "Actions" label to OAuth credentials picker * Prettier --- .../application_credentials/ha-config-application-credentials.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/config/application_credentials/ha-config-application-credentials.ts b/src/panels/config/application_credentials/ha-config-application-credentials.ts index e252d2c46b..cb2fa6c5db 100644 --- a/src/panels/config/application_credentials/ha-config-application-credentials.ts +++ b/src/panels/config/application_credentials/ha-config-application-credentials.ts @@ -106,6 +106,7 @@ export class HaConfigApplicationCredentials extends LitElement { }, actions: { title: "", + label: localize("ui.panel.config.generic.headers.actions"), type: "overflow-menu", showNarrow: true, hideable: false, From 6a5936b2b2c20431a5a434df352c1da280ab627d Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:05:33 +0100 Subject: [PATCH 07/64] Add correct link to backup.create_automatic (#23959) --- .../backup/components/config/ha-backup-config-schedule.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 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 1336473972..2eefb3f8bf 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 @@ -403,11 +403,11 @@ class HaBackupConfigSchedule extends LitElement { backup_create: html`backup.createbackup.create_automatic`, })} From 10498ce18d8a35320bd6bebd4155fcb98435b3ec Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 30 Jan 2025 12:36:10 +0100 Subject: [PATCH 08/64] Display device name in bluetooth panel (#23960) --- .../bluetooth-advertisement-monitor.ts | 86 +++++++++++++++++-- .../bluetooth/dialog-bluetooth-device-info.ts | 2 - src/translations/en.json | 2 + 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts index 07f7f98a1c..361eaced72 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts @@ -1,8 +1,9 @@ +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import type { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { storage } from "../../../../../common/decorators/storage"; import type { HASSDomEvent } from "../../../../../common/dom/fire_event"; import type { LocalizeFunc } from "../../../../../common/translations/localize"; import type { @@ -11,9 +12,6 @@ import type { } from "../../../../../components/data-table/ha-data-table"; import "../../../../../components/ha-fab"; import "../../../../../components/ha-icon-button"; -import "../../../../../layouts/hass-tabs-subpage-data-table"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; import type { BluetoothDeviceData, BluetoothScannersDetails, @@ -22,6 +20,10 @@ import { subscribeBluetoothAdvertisements, subscribeBluetoothScannersDetails, } from "../../../../../data/bluetooth"; +import type { DeviceRegistryEntry } from "../../../../../data/device_registry"; +import "../../../../../layouts/hass-tabs-subpage-data-table"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant, Route } from "../../../../../types"; import { showBluetoothDeviceInfoDialog } from "./show-dialog-bluetooth-device-info"; @customElement("bluetooth-advertisement-monitor") @@ -38,6 +40,22 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement { @state() private _scanners: BluetoothScannersDetails = {}; + @state() private _sourceDevices: Record = {}; + + @storage({ + key: "bluetooth-advertisement-table-grouping", + state: false, + subscribe: false, + }) + private _activeGrouping?: string = "source"; + + @storage({ + key: "bluetooth-advertisement-table-collapsed", + state: false, + subscribe: false, + }) + private _activeCollapsed: string[] = []; + private _unsub_advertisements?: UnsubscribeFunc; private _unsub_scanners?: UnsubscribeFunc; @@ -57,6 +75,19 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement { this._scanners = scanners; } ); + + const devices = Object.values(this.hass.devices); + const bluetoothDevices = devices.filter((device) => + device.connections.find((connection) => connection[0] === "bluetooth") + ); + this._sourceDevices = Object.fromEntries( + bluetoothDevices.map((device) => { + const connection = device.connections.find( + (c) => c[0] === "bluetooth" + )!; + return [connection[1], device]; + }) + ); } } @@ -91,14 +122,28 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement { filterable: true, sortable: true, }, + device: { + title: localize("ui.panel.config.bluetooth.device"), + filterable: true, + sortable: true, + template: (data) => html`${data.device || "-"}`, + }, source: { title: localize("ui.panel.config.bluetooth.source"), filterable: true, sortable: true, + groupable: true, + }, + source_address: { + title: localize("ui.panel.config.bluetooth.source_address"), + filterable: true, + sortable: true, + defaultHidden: true, }, rssi: { title: localize("ui.panel.config.bluetooth.rssi"), type: "numeric", + maxWidth: "60px", sortable: true, }, }; @@ -108,11 +153,22 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement { ); private _dataWithNamedSourceAndIds = memoizeOne((data) => - data.map((row) => ({ - ...row, - id: row.address, - source: this._scanners[row.source]?.name || row.source, - })) + data.map((row) => { + const device = this._sourceDevices[row.address]; + const scannerDevice = this._sourceDevices[row.source]; + const scanner = this._scanners[row.source]; + return { + ...row, + id: row.address, + source_address: row.source, + source: + scannerDevice?.name_by_user || + scannerDevice?.name || + scanner?.name || + row.source, + device: device?.name_by_user || device?.name || undefined, + }; + }) ); protected render(): TemplateResult { @@ -124,11 +180,23 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement { .columns=${this._columns(this.hass.localize)} .data=${this._dataWithNamedSourceAndIds(this._data)} @row-click=${this._handleRowClicked} + .initialGroupColumn=${this._activeGrouping} + .initialCollapsedGroups=${this._activeCollapsed} + @grouping-changed=${this._handleGroupingChanged} + @collapsed-changed=${this._handleCollapseChanged} clickable > `; } + private _handleGroupingChanged(ev: CustomEvent) { + this._activeGrouping = ev.detail.value; + } + + private _handleCollapseChanged(ev: CustomEvent) { + this._activeCollapsed = ev.detail.value; + } + private _handleRowClicked(ev: HASSDomEvent) { const entry = this._data.find((ent) => ent.address === ev.detail.id); showBluetoothDeviceInfoDialog(this, { diff --git a/src/panels/config/integrations/integration-panels/bluetooth/dialog-bluetooth-device-info.ts b/src/panels/config/integrations/integration-panels/bluetooth/dialog-bluetooth-device-info.ts index 2aeb28cb77..635da34b03 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/dialog-bluetooth-device-info.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/dialog-bluetooth-device-info.ts @@ -53,8 +53,6 @@ class DialogBluetoothDeviceInfo extends LitElement implements HassDialog { return html` Date: Thu, 30 Jan 2025 15:39:59 +0200 Subject: [PATCH 09/64] Use CSS variables to theme echarts (#23963) * Use CSS variables to theme echarts * fix styles --- src/common/color/colors.ts | 10 + src/components/chart/ha-chart-base.ts | 195 ++++++++++++++++-- .../chart/state-history-chart-line.ts | 9 +- .../chart/state-history-chart-timeline.ts | 9 +- src/components/chart/state-history-charts.ts | 5 +- 5 files changed, 202 insertions(+), 26 deletions(-) diff --git a/src/common/color/colors.ts b/src/common/color/colors.ts index a661562ead..aace900bbd 100644 --- a/src/common/color/colors.ts +++ b/src/common/color/colors.ts @@ -1,3 +1,4 @@ +import memoizeOne from "memoize-one"; import { theme2hex } from "./convert-color"; export const COLORS = [ @@ -74,3 +75,12 @@ export function getGraphColorByIndex( getColorByIndex(index); return theme2hex(themeColor); } + +export const getAllGraphColors = memoizeOne( + (style: CSSStyleDeclaration) => + COLORS.map((_color, index) => getGraphColorByIndex(index, style)), + (newArgs: [CSSStyleDeclaration], lastArgs: [CSSStyleDeclaration]) => + // this is not ideal, but we need to memoize the colors + newArgs[0].getPropertyValue("--graph-color-1") === + lastArgs[0].getPropertyValue("--graph-color-1") +); diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 42450c8bb8..aac85a0e01 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -20,6 +20,7 @@ import type { ECOption } from "../../resources/echarts"; import { listenMediaQuery } from "../../common/dom/media_query"; import type { Themes } from "../../data/ws-themes"; import { themesContext } from "../../data/context"; +import { getAllGraphColors } from "../../common/color/colors"; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; @@ -183,10 +184,9 @@ export class HaChartBase extends LitElement { } const echarts = (await import("../../resources/echarts")).default; - this.chart = echarts.init( - container, - this._themes.darkMode ? "dark" : "light" - ); + echarts.registerTheme("custom", this._createTheme()); + + this.chart = echarts.init(container, "custom"); this.chart.on("legendselectchanged", (params: any) => { if (this.externalHidden) { const isSelected = params.selected[params.name]; @@ -237,24 +237,14 @@ export class HaChartBase extends LitElement { } private _createOptions(): ECOption { - const darkMode = this._themes.darkMode ?? false; - const options = { - backgroundColor: "transparent", animation: !this._reducedMotion, - darkMode, + darkMode: this._themes.darkMode ?? false, aria: { show: true, }, dataZoom: this._getDataZoomConfig(), ...this.options, - legend: this.options?.legend - ? { - // we should create our own theme but this is a quick fix for now - inactiveColor: darkMode ? "#444" : "#ccc", - ...this.options.legend, - } - : undefined, }; const isMobile = window.matchMedia( @@ -274,6 +264,181 @@ export class HaChartBase extends LitElement { return options; } + private _createTheme() { + const style = getComputedStyle(this); + return { + color: getAllGraphColors(style), + backgroundColor: "transparent", + textStyle: { + color: style.getPropertyValue("--primary-text-color"), + }, + title: { + textStyle: { + color: style.getPropertyValue("--primary-text-color"), + }, + subtextStyle: { + color: style.getPropertyValue("--secondary-text-color"), + }, + }, + line: { + lineStyle: { + width: 1.5, + }, + symbolSize: 1, + symbol: "circle", + smooth: false, + }, + bar: { + itemStyle: { + barBorderWidth: 1.5, + }, + }, + categoryAxis: { + axisLine: { + show: false, + }, + axisTick: { + show: false, + }, + axisLabel: { + show: true, + color: style.getPropertyValue("--primary-text-color"), + }, + splitLine: { + show: false, + lineStyle: { + color: style.getPropertyValue("--divider-color"), + }, + }, + splitArea: { + show: false, + areaStyle: { + color: [ + style.getPropertyValue("--divider-color") + "3F", + style.getPropertyValue("--divider-color") + "7F", + ], + }, + }, + }, + valueAxis: { + axisLine: { + show: true, + lineStyle: { + color: style.getPropertyValue("--divider-color"), + }, + }, + axisTick: { + show: true, + lineStyle: { + color: style.getPropertyValue("--divider-color"), + }, + }, + axisLabel: { + show: true, + color: style.getPropertyValue("--primary-text-color"), + }, + splitLine: { + show: true, + lineStyle: { + color: style.getPropertyValue("--divider-color"), + }, + }, + splitArea: { + show: false, + areaStyle: { + color: [ + style.getPropertyValue("--divider-color") + "3F", + style.getPropertyValue("--divider-color") + "7F", + ], + }, + }, + }, + logAxis: { + axisLine: { + show: true, + lineStyle: { + color: style.getPropertyValue("--divider-color"), + }, + }, + axisTick: { + show: true, + lineStyle: { + color: style.getPropertyValue("--divider-color"), + }, + }, + axisLabel: { + show: true, + color: style.getPropertyValue("--primary-text-color"), + }, + splitLine: { + show: true, + lineStyle: { + color: style.getPropertyValue("--divider-color"), + }, + }, + splitArea: { + show: false, + areaStyle: { + color: [ + style.getPropertyValue("--divider-color") + "3F", + style.getPropertyValue("--divider-color") + "7F", + ], + }, + }, + }, + timeAxis: { + axisLine: { + show: true, + lineStyle: { + color: style.getPropertyValue("--divider-color"), + }, + }, + axisTick: { + show: true, + lineStyle: { + color: style.getPropertyValue("--divider-color"), + }, + }, + axisLabel: { + show: true, + color: style.getPropertyValue("--primary-text-color"), + }, + splitLine: { + show: true, + lineStyle: { + color: style.getPropertyValue("--divider-color"), + }, + }, + splitArea: { + show: false, + areaStyle: { + color: [ + style.getPropertyValue("--divider-color") + "3F", + style.getPropertyValue("--divider-color") + "7F", + ], + }, + }, + }, + legend: { + textStyle: { + color: style.getPropertyValue("--primary-text-color"), + }, + inactiveColor: style.getPropertyValue("--disabled-text-color"), + }, + tooltip: { + axisPointer: { + lineStyle: { + color: style.getPropertyValue("--divider-color"), + }, + crossStyle: { + color: style.getPropertyValue("--divider-color"), + }, + }, + }, + timeline: {}, + }; + } + private _getDefaultHeight() { return Math.max(this.clientWidth / 2, 400); } diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 9ba683cdf4..66da97fccf 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -158,9 +158,6 @@ export class StateHistoryChartLine extends LitElement { ) { const dayDifference = differenceInDays(this.endTime, this.startTime); const rtl = computeRTL(this.hass); - const splitLineStyle = this.hass.themes?.darkMode - ? { opacity: 0.15 } - : {}; this._chartOptions = { xAxis: { type: "time", @@ -176,7 +173,6 @@ export class StateHistoryChartLine extends LitElement { }, splitLine: { show: true, - lineStyle: splitLineStyle, }, minInterval: dayDifference >= 89 // quarter @@ -196,9 +192,8 @@ export class StateHistoryChartLine extends LitElement { nameTextStyle: { align: "left", }, - splitLine: { - show: true, - lineStyle: splitLineStyle, + axisLine: { + show: false, }, axisLabel: { margin: 5, diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index 910b4cd8d3..3e4962c3be 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -197,9 +197,12 @@ export class StateHistoryChartTimeline extends LitElement { max: this.endTime, axisTick: { show: true, - lineStyle: { - opacity: 0.4, - }, + }, + splitLine: { + show: false, + }, + axisLine: { + show: false, }, axisLabel: getTimeAxisLabelConfig( this.hass.locale, diff --git a/src/components/chart/state-history-charts.ts b/src/components/chart/state-history-charts.ts index b909fc1d09..45c767cf04 100644 --- a/src/components/chart/state-history-charts.ts +++ b/src/components/chart/state-history-charts.ts @@ -135,7 +135,7 @@ export class StateHistoryCharts extends LitElement { return html``; } if (!Array.isArray(item)) { - return html`
+ return html`
Date: Thu, 30 Jan 2025 18:02:56 +0100 Subject: [PATCH 10/64] Fix location icon when many locations in backup datatable (#23964) * Fix location icon when many locations in backup datatable * Reuse data * Don't copy twice * Improve naming --- .../config/backup/ha-config-backup-backups.ts | 131 +++++++++++------- 1 file changed, 83 insertions(+), 48 deletions(-) diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index 315403bbed..548cf9190d 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -141,7 +141,10 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { }; private _columns = memoizeOne( - (localize: LocalizeFunc): DataTableColumnContainer => ({ + ( + localize: LocalizeFunc, + maxDisplayedAgents: number + ): DataTableColumnContainer => ({ name: { title: localize("ui.panel.config.backup.name"), main: true, @@ -172,54 +175,75 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { locations: { title: localize("ui.panel.config.backup.locations"), showNarrow: true, - minWidth: "60px", - template: (backup) => html` -
- ${(backup.agent_ids || []).map((agentId) => { - const name = computeBackupAgentName( - this.hass.localize, - agentId, - this.agents - ); - if (isLocalAgent(agentId)) { + // 24 icon size, 4 gap, 16 left and right padding + minWidth: `${maxDisplayedAgents * 24 + (maxDisplayedAgents - 1) * 4 + 32}px`, + template: (backup) => { + const agentIds = backup.agent_ids; + const displayedAgentIds = + agentIds.length > maxDisplayedAgents + ? [...agentIds].splice(0, maxDisplayedAgents - 1) + : agentIds; + const agentsMore = Math.max( + agentIds.length - displayedAgentIds.length, + 0 + ); + return html` +
+ ${displayedAgentIds.map((agentId) => { + const name = computeBackupAgentName( + this.hass.localize, + agentId, + this.agents + ); + if (isLocalAgent(agentId)) { + return html` + + `; + } + if (isNetworkMountAgent(agentId)) { + return html` + + `; + } + const domain = computeDomain(agentId); return html` - + /> `; - } - if (isNetworkMountAgent(agentId)) { - return html` - - `; - } - const domain = computeDomain(agentId); - return html` - ${name} - `; - })} -
- `, + })} + ${agentsMore + ? html` + + +${agentsMore} + + ` + : nothing} +
+ `; + }, }, actions: { title: "", @@ -293,20 +317,31 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { } return filteredBackups.map((backup) => { const type = backup.with_automatic_settings ? "automatic" : "manual"; + const agentIds = Object.keys(backup.agents); return { ...backup, size: computeBackupSize(backup), - agent_ids: Object.keys(backup.agents).sort(compareAgents), + agent_ids: agentIds.sort(compareAgents), formatted_type: localize(`ui.panel.config.backup.type.${type}`), }; }); } ); + private _maxAgents = memoizeOne((data: BackupRow[]): number => + Math.max(...data.map((row) => row.agent_ids.length)) + ); + protected render(): TemplateResult { const backupInProgress = "state" in this.manager && this.manager.state === "in_progress"; + const data = this._data(this.backups, this._filters, this.hass.localize); + const maxDisplayedAgents = Math.min( + this._maxAgents(data), + this.narrow ? 3 : 5 + ); + return html` Date: Thu, 30 Jan 2025 17:43:39 +0100 Subject: [PATCH 11/64] Fix backup location config not updated (#23965) --- src/panels/config/backup/ha-config-backup-settings.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/panels/config/backup/ha-config-backup-settings.ts b/src/panels/config/backup/ha-config-backup-settings.ts index 79e0ead4e0..d7bb4266da 100644 --- a/src/panels/config/backup/ha-config-backup-settings.ts +++ b/src/panels/config/backup/ha-config-backup-settings.ts @@ -53,6 +53,8 @@ class HaConfigBackupSettings extends LitElement { public connectedCallback(): void { super.connectedCallback(); this._scrollToSection(); + // Update config the page is displayed (e.g. when coming back from a location detail page) + this._config = this.config; } private async _scrollToSection() { From 6aab60cf451c1debabecffbec94c94140b5d4adb Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 30 Jan 2025 18:43:06 +0200 Subject: [PATCH 12/64] Dynamically reorder energy devices (echarts) (#23966) * Dynamically reorder energy devices (echarts) * fix initial sorting in hui-energy-devices-detail-graph-card * fix dynamic reordering in devices detail --- .../energy/hui-energy-devices-detail-graph-card.ts | 13 +++++++++---- .../cards/energy/hui-energy-devices-graph-card.ts | 9 ++++----- 2 files changed, 13 insertions(+), 9 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 c56c31f23b..e0d2db9e60 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 @@ -334,10 +334,12 @@ export class HuiEnergyDevicesDetailGraphCard } untrackedConsumption.push(dataPoint); }); + // random id to always add untracked at the end + const order = Date.now(); const dataset: BarSeriesOption = { type: "bar", cursor: "default", - id: compare ? "compare-untracked" : "untracked", + id: compare ? `compare-untracked-${order}` : `untracked-${order}`, name: this.hass.localize( "ui.panel.lovelace.cards.energy.energy_devices_detail_graph.untracked_consumption" ), @@ -420,9 +422,10 @@ export class HuiEnergyDevicesDetailGraphCard data.push({ type: "bar", cursor: "default", + // add order to id, otherwise echarts refuses to reorder them id: compare - ? `compare-${source.stat_consumption}` - : source.stat_consumption, + ? `compare-${source.stat_consumption}-${order}` + : `${source.stat_consumption}-${order}`, name: source.name || getStatisticLabel( @@ -439,7 +442,9 @@ export class HuiEnergyDevicesDetailGraphCard stack: compare ? "devicesCompare" : "devices", }); }); - return data; + return sorted_devices.map( + (device) => data.find((d) => (d.id as string).includes(device))! + ); } static styles = css` 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 e12f747c90..3c99584415 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 @@ -88,7 +88,7 @@ export class HuiEnergyDevicesGraphCard @@ -110,18 +110,17 @@ export class HuiEnergyDevicesGraphCard } private _createOptions = memoizeOne( - (darkMode: boolean): ECOption => ({ + (data: BarSeriesOption[]): ECOption => ({ xAxis: { type: "value", name: "kWh", - splitLine: { - lineStyle: darkMode ? { opacity: 0.15 } : {}, - }, }, yAxis: { type: "category", inverse: true, triggerEvent: true, + // take order from data + data: data[0]?.data?.map((d: any) => d.value[1]), axisLabel: { formatter: this._getDeviceName.bind(this), overflow: "truncate", From c337bc5f97aadba16a2f41230d5908f0f741e895 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 30 Jan 2025 16:49:05 +0100 Subject: [PATCH 13/64] Improve backup settings display on mobile (#23967) --- .../components/config/ha-backup-config-data.ts | 5 +++-- .../config/ha-backup-config-schedule.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/panels/config/backup/components/config/ha-backup-config-data.ts b/src/panels/config/backup/components/config/ha-backup-config-data.ts index d1ebb6e67a..8e12093819 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-data.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-data.ts @@ -378,8 +378,9 @@ class HaBackupConfigData extends LitElement { } @media all and (max-width: 450px) { ha-md-select { - min-width: 160px; - width: 160px; + min-width: 140px; + width: 140px; + --md-filled-field-content-space: 0; } } `; 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 2eefb3f8bf..428ffdca6d 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 @@ -545,6 +545,12 @@ class HaBackupConfigSchedule extends LitElement { ha-md-select, ha-time-input { min-width: 160px; + width: 160px; + --md-filled-field-content-space: 0; + } + ha-time-input { + min-width: 120px; + width: 120px; } } ha-md-textfield#value { @@ -553,6 +559,16 @@ class HaBackupConfigSchedule extends LitElement { ha-md-select#type { min-width: 100px; } + @media all and (max-width: 450px) { + ha-md-textfield#value { + min-width: 60px; + margin: 0 -8px; + } + ha-md-select#type { + min-width: 120px; + width: 120px; + } + } ha-expansion-panel { --expansion-panel-summary-padding: 0 16px; --expansion-panel-content-padding: 0 16px; From 9449f5ad0a0deac74269b0baeb173799a7708b52 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 30 Jan 2025 18:08:13 +0100 Subject: [PATCH 14/64] Bumped version to 20250130.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 323d65a092..50db59c959 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250129.0" +version = "20250130.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" From cae1ca52f02e16758f44a8f634217cc113bb6502 Mon Sep 17 00:00:00 2001 From: ildar170975 <71872483+ildar170975@users.noreply.github.com> Date: Fri, 31 Jan 2025 09:41:38 +0300 Subject: [PATCH 15/64] Fix for "Increase generic entity row touch target (2) (#23973) * Revert "Fix for "Increase generic entity row touch target" (#23953)" This reverts commit 028472fc7bd08311801c5085075a727b909563e6. * conditional style --- src/panels/lovelace/components/hui-generic-entity-row.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts index 95d286d063..b166eeab77 100644 --- a/src/panels/lovelace/components/hui-generic-entity-row.ts +++ b/src/panels/lovelace/components/hui-generic-entity-row.ts @@ -200,7 +200,8 @@ export class HuiGenericEntityRow extends LitElement { padding-inline-start: 16px; padding-inline-end: 8px; flex: 1 1 30%; - min-height: 40px; + } + .info:not(:has(.secondary)) { line-height: 40px; } .info, @@ -235,7 +236,6 @@ export class HuiGenericEntityRow extends LitElement { } .value { direction: ltr; - min-height: 40px; line-height: 40px; } `; From f44c5d7a639340e243171ced73d1ef97830347c8 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:45:37 -0800 Subject: [PATCH 16/64] Improve statistics graph axis when using energy_date_selection (#23974) --- src/components/chart/statistics-chart.ts | 6 ++++++ .../lovelace/cards/hui-statistics-graph-card.ts | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 6c54e511d9..614b2bce4a 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -56,6 +56,8 @@ export class StatisticsChart extends LitElement { @property() public unit?: string; + @property({ attribute: false }) public startTime?: Date; + @property({ attribute: false }) public endTime?: Date; @property({ attribute: false, type: Array }) @@ -124,6 +126,8 @@ export class StatisticsChart extends LitElement { changedProps.has("fitYData") || changedProps.has("logarithmicScale") || changedProps.has("hideLegend") || + changedProps.has("startTime") || + changedProps.has("endTime") || changedProps.has("_legendData") ) { this._createOptions(); @@ -218,6 +222,8 @@ export class StatisticsChart extends LitElement { this.hass.config, dayDifference ), + min: this.startTime, + max: this.endTime, axisLine: { show: false, }, diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index 1ecde26862..f94e970073 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -6,7 +6,10 @@ import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import "../../../components/ha-card"; import { getEnergyDataCollection } from "../../../data/energy"; -import { getSuggestedPeriod } from "./energy/common/energy-chart-options"; +import { + getSuggestedMax, + getSuggestedPeriod, +} from "./energy/common/energy-chart-options"; import type { Statistics, StatisticsMetaData, @@ -274,10 +277,19 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { .unit=${this._unit} .minYAxis=${this._config.min_y_axis} .maxYAxis=${this._config.max_y_axis} + .startTime=${this._energyStart} + .endTime=${this._energyEnd && this._energyStart + ? getSuggestedMax( + differenceInDays(this._energyEnd, this._energyStart), + this._energyEnd + ) + : undefined} .fitYData=${this._config.fit_y_data || false} .hideLegend=${this._config.hide_legend || false} .logarithmicScale=${this._config.logarithmic_scale || false} - .daysToShow=${this._config.days_to_show || DEFAULT_DAYS_TO_SHOW} + .daysToShow=${this._energyStart && this._energyEnd + ? differenceInDays(this._energyEnd, this._energyStart) + : this._config.days_to_show || DEFAULT_DAYS_TO_SHOW} .height=${this._config.grid_options?.rows ? "100%" : undefined} >
From 251e6399f535a523f27b9493dabe13608e276c9b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 31 Jan 2025 09:42:51 +0100 Subject: [PATCH 17/64] Reduce chart height to 300px (#23979) --- src/components/chart/ha-chart-base.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index aac85a0e01..59a77ea6c1 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -440,7 +440,7 @@ export class HaChartBase extends LitElement { } private _getDefaultHeight() { - return Math.max(this.clientWidth / 2, 400); + return Math.max(this.clientWidth / 2, 300); } private _handleZoomReset() { @@ -470,7 +470,7 @@ export class HaChartBase extends LitElement { } .chart-container { position: relative; - max-height: var(--chart-max-height, 400px); + max-height: var(--chart-max-height, 300px); } .chart { width: 100%; From b388d1fd423aa38af9da3d43e8bfa281c0c84def Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 31 Jan 2025 12:02:39 +0200 Subject: [PATCH 18/64] Fix statistics echarts with negative values (#23983) * Fix statistics echarts with negative values * fix border-radius of negative bar values * revert timeline label width to previous max values --- .../chart/state-history-chart-timeline.ts | 2 +- src/components/chart/statistics-chart.ts | 23 +++++++++++++------ .../lovelace/cards/hui-history-graph-card.ts | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index 3e4962c3be..5618a158cb 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -183,7 +183,7 @@ export class StateHistoryChartTimeline extends LitElement { private _createOptions() { const narrow = this.narrow; const showNames = this.chunked || this.showNames; - const maxInternalLabelWidth = narrow ? 70 : 165; + const maxInternalLabelWidth = narrow ? 105 : 185; const labelWidth = showNames ? Math.max(this.paddingYAxis, this._yWidth) : 0; diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 614b2bce4a..0225818253 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -375,10 +375,12 @@ export class StatisticsChart extends LitElement { ) { // if the end of the previous data doesn't match the start of the current data, // we have to draw a gap so add a value at the end time, and then an empty value. - d.data!.push([prevEndTime, ...prevValues[i]!]); + d.data!.push( + this._transformDataValue([prevEndTime, ...prevValues[i]!]) + ); d.data!.push([prevEndTime, null]); } - d.data!.push([start, ...dataValues[i]!]); + d.data!.push(this._transformDataValue([start, ...dataValues[i]!])); }); prevValues = dataValues; prevEndTime = end; @@ -455,14 +457,14 @@ export class StatisticsChart extends LitElement { borderWidth: 1.5, } : undefined, - color: band ? color + "3F" : color + "7F", + color: + band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color, }; if (band && this.chartType === "line") { series.stack = `band-${statistic_id}`; + series.stackStrategy = "all"; (series as LineSeriesOption).symbol = "none"; - (series as LineSeriesOption).lineStyle = { - opacity: 0, - }; + (series as LineSeriesOption).lineStyle = { width: 1.5 }; if (drawBands && type === "max") { (series as LineSeriesOption).areaStyle = { color: color + "3F", @@ -495,7 +497,7 @@ export class StatisticsChart extends LitElement { } } else if (type === "max" && this.chartType === "line") { const max = stat.max || 0; - val.push(max - (stat.min || 0)); + val.push(Math.abs(max - (stat.min || 0))); val.push(max); } else { val.push(stat[type] ?? null); @@ -535,6 +537,13 @@ export class StatisticsChart extends LitElement { this._statisticIds = statisticIds; } + private _transformDataValue(val: [Date, ...(number | null)[]]) { + if (this.chartType === "bar" && val[1] && val[1] < 0) { + return { value: val, itemStyle: { borderRadius: [0, 0, 4, 4] } }; + } + return val; + } + static styles = css` :host { display: block; diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index b77360cef8..53758b07a5 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -244,7 +244,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { })}`; const columns = this._config.grid_options?.columns ?? 12; - const narrow = Number.isNaN(columns) || Number(columns) < 12; + const narrow = Number.isNaN(columns) || Number(columns) <= 12; return html` From 277202e363d6371ee149cac03b716afa080cecab Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 31 Jan 2025 11:46:01 +0100 Subject: [PATCH 19/64] Use smooth line for statistic line chart (#23984) * Use smooth line for statistic line chart * Use same smooth options as chartjs --- src/components/chart/statistics-chart.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 0225818253..409af35617 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -432,6 +432,8 @@ export class StatisticsChart extends LitElement { const series: LineSeriesOption | BarSeriesOption = { id: `${statistic_id}-${type}`, type: this.chartType, + smooth: this.chartType === "line" ? 0.4 : false, + smoothMonotone: "x", cursor: "default", data: [], name: name From 684cd0f6277a3704fc5ab7980bc55eef963c10d0 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 31 Jan 2025 12:13:46 +0200 Subject: [PATCH 20/64] Fix legend resetting on zoom (#23985) --- src/components/chart/ha-chart-base.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 59a77ea6c1..85a2e4a538 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -142,7 +142,6 @@ export class HaChartBase extends LitElement { "dataZoom", "dataset", "tooltip", - "legend", "grid", "visualMap", ], From 181122177b639331c3450d373c2d54f629d3c86d Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 31 Jan 2025 16:44:22 +0200 Subject: [PATCH 21/64] Echarts: fix Y scaling (#23988) * Echarts: fix scaling of Y axis * fix fit logic to only extend the limits * handle invalid min for log scale --- src/components/chart/state-history-chart-line.ts | 15 +++++++++++++-- src/components/chart/statistics-chart.ts | 12 +++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 66da97fccf..3dedd3cacf 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -158,6 +158,11 @@ export class StateHistoryChartLine extends LitElement { ) { const dayDifference = differenceInDays(this.endTime, this.startTime); const rtl = computeRTL(this.hass); + const minYAxis = + // log(0) is -Infinity, so we need to set a minimum value + this.logarithmicScale && typeof this.minYAxis === "number" + ? Math.max(this.minYAxis, 0.1) + : this.minYAxis; this._chartOptions = { xAxis: { type: "time", @@ -184,8 +189,14 @@ export class StateHistoryChartLine extends LitElement { yAxis: { type: this.logarithmicScale ? "log" : "value", name: this.unit, - min: this.fitYData ? this.minYAxis : undefined, - max: this.fitYData ? this.maxYAxis : undefined, + min: + this.fitYData && typeof minYAxis === "number" + ? ({ min }) => Math.min(min, minYAxis!) + : minYAxis, + max: + this.fitYData && typeof this.maxYAxis === "number" + ? ({ max }) => Math.max(max, this.maxYAxis!) + : this.maxYAxis, position: rtl ? "right" : "left", scale: true, nameGap: 2, diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 409af35617..6d88f3a5d6 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -247,9 +247,15 @@ export class StatisticsChart extends LitElement { }, position: computeRTL(this.hass) ? "right" : "left", // @ts-ignore - scale: this.chartType !== "bar", - min: this.fitYData ? undefined : this.minYAxis, - max: this.fitYData ? undefined : this.maxYAxis, + scale: true, + min: + this.fitYData && typeof this.minYAxis === "number" + ? ({ min }) => Math.min(min, this.minYAxis!) + : this.minYAxis, + max: + this.fitYData && typeof this.maxYAxis === "number" + ? ({ max }) => Math.max(max, this.maxYAxis!) + : this.maxYAxis, splitLine: { show: true, lineStyle: splitLineStyle, From 15f33e1f191b64e1a55b95b3e7f02fab545021f3 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 31 Jan 2025 16:42:46 +0200 Subject: [PATCH 22/64] Echarts: show all series in tooltip (#23989) * Echarts: show all series in tooltip * fix typo * remove duplicate tooltip entries in statistics chart * take last valid point instead of first --- .../chart/state-history-chart-line.ts | 103 +++++++++++------- src/components/chart/statistics-chart.ts | 12 +- 2 files changed, 74 insertions(+), 41 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 3dedd3cacf..f55c205000 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -88,45 +88,74 @@ export class StateHistoryChartLine extends LitElement { `; } - private _renderTooltip(params) { - return params - .map((param, index: number) => { - let value = `${formatNumber( - param.value[1] as number, - this.hass.locale, - getNumberFormatOptions( - undefined, - this.hass.entities[this._entityIds[param.seriesIndex]] - ) - )} ${this.unit}`; - const dataIndex = this._datasetToDataIndex[param.seriesIndex]; - const data = this.data[dataIndex]; - if (data.statistics && data.statistics.length > 0) { - value += "
     "; - const source = - data.states.length === 0 || - param.value[0] < data.states[0].last_changed - ? `${this.hass.localize( - "ui.components.history_charts.source_stats" - )}` - : `${this.hass.localize( - "ui.components.history_charts.source_history" - )}`; - value += source; + private _renderTooltip(params: any) { + const time = params[0].axisValue; + const title = + formatDateTimeWithSeconds( + new Date(time), + this.hass.locale, + this.hass.config + ) + "
"; + const datapoints: Record[] = []; + this._chartData.forEach((dataset, index) => { + const param = params.find( + (p: Record) => p.seriesIndex === index + ); + if (param) { + datapoints.push(param); + return; + } + // If the datapoint is not found, we need to find the last datapoint before the current time + let lastData; + const data = dataset.data || []; + for (let i = data.length - 1; i >= 0; i--) { + const point = data[i]; + if (point && point[0] <= time && point[1]) { + lastData = point; + break; } + } + if (!lastData) return; + datapoints.push({ + seriesName: dataset.name, + seriesIndex: index, + value: lastData, + // HTML copied from echarts. May change based on options + marker: ``, + }); + }); + return ( + title + + datapoints + .map((param) => { + let value = `${formatNumber( + param.value[1] as number, + this.hass.locale, + getNumberFormatOptions( + undefined, + this.hass.entities[this._entityIds[param.seriesIndex]] + ) + )} ${this.unit}`; + const dataIndex = this._datasetToDataIndex[param.seriesIndex]; + const data = this.data[dataIndex]; + if (data.statistics && data.statistics.length > 0) { + value += "
     "; + const source = + data.states.length === 0 || + param.value[0] < data.states[0].last_changed + ? `${this.hass.localize( + "ui.components.history_charts.source_stats" + )}` + : `${this.hass.localize( + "ui.components.history_charts.source_history" + )}`; + value += source; + } - const time = - index === 0 - ? formatDateTimeWithSeconds( - new Date(param.value[0]), - this.hass.locale, - this.hass.config - ) + "
" - : ""; - return `${time}${param.marker} ${param.seriesName}: ${value} - `; - }) - .join("
"); + return `${param.marker} ${param.seriesName}: ${value}`; + }) + .join("
") + ); } public willUpdate(changedProps: PropertyValues) { diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 6d88f3a5d6..f35d1cdcbb 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -185,9 +185,12 @@ export class StatisticsChart extends LitElement { this.requestUpdate("_hiddenStats"); } - private _renderTooltip = (params: any) => - params + private _renderTooltip = (params: any) => { + const rendered: Record = {}; + return params .map((param, index: number) => { + if (rendered[param.seriesName]) return ""; + rendered[param.seriesName] = true; const value = `${formatNumber( // max series can have 3 values, as the second value is the max-min to form a band (param.value[2] ?? param.value[1]) as number, @@ -206,10 +209,11 @@ export class StatisticsChart extends LitElement { this.hass.config ) + "
" : ""; - return `${time}${param.marker} ${param.seriesName}: ${value} - `; + return `${time}${param.marker} ${param.seriesName}: ${value}`; }) + .filter(Boolean) .join("
"); + }; private _createOptions() { const splitLineStyle = this.hass.themes?.darkMode ? { opacity: 0.15 } : {}; From f0a56e75f536e3bb542061162a9cbdd5bca0a399 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 31 Jan 2025 17:10:43 +0100 Subject: [PATCH 23/64] Improve encrypted backup dialog (#23991) * Improve encrypted backup dialog * Remove unused code --- .../dialog-download-decrypted-backup.ts | 225 ++++++++++++++++++ .../show-dialog-download-decrypted-backup.ts | 21 ++ .../config/backup/ha-config-backup-backups.ts | 7 +- .../config/backup/ha-config-backup-details.ts | 8 +- .../config/backup/helper/download_backup.ts | 182 ++++++-------- src/translations/en.json | 13 +- 6 files changed, 326 insertions(+), 130 deletions(-) create mode 100644 src/panels/config/backup/dialogs/dialog-download-decrypted-backup.ts create mode 100644 src/panels/config/backup/dialogs/show-dialog-download-decrypted-backup.ts diff --git a/src/panels/config/backup/dialogs/dialog-download-decrypted-backup.ts b/src/panels/config/backup/dialogs/dialog-download-decrypted-backup.ts new file mode 100644 index 0000000000..a9ac25a9d1 --- /dev/null +++ b/src/panels/config/backup/dialogs/dialog-download-decrypted-backup.ts @@ -0,0 +1,225 @@ +import { mdiClose } from "@mdi/js"; +import type { CSSResultGroup } from "lit"; +import { LitElement, css, html, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-dialog-header"; +import "../../../../components/ha-icon-button"; +import "../../../../components/ha-icon-next"; +import "../../../../components/ha-md-dialog"; +import type { HaMdDialog } from "../../../../components/ha-md-dialog"; +import "../../../../components/ha-md-list"; +import "../../../../components/ha-md-list-item"; +import "../../../../components/ha-svg-icon"; +import "../../../../components/ha-password-field"; +import "../../../../components/ha-alert"; +import { + canDecryptBackupOnDownload, + getPreferredAgentForDownload, +} from "../../../../data/backup"; +import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyle, haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { downloadBackupFile } from "../helper/download_backup"; +import type { DownloadDecryptedBackupDialogParams } from "./show-dialog-download-decrypted-backup"; + +@customElement("ha-dialog-download-decrypted-backup") +class DialogDownloadDecryptedBackup extends LitElement implements HassDialog { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _opened = false; + + @state() private _params?: DownloadDecryptedBackupDialogParams; + + @query("ha-md-dialog") private _dialog?: HaMdDialog; + + @state() private _encryptionKey = ""; + + @state() private _error = ""; + + public showDialog(params: DownloadDecryptedBackupDialogParams): void { + this._opened = true; + this._params = params; + } + + public closeDialog() { + this._dialog?.close(); + return true; + } + + private _dialogClosed() { + if (this._opened) { + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + this._opened = false; + this._params = undefined; + this._encryptionKey = ""; + this._error = ""; + } + + protected render() { + if (!this._opened || !this._params) { + return nothing; + } + + return html` + + + + + ${this.hass.localize( + "ui.panel.config.backup.dialogs.download.title" + )} + + + +
+

+ ${this.hass.localize( + "ui.panel.config.backup.dialogs.download.description" + )} +

+

+ ${this.hass.localize( + "ui.panel.config.backup.dialogs.download.download_backup_encrypted", + { + download_it_encrypted: html``, + } + )} +

+ + + + ${this._error + ? html`${this._error}` + : nothing} +
+
+ + ${this.hass.localize("ui.dialogs.generic.cancel")} + + + + ${this.hass.localize( + "ui.panel.config.backup.dialogs.download.download" + )} + +
+
+ `; + } + + private _cancel() { + this.closeDialog(); + } + + private async _submit() { + if (this._encryptionKey === "") { + return; + } + try { + await canDecryptBackupOnDownload( + this.hass, + this._params!.backup.backup_id, + this._agentId, + this._encryptionKey + ); + downloadBackupFile( + this.hass, + this._params!.backup.backup_id, + this._agentId, + this._encryptionKey + ); + this.closeDialog(); + } catch (err: any) { + if (err?.code === "password_incorrect") { + this._error = this.hass.localize( + "ui.panel.config.backup.dialogs.download.incorrect_encryption_key" + ); + } else if (err?.code === "decrypt_not_supported") { + this._error = this.hass.localize( + "ui.panel.config.backup.dialogs.download.decryption_not_supported" + ); + } else { + alert(err.message); + } + } + } + + private _keyChanged(ev) { + this._encryptionKey = ev.currentTarget.value; + this._error = ""; + } + + private get _agentId() { + if (this._params?.agentId) { + return this._params.agentId; + } + return getPreferredAgentForDownload( + Object.keys(this._params!.backup.agents) + ); + } + + private async _downloadEncrypted() { + downloadBackupFile( + this.hass, + this._params!.backup.backup_id, + this._agentId + ); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + ha-md-dialog { + --dialog-content-padding: 8px 24px; + max-width: 500px; + } + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-md-dialog { + max-width: none; + } + div[slot="content"] { + margin-top: 0; + } + } + + button.link { + background: none; + border: none; + padding: 0; + font-size: 14px; + color: var(--primary-color); + text-decoration: underline; + cursor: pointer; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-dialog-download-decrypted-backup": DialogDownloadDecryptedBackup; + } +} diff --git a/src/panels/config/backup/dialogs/show-dialog-download-decrypted-backup.ts b/src/panels/config/backup/dialogs/show-dialog-download-decrypted-backup.ts new file mode 100644 index 0000000000..9f6984171d --- /dev/null +++ b/src/panels/config/backup/dialogs/show-dialog-download-decrypted-backup.ts @@ -0,0 +1,21 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { BackupContent } from "../../../../data/backup"; + +export interface DownloadDecryptedBackupDialogParams { + backup: BackupContent; + agentId?: string; +} + +export const loadDownloadDecryptedBackupDialog = () => + import("./dialog-download-decrypted-backup"); + +export const showDownloadDecryptedBackupDialog = ( + element: HTMLElement, + params: DownloadDecryptedBackupDialogParams +) => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-dialog-download-decrypted-backup", + dialogImport: loadDownloadDecryptedBackupDialog, + dialogParams: params, + }); +}; diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index 548cf9190d..4bab6ee118 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -531,12 +531,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { } private async _downloadBackup(backup: BackupContent): Promise { - downloadBackup( - this.hass, - this, - backup, - this.config?.create_backup.password - ); + downloadBackup(this.hass, this, backup, this.config); } private async _deleteBackup(backup: BackupContent): Promise { diff --git a/src/panels/config/backup/ha-config-backup-details.ts b/src/panels/config/backup/ha-config-backup-details.ts index 6a9d8706c3..ee3a701f47 100644 --- a/src/panels/config/backup/ha-config-backup-details.ts +++ b/src/panels/config/backup/ha-config-backup-details.ts @@ -401,13 +401,7 @@ class HaConfigBackupDetails extends LitElement { } private async _downloadBackup(agentId?: string): Promise { - await downloadBackup( - this.hass, - this, - this._backup!, - this.config?.create_backup.password, - agentId - ); + await downloadBackup(this.hass, this, this._backup!, this.config, agentId); } private async _deleteBackup(): Promise { diff --git a/src/panels/config/backup/helper/download_backup.ts b/src/panels/config/backup/helper/download_backup.ts index e6b2f8d2c7..53a0d96823 100644 --- a/src/panels/config/backup/helper/download_backup.ts +++ b/src/panels/config/backup/helper/download_backup.ts @@ -1,20 +1,17 @@ import type { LitElement } from "lit"; +import { getSignedPath } from "../../../../data/auth"; +import type { BackupConfig, BackupContent } from "../../../../data/backup"; import { canDecryptBackupOnDownload, getBackupDownloadUrl, getPreferredAgentForDownload, - type BackupContent, } from "../../../../data/backup"; import type { HomeAssistant } from "../../../../types"; -import { - showAlertDialog, - showConfirmationDialog, - showPromptDialog, -} from "../../../lovelace/custom-card-helpers"; -import { getSignedPath } from "../../../../data/auth"; import { fileDownload } from "../../../../util/file_download"; +import { showAlertDialog } from "../../../lovelace/custom-card-helpers"; +import { showDownloadDecryptedBackupDialog } from "../dialogs/show-dialog-download-decrypted-backup"; -const triggerDownload = async ( +export const downloadBackupFile = async ( hass: HomeAssistant, backupId: string, preferedAgent: string, @@ -27,120 +24,79 @@ const triggerDownload = async ( fileDownload(signedUrl.path); }; -const downloadEncryptedBackup = async ( - hass: HomeAssistant, - element: LitElement, - backup: BackupContent, - agentId?: string -) => { - if ( - await showConfirmationDialog(element, { - title: "Encryption key incorrect", - text: hass.localize( - "ui.panel.config.backup.dialogs.download.incorrect_entered_encryption_key" - ), - confirmText: "Download encrypted", - }) - ) { - const agentIds = Object.keys(backup.agents); - const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds); - - triggerDownload(hass, backup.backup_id, preferedAgent); - } -}; - -const requestEncryptionKey = async ( - hass: HomeAssistant, - element: LitElement, - backup: BackupContent, - agentId?: string -): Promise => { - const encryptionKey = await showPromptDialog(element, { - title: hass.localize( - "ui.panel.config.backup.dialogs.show_encryption_key.title" - ), - text: hass.localize( - "ui.panel.config.backup.dialogs.download.incorrect_current_encryption_key" - ), - inputLabel: hass.localize( - "ui.panel.config.backup.dialogs.show_encryption_key.title" - ), - inputType: "password", - confirmText: hass.localize("ui.common.download"), - }); - if (encryptionKey === null) { - return; - } - downloadBackup(hass, element, backup, encryptionKey, agentId, true); -}; - export const downloadBackup = async ( hass: HomeAssistant, element: LitElement, backup: BackupContent, - encryptionKey?: string | null, - agentId?: string, - userProvided = false + backupConfig?: BackupConfig, + agentId?: string ): Promise => { const agentIds = Object.keys(backup.agents); const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds); const isProtected = backup.agents[preferedAgent]?.protected; - if (isProtected) { - if (encryptionKey) { - try { - await canDecryptBackupOnDownload( - hass, - backup.backup_id, - preferedAgent, - encryptionKey - ); - } catch (err: any) { - if (err?.code === "password_incorrect") { - if (userProvided) { - downloadEncryptedBackup(hass, element, backup, agentId); - } else { - requestEncryptionKey(hass, element, backup, agentId); - } - return; - } - if (err?.code === "decrypt_not_supported") { - showAlertDialog(element, { - title: hass.localize( - "ui.panel.config.backup.dialogs.download.decryption_unsupported_title" - ), - text: hass.localize( - "ui.panel.config.backup.dialogs.download.decryption_unsupported" - ), - confirm() { - triggerDownload(hass, backup.backup_id, preferedAgent); - }, - }); - encryptionKey = undefined; - return; - } - - showAlertDialog(element, { - title: hass.localize( - "ui.panel.config.backup.dialogs.download.error_check_title", - { - error: err.message, - } - ), - text: hass.localize( - "ui.panel.config.backup.dialogs.download.error_check_description", - { - error: err.message, - } - ), - }); - return; - } - } else { - requestEncryptionKey(hass, element, backup, agentId); - return; - } + if (!isProtected) { + downloadBackupFile(hass, backup.backup_id, preferedAgent); } - await triggerDownload(hass, backup.backup_id, preferedAgent, encryptionKey); + const encryptionKey = backupConfig?.create_backup?.password; + + if (!encryptionKey) { + showDownloadDecryptedBackupDialog(element, { + backup, + agentId: preferedAgent, + }); + return; + } + + try { + // Check if we can decrypt it + await canDecryptBackupOnDownload( + hass, + backup.backup_id, + preferedAgent, + encryptionKey + ); + downloadBackupFile(hass, backup.backup_id, preferedAgent, encryptionKey); + } catch (err: any) { + // If encryption key is incorrect, ask for encryption key + if (err?.code === "password_incorrect") { + showDownloadDecryptedBackupDialog(element, { + backup, + agentId: preferedAgent, + }); + return; + } + // If decryption is not supported, ask for confirmation and download it encrypted + if (err?.code === "decrypt_not_supported") { + showAlertDialog(element, { + title: hass.localize( + "ui.panel.config.backup.dialogs.download.decryption_unsupported_title" + ), + text: hass.localize( + "ui.panel.config.backup.dialogs.download.decryption_unsupported" + ), + confirm() { + downloadBackupFile(hass, backup.backup_id, preferedAgent); + }, + }); + return; + } + + // Else, show generic error + showAlertDialog(element, { + title: hass.localize( + "ui.panel.config.backup.dialogs.download.error_check_title", + { + error: err.message, + } + ), + text: hass.localize( + "ui.panel.config.backup.dialogs.download.error_check_description", + { + error: err.message, + } + ), + }); + } }; diff --git a/src/translations/en.json b/src/translations/en.json index 47076e459e..e78f1b5e44 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2392,11 +2392,16 @@ "download": { "decryption_unsupported_title": "Decryption unsupported", "decryption_unsupported": "Decryption is not supported for this backup. The downloaded backup will remain encrypted and can't be opened. To restore it, you will need the encryption key.", - "incorrect_entered_encryption_key": "The entered encryption key was incorrect, try again or download the encrypted backup. The encrypted backup can't be opened. To restore it, you will need the encryption key.", - "download_encrypted": "Download encrypted", - "incorrect_current_encryption_key": "This backup is encrypted with a different encryption key than the current one, please enter the encryption key of this backup.", "error_check_title": "Error checking backup", - "error_check_description": "An error occurred while checking the backup, please try again. Error message: {error}" + "error_check_description": "An error occurred while checking the backup, please try again. Error message: {error}", + "title": "Download backup", + "description": "This backup is encrypted with a different encryption key than the current one, please enter the encryption key of this backup.", + "download_backup_encrypted": "You can still {download_it_encrypted}. To restore it, you will need the encryption key.", + "download_it_encrypted": "download the backup encrypted", + "encryption_key": "Encryption key", + "incorrect_encryption_key": "Incorrect encryption key", + "decryption_not_supported": "Decryption not supported", + "download": "Download" } }, "agents": { From 3769f8c7c0601b42d087468b81059071840d022a Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 31 Jan 2025 16:37:47 +0200 Subject: [PATCH 24/64] Hide irrelevant errors from echarts zoom (#23992) --- src/state/logging-mixin.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/state/logging-mixin.ts b/src/state/logging-mixin.ts index 79e07ccd63..4a14a441af 100644 --- a/src/state/logging-mixin.ts +++ b/src/state/logging-mixin.ts @@ -29,21 +29,14 @@ export const loggingMixin = >( return; } if ( - // !__DEV__ && - ev.message.includes("ResizeObserver loop limit exceeded") || + (!__DEV__ && + ev.message.includes("ResizeObserver loop limit exceeded")) || ev.message.includes( "ResizeObserver loop completed with undelivered notifications" ) || - (ev.error.stack.includes("echarts") && - (ev.message.includes( - "Cannot read properties of undefined (reading 'hostedBy')" - ) || - ev.message.includes( - "Cannot read properties of undefined (reading 'scale')" - ) || - ev.message.includes( - "Cannot read properties of null (reading 'innerHTML')" - ))) + (ev.error.stack.includes("DataZoomModel") && + (ev.message.includes("undefined") || + ev.message.includes("Cannot read properties of null"))) ) { ev.preventDefault(); ev.stopImmediatePropagation(); From 2fcb64d4a131f7ab5a6fb33f980ad1e533dbec0a Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 31 Jan 2025 18:42:51 +0200 Subject: [PATCH 25/64] Echarts: auto scale Y in log charts (#23994) * Echarts: auto scale Y in log charts * fix statistics chart log scale --- .../chart/state-history-chart-line.ts | 46 +++++++++++++------ src/components/chart/statistics-chart.ts | 41 +++++++++++++---- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index f55c205000..a580f76a5f 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -187,11 +187,24 @@ export class StateHistoryChartLine extends LitElement { ) { const dayDifference = differenceInDays(this.endTime, this.startTime); const rtl = computeRTL(this.hass); - const minYAxis = - // log(0) is -Infinity, so we need to set a minimum value - this.logarithmicScale && typeof this.minYAxis === "number" - ? Math.max(this.minYAxis, 0.1) - : this.minYAxis; + let minYAxis: number | ((values: { min: number }) => number) | undefined = + this.minYAxis; + let maxYAxis: number | ((values: { max: number }) => number) | undefined = + this.maxYAxis; + if (typeof minYAxis === "number") { + if (this.fitYData) { + minYAxis = ({ min }) => Math.min(min, this.minYAxis!); + } + } else if (this.logarithmicScale) { + minYAxis = ({ min }) => (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); + } this._chartOptions = { xAxis: { type: "time", @@ -218,14 +231,8 @@ export class StateHistoryChartLine extends LitElement { yAxis: { type: this.logarithmicScale ? "log" : "value", name: this.unit, - min: - this.fitYData && typeof minYAxis === "number" - ? ({ min }) => Math.min(min, minYAxis!) - : minYAxis, - max: - this.fitYData && typeof this.maxYAxis === "number" - ? ({ max }) => Math.max(max, this.maxYAxis!) - : this.maxYAxis, + min: this._clampYAxis(minYAxis), + max: this._clampYAxis(maxYAxis), position: rtl ? "right" : "left", scale: true, nameGap: 2, @@ -644,6 +651,19 @@ export class StateHistoryChartLine extends LitElement { this._entityIds = entityIds; this._datasetToDataIndex = datasetToDataIndex; } + + private _clampYAxis(value?: number | ((values: any) => number)) { + 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); + } + if (typeof value === "function") { + return (values: any) => Math.max(value(values), 0.1); + } + } + return 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 f35d1cdcbb..9c7ea82e0d 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -218,6 +218,24 @@ export class StatisticsChart extends LitElement { private _createOptions() { const splitLineStyle = this.hass.themes?.darkMode ? { opacity: 0.15 } : {}; const dayDifference = this.daysToShow ?? 1; + let minYAxis: number | ((values: { min: number }) => number) | undefined = + this.minYAxis; + let maxYAxis: number | ((values: { max: number }) => number) | undefined = + this.maxYAxis; + if (typeof minYAxis === "number") { + if (this.fitYData) { + minYAxis = ({ min }) => Math.min(min, this.minYAxis!); + } + } else if (this.logarithmicScale) { + minYAxis = ({ min }) => (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); + } this._chartOptions = { xAxis: { type: "time", @@ -252,14 +270,8 @@ export class StatisticsChart extends LitElement { position: computeRTL(this.hass) ? "right" : "left", // @ts-ignore scale: true, - min: - this.fitYData && typeof this.minYAxis === "number" - ? ({ min }) => Math.min(min, this.minYAxis!) - : this.minYAxis, - max: - this.fitYData && typeof this.maxYAxis === "number" - ? ({ max }) => Math.max(max, this.maxYAxis!) - : this.maxYAxis, + min: this._clampYAxis(minYAxis), + max: this._clampYAxis(maxYAxis), splitLine: { show: true, lineStyle: splitLineStyle, @@ -556,6 +568,19 @@ export class StatisticsChart extends LitElement { return val; } + private _clampYAxis(value?: number | ((values: any) => number)) { + 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); + } + if (typeof value === "function") { + return (values: any) => Math.max(value(values), 0.1); + } + } + return value; + } + static styles = css` :host { display: block; From d1d746e7e6c1d68330b5b2bee671d6cdf68d7315 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 31 Jan 2025 17:38:50 +0100 Subject: [PATCH 26/64] Remove name from the chart series when using showNames = false (#23995) * Remove name from the chart series when using showNames = false * Remove translations --- .../chart/state-history-chart-line.ts | 122 ++++++++++++------ .../chart/state-history-chart-timeline.ts | 9 +- 2 files changed, 92 insertions(+), 39 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index a580f76a5f..92d7b5ace5 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -152,7 +152,10 @@ export class StateHistoryChartLine extends LitElement { value += source; } - return `${param.marker} ${param.seriesName}: ${value}`; + if (param.seriesName) { + return `${param.marker} ${param.seriesName}: ${value}`; + } + return `${param.marker} ${value}`; }) .join("
") ); @@ -417,13 +420,21 @@ export class StateHistoryChartLine extends LitElement { entityState.attributes.target_temp_low ); addDataSet( - `${this.hass.localize("ui.card.climate.current_temperature", { - name: name, - })}` + this.showNames + ? this.hass.localize("ui.card.climate.current_temperature", { + name: name, + }) + : this.hass.localize( + "component.climate.entity_component._.state_attributes.current_temperature.name" + ) ); if (hasHeat) { addDataSet( - `${this.hass.localize("ui.card.climate.heating", { name: name })}`, + this.showNames + ? this.hass.localize("ui.card.climate.heating", { name: name }) + : this.hass.localize( + "component.climate.entity_component._.state_attributes.hvac_action.state.heating" + ), computedStyles.getPropertyValue("--state-climate-heat-color"), true ); @@ -432,7 +443,11 @@ export class StateHistoryChartLine extends LitElement { } if (hasCool) { addDataSet( - `${this.hass.localize("ui.card.climate.cooling", { name: name })}`, + this.showNames + ? this.hass.localize("ui.card.climate.cooling", { name: name }) + : this.hass.localize( + "component.climate.entity_component._.state_attributes.hvac_action.state.cooling" + ), computedStyles.getPropertyValue("--state-climate-cool-color"), true ); @@ -442,22 +457,37 @@ export class StateHistoryChartLine extends LitElement { if (hasTargetRange) { addDataSet( - `${this.hass.localize("ui.card.climate.target_temperature_mode", { - name: name, - mode: this.hass.localize("ui.card.climate.high"), - })}` + this.showNames + ? this.hass.localize("ui.card.climate.target_temperature_mode", { + name: name, + mode: this.hass.localize("ui.card.climate.high"), + }) + : this.hass.localize( + "component.climate.entity_component._.state_attributes.target_temp_high.name" + ) ); addDataSet( - `${this.hass.localize("ui.card.climate.target_temperature_mode", { - name: name, - mode: this.hass.localize("ui.card.climate.low"), - })}` + this.showNames + ? this.hass.localize("ui.card.climate.target_temperature_mode", { + name: name, + mode: this.hass.localize("ui.card.climate.low"), + }) + : this.hass.localize( + "component.climate.entity_component._.state_attributes.target_temp_low.name" + ) ); } else { addDataSet( - `${this.hass.localize("ui.card.climate.target_temperature_entity", { - name: name, - })}` + this.showNames + ? this.hass.localize( + "ui.card.climate.target_temperature_entity", + { + name: name, + } + ) + : this.hass.localize( + "component.climate.entity_component._.state_attributes.temperature.name" + ) ); } @@ -510,19 +540,27 @@ export class StateHistoryChartLine extends LitElement { ); addDataSet( - `${this.hass.localize("ui.card.humidifier.target_humidity_entity", { - name: name, - })}` + this.showNames + ? this.hass.localize("ui.card.humidifier.target_humidity_entity", { + name: name, + }) + : this.hass.localize( + "component.humidifier.entity_component._.state_attributes.humidity.name" + ) ); if (hasCurrent) { addDataSet( - `${this.hass.localize( - "ui.card.humidifier.current_humidity_entity", - { - name: name, - } - )}` + this.showNames + ? this.hass.localize( + "ui.card.humidifier.current_humidity_entity", + { + name: name, + } + ) + : this.hass.localize( + "component.humidifier.entity_component._.state_attributes.current_humidity.name" + ) ); } @@ -530,25 +568,37 @@ export class StateHistoryChartLine extends LitElement { // If action attribute is not available, we shade the area when the device is on if (hasHumidifying) { addDataSet( - `${this.hass.localize("ui.card.humidifier.humidifying", { - name: name, - })}`, + this.showNames + ? this.hass.localize("ui.card.humidifier.humidifying", { + name: name, + }) + : this.hass.localize( + "component.humidifier.entity_component._.state_attributes.action.state.humidifying" + ), computedStyles.getPropertyValue("--state-humidifier-on-color"), true ); } else if (hasDrying) { addDataSet( - `${this.hass.localize("ui.card.humidifier.drying", { - name: name, - })}`, + this.showNames + ? this.hass.localize("ui.card.humidifier.drying", { + name: name, + }) + : this.hass.localize( + "component.humidifier.entity_component._.state_attributes.action.state.drying" + ), computedStyles.getPropertyValue("--state-humidifier-on-color"), true ); } else { addDataSet( - `${this.hass.localize("ui.card.humidifier.on_entity", { - name: name, - })}`, + this.showNames + ? this.hass.localize("ui.card.humidifier.on_entity", { + name: name, + }) + : this.hass.localize( + "component.humidifier.entity_component._.state.on" + ), undefined, true ); @@ -581,7 +631,7 @@ export class StateHistoryChartLine extends LitElement { pushData(new Date(entityState.last_changed), series); }); } else { - addDataSet(name); + addDataSet(this.showNames ? name : ""); let lastValue: number; let lastDate: Date; diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index 5618a158cb..cfd3b6534a 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -132,7 +132,9 @@ export class StateHistoryChartTimeline extends LitElement { const { value, name, marker } = Array.isArray(params) ? params[0] : params; - const title = `

${value![0]}

`; + const title = value![0] + ? `

${value![0]}

` + : ""; const durationInMs = value![2] - value![1]; const formattedDuration = `${this.hass.localize( "ui.components.history_charts.duration" @@ -281,8 +283,9 @@ export class StateHistoryChartTimeline extends LitElement { let prevState: string | null = null; let locState: string | null = null; let prevLastChanged = startTime; - const entityDisplay: string = - names[stateInfo.entity_id] || stateInfo.name; + const entityDisplay: string = this.showNames + ? names[stateInfo.entity_id] || stateInfo.name + : ""; const dataRow: unknown[] = []; stateInfo.data.forEach((entityState) => { From 6b691063a8c8bdfef8f13af059b12d8c32af23a6 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 31 Jan 2025 19:13:13 +0200 Subject: [PATCH 27/64] Hide "heating" data from climate charts (#23997) --- src/components/chart/state-history-chart-line.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 92d7b5ace5..c018dc4389 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -98,6 +98,7 @@ export class StateHistoryChartLine extends LitElement { ) + "
"; const datapoints: Record[] = []; this._chartData.forEach((dataset, index) => { + if (dataset.tooltip?.show === false) return; const param = params.find( (p: Record) => p.seriesIndex === index ); From 5abfb90b1653c4e3a02578ea522a747f7f751a5c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 31 Jan 2025 18:12:49 +0100 Subject: [PATCH 28/64] fix time input width (#23998) --- src/components/ha-base-time-input.ts | 5 +---- .../components/config/ha-backup-config-schedule.ts | 14 ++++++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/components/ha-base-time-input.ts b/src/components/ha-base-time-input.ts index 2936e8c96f..7b2a5b3525 100644 --- a/src/components/ha-base-time-input.ts +++ b/src/components/ha-base-time-input.ts @@ -329,15 +329,12 @@ export class HaBaseTimeInput extends LitElement { :host([clearable]) { position: relative; } - :host { - display: block; - } .time-input-wrap-wrap { display: flex; } .time-input-wrap { display: flex; - flex: 1; + flex: var(--time-input-flex, unset); border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0; overflow: hidden; position: relative; 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 428ffdca6d..d57389a001 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 @@ -537,20 +537,22 @@ class HaBackupConfigSchedule extends LitElement { ha-md-list-item { --md-item-overflow: visible; } - ha-md-select, - ha-time-input { + ha-md-select { min-width: 210px; } + ha-time-input { + min-width: 194px; + --time-input-flex: 1; + } @media all and (max-width: 450px) { - ha-md-select, - ha-time-input { + ha-md-select { min-width: 160px; width: 160px; --md-filled-field-content-space: 0; } ha-time-input { - min-width: 120px; - width: 120px; + min-width: 145px; + width: 145px; } } ha-md-textfield#value { From 1de740e7b59350fe4fb421ea090703df6b279c48 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 31 Jan 2025 18:20:12 +0100 Subject: [PATCH 29/64] Bumped version to 20250131.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 50db59c959..f2cb870bd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250130.0" +version = "20250131.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" From 3f8ff940028bcd7ca6a0345462f3a6b2c534c2f9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 1 Feb 2025 17:32:07 +0100 Subject: [PATCH 30/64] Make date period picker respect timezone settings (#23996) --- src/components/ha-date-range-picker.ts | 46 +++++++++++++++++-- src/panels/history/ha-panel-history.ts | 6 +-- src/panels/logbook/ha-panel-logbook.ts | 6 +-- .../components/hui-energy-period-selector.ts | 6 +-- 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/components/ha-date-range-picker.ts b/src/components/ha-date-range-picker.ts index 71273a66ef..7dfd0c7383 100644 --- a/src/components/ha-date-range-picker.ts +++ b/src/components/ha-date-range-picker.ts @@ -9,12 +9,13 @@ import { endOfMonth, endOfWeek, endOfYear, + isThisYear, startOfDay, startOfMonth, startOfWeek, startOfYear, - isThisYear, } from "date-fns"; +import { fromZonedTime, toZonedTime } from "date-fns-tz"; import type { PropertyValues, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -22,16 +23,18 @@ import { ifDefined } from "lit/directives/if-defined"; import { calcDate, shiftDateRange } from "../common/datetime/calc_date"; import { firstWeekdayIndex } from "../common/datetime/first_weekday"; import { - formatShortDateTimeWithYear, formatShortDateTime, + formatShortDateTimeWithYear, } from "../common/datetime/format_date_time"; import { useAmPm } from "../common/datetime/use_am_pm"; +import { fireEvent } from "../common/dom/fire_event"; +import { TimeZone } from "../data/translation"; import type { HomeAssistant } from "../types"; import "./date-range-picker"; import "./ha-icon-button"; -import "./ha-textarea"; import "./ha-icon-button-next"; import "./ha-icon-button-prev"; +import "./ha-textarea"; export type DateRangePickerRanges = Record; @@ -197,14 +200,15 @@ export class HaDateRangePicker extends LitElement { ?auto-apply=${this.autoApply} time-picker=${this.timePicker} twentyfour-hours=${this._hour24format} - start-date=${this.startDate.toISOString()} - end-date=${this.endDate.toISOString()} + start-date=${this._formatDate(this.startDate)} + end-date=${this._formatDate(this.endDate)} ?ranges=${this.ranges !== false} opening-direction=${ifDefined( this.openingDirection || this._calcedOpeningDirection )} first-day=${firstWeekdayIndex(this.hass.locale)} language=${this.hass.locale.language} + @change=${this._handleChange} >
${!this.minimal @@ -325,9 +329,31 @@ export class HaDateRangePicker extends LitElement { } private _applyDateRange() { + if (this.hass.locale.time_zone === TimeZone.server) { + const dateRangePicker = this._dateRangePicker; + + const startDate = fromZonedTime( + dateRangePicker.start, + this.hass.config.time_zone + ); + const endDate = fromZonedTime( + dateRangePicker.end, + this.hass.config.time_zone + ); + + dateRangePicker.clickRange([startDate, endDate]); + } + this._dateRangePicker.clickedApply(); } + private _formatDate(date: Date): string { + if (this.hass.locale.time_zone === TimeZone.server) { + return toZonedTime(date, this.hass.config.time_zone).toISOString(); + } + return date.toISOString(); + } + private get _dateRangePicker() { const dateRangePicker = this.shadowRoot!.querySelector( "date-range-picker" @@ -358,6 +384,16 @@ export class HaDateRangePicker extends LitElement { } } + private _handleChange(ev: CustomEvent) { + ev.stopPropagation(); + const startDate = ev.detail.startDate; + const endDate = ev.detail.endDate; + + fireEvent(this, "value-changed", { + value: { startDate, endDate }, + }); + } + static styles = css` ha-icon-button { diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts index 977134745d..c535c4ac8b 100644 --- a/src/panels/history/ha-panel-history.ts +++ b/src/panels/history/ha-panel-history.ts @@ -173,7 +173,7 @@ class HaPanelHistory extends LitElement { .endDate=${this._endDate} extended-presets time-picker - @change=${this._dateRangeChanged} + @value-changed=${this._dateRangeChanged} > @@ -233,8 +233,8 @@ export class HaPanelLogbook extends LitElement { } private _dateRangeChanged(ev) { - const startDate = ev.detail.startDate; - const endDate = ev.detail.endDate; + const startDate = ev.detail.value.startDate; + const endDate = ev.detail.value.endDate; if (endDate.getHours() === 0 && endDate.getMinutes() === 0) { endDate.setDate(endDate.getDate() + 1); endDate.setMilliseconds(endDate.getMilliseconds() - 1); diff --git a/src/panels/lovelace/components/hui-energy-period-selector.ts b/src/panels/lovelace/components/hui-energy-period-selector.ts index a32164ef5e..86add3acdd 100644 --- a/src/panels/lovelace/components/hui-energy-period-selector.ts +++ b/src/panels/lovelace/components/hui-energy-period-selector.ts @@ -246,7 +246,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { .startDate=${this._startDate} .endDate=${this._endDate || new Date()} .ranges=${this._ranges} - @change=${this._dateRangeChanged} + @value-changed=${this._dateRangeChanged} minimal >
@@ -346,7 +346,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { private _dateRangeChanged(ev) { const weekStartsOn = firstWeekdayIndex(this.hass.locale); this._startDate = calcDate( - ev.detail.startDate, + ev.detail.value.startDate, startOfDay, this.hass.locale, this.hass.config, @@ -355,7 +355,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { } ); this._endDate = calcDate( - ev.detail.endDate, + ev.detail.value.endDate, endOfDay, this.hass.locale, this.hass.config, From c786d2654231fae279f09f7161da4fefae36a4dc Mon Sep 17 00:00:00 2001 From: ildar170975 <71872483+ildar170975@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:07:11 +0300 Subject: [PATCH 31/64] Fix for "Increase generic entity row touch target (3): climate entities (#24002) return to max-height + set vertical alignment --- src/panels/lovelace/components/hui-generic-entity-row.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts index b166eeab77..1b7b2ac587 100644 --- a/src/panels/lovelace/components/hui-generic-entity-row.ts +++ b/src/panels/lovelace/components/hui-generic-entity-row.ts @@ -200,9 +200,8 @@ export class HuiGenericEntityRow extends LitElement { padding-inline-start: 16px; padding-inline-end: 8px; flex: 1 1 30%; - } - .info:not(:has(.secondary)) { - line-height: 40px; + min-height: 40px; + align-content: center; } .info, .info > * { @@ -236,7 +235,8 @@ export class HuiGenericEntityRow extends LitElement { } .value { direction: ltr; - line-height: 40px; + min-height: 40px; + align-content: center; } `; } From f8742ae690b93662abe6ba50060d205537f08aab Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 1 Feb 2025 17:16:43 +0100 Subject: [PATCH 32/64] Display year conditionally when script was last triggered on script list (#24012) Display year conditionally when script was last triggered in script list --- src/panels/config/script/ha-script-picker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 2cd3293317..7d72bcefc1 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -25,7 +25,7 @@ import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { computeCssColor } from "../../../common/color/compute-color"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import { formatShortDateTime } from "../../../common/datetime/format_date_time"; +import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time"; import { relativeTime } from "../../../common/datetime/relative_time"; import { storage } from "../../../common/decorators/storage"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; @@ -304,7 +304,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { return html` ${script.last_triggered ? dayDifference > 3 - ? formatShortDateTime( + ? formatShortDateTimeWithConditionalYear( date, this.hass.locale, this.hass.config From ff1159402eefdfdf02b1e86164c10196ee06b93c Mon Sep 17 00:00:00 2001 From: Philipp <84805847+insomniac2305@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:36:19 +0100 Subject: [PATCH 33/64] Fix browser media player showing more info dialog (#24021) --- src/panels/media-browser/ha-bar-media-player.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/media-browser/ha-bar-media-player.ts b/src/panels/media-browser/ha-bar-media-player.ts index db3b3ccf23..be5f24ce26 100644 --- a/src/panels/media-browser/ha-bar-media-player.ts +++ b/src/panels/media-browser/ha-bar-media-player.ts @@ -462,7 +462,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { } private _openMoreInfo() { - if (this._browserPlayer) { + if (this.entityId === BROWSER_PLAYER) { return; } fireEvent(this, "hass-more-info", { entityId: this.entityId }); From 0d50d2664fc7fc832e475ef8f0323c4d6c2a958a Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 3 Feb 2025 12:09:59 +0200 Subject: [PATCH 34/64] Fix legend in charts (#24025) * Fix legend in line charts * fix statistics graph legend --- .../chart/state-history-chart-line.ts | 23 +++++++++++++++++-- src/components/chart/statistics-chart.ts | 2 ++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index c018dc4389..ae721aad83 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -72,6 +72,8 @@ export class StateHistoryChartLine extends LitElement { @state() private _chartOptions?: ECOption; + private _hiddenStats = new Set(); + @state() private _yWidth = 25; private _chartTime: Date = new Date(); @@ -84,6 +86,9 @@ export class StateHistoryChartLine extends LitElement { .options=${this._chartOptions} .height=${this.height} style=${styleMap({ height: this.height })} + external-hidden + @dataset-hidden=${this._datasetHidden} + @dataset-unhidden=${this._datasetUnhidden} > `; } @@ -98,7 +103,11 @@ export class StateHistoryChartLine extends LitElement { ) + "
"; const datapoints: Record[] = []; this._chartData.forEach((dataset, index) => { - if (dataset.tooltip?.show === false) return; + if ( + dataset.tooltip?.show === false || + this._hiddenStats.has(dataset.name as string) + ) + return; const param = params.find( (p: Record) => p.seriesIndex === index ); @@ -162,6 +171,14 @@ export class StateHistoryChartLine extends LitElement { ); } + private _datasetHidden(ev: CustomEvent) { + this._hiddenStats.add(ev.detail.name); + } + + private _datasetUnhidden(ev: CustomEvent) { + this._hiddenStats.delete(ev.detail.name); + } + public willUpdate(changedProps: PropertyValues) { if ( changedProps.has("data") || @@ -264,6 +281,8 @@ export class StateHistoryChartLine extends LitElement { } as YAXisOption, legend: { show: this.showNames, + type: "scroll", + animationDurationUpdate: 400, icon: "circle", padding: [20, 0], }, @@ -632,7 +651,7 @@ export class StateHistoryChartLine extends LitElement { pushData(new Date(entityState.last_changed), series); }); } else { - addDataSet(this.showNames ? name : ""); + addDataSet(name); let lastValue: number; let lastDate: Date; diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 9c7ea82e0d..f5971528fd 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -279,6 +279,8 @@ export class StatisticsChart extends LitElement { }, legend: { show: !this.hideLegend, + type: "scroll", + animationDurationUpdate: 400, icon: "circle", padding: [20, 0], data: this._legendData, From af35b154003aa99a1be7bbe306e99d21c7708a2c Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 3 Feb 2025 12:36:59 +0200 Subject: [PATCH 35/64] Fix click action for timeline chart labels (#24039) * Fix click action for timeline chart labels * Use id in line charts --- .../chart/state-history-chart-line.ts | 22 ++++++++++-- .../chart/state-history-chart-timeline.ts | 35 +++++++++++-------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index ae721aad83..de7f127ddf 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -372,13 +372,18 @@ export class StateHistoryChartLine extends LitElement { prevValues = datavalues; }; - const addDataSet = (nameY: string, color?: string, fill = false) => { + const addDataSet = ( + id: string, + nameY: string, + color?: string, + fill = false + ) => { if (!color) { color = getGraphColorByIndex(colorIndex, computedStyles); colorIndex++; } data.push({ - id: nameY, + id, data: [], type: "line", cursor: "default", @@ -440,6 +445,7 @@ export class StateHistoryChartLine extends LitElement { entityState.attributes.target_temp_low ); addDataSet( + states.entity_id + "-current_temperature", this.showNames ? this.hass.localize("ui.card.climate.current_temperature", { name: name, @@ -450,6 +456,7 @@ export class StateHistoryChartLine extends LitElement { ); if (hasHeat) { addDataSet( + states.entity_id + "-heating", this.showNames ? this.hass.localize("ui.card.climate.heating", { name: name }) : this.hass.localize( @@ -463,6 +470,7 @@ export class StateHistoryChartLine extends LitElement { } if (hasCool) { addDataSet( + states.entity_id + "-cooling", this.showNames ? this.hass.localize("ui.card.climate.cooling", { name: name }) : this.hass.localize( @@ -477,6 +485,7 @@ export class StateHistoryChartLine extends LitElement { if (hasTargetRange) { addDataSet( + states.entity_id + "-target_temperature_mode", this.showNames ? this.hass.localize("ui.card.climate.target_temperature_mode", { name: name, @@ -487,6 +496,7 @@ export class StateHistoryChartLine extends LitElement { ) ); addDataSet( + states.entity_id + "-target_temperature_mode_low", this.showNames ? this.hass.localize("ui.card.climate.target_temperature_mode", { name: name, @@ -498,6 +508,7 @@ export class StateHistoryChartLine extends LitElement { ); } else { addDataSet( + states.entity_id + "-target_temperature", this.showNames ? this.hass.localize( "ui.card.climate.target_temperature_entity", @@ -560,6 +571,7 @@ export class StateHistoryChartLine extends LitElement { ); addDataSet( + states.entity_id + "-target_humidity", this.showNames ? this.hass.localize("ui.card.humidifier.target_humidity_entity", { name: name, @@ -571,6 +583,7 @@ export class StateHistoryChartLine extends LitElement { if (hasCurrent) { addDataSet( + states.entity_id + "-current_humidity", this.showNames ? this.hass.localize( "ui.card.humidifier.current_humidity_entity", @@ -588,6 +601,7 @@ export class StateHistoryChartLine extends LitElement { // If action attribute is not available, we shade the area when the device is on if (hasHumidifying) { addDataSet( + states.entity_id + "-humidifying", this.showNames ? this.hass.localize("ui.card.humidifier.humidifying", { name: name, @@ -600,6 +614,7 @@ export class StateHistoryChartLine extends LitElement { ); } else if (hasDrying) { addDataSet( + states.entity_id + "-drying", this.showNames ? this.hass.localize("ui.card.humidifier.drying", { name: name, @@ -612,6 +627,7 @@ export class StateHistoryChartLine extends LitElement { ); } else { addDataSet( + states.entity_id + "-on", this.showNames ? this.hass.localize("ui.card.humidifier.on_entity", { name: name, @@ -651,7 +667,7 @@ export class StateHistoryChartLine extends LitElement { pushData(new Date(entityState.last_changed), series); }); } else { - addDataSet(name); + addDataSet(states.entity_id, name); let lastValue: number; let lastDate: Date; diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index cfd3b6534a..4069473b62 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -67,7 +67,7 @@ export class StateHistoryChartTimeline extends LitElement { .hass=${this.hass} .options=${this._chartOptions} .height=${`${this.data.length * 30 + 30}px`} - .data=${this._chartData} + .data=${this._chartData as ECOption["series"]} @chart-click=${this._handleChartClick} > `; @@ -129,11 +129,11 @@ export class StateHistoryChartTimeline extends LitElement { private _renderTooltip: TooltipFormatterCallback = (params: TooltipPositionCallbackParams) => { - const { value, name, marker } = Array.isArray(params) + const { value, name, marker, seriesName } = Array.isArray(params) ? params[0] : params; - const title = value![0] - ? `

${value![0]}

` + const title = seriesName + ? `

${seriesName}

` : ""; const durationInMs = value![2] - value![1]; const formattedDuration = `${this.hass.localize( @@ -234,11 +234,15 @@ export class StateHistoryChartTimeline extends LitElement { width: labelWidth - labelMargin, overflow: "truncate", margin: labelMargin, - formatter: (label: string) => { - const width = Math.min( - measureTextWidth(label, 12) + labelMargin, - maxInternalLabelWidth - ); + formatter: (id: string) => { + const label = this._chartData.find((d) => d.id === id) + ?.name as string; + const width = label + ? Math.min( + measureTextWidth(label, 12) + labelMargin, + maxInternalLabelWidth + ) + : 0; if (width > this._yWidth) { this._yWidth = width; fireEvent(this, "y-width-changed", { @@ -284,7 +288,7 @@ export class StateHistoryChartTimeline extends LitElement { let locState: string | null = null; let prevLastChanged = startTime; const entityDisplay: string = this.showNames - ? names[stateInfo.entity_id] || stateInfo.name + ? names[stateInfo.entity_id] || stateInfo.name || stateInfo.entity_id : ""; const dataRow: unknown[] = []; @@ -313,7 +317,7 @@ export class StateHistoryChartTimeline extends LitElement { ); dataRow.push({ value: [ - entityDisplay, + stateInfo.entity_id, prevLastChanged, newLastChanged, locState, @@ -339,7 +343,7 @@ export class StateHistoryChartTimeline extends LitElement { ); dataRow.push({ value: [ - entityDisplay, + stateInfo.entity_id, prevLastChanged, endTime, locState, @@ -352,9 +356,10 @@ export class StateHistoryChartTimeline extends LitElement { }); } datasets.push({ + id: stateInfo.entity_id, data: dataRow, name: entityDisplay, - dimensions: ["index", "start", "end", "name", "color", "textColor"], + dimensions: ["id", "start", "end", "name", "color", "textColor"], type: "custom", encode: { x: [1, 2], @@ -370,10 +375,10 @@ export class StateHistoryChartTimeline extends LitElement { private _handleChartClick(e: CustomEvent): void { if (e.detail.targetType === "axisLabel") { - const dataset = this.data[e.detail.dataIndex]; + const dataset = this._chartData[e.detail.dataIndex]; if (dataset) { fireEvent(this, "hass-more-info", { - entityId: dataset.entity_id, + entityId: dataset.id as string, }); } } From 6eb43a7d619d6cfe7ea1e5c9f66041c8329cb0ea Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 3 Feb 2025 16:59:43 +0200 Subject: [PATCH 36/64] Workaround for chart size bug in editor preview (#24040) --- src/components/chart/state-history-chart-line.ts | 3 ++- src/components/chart/statistics-chart.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index de7f127ddf..01bd1916b1 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -267,7 +267,8 @@ export class StateHistoryChartLine extends LitElement { margin: 5, formatter: (value: number) => { const label = formatNumber(value, this.hass.locale); - const width = measureTextWidth(label, 12) + 5; + // adding 5px extra because preview is not accurate #24027 + const width = measureTextWidth(label, 12) + 5 + 5; if (width > this._yWidth) { this._yWidth = width; fireEvent(this, "y-width-changed", { diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index f5971528fd..2dd3fd68c5 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -287,7 +287,7 @@ export class StatisticsChart extends LitElement { }, grid: { ...(this.hideLegend ? { top: this.unit ? 30 : 5 } : {}), // undefined is the same as 0 - left: 20, + left: 5, right: 1, bottom: 0, containLabel: true, From 4698a6364223b40abdefc55f8a303c6ad5b84e1e Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 3 Feb 2025 14:27:33 +0200 Subject: [PATCH 37/64] Show seconds on x axis when chart is zoomed a lot (#24043) Show seconds on x axis when charts is zoomed a lot --- src/components/chart/axis-label.ts | 85 ++++++++----------- src/components/chart/ha-chart-base.ts | 64 +++++++++++++- .../chart/state-history-chart-line.ts | 20 ----- .../chart/state-history-chart-timeline.ts | 17 ---- src/components/chart/statistics-chart.ts | 47 +++++----- .../config/hardware/ha-config-hardware.ts | 8 -- .../energy/common/energy-chart-options.ts | 19 +---- 7 files changed, 126 insertions(+), 134 deletions(-) diff --git a/src/components/chart/axis-label.ts b/src/components/chart/axis-label.ts index d779269e33..1f8f9a8154 100644 --- a/src/components/chart/axis-label.ts +++ b/src/components/chart/axis-label.ts @@ -1,5 +1,4 @@ import type { HassConfig } from "home-assistant-js-websocket"; -import type { XAXisOption } from "echarts/types/dist/shared"; import type { FrontendLocaleData } from "../../data/translation"; import { formatDateMonth, @@ -7,56 +6,46 @@ import { formatDateVeryShort, formatDateWeekdayShort, } from "../../common/datetime/format_date"; -import { formatTime } from "../../common/datetime/format_time"; +import { + formatTime, + formatTimeWithSeconds, +} from "../../common/datetime/format_time"; -export function getLabelFormatter( +export function formatTimeLabel( + value: number | Date, locale: FrontendLocaleData, config: HassConfig, - dayDifference = 0 + minutesDifference: number ) { - return (value: number | Date) => { - const date = new Date(value); - if (dayDifference > 88) { - return date.getMonth() === 0 - ? `{bold|${formatDateMonthYear(date, locale, config)}}` - : formatDateMonth(date, locale, config); - } - if (dayDifference > 35) { - return date.getDate() === 1 - ? `{bold|${formatDateVeryShort(date, locale, config)}}` - : formatDateVeryShort(date, locale, config); - } - if (dayDifference > 7) { - const label = formatDateVeryShort(date, locale, config); - return date.getDate() === 1 ? `{bold|${label}}` : label; - } - if (dayDifference > 2) { - return formatDateWeekdayShort(date, locale, config); - } + const dayDifference = minutesDifference / 60 / 24; + const date = new Date(value); + if (dayDifference > 88) { + return date.getMonth() === 0 + ? `{bold|${formatDateMonthYear(date, locale, config)}}` + : formatDateMonth(date, locale, config); + } + if (dayDifference > 35) { + return date.getDate() === 1 + ? `{bold|${formatDateVeryShort(date, locale, config)}}` + : formatDateVeryShort(date, locale, config); + } + if (dayDifference > 7) { + const label = formatDateVeryShort(date, locale, config); + return date.getDate() === 1 ? `{bold|${label}}` : label; + } + if (dayDifference > 2) { + return formatDateWeekdayShort(date, locale, config); + } + if (minutesDifference && minutesDifference < 5) { + return formatTimeWithSeconds(date, locale, config); + } + if ( + date.getHours() === 0 && + date.getMinutes() === 0 && + date.getSeconds() === 0 + ) { // show only date for the beginning of the day - if ( - date.getHours() === 0 && - date.getMinutes() === 0 && - date.getSeconds() === 0 - ) { - return `{bold|${formatDateVeryShort(date, locale, config)}}`; - } - return formatTime(date, locale, config); - }; -} - -export function getTimeAxisLabelConfig( - locale: FrontendLocaleData, - config: HassConfig, - dayDifference?: number -): XAXisOption["axisLabel"] { - return { - formatter: getLabelFormatter(locale, config, dayDifference), - rich: { - bold: { - fontWeight: "bold", - }, - }, - hideOverlap: true, - }; + return `{bold|${formatDateVeryShort(date, locale, config)}}`; + } + return formatTime(date, locale, config); } diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 85a2e4a538..32e7f729eb 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -2,6 +2,7 @@ import type { PropertyValues } from "lit"; import { css, html, nothing, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; +import { classMap } from "lit/directives/class-map"; import { mdiRestart } from "@mdi/js"; import type { EChartsType } from "echarts/core"; import type { DataZoomComponentOption } from "echarts/components"; @@ -12,6 +13,7 @@ import type { YAXisOption, } from "echarts/types/dist/shared"; import { consume } from "@lit-labs/context"; +import { differenceInMinutes } from "date-fns"; import { fireEvent } from "../../common/dom/fire_event"; import type { HomeAssistant } from "../../types"; import { isMac } from "../../util/is_mac"; @@ -21,6 +23,7 @@ import { listenMediaQuery } from "../../common/dom/media_query"; import type { Themes } from "../../data/ws-themes"; import { themesContext } from "../../data/context"; import { getAllGraphColors } from "../../common/color/colors"; +import { formatTimeLabel } from "./axis-label"; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; @@ -45,6 +48,10 @@ export class HaChartBase extends LitElement { @state() private _isZoomed = false; + @state() private _zoomRatio = 1; + + @state() private _minutesDifference = 24 * 60; + private _modifierPressed = false; private _isTouchDevice = "ontouchstart" in window; @@ -152,7 +159,10 @@ export class HaChartBase extends LitElement { protected render() { return html`
+ formatTimeLabel( + value, + this.hass.locale, + this.hass.config, + this._minutesDifference * this._zoomRatio + ); + private async _setupChart() { if (this._loading) return; const container = this.renderRoot.querySelector(".chart") as HTMLDivElement; @@ -199,6 +217,7 @@ export class HaChartBase extends LitElement { this.chart.on("datazoom", (e: any) => { const { start, end } = e.batch?.[0] ?? e; this._isZoomed = start !== 0 || end !== 100; + this._zoomRatio = (end - start) / 100; }); this.chart.on("click", (e: ECElementEvent) => { fireEvent(this, "chart-click", e); @@ -236,6 +255,45 @@ export class HaChartBase extends LitElement { } private _createOptions(): ECOption { + let xAxis = this.options?.xAxis; + if (xAxis && !Array.isArray(xAxis) && xAxis.type === "time") { + if (xAxis.max && xAxis.min) { + this._minutesDifference = differenceInMinutes( + xAxis.max as Date, + xAxis.min as Date + ); + } + const dayDifference = this._minutesDifference / 60 / 24; + let minInterval: number | undefined; + if (dayDifference) { + minInterval = + dayDifference >= 89 // quarter + ? 28 * 3600 * 24 * 1000 + : dayDifference > 2 + ? 3600 * 24 * 1000 + : undefined; + } + xAxis = { + ...xAxis, + axisLabel: { + formatter: this._formatTimeLabel, + rich: { + bold: { + fontWeight: "bold", + }, + }, + hideOverlap: true, + ...xAxis.axisLabel, + }, + axisLine: { + show: false, + }, + splitLine: { + show: true, + }, + minInterval, + } as XAXisOption; + } const options = { animation: !this._reducedMotion, darkMode: this._themes.darkMode ?? false, @@ -244,6 +302,7 @@ export class HaChartBase extends LitElement { }, dataZoom: this._getDataZoomConfig(), ...this.options, + xAxis, }; const isMobile = window.matchMedia( @@ -485,6 +544,9 @@ export class HaChartBase extends LitElement { color: var(--primary-color); border: 1px solid var(--divider-color); } + .has-legend .zoom-reset { + top: 64px; + } `; } diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 01bd1916b1..be9e71e845 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -4,7 +4,6 @@ import { property, state } from "lit/decorators"; import type { VisualMapComponentOption } from "echarts/components"; import type { LineSeriesOption } from "echarts/charts"; import type { YAXisOption } from "echarts/types/dist/shared"; -import { differenceInDays } from "date-fns"; import { styleMap } from "lit/directives/style-map"; import { getGraphColorByIndex } from "../../common/color/colors"; import { computeRTL } from "../../common/util/compute_rtl"; @@ -18,7 +17,6 @@ import { getNumberFormatOptions, formatNumber, } from "../../common/number/format_number"; -import { getTimeAxisLabelConfig } from "./axis-label"; import { measureTextWidth } from "../../util/text"; import { fireEvent } from "../../common/dom/fire_event"; import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate"; @@ -206,7 +204,6 @@ export class StateHistoryChartLine extends LitElement { changedProps.has("paddingYAxis") || changedProps.has("_yWidth") ) { - const dayDifference = differenceInDays(this.endTime, this.startTime); const rtl = computeRTL(this.hass); let minYAxis: number | ((values: { min: number }) => number) | undefined = this.minYAxis; @@ -231,23 +228,6 @@ export class StateHistoryChartLine extends LitElement { type: "time", min: this.startTime, max: this.endTime, - axisLabel: getTimeAxisLabelConfig( - this.hass.locale, - this.hass.config, - dayDifference - ), - axisLine: { - show: false, - }, - splitLine: { - show: true, - }, - minInterval: - dayDifference >= 89 // quarter - ? 28 * 3600 * 24 * 1000 - : dayDifference > 2 - ? 3600 * 24 * 1000 - : undefined, }, yAxis: { type: this.logarithmicScale ? "log" : "value", diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index 4069473b62..39a22c827c 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -8,7 +8,6 @@ import type { TooltipFormatterCallback, TooltipPositionCallbackParams, } from "echarts/types/dist/shared"; -import { differenceInDays } from "date-fns"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration"; import { computeRTL } from "../../common/util/compute_rtl"; @@ -22,7 +21,6 @@ import { luminosity } from "../../common/color/rgb"; import { hex2rgb } from "../../common/color/convert-color"; import { measureTextWidth } from "../../util/text"; import { fireEvent } from "../../common/dom/fire_event"; -import { getTimeAxisLabelConfig } from "./axis-label"; @customElement("state-history-chart-timeline") export class StateHistoryChartTimeline extends LitElement { @@ -191,7 +189,6 @@ export class StateHistoryChartTimeline extends LitElement { : 0; const labelMargin = 5; const rtl = computeRTL(this.hass); - const dayDifference = differenceInDays(this.endTime, this.startTime); this._chartOptions = { xAxis: { type: "time", @@ -203,20 +200,6 @@ export class StateHistoryChartTimeline extends LitElement { splitLine: { show: false, }, - axisLine: { - show: false, - }, - axisLabel: getTimeAxisLabelConfig( - this.hass.locale, - this.hass.config, - dayDifference - ), - minInterval: - dayDifference >= 89 // quarter - ? 28 * 3600 * 24 * 1000 - : dayDifference > 2 - ? 3600 * 24 * 1000 - : undefined, }, yAxis: { type: "category", diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 2dd3fd68c5..62090d23ac 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -30,7 +30,6 @@ import { getNumberFormatOptions, } from "../../common/number/format_number"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; -import { getTimeAxisLabelConfig } from "./axis-label"; export const supportedStatTypeMap: Record = { mean: "mean", @@ -216,7 +215,6 @@ export class StatisticsChart extends LitElement { }; private _createOptions() { - const splitLineStyle = this.hass.themes?.darkMode ? { opacity: 0.15 } : {}; const dayDifference = this.daysToShow ?? 1; let minYAxis: number | ((values: { min: number }) => number) | undefined = this.minYAxis; @@ -236,29 +234,32 @@ export class StatisticsChart extends LitElement { } else if (this.logarithmicScale) { maxYAxis = ({ max }) => (max > 0 ? max * 1.05 : max * 0.95); } + const endTime = this.endTime ?? new Date(); + let startTime = this.startTime; + + if (!startTime) { + // Calculate default start time based on dayDifference + startTime = new Date( + endTime.getTime() - dayDifference * 24 * 3600 * 1000 + ); + + // Check chart data for earlier points + this._chartData.forEach((series) => { + if (!Array.isArray(series.data)) return; + series.data.forEach((point) => { + const timestamp = Array.isArray(point) ? point[0] : point.value?.[0]; + if (new Date(timestamp) < startTime!) { + startTime = new Date(timestamp); + } + }); + }); + } + this._chartOptions = { xAxis: { type: "time", - axisLabel: getTimeAxisLabelConfig( - this.hass.locale, - this.hass.config, - dayDifference - ), - min: this.startTime, - max: this.endTime, - axisLine: { - show: false, - }, - splitLine: { - show: true, - lineStyle: splitLineStyle, - }, - minInterval: - dayDifference >= 89 // quarter - ? 28 * 3600 * 24 * 1000 - : dayDifference > 2 - ? 3600 * 24 * 1000 - : undefined, + min: startTime, + max: endTime, }, yAxis: { type: this.logarithmicScale ? "log" : "value", @@ -274,7 +275,6 @@ export class StatisticsChart extends LitElement { max: this._clampYAxis(maxYAxis), splitLine: { show: true, - lineStyle: splitLineStyle, }, }, legend: { @@ -348,6 +348,7 @@ 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/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts index 004bcb0cd1..12c14def9a 100644 --- a/src/panels/config/hardware/ha-config-hardware.ts +++ b/src/panels/config/hardware/ha-config-hardware.ts @@ -39,7 +39,6 @@ import { hardwareBrandsUrl } from "../../../util/brands-url"; import { showhardwareAvailableDialog } from "./show-dialog-hardware-available"; import { extractApiErrorMessage } from "../../../data/hassio/common"; import type { ECOption } from "../../../resources/echarts"; -import { getTimeAxisLabelConfig } from "../../../components/chart/axis-label"; const DATASAMPLES = 60; @@ -153,13 +152,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { this._chartOptions = { xAxis: { type: "time", - axisLabel: getTimeAxisLabelConfig(this.hass.locale, this.hass.config), - splitLine: { - show: true, - }, - axisLine: { - show: false, - }, }, yAxis: { type: "value", 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 3619c731d3..76dde725aa 100644 --- a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts +++ b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts @@ -10,7 +10,6 @@ import { formatNumber } from "../../../../../common/number/format_number"; import { formatDateVeryShort } from "../../../../../common/datetime/format_date"; import { formatTime } from "../../../../../common/datetime/format_time"; import type { ECOption } from "../../../../../resources/echarts"; -import { getTimeAxisLabelConfig } from "../../../../../components/chart/axis-label"; export function getSuggestedMax(dayDifference: number, end: Date): number { let suggestedMax = new Date(end); @@ -52,23 +51,9 @@ export function getCommonOptions( const options: ECOption = { xAxis: { - id: "xAxisMain", type: "time", - min: start.getTime(), - max: getSuggestedMax(dayDifference, end), - axisLabel: getTimeAxisLabelConfig(locale, config, dayDifference), - axisLine: { - show: false, - }, - splitLine: { - show: true, - }, - minInterval: - dayDifference >= 89 // quarter - ? 28 * 3600 * 24 * 1000 - : dayDifference > 2 - ? 3600 * 24 * 1000 - : undefined, + min: start, + max: end, }, yAxis: { type: "value", From 72df585c5ef1b7ffcca9fe4f83e24b96eb92ff7d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 3 Feb 2025 14:40:49 +0100 Subject: [PATCH 38/64] Fix download unencrypted backup logic (#24045) --- src/panels/config/backup/helper/download_backup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/config/backup/helper/download_backup.ts b/src/panels/config/backup/helper/download_backup.ts index 53a0d96823..3c08cc1df7 100644 --- a/src/panels/config/backup/helper/download_backup.ts +++ b/src/panels/config/backup/helper/download_backup.ts @@ -37,6 +37,7 @@ export const downloadBackup = async ( if (!isProtected) { downloadBackupFile(hass, backup.backup_id, preferedAgent); + return; } const encryptionKey = backupConfig?.create_backup?.password; From 0f4b6b423a36c77474c7eb4d6c71d2b9ddb42d04 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 3 Feb 2025 15:12:28 +0100 Subject: [PATCH 39/64] Improve chart height and narrow option in grid section (#24046) * Fix chart size in grid * Set minimal height to 2 for history chart * Update history chart --- src/panels/lovelace/cards/hui-history-graph-card.ts | 9 ++++----- src/panels/lovelace/cards/hui-statistics-graph-card.ts | 4 +++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index 53758b07a5..7f4a1c47ca 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -65,7 +65,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { return { columns: 12, min_columns: 6, - min_rows: this._config?.entities?.length || 1, + min_rows: 2, }; } @@ -244,7 +244,8 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { })}`; const columns = this._config.grid_options?.columns ?? 12; - const narrow = Number.isNaN(columns) || Number(columns) <= 12; + const narrow = typeof columns === "number" && columns <= 12; + const hasFixedHeight = typeof this._config.grid_options?.rows === "number"; return html` @@ -283,9 +284,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { .minYAxis=${this._config.min_y_axis} .maxYAxis=${this._config.max_y_axis} .fitYData=${this._config.fit_y_data || false} - .height=${this._config.grid_options?.rows - ? "100%" - : undefined} + .height=${hasFixedHeight ? "100%" : undefined} .narrow=${narrow} > `} diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index f94e970073..6cc5b73a70 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -258,6 +258,8 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { return nothing; } + const hasFixedHeight = typeof this._config.grid_options?.rows === "number"; + return html`
From b263b749162c3a8fa00751b6f5e456d8b40ce767 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 3 Feb 2025 16:00:08 +0100 Subject: [PATCH 40/64] Increase margin to avoid fab overlap on backup overview page (#24047) --- src/panels/config/backup/ha-config-backup-overview.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/panels/config/backup/ha-config-backup-overview.ts b/src/panels/config/backup/ha-config-backup-overview.ts index e71212917c..8d23b61b66 100644 --- a/src/panels/config/backup/ha-config-backup-overview.ts +++ b/src/panels/config/backup/ha-config-backup-overview.ts @@ -221,8 +221,7 @@ class HaConfigBackupOverview extends LitElement { gap: 24px; display: flex; flex-direction: column; - margin-bottom: 24px; - margin-bottom: 72px; + margin-bottom: calc(env(safe-area-inset-bottom) + 72px); } .card-actions { display: flex; From fcf655b0ec173e7f405e724f7eeb762fcd1a2e65 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 3 Feb 2025 19:13:35 +0200 Subject: [PATCH 41/64] FIx console errors in charts (#24048) * FIx console errors in charts * handle undefined unit_of_measurement --- src/components/chart/ha-chart-base.ts | 11 ++--------- src/components/chart/state-history-chart-line.ts | 3 ++- src/components/chart/statistics-chart.ts | 3 ++- src/state/logging-mixin.ts | 5 +---- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 32e7f729eb..e83bb757a8 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -143,15 +143,7 @@ export class HaChartBase extends LitElement { this.chart.setOption(this._createOptions(), { lazyUpdate: true, // if we replace the whole object, it will reset the dataZoom - replaceMerge: [ - "xAxis", - "yAxis", - "dataZoom", - "dataset", - "tooltip", - "grid", - "visualMap", - ], + replaceMerge: ["grid"], }); } } @@ -503,6 +495,7 @@ export class HaChartBase extends LitElement { private _handleZoomReset() { this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 }); + this._modifierPressed = false; } private _handleWheel(e: WheelEvent) { diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index be9e71e845..2afbc7f3bd 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -132,6 +132,7 @@ export class StateHistoryChartLine extends LitElement { marker: ``, }); }); + const unit = this.unit ? ` ${this.unit}` : ""; return ( title + datapoints @@ -143,7 +144,7 @@ export class StateHistoryChartLine extends LitElement { undefined, this.hass.entities[this._entityIds[param.seriesIndex]] ) - )} ${this.unit}`; + )}${unit}`; const dataIndex = this._datasetToDataIndex[param.seriesIndex]; const data = this.data[dataIndex]; if (data.statistics && data.statistics.length > 0) { diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 62090d23ac..05eb8fe654 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -186,6 +186,7 @@ export class StatisticsChart extends LitElement { private _renderTooltip = (params: any) => { const rendered: Record = {}; + const unit = this.unit ? ` ${this.unit}` : ""; return params .map((param, index: number) => { if (rendered[param.seriesName]) return ""; @@ -198,7 +199,7 @@ export class StatisticsChart extends LitElement { undefined, this.hass.entities[this._statisticIds[param.seriesIndex]] ) - )} ${this.unit}`; + )}${unit}`; const time = index === 0 diff --git a/src/state/logging-mixin.ts b/src/state/logging-mixin.ts index 4a14a441af..8ba4b630b3 100644 --- a/src/state/logging-mixin.ts +++ b/src/state/logging-mixin.ts @@ -33,10 +33,7 @@ export const loggingMixin = >( ev.message.includes("ResizeObserver loop limit exceeded")) || ev.message.includes( "ResizeObserver loop completed with undelivered notifications" - ) || - (ev.error.stack.includes("DataZoomModel") && - (ev.message.includes("undefined") || - ev.message.includes("Cannot read properties of null"))) + ) ) { ev.preventDefault(); ev.stopImmediatePropagation(); From ef3bea71a01a0925e788366f77e507208bd7b5f7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 Feb 2025 18:20:20 +0100 Subject: [PATCH 42/64] Bumped version to 20250203.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f2cb870bd0..0a60ae76fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250131.0" +version = "20250203.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" From a26701808fd2a95ce7a06a6955cd2f8cb9c3c790 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 4 Feb 2025 16:04:11 +0100 Subject: [PATCH 43/64] Add support for add-on update type for backups in the UI (#24044) * Add support for add-on update type for backups in the UI * Add type to backup detail page * Use new model * Fix detail page * Fix type --- src/data/backup.ts | 29 ++++- .../overview/ha-backup-overview-backups.ts | 106 +++++++++--------- .../config/backup/ha-config-backup-backups.ts | 48 ++++---- .../config/backup/ha-config-backup-details.ts | 16 +++ src/translations/en.json | 4 +- 5 files changed, 126 insertions(+), 77 deletions(-) diff --git a/src/data/backup.ts b/src/data/backup.ts index 6bf43ade24..756ca4b47a 100644 --- a/src/data/backup.ts +++ b/src/data/backup.ts @@ -1,6 +1,8 @@ +import { memoize } from "@fullcalendar/core/internal"; import { setHours, setMinutes } from "date-fns"; import type { HassConfig } from "home-assistant-js-websocket"; import memoizeOne from "memoize-one"; +import checkValidDate from "../common/datetime/check_valid_date"; import { formatDateTime, formatDateTimeNumeric, @@ -11,7 +13,6 @@ import type { HomeAssistant } from "../types"; import { fileDownload } from "../util/file_download"; import { domainToName } from "./integration"; import type { FrontendLocaleData } from "./translation"; -import checkValidDate from "../common/datetime/check_valid_date"; export const enum BackupScheduleRecurrence { NEVER = "never", @@ -104,6 +105,9 @@ export interface BackupContent { name: string; agents: Record; failed_agent_ids?: string[]; + extra_metadata?: { + "supervisor.addon_update"?: string; + }; with_automatic_settings: boolean; } @@ -319,6 +323,29 @@ export const computeBackupAgentName = ( export const computeBackupSize = (backup: BackupContent) => Math.max(...Object.values(backup.agents).map((agent) => agent.size)); +export type BackupType = "automatic" | "manual" | "addon_update"; + +const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "manual", "addon_update"]; + +export const getBackupTypes = memoize((isHassio: boolean) => + isHassio + ? BACKUP_TYPE_ORDER + : BACKUP_TYPE_ORDER.filter((type) => type !== "addon_update") +); + +export const computeBackupType = ( + backup: BackupContent, + isHassio: boolean +): BackupType => { + if (backup.with_automatic_settings) { + return "automatic"; + } + if (isHassio && backup.extra_metadata?.["supervisor.addon_update"] != null) { + return "addon_update"; + } + return "manual"; +}; + export const compareAgents = (a: string, b: string) => { const isLocalA = isLocalAgent(a); const isLocalB = isLocalAgent(b); diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts b/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts index 882902b602..c140713aba 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts @@ -1,16 +1,19 @@ -import { mdiCalendarSync, mdiGestureTap } from "@mdi/js"; +import { mdiCalendarSync, mdiGestureTap, mdiPuzzle } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; import "../../../../../components/ha-button"; import "../../../../../components/ha-card"; import "../../../../../components/ha-icon-next"; import "../../../../../components/ha-md-list"; import "../../../../../components/ha-md-list-item"; +import type { BackupContent, BackupType } from "../../../../../data/backup"; import { computeBackupSize, - type BackupContent, + computeBackupType, + getBackupTypes, } from "../../../../../data/backup"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; @@ -21,6 +24,12 @@ interface BackupStats { size: number; } +const TYPE_ICONS: Record = { + automatic: mdiCalendarSync, + manual: mdiGestureTap, + addon_update: mdiPuzzle, +}; + const computeBackupStats = (backups: BackupContent[]): BackupStats => backups.reduce( (stats, backup) => { @@ -37,23 +46,22 @@ class HaBackupOverviewBackups extends LitElement { @property({ attribute: false }) public backups: BackupContent[] = []; - private _automaticStats = memoizeOne((backups: BackupContent[]) => { - const automaticBackups = backups.filter( - (backup) => backup.with_automatic_settings - ); - return computeBackupStats(automaticBackups); - }); - - private _manualStats = memoizeOne((backups: BackupContent[]) => { - const manualBackups = backups.filter( - (backup) => !backup.with_automatic_settings - ); - return computeBackupStats(manualBackups); - }); + private _stats = memoizeOne( + ( + backups: BackupContent[], + isHassio: boolean + ): [BackupType, BackupStats][] => + getBackupTypes(isHassio).map((type) => { + const backupsOfType = backups.filter( + (backup) => computeBackupType(backup, isHassio) === type + ); + return [type, computeBackupStats(backupsOfType)] as const; + }) + ); render() { - const automaticStats = this._automaticStats(this.backups); - const manualStats = this._manualStats(this.backups); + const isHassio = isComponentLoaded(this.hass, "hassio"); + const stats = this._stats(this.backups, isHassio); return html` @@ -62,44 +70,32 @@ class HaBackupOverviewBackups extends LitElement {
- - -
- ${this.hass.localize( - "ui.panel.config.backup.overview.backups.automatic", - { count: automaticStats.count } - )} -
-
- ${this.hass.localize( - "ui.panel.config.backup.overview.backups.total_size", - { size: bytesToString(automaticStats.size, 1) } - )} -
- -
- - -
- ${this.hass.localize( - "ui.panel.config.backup.overview.backups.manual", - { count: manualStats.count } - )} -
-
- ${this.hass.localize( - "ui.panel.config.backup.overview.backups.total_size", - { size: bytesToString(manualStats.size, 1) } - )} -
- -
+ ${stats.map( + ([type, { count, size }]) => html` + + +
+ ${this.hass.localize( + `ui.panel.config.backup.overview.backups.${type}`, + { count } + )} +
+
+ ${this.hass.localize( + "ui.panel.config.backup.overview.backups.total_size", + { size: bytesToString(size) } + )} +
+ +
+ ` + )}
diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index 4bab6ee118..6caa2190af 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -11,6 +11,7 @@ import type { CSSResultGroup, TemplateResult } from "lit"; import { html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { relativeTime } from "../../../common/datetime/relative_time"; import { storage } from "../../../common/decorators/storage"; import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event"; @@ -42,9 +43,11 @@ import { compareAgents, computeBackupAgentName, computeBackupSize, + computeBackupType, deleteBackup, generateBackup, generateBackupWithAutomaticSettings, + getBackupTypes, isLocalAgent, isNetworkMountAgent, } from "../../../data/backup"; @@ -74,10 +77,6 @@ interface BackupRow extends DataTableRowData, BackupContent { agent_ids: string[]; } -type BackupType = "automatic" | "manual"; - -const TYPE_ORDER: BackupType[] = ["automatic", "manual"]; - @customElement("ha-config-backup-backups") class HaConfigBackupBackups extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -277,9 +276,13 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { ); private _groupOrder = memoizeOne( - (activeGrouping: string | undefined, localize: LocalizeFunc) => + ( + activeGrouping: string | undefined, + localize: LocalizeFunc, + isHassio: boolean + ) => activeGrouping === "formatted_type" - ? TYPE_ORDER.map((type) => + ? getBackupTypes(isHassio).map((type) => localize(`ui.panel.config.backup.type.${type}`) ) : undefined @@ -303,20 +306,19 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { ( backups: BackupContent[], filters: DataTableFiltersValues, - localize: LocalizeFunc + localize: LocalizeFunc, + isHassio: boolean ): BackupRow[] => { const typeFilter = filters["ha-filter-states"] as string[] | undefined; let filteredBackups = backups; if (typeFilter?.length) { - filteredBackups = filteredBackups.filter( - (backup) => - (backup.with_automatic_settings && - typeFilter.includes("automatic")) || - (!backup.with_automatic_settings && typeFilter.includes("manual")) - ); + filteredBackups = filteredBackups.filter((backup) => { + const type = computeBackupType(backup, isHassio); + return typeFilter.includes(type); + }); } return filteredBackups.map((backup) => { - const type = backup.with_automatic_settings ? "automatic" : "manual"; + const type = computeBackupType(backup, isHassio); const agentIds = Object.keys(backup.agents); return { ...backup, @@ -335,8 +337,13 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { protected render(): TemplateResult { const backupInProgress = "state" in this.manager && this.manager.state === "in_progress"; - - const data = this._data(this.backups, this._filters, this.hass.localize); + const isHassio = isComponentLoaded(this.hass, "hassio"); + const data = this._data( + this.backups, + this._filters, + this.hass.localize, + isHassio + ); const maxDisplayedAgents = Math.min( this._maxAgents(data), this.narrow ? 3 : 5 @@ -371,7 +378,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { .initialCollapsedGroups=${this._activeCollapsed} .groupOrder=${this._groupOrder( this._activeGrouping, - this.hass.localize + this.hass.localize, + isHassio )} @grouping-changed=${this._handleGroupingChanged} @collapsed-changed=${this._handleCollapseChanged} @@ -435,7 +443,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { .hass=${this.hass} .label=${this.hass.localize("ui.panel.config.backup.backup_type")} .value=${this._filters["ha-filter-states"]} - .states=${this._states(this.hass.localize)} + .states=${this._states(this.hass.localize, isHassio)} @data-table-filter-changed=${this._filterChanged} slot="filter-pane" expanded @@ -460,8 +468,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { `; } - private _states = memoizeOne((localize: LocalizeFunc) => - TYPE_ORDER.map((type) => ({ + private _states = memoizeOne((localize: LocalizeFunc, isHassio: boolean) => + getBackupTypes(isHassio).map((type) => ({ value: type, label: localize(`ui.panel.config.backup.type.${type}`), })) diff --git a/src/panels/config/backup/ha-config-backup-details.ts b/src/panels/config/backup/ha-config-backup-details.ts index ee3a701f47..c3bbd93552 100644 --- a/src/panels/config/backup/ha-config-backup-details.ts +++ b/src/panels/config/backup/ha-config-backup-details.ts @@ -31,6 +31,7 @@ import { compareAgents, computeBackupAgentName, computeBackupSize, + computeBackupType, deleteBackup, fetchBackupDetails, isLocalAgent, @@ -46,6 +47,7 @@ import { showRestoreBackupDialog } from "./dialogs/show-dialog-restore-backup"; import { fireEvent } from "../../../common/dom/fire_event"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { downloadBackup } from "./helper/download_backup"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; interface Agent extends BackupContentAgent { id: string; @@ -110,6 +112,8 @@ class HaConfigBackupDetails extends LitElement { return nothing; } + const isHassio = isComponentLoaded(this.hass, "hassio"); + return html`
+ + + ${this.hass.localize( + "ui.panel.config.backup.backup_type" + )} + + + ${this.hass.localize( + `ui.panel.config.backup.type.${computeBackupType(this._backup, isHassio)}` + )} + + ${this.hass.localize( diff --git a/src/translations/en.json b/src/translations/en.json index e78f1b5e44..7def0d0eda 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2223,7 +2223,8 @@ "backup_type": "Type", "type": { "manual": "Manual", - "automatic": "Automatic" + "automatic": "Automatic", + "addon_update": "Add-on update" }, "locations": "Locations", "create": { @@ -2565,6 +2566,7 @@ "title": "My backups", "automatic": "{count} automatic {count, plural,\n one {backup}\n other {backups}\n}", "manual": "{count} manual {count, plural,\n one {backup}\n other {backups}\n}", + "addon_update": "{count} add-on update {count, plural,\n one {backup}\n other {backups}\n}", "total_size": "{size} in total", "show_all": "Show all backups" }, From 849922f7bede761994f213a65c9f283fcdb911e7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 4 Feb 2025 13:38:03 +0100 Subject: [PATCH 44/64] Dont show voice wizard for voip (#24050) dont show voice wizard for voip --- .../config-flow/step-flow-create-entry.ts | 3 +- .../voice-assistant-setup-dialog.ts | 125 ++++++++++-------- .../voice-assistant-setup-step-success.ts | 2 +- .../voice-assistant-setup-step-wake-word.ts | 26 +++- .../config/devices/ha-config-device-page.ts | 13 +- 5 files changed, 98 insertions(+), 71 deletions(-) diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index 1615a6c451..06eab5c85b 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -65,7 +65,8 @@ class StepFlowCreateEntry extends LitElement { if ( devices.length !== 1 || - devices[0].primary_config_entry !== this.step.result?.entry_id + devices[0].primary_config_entry !== this.step.result?.entry_id || + this.step.result.domain === "voip" ) { return; } diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts index eadf4a901a..81ed1c1113 100644 --- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts @@ -47,6 +47,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement { @state() private _assistConfiguration?: AssistSatelliteConfiguration; + @state() private _error?: string; + private _previousSteps: STEP[] = []; private _nextStep?: STEP; @@ -165,79 +167,86 @@ export class HaVoiceAssistantSetupDialog extends LitElement { "update" )} >` - : assistEntityState?.state === UNAVAILABLE - ? this.hass.localize( - "ui.panel.config.voice_assistants.satellite_wizard.not_available" - ) - : this._step === STEP.CHECK - ? html`` - : this._step === STEP.WAKEWORD - ? html`${this._error}` + : assistEntityState?.state === UNAVAILABLE + ? html`${this.hass.localize( + "ui.panel.config.voice_assistants.satellite_wizard.not_available" + )}` + : this._step === STEP.CHECK + ? html`` - : this._step === STEP.CHANGE_WAKEWORD - ? html` - - ` - : this._step === STEP.AREA + >` + : this._step === STEP.WAKEWORD + ? html`` + : this._step === STEP.CHANGE_WAKEWORD ? html` - - ` - : this._step === STEP.PIPELINE - ? html`` - : this._step === STEP.CLOUD - ? html` + ` + : this._step === STEP.AREA + ? html` + ` - : this._step === STEP.LOCAL - ? html` + ` + : this._step === STEP.PIPELINE + ? html`` + : this._step === STEP.CLOUD + ? html`` - : this._step === STEP.SUCCESS - ? html`` + : this._step === STEP.LOCAL + ? html`` - : nothing} + >` + : this._step === STEP.SUCCESS + ? html`` + : nothing}
`; } private async _fetchAssistConfiguration() { - this._assistConfiguration = await fetchAssistSatelliteConfiguration( - this.hass, - this._findDomainEntityId( - this._params!.deviceId, - this.hass.entities, - "assist_satellite" - )! - ); - return this._assistConfiguration; + try { + this._assistConfiguration = await fetchAssistSatelliteConfiguration( + this.hass, + this._findDomainEntityId( + this._params!.deviceId, + this.hass.entities, + "assist_satellite" + )! + ); + } catch (err: any) { + this._error = err.message; + } } private _goToPreviousStep() { @@ -293,6 +302,10 @@ export class HaVoiceAssistantSetupDialog extends LitElement { .skip-btn { margin-top: 6px; } + ha-alert { + margin: 24px; + display: block; + } `, ]; } diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-success.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-success.ts index d6416e312d..e9db911f49 100644 --- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-success.ts +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-success.ts @@ -85,7 +85,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
${this.assistConfiguration && this.assistConfiguration.available_wake_words.length > 1 - ? html`
+ ? html`
` : nothing}
- `; + ${this.assistConfiguration && + this.assistConfiguration.available_wake_words.length > 1 + ? html`` + : nothing}`; } private async _listenWakeWord() { diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index ad7e23738f..84d72920e0 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -1073,7 +1073,14 @@ export class HaConfigDevicePage extends LitElement { (ent) => computeDomain(ent.entity_id) === "assist_satellite" ); + const domains = this._integrations( + device, + this.entries, + this.manifests + ).map((int) => int.domain); + if ( + !domains.includes("voip") && assistSatellite && assistSatelliteSupportsSetupFlow( this.hass.states[assistSatellite.entity_id] @@ -1088,12 +1095,6 @@ export class HaConfigDevicePage extends LitElement { }); } - const domains = this._integrations( - device, - this.entries, - this.manifests - ).map((int) => int.domain); - if (domains.includes("mqtt")) { const mqtt = await import( "./device-detail/integration-elements/mqtt/device-actions" From 776c4da6887e3b2fa861089a5690e8f3cb526669 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 4 Feb 2025 18:06:41 +0100 Subject: [PATCH 45/64] Add support package download to cloud (#24051) --- src/data/cloud.ts | 3 + .../config/cloud/account/cloud-account.ts | 31 ++- .../account/dialog-cloud-support-package.ts | 206 ++++++++++++++++++ .../show-dialog-cloud-support-package.ts | 12 + src/panels/config/cloud/login/cloud-login.ts | 25 ++- src/translations/en.json | 1 + 6 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 src/panels/config/cloud/account/dialog-cloud-support-package.ts create mode 100644 src/panels/config/cloud/account/show-dialog-cloud-support-package.ts diff --git a/src/data/cloud.ts b/src/data/cloud.ts index 1a35be028b..b7fea8fae2 100644 --- a/src/data/cloud.ts +++ b/src/data/cloud.ts @@ -181,3 +181,6 @@ export const updateCloudGoogleEntityConfig = ( export const cloudSyncGoogleAssistant = (hass: HomeAssistant) => hass.callApi("POST", "cloud/google_actions/sync"); + +export const fetchSupportPackage = (hass: HomeAssistant) => + hass.callApi("GET", "cloud/support_package"); diff --git a/src/panels/config/cloud/account/cloud-account.ts b/src/panels/config/cloud/account/cloud-account.ts index 5ccbe1ef60..82730ce21c 100644 --- a/src/panels/config/cloud/account/cloud-account.ts +++ b/src/panels/config/cloud/account/cloud-account.ts @@ -1,15 +1,15 @@ import "@material/mwc-button"; +import { mdiDeleteForever, mdiDotsVertical, mdiDownload } from "@mdi/js"; import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { mdiDeleteForever, mdiDotsVertical } from "@mdi/js"; import { formatDateTime } from "../../../../common/datetime/format_date_time"; import { fireEvent } from "../../../../common/dom/fire_event"; import { debounce } from "../../../../common/util/debounce"; import "../../../../components/ha-alert"; -import "../../../../components/ha-card"; -import "../../../../components/ha-tip"; -import "../../../../components/ha-list-item"; import "../../../../components/ha-button-menu"; +import "../../../../components/ha-card"; +import "../../../../components/ha-list-item"; +import "../../../../components/ha-tip"; import type { CloudStatusLoggedIn, SubscriptionInfo, @@ -32,6 +32,7 @@ import "./cloud-ice-servers-pref"; import "./cloud-remote-pref"; import "./cloud-tts-pref"; import "./cloud-webhooks"; +import { showSupportPackageDialog } from "./show-dialog-cloud-support-package"; @customElement("cloud-account") export class CloudAccount extends SubscribeMixin(LitElement) { @@ -52,7 +53,7 @@ export class CloudAccount extends SubscribeMixin(LitElement) { .narrow=${this.narrow} header="Home Assistant Cloud" > - + + + ${this.hass.localize( + "ui.panel.config.cloud.account.download_support_package" + )} + +
@@ -286,6 +293,16 @@ export class CloudAccount extends SubscribeMixin(LitElement) { fireEvent(this, "ha-refresh-cloud-status"); } + private _handleMenuAction(ev) { + switch (ev.detail.index) { + case 0: + this._deleteCloudData(); + break; + case 1: + this._downloadSupportPackage(); + } + } + private async _deleteCloudData() { const confirm = await showConfirmationDialog(this, { title: this.hass.localize( @@ -316,6 +333,10 @@ export class CloudAccount extends SubscribeMixin(LitElement) { } } + private async _downloadSupportPackage() { + showSupportPackageDialog(this); + } + static get styles() { return [ haStyle, diff --git a/src/panels/config/cloud/account/dialog-cloud-support-package.ts b/src/panels/config/cloud/account/dialog-cloud-support-package.ts new file mode 100644 index 0000000000..920db783a4 --- /dev/null +++ b/src/panels/config/cloud/account/dialog-cloud-support-package.ts @@ -0,0 +1,206 @@ +import "@material/mwc-button"; +import "@material/mwc-list/mwc-list-item"; +import { mdiClose } from "@mdi/js"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-button"; +import "../../../../components/ha-circular-progress"; +import "../../../../components/ha-dialog-header"; +import "../../../../components/ha-markdown-element"; +import "../../../../components/ha-md-dialog"; +import type { HaMdDialog } from "../../../../components/ha-md-dialog"; +import "../../../../components/ha-select"; +import "../../../../components/ha-textarea"; +import { fetchSupportPackage } from "../../../../data/cloud"; +import type { HomeAssistant } from "../../../../types"; +import { fileDownload } from "../../../../util/file_download"; + +@customElement("dialog-cloud-support-package") +export class DialogSupportPackage extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _open = false; + + @state() private _supportPackage?: string; + + @query("ha-md-dialog") private _dialog?: HaMdDialog; + + public showDialog() { + this._open = true; + this._loadSupportPackage(); + } + + private _dialogClosed(): void { + this._open = false; + this._supportPackage = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + public closeDialog() { + this._dialog?.close(); + return true; + } + + protected render() { + if (!this._open) { + return nothing; + } + return html` + + + + Download support package + + +
+ ${this._supportPackage + ? html`` + : html` +
+ + Generating preview... +
+ `} +
+ +
+ `; + } + + private async _loadSupportPackage() { + this._supportPackage = await fetchSupportPackage(this.hass); + } + + private async _download() { + fileDownload( + "data:text/plain;charset=utf-8," + + encodeURIComponent(this._supportPackage || ""), + "support-package.md" + ); + } + + static styles = css` + ha-md-dialog { + min-width: 90vw; + min-height: 90vh; + } + + .progress-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: calc(90vh - 260px); + width: 100%; + } + + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-md-dialog { + min-width: 100vw; + min-height: 100vh; + } + .progress-container { + height: calc(100vh - 260px); + } + } + + .footer { + flex-direction: column; + } + .actions { + display: flex; + gap: 8px; + justify-content: flex-end; + } + hr { + border: none; + border-top: 1px solid var(--divider-color); + width: calc(100% + 48px); + margin-right: -24px; + margin-left: -24px; + } + table, + th, + td { + border: none; + } + + table { + width: 100%; + display: table; + border-collapse: collapse; + border-spacing: 0; + } + + table tr { + border-bottom: none; + } + + table > tbody > tr:nth-child(odd) { + background-color: rgba(var(--rgb-primary-text-color), 0.04); + } + + table > tbody > tr > td { + border-radius: 0; + } + + table > tbody > tr { + -webkit-transition: background-color 0.25s ease; + transition: background-color 0.25s ease; + } + + table > tbody > tr:hover { + background-color: rgba(var(--rgb-primary-text-color), 0.08); + } + + tr { + border-bottom: 1px solid var(--divider-color); + } + + td, + th { + padding: 15px 5px; + display: table-cell; + text-align: left; + vertical-align: middle; + border-radius: 2px; + } + details { + background-color: var(--secondary-background-color); + padding: 16px 24px; + margin: 8px 0; + border: 1px solid var(--divider-color); + border-radius: 16px; + } + summary { + font-weight: bold; + cursor: pointer; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-cloud-support-package": DialogSupportPackage; + } +} diff --git a/src/panels/config/cloud/account/show-dialog-cloud-support-package.ts b/src/panels/config/cloud/account/show-dialog-cloud-support-package.ts new file mode 100644 index 0000000000..697b16d0e9 --- /dev/null +++ b/src/panels/config/cloud/account/show-dialog-cloud-support-package.ts @@ -0,0 +1,12 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; + +export const loadSupportPackageDialog = () => + import("./dialog-cloud-support-package"); + +export const showSupportPackageDialog = (element: HTMLElement): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-cloud-support-package", + dialogImport: loadSupportPackageDialog, + dialogParams: {}, + }); +}; diff --git a/src/panels/config/cloud/login/cloud-login.ts b/src/panels/config/cloud/login/cloud-login.ts index 1a05db180f..5fe14e24ba 100644 --- a/src/panels/config/cloud/login/cloud-login.ts +++ b/src/panels/config/cloud/login/cloud-login.ts @@ -1,6 +1,6 @@ import "@material/mwc-button"; import "@material/mwc-list/mwc-list"; -import { mdiDeleteForever, mdiDotsVertical } from "@mdi/js"; +import { mdiDeleteForever, mdiDotsVertical, mdiDownload } from "@mdi/js"; import type { TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -27,6 +27,7 @@ import "../../../../layouts/hass-subpage"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import "../../ha-config-section"; +import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package"; @customElement("cloud-login") export class CloudLogin extends LitElement { @@ -57,7 +58,7 @@ export class CloudLogin extends LitElement { .narrow=${this.narrow} header="Home Assistant Cloud" > - + + + ${this.hass.localize( + "ui.panel.config.cloud.account.download_support_package" + )} + +
@@ -348,6 +355,16 @@ export class CloudLogin extends LitElement { fireEvent(this, "flash-message-changed", { value: "" }); } + private _handleMenuAction(ev) { + switch (ev.detail.index) { + case 0: + this._deleteCloudData(); + break; + case 1: + this._downloadSupportPackage(); + } + } + private async _deleteCloudData() { const confirm = await showConfirmationDialog(this, { title: this.hass.localize( @@ -377,6 +394,10 @@ export class CloudLogin extends LitElement { } } + private async _downloadSupportPackage() { + showSupportPackageDialog(this); + } + static get styles() { return [ haStyle, diff --git a/src/translations/en.json b/src/translations/en.json index 7def0d0eda..9178ea9ce3 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4591,6 +4591,7 @@ "account_created": "Account created! Check your email for instructions on how to activate your account." }, "account": { + "download_support_package": "Download support package", "reset_cloud_data": "Reset cloud data", "reset_data_confirm_title": "Reset cloud data?", "reset_data_confirm_text": "This will reset all your cloud settings. This includes your remote connection, Google Assistant and Amazon Alexa integrations. This action cannot be undone.", From 53f090356e9449c0f913ca63f4a01a9462f67c63 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 4 Feb 2025 12:47:24 +0100 Subject: [PATCH 46/64] Improve value formatting inside backup tooltip (#24057) --- .../chart/state-history-chart-line.ts | 25 ++++++---- src/components/chart/statistics-chart.ts | 49 +++++++++++-------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 2afbc7f3bd..b2f28c9a48 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -20,6 +20,7 @@ import { import { measureTextWidth } from "../../util/text"; import { fireEvent } from "../../common/dom/fire_event"; import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate"; +import { blankBeforeUnit } from "../../common/translations/blank_before_unit"; const safeParseFloat = (value) => { const parsed = parseFloat(value); @@ -132,19 +133,25 @@ export class StateHistoryChartLine extends LitElement { marker: ``, }); }); - const unit = this.unit ? ` ${this.unit}` : ""; + const unit = this.unit + ? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}` + : ""; + return ( title + datapoints .map((param) => { - let value = `${formatNumber( - param.value[1] as number, - this.hass.locale, - getNumberFormatOptions( - undefined, - this.hass.entities[this._entityIds[param.seriesIndex]] - ) - )}${unit}`; + const entityId = this._entityIds[param.seriesIndex]; + const stateObj = this.hass.states[entityId]; + const entry = this.hass.entities[entityId]; + const stateValue = String(param.value[1]); + let value = stateObj + ? this.hass.formatEntityState(stateObj, stateValue) + : `${formatNumber( + stateValue, + this.hass.locale, + getNumberFormatOptions(undefined, entry) + )}${unit}`; const dataIndex = this._datasetToDataIndex[param.seriesIndex]; const data = this.data[dataIndex]; if (data.statistics && data.statistics.length > 0) { diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 05eb8fe654..f6a03bd9c8 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -1,15 +1,22 @@ -import type { PropertyValues, TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; import type { BarSeriesOption, LineSeriesOption, } from "echarts/types/dist/shared"; +import type { PropertyValues, TemplateResult } from "lit"; +import { css, html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; +import memoizeOne from "memoize-one"; import { getGraphColorByIndex } from "../../common/color/colors"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; +import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; +import { + formatNumber, + getNumberFormatOptions, +} from "../../common/number/format_number"; +import { blankBeforeUnit } from "../../common/translations/blank_before_unit"; +import { computeRTL } from "../../common/util/compute_rtl"; import type { Statistics, StatisticsMetaData, @@ -21,15 +28,9 @@ import { getStatisticMetadata, statisticsHaveType, } from "../../data/recorder"; +import type { ECOption } from "../../resources/echarts"; import type { HomeAssistant } from "../../types"; import "./ha-chart-base"; -import { computeRTL } from "../../common/util/compute_rtl"; -import type { ECOption } from "../../resources/echarts"; -import { - formatNumber, - getNumberFormatOptions, -} from "../../common/number/format_number"; -import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; export const supportedStatTypeMap: Record = { mean: "mean", @@ -186,20 +187,26 @@ export class StatisticsChart extends LitElement { private _renderTooltip = (params: any) => { const rendered: Record = {}; - const unit = this.unit ? ` ${this.unit}` : ""; + const unit = this.unit + ? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}` + : ""; return params .map((param, index: number) => { if (rendered[param.seriesName]) return ""; rendered[param.seriesName] = true; - const value = `${formatNumber( - // max series can have 3 values, as the second value is the max-min to form a band - (param.value[2] ?? param.value[1]) as number, - this.hass.locale, - getNumberFormatOptions( - undefined, - this.hass.entities[this._statisticIds[param.seriesIndex]] - ) - )}${unit}`; + + const statisticId = this._statisticIds[param.seriesIndex]; + const stateObj = this.hass.states[statisticId]; + const entry = this.hass.entities[statisticId]; + const stateValue = String(param.value[1]); + + const value = stateObj + ? this.hass.formatEntityState(stateObj, stateValue) + : `${formatNumber( + stateValue, + this.hass.locale, + getNumberFormatOptions(undefined, entry) + )}${unit}`; const time = index === 0 From ce0f02a45bf47cb0ee5909075caa987961007617 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 4 Feb 2025 14:45:38 +0100 Subject: [PATCH 47/64] Display unavailable backups locations (#24058) Display anavailable backups locations --- .../config/ha-backup-config-agents.ts | 169 ++++++++++++------ src/translations/en.json | 1 + 2 files changed, 115 insertions(+), 55 deletions(-) diff --git a/src/panels/config/backup/components/config/ha-backup-config-agents.ts b/src/panels/config/backup/components/config/ha-backup-config-agents.ts index 3d6463af8c..4444e917a2 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-agents.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-agents.ts @@ -1,4 +1,4 @@ -import { mdiCog, mdiHarddisk, mdiNas } from "@mdi/js"; +import { mdiCog, mdiDelete, mdiHarddisk, mdiNas } from "@mdi/js"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; @@ -41,13 +41,6 @@ class HaBackupConfigAgents extends LitElement { @state() private value?: string[]; - private _availableAgents = memoizeOne( - (agents: BackupAgent[], cloudStatus: CloudStatus) => - agents.filter( - (agent) => agent.agent_id !== CLOUD_AGENT || cloudStatus.logged_in - ) - ); - private get _value() { return this.value ?? DEFAULT_AGENTS; } @@ -86,19 +79,84 @@ class HaBackupConfigAgents extends LitElement { return ""; } - protected render() { - const agents = this._availableAgents(this.agents, this.cloudStatus); + private _availableAgents = memoizeOne( + (agents: BackupAgent[], cloudStatus: CloudStatus) => + agents.filter( + (agent) => agent.agent_id !== CLOUD_AGENT || cloudStatus.logged_in + ) + ); + + private _unavailableAgents = memoizeOne( + ( + agents: BackupAgent[], + cloudStatus: CloudStatus, + selectedAgentIds: string[] + ) => { + const availableAgentIds = this._availableAgents(agents, cloudStatus).map( + (agent) => agent.agent_id + ); + + return selectedAgentIds + .filter((agent) => !availableAgentIds.includes(agent)) + .map((id) => ({ + agent_id: id, + name: id.split(".")[1] || id, // Use the id as name as it is not available in the list + })); + } + ); + + private _renderAgentIcon(agentId: string) { + if (isLocalAgent(agentId)) { + return html` + + `; + } + + if (isNetworkMountAgent(agentId)) { + return html``; + } + + const domain = computeDomain(agentId); + return html` - ${agents.length > 0 + + `; + } + + protected render() { + const availableAgents = this._availableAgents( + this.agents, + this.cloudStatus + ); + const unavailableAgents = this._unavailableAgents( + this.agents, + this.cloudStatus, + this._value + ); + + const allAgents = [...availableAgents, ...unavailableAgents]; + + return html` + ${allAgents.length > 0 ? html` - ${agents.map((agent) => { + ${availableAgents.map((agent) => { const agentId = agent.agent_id; - const domain = computeDomain(agentId); const name = computeBackupAgentName( this.hass.localize, agentId, - this.agents + allAgents ); const description = this._description(agentId); const noCloudSubscription = @@ -108,32 +166,7 @@ class HaBackupConfigAgents extends LitElement { return html` - ${isLocalAgent(agentId) - ? html` - - - ` - : isNetworkMountAgent(agentId) - ? html` - - ` - : html` - - `} + ${this._renderAgentIcon(agentId)}
${name}
${description ? html`
${description}
` @@ -151,14 +184,44 @@ class HaBackupConfigAgents extends LitElement {
`; })} + ${unavailableAgents.length > 0 && this.showSettings + ? html` +

+ ${this.hass.localize( + "ui.panel.config.backup.agents.unavailable_agents" + )} +

+ ${unavailableAgents.map((agent) => { + const agentId = agent.agent_id; + const name = computeBackupAgentName( + this.hass.localize, + agentId, + allAgents + ); + + return html` + + ${this._renderAgentIcon(agentId)} +
${name}
+ +
+ `; + })} + ` + : nothing}
` : html` @@ -174,6 +237,13 @@ class HaBackupConfigAgents extends LitElement { navigate(`/config/backup/location/${agentId}`); } + private _deleteAgent(ev): void { + ev.stopPropagation(); + const agentId = ev.currentTarget.id; + this.value = this._value.filter((agent) => agent !== agentId); + fireEvent(this, "value-changed", { value: this.value }); + } + private _agentToggled(ev) { ev.stopPropagation(); const value = ev.currentTarget.checked; @@ -185,19 +255,8 @@ class HaBackupConfigAgents extends LitElement { this.value = this._value.filter((agent) => agent !== agentId); } - const availableAgents = this._availableAgents( - this.agents, - this.cloudStatus - ); - // Ensure we don't have duplicates, agents exist in the list and cloud is logged in - this.value = [...new Set(this.value)] - .filter((id) => availableAgents.some((agent) => agent.agent_id === id)) - .filter( - (id) => - id !== CLOUD_AGENT || - (this.cloudStatus.logged_in && this.cloudStatus.active_subscription) - ); + this.value = [...new Set(this.value)]; fireEvent(this, "value-changed", { value: this.value }); } diff --git a/src/translations/en.json b/src/translations/en.json index 9178ea9ce3..8271cb33d7 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2409,6 +2409,7 @@ "cloud_agent_description": "Note: It stores only the most recent backup, regardless of your retention settings, with a maximum size of 5 GB.", "cloud_agent_no_subcription": "You currently do not have an active Home Assistant Cloud subscription.", "network_mount_agent_description": "Network storage", + "unavailable_agents": "Unavailable locations", "no_agents": "No locations configured", "encryption_turned_off": "Encryption turned off", "local_agent": "This system" From 31180e3a9e6172c5582e53d47a8e85ef4c34170c Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 4 Feb 2025 14:28:31 +0200 Subject: [PATCH 48/64] Fix energy charts with leap years (#24059) * Fix energy charts with leap years * handle quarters --- .../energy/common/energy-chart-options.ts | 50 ++++++++++++++++--- .../hui-energy-devices-detail-graph-card.ts | 12 +++-- .../cards/energy/hui-energy-gas-graph-card.ts | 15 ++++-- .../energy/hui-energy-solar-graph-card.ts | 15 ++++-- .../energy/hui-energy-usage-graph-card.ts | 10 ++-- .../energy/hui-energy-water-graph-card.ts | 15 ++++-- 6 files changed, 87 insertions(+), 30 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 76dde725aa..943ade0bb5 100644 --- a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts +++ b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts @@ -1,5 +1,16 @@ import type { HassConfig } from "home-assistant-js-websocket"; -import { addHours, subHours, differenceInDays } from "date-fns"; +import { + differenceInMonths, + subHours, + differenceInDays, + differenceInYears, + startOfYear, + addMilliseconds, + startOfMonth, + addYears, + addMonths, + addHours, +} from "date-fns"; import type { BarSeriesOption, CallbackDataParams, @@ -7,7 +18,10 @@ import type { } from "echarts/types/dist/shared"; import type { FrontendLocaleData } from "../../../../../data/translation"; import { formatNumber } from "../../../../../common/number/format_number"; -import { formatDateVeryShort } from "../../../../../common/datetime/format_date"; +import { + formatDateMonthYear, + formatDateVeryShort, +} from "../../../../../common/datetime/format_date"; import { formatTime } from "../../../../../common/datetime/format_time"; import type { ECOption } from "../../../../../resources/echarts"; @@ -53,7 +67,7 @@ export function getCommonOptions( xAxis: { type: "time", min: start, - max: end, + max: getSuggestedMax(dayDifference, end), }, yAxis: { type: "value", @@ -88,7 +102,6 @@ export function getCommonOptions( } }); return [mainItems, compareItems] - .filter((items) => items.length > 0) .map((items) => formatTooltip( items, @@ -100,6 +113,7 @@ export function getCommonOptions( formatTotal ) ) + .filter(Boolean) .join("

"); } return formatTooltip( @@ -126,14 +140,16 @@ function formatTooltip( unit?: string, formatTotal?: (total: number) => string ) { - if (!params[0].value) { + if (!params[0]?.value) { return ""; } // when comparing the first value is offset to match the main period // and the real date is in the third value const date = new Date(params[0].value?.[2] ?? params[0].value?.[0]); let period: string; - if (dayDifference > 0) { + if (dayDifference > 89) { + period = `${formatDateMonthYear(date, locale, config)}`; + } else if (dayDifference > 0) { period = `${formatDateVeryShort(date, locale, config)}`; } else { period = `${ @@ -242,3 +258,25 @@ export function fillDataGapsAndRoundCaps(datasets: BarSeriesOption[]) { } }); } + +export function getCompareTransform(start: Date, compareStart?: Date) { + if (!compareStart) { + return (ts: Date) => ts; + } + const compareYearDiff = differenceInYears(start, compareStart); + if ( + compareYearDiff !== 0 && + start.getTime() === startOfYear(start).getTime() + ) { + return (ts: Date) => addYears(ts, compareYearDiff); + } + const compareMonthDiff = differenceInMonths(start, compareStart); + if ( + compareMonthDiff !== 0 && + start.getTime() === startOfMonth(start).getTime() + ) { + return (ts: Date) => addMonths(ts, compareMonthDiff); + } + const compareOffset = start.getTime() - compareStart.getTime(); + return (ts: Date) => addMilliseconds(ts, compareOffset); +} 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 e0d2db9e60..8a609fe507 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 @@ -33,6 +33,7 @@ import { hasConfigChanged } from "../../common/has-changed"; import { fillDataGapsAndRoundCaps, getCommonOptions, + getCompareTransform, } from "./common/energy-chart-options"; import { storage } from "../../../../common/decorators/storage"; import type { ECOption } from "../../../../resources/echarts"; @@ -319,18 +320,19 @@ export class HuiEnergyDevicesDetailGraphCard datapoint[1]; }); }); - const compareOffset = compare - ? this._start.getTime() - this._compareStart!.getTime() - : 0; + const compareTransform = getCompareTransform( + this._start, + this._compareStart! + ); const untrackedConsumption: BarSeriesOption["data"] = []; Object.keys(consumptionData.total).forEach((time) => { const value = consumptionData.total[time] - (totalDeviceConsumption[time] || 0); - const dataPoint = [Number(time), value]; + const dataPoint: (Date | string | number)[] = [time, value]; if (compare) { dataPoint[2] = dataPoint[0]; - dataPoint[0] += compareOffset; + dataPoint[0] = compareTransform(new Date(time)); } untrackedConsumption.push(dataPoint); }); diff --git a/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts index 0c19285d70..6612a58c8f 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts @@ -29,6 +29,7 @@ import { hasConfigChanged } from "../../common/has-changed"; import { fillDataGapsAndRoundCaps, getCommonOptions, + getCompareTransform, } from "./common/energy-chart-options"; import type { ECOption } from "../../../../resources/echarts"; @@ -213,9 +214,10 @@ export class HuiEnergyGasGraphCard compare = false ) { const data: BarSeriesOption[] = []; - const compareOffset = compare - ? this._start.getTime() - this._compareStart!.getTime() - : 0; + const compareTransform = getCompareTransform( + this._start, + this._compareStart! + ); gasSources.forEach((source, idx) => { let prevStart: number | null = null; @@ -236,10 +238,13 @@ export class HuiEnergyGasGraphCard if (prevStart === point.start) { continue; } - const dataPoint = [point.start, point.change]; + const dataPoint: (Date | string | number)[] = [ + point.start, + point.change, + ]; if (compare) { dataPoint[2] = dataPoint[0]; - dataPoint[0] += compareOffset; + dataPoint[0] = compareTransform(new Date(point.start)); } gasConsumptionData.push(dataPoint); prevStart = point.start; 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 e679022a1b..e1ffa869f0 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 @@ -30,6 +30,7 @@ import { hasConfigChanged } from "../../common/has-changed"; import { fillDataGapsAndRoundCaps, getCommonOptions, + getCompareTransform, } from "./common/energy-chart-options"; import type { ECOption } from "../../../../resources/echarts"; @@ -231,9 +232,10 @@ export class HuiEnergySolarGraphCard compare = false ) { const data: BarSeriesOption[] = []; - const compareOffset = compare - ? this._start.getTime() - this._compareStart!.getTime() - : 0; + const compareTransform = getCompareTransform( + this._start, + this._compareStart! + ); solarSources.forEach((source, idx) => { let prevStart: number | null = null; @@ -255,10 +257,13 @@ export class HuiEnergySolarGraphCard if (prevStart === point.start) { continue; } - const dataPoint = [point.start, point.change]; + const dataPoint: (Date | string | number)[] = [ + point.start, + point.change, + ]; if (compare) { dataPoint[2] = dataPoint[0]; - dataPoint[0] += compareOffset; + dataPoint[0] = compareTransform(new Date(point.start)); } solarProductionData.push(dataPoint); prevStart = point.start; 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 74fc5dc83f..9836c7f061 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 @@ -27,6 +27,7 @@ import { hasConfigChanged } from "../../common/has-changed"; import { fillDataGapsAndRoundCaps, getCommonOptions, + getCompareTransform, } from "./common/energy-chart-options"; import type { ECOption } from "../../../../resources/echarts"; @@ -476,9 +477,10 @@ export class HuiEnergyUsageGraphCard (a, b) => Number(a) - Number(b) ); - const compareOffset = compare - ? this._start.getTime() - this._compareStart!.getTime() - : 0; + const compareTransform = getCompareTransform( + this._start, + this._compareStart! + ); Object.entries(combinedData).forEach(([type, sources]) => { Object.entries(sources).forEach(([statId, source]) => { @@ -494,7 +496,7 @@ export class HuiEnergyUsageGraphCard ]; if (compare) { dataPoint[2] = dataPoint[0]; - dataPoint[0] += compareOffset; + dataPoint[0] = compareTransform(dataPoint[0]); } points.push(dataPoint); } diff --git a/src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts index 919bd06ed1..adfc3d217a 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts @@ -28,6 +28,7 @@ import { hasConfigChanged } from "../../common/has-changed"; import { fillDataGapsAndRoundCaps, getCommonOptions, + getCompareTransform, } from "./common/energy-chart-options"; import type { ECOption } from "../../../../resources/echarts"; import { formatNumber } from "../../../../common/number/format_number"; @@ -211,9 +212,10 @@ export class HuiEnergyWaterGraphCard compare = false ) { const data: BarSeriesOption[] = []; - const compareOffset = compare - ? this._start.getTime() - this._compareStart!.getTime() - : 0; + const compareTransform = getCompareTransform( + this._start, + this._compareStart! + ); waterSources.forEach((source, idx) => { let prevStart: number | null = null; @@ -234,10 +236,13 @@ export class HuiEnergyWaterGraphCard if (prevStart === point.start) { continue; } - const dataPoint = [point.start, point.change]; + const dataPoint: (Date | string | number)[] = [ + point.start, + point.change, + ]; if (compare) { dataPoint[2] = dataPoint[0]; - dataPoint[0] += compareOffset; + dataPoint[0] = compareTransform(new Date(point.start)); } waterConsumptionData.push(dataPoint); prevStart = point.start; From e5fea9846045973c37adc09b9cde67e867ae52ec Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 4 Feb 2025 18:22:43 +0100 Subject: [PATCH 49/64] Bumped version to 20250204.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0a60ae76fc..53922f4d91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250203.0" +version = "20250204.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" From 786ff787d1b10a75dfed4a5f6b7586c8657af61d Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 5 Feb 2025 11:22:31 +0200 Subject: [PATCH 50/64] Fix spacing & colors in statistics-graph chart (#24068) * Fix statistic chart colors * Fix spacing in statistics-graph * set start time based on data --- src/components/chart/ha-chart-base.ts | 78 +++++++++++++----------- src/components/chart/statistics-chart.ts | 61 ++++++++++-------- 2 files changed, 77 insertions(+), 62 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index e83bb757a8..9c9e24c4c1 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -248,43 +248,49 @@ export class HaChartBase extends LitElement { private _createOptions(): ECOption { let xAxis = this.options?.xAxis; - if (xAxis && !Array.isArray(xAxis) && xAxis.type === "time") { - if (xAxis.max && xAxis.min) { - this._minutesDifference = differenceInMinutes( - xAxis.max as Date, - xAxis.min as Date - ); - } - const dayDifference = this._minutesDifference / 60 / 24; - let minInterval: number | undefined; - if (dayDifference) { - minInterval = - dayDifference >= 89 // quarter - ? 28 * 3600 * 24 * 1000 - : dayDifference > 2 - ? 3600 * 24 * 1000 - : undefined; - } - xAxis = { - ...xAxis, - axisLabel: { - formatter: this._formatTimeLabel, - rich: { - bold: { - fontWeight: "bold", - }, + if (xAxis) { + xAxis = Array.isArray(xAxis) ? xAxis : [xAxis]; + xAxis = xAxis.map((axis: XAXisOption) => { + if (axis.type !== "time" || axis.show === false) { + return axis; + } + if (axis.max && axis.min) { + this._minutesDifference = differenceInMinutes( + axis.max as Date, + axis.min as Date + ); + } + const dayDifference = this._minutesDifference / 60 / 24; + let minInterval: number | undefined; + if (dayDifference) { + minInterval = + dayDifference >= 89 // quarter + ? 28 * 3600 * 24 * 1000 + : dayDifference > 2 + ? 3600 * 24 * 1000 + : undefined; + } + return { + axisLine: { + show: false, }, - hideOverlap: true, - ...xAxis.axisLabel, - }, - axisLine: { - show: false, - }, - splitLine: { - show: true, - }, - minInterval, - } as XAXisOption; + splitLine: { + show: true, + }, + ...axis, + axisLabel: { + formatter: this._formatTimeLabel, + rich: { + bold: { + fontWeight: "bold", + }, + }, + hideOverlap: true, + ...axis.axisLabel, + }, + minInterval, + } as XAXisOption; + }); } const options = { animation: !this._reducedMotion, diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index f6a03bd9c8..2ade954598 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -128,7 +128,8 @@ export class StatisticsChart extends LitElement { changedProps.has("hideLegend") || changedProps.has("startTime") || changedProps.has("endTime") || - changedProps.has("_legendData") + changedProps.has("_legendData") || + changedProps.has("_chartData") ) { this._createOptions(); } @@ -246,29 +247,38 @@ export class StatisticsChart extends LitElement { let startTime = this.startTime; if (!startTime) { - // Calculate default start time based on dayDifference - startTime = new Date( - endTime.getTime() - dayDifference * 24 * 3600 * 1000 - ); - - // Check chart data for earlier points + // set start time to the earliest point in the chart data this._chartData.forEach((series) => { - if (!Array.isArray(series.data)) return; - series.data.forEach((point) => { - const timestamp = Array.isArray(point) ? point[0] : point.value?.[0]; - if (new Date(timestamp) < startTime!) { - startTime = new Date(timestamp); - } - }); + if (!Array.isArray(series.data) || !series.data[0]) return; + const firstPoint = series.data[0] as any; + const timestamp = Array.isArray(firstPoint) + ? firstPoint[0] + : firstPoint.value?.[0]; + if (timestamp && (!startTime || new Date(timestamp) < startTime)) { + startTime = new Date(timestamp); + } }); + + if (!startTime) { + // Calculate default start time based on dayDifference + startTime = new Date( + endTime.getTime() - dayDifference * 24 * 3600 * 1000 + ); + } } this._chartOptions = { - xAxis: { - type: "time", - min: startTime, - max: endTime, - }, + xAxis: [ + { + type: "time", + min: startTime, + max: endTime, + }, + { + type: "time", + show: false, + }, + ], yAxis: { type: this.logarithmicScale ? "log" : "value", name: this.unit, @@ -462,6 +472,9 @@ export class StatisticsChart extends LitElement { displayedLegend = displayedLegend || showLegend; } statTypes.push(type); + const borderColor = + band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color; + const backgroundColor = band ? color + "3F" : color + "7F"; const series: LineSeriesOption | BarSeriesOption = { id: `${statistic_id}-${type}`, type: this.chartType, @@ -485,21 +498,16 @@ export class StatisticsChart extends LitElement { this.chartType === "bar" ? { borderRadius: [4, 4, 0, 0], - borderColor: - band && hasMean - ? color + (this.hideLegend ? "00" : "7F") - : color, + borderColor, borderWidth: 1.5, } : undefined, - color: - band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color, + color: this.chartType === "bar" ? backgroundColor : borderColor, }; if (band && this.chartType === "line") { series.stack = `band-${statistic_id}`; series.stackStrategy = "all"; (series as LineSeriesOption).symbol = "none"; - (series as LineSeriesOption).lineStyle = { width: 1.5 }; if (drawBands && type === "max") { (series as LineSeriesOption).areaStyle = { color: color + "3F", @@ -561,6 +569,7 @@ export class StatisticsChart extends LitElement { color, type: this.chartType, data: [], + xAxisIndex: 1, }); }); From 553bb61db7c614fea0f63a23d7ad7f54edcfc111 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 5 Feb 2025 10:18:29 +0100 Subject: [PATCH 51/64] Fix statistic chart tooltip values (#24074) --- src/components/chart/statistics-chart.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 2ade954598..46c6d7477e 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -199,15 +199,18 @@ export class StatisticsChart extends LitElement { const statisticId = this._statisticIds[param.seriesIndex]; const stateObj = this.hass.states[statisticId]; const entry = this.hass.entities[statisticId]; - const stateValue = String(param.value[1]); + // max series can have 3 values, as the second value is the max-min to form a band + const rawValue = String(param.value[2] ?? param.value[1]); - const value = stateObj - ? this.hass.formatEntityState(stateObj, stateValue) - : `${formatNumber( - stateValue, - this.hass.locale, - getNumberFormatOptions(undefined, entry) - )}${unit}`; + const options = getNumberFormatOptions(stateObj, entry) ?? { + maximumFractionDigits: 2, + }; + + const value = `${formatNumber( + rawValue, + this.hass.locale, + options + )}${unit}`; const time = index === 0 From 7cbdb1dcfd012d97e33432a6f61dcfd8ccfa3dac Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Feb 2025 11:04:39 +0100 Subject: [PATCH 52/64] Fix condition in tracing graph (#24075) --- src/components/trace/hat-graph-branch.ts | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/components/trace/hat-graph-branch.ts b/src/components/trace/hat-graph-branch.ts index c5da464bcf..04a9c936c8 100644 --- a/src/components/trace/hat-graph-branch.ts +++ b/src/components/trace/hat-graph-branch.ts @@ -1,4 +1,4 @@ -import { css, html, LitElement, svg } from "lit"; +import { css, html, LitElement, nothing, svg } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { BRANCH_HEIGHT, SPACING } from "./hat-graph-const"; @@ -41,8 +41,8 @@ export class HatGraphBranch extends LitElement { branches.push({ x: width / 2 + total_width, height, - start: c.hasAttribute("graphStart"), - end: c.hasAttribute("graphEnd"), + start: c.hasAttribute("graph-start"), + end: c.hasAttribute("graph-end"), track: c.hasAttribute("track"), }); total_width += width; @@ -65,11 +65,8 @@ export class HatGraphBranch extends LitElement { return html` ${!this.start - ? svg` - + ? html` + ${this._branches.map((branch) => branch.start ? "" @@ -86,7 +83,7 @@ export class HatGraphBranch extends LitElement { )} ` - : ""} + : nothing}
${this._branches.map((branch) => { @@ -107,11 +104,8 @@ export class HatGraphBranch extends LitElement {
${!this.short - ? svg` - + ? html` + ${this._branches.map((branch) => { if (branch.end) return ""; return svg` @@ -128,7 +122,7 @@ export class HatGraphBranch extends LitElement { })} ` - : ""} + : nothing} `; } From 4a94cfc05b44f07b655bc480388d8f532d8e1a17 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Feb 2025 12:48:13 +0100 Subject: [PATCH 53/64] Set list color of update more info to dialog background (#24076) --- src/dialogs/more-info/controls/more-info-update.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dialogs/more-info/controls/more-info-update.ts b/src/dialogs/more-info/controls/more-info-update.ts index 6d6619ab25..10486c4ce3 100644 --- a/src/dialogs/more-info/controls/more-info-update.ts +++ b/src/dialogs/more-info/controls/more-info-update.ts @@ -448,6 +448,10 @@ class MoreInfoUpdate extends LitElement { box-sizing: border-box; margin-bottom: -16px; margin-top: -4px; + --md-sys-color-surface: var( + --ha-dialog-surface-background, + var(--mdc-theme-surface, #fff) + ); } ha-md-list-item { From 6efe237639d6db3b8ae861d8513d2ad29bbace20 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Feb 2025 12:47:39 +0100 Subject: [PATCH 54/64] Fix label truncated timeline chart (#24077) --- src/components/chart/state-history-chart-timeline.ts | 2 +- src/util/text.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index 39a22c827c..89a59a7513 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -214,7 +214,7 @@ export class StateHistoryChartTimeline extends LitElement { }, axisLabel: { show: showNames, - width: labelWidth - labelMargin, + width: labelWidth, overflow: "truncate", margin: labelMargin, formatter: (id: string) => { diff --git a/src/util/text.ts b/src/util/text.ts index b332c2ec0a..3615c0bc4d 100644 --- a/src/util/text.ts +++ b/src/util/text.ts @@ -21,5 +21,8 @@ export function measureTextWidth( } context.font = `${fontSize}px ${fontFamily}`; - return Math.ceil(context.measureText(text).width); + const textMetrics = context.measureText(text); + return Math.ceil( + textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft + ); } From e50b658db7beb88fe7eb266e99d69fe6088fcba9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Feb 2025 15:16:33 +0100 Subject: [PATCH 55/64] Set min height for graphs, adjust margins (#24078) * Set min height for graphs, adjust margins * stats + header adjustments * set min to 200 --- src/components/chart/ha-chart-base.ts | 2 +- src/components/chart/state-history-charts.ts | 6 +++++- src/panels/lovelace/cards/hui-history-graph-card.ts | 4 +++- src/panels/lovelace/cards/hui-statistics-graph-card.ts | 9 ++++++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 9c9e24c4c1..84d06e9e93 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -496,7 +496,7 @@ export class HaChartBase extends LitElement { } private _getDefaultHeight() { - return Math.max(this.clientWidth / 2, 300); + return Math.max(this.clientWidth / 2, 200); } private _handleZoomReset() { diff --git a/src/components/chart/state-history-charts.ts b/src/components/chart/state-history-charts.ts index 45c767cf04..8deb9694a7 100644 --- a/src/components/chart/state-history-charts.ts +++ b/src/components/chart/state-history-charts.ts @@ -157,7 +157,7 @@ export class StateHistoryCharts extends LitElement { >
`; } - return html`
+ return html`
+ + ${this._config.title + ? html`

${this._config.title}

` + : nothing}
Date: Wed, 5 Feb 2025 13:47:21 +0200 Subject: [PATCH 56/64] Fix device energy bar chart (#24079) --- .../cards/energy/common/energy-chart-options.ts | 4 +++- .../energy/hui-energy-devices-detail-graph-card.ts | 14 ++++++++------ 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 943ade0bb5..2e576c7c9e 100644 --- a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts +++ b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts @@ -199,7 +199,9 @@ export function fillDataGapsAndRoundCaps(datasets: BarSeriesOption[]) { const buckets = Array.from( new Set( datasets - .map((dataset) => dataset.data!.map((datapoint) => datapoint![0])) + .map((dataset) => + dataset.data!.map((datapoint) => Number(datapoint![0])) + ) .flat() ) ).sort((a, b) => a - b); 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 8a609fe507..a1501155b9 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 @@ -327,12 +327,13 @@ export class HuiEnergyDevicesDetailGraphCard const untrackedConsumption: BarSeriesOption["data"] = []; Object.keys(consumptionData.total).forEach((time) => { + const ts = Number(time); const value = consumptionData.total[time] - (totalDeviceConsumption[time] || 0); - const dataPoint: (Date | string | number)[] = [time, value]; + const dataPoint: number[] = [ts, value]; if (compare) { dataPoint[2] = dataPoint[0]; - dataPoint[0] = compareTransform(new Date(time)); + dataPoint[0] = compareTransform(new Date(ts)).getTime(); } untrackedConsumption.push(dataPoint); }); @@ -377,9 +378,10 @@ export class HuiEnergyDevicesDetailGraphCard compare = false ) { const data: BarSeriesOption[] = []; - const compareOffset = compare - ? this._start.getTime() - this._compareStart!.getTime() - : 0; + const compareTransform = getCompareTransform( + this._start, + this._compareStart! + ); devices.forEach((source, idx) => { const order = sorted_devices.indexOf(source.stat_consumption); @@ -414,7 +416,7 @@ export class HuiEnergyDevicesDetailGraphCard const dataPoint = [point.start, point.change]; if (compare) { dataPoint[2] = dataPoint[0]; - dataPoint[0] += compareOffset; + dataPoint[0] = compareTransform(new Date(point.start)).getTime(); } consumptionData.push(dataPoint); prevStart = point.start; From f2e35dc70a30757deb52c06b34c71d6cf7df2e53 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 5 Feb 2025 16:16:16 +0200 Subject: [PATCH 57/64] Fix chart preview (#24080) * Fix chart preview * Revert change to timeline-chart labels --- src/components/chart/ha-chart-base.ts | 1 + src/components/chart/state-history-chart-line.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 84d06e9e93..3aa666f93d 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -524,6 +524,7 @@ export class HaChartBase extends LitElement { :host { display: block; position: relative; + letter-spacing: normal; } .chart-container { position: relative; diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index b2f28c9a48..60006950ac 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -255,8 +255,7 @@ export class StateHistoryChartLine extends LitElement { margin: 5, formatter: (value: number) => { const label = formatNumber(value, this.hass.locale); - // adding 5px extra because preview is not accurate #24027 - const width = measureTextWidth(label, 12) + 5 + 5; + const width = measureTextWidth(label, 12) + 5; if (width > this._yWidth) { this._yWidth = width; fireEvent(this, "y-width-changed", { From d51f8995dd3f6fbd9819ef49c61a985f1a22f58c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Feb 2025 15:04:14 +0100 Subject: [PATCH 58/64] Charts: add styles for legend page controls (#24081) --- src/components/chart/ha-chart-base.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 3aa666f93d..f354a66fbd 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -480,6 +480,11 @@ export class HaChartBase extends LitElement { color: style.getPropertyValue("--primary-text-color"), }, inactiveColor: style.getPropertyValue("--disabled-text-color"), + pageIconColor: style.getPropertyValue("--primary-text-color"), + pageIconInactiveColor: style.getPropertyValue("--disabled-text-color"), + pageTextStyle: { + color: style.getPropertyValue("--secondary-text-color"), + }, }, tooltip: { axisPointer: { From 8f6867f142763a2c1599252e6262c4f9c4745244 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Feb 2025 15:37:13 +0100 Subject: [PATCH 59/64] Chart: Add tooltip styling to theme (#24082) --- src/components/chart/ha-chart-base.ts | 36 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index f354a66fbd..59bac76e9f 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -1,28 +1,28 @@ -import type { PropertyValues } from "lit"; -import { css, html, nothing, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { styleMap } from "lit/directives/style-map"; -import { classMap } from "lit/directives/class-map"; -import { mdiRestart } from "@mdi/js"; -import type { EChartsType } from "echarts/core"; -import type { DataZoomComponentOption } from "echarts/components"; +import { consume } from "@lit-labs/context"; import { ResizeController } from "@lit-labs/observers/resize-controller"; +import { mdiRestart } from "@mdi/js"; +import { differenceInMinutes } from "date-fns"; +import type { DataZoomComponentOption } from "echarts/components"; +import type { EChartsType } from "echarts/core"; import type { ECElementEvent, XAXisOption, YAXisOption, } from "echarts/types/dist/shared"; -import { consume } from "@lit-labs/context"; -import { differenceInMinutes } from "date-fns"; +import type { PropertyValues } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { styleMap } from "lit/directives/style-map"; +import { getAllGraphColors } from "../../common/color/colors"; import { fireEvent } from "../../common/dom/fire_event"; +import { listenMediaQuery } from "../../common/dom/media_query"; +import { themesContext } from "../../data/context"; +import type { Themes } from "../../data/ws-themes"; +import type { ECOption } from "../../resources/echarts"; import type { HomeAssistant } from "../../types"; import { isMac } from "../../util/is_mac"; import "../ha-icon-button"; -import type { ECOption } from "../../resources/echarts"; -import { listenMediaQuery } from "../../common/dom/media_query"; -import type { Themes } from "../../data/ws-themes"; -import { themesContext } from "../../data/context"; -import { getAllGraphColors } from "../../common/color/colors"; import { formatTimeLabel } from "./axis-label"; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; @@ -487,6 +487,12 @@ export class HaChartBase extends LitElement { }, }, tooltip: { + backgroundColor: style.getPropertyValue("--card-background-color"), + borderColor: style.getPropertyValue("--divider-color"), + textStyle: { + color: style.getPropertyValue("--primary-text-color"), + fontSize: 12, + }, axisPointer: { lineStyle: { color: style.getPropertyValue("--divider-color"), From 56539e806513c54eb7081429d7e66a6c89854b93 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Feb 2025 16:02:43 +0100 Subject: [PATCH 60/64] Charts: set tooltip triggerOn to click on mobile (#24083) set tooltip triggerOn to click on mobile --- src/components/chart/ha-chart-base.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 59bac76e9f..9666902885 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -314,6 +314,7 @@ export class HaChartBase extends LitElement { tooltips.forEach((tooltip) => { tooltip.confine = true; tooltip.appendTo = undefined; + tooltip.triggerOn = "click"; }); options.tooltip = tooltips; } From 172d6c30791d6dc84b306d90f9e2fa2b479d343a Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 5 Feb 2025 17:04:03 +0200 Subject: [PATCH 61/64] Disable chart update animation (#24084) --- src/components/chart/state-history-chart-line.ts | 1 + src/components/chart/statistics-chart.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 60006950ac..d268bd5d7c 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -379,6 +379,7 @@ export class StateHistoryChartLine extends LitElement { color, symbol: "circle", step: "end", + animationDurationUpdate: 0, symbolSize: 1, lineStyle: { width: fill ? 0 : 1.5, diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 46c6d7477e..ac9aabf85e 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -308,7 +308,7 @@ export class StatisticsChart extends LitElement { }, grid: { ...(this.hideLegend ? { top: this.unit ? 30 : 5 } : {}), // undefined is the same as 0 - left: 5, + left: 1, right: 1, bottom: 0, containLabel: true, @@ -494,6 +494,7 @@ export class StatisticsChart extends LitElement { ), symbol: "circle", symbolSize: 0, + animationDurationUpdate: 0, lineStyle: { width: 1.5, }, From bd74d39dd8d1690fe28d734ed4714e73cfdb951e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Feb 2025 16:12:48 +0100 Subject: [PATCH 62/64] Use max of width and actualBoundingBox to get text width (#24085) --- src/util/text.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/util/text.ts b/src/util/text.ts index 3615c0bc4d..edd6afdd08 100644 --- a/src/util/text.ts +++ b/src/util/text.ts @@ -23,6 +23,9 @@ export function measureTextWidth( context.font = `${fontSize}px ${fontFamily}`; const textMetrics = context.measureText(text); return Math.ceil( - textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft + Math.max( + textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft, + textMetrics.width + ) ); } From 44dcca9923d37e2509557eff85f3190bf12f4a63 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 3 Feb 2025 12:21:30 +0200 Subject: [PATCH 63/64] Fix chart height (#24028) --- src/components/chart/ha-chart-base.ts | 2 +- src/panels/lovelace/cards/hui-history-graph-card.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 9666902885..4988ff75c8 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -540,7 +540,7 @@ export class HaChartBase extends LitElement { } .chart-container { position: relative; - max-height: var(--chart-max-height, 300px); + max-height: var(--chart-max-height, 350px); } .chart { width: 100%; diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index 818554dfaa..0907d60254 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -260,6 +260,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
${this._error @@ -320,6 +321,9 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { height: 100%; --timeline-top-margin: 16px; } + .has-rows { + --chart-max-height: 100%; + } `; } From f1173dd84b9c31a61176e9348578e97c01120f1d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Feb 2025 16:27:17 +0100 Subject: [PATCH 64/64] Bumped version to 20250205.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 53922f4d91..193a84b4b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250204.0" +version = "20250205.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md"