From 0f16ba93259e2566f6d59aae0ceeeb2aa201a731 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 31 Jul 2021 09:33:41 -0700 Subject: [PATCH] Speed up data loading and allow embedding individual energy cards (#9660) Co-authored-by: Bram Kragten --- src/data/energy.ts | 139 ++++++++ .../energy/strategies/energy-strategy.ts | 37 +- .../hui-energy-carbon-consumed-gauge-card.ts | 137 ++------ .../energy/hui-energy-devices-graph-card.ts | 117 ++----- .../energy/hui-energy-distribution-card.ts | 129 ++----- .../hui-energy-grid-neutrality-gauge-card.ts | 72 ++-- .../hui-energy-solar-consumed-gauge-card.ts | 73 ++-- .../energy/hui-energy-solar-graph-card.ts | 273 ++++++--------- .../energy/hui-energy-sources-table-card.ts | 92 ++--- .../energy/hui-energy-usage-graph-card.ts | 317 +++++++----------- src/panels/lovelace/cards/types.ts | 10 - 11 files changed, 558 insertions(+), 838 deletions(-) diff --git a/src/data/energy.ts b/src/data/energy.ts index 216bf566ff..ac1ccb622a 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -1,4 +1,9 @@ +import { Collection, getCollection } from "home-assistant-js-websocket"; +import { subscribeOne } from "../common/util/subscribe-one"; import { HomeAssistant } from "../types"; +import { ConfigEntry, getConfigEntries } from "./config_entries"; +import { subscribeEntityRegistry } from "./entity_registry"; +import { fetchStatistics, Statistics } from "./history"; export const emptyFlowFromGridSourceEnergyPreference = (): FlowFromGridSourceEnergyPreference => ({ @@ -128,3 +133,137 @@ export const energySourcesByType = (prefs: EnergyPreferences) => { } return types; }; + +export interface EnergyData { + start: Date; + end?: Date; + prefs: EnergyPreferences; + info: EnergyInfo; + stats: Statistics; + co2SignalConfigEntry?: ConfigEntry; + co2SignalEntity?: string; +} + +const getEnergyData = async ( + hass: HomeAssistant, + prefs: EnergyPreferences, + start: Date, + end?: Date +): Promise => { + const [configEntries, entityRegistryEntries, info] = await Promise.all([ + getConfigEntries(hass), + subscribeOne(hass.connection, subscribeEntityRegistry), + getEnergyInfo(hass), + ]); + + const co2SignalConfigEntry = configEntries.find( + (entry) => entry.domain === "co2signal" + ); + + let co2SignalEntity: string | undefined; + + if (co2SignalConfigEntry) { + for (const entry of entityRegistryEntries) { + if (entry.config_entry_id !== co2SignalConfigEntry.entry_id) { + continue; + } + + // The integration offers 2 entities. We want the % one. + const co2State = hass.states[entry.entity_id]; + if (!co2State || co2State.attributes.unit_of_measurement !== "%") { + continue; + } + + co2SignalEntity = co2State.entity_id; + break; + } + } + + const statIDs: string[] = []; + + if (co2SignalEntity !== undefined) { + statIDs.push(co2SignalEntity); + } + + for (const source of prefs.energy_sources) { + if (source.type === "solar") { + statIDs.push(source.stat_energy_from); + continue; + } + + // grid source + for (const flowFrom of source.flow_from) { + statIDs.push(flowFrom.stat_energy_from); + } + for (const flowTo of source.flow_to) { + statIDs.push(flowTo.stat_energy_to); + } + } + + const stats = await fetchStatistics(hass!, start, end, statIDs); + + return { + start, + end, + info, + prefs, + stats, + co2SignalConfigEntry, + co2SignalEntity, + }; +}; + +export interface EnergyCollection extends Collection { + start: Date; + end?: Date; + prefs?: EnergyPreferences; + clearPrefs(): void; + setPeriod(newStart: Date, newEnd?: Date): void; + getDeviceStatIds(): string[]; +} + +export const getEnergyDataCollection = ( + hass: HomeAssistant, + prefs?: EnergyPreferences +): EnergyCollection => { + if ((hass.connection as any)._energy) { + return (hass.connection as any)._energy; + } + + const collection = getCollection( + hass.connection, + "_energy", + async () => { + if (!collection.prefs) { + // This will raise if not found. + // Detect by checking `e.code === "not_found" + collection.prefs = await getEnergyPreferences(hass); + } + + return getEnergyData( + hass, + collection.prefs, + collection.start, + collection.end + ); + } + ) as EnergyCollection; + + collection.prefs = prefs; + collection.start = new Date(); + collection.start.setHours(0, 0, 0, 0); + collection.start.setTime(collection.start.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint + + collection.clearPrefs = () => { + collection.prefs = undefined; + }; + collection.setPeriod = (newStart: Date, newEnd?: Date) => { + collection.start = newStart; + collection.end = newEnd; + }; + collection.getDeviceStatIds = () => + collection.state.prefs.device_consumption.map( + (device) => device.stat_consumption + ); + return collection; +}; diff --git a/src/panels/energy/strategies/energy-strategy.ts b/src/panels/energy/strategies/energy-strategy.ts index 86ff55d8d1..04b05a0175 100644 --- a/src/panels/energy/strategies/energy-strategy.ts +++ b/src/panels/energy/strategies/energy-strategy.ts @@ -1,5 +1,6 @@ import { EnergyPreferences, + getEnergyDataCollection, getEnergyPreferences, GridSourceTypeEnergyPreference, } from "../../../data/energy"; @@ -26,10 +27,10 @@ export class EnergyStrategy { const view: LovelaceViewConfig = { cards: [] }; - let energyPrefs: EnergyPreferences; + let prefs: EnergyPreferences; try { - energyPrefs = await getEnergyPreferences(hass); + prefs = await getEnergyPreferences(hass); } catch (e) { if (e.code === "not_found") { return setupWizard(); @@ -43,20 +44,21 @@ export class EnergyStrategy { view.type = "sidebar"; - const hasGrid = energyPrefs.energy_sources.find( + const hasGrid = prefs.energy_sources.find( (source) => source.type === "grid" ) as GridSourceTypeEnergyPreference; const hasReturn = hasGrid && hasGrid.flow_to.length; - const hasSolar = energyPrefs.energy_sources.some( + const hasSolar = prefs.energy_sources.some( (source) => source.type === "solar" ); + getEnergyDataCollection(hass, prefs); + // Only include if we have a grid source. if (hasGrid) { view.cards!.push({ title: "Energy usage", type: "energy-usage-graph", - prefs: energyPrefs, }); } @@ -65,7 +67,6 @@ export class EnergyStrategy { view.cards!.push({ title: "Solar production", type: "energy-solar-graph", - prefs: energyPrefs, }); } @@ -74,7 +75,6 @@ export class EnergyStrategy { view.cards!.push({ title: "Energy distribution", type: "energy-distribution", - prefs: energyPrefs, view_layout: { position: "sidebar" }, }); } @@ -83,16 +83,6 @@ export class EnergyStrategy { view.cards!.push({ title: "Sources", type: "energy-sources-table", - prefs: energyPrefs, - }); - } - - // Only include if we have a solar source. - if (hasSolar) { - view.cards!.push({ - type: "energy-solar-consumed-gauge", - prefs: energyPrefs, - view_layout: { position: "sidebar" }, }); } @@ -100,7 +90,14 @@ export class EnergyStrategy { if (hasReturn) { view.cards!.push({ type: "energy-grid-neutrality-gauge", - prefs: energyPrefs, + view_layout: { position: "sidebar" }, + }); + } + + // Only include if we have a solar source. + if (hasSolar && hasReturn) { + view.cards!.push({ + type: "energy-solar-consumed-gauge", view_layout: { position: "sidebar" }, }); } @@ -109,17 +106,15 @@ export class EnergyStrategy { if (hasGrid) { view.cards!.push({ type: "energy-carbon-consumed-gauge", - prefs: energyPrefs, view_layout: { position: "sidebar" }, }); } // Only include if we have at least 1 device in the config. - if (energyPrefs.device_consumption.length) { + if (prefs.device_consumption.length) { view.cards!.push({ title: "Monitor individual devices", type: "energy-devices-graph", - prefs: energyPrefs, }); } diff --git a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts index 84a2e54206..b1a7504fa3 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts @@ -1,19 +1,20 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import { round } from "../../../../common/number/round"; -import { subscribeOne } from "../../../../common/util/subscribe-one"; import "../../../../components/ha-card"; import "../../../../components/ha-gauge"; -import { getConfigEntries } from "../../../../data/config_entries"; -import { energySourcesByType } from "../../../../data/energy"; -import { subscribeEntityRegistry } from "../../../../data/entity_registry"; +import { + EnergyData, + energySourcesByType, + getEnergyDataCollection, +} from "../../../../data/energy"; import { calculateStatisticsSumGrowth, calculateStatisticsSumGrowthWithPercentage, - fetchStatistics, - Statistics, } from "../../../../data/history"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import { createEntityNotFoundWarning } from "../../components/hui-warning"; import type { LovelaceCard } from "../../types"; @@ -21,14 +22,15 @@ import { severityMap } from "../hui-gauge-card"; import type { EnergyCarbonGaugeCardConfig } from "../types"; @customElement("hui-energy-carbon-consumed-gauge-card") -class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard { +class HuiEnergyCarbonGaugeCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config?: EnergyCarbonGaugeCardConfig; - @state() private _stats?: Statistics; - - @state() private _co2SignalEntity?: string | null; + @state() private _data?: EnergyData; public getCardSize(): number { return 4; @@ -38,12 +40,12 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard { this._config = config; } - public willUpdate(changedProps) { - super.willUpdate(changedProps); - - if (!this.hasUpdated) { - this._getStatistics(); - } + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => { + this._data = data; + }), + ]; } protected render(): TemplateResult { @@ -51,52 +53,55 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard { return html``; } - if (this._co2SignalEntity === null) { - return html``; - } - - if (!this._stats || !this._co2SignalEntity) { + if (!this._data) { return html`Loading...`; } - const co2State = this.hass.states[this._co2SignalEntity]; + if (!this._data.co2SignalEntity) { + return html``; + } + + const co2State = this.hass.states[this._data.co2SignalEntity]; if (!co2State) { return html` - ${createEntityNotFoundWarning(this.hass, this._co2SignalEntity)} + ${createEntityNotFoundWarning(this.hass, this._data.co2SignalEntity)} `; } - const prefs = this._config!.prefs; + const prefs = this._data.prefs; const types = energySourcesByType(prefs); const totalGridConsumption = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.grid![0].flow_from.map((flow) => flow.stat_energy_from) ); let value: number | undefined; - if (this._co2SignalEntity in this._stats && totalGridConsumption) { + if ( + this._data.co2SignalEntity in this._data.stats && + totalGridConsumption + ) { const highCarbonEnergy = calculateStatisticsSumGrowthWithPercentage( - this._stats[this._co2SignalEntity], + this._data.stats[this._data.co2SignalEntity], types .grid![0].flow_from.map( - (flow) => this._stats![flow.stat_energy_from] + (flow) => this._data!.stats![flow.stat_energy_from] ) .filter(Boolean) ) || 0; const totalSolarProduction = types.solar ? calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.solar.map((source) => source.stat_energy_from) ) : undefined; const totalGridReturned = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.grid![0].flow_to.map((flow) => flow.stat_energy_to) ); @@ -139,78 +144,6 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard { return severityMap.normal; } - private async _fetchCO2SignalEntity() { - const [configEntries, entityRegistryEntries] = await Promise.all([ - getConfigEntries(this.hass), - subscribeOne(this.hass.connection, subscribeEntityRegistry), - ]); - - const co2ConfigEntry = configEntries.find( - (entry) => entry.domain === "co2signal" - ); - - if (!co2ConfigEntry) { - this._co2SignalEntity = null; - return; - } - - for (const entry of entityRegistryEntries) { - if (entry.config_entry_id !== co2ConfigEntry.entry_id) { - continue; - } - - // The integration offers 2 entities. We want the % one. - const co2State = this.hass.states[entry.entity_id]; - if (!co2State || co2State.attributes.unit_of_measurement !== "%") { - continue; - } - - this._co2SignalEntity = co2State.entity_id; - return; - } - this._co2SignalEntity = null; - } - - private async _getStatistics(): Promise { - await this._fetchCO2SignalEntity(); - - if (this._co2SignalEntity === null) { - return; - } - - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - const statistics: string[] = []; - const prefs = this._config!.prefs; - for (const source of prefs.energy_sources) { - if (source.type === "solar") { - statistics.push(source.stat_energy_from); - continue; - } - - // grid source - for (const flowFrom of source.flow_from) { - statistics.push(flowFrom.stat_energy_from); - } - for (const flowTo of source.flow_to) { - statistics.push(flowTo.stat_energy_to); - } - } - - if (this._co2SignalEntity) { - statistics.push(this._co2SignalEntity); - } - - this._stats = await fetchStatistics( - this.hass!, - startDate, - undefined, - statistics - ); - } - static get styles(): CSSResultGroup { return css` ha-card { 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 f9d8c1cb5d..bb7a1004b4 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 @@ -4,16 +4,11 @@ import { ChartOptions, ParsedDataType, } from "chart.js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import memoizeOne from "memoize-one"; import { getColorByIndex } from "../../../../common/color/colors"; import { computeStateName } from "../../../../common/entity/compute_state_name"; import { @@ -22,18 +17,21 @@ import { } from "../../../../common/string/format_number"; import "../../../../components/chart/ha-chart-base"; import "../../../../components/ha-card"; +import { EnergyData, getEnergyDataCollection } from "../../../../data/energy"; import { calculateStatisticSumGrowth, fetchStatistics, Statistics, } from "../../../../data/history"; +import { FrontendLocaleData } from "../../../../data/translation"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; import { EnergyDevicesGraphCardConfig } from "../types"; @customElement("hui-energy-devices-graph-card") export class HuiEnergyDevicesGraphCard - extends LitElement + extends SubscribeMixin(LitElement) implements LovelaceCard { @property({ attribute: false }) public hass!: HomeAssistant; @@ -44,32 +42,12 @@ export class HuiEnergyDevicesGraphCard @state() private _chartData?: ChartData; - @state() private _chartOptions?: ChartOptions; - - private _fetching = false; - - private _interval?: number; - - public disconnectedCallback() { - super.disconnectedCallback(); - if (this._interval) { - clearInterval(this._interval); - this._interval = undefined; - } - } - - public connectedCallback() { - super.connectedCallback(); - if (!this.hasUpdated) { - return; - } - this._getStatistics(); - // statistics are created every hour - clearInterval(this._interval); - this._interval = window.setInterval( - () => this._getStatistics(), - 1000 * 60 * 60 - ); + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => + this._getStatistics(data) + ), + ]; } public getCardSize(): Promise | number { @@ -80,30 +58,6 @@ export class HuiEnergyDevicesGraphCard this._config = config; } - public willUpdate(changedProps: PropertyValues) { - super.willUpdate(changedProps); - if (!this.hasUpdated) { - this._createOptions(); - } - if (!this._config || !changedProps.has("_config")) { - return; - } - - const oldConfig = changedProps.get("_config") as - | EnergyDevicesGraphCardConfig - | undefined; - - if (oldConfig !== this._config) { - this._getStatistics(); - // statistics are created every hour - clearInterval(this._interval); - this._interval = window.setInterval( - () => this._getStatistics(), - 1000 * 60 * 60 - ); - } - } - protected render(): TemplateResult { if (!this.hass || !this._config) { return html``; @@ -122,7 +76,7 @@ export class HuiEnergyDevicesGraphCard ${this._chartData ? html`` : ""} @@ -131,8 +85,8 @@ export class HuiEnergyDevicesGraphCard `; } - private _createOptions() { - this._chartOptions = { + private _createOptions = memoizeOne( + (locale: FrontendLocaleData): ChartOptions => ({ parsing: false, animation: false, responsive: true, @@ -153,37 +107,24 @@ export class HuiEnergyDevicesGraphCard label: (context) => `${context.dataset.label}: ${formatNumber( context.parsed.x, - this.hass.locale + locale )} kWh`, }, }, }, // @ts-expect-error locale: numberFormatToLocale(this.hass.locale), - }; - } + }) + ); - private async _getStatistics(): Promise { - if (this._fetching) { - return; - } - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - this._fetching = true; - const prefs = this._config!.prefs; - - try { - this._data = await fetchStatistics( - this.hass!, - startDate, - undefined, - prefs.device_consumption.map((device) => device.stat_consumption) - ); - } finally { - this._fetching = false; - } + private async _getStatistics(energyData: EnergyData): Promise { + const energyCollection = getEnergyDataCollection(this.hass); + this._data = await fetchStatistics( + this.hass, + energyCollection.start, + energyCollection.end, + energyCollection.getDeviceStatIds() + ); const statisticsData = Object.values(this._data!); let endTime: Date; @@ -213,8 +154,8 @@ export class HuiEnergyDevicesGraphCard }, ]; - for (let idx = 0; idx < prefs.device_consumption.length; idx++) { - const device = prefs.device_consumption[idx]; + for (let idx = 0; idx < energyData.prefs.device_consumption.length; idx++) { + const device = energyData.prefs.device_consumption[idx]; const entity = this.hass.states[device.stat_consumption]; const label = entity ? computeStateName(entity) : device.stat_consumption; diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts index a3b27745f6..2f5ac887eb 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -6,23 +6,24 @@ import { mdiSolarPower, mdiTransmissionTower, } from "@mdi/js"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, html, LitElement, svg } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { formatNumber } from "../../../../common/string/format_number"; -import { subscribeOne } from "../../../../common/util/subscribe-one"; import "../../../../components/ha-card"; import "../../../../components/ha-svg-icon"; -import { getConfigEntries } from "../../../../data/config_entries"; -import { energySourcesByType } from "../../../../data/energy"; -import { subscribeEntityRegistry } from "../../../../data/entity_registry"; +import { + EnergyData, + energySourcesByType, + getEnergyDataCollection, +} from "../../../../data/energy"; import { calculateStatisticsSumGrowth, calculateStatisticsSumGrowthWithPercentage, - fetchStatistics, - Statistics, } from "../../../../data/history"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; import { EnergyDistributionCardConfig } from "../types"; @@ -30,34 +31,30 @@ import { EnergyDistributionCardConfig } from "../types"; const CIRCLE_CIRCUMFERENCE = 238.76104; @customElement("hui-energy-distribution-card") -class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { +class HuiEnergyDistrubutionCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config?: EnergyDistributionCardConfig; - @state() private _stats?: Statistics; - - @state() private _co2SignalEntity?: string; - - private _fetching = false; + @state() private _data?: EnergyData; public setConfig(config: EnergyDistributionCardConfig): void { this._config = config; } - public getCardSize(): Promise | number { - return 3; + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => { + this._data = data; + }), + ]; } - public willUpdate(changedProps) { - super.willUpdate(changedProps); - - if (!this._fetching && !this._stats) { - this._fetching = true; - this._getStatistics().then(() => { - this._fetching = false; - }); - } + public getCardSize(): Promise | number { + return 3; } protected render() { @@ -65,11 +62,11 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { return html``; } - if (!this._stats) { + if (!this._data) { return html`Loading…`; } - const prefs = this._config!.prefs; + const prefs = this._data.prefs; const types = energySourcesByType(prefs); // The strategy only includes this card if we have a grid. @@ -80,7 +77,7 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { const totalGridConsumption = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.grid![0].flow_from.map((flow) => flow.stat_energy_from) ) ?? 0; @@ -89,7 +86,7 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { if (hasSolarProduction) { totalSolarProduction = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.solar!.map((source) => source.stat_energy_from) ) || 0; } @@ -99,7 +96,7 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { if (hasReturnToGrid) { productionReturnedToGrid = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.grid![0].flow_to.map((flow) => flow.stat_energy_to) ) || 0; } @@ -124,16 +121,21 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { let electricityMapUrl: string | undefined; - if (this._co2SignalEntity && this._co2SignalEntity in this._stats) { + if ( + this._data.co2SignalEntity && + this._data.co2SignalEntity in this._data.stats + ) { // Calculate high carbon consumption const highCarbonConsumption = calculateStatisticsSumGrowthWithPercentage( - this._stats[this._co2SignalEntity], + this._data.stats[this._data.co2SignalEntity], types - .grid![0].flow_from.map((flow) => this._stats![flow.stat_energy_from]) + .grid![0].flow_from.map( + (flow) => this._data!.stats[flow.stat_energy_from] + ) .filter(Boolean) ); - const co2State = this.hass.states[this._co2SignalEntity]; + const co2State = this.hass.states[this._data.co2SignalEntity]; if (co2State) { electricityMapUrl = `https://www.electricitymap.org/zone/${co2State.attributes.country_code}`; @@ -401,69 +403,6 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { `; } - private async _getStatistics(): Promise { - const [configEntries, entityRegistryEntries] = await Promise.all([ - getConfigEntries(this.hass), - subscribeOne(this.hass.connection, subscribeEntityRegistry), - ]); - - const co2ConfigEntry = configEntries.find( - (entry) => entry.domain === "co2signal" - ); - - this._co2SignalEntity = undefined; - - if (co2ConfigEntry) { - for (const entry of entityRegistryEntries) { - if (entry.config_entry_id !== co2ConfigEntry.entry_id) { - continue; - } - - // The integration offers 2 entities. We want the % one. - const co2State = this.hass.states[entry.entity_id]; - if (!co2State || co2State.attributes.unit_of_measurement !== "%") { - continue; - } - - this._co2SignalEntity = co2State.entity_id; - break; - } - } - - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - const statistics: string[] = []; - - if (this._co2SignalEntity !== undefined) { - statistics.push(this._co2SignalEntity); - } - - const prefs = this._config!.prefs; - for (const source of prefs.energy_sources) { - if (source.type === "solar") { - statistics.push(source.stat_energy_from); - continue; - } - - // grid source - for (const flowFrom of source.flow_from) { - statistics.push(flowFrom.stat_energy_from); - } - for (const flowTo of source.flow_to) { - statistics.push(flowTo.stat_energy_to); - } - } - - this._stats = await fetchStatistics( - this.hass!, - startDate, - undefined, - statistics - ); - } - static styles = css` :host { --mdc-icon-size: 24px; diff --git a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts index 97f8dab168..34ccbd0e98 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts @@ -1,15 +1,17 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { formatNumber } from "../../../../common/string/format_number"; import "../../../../components/ha-card"; import "../../../../components/ha-gauge"; import type { LevelDefinition } from "../../../../components/ha-gauge"; -import { GridSourceTypeEnergyPreference } from "../../../../data/energy"; import { - calculateStatisticsSumGrowth, - fetchStatistics, - Statistics, -} from "../../../../data/history"; + EnergyData, + getEnergyDataCollection, + GridSourceTypeEnergyPreference, +} from "../../../../data/energy"; +import { calculateStatisticsSumGrowth } from "../../../../data/history"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import type { LovelaceCard } from "../../types"; import type { EnergyGridGaugeCardConfig } from "../types"; @@ -21,12 +23,23 @@ const LEVELS: LevelDefinition[] = [ ]; @customElement("hui-energy-grid-neutrality-gauge-card") -class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard { +class HuiEnergyGridGaugeCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ @property({ attribute: false }) public hass?: HomeAssistant; @state() private _config?: EnergyGridGaugeCardConfig; - @state() private _stats?: Statistics; + @state() private _data?: EnergyData; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass!).subscribe((data) => { + this._data = data; + }), + ]; + } public getCardSize(): number { return 4; @@ -36,24 +49,16 @@ class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard { this._config = config; } - public willUpdate(changedProps) { - super.willUpdate(changedProps); - - if (!this.hasUpdated) { - this._getStatistics(); - } - } - protected render(): TemplateResult { if (!this._config || !this.hass) { return html``; } - if (!this._stats) { + if (!this._data) { return html`Loading...`; } - const prefs = this._config!.prefs; + const prefs = this._data.prefs; const gridSource = prefs.energy_sources.find( (src) => src.type === "grid" ) as GridSourceTypeEnergyPreference | undefined; @@ -65,12 +70,12 @@ class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard { } const consumedFromGrid = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, gridSource.flow_from.map((flow) => flow.stat_energy_from) ); const returnedToGrid = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, gridSource.flow_to.map((flow) => flow.stat_energy_to) ); @@ -111,35 +116,6 @@ class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard { `; } - private async _getStatistics(): Promise { - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - const statistics: string[] = []; - const prefs = this._config!.prefs; - for (const source of prefs.energy_sources) { - if (source.type === "solar") { - continue; - } - - // grid source - for (const flowFrom of source.flow_from) { - statistics.push(flowFrom.stat_energy_from); - } - for (const flowTo of source.flow_to) { - statistics.push(flowTo.stat_energy_to); - } - } - - this._stats = await fetchStatistics( - this.hass!, - startDate, - undefined, - statistics - ); - } - static get styles(): CSSResultGroup { return css` ha-card { diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts index 3d79fe48ab..97b6562a20 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts @@ -1,26 +1,39 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import "../../../../components/ha-card"; import "../../../../components/ha-gauge"; -import { energySourcesByType } from "../../../../data/energy"; import { - calculateStatisticsSumGrowth, - fetchStatistics, - Statistics, -} from "../../../../data/history"; + EnergyData, + energySourcesByType, + getEnergyDataCollection, +} from "../../../../data/energy"; +import { calculateStatisticsSumGrowth } from "../../../../data/history"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import type { LovelaceCard } from "../../types"; import { severityMap } from "../hui-gauge-card"; import type { EnergySolarGaugeCardConfig } from "../types"; @customElement("hui-energy-solar-consumed-gauge-card") -class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard { +class HuiEnergySolarGaugeCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ @property({ attribute: false }) public hass?: HomeAssistant; @state() private _config?: EnergySolarGaugeCardConfig; - @state() private _stats?: Statistics; + @state() private _data?: EnergyData; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass!).subscribe((data) => { + this._data = data; + }), + ]; + } public getCardSize(): number { return 4; @@ -30,33 +43,25 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard { this._config = config; } - public willUpdate(changedProps) { - super.willUpdate(changedProps); - - if (!this.hasUpdated) { - this._getStatistics(); - } - } - protected render(): TemplateResult { if (!this._config || !this.hass) { return html``; } - if (!this._stats) { + if (!this._data) { return html`Loading...`; } - const prefs = this._config!.prefs; + const prefs = this._data.prefs; const types = energySourcesByType(prefs); const totalSolarProduction = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.solar!.map((source) => source.stat_energy_from) ); const productionReturnedToGrid = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.grid![0].flow_to.map((flow) => flow.stat_energy_to) ); @@ -101,36 +106,6 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard { return severityMap.normal; } - private async _getStatistics(): Promise { - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - const statistics: string[] = []; - const prefs = this._config!.prefs; - for (const source of prefs.energy_sources) { - if (source.type === "solar") { - statistics.push(source.stat_energy_from); - continue; - } - - // grid source - for (const flowFrom of source.flow_from) { - statistics.push(flowFrom.stat_energy_from); - } - for (const flowTo of source.flow_to) { - statistics.push(flowTo.stat_energy_to); - } - } - - this._stats = await fetchStatistics( - this.hass!, - startDate, - undefined, - statistics - ); - } - static get styles(): CSSResultGroup { return css` ha-card { diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts index 391585329b..2d4fbfc92e 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts @@ -1,19 +1,13 @@ -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import memoizeOne from "memoize-one"; import { classMap } from "lit/directives/class-map"; import "../../../../components/ha-card"; import { ChartData, ChartDataset, ChartOptions } from "chart.js"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; import { EnergySolarGraphCardConfig } from "../types"; -import { fetchStatistics, Statistics } from "../../../../data/history"; import { hex2rgb, lab2rgb, @@ -21,7 +15,12 @@ import { rgb2lab, } from "../../../../common/color/convert-color"; import { labDarken } from "../../../../common/color/lab"; -import { SolarSourceTypeEnergyPreference } from "../../../../data/energy"; +import { + EnergyCollection, + EnergyData, + getEnergyDataCollection, + SolarSourceTypeEnergyPreference, +} from "../../../../data/energy"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { ForecastSolarForecast, @@ -35,52 +34,32 @@ import { formatNumber, numberFormatToLocale, } from "../../../../common/string/format_number"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; +import { FrontendLocaleData } from "../../../../data/translation"; @customElement("hui-energy-solar-graph-card") export class HuiEnergySolarGraphCard - extends LitElement + extends SubscribeMixin(LitElement) implements LovelaceCard { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config?: EnergySolarGraphCardConfig; - @state() private _data?: Statistics; - @state() private _chartData: ChartData = { datasets: [], }; @state() private _forecasts?: Record; - @state() private _chartOptions?: ChartOptions; - @state() private _showAllForecastData = false; - private _fetching = false; - - private _interval?: number; - - public disconnectedCallback() { - super.disconnectedCallback(); - if (this._interval) { - clearInterval(this._interval); - this._interval = undefined; - } - } - - public connectedCallback() { - super.connectedCallback(); - if (!this.hasUpdated) { - return; - } - this._getStatistics(); - // statistics are created every hour - clearInterval(this._interval); - this._interval = window.setInterval( - () => this._getStatistics(), - 1000 * 60 * 60 - ); + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => + this._getStatistics(data) + ), + ]; } public getCardSize(): Promise | number { @@ -91,30 +70,6 @@ export class HuiEnergySolarGraphCard this._config = config; } - public willUpdate(changedProps: PropertyValues) { - super.willUpdate(changedProps); - if (!this.hasUpdated) { - this._createOptions(); - } - if (!this._config || !changedProps.has("_config")) { - return; - } - - const oldConfig = changedProps.get("_config") as - | EnergySolarGraphCardConfig - | undefined; - - if (oldConfig !== this._config) { - this._getStatistics(); - // statistics are created every hour - clearInterval(this._interval); - this._interval = window.setInterval( - () => this._getStatistics(), - 1000 * 60 * 60 - ); - } - } - protected render(): TemplateResult { if (!this.hass || !this._config) { return html``; @@ -132,7 +87,10 @@ export class HuiEnergySolarGraphCard > @@ -140,118 +98,100 @@ export class HuiEnergySolarGraphCard `; } - private _createOptions() { - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - const startTime = startDate.getTime(); + private _createOptions = memoizeOne( + ( + energyCollection: EnergyCollection, + locale: FrontendLocaleData + ): ChartOptions => { + const startTime = energyCollection.start.getTime(); - this._chartOptions = { - parsing: false, - animation: false, - scales: { - x: { - type: "time", - suggestedMin: startTime, - suggestedMax: startTime + 24 * 60 * 60 * 1000, - adapters: { - date: { - locale: this.hass.locale, + return { + parsing: false, + animation: false, + scales: { + x: { + type: "time", + suggestedMin: startTime, + suggestedMax: startTime + 24 * 60 * 60 * 1000, + adapters: { + date: { + locale: locale, + }, + }, + ticks: { + maxRotation: 0, + sampleSize: 5, + autoSkipPadding: 20, + major: { + enabled: true, + }, + font: (context) => + context.tick && context.tick.major + ? ({ weight: "bold" } as any) + : {}, + }, + time: { + tooltipFormat: "datetime", + }, + offset: true, + }, + y: { + type: "linear", + title: { + display: true, + text: "kWh", + }, + ticks: { + beginAtZero: true, }, }, - ticks: { - maxRotation: 0, - sampleSize: 5, - autoSkipPadding: 20, - major: { - enabled: true, + }, + plugins: { + tooltip: { + mode: "nearest", + callbacks: { + label: (context) => + `${context.dataset.label}: ${formatNumber( + context.parsed.y, + locale + )} kWh`, }, - font: (context) => - context.tick && context.tick.major - ? ({ weight: "bold" } as any) - : {}, }, - time: { - tooltipFormat: "datetime", + filler: { + propagate: false, }, - offset: true, - }, - y: { - type: "linear", - title: { - display: true, - text: "kWh", - }, - ticks: { - beginAtZero: true, + legend: { + display: false, + labels: { + usePointStyle: true, + }, }, }, - }, - plugins: { - tooltip: { + hover: { mode: "nearest", - callbacks: { - label: (context) => - `${context.dataset.label}: ${formatNumber( - context.parsed.y, - this.hass.locale - )} kWh`, + }, + elements: { + line: { + tension: 0.3, + borderWidth: 1.5, + }, + bar: { borderWidth: 1.5, borderRadius: 4 }, + point: { + hitRadius: 5, }, }, - filler: { - propagate: false, - }, - legend: { - display: false, - labels: { - usePointStyle: true, - }, - }, - }, - hover: { - mode: "nearest", - }, - elements: { - line: { - tension: 0.3, - borderWidth: 1.5, - }, - bar: { borderWidth: 1.5, borderRadius: 4 }, - point: { - hitRadius: 5, - }, - }, - // @ts-expect-error - locale: numberFormatToLocale(this.hass.locale), - }; - } - - private async _getStatistics(): Promise { - if (this._fetching) { - return; + // @ts-expect-error + locale: numberFormatToLocale(locale), + }; } + ); - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - this._fetching = true; - + private async _getStatistics(energyData: EnergyData): Promise { const solarSources: SolarSourceTypeEnergyPreference[] = - this._config!.prefs.energy_sources.filter( + energyData.prefs.energy_sources.filter( (source) => source.type === "solar" ) as SolarSourceTypeEnergyPreference[]; - try { - this._data = await fetchStatistics( - this.hass!, - startDate, - undefined, - solarSources.map((source) => source.stat_energy_from) - ); - } finally { - this._fetching = false; - } - if ( isComponentLoaded(this.hass, "forecast_solar") && solarSources.some((source) => source.config_entry_solar_forecast) @@ -259,16 +199,7 @@ export class HuiEnergySolarGraphCard this._forecasts = await getForecastSolarForecasts(this.hass); } - this._renderChart(); - } - - private _renderChart() { - const solarSources: SolarSourceTypeEnergyPreference[] = - this._config!.prefs.energy_sources.filter( - (source) => source.type === "solar" - ) as SolarSourceTypeEnergyPreference[]; - - const statisticsData = Object.values(this._data!); + const statisticsData = Object.values(energyData.stats); const datasets: ChartDataset<"bar">[] = []; let endTime: Date; @@ -311,8 +242,8 @@ export class HuiEnergySolarGraphCard let prevStart: string | null = null; // Process solar production data. - if (this._data![source.stat_energy_from]) { - for (const point of this._data![source.stat_energy_from]) { + if (energyData.stats[source.stat_energy_from]) { + for (const point of energyData.stats[source.stat_energy_from]) { if (!point.sum) { continue; } diff --git a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts index 83252fe9fe..68c1615ac6 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts @@ -1,5 +1,6 @@ // @ts-ignore import dataTableStyles from "@material/data-table/dist/mdc.data-table.min.css"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, @@ -22,31 +23,34 @@ import { formatNumber } from "../../../../common/string/format_number"; import "../../../../components/chart/statistics-chart"; import "../../../../components/ha-card"; import { - EnergyInfo, + EnergyData, energySourcesByType, - getEnergyInfo, + getEnergyDataCollection, } from "../../../../data/energy"; -import { - calculateStatisticSumGrowth, - fetchStatistics, - Statistics, -} from "../../../../data/history"; +import { calculateStatisticSumGrowth } from "../../../../data/history"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; import { EnergySourcesTableCardConfig } from "../types"; @customElement("hui-energy-sources-table-card") export class HuiEnergySourcesTableCard - extends LitElement + extends SubscribeMixin(LitElement) implements LovelaceCard { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config?: EnergySourcesTableCardConfig; - @state() private _stats?: Statistics; + @state() private _data?: EnergyData; - @state() private _energyInfo?: EnergyInfo; + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => { + this._data = data; + }), + ]; + } public getCardSize(): Promise | number { return 3; @@ -56,18 +60,12 @@ export class HuiEnergySourcesTableCard this._config = config; } - public willUpdate() { - if (!this.hasUpdated) { - this._getEnergyInfo().then(() => this._getStatistics()); - } - } - protected render(): TemplateResult { if (!this.hass || !this._config) { return html``; } - if (!this._stats) { + if (!this._data) { return html`Loading...`; } @@ -75,7 +73,7 @@ export class HuiEnergySourcesTableCard let totalSolar = 0; let totalCost = 0; - const types = energySourcesByType(this._config.prefs); + const types = energySourcesByType(this._data.prefs); const computedStyles = getComputedStyle(this); const solarColor = computedStyles @@ -140,7 +138,7 @@ export class HuiEnergySourcesTableCard const entity = this.hass.states[source.stat_energy_from]; const energy = calculateStatisticSumGrowth( - this._stats![source.stat_energy_from] + this._data!.stats[source.stat_energy_from] ) || 0; totalSolar += energy; const color = @@ -195,14 +193,16 @@ export class HuiEnergySourcesTableCard const entity = this.hass.states[flow.stat_energy_from]; const energy = calculateStatisticSumGrowth( - this._stats![flow.stat_energy_from] + this._data!.stats[flow.stat_energy_from] ) || 0; totalGrid += energy; const cost_stat = flow.stat_cost || - this._energyInfo!.cost_sensors[flow.stat_energy_from]; + this._data!.info.cost_sensors[flow.stat_energy_from]; const cost = cost_stat - ? calculateStatisticSumGrowth(this._stats![cost_stat]) || 0 + ? calculateStatisticSumGrowth( + this._data!.stats[cost_stat] + ) || 0 : null; if (cost !== null) { totalCost += cost; @@ -253,15 +253,16 @@ export class HuiEnergySourcesTableCard const entity = this.hass.states[flow.stat_energy_to]; const energy = (calculateStatisticSumGrowth( - this._stats![flow.stat_energy_to] + this._data!.stats[flow.stat_energy_to] ) || 0) * -1; totalGrid += energy; const cost_stat = flow.stat_compensation || - this._energyInfo!.cost_sensors[flow.stat_energy_to]; + this._data!.info.cost_sensors[flow.stat_energy_to]; const cost = cost_stat - ? (calculateStatisticSumGrowth(this._stats![cost_stat]) || - 0) * -1 + ? (calculateStatisticSumGrowth( + this._data!.stats[cost_stat] + ) || 0) * -1 : null; if (cost !== null) { totalCost += cost; @@ -333,45 +334,6 @@ export class HuiEnergySourcesTableCard `; } - private async _getEnergyInfo() { - this._energyInfo = await getEnergyInfo(this.hass); - } - - private async _getStatistics(): Promise { - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - const statistics: string[] = Object.values(this._energyInfo!.cost_sensors); - const prefs = this._config!.prefs; - for (const source of prefs.energy_sources) { - if (source.type === "solar") { - statistics.push(source.stat_energy_from); - } else { - // grid source - for (const flowFrom of source.flow_from) { - statistics.push(flowFrom.stat_energy_from); - if (flowFrom.stat_cost) { - statistics.push(flowFrom.stat_cost); - } - } - for (const flowTo of source.flow_to) { - statistics.push(flowTo.stat_energy_to); - if (flowTo.stat_compensation) { - statistics.push(flowTo.stat_compensation); - } - } - } - } - - this._stats = await fetchStatistics( - this.hass!, - startDate, - undefined, - statistics - ); - } - static get styles(): CSSResultGroup { return css` ${unsafeCSS(dataTableStyles)} diff --git a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts index 5c455a4865..270e6488ca 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts @@ -1,14 +1,9 @@ import { ChartData, ChartDataset, ChartOptions } from "chart.js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import memoizeOne from "memoize-one"; import { hex2rgb, lab2rgb, @@ -24,52 +19,36 @@ import { } from "../../../../common/string/format_number"; import "../../../../components/chart/ha-chart-base"; import "../../../../components/ha-card"; -import { fetchStatistics, Statistics } from "../../../../data/history"; +import { + EnergyCollection, + EnergyData, + getEnergyDataCollection, +} from "../../../../data/energy"; +import { FrontendLocaleData } from "../../../../data/translation"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; import { EnergyUsageGraphCardConfig } from "../types"; @customElement("hui-energy-usage-graph-card") export class HuiEnergyUsageGraphCard - extends LitElement + extends SubscribeMixin(LitElement) implements LovelaceCard { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config?: EnergyUsageGraphCardConfig; - @state() private _data?: Statistics; - @state() private _chartData: ChartData = { datasets: [], }; - @state() private _chartOptions?: ChartOptions; - - private _fetching = false; - - private _interval?: number; - - public disconnectedCallback() { - super.disconnectedCallback(); - if (this._interval) { - clearInterval(this._interval); - this._interval = undefined; - } - } - - public connectedCallback() { - super.connectedCallback(); - if (!this.hasUpdated) { - return; - } - this._getStatistics(); - // statistics are created every hour - clearInterval(this._interval); - this._interval = window.setInterval( - () => this._getStatistics(), - 1000 * 60 * 60 - ); + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => + this._getStatistics(data) + ), + ]; } public getCardSize(): Promise | number { @@ -80,30 +59,6 @@ export class HuiEnergyUsageGraphCard this._config = config; } - public willUpdate(changedProps: PropertyValues) { - super.willUpdate(changedProps); - if (!this.hasUpdated) { - this._createOptions(); - } - if (!this._config || !changedProps.has("_config")) { - return; - } - - const oldConfig = changedProps.get("_config") as - | EnergyUsageGraphCardConfig - | undefined; - - if (oldConfig !== this._config) { - this._getStatistics(); - // statistics are created every hour - clearInterval(this._interval); - this._interval = window.setInterval( - () => this._getStatistics(), - 1000 * 60 * 60 - ); - } - } - protected render(): TemplateResult { if (!this.hass || !this._config) { return html``; @@ -121,7 +76,10 @@ export class HuiEnergyUsageGraphCard > @@ -129,137 +87,130 @@ export class HuiEnergyUsageGraphCard `; } - private _createOptions() { - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - const startTime = startDate.getTime(); + private _createOptions = memoizeOne( + ( + energyCollection: EnergyCollection, + locale: FrontendLocaleData + ): ChartOptions => { + const startTime = energyCollection.start.getTime(); - this._chartOptions = { - parsing: false, - animation: false, - scales: { - x: { - type: "time", - suggestedMin: startTime, - suggestedMax: startTime + 24 * 60 * 60 * 1000, - adapters: { - date: { - locale: this.hass.locale, + return { + parsing: false, + animation: false, + scales: { + x: { + type: "time", + suggestedMin: startTime, + suggestedMax: startTime + 24 * 60 * 60 * 1000, + adapters: { + date: { + locale: locale, + }, + }, + ticks: { + maxRotation: 0, + sampleSize: 5, + autoSkipPadding: 20, + major: { + enabled: true, + }, + font: (context) => + context.tick && context.tick.major + ? ({ weight: "bold" } as any) + : {}, + }, + time: { + tooltipFormat: "datetime", + }, + offset: true, + }, + y: { + stacked: true, + type: "linear", + title: { + display: true, + text: "kWh", + }, + ticks: { + beginAtZero: true, + callback: (value) => formatNumber(Math.abs(value), locale), }, }, - ticks: { - maxRotation: 0, - sampleSize: 5, - autoSkipPadding: 20, - major: { - enabled: true, - }, - font: (context) => - context.tick && context.tick.major - ? ({ weight: "bold" } as any) - : {}, - }, - time: { - tooltipFormat: "datetime", - }, - offset: true, }, - y: { - stacked: true, - type: "linear", - title: { - display: true, - text: "kWh", - }, - ticks: { - beginAtZero: true, - callback: (value) => - formatNumber(Math.abs(value), this.hass.locale), - }, - }, - }, - plugins: { - tooltip: { - mode: "x", - intersect: true, - position: "nearest", - filter: (val) => val.formattedValue !== "0", - callbacks: { - label: (context) => - `${context.dataset.label}: ${formatNumber( - Math.abs(context.parsed.y), - this.hass.locale - )} kWh`, - footer: (contexts) => { - let totalConsumed = 0; - let totalReturned = 0; - for (const context of contexts) { - const value = (context.dataset.data[context.dataIndex] as any) - .y; - if (value > 0) { - totalConsumed += value; - } else { - totalReturned += Math.abs(value); + plugins: { + tooltip: { + mode: "x", + intersect: true, + position: "nearest", + filter: (val) => val.formattedValue !== "0", + callbacks: { + label: (context) => + `${context.dataset.label}: ${formatNumber( + Math.abs(context.parsed.y), + locale + )} kWh`, + footer: (contexts) => { + let totalConsumed = 0; + let totalReturned = 0; + for (const context of contexts) { + const value = (context.dataset.data[context.dataIndex] as any) + .y; + if (value > 0) { + totalConsumed += value; + } else { + totalReturned += Math.abs(value); + } } - } - return [ - totalConsumed - ? `Total consumed: ${formatNumber( - totalConsumed, - this.hass.locale - )} kWh` - : "", - totalReturned - ? `Total returned: ${formatNumber( - totalReturned, - this.hass.locale - )} kWh` - : "", - ].filter(Boolean); + return [ + totalConsumed + ? `Total consumed: ${formatNumber( + totalConsumed, + locale + )} kWh` + : "", + totalReturned + ? `Total returned: ${formatNumber( + totalReturned, + locale + )} kWh` + : "", + ].filter(Boolean); + }, + }, + }, + filler: { + propagate: false, + }, + legend: { + display: false, + labels: { + usePointStyle: true, }, }, }, - filler: { - propagate: false, + hover: { + mode: "nearest", }, - legend: { - display: false, - labels: { - usePointStyle: true, + elements: { + bar: { borderWidth: 1.5, borderRadius: 4 }, + point: { + hitRadius: 5, }, }, - }, - hover: { - mode: "nearest", - }, - elements: { - bar: { borderWidth: 1.5, borderRadius: 4 }, - point: { - hitRadius: 5, - }, - }, - // @ts-expect-error - locale: numberFormatToLocale(this.hass.locale), - }; - } - - private async _getStatistics(): Promise { - if (this._fetching) { - return; + // @ts-expect-error + locale: numberFormatToLocale(locale), + }; } - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint + ); - this._fetching = true; - const prefs = this._config!.prefs; + private async _getStatistics(energyData: EnergyData): Promise { const statistics: { to_grid?: string[]; from_grid?: string[]; solar?: string[]; } = {}; - for (const source of prefs.energy_sources) { + for (const source of energyData.prefs.energy_sources) { if (source.type === "solar") { if (statistics.solar) { statistics.solar.push(source.stat_energy_from); @@ -286,19 +237,7 @@ export class HuiEnergyUsageGraphCard } } - try { - this._data = await fetchStatistics( - this.hass!, - startDate, - undefined, - // Array.flat() - ([] as string[]).concat(...Object.values(statistics)) - ); - } finally { - this._fetching = false; - } - - const statisticsData = Object.values(this._data!); + const statisticsData = Object.values(energyData.stats); const datasets: ChartDataset<"bar">[] = []; let endTime: Date; @@ -346,7 +285,7 @@ export class HuiEnergyUsageGraphCard const totalStats: { [start: string]: number } = {}; const sets: { [statId: string]: { [start: string]: number } } = {}; statIds!.forEach((id) => { - const stats = this._data![id]; + const stats = energyData.stats[id]; if (!stats) { return; } diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 4894837543..f1b5ff096b 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -1,4 +1,3 @@ -import { EnergyPreferences } from "../../../data/energy"; import { StatisticType } from "../../../data/history"; import { ActionConfig, LovelaceCardConfig } from "../../../data/lovelace"; import { FullCalendarView } from "../../../types"; @@ -93,54 +92,45 @@ export interface ButtonCardConfig extends LovelaceCardConfig { export interface EnergySummaryCardConfig extends LovelaceCardConfig { type: "energy-summary"; title?: string; - prefs: EnergyPreferences; } export interface EnergyDistributionCardConfig extends LovelaceCardConfig { type: "energy-distribution"; title?: string; - prefs: EnergyPreferences; } export interface EnergyUsageGraphCardConfig extends LovelaceCardConfig { type: "energy-summary-graph"; title?: string; - prefs: EnergyPreferences; } export interface EnergySolarGraphCardConfig extends LovelaceCardConfig { type: "energy-solar-graph"; title?: string; - prefs: EnergyPreferences; } export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig { type: "energy-devices-graph"; title?: string; - prefs: EnergyPreferences; } export interface EnergySourcesTableCardConfig extends LovelaceCardConfig { type: "energy-sources-table"; title?: string; - prefs: EnergyPreferences; } export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig { type: "energy-solar-consumed-gauge"; title?: string; - prefs: EnergyPreferences; } export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig { type: "energy-grid-result-gauge"; title?: string; - prefs: EnergyPreferences; } export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig { type: "energy-carbon-consumed-gauge"; title?: string; - prefs: EnergyPreferences; } export interface EntityFilterCardConfig extends LovelaceCardConfig {