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

Use hui-root for panel energy (#28149)

* Use hui-root for panel energy

* Review feedback

* Set empty prefs
This commit is contained in:
Paul Bottein
2025-11-27 14:35:36 +01:00
committed by Bram Kragten
parent 690fd5a061
commit bcae64df88
2 changed files with 187 additions and 224 deletions

View File

@@ -26,16 +26,24 @@ import type { LovelaceConfig } from "../../data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../data/lovelace/config/view"; import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
import type { StatisticValue } from "../../data/recorder"; import type { StatisticValue } from "../../data/recorder";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant, PanelInfo } from "../../types";
import { fileDownload } from "../../util/file_download"; import { fileDownload } from "../../util/file_download";
import "../lovelace/components/hui-energy-period-selector"; import "../lovelace/components/hui-energy-period-selector";
import "../lovelace/hui-root";
import type { Lovelace } from "../lovelace/types"; import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view"; import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container"; import "../lovelace/views/hui-view-container";
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard"; export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
const EMPTY_PREFERENCES: EnergyPreferences = {
energy_sources: [],
device_consumption: [],
device_consumption_water: [],
};
const OVERVIEW_VIEW = { const OVERVIEW_VIEW = {
path: "overview",
strategy: { strategy: {
type: "energy-overview", type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY, collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
@@ -43,8 +51,8 @@ const OVERVIEW_VIEW = {
} as LovelaceViewConfig; } as LovelaceViewConfig;
const ELECTRICITY_VIEW = { const ELECTRICITY_VIEW = {
back_path: "/energy",
path: "electricity", path: "electricity",
back_path: "/energy",
strategy: { strategy: {
type: "energy-electricity", type: "energy-electricity",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY, collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
@@ -72,54 +80,96 @@ class PanelEnergy extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false; @property({ type: Boolean, reflect: true }) public narrow = false;
@property({ attribute: false }) public panel?: PanelInfo;
@state() private _lovelace?: Lovelace; @state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search); @state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _error?: string;
@property({ attribute: false }) public route?: { @property({ attribute: false }) public route?: {
path: string; path: string;
prefix: string; prefix: string;
}; };
@state() @state()
private _config?: LovelaceConfig; private _prefs?: EnergyPreferences;
get _viewPath(): string | undefined { @state()
const viewPath: string | undefined = this.route!.path.split("/")[1]; private _error?: string;
return viewPath ? decodeURI(viewPath) : undefined;
}
public connectedCallback() { public willUpdate(changedProps: PropertyValues) {
super.connectedCallback(); super.willUpdate(changedProps);
this._loadLovelaceConfig(); // Initial setup
}
public async willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) { if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace"); this.hass.loadFragmentTranslation("lovelace");
this._loadConfig();
return;
} }
if (!changedProps.has("hass")) { if (!changedProps.has("hass")) {
return; return;
} }
const oldHass = changedProps.get("hass") as this["hass"]; const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass?.locale !== this.hass.locale) { if (oldHass && oldHass.localize !== this.hass.localize) {
this._setLovelace(); this._setLovelace();
} else if (oldHass && oldHass.localize !== this.hass.localize) {
this._reloadView();
} }
} }
private async _loadLovelaceConfig() { private _fetchEnergyPrefs = async (): Promise<
EnergyPreferences | undefined
> => {
const collection = getEnergyDataCollection(this.hass, {
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try { try {
this._config = undefined; await collection.refresh();
this._config = await this._generateLovelaceConfig(); } catch (err: any) {
} catch (err) { if (err.code === "not_found") {
this._error = (err as Error).message; return undefined;
}
throw err;
} }
return collection.prefs;
};
this._setLovelace(); private async _loadConfig() {
try {
this._error = undefined;
const prefs = await this._fetchEnergyPrefs();
this._prefs = prefs || EMPTY_PREFERENCES;
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to load prefs:", err);
this._prefs = EMPTY_PREFERENCES;
this._error = (err as Error).message || "Unknown error";
}
await this._setLovelace();
// Navigate to first view if not there yet
const firstPath = this._lovelace!.config?.views?.[0]?.path;
const viewPath: string | undefined = this.route!.path.split("/")[1];
if (viewPath !== firstPath) {
navigate(`${this.route!.prefix}/${firstPath}`);
}
}
private async _setLovelace() {
const config = await this._generateLovelaceConfig();
this._lovelace = {
config: config,
rawConfig: config,
editMode: false,
urlPath: "energy",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
} }
private _back(ev) { private _back(ev) {
@@ -128,7 +178,17 @@ class PanelEnergy extends LitElement {
} }
protected render() { protected render() {
if (!this._config && !this._error) { if (this._error) {
return html`
<div class="centered">
<ha-alert alert-type="error">
An error occurred loading energy preferences: ${this._error}
</ha-alert>
</div>
`;
}
if (!this._prefs) {
// Still loading // Still loading
return html` return html`
<div class="centered"> <div class="centered">
@@ -136,20 +196,31 @@ class PanelEnergy extends LitElement {
</div> </div>
`; `;
} }
const isSingleView = this._config?.views.length === 1;
const viewPath = this._viewPath; if (!this._lovelace) {
const viewIndex = this._config return nothing;
? Math.max( }
this._config.views.findIndex((view) => view.path === viewPath),
0 const viewPath: string | undefined = this.route!.path.split("/")[1];
)
: 0; const views = this._lovelace.config?.views || [];
const showBack = const viewIndex = Math.max(
this._searchParms.has("historyBack") || (!isSingleView && viewIndex > 0); views.findIndex((view) => view.path === viewPath),
0
);
const showBack = this._searchParms.has("historyBack") || viewIndex > 0;
return html` return html`
<div class="header"> <hui-root
<div class="toolbar"> .hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.route=${this.route}
.panel=${this.panel}
@reload-energy-panel=${this._reloadConfig}
>
<div class="toolbar" slot="toolbar">
${showBack ${showBack
? html` ? html`
<ha-icon-button-arrow-prev <ha-icon-button-arrow-prev
@@ -175,14 +246,17 @@ class PanelEnergy extends LitElement {
.collectionKey=${DEFAULT_ENERGY_COLLECTION_KEY} .collectionKey=${DEFAULT_ENERGY_COLLECTION_KEY}
> >
${this.hass.user?.is_admin ${this.hass.user?.is_admin
? html` <ha-list-item ? html`
slot="overflow-menu" <ha-list-item
graphic="icon" slot="overflow-menu"
@request-selected=${this._navigateConfig} graphic="icon"
> @request-selected=${this._navigateConfig}
<ha-svg-icon slot="graphic" .path=${mdiPencil}> </ha-svg-icon> >
${this.hass!.localize("ui.panel.energy.configure")} <ha-svg-icon slot="graphic" .path=${mdiPencil}>
</ha-list-item>` </ha-svg-icon>
${this.hass!.localize("ui.panel.energy.configure")}
</ha-list-item>
`
: nothing} : nothing}
<ha-list-item <ha-list-item
slot="overflow-menu" slot="overflow-menu"
@@ -194,54 +268,15 @@ class PanelEnergy extends LitElement {
</ha-list-item> </ha-list-item>
</hui-energy-period-selector> </hui-energy-period-selector>
</div> </div>
</div> </hui-root>
<hui-view-container
.hass=${this.hass}
@reload-energy-panel=${this._reloadView}
>
${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=${viewIndex}
></hui-view>`
: nothing}
</hui-view-container>
`; `;
} }
private _fetchEnergyPrefs = async (): Promise<
EnergyPreferences | undefined
> => {
const collection = getEnergyDataCollection(this.hass, {
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try {
await collection.refresh();
} catch (err: any) {
if (err.code === "not_found") {
return undefined;
}
throw err;
}
return collection.prefs;
};
private async _generateLovelaceConfig(): Promise<LovelaceConfig> { private async _generateLovelaceConfig(): Promise<LovelaceConfig> {
const prefs = await this._fetchEnergyPrefs();
if ( if (
!prefs || !this._prefs ||
(prefs.device_consumption.length === 0 && (this._prefs.device_consumption.length === 0 &&
prefs.energy_sources.length === 0) this._prefs.energy_sources.length === 0)
) { ) {
await import("./cards/energy-setup-wizard-card"); await import("./cards/energy-setup-wizard-card");
return { return {
@@ -249,7 +284,7 @@ class PanelEnergy extends LitElement {
}; };
} }
const isElectricityOnly = prefs.energy_sources.every((source) => const isElectricityOnly = this._prefs.energy_sources.every((source) =>
["grid", "solar", "battery"].includes(source.type) ["grid", "solar", "battery"].includes(source.type)
); );
if (isElectricityOnly) { if (isElectricityOnly) {
@@ -259,8 +294,8 @@ class PanelEnergy extends LitElement {
} }
const hasWater = const hasWater =
prefs.energy_sources.some((source) => source.type === "water") || this._prefs.energy_sources.some((source) => source.type === "water") ||
prefs.device_consumption_water?.length > 0; this._prefs.device_consumption_water?.length > 0;
const views: LovelaceViewConfig[] = [OVERVIEW_VIEW, ELECTRICITY_VIEW]; const views: LovelaceViewConfig[] = [OVERVIEW_VIEW, ELECTRICITY_VIEW];
if (hasWater) { if (hasWater) {
@@ -269,25 +304,6 @@ class PanelEnergy extends LitElement {
return { views }; return { views };
} }
private _setLovelace() {
if (!this._config) {
return;
}
this._lovelace = {
config: this._config,
rawConfig: this._config,
editMode: false,
urlPath: "energy",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
private _navigateConfig(ev) { private _navigateConfig(ev) {
ev.stopPropagation(); ev.stopPropagation();
navigate("/config/energy?historyBack=1"); navigate("/config/energy?historyBack=1");
@@ -593,8 +609,8 @@ class PanelEnergy extends LitElement {
fileDownload(url, "energy.csv"); fileDownload(url, "energy.csv");
} }
private _reloadView() { private _reloadConfig() {
this._loadLovelaceConfig(); this._loadConfig();
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -620,45 +636,6 @@ class PanelEnergy extends LitElement {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
} }
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
position: fixed;
top: 0;
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar { .toolbar {
height: var(--header-height); height: var(--header-height);
display: flex; display: flex;
@@ -677,24 +654,6 @@ class PanelEnergy extends LitElement {
line-height: var(--ha-line-height-normal); line-height: var(--ha-line-height-normal);
flex-grow: 1; flex-grow: 1;
} }
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
.centered { .centered {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@@ -127,7 +127,7 @@ interface UndoStackItem {
@customElement("hui-root") @customElement("hui-root")
class HUIRoot extends LitElement { class HUIRoot extends LitElement {
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>; @property({ attribute: false }) public panel?: PanelInfo;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -543,68 +543,72 @@ class HUIRoot extends LitElement {
})} })}
> >
<div class="header"> <div class="header">
<div class="toolbar"> <slot name="toolbar">
<div class="toolbar">
${this._editMode
? html`
<div class="main-title">
${dashboardTitle ||
this.hass!.localize("ui.panel.lovelace.editor.header")}
<ha-icon-button
slot="actionItems"
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_lovelace.edit_title"
)}
.path=${mdiPencil}
class="edit-icon"
@click=${this._editDashboard}
></ha-icon-button>
</div>
<div class="action-items">${this._renderActionItems()}</div>
`
: html`
${isSubview
? html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
slot="navigationIcon"
@click=${this._goBack}
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${isSubview
? html`
<div class="main-title">${curViewConfig.title}</div>
`
: hasTabViews
? tabs
: html`
<div class="main-title">
${views[0]?.title ?? dashboardTitle}
</div>
`}
<div class="action-items">${this._renderActionItems()}</div>
`}
</div>
${this._editMode ${this._editMode
? html` ? html`
<div class="main-title"> <div class="tab-bar">
${dashboardTitle || ${tabs}
this.hass!.localize("ui.panel.lovelace.editor.header")}
<ha-icon-button <ha-icon-button
slot="actionItems" slot="nav"
id="add-view"
@click=${this._addView}
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_lovelace.edit_title" "ui.panel.lovelace.editor.edit_view.add"
)} )}
.path=${mdiPencil} .path=${mdiPlus}
class="edit-icon"
@click=${this._editDashboard}
></ha-icon-button> ></ha-icon-button>
</div> </div>
<div class="action-items">${this._renderActionItems()}</div>
` `
: html` : nothing}
${isSubview </slot>
? html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
slot="navigationIcon"
@click=${this._goBack}
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${isSubview
? html`<div class="main-title">${curViewConfig.title}</div>`
: hasTabViews
? tabs
: html`
<div class="main-title">
${views[0]?.title ?? dashboardTitle}
</div>
`}
<div class="action-items">${this._renderActionItems()}</div>
`}
</div>
${this._editMode
? html`
<div class="tab-bar">
${tabs}
<ha-icon-button
slot="nav"
id="add-view"
@click=${this._addView}
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.add"
)}
.path=${mdiPlus}
></ha-icon-button>
</div>
`
: nothing}
</div> </div>
<hui-view-container <hui-view-container
class=${this._editMode ? "has-tab-bar" : ""} class=${this._editMode ? "has-tab-bar" : ""}