1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-24 12:49:19 +00:00

Update Energy dashboard layout (#28283)

This commit is contained in:
Petar Petrov
2025-12-02 17:01:17 +02:00
committed by GitHub
parent 0d51648de1
commit 3d327ed628
11 changed files with 252 additions and 232 deletions

View File

@@ -1,11 +1,10 @@
import { mdiDownload, mdiPencil } from "@mdi/js";
import { mdiDownload } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { goBack, navigate } from "../../common/navigate";
import { navigate } from "../../common/navigate";
import "../../components/ha-alert";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-list-item";
import "../../components/ha-menu-button";
import "../../components/ha-top-app-bar-fixed";
import type {
@@ -23,16 +22,14 @@ import {
getSummedData,
} from "../../data/energy";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import {
isStrategyView,
type LovelaceViewConfig,
} from "../../data/lovelace/config/view";
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
import type { StatisticValue } from "../../data/recorder";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, PanelInfo } from "../../types";
import { fileDownload } from "../../util/file_download";
import "../lovelace/components/hui-energy-period-selector";
import "../lovelace/hui-root";
import type { ExtraActionItem } from "../lovelace/hui-root";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
@@ -51,37 +48,38 @@ const OVERVIEW_VIEW = {
strategy: {
type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
allow_compare: false,
},
} as LovelaceViewConfig;
const ENERGY_VIEW = {
path: "electricity",
back_path: "/energy",
strategy: {
type: "energy",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
allow_compare: true,
},
} as LovelaceViewConfig;
const WATER_VIEW = {
back_path: "/energy",
path: "water",
strategy: {
type: "water",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
allow_compare: true,
},
} as LovelaceViewConfig;
const GAS_VIEW = {
path: "gas",
strategy: {
type: "gas",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
} as LovelaceViewConfig;
const POWER_VIEW = {
back_path: "/energy",
path: "power",
path: "now",
strategy: {
type: "power",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
allow_compare: false,
collection_key: "energy_dashboard_now",
},
} as LovelaceViewConfig;
@@ -101,8 +99,6 @@ class PanelEnergy extends LitElement {
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
@property({ attribute: false }) public route?: {
path: string;
prefix: string;
@@ -114,6 +110,16 @@ class PanelEnergy extends LitElement {
@state()
private _error?: string;
private get _extraActionItems(): ExtraActionItem[] {
return [
{
icon: mdiDownload,
labelKey: "ui.panel.energy.download_data",
action: this._dumpCSV,
},
];
}
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
// Initial setup
@@ -188,16 +194,11 @@ class PanelEnergy extends LitElement {
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
setEditMode: () => this._navigateConfig(),
showToast: () => undefined,
};
}
private _back(ev) {
ev.stopPropagation();
goBack();
}
protected render() {
if (this._error) {
return html`
@@ -222,17 +223,6 @@ class PanelEnergy extends LitElement {
return nothing;
}
const viewPath: string | undefined = this.route!.path.split("/")[1];
const views = this._lovelace.config?.views || [];
const viewIndex = Math.max(
views.findIndex((view) => view.path === viewPath),
0
);
const view = views[viewIndex];
const showBack = this._searchParms.has("historyBack") || viewIndex > 0;
return html`
<hui-root
.hass=${this.hass}
@@ -240,60 +230,9 @@ class PanelEnergy extends LitElement {
.lovelace=${this._lovelace}
.route=${this.route}
.panel=${this.panel}
.extraActionItems=${this._extraActionItems}
@reload-energy-panel=${this._reloadConfig}
>
<div class="toolbar" slot="toolbar">
${showBack
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${!this.narrow
? html`<div class="main-title">
${this.hass.localize(
`ui.panel.energy.title.${viewPath}` as LocalizeKeys
) || this.hass.localize("panel.energy")}
</div>`
: nothing}
<hui-energy-period-selector
.hass=${this.hass}
.collectionKey=${DEFAULT_ENERGY_COLLECTION_KEY}
.allowCompare=${isStrategyView(view) && view.strategy.allow_compare}
>
${this.hass.user?.is_admin
? html`
<ha-list-item
slot="overflow-menu"
graphic="icon"
@request-selected=${this._navigateConfig}
>
<ha-svg-icon slot="graphic" .path=${mdiPencil}>
</ha-svg-icon>
${this.hass!.localize("ui.panel.energy.configure")}
</ha-list-item>
`
: nothing}
<ha-list-item
slot="overflow-menu"
graphic="icon"
@request-selected=${this._dumpCSV}
>
<ha-svg-icon slot="graphic" .path=${mdiDownload}> </ha-svg-icon>
${this.hass!.localize("ui.panel.energy.download_data")}
</ha-list-item>
</hui-energy-period-selector>
</div>
</hui-root>
></hui-root>
`;
}
@@ -325,29 +264,44 @@ class PanelEnergy extends LitElement {
this._prefs.energy_sources.some((source) => source.type === "water") ||
this._prefs.device_consumption_water?.length > 0;
const hasGas = this._prefs.energy_sources.some(
(source) => source.type === "gas"
);
const views: LovelaceViewConfig[] = [];
if (hasEnergy) {
views.push(ENERGY_VIEW);
}
if (hasPower) {
views.push(POWER_VIEW);
if (hasGas) {
views.push(GAS_VIEW);
}
if (hasWater) {
views.push(WATER_VIEW);
}
if (hasPower) {
views.push(POWER_VIEW);
}
if (views.length > 1) {
views.unshift(OVERVIEW_VIEW);
}
return { views };
return {
views: views.map((view) => ({
...view,
title:
view.title ||
this.hass.localize(
`ui.panel.energy.title.${view.path}` as LocalizeKeys
),
})),
};
}
private _navigateConfig(ev) {
ev.stopPropagation();
private _navigateConfig(ev?: Event) {
ev?.stopPropagation();
navigate("/config/energy?historyBack=1");
}
private async _dumpCSV(ev) {
ev.stopPropagation();
private _dumpCSV = async () => {
const energyData = getEnergyDataCollection(this.hass, {
key: "energy_dashboard",
});
@@ -659,7 +613,7 @@ class PanelEnergy extends LitElement {
});
const url = window.URL.createObjectURL(blob);
fileDownload(url, "energy.csv");
}
};
private _reloadConfig() {
this._loadConfig();
@@ -669,21 +623,8 @@ class PanelEnergy extends LitElement {
return [
haStyle,
css`
:host hui-energy-period-selector {
flex-grow: 1;
padding-left: 32px;
padding-inline-start: 32px;
padding-inline-end: initial;
--disabled-text-color: rgba(var(--rgb-text-primary-color), 0.5);
direction: var(--direction);
--date-range-picker-max-height: calc(100vh - 80px);
}
:host([narrow]) hui-energy-period-selector {
padding-left: 0px;
padding-inline-start: 0px;
padding-inline-end: initial;
}
:host {
--ha-view-sections-column-max-width: 100%;
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;

View File

@@ -17,7 +17,7 @@ const sourceHasCost = (source: Record<string, any>): boolean =>
);
@customElement("energy-overview-view-strategy")
export class EnergyViewStrategy extends ReactiveElement {
export class EnergyOverviewViewStrategy extends ReactiveElement {
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
@@ -55,6 +55,9 @@ export class EnergyViewStrategy extends ReactiveElement {
const hasBattery = prefs.energy_sources.some(
(source) => source.type === "battery"
);
const hasSolar = prefs.energy_sources.some(
(source) => source.type === "solar"
);
const hasWaterSources = prefs.energy_sources.some(
(source) => source.type === "water"
);
@@ -65,9 +68,6 @@ export class EnergyViewStrategy extends ReactiveElement {
(source.type === "battery" && source.stat_rate) ||
(source.type === "grid" && source.power?.length)
);
const hasPowerDevices = prefs.device_consumption.find(
(device) => device.stat_rate
);
const hasCost = prefs.energy_sources.some(
(source) =>
sourceHasCost(source) ||
@@ -78,94 +78,67 @@ export class EnergyViewStrategy extends ReactiveElement {
const overviewSection: LovelaceSectionConfig = {
type: "grid",
column_span: 24,
cards: [],
cards: [
{
type: "energy-date-selection",
collection_key: collectionKey,
allow_compare: false,
},
],
};
// Only include if we have a grid or battery.
if (hasGrid || hasBattery) {
if (hasGrid || hasBattery || hasSolar) {
overviewSection.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_distribution_title"),
type: "energy-distribution",
collection_key: collectionKey,
});
}
if (hasCost) {
overviewSection.cards!.push({
type: "energy-sources-table",
collection_key: collectionKey,
show_only_totals: true,
});
}
view.sections!.push(overviewSection);
const powerSection: LovelaceSectionConfig = {
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.energy.title.power"),
tap_action: {
action: "navigate",
navigation_path: "/energy/power",
if (hasCost) {
view.sections!.push({
type: "grid",
cards: [
{
title: hass.localize(
"ui.panel.energy.cards.energy_sources_table_title"
),
type: "energy-sources-table",
collection_key: collectionKey,
show_only_totals: true,
},
},
],
};
if (hasPowerDevices) {
const showFloorsNAreas = !prefs.device_consumption.some(
(d) => d.included_in_stat
);
powerSection.cards!.push({
title: hass.localize("ui.panel.energy.cards.power_sankey_title"),
type: "power-sankey",
collection_key: collectionKey,
group_by_floor: showFloorsNAreas,
group_by_area: showFloorsNAreas,
grid_options: {
columns: 24,
},
],
});
powerSection.column_span = 24;
view.sections!.push(powerSection);
} else if (hasPowerSources) {
powerSection.cards!.push({
title: hass.localize("ui.panel.energy.cards.power_sources_graph_title"),
type: "power-sources-graph",
collection_key: collectionKey,
});
view.sections!.push(powerSection);
}
const energySection: LovelaceSectionConfig = {
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.energy.title.energy"),
tap_action: {
action: "navigate",
navigation_path: "/energy/electricity",
if (hasPowerSources) {
view.sections!.push({
type: "grid",
cards: [
{
title: hass.localize(
"ui.panel.energy.cards.power_sources_graph_title"
),
type: "power-sources-graph",
collection_key: collectionKey,
show_legend: false,
},
},
],
};
view.sections!.push(energySection);
if (hasGrid || hasBattery) {
energySection.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_usage_graph_title"),
type: "energy-usage-graph",
collection_key: "energy_dashboard",
],
});
}
if (prefs!.device_consumption.length > 3) {
energySection.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_top_consumers_title"
),
type: "energy-devices-graph",
collection_key: collectionKey,
max_devices: 3,
modes: ["bar"],
if (hasGrid || hasBattery) {
view.sections!.push({
type: "grid",
cards: [
{
title: hass.localize(
"ui.panel.energy.cards.energy_usage_graph_title"
),
type: "energy-usage-graph",
collection_key: "energy_dashboard",
},
],
});
}
@@ -173,10 +146,6 @@ export class EnergyViewStrategy extends ReactiveElement {
view.sections!.push({
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.energy.title.gas"),
},
{
title: hass.localize(
"ui.panel.energy.cards.energy_gas_graph_title"
@@ -192,14 +161,6 @@ export class EnergyViewStrategy extends ReactiveElement {
view.sections!.push({
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.energy.title.water"),
tap_action: {
action: "navigate",
navigation_path: "/energy/water",
},
},
hasWaterSources
? {
title: hass.localize(
@@ -225,6 +186,6 @@ export class EnergyViewStrategy extends ReactiveElement {
declare global {
interface HTMLElementTagNameMap {
"energy-overview-view-strategy": EnergyViewStrategy;
"energy-overview-view-strategy": EnergyOverviewViewStrategy;
}
}

View File

@@ -50,6 +50,10 @@ export class EnergyViewStrategy extends ReactiveElement {
(d) => d.included_in_stat
);
view.cards!.push({
type: "energy-date-selection",
collection_key: collectionKey,
});
view.cards!.push({
type: "energy-compare",
collection_key: "energy_dashboard",
@@ -133,11 +137,11 @@ export class EnergyViewStrategy extends ReactiveElement {
// Only include if we have at least 1 device in the config.
if (prefs.device_consumption.length) {
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_sankey_title"),
type: "energy-sankey",
title: hass.localize(
"ui.panel.energy.cards.energy_devices_detail_graph_title"
),
type: "energy-devices-detail-graph",
collection_key: "energy_dashboard",
group_by_floor: showFloorsNAreas,
group_by_area: showFloorsNAreas,
});
view.cards!.push({
title: hass.localize(
@@ -147,11 +151,11 @@ export class EnergyViewStrategy extends ReactiveElement {
collection_key: "energy_dashboard",
});
view.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_devices_detail_graph_title"
),
type: "energy-devices-detail-graph",
title: hass.localize("ui.panel.energy.cards.energy_sankey_title"),
type: "energy-sankey",
collection_key: "energy_dashboard",
group_by_floor: showFloorsNAreas,
group_by_area: showFloorsNAreas,
});
}

View File

@@ -0,0 +1,70 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getEnergyDataCollection } from "../../../data/energy";
import type { HomeAssistant } from "../../../types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
@customElement("gas-view-strategy")
export class GasViewStrategy extends ReactiveElement {
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = {
type: "sections",
sections: [{ type: "grid", cards: [] }],
};
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
});
const prefs = energyCollection.prefs;
const hasGasSources = prefs?.energy_sources.some(
(source) => source.type === "gas"
);
// No gas sources available
if (!prefs || !hasGasSources) {
return view;
}
const section = view.sections![0] as LovelaceSectionConfig;
section.cards!.push({
type: "energy-date-selection",
collection_key: collectionKey,
});
section.cards!.push({
type: "energy-compare",
collection_key: collectionKey,
});
section.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_gas_graph_title"),
type: "energy-gas-graph",
collection_key: collectionKey,
});
section.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_sources_table_title"),
type: "energy-sources-table",
collection_key: collectionKey,
types: ["gas"],
});
return view;
}
}
declare global {
interface HTMLElementTagNameMap {
"gas-view-strategy": GasViewStrategy;
}
}

View File

@@ -5,6 +5,7 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
@customElement("power-view-strategy")
export class PowerViewStrategy extends ReactiveElement {
@@ -12,7 +13,10 @@ export class PowerViewStrategy extends ReactiveElement {
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = { cards: [] };
const view: LovelaceViewConfig = {
type: "sections",
sections: [{ type: "grid", cards: [] }],
};
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
@@ -20,6 +24,7 @@ export class PowerViewStrategy extends ReactiveElement {
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
});
await energyCollection.refresh();
const prefs = energyCollection.prefs;
const hasPowerSources = prefs?.energy_sources.some(
@@ -37,31 +42,32 @@ export class PowerViewStrategy extends ReactiveElement {
return view;
}
view.type = "sidebar";
const section = view.sections![0] as LovelaceSectionConfig;
view.cards!.push({
type: "energy-compare",
collection_key: collectionKey,
});
if (hasPowerSources) {
section.cards!.push({
title: hass.localize("ui.panel.energy.cards.power_sources_graph_title"),
type: "power-sources-graph",
collection_key: collectionKey,
grid_options: {
columns: 36,
},
});
}
if (hasPowerDevices) {
const showFloorsNAreas = !prefs.device_consumption.some(
(d) => d.included_in_stat
);
view.cards!.push({
section.cards!.push({
title: hass.localize("ui.panel.energy.cards.power_sankey_title"),
type: "power-sankey",
collection_key: collectionKey,
group_by_floor: showFloorsNAreas,
group_by_area: showFloorsNAreas,
});
}
if (hasPowerSources) {
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.power_sources_graph_title"),
type: "power-sources-graph",
collection_key: collectionKey,
grid_options: {
columns: 36,
},
});
}

View File

@@ -5,6 +5,7 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
@customElement("water-view-strategy")
export class WaterViewStrategy extends ReactiveElement {
@@ -12,7 +13,10 @@ export class WaterViewStrategy extends ReactiveElement {
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = { cards: [] };
const view: LovelaceViewConfig = {
type: "sections",
sections: [{ type: "grid", cards: [] }],
};
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
@@ -32,15 +36,19 @@ export class WaterViewStrategy extends ReactiveElement {
return view;
}
view.type = "sidebar";
const section = view.sections![0] as LovelaceSectionConfig;
view.cards!.push({
section.cards!.push({
type: "energy-date-selection",
collection_key: collectionKey,
});
section.cards!.push({
type: "energy-compare",
collection_key: collectionKey,
});
if (hasWaterSources) {
view.cards!.push({
section.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_water_graph_title"),
type: "energy-water-graph",
collection_key: collectionKey,
@@ -48,7 +56,7 @@ export class WaterViewStrategy extends ReactiveElement {
}
if (hasWaterSources) {
view.cards!.push({
section.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_sources_table_title"
),
@@ -63,7 +71,7 @@ export class WaterViewStrategy extends ReactiveElement {
const showFloorsNAreas = !prefs.device_consumption_water.some(
(d) => d.included_in_stat
);
view.cards!.push({
section.cards!.push({
title: hass.localize("ui.panel.energy.cards.water_sankey_title"),
type: "water-sankey",
collection_key: collectionKey,

View File

@@ -132,7 +132,7 @@ export class HuiPowerSourcesGraphCard
compareEnd
),
legend: {
show: true,
show: this._config?.show_legend !== false,
type: "custom",
data: legendData,
},

View File

@@ -235,6 +235,7 @@ export interface WaterSankeyCardConfig extends EnergyCardBaseConfig {
export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
type: "power-sources-graph";
title?: string;
show_legend?: boolean;
}
export interface PowerSankeyCardConfig extends EnergyCardBaseConfig {

View File

@@ -112,6 +112,12 @@ interface ActionItem {
subItems?: SubActionItem[];
}
export interface ExtraActionItem {
icon: string;
labelKey: LocalizeKeys;
action: () => void;
}
interface SubActionItem {
icon: string;
key: LocalizeKeys;
@@ -140,6 +146,8 @@ class HUIRoot extends LitElement {
prefix: string;
};
@property({ attribute: false }) public extraActionItems?: ExtraActionItem[];
@state() private _curView?: number | "hass-unused-entities";
private _configChangedByUndo = false;
@@ -347,6 +355,25 @@ class HUIRoot extends LitElement {
},
];
// Add extra action items from parent components
if (this.extraActionItems) {
this.extraActionItems.forEach((extraItem) => {
items.push({
icon: extraItem.icon,
key: extraItem.labelKey,
buttonAction: extraItem.action,
overflowAction: (ev: CustomEvent<RequestSelectedDetail>) => {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
extraItem.action();
},
visible: true,
overflow: this.narrow,
});
});
}
const overflowItems = items.filter((i) => i.visible && i.overflow);
const overflowCanPromote =
overflowItems.length === 1 && overflowItems[0].overflow_can_promote;

View File

@@ -42,6 +42,7 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
import("../../energy/strategies/energy-overview-view-strategy"),
energy: () => import("../../energy/strategies/energy-view-strategy"),
water: () => import("../../energy/strategies/water-view-strategy"),
gas: () => import("../../energy/strategies/gas-view-strategy"),
power: () => import("../../energy/strategies/power-view-strategy"),
map: () => import("./map/map-view-strategy"),
iframe: () => import("./iframe/iframe-view-strategy"),

View File

@@ -9578,10 +9578,11 @@
},
"energy": {
"title": {
"energy": "Energy",
"power": "Power",
"overview": "Summary",
"electricity": "Energy",
"gas": "Gas",
"water": "Water"
"water": "Water",
"now": "Now"
},
"download_data": "[%key:ui::panel::history::download_data%]",
"configure": "[%key:ui::dialogs::quick-bar::commands::navigation::energy%]",