diff --git a/src/panels/energy/ha-panel-energy.ts b/src/panels/energy/ha-panel-energy.ts index 933852737af6..88f7d03136b3 100644 --- a/src/panels/energy/ha-panel-energy.ts +++ b/src/panels/energy/ha-panel-energy.ts @@ -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,12 +32,27 @@ 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-overview", + collection_key: DEFAULT_ENERGY_COLLECTION_KEY, + }, + }, { strategy: { type: "energy", + 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"); } @@ -61,19 +95,67 @@ 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) { + await this._setLovelace(); + } 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`
+ +
`; + } + let viewPath = this._viewPath; + const { prefs } = this._energyCollection; + if ( + prefs.energy_sources.every((source) => + ["grid", "solar", "battery"].includes(source.type) + ) + ) { + // if only electricity sources, show electricity view directly + viewPath = "electricity"; + } + const viewIndex = Math.max( + ENERGY_LOVELACE_CONFIG.views.findIndex((view) => view.path === viewPath), + 0 + ); + return html`
@@ -99,7 +181,7 @@ class PanelEnergy extends LitElement { ${this.hass.user?.is_admin ? html` - + ${this._error + ? html`
+ + An error occurred while fetching your energy preferences: + ${this._error} + +
` + : this._lovelace + ? html`` + : nothing} `; } @@ -160,9 +251,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 +548,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 +654,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; + } `, ]; } diff --git a/src/panels/energy/strategies/energy-overview-view-strategy.ts b/src/panels/energy/strategies/energy-overview-view-strategy.ts new file mode 100644 index 000000000000..395a953e49fd --- /dev/null +++ b/src/panels/energy/strategies/energy-overview-view-strategy.ts @@ -0,0 +1,193 @@ +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 { + const view: LovelaceViewConfig = { type: "sections", sections: [] }; + + 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; + 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 energySection: LovelaceSectionConfig = { + type: "grid", + cards: [ + { + type: "heading", + heading: hass.localize("ui.panel.energy.overview.electricity"), + tap_action: { + action: "navigate", + navigation_path: "/energy/electricity", + }, + }, + ], + }; + // Only include if we have a grid or battery. + if (hasGrid || hasBattery) { + energySection.cards!.push({ + title: hass.localize("ui.panel.energy.cards.energy_distribution_title"), + type: "energy-distribution", + view_layout: { position: "sidebar" }, + collection_key: collectionKey, + }); + } + + if (prefs!.device_consumption.length > 0) { + energySection.cards!.push({ + title: hass.localize( + "ui.panel.energy.cards.energy_top_consumers_title" + ), + type: "energy-devices-graph", + collection_key: collectionKey, + max_devices: 5, + }); + } 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, + }); + } + + energySection.cards!.push({ + type: "grid", + columns: 2, + square: true, + cards: gauges, + }); + } + + view.sections!.push(energySection); + + if (hasGrid || hasSolar || hasBattery || hasGas || hasWater) { + view.sections!.push({ + type: "grid", + cards: [ + { + type: "heading", + heading: hass.localize( + "ui.panel.energy.cards.energy_sources_table_title" + ), + }, + { + type: "energy-sources-table", + collection_key: collectionKey, + }, + ], + }); + } + + 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; + } +} diff --git a/src/panels/energy/strategies/energy-view-strategy.ts b/src/panels/energy/strategies/energy-view-strategy.ts index a861fb2ad250..2d00c645f5f1 100644 --- a/src/panels/energy/strategies/energy-view-strategy.ts +++ b/src/panels/energy/strategies/energy-view-strategy.ts @@ -1,25 +1,11 @@ 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"; - -const setupWizard = async (): Promise => { - await import("../cards/energy-setup-wizard-card"); - return { - type: "panel", - cards: [ - { - type: "custom:energy-setup-wizard-card", - }, - ], - }; -}; +import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy"; @customElement("energy-view-strategy") export class EnergyViewStrategy extends ReactiveElement { @@ -29,27 +15,21 @@ export class EnergyViewStrategy extends ReactiveElement { ): Promise { 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}.`, - }); - return view; - } + const energyCollection = getEnergyDataCollection(hass, { + key: collectionKey, + }); + 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,12 +129,15 @@ export class EnergyViewStrategy extends ReactiveElement { // Only include if we have at least 1 device in the config. if (prefs.device_consumption.length) { + const showFloorsNAreas = !prefs.device_consumption.some( + (d) => d.included_in_stat + ); 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.cards!.push({ title: hass.localize( @@ -184,15 +146,12 @@ export class EnergyViewStrategy extends ReactiveElement { type: "energy-devices-graph", collection_key: "energy_dashboard", }); - const showFloorsNAreas = !prefs.device_consumption.some( - (d) => d.included_in_stat - ); 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, }); } diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index f6992f518097..2a0cec648edc 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts @@ -379,7 +379,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(totalUsed, this.hass.locale)} kWh`, diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index b90bffb75a8b..5b90a1636b1f 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -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; diff --git a/src/panels/lovelace/strategies/get-strategy.ts b/src/panels/lovelace/strategies/get-strategy.ts index 80a4cb3ed851..03207d9787e5 100644 --- a/src/panels/lovelace/strategies/get-strategy.ts +++ b/src/panels/lovelace/strategies/get-strategy.ts @@ -38,6 +38,8 @@ const STRATEGIES: Record> = { view: { "original-states": () => import("./original-states/original-states-view-strategy"), + "energy-overview": () => + import("../../energy/strategies/energy-overview-view-strategy"), energy: () => import("../../energy/strategies/energy-view-strategy"), map: () => import("./map/map-view-strategy"), iframe: () => import("./iframe/iframe-view-strategy"), diff --git a/src/translations/en.json b/src/translations/en.json index f060f73dc695..ec573f69434a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -9310,6 +9310,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%]", "compare": { @@ -9339,7 +9344,8 @@ "energy_sources_table_title": "Sources", "energy_devices_graph_title": "Individual devices total usage", "energy_devices_detail_graph_title": "Individual devices detail usage", - "energy_sankey_title": "Energy flow" + "energy_sankey_title": "Energy flow", + "energy_top_consumers_title": "Top consumers" } }, "history": {