From 8c8adc4c5044b2657da9a29e5d881924f3368c21 Mon Sep 17 00:00:00 2001 From: karwosts Date: Fri, 6 Dec 2024 06:14:31 -0800 Subject: [PATCH] Support for hierarchy of individual energy devices --- src/components/chart/ha-chart-base.ts | 1 + src/data/energy.ts | 1 + .../dialogs/dialog-energy-device-settings.ts | 20 +++++ .../hui-energy-devices-detail-graph-card.ts | 84 ++++++++++++++++--- 4 files changed, 96 insertions(+), 10 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 049f4b6091ed..80f4511af898 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -32,6 +32,7 @@ interface Tooltip export interface ChartDatasetExtra { show_legend?: boolean; legend_label?: string; + id?: string; } @customElement("ha-chart-base") diff --git a/src/data/energy.ts b/src/data/energy.ts index 5e082ac675fc..2a1c49859f42 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -98,6 +98,7 @@ export interface DeviceConsumptionEnergyPreference { // This is an ever increasing value stat_consumption: string; name?: string; + parent_stat?: string; } export interface FlowFromGridSourceEnergyPreference { diff --git a/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts index 3852ea6038fb..bdf29178b059 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts @@ -108,6 +108,15 @@ export class DialogEnergyDeviceSettings > + + + ${this.hass.localize("ui.common.cancel")} @@ -141,6 +150,17 @@ export class DialogEnergyDeviceSettings this._device = newDevice; } + private _parentChanged(ev) { + const newDevice = { + ...this._device!, + parent_stat: ev.target!.value, + } as DeviceConsumptionEnergyPreference; + if (!newDevice.parent_stat) { + delete newDevice.parent_stat; + } + this._device = newDevice; + } + private async _save() { try { await this._params!.saveCallback(this._device!); 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 9e2dfd92b975..271ed258742c 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 @@ -232,6 +232,16 @@ export class HuiEnergyDevicesDetailGraphCard const computedStyle = getComputedStyle(this); + const devices = energyData.prefs.device_consumption; + + const childMap: Record = {}; + devices.forEach((d) => { + if (d.parent_stat) { + childMap[d.parent_stat] = childMap[d.parent_stat] || []; + childMap[d.parent_stat].push(d.stat_consumption); + } + }); + const growthValues = {}; energyData.prefs.device_consumption.forEach((device) => { const value = @@ -247,6 +257,22 @@ export class HuiEnergyDevicesDetailGraphCard ); sorted_devices.sort((a, b) => growthValues[b] - growthValues[a]); + const ordered_devices: string[] = []; + + // Recursively build an ordered list of devices, where each device has all its children immediately following it. + function orderDevices(parent?: string) { + sorted_devices.forEach((device) => { + const parent_stat = energyData.prefs.device_consumption.find( + (prf) => prf.stat_consumption === device + )?.parent_stat; + if ((!parent && !parent_stat) || parent === parent_stat) { + ordered_devices.push(device); + orderDevices(device); + } + }); + } + orderDevices(); + const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = []; const datasetExtras: ChartDatasetExtra[] = []; @@ -256,7 +282,8 @@ export class HuiEnergyDevicesDetailGraphCard data, energyData.statsMetadata, energyData.prefs.device_consumption, - sorted_devices + ordered_devices, + childMap ); datasets.push(...processedData); @@ -283,7 +310,9 @@ export class HuiEnergyDevicesDetailGraphCard this._processUntracked( computedStyle, processedData, + processedDataExtras, consumptionData, + energyData.prefs.device_consumption, false ); datasets.push(untrackedData); @@ -316,7 +345,8 @@ export class HuiEnergyDevicesDetailGraphCard compareData, energyData.statsMetadata, energyData.prefs.device_consumption, - sorted_devices, + ordered_devices, + childMap, true ); @@ -330,7 +360,9 @@ export class HuiEnergyDevicesDetailGraphCard } = this._processUntracked( computedStyle, processedCompareData, + processedCompareDataExtras, consumptionCompareData, + energyData.prefs.device_consumption, true ); datasets.push(untrackedCompareData); @@ -353,16 +385,27 @@ export class HuiEnergyDevicesDetailGraphCard private _processUntracked( computedStyle: CSSStyleDeclaration, processedData, + processedDataExtras, consumptionData, + deviceConsumptionPrefs, compare: boolean ): { dataset; datasetExtra } { const totalDeviceConsumption: { [start: number]: number } = {}; - processedData.forEach((device) => { - device.data.forEach((datapoint) => { - totalDeviceConsumption[datapoint.x] = - (totalDeviceConsumption[datapoint.x] || 0) + datapoint.y; - }); + processedData.forEach((device, idx) => { + const stat = deviceConsumptionPrefs.find( + (pref) => pref.stat_consumption === processedDataExtras[idx].id + ); + + // If a child is hidden, don't count it in the total, because the parent device will grow to encompass that consumption. + const hiddenChild = + stat.parent_stat && this._hiddenStats.includes(stat.stat_consumption); + if (!hiddenChild) { + device.data.forEach((datapoint) => { + totalDeviceConsumption[datapoint.x] = + (totalDeviceConsumption[datapoint.x] || 0) + datapoint.y; + }); + } }); const untrackedConsumption: { x: number; y: number }[] = []; @@ -409,6 +452,7 @@ export class HuiEnergyDevicesDetailGraphCard statisticsMetaData: Record, devices: DeviceConsumptionEnergyPreference[], sorted_devices: string[], + childMap: Record, compare = false ) { const data: ChartDataset<"bar", ScatterDataPoint[]>[] = []; @@ -421,7 +465,7 @@ export class HuiEnergyDevicesDetailGraphCard const consumptionData: ScatterDataPoint[] = []; - // Process gas consumption data. + // Process device consumption data. if (source.stat_consumption in statistics) { const stats = statistics[source.stat_consumption]; let end; @@ -434,9 +478,26 @@ export class HuiEnergyDevicesDetailGraphCard continue; } const date = new Date(point.start); + + let sumChildren = 0; + const sumVisibleChildren = (parent) => { + const children = childMap[parent] || []; + children.forEach((c) => { + if (this._hiddenStats.includes(c)) { + sumVisibleChildren(c); + } else { + const cStats = statistics[c]; + sumChildren += + cStats?.find((cStat) => cStat.start === point.start) + ?.change || 0; + } + }); + }; + sumVisibleChildren(source.stat_consumption); + consumptionData.push({ x: date.getTime(), - y: point.change, + y: point.change - sumChildren, }); prevStart = point.start; end = point.end; @@ -472,7 +533,10 @@ export class HuiEnergyDevicesDetailGraphCard pointStyle: compare ? false : "circle", xAxisID: compare ? "xAxisCompare" : undefined, }); - dataExtras.push({ show_legend: !compare && !itemExceedsMax }); + dataExtras.push({ + show_legend: !compare && !itemExceedsMax, + id: source.stat_consumption, + }); }); return { data, dataExtras }; }