From aaa95266b6753f589f95f6c116c86e1014bd502e Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 1 Oct 2025 15:12:56 +0300 Subject: [PATCH 01/14] Add pie chart mode to energy devices graph --- .../energy/hui-energy-devices-graph-card.ts | 107 +++++++++++------- 1 file changed, 66 insertions(+), 41 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index 514d9b3c7779..7252b8ba928a 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 @@ -4,7 +4,8 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; -import type { BarSeriesOption } from "echarts/charts"; +import type { BarSeriesOption, PieSeriesOption } from "echarts/charts"; +import { PieChart } from "echarts/charts"; import type { ECElementEvent } from "echarts/types/dist/shared"; import { getGraphColorByIndex } from "../../../../common/color/colors"; import { formatNumber } from "../../../../common/number/format_number"; @@ -26,6 +27,8 @@ import "../../../../components/ha-card"; import { fireEvent } from "../../../../common/dom/fire_event"; import { measureTextWidth } from "../../../../util/text"; +const MAX_PIE_LABELS = 5; + @customElement("hui-energy-devices-graph-card") export class HuiEnergyDevicesGraphCard extends SubscribeMixin(LitElement) @@ -35,10 +38,12 @@ export class HuiEnergyDevicesGraphCard @state() private _config?: EnergyDevicesGraphCardConfig; - @state() private _chartData: BarSeriesOption[] = []; + @state() private _chartData: (BarSeriesOption | PieSeriesOption)[] = []; @state() private _data?: EnergyData; + @state() private _chartType: "bar" | "pie" = "pie"; + protected hassSubscribeRequiredHostProps = ["_config"]; public hassSubscribe(): UnsubscribeFunc[] { @@ -86,9 +91,10 @@ export class HuiEnergyDevicesGraphCard @@ -97,31 +103,44 @@ export class HuiEnergyDevicesGraphCard private _renderTooltip(params: any) { const title = `

${this._getDeviceName( - params.value[1] + params.name )}

`; const value = `${formatNumber( - params.value[0] as number, + params.value as number, this.hass.locale, - params.value[0] < 0.1 ? { maximumFractionDigits: 3 } : undefined + params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined )} kWh`; return `${title}${params.marker} ${params.seriesName}: ${value}`; } - private _createOptions = memoizeOne((data: BarSeriesOption[]): ECOption => { + private _createOptions = memoizeOne((data: (BarSeriesOption | PieSeriesOption)[], chartType: "bar" | "pie"): ECOption => { const isMobile = window.matchMedia( "all and (max-width: 450px), all and (max-height: 500px)" ).matches; - return { - xAxis: { + const options: ECOption = { + grid: { + top: 5, + left: 5, + right: 40, + bottom: 0, + containLabel: true, + }, + tooltip: { + show: true, + formatter: this._renderTooltip.bind(this), + }, + }; + if (chartType === "bar") { + options.xAxis = { type: "value", name: "kWh", - }, - yAxis: { + }; + options.yAxis = { type: "category", inverse: true, triggerEvent: true, // take order from data - data: data[0]?.data?.map((d: any) => d.value[1]), + data: data[0]?.data?.map((d: any) => d.name), axisLabel: { formatter: this._getDeviceName.bind(this), overflow: "truncate", @@ -132,24 +151,14 @@ export class HuiEnergyDevicesGraphCard Math.max( ...(data[0]?.data?.map( (d: any) => - measureTextWidth(this._getDeviceName(d.value[1]), 12) + 5 + measureTextWidth(this._getDeviceName(d.name), 12) + 5 ) || []) ) ), }, - }, - grid: { - top: 5, - left: 5, - right: 40, - bottom: 0, - containLabel: true, - }, - tooltip: { - show: true, - formatter: this._renderTooltip.bind(this), - }, - }; + }; + } + return options; }); private _getDeviceName(statisticId: string): string { @@ -169,12 +178,13 @@ export class HuiEnergyDevicesGraphCard const data = energyData.stats; const compareData = energyData.statsCompare; - const chartData: NonNullable = []; - const chartDataCompare: NonNullable = []; + const chartData: NonNullable<(BarSeriesOption | PieSeriesOption)["data"]> = []; + const chartDataCompare: NonNullable<(BarSeriesOption | PieSeriesOption)["data"]> = []; - const datasets: BarSeriesOption[] = [ + const datasets: (BarSeriesOption | PieSeriesOption)[] = [ { - type: "bar", + type: this._chartType, + radius: ['40%', '70%'], name: this.hass.localize( "ui.panel.lovelace.cards.energy.energy_devices_graph.energy_usage" ), @@ -184,12 +194,15 @@ export class HuiEnergyDevicesGraphCard data: chartData, barWidth: compareData ? 10 : 20, cursor: "default", - }, + label: this._chartType === "pie" ? { + formatter: ({name}) => this._getDeviceName(name), + } : undefined, + } as BarSeriesOption | PieSeriesOption, ]; if (compareData) { datasets.push({ - type: "bar", + type: this._chartType, name: this.hass.localize( "ui.panel.lovelace.cards.energy.energy_devices_graph.previous_energy_usage" ), @@ -199,7 +212,10 @@ export class HuiEnergyDevicesGraphCard data: chartDataCompare, barWidth: 10, cursor: "default", - }); + label: this._chartType === "pie" ? { + formatter: ({name}) => this._getDeviceName(name), + } : undefined, + } as BarSeriesOption | PieSeriesOption); } const computedStyle = getComputedStyle(this); @@ -213,7 +229,8 @@ export class HuiEnergyDevicesGraphCard chartData.push({ id, - value: [value, device.stat_consumption], + value: value, + name: device.stat_consumption, itemStyle: { color: color + "7F", borderColor: color, @@ -230,7 +247,8 @@ export class HuiEnergyDevicesGraphCard chartDataCompare.push({ id, - value: [compareValue, device.stat_consumption], + value: compareValue, + name: device.stat_consumption, itemStyle: { color: color + "32", borderColor: color + "7F", @@ -239,12 +257,19 @@ export class HuiEnergyDevicesGraphCard } }); - chartData.sort((a: any, b: any) => b.value[0] - a.value[0]); - - chartData.length = Math.min( - this._config?.max_devices || Infinity, - chartData.length - ); + datasets.forEach((dataset) => { + dataset.data!.sort((a: any, b: any) => b.value - a.value); + + dataset.data!.length = Math.min( + this._config?.max_devices || Infinity, + dataset.data!.length + ); + if (dataset.data!.length > MAX_PIE_LABELS) { + for (let i = MAX_PIE_LABELS; i < dataset.data!.length; i++) { + (dataset.data![i]! as Record).label = {show: false}; + } + } + }); this._chartData = datasets; await this.updateComplete; From 49f5512dd4ee44da78fc996270347a8b8db28fa3 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 1 Oct 2025 16:18:30 +0300 Subject: [PATCH 02/14] universal transition --- .../energy/hui-energy-devices-graph-card.ts | 166 +++++++++++------- src/translations/en.json | 3 +- 2 files changed, 106 insertions(+), 63 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index 7252b8ba928a..32e45b6fbd87 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 @@ -2,6 +2,7 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { mdiChartDonut, mdiChartBar } from "@mdi/js"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import type { BarSeriesOption, PieSeriesOption } from "echarts/charts"; @@ -26,6 +27,7 @@ import type { ECOption } from "../../../../resources/echarts"; import "../../../../components/ha-card"; import { fireEvent } from "../../../../common/dom/fire_event"; import { measureTextWidth } from "../../../../util/text"; +import "../../../../components/ha-icon-button"; const MAX_PIE_LABELS = 5; @@ -42,7 +44,7 @@ export class HuiEnergyDevicesGraphCard @state() private _data?: EnergyData; - @state() private _chartType: "bar" | "pie" = "pie"; + @state() private _chartType: "bar" | "pie" = "bar"; protected hassSubscribeRequiredHostProps = ["_config"]; @@ -80,9 +82,16 @@ export class HuiEnergyDevicesGraphCard return html` - ${this._config.title - ? html`

${this._config.title}

` - : ""} +
+ ${this._config.title ? this._config.title : nothing} + +
@@ -113,53 +122,62 @@ export class HuiEnergyDevicesGraphCard return `${title}${params.marker} ${params.seriesName}: ${value}`; } - private _createOptions = memoizeOne((data: (BarSeriesOption | PieSeriesOption)[], chartType: "bar" | "pie"): ECOption => { - const isMobile = window.matchMedia( - "all and (max-width: 450px), all and (max-height: 500px)" - ).matches; - const options: ECOption = { - grid: { - top: 5, - left: 5, - right: 40, - bottom: 0, - containLabel: true, - }, - tooltip: { - show: true, - formatter: this._renderTooltip.bind(this), - }, - }; - if (chartType === "bar") { - options.xAxis = { - type: "value", - name: "kWh", - }; - options.yAxis = { - type: "category", - inverse: true, - triggerEvent: true, - // take order from data - data: data[0]?.data?.map((d: any) => d.name), - axisLabel: { - formatter: this._getDeviceName.bind(this), - overflow: "truncate", - fontSize: 12, - margin: 5, - width: Math.min( - isMobile ? 100 : 200, - Math.max( - ...(data[0]?.data?.map( - (d: any) => - measureTextWidth(this._getDeviceName(d.name), 12) + 5 - ) || []) - ) - ), + private _createOptions = memoizeOne( + ( + data: (BarSeriesOption | PieSeriesOption)[], + chartType: "bar" | "pie" + ): ECOption => { + const options: ECOption = { + grid: { + top: 5, + left: 5, + right: 40, + bottom: 0, + containLabel: true, + }, + tooltip: { + show: true, + formatter: this._renderTooltip.bind(this), }, + xAxis: {show: false}, + yAxis: {show: false}, }; + if (chartType === "bar") { + const isMobile = window.matchMedia( + "all and (max-width: 450px), all and (max-height: 500px)" + ).matches; + options.xAxis = { + show: true, + type: "value", + name: "kWh", + }; + options.yAxis = { + show: true, + type: "category", + inverse: true, + triggerEvent: true, + // take order from data + data: data[0]?.data?.map((d: any) => d.name), + axisLabel: { + formatter: this._getDeviceName.bind(this), + overflow: "truncate", + fontSize: 12, + margin: 5, + width: Math.min( + isMobile ? 100 : 200, + Math.max( + ...(data[0]?.data?.map( + (d: any) => + measureTextWidth(this._getDeviceName(d.name), 12) + 5 + ) || []) + ) + ), + }, + }; + } + return options; } - return options; - }); + ); private _getDeviceName(statisticId: string): string { return ( @@ -178,43 +196,54 @@ export class HuiEnergyDevicesGraphCard const data = energyData.stats; const compareData = energyData.statsCompare; - const chartData: NonNullable<(BarSeriesOption | PieSeriesOption)["data"]> = []; - const chartDataCompare: NonNullable<(BarSeriesOption | PieSeriesOption)["data"]> = []; + const chartData: NonNullable<(BarSeriesOption | PieSeriesOption)["data"]> = + []; + const chartDataCompare: NonNullable< + (BarSeriesOption | PieSeriesOption)["data"] + > = []; const datasets: (BarSeriesOption | PieSeriesOption)[] = [ { type: this._chartType, - radius: ['40%', '70%'], + radius: ["40%", "70%"], + universalTransition: true, name: this.hass.localize( "ui.panel.lovelace.cards.energy.energy_devices_graph.energy_usage" ), itemStyle: { - borderRadius: [0, 4, 4, 0], + borderRadius: this._chartType === "bar" ? [0, 4, 4, 0] : 4, }, data: chartData, barWidth: compareData ? 10 : 20, cursor: "default", - label: this._chartType === "pie" ? { - formatter: ({name}) => this._getDeviceName(name), - } : undefined, + label: + this._chartType === "pie" + ? { + formatter: ({ name }) => this._getDeviceName(name), + } + : undefined, } as BarSeriesOption | PieSeriesOption, ]; if (compareData) { datasets.push({ type: this._chartType, + universalTransition: true, name: this.hass.localize( "ui.panel.lovelace.cards.energy.energy_devices_graph.previous_energy_usage" ), itemStyle: { - borderRadius: [0, 4, 4, 0], + borderRadius: this._chartType === "bar" ? [0, 4, 4, 0] : 4, }, data: chartDataCompare, barWidth: 10, cursor: "default", - label: this._chartType === "pie" ? { - formatter: ({name}) => this._getDeviceName(name), - } : undefined, + label: + this._chartType === "pie" + ? { + formatter: ({ name }) => this._getDeviceName(name), + } + : undefined, } as BarSeriesOption | PieSeriesOption); } @@ -259,14 +288,14 @@ export class HuiEnergyDevicesGraphCard datasets.forEach((dataset) => { dataset.data!.sort((a: any, b: any) => b.value - a.value); - + dataset.data!.length = Math.min( this._config?.max_devices || Infinity, dataset.data!.length ); if (dataset.data!.length > MAX_PIE_LABELS) { for (let i = MAX_PIE_LABELS; i < dataset.data!.length; i++) { - (dataset.data![i]! as Record).label = {show: false}; + (dataset.data![i]! as Record).label = { show: false }; } } }); @@ -287,8 +316,16 @@ export class HuiEnergyDevicesGraphCard } } + private _handleChartTypeChange(): void { + this._chartType = this._chartType === "pie" ? "bar" : "pie"; + this._getStatistics(this._data!); + } + static styles = css` .card-header { + display: flex; + justify-content: space-between; + align-items: center; padding-bottom: 0; } .content { @@ -300,6 +337,11 @@ export class HuiEnergyDevicesGraphCard ha-chart-base { --chart-max-height: none; } + ha-icon-button { + transform: rotate(90deg); + color: var(--secondary-text-color); + cursor: pointer; + } `; } diff --git a/src/translations/en.json b/src/translations/en.json index 9b3b0b738b1e..abed66b7e6ae 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -7025,7 +7025,8 @@ }, "energy_devices_graph": { "energy_usage": "Energy usage", - "previous_energy_usage": "Previous energy usage" + "previous_energy_usage": "Previous energy usage", + "change_chart_type": "Change chart type" }, "energy_devices_detail_graph": { "untracked_consumption": "Untracked consumption", From c208431956fa5ee38ae66c9010541a60e8847b86 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 1 Oct 2025 16:25:03 +0300 Subject: [PATCH 03/14] format --- .../lovelace/cards/energy/hui-energy-devices-graph-card.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index 32e45b6fbd87..142852472362 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 @@ -139,8 +139,8 @@ export class HuiEnergyDevicesGraphCard show: true, formatter: this._renderTooltip.bind(this), }, - xAxis: {show: false}, - yAxis: {show: false}, + xAxis: { show: false }, + yAxis: { show: false }, }; if (chartType === "bar") { const isMobile = window.matchMedia( From ed467991cf533f223a72bcc9b0070df5f501d486 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 1 Oct 2025 16:24:10 +0300 Subject: [PATCH 04/14] Add hide_compound_stats option to energy-devices-graph-card (#27263) * Add hide_compound_stats option to energy-devices-graph-card * Update src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts Co-authored-by: Bram Kragten * format --------- Co-authored-by: Bram Kragten --- .../cards/energy/hui-energy-devices-graph-card.ts | 9 +++++++++ src/panels/lovelace/cards/types.ts | 1 + 2 files changed, 10 insertions(+) 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 142852472362..35534cd9b23e 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 @@ -249,7 +249,16 @@ export class HuiEnergyDevicesGraphCard const computedStyle = getComputedStyle(this); + const exclude = this._config?.hide_compound_stats + ? energyData.prefs.device_consumption + .map((d) => d.included_in_stat) + .filter(Boolean) + : []; + energyData.prefs.device_consumption.forEach((device, id) => { + if (exclude.includes(device.stat_consumption)) { + return; + } const value = device.stat_consumption in data ? calculateStatisticSumGrowth(data[device.stat_consumption]) || 0 diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 3ba4ad3b4f88..9d09c7ccdd12 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -176,6 +176,7 @@ export interface EnergyDevicesGraphCardConfig extends EnergyCardBaseConfig { type: "energy-devices-graph"; title?: string; max_devices?: number; + hide_compound_stats?: boolean; } export interface EnergyDevicesDetailGraphCardConfig From 3231316b27b30a30e3cc16c70ae6c19a10251868 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 1 Oct 2025 16:34:43 +0300 Subject: [PATCH 05/14] Save chart type in storage --- .../cards/energy/hui-energy-devices-graph-card.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 35534cd9b23e..cf50585a8b5a 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 @@ -28,6 +28,7 @@ import "../../../../components/ha-card"; import { fireEvent } from "../../../../common/dom/fire_event"; import { measureTextWidth } from "../../../../util/text"; import "../../../../components/ha-icon-button"; +import { storage } from "../../../../common/decorators/storage"; const MAX_PIE_LABELS = 5; @@ -44,7 +45,13 @@ export class HuiEnergyDevicesGraphCard @state() private _data?: EnergyData; - @state() private _chartType: "bar" | "pie" = "bar"; + @state() + @storage({ + key: "energy-devices-graph-chart-type", + state: true, + subscribe: false, + }) + private _chartType: "bar" | "pie" = "bar"; protected hassSubscribeRequiredHostProps = ["_config"]; From 3c15fc32b3a98ca0ff1964cfb8ed7e2b001b85e3 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 2 Oct 2025 10:01:02 +0300 Subject: [PATCH 06/14] show untracked compound energy and total energy --- .../energy/hui-energy-devices-graph-card.ts | 127 +++++++++++++----- src/translations/en.json | 4 +- 2 files changed, 98 insertions(+), 33 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index cf50585a8b5a..9f74d11c05f9 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 @@ -12,7 +12,11 @@ import { getGraphColorByIndex } from "../../../../common/color/colors"; import { formatNumber } from "../../../../common/number/format_number"; import "../../../../components/chart/ha-chart-base"; import type { EnergyData } from "../../../../data/energy"; -import { getEnergyDataCollection } from "../../../../data/energy"; +import { + computeConsumptionData, + getEnergyDataCollection, + getSummedData, +} from "../../../../data/energy"; import { calculateStatisticSumGrowth, getStatisticLabel, @@ -30,8 +34,6 @@ import { measureTextWidth } from "../../../../util/text"; import "../../../../components/ha-icon-button"; import { storage } from "../../../../common/decorators/storage"; -const MAX_PIE_LABELS = 5; - @customElement("hui-energy-devices-graph-card") export class HuiEnergyDevicesGraphCard extends SubscribeMixin(LitElement) @@ -53,6 +55,8 @@ export class HuiEnergyDevicesGraphCard }) private _chartType: "bar" | "pie" = "bar"; + private _compoundStats: string[] = []; + protected hassSubscribeRequiredHostProps = ["_config"]; public hassSubscribe(): UnsubscribeFunc[] { @@ -187,15 +191,18 @@ export class HuiEnergyDevicesGraphCard ); private _getDeviceName(statisticId: string): string { + const suffix = this._compoundStats.includes(statisticId) + ? ` (${this.hass.localize("ui.panel.lovelace.cards.energy.energy_devices_graph.untracked")})` + : ""; return ( - this._data?.prefs.device_consumption.find( + (this._data?.prefs.device_consumption.find( (d) => d.stat_consumption === statisticId )?.name || - getStatisticLabel( - this.hass, - statisticId, - this._data?.statsMetadata[statisticId] - ) + getStatisticLabel( + this.hass, + statisticId, + this._data?.statsMetadata[statisticId] + )) + suffix ); } @@ -212,7 +219,7 @@ export class HuiEnergyDevicesGraphCard const datasets: (BarSeriesOption | PieSeriesOption)[] = [ { type: this._chartType, - radius: ["40%", "70%"], + radius: [compareData ? "50%" : "40%", "70%"], universalTransition: true, name: this.hass.localize( "ui.panel.lovelace.cards.energy.energy_devices_graph.energy_usage" @@ -223,6 +230,7 @@ export class HuiEnergyDevicesGraphCard data: chartData, barWidth: compareData ? 10 : 20, cursor: "default", + minShowLabelAngle: 15, label: this._chartType === "pie" ? { @@ -235,6 +243,7 @@ export class HuiEnergyDevicesGraphCard if (compareData) { datasets.push({ type: this._chartType, + radius: ["30%", "50%"], universalTransition: true, name: this.hass.localize( "ui.panel.lovelace.cards.energy.energy_devices_graph.previous_energy_usage" @@ -245,10 +254,12 @@ export class HuiEnergyDevicesGraphCard data: chartDataCompare, barWidth: 10, cursor: "default", - label: + label: this._chartType === "pie" ? { show: false } : undefined, + emphasis: this._chartType === "pie" ? { - formatter: ({ name }) => this._getDeviceName(name), + focus: "series", + blurScope: "global", } : undefined, } as BarSeriesOption | PieSeriesOption); @@ -256,25 +267,75 @@ export class HuiEnergyDevicesGraphCard const computedStyle = getComputedStyle(this); - const exclude = this._config?.hide_compound_stats - ? energyData.prefs.device_consumption - .map((d) => d.included_in_stat) - .filter(Boolean) - : []; + if (this._chartType === "pie") { + const { summedData } = getSummedData(energyData); + const { consumption } = computeConsumptionData(summedData); + const totalUsed = consumption.total.used_total; + datasets.push({ + type: "pie", + radius: ["0%", compareData ? "30%" : "40%"], + name: this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_devices_graph.total_energy_usage" + ), + data: [totalUsed], + label: { + show: true, + position: "center", + color: computedStyle.getPropertyValue("--secondary-text-color"), + fontSize: computedStyle.getPropertyValue("--ha-font-size-l"), + lineHeight: 24, + fontWeight: "bold", + formatter: `{a}\n${formatNumber(totalUsed, this.hass.locale)} kWh`, + }, + cursor: "default", + itemStyle: { + color: "rgba(0, 0, 0, 0)", + }, + tooltip: { formatter: () => "" }, // `show: false` doesn't hide the previous tooltip + }); + } + + this._compoundStats = energyData.prefs.device_consumption + .map((d) => d.included_in_stat) + .filter(Boolean) as string[]; - energyData.prefs.device_consumption.forEach((device, id) => { - if (exclude.includes(device.stat_consumption)) { - return; - } - const value = + const devices = energyData.prefs.device_consumption; + const devicesTotals: Record = {}; + devices.forEach((device) => { + devicesTotals[device.stat_consumption] = device.stat_consumption in data ? calculateStatisticSumGrowth(data[device.stat_consumption]) || 0 : 0; - const color = getGraphColorByIndex(id, computedStyle); + }); + const devicesTotalsCompare: Record = {}; + if (compareData) { + devices.forEach((device) => { + devicesTotalsCompare[device.stat_consumption] = + device.stat_consumption in compareData + ? calculateStatisticSumGrowth( + compareData[device.stat_consumption] + ) || 0 + : 0; + }); + } + devices.forEach((device, idx) => { + let value = devicesTotals[device.stat_consumption]; + if (!this._config?.hide_compound_stats) { + const childSum = devices.reduce((acc, d) => { + if (d.included_in_stat === device.stat_consumption) { + return acc + devicesTotals[d.stat_consumption]; + } + return acc; + }, 0); + value -= Math.min(value, childSum); + } else if (this._compoundStats.includes(device.stat_consumption)) { + return; + } + const color = getGraphColorByIndex(idx, computedStyle); chartData.push({ - id, - value: value, + id: device.stat_consumption, + value, name: device.stat_consumption, itemStyle: { color: color + "7F", @@ -283,15 +344,22 @@ export class HuiEnergyDevicesGraphCard }); if (compareData) { - const compareValue = + let compareValue = device.stat_consumption in compareData ? calculateStatisticSumGrowth( compareData[device.stat_consumption] ) || 0 : 0; + const compareChildSum = devices.reduce((acc, d) => { + if (d.included_in_stat === device.stat_consumption) { + return acc + devicesTotalsCompare[d.stat_consumption]; + } + return acc; + }, 0); + compareValue -= Math.min(compareValue, compareChildSum); chartDataCompare.push({ - id, + id: device.stat_consumption, value: compareValue, name: device.stat_consumption, itemStyle: { @@ -309,11 +377,6 @@ export class HuiEnergyDevicesGraphCard this._config?.max_devices || Infinity, dataset.data!.length ); - if (dataset.data!.length > MAX_PIE_LABELS) { - for (let i = MAX_PIE_LABELS; i < dataset.data!.length; i++) { - (dataset.data![i]! as Record).label = { show: false }; - } - } }); this._chartData = datasets; diff --git a/src/translations/en.json b/src/translations/en.json index abed66b7e6ae..391d3c3420cc 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -7026,7 +7026,9 @@ "energy_devices_graph": { "energy_usage": "Energy usage", "previous_energy_usage": "Previous energy usage", - "change_chart_type": "Change chart type" + "total_energy_usage": "Total energy usage", + "change_chart_type": "Change chart type", + "untracked": "untracked" }, "energy_devices_detail_graph": { "untracked_consumption": "Untracked consumption", From 2d6d0300b86de50e3bf1c384989d88edbf88a559 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 19:29:17 +0200 Subject: [PATCH 07/14] Update dependency lint-staged to v16.2.3 (#27285) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 8a3950a9fb33..074f6c806dec 100644 --- a/package.json +++ b/package.json @@ -203,7 +203,7 @@ "husky": "9.1.7", "jsdom": "27.0.0", "jszip": "3.10.1", - "lint-staged": "16.2.2", + "lint-staged": "16.2.3", "lit-analyzer": "2.0.3", "lodash.merge": "4.6.2", "lodash.template": "4.5.0", diff --git a/yarn.lock b/yarn.lock index 7072bbf1ce0a..eb5bf22e8a57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9322,7 +9322,7 @@ __metadata: leaflet: "npm:1.9.4" leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch" leaflet.markercluster: "npm:1.5.3" - lint-staged: "npm:16.2.2" + lint-staged: "npm:16.2.3" lit: "npm:3.3.1" lit-analyzer: "npm:2.0.3" lit-html: "npm:3.3.1" @@ -10657,9 +10657,9 @@ __metadata: languageName: node linkType: hard -"lint-staged@npm:16.2.2": - version: 16.2.2 - resolution: "lint-staged@npm:16.2.2" +"lint-staged@npm:16.2.3": + version: 16.2.3 + resolution: "lint-staged@npm:16.2.3" dependencies: commander: "npm:^14.0.1" listr2: "npm:^9.0.4" @@ -10670,7 +10670,7 @@ __metadata: yaml: "npm:^2.8.1" bin: lint-staged: bin/lint-staged.js - checksum: 10/8a6dc0a54fabd095c99ce5962b50df6cee3c8b1191b5e4ff345037a721284cc77532c359e125a0877cf83314a7847b361879da6608e142b22aad2fb32ff66e3a + checksum: 10/7c83cb478aa8004eecc8c91d633abe2865ffc037957ae9ee2669e49b76b76fe3512ba431277efc29cec7a38641e7d8a62f3378a41b624c88bde6fbef5524e2cb languageName: node linkType: hard From 0d3315936eac06ca66934d799dc9f8807bab5d19 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:06:29 +0300 Subject: [PATCH 08/14] Update dependency @codemirror/view to v6.38.4 (#27288) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 074f6c806dec..e247c2b44c01 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@codemirror/legacy-modes": "6.5.1", "@codemirror/search": "6.5.11", "@codemirror/state": "6.5.2", - "@codemirror/view": "6.38.3", + "@codemirror/view": "6.38.4", "@date-fns/tz": "1.4.1", "@egjs/hammerjs": "2.0.17", "@formatjs/intl-datetimeformat": "6.18.0", diff --git a/yarn.lock b/yarn.lock index eb5bf22e8a57..a586328815c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1284,15 +1284,15 @@ __metadata: languageName: node linkType: hard -"@codemirror/view@npm:6.38.3, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0": - version: 6.38.3 - resolution: "@codemirror/view@npm:6.38.3" +"@codemirror/view@npm:6.38.4, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0": + version: 6.38.4 + resolution: "@codemirror/view@npm:6.38.4" dependencies: "@codemirror/state": "npm:^6.5.0" crelt: "npm:^1.0.6" style-mod: "npm:^4.1.0" w3c-keyname: "npm:^2.2.4" - checksum: 10/2df41450399cbac0eaf06dba822418dd6926e48344b9255902248075ef040c957dfe97fe842a755e284a2fd4a66dc17b9638385f46ad74e926baac2e797335a2 + checksum: 10/86b3894e9e7c2113aabb1db8684d0520378339c194fa56a688fc26cd7d40336bb9df1f5f19f68309d95f14b80ecf0b70c0ffe5e43f2ec11c4bab18f2d5ee4494 languageName: node linkType: hard @@ -9187,7 +9187,7 @@ __metadata: "@codemirror/legacy-modes": "npm:6.5.1" "@codemirror/search": "npm:6.5.11" "@codemirror/state": "npm:6.5.2" - "@codemirror/view": "npm:6.38.3" + "@codemirror/view": "npm:6.38.4" "@date-fns/tz": "npm:1.4.1" "@egjs/hammerjs": "npm:2.0.17" "@formatjs/intl-datetimeformat": "npm:6.18.0" From 36daad1a10e8a9854903b68d6c332c33629186ac Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:19:24 -0700 Subject: [PATCH 09/14] Add a sub-editor to hui-entity-editor (#27157) * Add a sub-editor to hui-entity-editor * item styling --- src/panels/lovelace/cards/types.ts | 2 +- .../lovelace/components/hui-entity-editor.ts | 163 +++++++++++++++--- .../hui-generic-entity-row-editor.ts | 5 +- .../config-elements/hui-glance-card-editor.ts | 92 ++++++++++ .../hui-history-graph-card-editor.ts | 53 ++++++ .../lovelace/editor/hui-element-editor.ts | 6 + .../lovelace/editor/hui-sub-element-editor.ts | 3 + .../editor/structs/entities-struct.ts | 2 + src/panels/lovelace/types.ts | 1 + src/translations/en.json | 1 + 10 files changed, 302 insertions(+), 26 deletions(-) diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 9d09c7ccdd12..207ce6c509d9 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -283,7 +283,7 @@ export interface GlanceConfigEntity extends ConfigEntity { image?: string; show_state?: boolean; state_color?: boolean; - format: TimestampRenderingFormat; + format?: TimestampRenderingFormat; } export interface GlanceCardConfig extends LovelaceCardConfig { diff --git a/src/panels/lovelace/components/hui-entity-editor.ts b/src/panels/lovelace/components/hui-entity-editor.ts index 7e77ffa041bf..02f51ac2fe51 100644 --- a/src/panels/lovelace/components/hui-entity-editor.ts +++ b/src/panels/lovelace/components/hui-entity-editor.ts @@ -1,4 +1,4 @@ -import { mdiDrag } from "@mdi/js"; +import { mdiClose, mdiDrag, mdiPencil } from "@mdi/js"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { repeat } from "lit/directives/repeat"; @@ -12,6 +12,7 @@ import "../../../components/ha-icon-button"; import "../../../components/ha-sortable"; import type { HomeAssistant } from "../../../types"; import type { EntityConfig } from "../entity-rows/types"; +import { computeRTL } from "../../../common/util/compute_rtl"; @customElement("hui-entity-editor") export class HuiEntityEditor extends LitElement { @@ -24,6 +25,8 @@ export class HuiEntityEditor extends LitElement { @property() public label?: string; + @property({ attribute: "can-edit", type: Boolean }) public canEdit?; + private _entityKeys = new WeakMap(); private _getKey(action: EntityConfig) { @@ -34,6 +37,70 @@ export class HuiEntityEditor extends LitElement { return this._entityKeys.get(action)!; } + private _renderItem(item: EntityConfig, index: number) { + const stateObj = this.hass!.states[item.entity]; + + const entityName = + stateObj && this.hass!.formatEntityName(stateObj, "entity"); + const deviceName = + stateObj && this.hass!.formatEntityName(stateObj, "device"); + const areaName = stateObj && this.hass!.formatEntityName(stateObj, "area"); + + const isRTL = computeRTL(this.hass!); + + const primary = item.name || entityName || deviceName || item.entity; + const secondary = [areaName, entityName ? deviceName : undefined] + .filter(Boolean) + .join(isRTL ? " ◂ " : " ▸ "); + + return html` + + + +
${primary}
+ ${secondary + ? html`
+ ${secondary} +
` + : nothing} + + +
+ `; + } + + private _editItem(ev) { + const index = (ev.currentTarget as any).index; + fireEvent(this, "edit-detail-element", { + subElementConfig: { + index, + type: "row", + elementConfig: this.entities![index], + }, + }); + } + + private _deleteItem(ev) { + const index = ev.target.index; + const newConfigEntities = this.entities!.slice(0, index).concat( + this.entities!.slice(index + 1) + ); + fireEvent(this, "entities-changed", { entities: newConfigEntities }); + } + protected render() { if (!this.entities) { return nothing; @@ -47,29 +114,48 @@ export class HuiEntityEditor extends LitElement { this.hass!.localize("ui.panel.lovelace.editor.card.config.required") + ")"} - -
- ${repeat( - this.entities, - (entityConf) => this._getKey(entityConf), - (entityConf, index) => html` -
-
- -
- -
- ` - )} -
-
+ ${this.canEdit + ? html` +
+ + + ${this.entities.map((item, index) => + this._renderItem(item, index) + )} + + +
+ ` + : html` +
+ ${repeat( + this.entities, + (entityConf) => this._getKey(entityConf), + (entityConf, index) => html` +
+
+ +
+ +
+ ` + )} +
+
`} ({ + name: action, + selector: { + ui_action: { + default_action: "none" as const, + }, + }, + })), + }, +] as const; + const SCHEMA = [ { name: "title", selector: { text: {} } }, { @@ -68,6 +115,8 @@ export class HuiGlanceCardEditor @state() private _config?: GlanceCardConfig; + @state() private _subElementEditorConfig?: SubElementEditorConfig; + @state() private _configEntities?: ConfigEntity[]; public setConfig(config: GlanceCardConfig): void { @@ -81,6 +130,19 @@ export class HuiGlanceCardEditor return nothing; } + if (this._subElementEditorConfig) { + return html` + + + `; + } + const data = { show_name: true, show_icon: true, @@ -98,12 +160,42 @@ export class HuiGlanceCardEditor > `; } + private _goBack(): void { + this._subElementEditorConfig = undefined; + } + + private _editDetailElement(ev: HASSDomEvent): void { + this._subElementEditorConfig = ev.detail.subElementConfig; + } + + private _handleSubEntityChanged(ev: CustomEvent): void { + ev.stopPropagation(); + + const index = this._subElementEditorConfig!.index!; + + const newEntities = this._configEntities!.concat(); + const newConfig = ev.detail.config as EntityConfig; + this._subElementEditorConfig = { + ...this._subElementEditorConfig!, + elementConfig: newConfig, + }; + newEntities[index] = newConfig; + let config = this._config!; + config = { ...config, entities: newEntities }; + this._config = config; + this._configEntities = processEditorEntities(config.entities); + + fireEvent(this, "config-changed", { config }); + } + private _valueChanged(ev: CustomEvent): void { const config = ev.detail.value; fireEvent(this, "config-changed", { config }); diff --git a/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts index 81ce443d72ab..b7f4e73e29e9 100644 --- a/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts @@ -18,6 +18,9 @@ import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../types"; import type { HistoryGraphCardConfig } from "../../cards/types"; import "../../components/hui-entity-editor"; +import "../hui-sub-element-editor"; +import type { EditDetailElementEvent, SubElementEditorConfig } from "../types"; +import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import type { EntityConfig } from "../../entity-rows/types"; import type { LovelaceCardEditor } from "../../types"; import { processEditorEntities } from "../process-editor-entities"; @@ -40,6 +43,11 @@ const cardConfigStruct = assign( }) ); +const SUB_SCHEMA = [ + { name: "entity", selector: { entity: {} }, required: true }, + { name: "name", selector: { text: {} } }, +] as const; + @customElement("hui-history-graph-card-editor") export class HuiHistoryGraphCardEditor extends LitElement @@ -49,6 +57,8 @@ export class HuiHistoryGraphCardEditor @state() private _config?: HistoryGraphCardConfig; + @state() private _subElementEditorConfig?: SubElementEditorConfig; + @state() private _configEntities?: EntityConfig[]; public setConfig(config: HistoryGraphCardConfig): void { @@ -110,6 +120,19 @@ export class HuiHistoryGraphCardEditor return nothing; } + if (this._subElementEditorConfig) { + return html` + + + `; + } + const schema = this._schema( this._config!.min_y_axis !== undefined || this._config!.max_y_axis !== undefined @@ -126,11 +149,41 @@ export class HuiHistoryGraphCardEditor `; } + private _goBack(): void { + this._subElementEditorConfig = undefined; + } + + private _editDetailElement(ev: HASSDomEvent): void { + this._subElementEditorConfig = ev.detail.subElementConfig; + } + + private _handleSubEntityChanged(ev: CustomEvent): void { + ev.stopPropagation(); + + const index = this._subElementEditorConfig!.index!; + + const newEntities = this._configEntities!.concat(); + const newConfig = ev.detail.config as EntityConfig; + this._subElementEditorConfig = { + ...this._subElementEditorConfig!, + elementConfig: newConfig, + }; + newEntities[index] = newConfig; + let config = this._config!; + config = { ...config, entities: newEntities }; + this._config = config; + this._configEntities = processEditorEntities(config.entities); + + fireEvent(this, "config-changed", { config }); + } + private _valueChanged(ev: CustomEvent): void { fireEvent(this, "config-changed", { config: ev.detail.value }); } diff --git a/src/panels/lovelace/editor/hui-element-editor.ts b/src/panels/lovelace/editor/hui-element-editor.ts index b8a21bb35f93..1f59e8f02ee1 100644 --- a/src/panels/lovelace/editor/hui-element-editor.ts +++ b/src/panels/lovelace/editor/hui-element-editor.ts @@ -57,6 +57,8 @@ export abstract class HuiElementEditor< @property({ attribute: false }) public context?: C; + @property({ attribute: false }) public schema?; + @state() private _config?: T; @state() private _configElement?: LovelaceGenericElementEditor; @@ -312,6 +314,9 @@ export abstract class HuiElementEditor< if (this._configElement && changedProperties.has("context")) { this._configElement.context = this.context; } + if (this._configElement && changedProperties.has("schema")) { + this._configElement.schema = this.schema; + } } private _handleUIConfigChanged(ev: UIConfigChangedEvent) { @@ -399,6 +404,7 @@ export abstract class HuiElementEditor< configElement.lovelace = this.lovelace; } configElement.context = this.context; + configElement.schema = this.schema; configElement.addEventListener("config-changed", (ev) => this._handleUIConfigChanged(ev as UIConfigChangedEvent) ); diff --git a/src/panels/lovelace/editor/hui-sub-element-editor.ts b/src/panels/lovelace/editor/hui-sub-element-editor.ts index eec8aa019ced..a6ff5e6a61e5 100644 --- a/src/panels/lovelace/editor/hui-sub-element-editor.ts +++ b/src/panels/lovelace/editor/hui-sub-element-editor.ts @@ -27,6 +27,8 @@ export class HuiSubElementEditor extends LitElement { @property({ attribute: false }) public config!: SubElementEditorConfig; + @property({ attribute: false }) public schema?; + @state() private _guiModeAvailable = true; @state() private _guiMode = true; @@ -89,6 +91,7 @@ export class HuiSubElementEditor extends LitElement { .hass=${this.hass} .value=${this.config.elementConfig} .context=${this.config.context} + .schema=${this.schema} @config-changed=${this._handleConfigChanged} @GUImode-changed=${this._handleGUIModeChanged} > diff --git a/src/panels/lovelace/editor/structs/entities-struct.ts b/src/panels/lovelace/editor/structs/entities-struct.ts index 870e097b85f3..c0bb7aa4a7df 100644 --- a/src/panels/lovelace/editor/structs/entities-struct.ts +++ b/src/panels/lovelace/editor/structs/entities-struct.ts @@ -18,6 +18,8 @@ export const entitiesConfigStruct = union([ hold_action: optional(actionConfigStruct), double_tap_action: optional(actionConfigStruct), confirmation: optional(actionConfigStructConfirmation), + show_last_changed: optional(boolean()), + show_state: optional(boolean()), }), string(), ]); diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index ea580f59d898..6cfb3a3b34d2 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -169,6 +169,7 @@ export interface LovelaceGenericElementEditor extends HTMLElement { hass?: HomeAssistant; lovelace?: LovelaceConfig; context?: C; + schema?: any; setConfig(config: any): void; focusYamlEditor?: () => void; } diff --git a/src/translations/en.json b/src/translations/en.json index 391d3c3420cc..58da898c88a7 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -7723,6 +7723,7 @@ "show_icon": "Show icon", "show_name": "Show name", "show_state": "Show state", + "show_last_changed": "Show last changed", "tap_action": "Tap behavior", "interactions": "Interactions", "title": "Title", From 165f265694c326f866e887092ffe577eecc1027f Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 2 Oct 2025 11:43:13 +0300 Subject: [PATCH 10/14] fix compare order --- .../cards/energy/hui-energy-devices-graph-card.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index 9f74d11c05f9..72d3902f780e 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 @@ -126,7 +126,7 @@ export class HuiEnergyDevicesGraphCard params.name )}`; const value = `${formatNumber( - params.value as number, + params.value[0] as number, this.hass.locale, params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined )} kWh`; @@ -335,7 +335,7 @@ export class HuiEnergyDevicesGraphCard chartData.push({ id: device.stat_consumption, - value, + value: [value, device.stat_consumption] as any, name: device.stat_consumption, itemStyle: { color: color + "7F", @@ -360,7 +360,7 @@ export class HuiEnergyDevicesGraphCard chartDataCompare.push({ id: device.stat_consumption, - value: compareValue, + value: [compareValue, device.stat_consumption] as any, name: device.stat_consumption, itemStyle: { color: color + "32", @@ -370,9 +370,9 @@ export class HuiEnergyDevicesGraphCard } }); - datasets.forEach((dataset) => { - dataset.data!.sort((a: any, b: any) => b.value - a.value); + chartData.sort((a: any, b: any) => b.value[0] - a.value[0]); + datasets.forEach((dataset) => { dataset.data!.length = Math.min( this._config?.max_devices || Infinity, dataset.data!.length @@ -384,6 +384,7 @@ export class HuiEnergyDevicesGraphCard } private _handleChartClick(e: CustomEvent): void { + console.log(e); if ( e.detail.targetType === "axisLabel" && e.detail.value && From 046d87d828c8d7265f92c31c495995130dc33293 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 2 Oct 2025 11:51:14 +0300 Subject: [PATCH 11/14] handle label click in pie chart --- .../cards/energy/hui-energy-devices-graph-card.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 72d3902f780e..e2807a8c17eb 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 @@ -384,7 +384,6 @@ export class HuiEnergyDevicesGraphCard } private _handleChartClick(e: CustomEvent): void { - console.log(e); if ( e.detail.targetType === "axisLabel" && e.detail.value && @@ -393,6 +392,13 @@ export class HuiEnergyDevicesGraphCard fireEvent(this, "hass-more-info", { entityId: e.detail.value as string, }); + } else if ( + e.detail.seriesType === "pie" && + e.detail.event?.target?.type === "tspan" // label + ) { + fireEvent(this, "hass-more-info", { + entityId: (e.detail.data as any).id as string, + }); } } From 9f40b344ec4c2b9506165fa639a647415c716c02 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 2 Oct 2025 12:12:53 +0300 Subject: [PATCH 12/14] order compare data based on current data --- .../lovelace/cards/energy/hui-energy-devices-graph-card.ts | 3 +++ 1 file changed, 3 insertions(+) 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 e2807a8c17eb..fa6517ae9720 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 @@ -371,6 +371,9 @@ export class HuiEnergyDevicesGraphCard }); chartData.sort((a: any, b: any) => b.value[0] - a.value[0]); + datasets[1].data = chartData.map((d) => + chartDataCompare.find((d2) => (d2 as any).id === d.id) + ) as typeof chartDataCompare; datasets.forEach((dataset) => { dataset.data!.length = Math.min( From 1d45c6c3fa87fa8c41b37405fca3551a712f7950 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 2 Oct 2025 12:31:13 +0300 Subject: [PATCH 13/14] show untracked energy in tooltip --- .../energy/hui-energy-devices-graph-card.ts | 80 ++++++++++++------- src/translations/en.json | 3 +- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index fa6517ae9720..4a4e0db8ad45 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 @@ -267,34 +267,6 @@ export class HuiEnergyDevicesGraphCard const computedStyle = getComputedStyle(this); - if (this._chartType === "pie") { - const { summedData } = getSummedData(energyData); - const { consumption } = computeConsumptionData(summedData); - const totalUsed = consumption.total.used_total; - datasets.push({ - type: "pie", - radius: ["0%", compareData ? "30%" : "40%"], - name: this.hass.localize( - "ui.panel.lovelace.cards.energy.energy_devices_graph.total_energy_usage" - ), - data: [totalUsed], - label: { - show: true, - position: "center", - color: computedStyle.getPropertyValue("--secondary-text-color"), - fontSize: computedStyle.getPropertyValue("--ha-font-size-l"), - lineHeight: 24, - fontWeight: "bold", - formatter: `{a}\n${formatNumber(totalUsed, this.hass.locale)} kWh`, - }, - cursor: "default", - itemStyle: { - color: "rgba(0, 0, 0, 0)", - }, - tooltip: { formatter: () => "" }, // `show: false` doesn't hide the previous tooltip - }); - } - this._compoundStats = energyData.prefs.device_consumption .map((d) => d.included_in_stat) .filter(Boolean) as string[]; @@ -371,9 +343,11 @@ export class HuiEnergyDevicesGraphCard }); chartData.sort((a: any, b: any) => b.value[0] - a.value[0]); - datasets[1].data = chartData.map((d) => - chartDataCompare.find((d2) => (d2 as any).id === d.id) - ) as typeof chartDataCompare; + if (compareData) { + datasets[1].data = chartData.map((d) => + chartDataCompare.find((d2) => (d2 as any).id === d.id) + ) as typeof chartDataCompare; + } datasets.forEach((dataset) => { dataset.data!.length = Math.min( @@ -382,6 +356,50 @@ export class HuiEnergyDevicesGraphCard ); }); + if (this._chartType === "pie") { + const { summedData } = getSummedData(energyData); + const { consumption } = computeConsumptionData(summedData); + const totalUsed = consumption.total.used_total; + const showUntracked = + "from_grid" in summedData || + "solar" in summedData || + "from_battery" in summedData; + const untracked = showUntracked + ? totalUsed - + chartData.reduce((acc: number, d: any) => acc + d.value[0], 0) + : 0; + datasets.push({ + type: "pie", + radius: ["0%", compareData ? "30%" : "40%"], + name: this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_devices_graph.total_energy_usage" + ), + data: [totalUsed], + label: { + show: true, + position: "center", + color: computedStyle.getPropertyValue("--secondary-text-color"), + fontSize: computedStyle.getPropertyValue("--ha-font-size-l"), + lineHeight: 24, + fontWeight: "bold", + formatter: `{a}\n${formatNumber(totalUsed, this.hass.locale)} kWh`, + }, + cursor: "default", + itemStyle: { + color: "rgba(0, 0, 0, 0)", + }, + tooltip: { + formatter: () => + untracked > 0 + ? this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_devices_graph.includes_untracked", + { num: formatNumber(untracked, this.hass.locale) } + ) + : "", + }, + }); + } + this._chartData = datasets; await this.updateComplete; } diff --git a/src/translations/en.json b/src/translations/en.json index 58da898c88a7..92699d6e2d69 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -7028,7 +7028,8 @@ "previous_energy_usage": "Previous energy usage", "total_energy_usage": "Total energy usage", "change_chart_type": "Change chart type", - "untracked": "untracked" + "untracked": "untracked", + "includes_untracked": "Includes {num} kWh of untracked energy" }, "energy_devices_detail_graph": { "untracked_consumption": "Untracked consumption", From e8866f2f561f393faf894e4e6a8217fa282350a4 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 14 Oct 2025 14:23:23 +0300 Subject: [PATCH 14/14] Apply suggestions from code review Co-authored-by: Bram Kragten --- .../lovelace/cards/energy/hui-energy-devices-graph-card.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index f081915cc52d..8962eed28a78 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 @@ -97,8 +97,8 @@ export class HuiEnergyDevicesGraphCard
${this._config.title ? this._config.title : nothing}