From 0a9dccfd19546c4667b2c51c9d411e7895fef09a Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 27 Nov 2025 13:21:55 +0200 Subject: [PATCH] Refactor power sankey hierarchy to handle devices with not power sensor (#28164) --- .../cards/energy/hui-power-sankey-card.ts | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts b/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts index d6e04e1c96..8190c357c6 100644 --- a/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts +++ b/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts @@ -23,6 +23,9 @@ const DEFAULT_CONFIG: Partial = { group_by_area: true, }; +// Minimum power threshold in kW to display a device node +const MIN_POWER_THRESHOLD = 0.01; + interface PowerData { solar: number; from_grid: number; @@ -251,23 +254,75 @@ class HuiPowerSankeyCard let untrackedConsumption = homeNode.value; const deviceNodes: Node[] = []; const parentLinks: Record = {}; + + // Build a map of device relationships for hierarchy resolution + // Key: stat_consumption (energy), Value: { stat_rate, included_in_stat } + const deviceMap = new Map< + string, + { stat_rate?: string; included_in_stat?: string } + >(); + prefs.device_consumption.forEach((device) => { + deviceMap.set(device.stat_consumption, { + stat_rate: device.stat_rate, + included_in_stat: device.included_in_stat, + }); + }); + + // Set of stat_rate entities that will be rendered as nodes + const renderedStatRates = new Set(); + prefs.device_consumption.forEach((device) => { + if (device.stat_rate) { + const value = this._getCurrentPower(device.stat_rate); + if (value >= MIN_POWER_THRESHOLD) { + renderedStatRates.add(device.stat_rate); + } + } + }); + + // Find the effective parent for power hierarchy + // Walks up the chain to find an ancestor with stat_rate that will be rendered + const findEffectiveParent = ( + includedInStat: string | undefined + ): string | undefined => { + let currentParent = includedInStat; + while (currentParent) { + const parentDevice = deviceMap.get(currentParent); + if (!parentDevice) { + return undefined; + } + // If this parent has a stat_rate and will be rendered, use it + if ( + parentDevice.stat_rate && + renderedStatRates.has(parentDevice.stat_rate) + ) { + return parentDevice.stat_rate; + } + // Otherwise, continue up the chain + currentParent = parentDevice.included_in_stat; + } + return undefined; + }; + prefs.device_consumption.forEach((device, idx) => { if (!device.stat_rate) { return; } const value = this._getCurrentPower(device.stat_rate); - if (value < 0.01) { + if (value < MIN_POWER_THRESHOLD) { return; } + // Find the effective parent (may be different from direct parent if parent has no stat_rate) + const effectiveParent = findEffectiveParent(device.included_in_stat); + const node = { id: device.stat_rate, label: device.name || this._getEntityLabel(device.stat_rate), value, color: getGraphColorByIndex(idx, computedStyle), index: 4, - parent: device.included_in_stat, + parent: effectiveParent, }; if (node.parent) { parentLinks[node.id] = node.parent;