1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-20 02:38:53 +00:00

Split Energy panel into overview and electricity view (#27534)

This commit is contained in:
Petar Petrov
2025-11-19 15:11:27 +02:00
committed by GitHub
parent 20d0548d33
commit f8923ed648
8 changed files with 425 additions and 112 deletions

View File

@@ -1,4 +1,4 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { mdiPencil, mdiDownload } from "@mdi/js";
import { customElement, property, state } from "lit/decorators";
@@ -6,6 +6,7 @@ import "../../components/ha-menu-button";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-list-item";
import "../../components/ha-top-app-bar-fixed";
import "../../components/ha-alert";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
@@ -21,6 +22,7 @@ import type {
GasSourceTypeEnergyPreference,
WaterSourceTypeEnergyPreference,
DeviceConsumptionEnergyPreference,
EnergyCollection,
} from "../../data/energy";
import {
computeConsumptionData,
@@ -30,13 +32,28 @@ import {
import { fileDownload } from "../../util/file_download";
import type { StatisticValue } from "../../data/recorder";
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
const ENERGY_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "energy",
type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
},
{
strategy: {
type: "energy-electricity",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
path: "electricity",
},
{
type: "panel",
path: "setup",
cards: [{ type: "custom:energy-setup-wizard-card" }],
},
],
};
@@ -46,13 +63,30 @@ class PanelEnergy extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _viewIndex = 0;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
public willUpdate(changedProps: PropertyValues) {
@state() private _error?: string;
@property({ attribute: false }) public route?: {
path: string;
prefix: string;
};
private _energyCollection?: EnergyCollection;
get _viewPath(): string | undefined {
const viewPath: string | undefined = this.route!.path.split("/")[1];
return viewPath ? decodeURI(viewPath) : undefined;
}
public connectedCallback() {
super.connectedCallback();
this._loadPrefs();
}
public async willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
}
@@ -62,22 +96,71 @@ class PanelEnergy extends LitElement {
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
}
if (oldHass && oldHass.localize !== this.hass.localize) {
} else if (oldHass && oldHass.localize !== this.hass.localize) {
this._reloadView();
}
}
private async _loadPrefs() {
if (this._viewPath === "setup") {
await import("./cards/energy-setup-wizard-card");
} else {
this._energyCollection = getEnergyDataCollection(this.hass, {
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try {
// Have to manually refresh here as we don't want to subscribe yet
await this._energyCollection.refresh();
} catch (err: any) {
if (err.code === "not_found") {
navigate("/energy/setup");
}
this._error = err.message;
return;
}
const prefs = this._energyCollection.prefs!;
if (
prefs.device_consumption.length === 0 &&
prefs.energy_sources.length === 0
) {
// No energy sources available, start from scratch
navigate("/energy/setup");
}
}
}
private _back(ev) {
ev.stopPropagation();
goBack();
}
protected render(): TemplateResult {
protected render() {
if (!this._energyCollection?.prefs) {
// Still loading
return html`<div class="centered">
<ha-spinner size="large"></ha-spinner>
</div>`;
}
const { prefs } = this._energyCollection;
const isSingleView = prefs.energy_sources.every((source) =>
["grid", "solar", "battery"].includes(source.type)
);
let viewPath = this._viewPath;
if (isSingleView) {
// if only electricity sources, show electricity view directly
viewPath = "electricity";
}
const viewIndex = Math.max(
ENERGY_LOVELACE_CONFIG.views.findIndex((view) => view.path === viewPath),
0
);
const showBack =
this._searchParms.has("historyBack") || (!isSingleView && viewIndex > 0);
return html`
<div class="header">
<div class="toolbar">
${this._searchParms.has("historyBack")
${showBack
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
@@ -99,7 +182,7 @@ class PanelEnergy extends LitElement {
<hui-energy-period-selector
.hass=${this.hass}
collection-key="energy_dashboard"
.collectionKey=${DEFAULT_ENERGY_COLLECTION_KEY}
>
${this.hass.user?.is_admin
? html` <ha-list-item
@@ -127,12 +210,21 @@ class PanelEnergy extends LitElement {
.hass=${this.hass}
@reload-energy-panel=${this._reloadView}
>
<hui-view
${this._error
? html`<div class="centered">
<ha-alert alert-type="error">
An error occurred while fetching your energy preferences:
${this._error}
</ha-alert>
</div>`
: this._lovelace
? html`<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
.index=${viewIndex}
></hui-view>`
: nothing}
</hui-view-container>
`;
}
@@ -160,9 +252,7 @@ class PanelEnergy extends LitElement {
private async _dumpCSV(ev) {
ev.stopPropagation();
const energyData = getEnergyDataCollection(this.hass, {
key: "energy_dashboard",
});
const energyData = this._energyCollection!;
if (!energyData.prefs || !energyData.state.stats) {
return;
@@ -459,11 +549,11 @@ class PanelEnergy extends LitElement {
}
private _reloadView() {
// Force strategy to be re-run by make a copy of the view
// Force strategy to be re-run by making a copy of the view
const config = this._lovelace!.config;
this._lovelace = {
...this._lovelace!,
config: { ...config, views: [{ ...config.views[0] }] },
config: { ...config, views: config.views.map((view) => ({ ...view })) },
};
}
@@ -565,6 +655,13 @@ class PanelEnergy extends LitElement {
flex: 1 1 100%;
max-width: 100%;
}
.centered {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
`,
];
}

View File

@@ -1,55 +1,35 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import type {
EnergyPreferences,
GridSourceTypeEnergyPreference,
} from "../../../data/energy";
import { getEnergyPreferences } from "../../../data/energy";
import type { GridSourceTypeEnergyPreference } from "../../../data/energy";
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";
const setupWizard = async (): Promise<LovelaceViewConfig> => {
await import("../cards/energy-setup-wizard-card");
return {
type: "panel",
cards: [
{
type: "custom:energy-setup-wizard-card",
},
],
};
};
@customElement("energy-view-strategy")
export class EnergyViewStrategy extends ReactiveElement {
@customElement("energy-electricity-view-strategy")
export class EnergyElectricityViewStrategy extends ReactiveElement {
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = { cards: [] };
let prefs: EnergyPreferences;
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
try {
prefs = await getEnergyPreferences(hass);
} catch (err: any) {
if (err.code === "not_found") {
return setupWizard();
}
view.cards!.push({
type: "markdown",
content: `An error occurred while fetching your energy preferences: ${err.message}.`,
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
});
return view;
}
const prefs = energyCollection.prefs;
// No energy sources available, start from scratch
// No energy sources available
if (
prefs!.device_consumption.length === 0 &&
prefs!.energy_sources.length === 0
!prefs ||
(prefs.device_consumption.length === 0 &&
prefs.energy_sources.length === 0)
) {
return setupWizard();
return view;
}
view.type = "sidebar";
@@ -63,13 +43,9 @@ export class EnergyViewStrategy extends ReactiveElement {
const hasSolar = prefs.energy_sources.some(
(source) => source.type === "solar"
);
const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
const hasBattery = prefs.energy_sources.some(
(source) => source.type === "battery"
);
const hasWater = prefs.energy_sources.some(
(source) => source.type === "water"
);
view.cards!.push({
type: "energy-compare",
@@ -94,24 +70,6 @@ export class EnergyViewStrategy extends ReactiveElement {
});
}
// Only include if we have a gas source.
if (hasGas) {
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_gas_graph_title"),
type: "energy-gas-graph",
collection_key: "energy_dashboard",
});
}
// Only include if we have a water source.
if (hasWater) {
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_water_graph_title"),
type: "energy-water-graph",
collection_key: "energy_dashboard",
});
}
// Only include if we have a grid or battery.
if (hasGrid || hasBattery) {
view.cards!.push({
@@ -122,13 +80,14 @@ export class EnergyViewStrategy extends ReactiveElement {
});
}
if (hasGrid || hasSolar || hasGas || hasWater || hasBattery) {
if (hasGrid || hasSolar || hasBattery) {
view.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_sources_table_title"
),
type: "energy-sources-table",
collection_key: "energy_dashboard",
types: ["grid", "solar", "battery"],
});
}
@@ -170,20 +129,6 @@ 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_devices_detail_graph_title"
),
type: "energy-devices-detail-graph",
collection_key: "energy_dashboard",
});
view.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_devices_graph_title"
),
type: "energy-devices-graph",
collection_key: "energy_dashboard",
});
const showFloorsNAreas = !prefs.device_consumption.some(
(d) => d.included_in_stat
);
@@ -194,6 +139,20 @@ export class EnergyViewStrategy extends ReactiveElement {
group_by_floor: showFloorsNAreas,
group_by_area: showFloorsNAreas,
});
view.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_devices_graph_title"
),
type: "energy-devices-graph",
collection_key: "energy_dashboard",
});
view.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_devices_detail_graph_title"
),
type: "energy-devices-detail-graph",
collection_key: "energy_dashboard",
});
}
return view;
@@ -202,6 +161,6 @@ export class EnergyViewStrategy extends ReactiveElement {
declare global {
interface HTMLElementTagNameMap {
"energy-view-strategy": EnergyViewStrategy;
"energy-electricity-view-strategy": EnergyElectricityViewStrategy;
}
}

View File

@@ -0,0 +1,218 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import type { GridSourceTypeEnergyPreference } from "../../../data/energy";
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 type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
@customElement("energy-overview-view-strategy")
export class EnergyViewStrategy extends ReactiveElement {
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = {
type: "sections",
sections: [],
dense_section_placement: true,
max_columns: 2,
};
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
});
const prefs = energyCollection.prefs;
// No energy sources available
if (
!prefs ||
(prefs.device_consumption.length === 0 &&
prefs.energy_sources.length === 0)
) {
return view;
}
const hasGrid = prefs.energy_sources.find(
(source) =>
source.type === "grid" &&
(source.flow_from?.length || source.flow_to?.length)
) as GridSourceTypeEnergyPreference;
const hasReturn = hasGrid && hasGrid.flow_to.length > 0;
const hasSolar = prefs.energy_sources.some(
(source) => source.type === "solar"
);
const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
const hasBattery = prefs.energy_sources.some(
(source) => source.type === "battery"
);
const hasWater = prefs.energy_sources.some(
(source) => source.type === "water"
);
const hasPowerSources = prefs.energy_sources.find(
(source) =>
(source.type === "solar" && source.stat_rate) ||
(source.type === "battery" && source.stat_rate) ||
(source.type === "grid" && source.power?.length)
);
const hasPowerDevices = prefs.device_consumption.find(
(device) => device.stat_rate
);
const overviewSection: LovelaceSectionConfig = {
type: "grid",
column_span: 24,
cards: [],
};
if (hasPowerSources && hasPowerDevices) {
overviewSection.cards!.push({
title: hass.localize("ui.panel.energy.cards.power_sankey_title"),
type: "power-sankey",
collection_key: collectionKey,
grid_options: {
columns: 24,
},
});
}
// Only include if we have a grid or battery.
if (hasGrid || hasBattery) {
overviewSection.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_distribution_title"),
type: "energy-distribution",
collection_key: collectionKey,
});
}
if (hasGrid || hasSolar || hasBattery || hasGas || hasWater) {
overviewSection.cards!.push({
type: "energy-sources-table",
collection_key: collectionKey,
});
}
view.sections!.push(overviewSection);
const electricitySection: LovelaceSectionConfig = {
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.energy.overview.electricity"),
tap_action: {
action: "navigate",
navigation_path: "/energy/electricity",
},
},
],
};
if (hasPowerSources) {
electricitySection.cards!.push({
type: "power-sources-graph",
collection_key: collectionKey,
});
}
if (prefs!.device_consumption.length > 3) {
electricitySection.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"],
});
} else if (hasGrid) {
const gauges: LovelaceCardConfig[] = [];
// Only include if we have a grid source & return.
if (hasReturn) {
gauges.push({
type: "energy-grid-neutrality-gauge",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
});
}
gauges.push({
type: "energy-carbon-consumed-gauge",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
});
// Only include if we have a solar source.
if (hasSolar) {
if (hasReturn) {
gauges.push({
type: "energy-solar-consumed-gauge",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
});
}
gauges.push({
type: "energy-self-sufficiency-gauge",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
});
}
electricitySection.cards!.push({
type: "grid",
columns: 2,
square: false,
cards: gauges,
});
}
view.sections!.push(electricitySection);
if (hasGas) {
view.sections!.push({
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.energy.overview.gas"),
},
{
title: hass.localize(
"ui.panel.energy.cards.energy_gas_graph_title"
),
type: "energy-gas-graph",
collection_key: collectionKey,
},
],
});
}
if (hasWater) {
view.sections!.push({
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.energy.overview.water"),
},
{
title: hass.localize(
"ui.panel.energy.cards.energy_water_graph_title"
),
type: "energy-water-graph",
collection_key: collectionKey,
},
],
});
}
return view;
}
}
declare global {
interface HTMLElementTagNameMap {
"energy-overview-view-strategy": EnergyViewStrategy;
}
}

View File

@@ -135,11 +135,13 @@ export class HuiEnergyDevicesGraphCard
return nothing;
}
const modes = this._getAllowedModes();
return html`
<ha-card>
<div class="card-header">
<span>${this._config.title ? this._config.title : nothing}</span>
${this._getAllowedModes().length > 1
${modes.length > 1
? html`
<ha-icon-button
.path=${this._chartType === "pie"
@@ -166,7 +168,7 @@ export class HuiEnergyDevicesGraphCard
this._chartType,
this._legendData
)}
.height=${`${Math.max(300, (this._legendData?.length || 0) * 28 + 50)}px`}
.height=${`${Math.max(modes.includes("pie") ? 300 : 100, (this._legendData?.length || 0) * 28 + 50)}px`}
.extraComponents=${[PieChart]}
@chart-click=${this._handleChartClick}
@dataset-hidden=${this._datasetHidden}
@@ -492,7 +494,7 @@ export class HuiEnergyDevicesGraphCard
show: true,
position: "center",
color: computedStyle.getPropertyValue("--secondary-text-color"),
fontSize: computedStyle.getPropertyValue("--ha-font-size-l"),
fontSize: computedStyle.getPropertyValue("--ha-font-size-m"),
lineHeight: 24,
fontWeight: "bold",
formatter: `{a}\n${formatNumber(totalChart, this.hass.locale)} kWh`,

View File

@@ -21,6 +21,7 @@ import { hasConfigChanged } from "../../common/has-changed";
import { getCommonOptions, fillLineGaps } from "./common/energy-chart-options";
import type { ECOption } from "../../../../resources/echarts/echarts";
import { hex2rgb } from "../../../../common/color/convert-color";
import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base";
@customElement("hui-power-sources-graph-card")
export class HuiPowerSourcesGraphCard
@@ -33,6 +34,8 @@ export class HuiPowerSourcesGraphCard
@state() private _chartData: LineSeriesOption[] = [];
@state() private _legendData?: CustomLegendOption["data"];
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@@ -91,7 +94,8 @@ export class HuiPowerSourcesGraphCard
this.hass.locale,
this.hass.config,
this._compareStart,
this._compareEnd
this._compareEnd,
this._legendData
)}
></ha-chart-base>
${!this._chartData.some((dataset) => dataset.data!.length)
@@ -115,9 +119,10 @@ export class HuiPowerSourcesGraphCard
locale: FrontendLocaleData,
config: HassConfig,
compareStart?: Date,
compareEnd?: Date
): ECOption =>
getCommonOptions(
compareEnd?: Date,
legendData?: CustomLegendOption["data"]
): ECOption => ({
...getCommonOptions(
start,
end,
locale,
@@ -125,11 +130,18 @@ export class HuiPowerSourcesGraphCard
"kW",
compareStart,
compareEnd
)
),
legend: {
show: true,
type: "custom",
data: legendData,
},
})
);
private async _getStatistics(energyData: EnergyData): Promise<void> {
const datasets: LineSeriesOption[] = [];
this._legendData = [];
const statIds = {
solar: {
@@ -238,6 +250,15 @@ export class HuiPowerSourcesGraphCard
z: 4 - keyIndex, // draw in reverse order but above positive series
});
}
this._legendData!.push({
id: key,
secondaryIds: key !== "solar" ? [`${key}-negative`] : [],
name: statIds[key].name,
itemStyle: {
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
borderColor: colorHex,
},
});
}
});
@@ -268,11 +289,23 @@ export class HuiPowerSourcesGraphCard
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.power_graph.usage"
),
color: computedStyles.getPropertyValue("--primary-color"),
lineStyle: { width: 2 },
color: computedStyles.getPropertyValue("--primary-text-color"),
lineStyle: {
type: [7, 2],
width: 1.5,
},
data: usageData,
z: 5,
});
this._legendData!.push({
id: "usage",
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.power_graph.usage"
),
itemStyle: {
color: computedStyles.getPropertyValue("--primary-text-color"),
},
});
}
private _processData(stats: StatisticValue[][]) {

View File

@@ -150,11 +150,6 @@ export interface EnergyCardBaseConfig extends LovelaceCardConfig {
collection_key?: string;
}
export interface EnergySummaryCardConfig extends EnergyCardBaseConfig {
type: "energy-summary";
title?: string;
}
export interface EnergyDistributionCardConfig extends EnergyCardBaseConfig {
type: "energy-distribution";
title?: string;

View File

@@ -38,7 +38,10 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
view: {
"original-states": () =>
import("./original-states/original-states-view-strategy"),
energy: () => import("../../energy/strategies/energy-view-strategy"),
"energy-overview": () =>
import("../../energy/strategies/energy-overview-view-strategy"),
"energy-electricity": () =>
import("../../energy/strategies/energy-electricity-view-strategy"),
map: () => import("./map/map-view-strategy"),
iframe: () => import("./iframe/iframe-view-strategy"),
area: () => import("./areas/area-view-strategy"),

View File

@@ -7166,7 +7166,7 @@
"grid": "Grid",
"solar": "Solar",
"battery": "Battery",
"usage": "Used"
"usage": "Consumption"
},
"energy_compare": {
"info": "You are comparing the period {start} with the period {end}",
@@ -9428,6 +9428,11 @@
}
},
"energy": {
"overview": {
"electricity": "Electricity",
"gas": "Gas",
"water": "Water"
},
"download_data": "[%key:ui::panel::history::download_data%]",
"configure": "[%key:ui::dialogs::quick-bar::commands::navigation::energy%]",
"setup": {
@@ -9453,6 +9458,7 @@
"energy_devices_graph_title": "Individual devices total usage",
"energy_devices_detail_graph_title": "Individual devices detail usage",
"energy_sankey_title": "Energy flow",
"energy_top_consumers_title": "Top consumers",
"power_sankey_title": "Current power flow"
}
},