Use the new included_in_stat hierarchy in the Energy Sankey card (#25306)

This commit is contained in:
Petar Petrov 2025-05-08 18:01:18 +03:00 committed by GitHub
parent 4d2d94c54f
commit 6692d9c6aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 100 additions and 10 deletions

View File

@ -105,10 +105,41 @@ export class HaSankeyChart extends LitElement {
private _createData = memoizeOne((data: SankeyChartData, width = 0) => { private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
const filteredNodes = data.nodes.filter((n) => n.value > 0); const filteredNodes = data.nodes.filter((n) => n.value > 0);
const indexes = [...new Set(filteredNodes.map((n) => n.index))]; const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
const depthMap = new Map<number, number>(); const depthMap = new Map<number, number>();
indexes.sort().forEach((index, i) => { const sections: Node[][] = [];
indexes.forEach((index, i) => {
depthMap.set(index, i); depthMap.set(index, i);
const nodesWithIndex = filteredNodes.filter((n) => n.index === index);
if (nodesWithIndex.length > 0) {
sections.push(
sections.length > 0
? nodesWithIndex.sort((a, b) => {
// sort by the order of their parents in the previous section with orphans at the end
const aParentIndex = this._findParentIndex(
a.id,
data.links,
sections
);
const bParentIndex = this._findParentIndex(
b.id,
data.links,
sections
);
if (aParentIndex === bParentIndex) {
return 0;
}
if (aParentIndex === -1) {
return 1;
}
if (bParentIndex === -1) {
return -1;
}
return aParentIndex - bParentIndex;
})
: nodesWithIndex
);
}
}); });
const links = this._processLinks(filteredNodes, data.links); const links = this._processLinks(filteredNodes, data.links);
const sectionWidth = width / indexes.length; const sectionWidth = width / indexes.length;
@ -117,7 +148,7 @@ export class HaSankeyChart extends LitElement {
return { return {
id: "sankey", id: "sankey",
type: "sankey", type: "sankey",
nodes: filteredNodes.map((node) => ({ nodes: sections.flat().map((node) => ({
id: node.id, id: node.id,
value: node.value, value: node.value,
itemStyle: { itemStyle: {
@ -227,6 +258,23 @@ export class HaSankeyChart extends LitElement {
return links; return links;
} }
private _findParentIndex(id: string, links: Link[], sections: Node[][]) {
const parent = links.find((l) => l.target === id)?.source;
if (!parent) {
return -1;
}
let offset = 0;
for (let i = sections.length - 1; i >= 0; i--) {
const section = sections[i];
const index = section.findIndex((n) => n.id === parent);
if (index !== -1) {
return offset + index;
}
offset += section.length;
}
return -1;
}
static styles = css` static styles = css`
:host { :host {
display: block; display: block;

View File

@ -227,6 +227,7 @@ class HuiEnergySankeyCard
let untrackedConsumption = homeNode.value; let untrackedConsumption = homeNode.value;
const deviceNodes: Node[] = []; const deviceNodes: Node[] = [];
const parentLinks: Record<string, string> = {};
prefs.device_consumption.forEach((device, idx) => { prefs.device_consumption.forEach((device, idx) => {
const value = const value =
device.stat_consumption in this._data!.stats device.stat_consumption in this._data!.stats
@ -238,7 +239,7 @@ class HuiEnergySankeyCard
return; return;
} }
untrackedConsumption -= value; untrackedConsumption -= value;
deviceNodes.push({ const node = {
id: device.stat_consumption, id: device.stat_consumption,
label: label:
device.name || device.name ||
@ -251,12 +252,24 @@ class HuiEnergySankeyCard
tooltip: `${formatNumber(value, this.hass.locale)} kWh`, tooltip: `${formatNumber(value, this.hass.locale)} kWh`,
color: getGraphColorByIndex(idx, computedStyle), color: getGraphColorByIndex(idx, computedStyle),
index: 4, index: 4,
parent: device.included_in_stat,
};
if (node.parent) {
parentLinks[node.id] = node.parent;
links.push({
source: node.parent,
target: node.id,
}); });
}
deviceNodes.push(node);
}); });
const devicesWithoutParent = deviceNodes.filter(
(node) => !parentLinks[node.id]
);
const { group_by_area, group_by_floor } = this._config; const { group_by_area, group_by_floor } = this._config;
if (group_by_area || group_by_floor) { if (group_by_area || group_by_floor) {
const { areas, floors } = this._groupByFloorAndArea(deviceNodes); const { areas, floors } = this._groupByFloorAndArea(devicesWithoutParent);
Object.keys(floors) Object.keys(floors)
.sort( .sort(
@ -310,7 +323,6 @@ class HuiEnergySankeyCard
// Link devices to the appropriate target (area, floor, or home) // Link devices to the appropriate target (area, floor, or home)
areas[areaId].devices.forEach((device) => { areas[areaId].devices.forEach((device) => {
nodes.push(device);
links.push({ links.push({
source: targetNodeId, source: targetNodeId,
target: device.id, target: device.id,
@ -320,8 +332,7 @@ class HuiEnergySankeyCard
}); });
}); });
} else { } else {
deviceNodes.forEach((deviceNode) => { devicesWithoutParent.forEach((deviceNode) => {
nodes.push(deviceNode);
links.push({ links.push({
source: "home", source: "home",
target: deviceNode.id, target: deviceNode.id,
@ -329,6 +340,12 @@ class HuiEnergySankeyCard
}); });
}); });
} }
const deviceSections = this._getDeviceSections(parentLinks, deviceNodes);
deviceSections.forEach((section, index) => {
section.forEach((node: Node) => {
nodes.push({ ...node, index: 4 + index });
});
});
// untracked consumption // untracked consumption
if (untrackedConsumption > 0) { if (untrackedConsumption > 0) {
@ -340,7 +357,7 @@ class HuiEnergySankeyCard
value: untrackedConsumption, value: untrackedConsumption,
tooltip: `${formatNumber(untrackedConsumption, this.hass.locale)} kWh`, tooltip: `${formatNumber(untrackedConsumption, this.hass.locale)} kWh`,
color: computedStyle.getPropertyValue("--state-unavailable-color"), color: computedStyle.getPropertyValue("--state-unavailable-color"),
index: 4, index: 3 + deviceSections.length,
}); });
links.push({ links.push({
source: "home", source: "home",
@ -428,6 +445,31 @@ class HuiEnergySankeyCard
return { areas, floors }; return { areas, floors };
} }
protected _getDeviceSections(
parentLinks: Record<string, string>,
deviceNodes: Node[]
): Node[][] {
const parentSection: Node[] = [];
const childSection: Node[] = [];
const parentIds = Object.values(parentLinks);
const remainingLinks: typeof parentLinks = {};
deviceNodes.forEach((deviceNode) => {
if (parentIds.includes(deviceNode.id)) {
parentSection.push(deviceNode);
remainingLinks[deviceNode.id] = parentLinks[deviceNode.id];
} else {
childSection.push(deviceNode);
}
});
if (parentSection.length > 0) {
return [
...this._getDeviceSections(remainingLinks, parentSection),
childSection,
];
}
return [deviceNodes];
}
static styles = css` static styles = css`
:host { :host {
display: block; display: block;