diff --git a/src/components/chart/ha-sankey-chart.ts b/src/components/chart/ha-sankey-chart.ts index 10ede23c15..e9f9e79712 100644 --- a/src/components/chart/ha-sankey-chart.ts +++ b/src/components/chart/ha-sankey-chart.ts @@ -2,9 +2,13 @@ import { customElement, property, state } from "lit/decorators"; import { LitElement, html, css } from "lit"; import type { EChartsType } from "echarts/core"; import type { SankeySeriesOption } from "echarts/types/dist/echarts"; -import type { CallbackDataParams } from "echarts/types/src/util/types"; +import type { + CallbackDataParams, + ECElementEvent, +} from "echarts/types/src/util/types"; import memoizeOne from "memoize-one"; import { ResizeController } from "@lit-labs/observers/resize-controller"; +import { fireEvent } from "../../common/dom/fire_event"; import SankeyChart from "../../resources/echarts/components/sankey/install"; import type { HomeAssistant } from "../../types"; import type { ECOption } from "../../resources/echarts/echarts"; @@ -21,6 +25,7 @@ export interface Node { label?: string; color?: string; passThrough?: boolean; + entityId?: string; } export interface Link { source: string; @@ -83,6 +88,7 @@ export class HaSankeyChart extends LitElement { .options=${options} height="100%" .extraComponents=${[SankeyChart]} + @chart-click=${this._handleChartClick} >`; } @@ -103,6 +109,22 @@ export class HaSankeyChart extends LitElement { return null; }; + private _handleChartClick = (ev: CustomEvent) => { + const detail = ev.detail; + // Only handle node clicks (not links) + if (detail.dataType !== "node") { + return; + } + const nodeId = (detail.data as Record)?.id; + if (!nodeId) { + return; + } + const node = this.data.nodes.find((n) => n.id === nodeId); + if (node?.entityId) { + fireEvent(this, "node-click", { node }); + } + }; + 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))].sort(); @@ -294,4 +316,7 @@ declare global { interface HTMLElementTagNameMap { "ha-sankey-chart": HaSankeyChart; } + interface HASSDomEvents { + "node-click": { node: Node }; + } } 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 7b6919e7e9..c65beaa3d1 100644 --- a/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts +++ b/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts @@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import "../../../../components/ha-card"; import "../../../../components/ha-svg-icon"; +import { fireEvent } from "../../../../common/dom/fire_event"; import type { EnergyData, EnergyPreferences } from "../../../../data/energy"; import { formatPowerShort, @@ -326,6 +327,7 @@ class HuiPowerSankeyCard color: getGraphColorByIndex(idx, computedStyle), index: 4, parent: effectiveParent, + entityId: device.stat_rate, }; if (node.parent) { parentLinks[node.id] = node.parent; @@ -461,6 +463,7 @@ class HuiPowerSankeyCard .data=${{ nodes, links }} .vertical=${vertical} .valueFormatter=${this._valueFormatter} + @node-click=${this._handleNodeClick} >` : html`${this.hass.localize( "ui.panel.lovelace.cards.energy.no_data" @@ -475,6 +478,13 @@ class HuiPowerSankeyCard ${formatPowerShort(this.hass, value)} `; + private _handleNodeClick(ev: CustomEvent<{ node: Node }>) { + const { node } = ev.detail; + if (node.entityId) { + fireEvent(this, "hass-more-info", { entityId: node.entityId }); + } + } + /** * Compute real-time power data from current entity states. * Similar to computeConsumptionData but for instantaneous power. diff --git a/test/panels/lovelace/cards/energy/common/hui-power-sankey-card.test.ts b/test/panels/lovelace/cards/energy/common/hui-power-sankey-card.test.ts new file mode 100644 index 0000000000..4cb8768e79 --- /dev/null +++ b/test/panels/lovelace/cards/energy/common/hui-power-sankey-card.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from "vitest"; +import type { Node } from "../../../../../../src/components/chart/ha-sankey-chart"; + +describe("hui-power-sankey-card", () => { + describe("node click handling", () => { + it("should identify device nodes as clickable via entityId", () => { + const nodes: Node[] = [ + { id: "solar", value: 1000, index: 0, label: "Solar" }, + { id: "home", value: 1500, index: 1, label: "Home" }, + { + id: "sensor.device1", + value: 200, + index: 4, + label: "Device 1", + entityId: "sensor.device1", + }, + ]; + + const clickableNodes = nodes.filter((n) => n.entityId); + expect(clickableNodes).toHaveLength(1); + expect(clickableNodes[0].entityId).toBe("sensor.device1"); + }); + + it("should not make source/area/floor nodes clickable", () => { + const nodes: Node[] = [ + { id: "solar", value: 1000, index: 0 }, + { id: "grid", value: 500, index: 0 }, + { id: "home", value: 1500, index: 1 }, + { id: "floor_1", value: 800, index: 2 }, + { id: "area_kitchen", value: 400, index: 3 }, + ]; + + const clickableNodes = nodes.filter((n) => n.entityId); + expect(clickableNodes).toHaveLength(0); + }); + }); +});