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) => {
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>();
indexes.sort().forEach((index, i) => {
const sections: Node[][] = [];
indexes.forEach((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 sectionWidth = width / indexes.length;
@ -117,7 +148,7 @@ export class HaSankeyChart extends LitElement {
return {
id: "sankey",
type: "sankey",
nodes: filteredNodes.map((node) => ({
nodes: sections.flat().map((node) => ({
id: node.id,
value: node.value,
itemStyle: {
@ -227,6 +258,23 @@ export class HaSankeyChart extends LitElement {
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`
:host {
display: block;

View File

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