Grouping options for hui-energy-sankey-card (#25207)

* Grouping options for hui-energy-sankey-card

* use getEntityContext for area/floor

* Update rspack monorepo to v1.3.7 (#25206)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Update vaadinWebComponents monorepo to v24.7.4 (#25204)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Add typography styles (#25171)

* Add typoghrapy styles

* Split styles

* Fix duplicated html

* remove unused paper vars

* Fix vars

* Add vars autocompletion extension

* Fix css syntax highlighting

* History tooltip RTL fix (#24917)

* History tooltip RTL fix

* Fix background color

* Render todo items with no state, change look of read only items (#24529)

* Render todo items with no state, change look of read only items

Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Wendelin <w@pe8.at>

* Themeable badge icon size and badge font size (#25185)

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

* Add context to config update entries (#25208)

* Add context to config update entries

* Add no area

* import fix

* import fix

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Yosi Levy <37745463+yosilevy@users.noreply.github.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Wendelin <w@pe8.at>
Co-authored-by: emufan <emufan@users.noreply.github.com>
This commit is contained in:
Petar Petrov 2025-04-30 11:00:51 +03:00 committed by GitHub
parent 5d2d6dcd6c
commit ded5ade0f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 135 additions and 101 deletions

View File

@ -106,6 +106,10 @@ 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 depthMap = new Map<number, number>();
indexes.sort().forEach((index, i) => {
depthMap.set(index, i);
});
const links = this._processLinks(filteredNodes, data.links);
const sectionWidth = width / indexes.length;
const labelSpace = sectionWidth - NODE_SIZE - LABEL_DISTANCE;
@ -119,7 +123,7 @@ export class HaSankeyChart extends LitElement {
itemStyle: {
color: node.color,
},
depth: node.index,
depth: depthMap.get(node.index),
})),
links,
draggable: false,

View File

@ -22,6 +22,12 @@ import "../../../../components/chart/ha-sankey-chart";
import type { Link, Node } from "../../../../components/chart/ha-sankey-chart";
import { getGraphColorByIndex } from "../../../../common/color/colors";
import { formatNumber } from "../../../../common/number/format_number";
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
const DEFAULT_CONFIG: Partial<EnergySankeyCardConfig> = {
group_by_floor: true,
group_by_area: true,
};
@customElement("hui-energy-sankey-card")
class HuiEnergySankeyCard
@ -37,7 +43,7 @@ class HuiEnergySankeyCard
protected hassSubscribeRequiredHostProps = ["_config"];
public setConfig(config: EnergySankeyCardConfig): void {
this._config = config;
this._config = { ...DEFAULT_CONFIG, ...config };
}
public hassSubscribe(): UnsubscribeFunc[] {
@ -219,22 +225,9 @@ class HuiEnergySankeyCard
homeNode.value -= totalToGrid;
}
// Group devices by areas and floors
const areas: Record<string, { value: number; devices: Node[] }> = {
no_area: {
value: 0,
devices: [],
},
};
const floors: Record<string, { value: number; areas: string[] }> = {
no_floor: {
value: 0,
areas: ["no_area"],
},
};
let untrackedConsumption = homeNode.value;
const deviceNodes: Node[] = [];
prefs.device_consumption.forEach((device, idx) => {
const entity = this.hass.entities[device.stat_consumption];
const value =
device.stat_consumption in this._data!.stats
? calculateStatisticSumGrowth(
@ -245,7 +238,7 @@ class HuiEnergySankeyCard
return;
}
untrackedConsumption -= value;
const deviceNode: Node = {
deviceNodes.push({
id: device.stat_consumption,
label:
device.name ||
@ -258,47 +251,12 @@ class HuiEnergySankeyCard
tooltip: `${formatNumber(value, this.hass.locale)} kWh`,
color: getGraphColorByIndex(idx, computedStyle),
index: 4,
};
const entityAreaId =
entity?.area_id ??
(entity?.device_id && this.hass.devices[entity.device_id]?.area_id);
if (entityAreaId && entityAreaId in this.hass.areas) {
const area = this.hass.areas[entityAreaId];
if (area.area_id in areas) {
areas[area.area_id].value += deviceNode.value;
areas[area.area_id].devices.push(deviceNode);
} else {
areas[area.area_id] = {
value: deviceNode.value,
devices: [deviceNode],
};
}
// see if the area has a floor
if (area.floor_id && area.floor_id in this.hass.floors) {
if (area.floor_id in floors) {
floors[area.floor_id].value += deviceNode.value;
if (!floors[area.floor_id].areas.includes(area.area_id)) {
floors[area.floor_id].areas.push(area.area_id);
}
} else {
floors[area.floor_id] = {
value: deviceNode.value,
areas: [area.area_id],
};
}
} else {
floors.no_floor.value += deviceNode.value;
if (!floors.no_floor.areas.includes(area.area_id)) {
floors.no_floor.areas.unshift(area.area_id);
}
}
} else {
areas.no_area.value += deviceNode.value;
areas.no_area.devices.push(deviceNode);
}
});
});
const { group_by_area, group_by_floor } = this._config;
if (group_by_area || group_by_floor) {
const { areas, floors } = this._groupByFloorAndArea(deviceNodes);
Object.keys(floors)
.sort(
@ -308,7 +266,7 @@ class HuiEnergySankeyCard
)
.forEach((floorId) => {
let floorNodeId = `floor_${floorId}`;
if (floorId === "no_floor") {
if (floorId === "no_floor" || !group_by_floor) {
// link "no_floor" areas to home
floorNodeId = "home";
} else {
@ -326,11 +284,14 @@ class HuiEnergySankeyCard
});
}
floors[floorId].areas.forEach((areaId) => {
let areaNodeId = `area_${areaId}`;
if (areaId === "no_area") {
// link "no_area" devices to home
areaNodeId = "home";
let targetNodeId: string;
if (areaId === "no_area" || !group_by_area) {
// If group_by_area is false, link devices to floor or home
targetNodeId = floorNodeId;
} else {
// Create area node and link it to floor
const areaNodeId = `area_${areaId}`;
nodes.push({
id: areaNodeId,
label: this.hass.areas[areaId]!.name,
@ -344,17 +305,31 @@ class HuiEnergySankeyCard
target: areaNodeId,
value: areas[areaId].value,
});
targetNodeId = areaNodeId;
}
// Link devices to the appropriate target (area, floor, or home)
areas[areaId].devices.forEach((device) => {
nodes.push(device);
links.push({
source: areaNodeId,
source: targetNodeId,
target: device.id,
value: device.value,
});
});
});
});
} else {
deviceNodes.forEach((deviceNode) => {
nodes.push(deviceNode);
links.push({
source: "home",
target: deviceNode.id,
value: deviceNode.value,
});
});
}
// untracked consumption
if (untrackedConsumption > 0) {
nodes.push({
@ -400,6 +375,59 @@ class HuiEnergySankeyCard
private _valueFormatter = (value: number) =>
`${formatNumber(value, this.hass.locale)} kWh`;
protected _groupByFloorAndArea(deviceNodes: Node[]) {
const areas: Record<string, { value: number; devices: Node[] }> = {
no_area: {
value: 0,
devices: [],
},
};
const floors: Record<string, { value: number; areas: string[] }> = {
no_floor: {
value: 0,
areas: ["no_area"],
},
};
deviceNodes.forEach((deviceNode) => {
const entity = this.hass.states[deviceNode.id];
const { area, floor } = getEntityContext(entity, this.hass);
if (area) {
if (area.area_id in areas) {
areas[area.area_id].value += deviceNode.value;
areas[area.area_id].devices.push(deviceNode);
} else {
areas[area.area_id] = {
value: deviceNode.value,
devices: [deviceNode],
};
}
// see if the area has a floor
if (floor) {
if (floor.floor_id in floors) {
floors[floor.floor_id].value += deviceNode.value;
if (!floors[floor.floor_id].areas.includes(area.area_id)) {
floors[floor.floor_id].areas.push(area.area_id);
}
} else {
floors[floor.floor_id] = {
value: deviceNode.value,
areas: [area.area_id],
};
}
} else {
floors.no_floor.value += deviceNode.value;
if (!floors.no_floor.areas.includes(area.area_id)) {
floors.no_floor.areas.unshift(area.area_id);
}
}
} else {
areas.no_area.value += deviceNode.value;
areas.no_area.devices.push(deviceNode);
}
});
return { areas, floors };
}
static styles = css`
:host {
display: block;

View File

@ -201,6 +201,8 @@ export interface EnergySankeyCardConfig extends EnergyCardBaseConfig {
type: "energy-sankey";
title?: string;
layout?: "vertical" | "horizontal";
group_by_floor?: boolean;
group_by_area?: boolean;
}
export interface EntityFilterCardConfig extends LovelaceCardConfig {