Speed up data loading and allow embedding individual energy cards (#9660)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Paulus Schoutsen 2021-07-31 09:33:41 -07:00 committed by GitHub
parent 539d2b880c
commit 0f16ba9325
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 558 additions and 838 deletions

View File

@ -1,4 +1,9 @@
import { Collection, getCollection } from "home-assistant-js-websocket";
import { subscribeOne } from "../common/util/subscribe-one";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { ConfigEntry, getConfigEntries } from "./config_entries";
import { subscribeEntityRegistry } from "./entity_registry";
import { fetchStatistics, Statistics } from "./history";
export const emptyFlowFromGridSourceEnergyPreference = export const emptyFlowFromGridSourceEnergyPreference =
(): FlowFromGridSourceEnergyPreference => ({ (): FlowFromGridSourceEnergyPreference => ({
@ -128,3 +133,137 @@ export const energySourcesByType = (prefs: EnergyPreferences) => {
} }
return types; 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<EnergyData> => {
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<EnergyData> {
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<EnergyData>(
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;
};

View File

@ -1,5 +1,6 @@
import { import {
EnergyPreferences, EnergyPreferences,
getEnergyDataCollection,
getEnergyPreferences, getEnergyPreferences,
GridSourceTypeEnergyPreference, GridSourceTypeEnergyPreference,
} from "../../../data/energy"; } from "../../../data/energy";
@ -26,10 +27,10 @@ export class EnergyStrategy {
const view: LovelaceViewConfig = { cards: [] }; const view: LovelaceViewConfig = { cards: [] };
let energyPrefs: EnergyPreferences; let prefs: EnergyPreferences;
try { try {
energyPrefs = await getEnergyPreferences(hass); prefs = await getEnergyPreferences(hass);
} catch (e) { } catch (e) {
if (e.code === "not_found") { if (e.code === "not_found") {
return setupWizard(); return setupWizard();
@ -43,20 +44,21 @@ export class EnergyStrategy {
view.type = "sidebar"; view.type = "sidebar";
const hasGrid = energyPrefs.energy_sources.find( const hasGrid = prefs.energy_sources.find(
(source) => source.type === "grid" (source) => source.type === "grid"
) as GridSourceTypeEnergyPreference; ) as GridSourceTypeEnergyPreference;
const hasReturn = hasGrid && hasGrid.flow_to.length; const hasReturn = hasGrid && hasGrid.flow_to.length;
const hasSolar = energyPrefs.energy_sources.some( const hasSolar = prefs.energy_sources.some(
(source) => source.type === "solar" (source) => source.type === "solar"
); );
getEnergyDataCollection(hass, prefs);
// Only include if we have a grid source. // Only include if we have a grid source.
if (hasGrid) { if (hasGrid) {
view.cards!.push({ view.cards!.push({
title: "Energy usage", title: "Energy usage",
type: "energy-usage-graph", type: "energy-usage-graph",
prefs: energyPrefs,
}); });
} }
@ -65,7 +67,6 @@ export class EnergyStrategy {
view.cards!.push({ view.cards!.push({
title: "Solar production", title: "Solar production",
type: "energy-solar-graph", type: "energy-solar-graph",
prefs: energyPrefs,
}); });
} }
@ -74,7 +75,6 @@ export class EnergyStrategy {
view.cards!.push({ view.cards!.push({
title: "Energy distribution", title: "Energy distribution",
type: "energy-distribution", type: "energy-distribution",
prefs: energyPrefs,
view_layout: { position: "sidebar" }, view_layout: { position: "sidebar" },
}); });
} }
@ -83,16 +83,6 @@ export class EnergyStrategy {
view.cards!.push({ view.cards!.push({
title: "Sources", title: "Sources",
type: "energy-sources-table", 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) { if (hasReturn) {
view.cards!.push({ view.cards!.push({
type: "energy-grid-neutrality-gauge", 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" }, view_layout: { position: "sidebar" },
}); });
} }
@ -109,17 +106,15 @@ export class EnergyStrategy {
if (hasGrid) { if (hasGrid) {
view.cards!.push({ view.cards!.push({
type: "energy-carbon-consumed-gauge", type: "energy-carbon-consumed-gauge",
prefs: energyPrefs,
view_layout: { position: "sidebar" }, view_layout: { position: "sidebar" },
}); });
} }
// Only include if we have at least 1 device in the config. // 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({ view.cards!.push({
title: "Monitor individual devices", title: "Monitor individual devices",
type: "energy-devices-graph", type: "energy-devices-graph",
prefs: energyPrefs,
}); });
} }

View File

@ -1,19 +1,20 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { round } from "../../../../common/number/round"; import { round } from "../../../../common/number/round";
import { subscribeOne } from "../../../../common/util/subscribe-one";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-gauge"; import "../../../../components/ha-gauge";
import { getConfigEntries } from "../../../../data/config_entries"; import {
import { energySourcesByType } from "../../../../data/energy"; EnergyData,
import { subscribeEntityRegistry } from "../../../../data/entity_registry"; energySourcesByType,
getEnergyDataCollection,
} from "../../../../data/energy";
import { import {
calculateStatisticsSumGrowth, calculateStatisticsSumGrowth,
calculateStatisticsSumGrowthWithPercentage, calculateStatisticsSumGrowthWithPercentage,
fetchStatistics,
Statistics,
} from "../../../../data/history"; } from "../../../../data/history";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { createEntityNotFoundWarning } from "../../components/hui-warning"; import { createEntityNotFoundWarning } from "../../components/hui-warning";
import type { LovelaceCard } from "../../types"; import type { LovelaceCard } from "../../types";
@ -21,14 +22,15 @@ import { severityMap } from "../hui-gauge-card";
import type { EnergyCarbonGaugeCardConfig } from "../types"; import type { EnergyCarbonGaugeCardConfig } from "../types";
@customElement("hui-energy-carbon-consumed-gauge-card") @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; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergyCarbonGaugeCardConfig; @state() private _config?: EnergyCarbonGaugeCardConfig;
@state() private _stats?: Statistics; @state() private _data?: EnergyData;
@state() private _co2SignalEntity?: string | null;
public getCardSize(): number { public getCardSize(): number {
return 4; return 4;
@ -38,12 +40,12 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
this._config = config; this._config = config;
} }
public willUpdate(changedProps) { public hassSubscribe(): UnsubscribeFunc[] {
super.willUpdate(changedProps); return [
getEnergyDataCollection(this.hass).subscribe((data) => {
if (!this.hasUpdated) { this._data = data;
this._getStatistics(); }),
} ];
} }
protected render(): TemplateResult { protected render(): TemplateResult {
@ -51,52 +53,55 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
return html``; return html``;
} }
if (this._co2SignalEntity === null) { if (!this._data) {
return html``;
}
if (!this._stats || !this._co2SignalEntity) {
return html`Loading...`; 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) { if (!co2State) {
return html`<hui-warning> return html`<hui-warning>
${createEntityNotFoundWarning(this.hass, this._co2SignalEntity)} ${createEntityNotFoundWarning(this.hass, this._data.co2SignalEntity)}
</hui-warning>`; </hui-warning>`;
} }
const prefs = this._config!.prefs; const prefs = this._data.prefs;
const types = energySourcesByType(prefs); const types = energySourcesByType(prefs);
const totalGridConsumption = calculateStatisticsSumGrowth( const totalGridConsumption = calculateStatisticsSumGrowth(
this._stats, this._data.stats,
types.grid![0].flow_from.map((flow) => flow.stat_energy_from) types.grid![0].flow_from.map((flow) => flow.stat_energy_from)
); );
let value: number | undefined; let value: number | undefined;
if (this._co2SignalEntity in this._stats && totalGridConsumption) { if (
this._data.co2SignalEntity in this._data.stats &&
totalGridConsumption
) {
const highCarbonEnergy = const highCarbonEnergy =
calculateStatisticsSumGrowthWithPercentage( calculateStatisticsSumGrowthWithPercentage(
this._stats[this._co2SignalEntity], this._data.stats[this._data.co2SignalEntity],
types types
.grid![0].flow_from.map( .grid![0].flow_from.map(
(flow) => this._stats![flow.stat_energy_from] (flow) => this._data!.stats![flow.stat_energy_from]
) )
.filter(Boolean) .filter(Boolean)
) || 0; ) || 0;
const totalSolarProduction = types.solar const totalSolarProduction = types.solar
? calculateStatisticsSumGrowth( ? calculateStatisticsSumGrowth(
this._stats, this._data.stats,
types.solar.map((source) => source.stat_energy_from) types.solar.map((source) => source.stat_energy_from)
) )
: undefined; : undefined;
const totalGridReturned = calculateStatisticsSumGrowth( const totalGridReturned = calculateStatisticsSumGrowth(
this._stats, this._data.stats,
types.grid![0].flow_to.map((flow) => flow.stat_energy_to) types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
); );
@ -139,78 +144,6 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
return severityMap.normal; 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<void> {
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 { static get styles(): CSSResultGroup {
return css` return css`
ha-card { ha-card {

View File

@ -4,16 +4,11 @@ import {
ChartOptions, ChartOptions,
ParsedDataType, ParsedDataType,
} from "chart.js"; } from "chart.js";
import { import { UnsubscribeFunc } from "home-assistant-js-websocket";
css, import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { getColorByIndex } from "../../../../common/color/colors"; import { getColorByIndex } from "../../../../common/color/colors";
import { computeStateName } from "../../../../common/entity/compute_state_name"; import { computeStateName } from "../../../../common/entity/compute_state_name";
import { import {
@ -22,18 +17,21 @@ import {
} from "../../../../common/string/format_number"; } from "../../../../common/string/format_number";
import "../../../../components/chart/ha-chart-base"; import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
import { import {
calculateStatisticSumGrowth, calculateStatisticSumGrowth,
fetchStatistics, fetchStatistics,
Statistics, Statistics,
} from "../../../../data/history"; } from "../../../../data/history";
import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types"; import { LovelaceCard } from "../../types";
import { EnergyDevicesGraphCardConfig } from "../types"; import { EnergyDevicesGraphCardConfig } from "../types";
@customElement("hui-energy-devices-graph-card") @customElement("hui-energy-devices-graph-card")
export class HuiEnergyDevicesGraphCard export class HuiEnergyDevicesGraphCard
extends LitElement extends SubscribeMixin(LitElement)
implements LovelaceCard implements LovelaceCard
{ {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -44,32 +42,12 @@ export class HuiEnergyDevicesGraphCard
@state() private _chartData?: ChartData; @state() private _chartData?: ChartData;
@state() private _chartOptions?: ChartOptions; public hassSubscribe(): UnsubscribeFunc[] {
return [
private _fetching = false; getEnergyDataCollection(this.hass).subscribe((data) =>
this._getStatistics(data)
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 getCardSize(): Promise<number> | number { public getCardSize(): Promise<number> | number {
@ -80,30 +58,6 @@ export class HuiEnergyDevicesGraphCard
this._config = config; 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 { protected render(): TemplateResult {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return html``; return html``;
@ -122,7 +76,7 @@ export class HuiEnergyDevicesGraphCard
${this._chartData ${this._chartData
? html`<ha-chart-base ? html`<ha-chart-base
.data=${this._chartData} .data=${this._chartData}
.options=${this._chartOptions} .options=${this._createOptions(this.hass.locale)}
chart-type="bar" chart-type="bar"
></ha-chart-base>` ></ha-chart-base>`
: ""} : ""}
@ -131,8 +85,8 @@ export class HuiEnergyDevicesGraphCard
`; `;
} }
private _createOptions() { private _createOptions = memoizeOne(
this._chartOptions = { (locale: FrontendLocaleData): ChartOptions => ({
parsing: false, parsing: false,
animation: false, animation: false,
responsive: true, responsive: true,
@ -153,37 +107,24 @@ export class HuiEnergyDevicesGraphCard
label: (context) => label: (context) =>
`${context.dataset.label}: ${formatNumber( `${context.dataset.label}: ${formatNumber(
context.parsed.x, context.parsed.x,
this.hass.locale locale
)} kWh`, )} kWh`,
}, },
}, },
}, },
// @ts-expect-error // @ts-expect-error
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(this.hass.locale),
}; })
} );
private async _getStatistics(): Promise<void> { private async _getStatistics(energyData: EnergyData): Promise<void> {
if (this._fetching) { const energyCollection = getEnergyDataCollection(this.hass);
return; this._data = await fetchStatistics(
} this.hass,
const startDate = new Date(); energyCollection.start,
startDate.setHours(0, 0, 0, 0); energyCollection.end,
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint energyCollection.getDeviceStatIds()
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;
}
const statisticsData = Object.values(this._data!); const statisticsData = Object.values(this._data!);
let endTime: Date; let endTime: Date;
@ -213,8 +154,8 @@ export class HuiEnergyDevicesGraphCard
}, },
]; ];
for (let idx = 0; idx < prefs.device_consumption.length; idx++) { for (let idx = 0; idx < energyData.prefs.device_consumption.length; idx++) {
const device = prefs.device_consumption[idx]; const device = energyData.prefs.device_consumption[idx];
const entity = this.hass.states[device.stat_consumption]; const entity = this.hass.states[device.stat_consumption];
const label = entity ? computeStateName(entity) : device.stat_consumption; const label = entity ? computeStateName(entity) : device.stat_consumption;

View File

@ -6,23 +6,24 @@ import {
mdiSolarPower, mdiSolarPower,
mdiTransmissionTower, mdiTransmissionTower,
} from "@mdi/js"; } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, svg } from "lit"; import { css, html, LitElement, svg } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { formatNumber } from "../../../../common/string/format_number"; import { formatNumber } from "../../../../common/string/format_number";
import { subscribeOne } from "../../../../common/util/subscribe-one";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { getConfigEntries } from "../../../../data/config_entries"; import {
import { energySourcesByType } from "../../../../data/energy"; EnergyData,
import { subscribeEntityRegistry } from "../../../../data/entity_registry"; energySourcesByType,
getEnergyDataCollection,
} from "../../../../data/energy";
import { import {
calculateStatisticsSumGrowth, calculateStatisticsSumGrowth,
calculateStatisticsSumGrowthWithPercentage, calculateStatisticsSumGrowthWithPercentage,
fetchStatistics,
Statistics,
} from "../../../../data/history"; } from "../../../../data/history";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types"; import { LovelaceCard } from "../../types";
import { EnergyDistributionCardConfig } from "../types"; import { EnergyDistributionCardConfig } from "../types";
@ -30,46 +31,42 @@ import { EnergyDistributionCardConfig } from "../types";
const CIRCLE_CIRCUMFERENCE = 238.76104; const CIRCLE_CIRCUMFERENCE = 238.76104;
@customElement("hui-energy-distribution-card") @customElement("hui-energy-distribution-card")
class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { class HuiEnergyDistrubutionCard
extends SubscribeMixin(LitElement)
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergyDistributionCardConfig; @state() private _config?: EnergyDistributionCardConfig;
@state() private _stats?: Statistics; @state() private _data?: EnergyData;
@state() private _co2SignalEntity?: string;
private _fetching = false;
public setConfig(config: EnergyDistributionCardConfig): void { public setConfig(config: EnergyDistributionCardConfig): void {
this._config = config; this._config = config;
} }
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass).subscribe((data) => {
this._data = data;
}),
];
}
public getCardSize(): Promise<number> | number { public getCardSize(): Promise<number> | number {
return 3; return 3;
} }
public willUpdate(changedProps) {
super.willUpdate(changedProps);
if (!this._fetching && !this._stats) {
this._fetching = true;
this._getStatistics().then(() => {
this._fetching = false;
});
}
}
protected render() { protected render() {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
if (!this._stats) { if (!this._data) {
return html`Loading…`; return html`Loading…`;
} }
const prefs = this._config!.prefs; const prefs = this._data.prefs;
const types = energySourcesByType(prefs); const types = energySourcesByType(prefs);
// The strategy only includes this card if we have a grid. // The strategy only includes this card if we have a grid.
@ -80,7 +77,7 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
const totalGridConsumption = const totalGridConsumption =
calculateStatisticsSumGrowth( calculateStatisticsSumGrowth(
this._stats, this._data.stats,
types.grid![0].flow_from.map((flow) => flow.stat_energy_from) types.grid![0].flow_from.map((flow) => flow.stat_energy_from)
) ?? 0; ) ?? 0;
@ -89,7 +86,7 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
if (hasSolarProduction) { if (hasSolarProduction) {
totalSolarProduction = totalSolarProduction =
calculateStatisticsSumGrowth( calculateStatisticsSumGrowth(
this._stats, this._data.stats,
types.solar!.map((source) => source.stat_energy_from) types.solar!.map((source) => source.stat_energy_from)
) || 0; ) || 0;
} }
@ -99,7 +96,7 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
if (hasReturnToGrid) { if (hasReturnToGrid) {
productionReturnedToGrid = productionReturnedToGrid =
calculateStatisticsSumGrowth( calculateStatisticsSumGrowth(
this._stats, this._data.stats,
types.grid![0].flow_to.map((flow) => flow.stat_energy_to) types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
) || 0; ) || 0;
} }
@ -124,16 +121,21 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
let electricityMapUrl: string | undefined; 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 // Calculate high carbon consumption
const highCarbonConsumption = calculateStatisticsSumGrowthWithPercentage( const highCarbonConsumption = calculateStatisticsSumGrowthWithPercentage(
this._stats[this._co2SignalEntity], this._data.stats[this._data.co2SignalEntity],
types 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) .filter(Boolean)
); );
const co2State = this.hass.states[this._co2SignalEntity]; const co2State = this.hass.states[this._data.co2SignalEntity];
if (co2State) { if (co2State) {
electricityMapUrl = `https://www.electricitymap.org/zone/${co2State.attributes.country_code}`; electricityMapUrl = `https://www.electricitymap.org/zone/${co2State.attributes.country_code}`;
@ -401,69 +403,6 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
`; `;
} }
private async _getStatistics(): Promise<void> {
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` static styles = css`
:host { :host {
--mdc-icon-size: 24px; --mdc-icon-size: 24px;

View File

@ -1,15 +1,17 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { formatNumber } from "../../../../common/string/format_number"; import { formatNumber } from "../../../../common/string/format_number";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-gauge"; import "../../../../components/ha-gauge";
import type { LevelDefinition } from "../../../../components/ha-gauge"; import type { LevelDefinition } from "../../../../components/ha-gauge";
import { GridSourceTypeEnergyPreference } from "../../../../data/energy";
import { import {
calculateStatisticsSumGrowth, EnergyData,
fetchStatistics, getEnergyDataCollection,
Statistics, GridSourceTypeEnergyPreference,
} from "../../../../data/history"; } from "../../../../data/energy";
import { calculateStatisticsSumGrowth } from "../../../../data/history";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { LovelaceCard } from "../../types"; import type { LovelaceCard } from "../../types";
import type { EnergyGridGaugeCardConfig } from "../types"; import type { EnergyGridGaugeCardConfig } from "../types";
@ -21,12 +23,23 @@ const LEVELS: LevelDefinition[] = [
]; ];
@customElement("hui-energy-grid-neutrality-gauge-card") @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; @property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EnergyGridGaugeCardConfig; @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 { public getCardSize(): number {
return 4; return 4;
@ -36,24 +49,16 @@ class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard {
this._config = config; this._config = config;
} }
public willUpdate(changedProps) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._getStatistics();
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config || !this.hass) { if (!this._config || !this.hass) {
return html``; return html``;
} }
if (!this._stats) { if (!this._data) {
return html`Loading...`; return html`Loading...`;
} }
const prefs = this._config!.prefs; const prefs = this._data.prefs;
const gridSource = prefs.energy_sources.find( const gridSource = prefs.energy_sources.find(
(src) => src.type === "grid" (src) => src.type === "grid"
) as GridSourceTypeEnergyPreference | undefined; ) as GridSourceTypeEnergyPreference | undefined;
@ -65,12 +70,12 @@ class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard {
} }
const consumedFromGrid = calculateStatisticsSumGrowth( const consumedFromGrid = calculateStatisticsSumGrowth(
this._stats, this._data.stats,
gridSource.flow_from.map((flow) => flow.stat_energy_from) gridSource.flow_from.map((flow) => flow.stat_energy_from)
); );
const returnedToGrid = calculateStatisticsSumGrowth( const returnedToGrid = calculateStatisticsSumGrowth(
this._stats, this._data.stats,
gridSource.flow_to.map((flow) => flow.stat_energy_to) gridSource.flow_to.map((flow) => flow.stat_energy_to)
); );
@ -111,35 +116,6 @@ class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard {
`; `;
} }
private async _getStatistics(): Promise<void> {
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 { static get styles(): CSSResultGroup {
return css` return css`
ha-card { ha-card {

View File

@ -1,26 +1,39 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-gauge"; import "../../../../components/ha-gauge";
import { energySourcesByType } from "../../../../data/energy";
import { import {
calculateStatisticsSumGrowth, EnergyData,
fetchStatistics, energySourcesByType,
Statistics, getEnergyDataCollection,
} from "../../../../data/history"; } from "../../../../data/energy";
import { calculateStatisticsSumGrowth } from "../../../../data/history";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { LovelaceCard } from "../../types"; import type { LovelaceCard } from "../../types";
import { severityMap } from "../hui-gauge-card"; import { severityMap } from "../hui-gauge-card";
import type { EnergySolarGaugeCardConfig } from "../types"; import type { EnergySolarGaugeCardConfig } from "../types";
@customElement("hui-energy-solar-consumed-gauge-card") @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; @property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EnergySolarGaugeCardConfig; @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 { public getCardSize(): number {
return 4; return 4;
@ -30,33 +43,25 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard {
this._config = config; this._config = config;
} }
public willUpdate(changedProps) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._getStatistics();
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config || !this.hass) { if (!this._config || !this.hass) {
return html``; return html``;
} }
if (!this._stats) { if (!this._data) {
return html`Loading...`; return html`Loading...`;
} }
const prefs = this._config!.prefs; const prefs = this._data.prefs;
const types = energySourcesByType(prefs); const types = energySourcesByType(prefs);
const totalSolarProduction = calculateStatisticsSumGrowth( const totalSolarProduction = calculateStatisticsSumGrowth(
this._stats, this._data.stats,
types.solar!.map((source) => source.stat_energy_from) types.solar!.map((source) => source.stat_energy_from)
); );
const productionReturnedToGrid = calculateStatisticsSumGrowth( const productionReturnedToGrid = calculateStatisticsSumGrowth(
this._stats, this._data.stats,
types.grid![0].flow_to.map((flow) => flow.stat_energy_to) types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
); );
@ -101,36 +106,6 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard {
return severityMap.normal; return severityMap.normal;
} }
private async _getStatistics(): Promise<void> {
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 { static get styles(): CSSResultGroup {
return css` return css`
ha-card { ha-card {

View File

@ -1,19 +1,13 @@
import { import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators"; 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 { classMap } from "lit/directives/class-map";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import { ChartData, ChartDataset, ChartOptions } from "chart.js"; import { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types"; import { LovelaceCard } from "../../types";
import { EnergySolarGraphCardConfig } from "../types"; import { EnergySolarGraphCardConfig } from "../types";
import { fetchStatistics, Statistics } from "../../../../data/history";
import { import {
hex2rgb, hex2rgb,
lab2rgb, lab2rgb,
@ -21,7 +15,12 @@ import {
rgb2lab, rgb2lab,
} from "../../../../common/color/convert-color"; } from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab"; 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 { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { import {
ForecastSolarForecast, ForecastSolarForecast,
@ -35,52 +34,32 @@ import {
formatNumber, formatNumber,
numberFormatToLocale, numberFormatToLocale,
} from "../../../../common/string/format_number"; } from "../../../../common/string/format_number";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { FrontendLocaleData } from "../../../../data/translation";
@customElement("hui-energy-solar-graph-card") @customElement("hui-energy-solar-graph-card")
export class HuiEnergySolarGraphCard export class HuiEnergySolarGraphCard
extends LitElement extends SubscribeMixin(LitElement)
implements LovelaceCard implements LovelaceCard
{ {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergySolarGraphCardConfig; @state() private _config?: EnergySolarGraphCardConfig;
@state() private _data?: Statistics;
@state() private _chartData: ChartData = { @state() private _chartData: ChartData = {
datasets: [], datasets: [],
}; };
@state() private _forecasts?: Record<string, ForecastSolarForecast>; @state() private _forecasts?: Record<string, ForecastSolarForecast>;
@state() private _chartOptions?: ChartOptions;
@state() private _showAllForecastData = false; @state() private _showAllForecastData = false;
private _fetching = false; public hassSubscribe(): UnsubscribeFunc[] {
return [
private _interval?: number; getEnergyDataCollection(this.hass).subscribe((data) =>
this._getStatistics(data)
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 getCardSize(): Promise<number> | number { public getCardSize(): Promise<number> | number {
@ -91,30 +70,6 @@ export class HuiEnergySolarGraphCard
this._config = config; 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 { protected render(): TemplateResult {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return html``; return html``;
@ -132,7 +87,10 @@ export class HuiEnergySolarGraphCard
> >
<ha-chart-base <ha-chart-base
.data=${this._chartData} .data=${this._chartData}
.options=${this._chartOptions} .options=${this._createOptions(
getEnergyDataCollection(this.hass),
this.hass.locale
)}
chart-type="bar" chart-type="bar"
></ha-chart-base> ></ha-chart-base>
</div> </div>
@ -140,12 +98,14 @@ export class HuiEnergySolarGraphCard
`; `;
} }
private _createOptions() { private _createOptions = memoizeOne(
const startDate = new Date(); (
startDate.setHours(0, 0, 0, 0); energyCollection: EnergyCollection,
const startTime = startDate.getTime(); locale: FrontendLocaleData
): ChartOptions => {
const startTime = energyCollection.start.getTime();
this._chartOptions = { return {
parsing: false, parsing: false,
animation: false, animation: false,
scales: { scales: {
@ -155,7 +115,7 @@ export class HuiEnergySolarGraphCard
suggestedMax: startTime + 24 * 60 * 60 * 1000, suggestedMax: startTime + 24 * 60 * 60 * 1000,
adapters: { adapters: {
date: { date: {
locale: this.hass.locale, locale: locale,
}, },
}, },
ticks: { ticks: {
@ -193,7 +153,7 @@ export class HuiEnergySolarGraphCard
label: (context) => label: (context) =>
`${context.dataset.label}: ${formatNumber( `${context.dataset.label}: ${formatNumber(
context.parsed.y, context.parsed.y,
this.hass.locale locale
)} kWh`, )} kWh`,
}, },
}, },
@ -221,37 +181,17 @@ export class HuiEnergySolarGraphCard
}, },
}, },
// @ts-expect-error // @ts-expect-error
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(locale),
}; };
} }
);
private async _getStatistics(): Promise<void> { private async _getStatistics(energyData: EnergyData): Promise<void> {
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 solarSources: SolarSourceTypeEnergyPreference[] = const solarSources: SolarSourceTypeEnergyPreference[] =
this._config!.prefs.energy_sources.filter( energyData.prefs.energy_sources.filter(
(source) => source.type === "solar" (source) => source.type === "solar"
) as SolarSourceTypeEnergyPreference[]; ) as SolarSourceTypeEnergyPreference[];
try {
this._data = await fetchStatistics(
this.hass!,
startDate,
undefined,
solarSources.map((source) => source.stat_energy_from)
);
} finally {
this._fetching = false;
}
if ( if (
isComponentLoaded(this.hass, "forecast_solar") && isComponentLoaded(this.hass, "forecast_solar") &&
solarSources.some((source) => source.config_entry_solar_forecast) solarSources.some((source) => source.config_entry_solar_forecast)
@ -259,16 +199,7 @@ export class HuiEnergySolarGraphCard
this._forecasts = await getForecastSolarForecasts(this.hass); this._forecasts = await getForecastSolarForecasts(this.hass);
} }
this._renderChart(); const statisticsData = Object.values(energyData.stats);
}
private _renderChart() {
const solarSources: SolarSourceTypeEnergyPreference[] =
this._config!.prefs.energy_sources.filter(
(source) => source.type === "solar"
) as SolarSourceTypeEnergyPreference[];
const statisticsData = Object.values(this._data!);
const datasets: ChartDataset<"bar">[] = []; const datasets: ChartDataset<"bar">[] = [];
let endTime: Date; let endTime: Date;
@ -311,8 +242,8 @@ export class HuiEnergySolarGraphCard
let prevStart: string | null = null; let prevStart: string | null = null;
// Process solar production data. // Process solar production data.
if (this._data![source.stat_energy_from]) { if (energyData.stats[source.stat_energy_from]) {
for (const point of this._data![source.stat_energy_from]) { for (const point of energyData.stats[source.stat_energy_from]) {
if (!point.sum) { if (!point.sum) {
continue; continue;
} }

View File

@ -1,5 +1,6 @@
// @ts-ignore // @ts-ignore
import dataTableStyles from "@material/data-table/dist/mdc.data-table.min.css"; import dataTableStyles from "@material/data-table/dist/mdc.data-table.min.css";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -22,31 +23,34 @@ import { formatNumber } from "../../../../common/string/format_number";
import "../../../../components/chart/statistics-chart"; import "../../../../components/chart/statistics-chart";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import { import {
EnergyInfo, EnergyData,
energySourcesByType, energySourcesByType,
getEnergyInfo, getEnergyDataCollection,
} from "../../../../data/energy"; } from "../../../../data/energy";
import { import { calculateStatisticSumGrowth } from "../../../../data/history";
calculateStatisticSumGrowth, import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
fetchStatistics,
Statistics,
} from "../../../../data/history";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types"; import { LovelaceCard } from "../../types";
import { EnergySourcesTableCardConfig } from "../types"; import { EnergySourcesTableCardConfig } from "../types";
@customElement("hui-energy-sources-table-card") @customElement("hui-energy-sources-table-card")
export class HuiEnergySourcesTableCard export class HuiEnergySourcesTableCard
extends LitElement extends SubscribeMixin(LitElement)
implements LovelaceCard implements LovelaceCard
{ {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergySourcesTableCardConfig; @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> | number { public getCardSize(): Promise<number> | number {
return 3; return 3;
@ -56,18 +60,12 @@ export class HuiEnergySourcesTableCard
this._config = config; this._config = config;
} }
public willUpdate() {
if (!this.hasUpdated) {
this._getEnergyInfo().then(() => this._getStatistics());
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return html``; return html``;
} }
if (!this._stats) { if (!this._data) {
return html`Loading...`; return html`Loading...`;
} }
@ -75,7 +73,7 @@ export class HuiEnergySourcesTableCard
let totalSolar = 0; let totalSolar = 0;
let totalCost = 0; let totalCost = 0;
const types = energySourcesByType(this._config.prefs); const types = energySourcesByType(this._data.prefs);
const computedStyles = getComputedStyle(this); const computedStyles = getComputedStyle(this);
const solarColor = computedStyles const solarColor = computedStyles
@ -140,7 +138,7 @@ export class HuiEnergySourcesTableCard
const entity = this.hass.states[source.stat_energy_from]; const entity = this.hass.states[source.stat_energy_from];
const energy = const energy =
calculateStatisticSumGrowth( calculateStatisticSumGrowth(
this._stats![source.stat_energy_from] this._data!.stats[source.stat_energy_from]
) || 0; ) || 0;
totalSolar += energy; totalSolar += energy;
const color = const color =
@ -195,14 +193,16 @@ export class HuiEnergySourcesTableCard
const entity = this.hass.states[flow.stat_energy_from]; const entity = this.hass.states[flow.stat_energy_from];
const energy = const energy =
calculateStatisticSumGrowth( calculateStatisticSumGrowth(
this._stats![flow.stat_energy_from] this._data!.stats[flow.stat_energy_from]
) || 0; ) || 0;
totalGrid += energy; totalGrid += energy;
const cost_stat = const cost_stat =
flow.stat_cost || flow.stat_cost ||
this._energyInfo!.cost_sensors[flow.stat_energy_from]; this._data!.info.cost_sensors[flow.stat_energy_from];
const cost = cost_stat const cost = cost_stat
? calculateStatisticSumGrowth(this._stats![cost_stat]) || 0 ? calculateStatisticSumGrowth(
this._data!.stats[cost_stat]
) || 0
: null; : null;
if (cost !== null) { if (cost !== null) {
totalCost += cost; totalCost += cost;
@ -253,15 +253,16 @@ export class HuiEnergySourcesTableCard
const entity = this.hass.states[flow.stat_energy_to]; const entity = this.hass.states[flow.stat_energy_to];
const energy = const energy =
(calculateStatisticSumGrowth( (calculateStatisticSumGrowth(
this._stats![flow.stat_energy_to] this._data!.stats[flow.stat_energy_to]
) || 0) * -1; ) || 0) * -1;
totalGrid += energy; totalGrid += energy;
const cost_stat = const cost_stat =
flow.stat_compensation || flow.stat_compensation ||
this._energyInfo!.cost_sensors[flow.stat_energy_to]; this._data!.info.cost_sensors[flow.stat_energy_to];
const cost = cost_stat const cost = cost_stat
? (calculateStatisticSumGrowth(this._stats![cost_stat]) || ? (calculateStatisticSumGrowth(
0) * -1 this._data!.stats[cost_stat]
) || 0) * -1
: null; : null;
if (cost !== null) { if (cost !== null) {
totalCost += cost; totalCost += cost;
@ -333,45 +334,6 @@ export class HuiEnergySourcesTableCard
</ha-card>`; </ha-card>`;
} }
private async _getEnergyInfo() {
this._energyInfo = await getEnergyInfo(this.hass);
}
private async _getStatistics(): Promise<void> {
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 { static get styles(): CSSResultGroup {
return css` return css`
${unsafeCSS(dataTableStyles)} ${unsafeCSS(dataTableStyles)}

View File

@ -1,14 +1,9 @@
import { ChartData, ChartDataset, ChartOptions } from "chart.js"; import { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { import { UnsubscribeFunc } from "home-assistant-js-websocket";
css, import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { import {
hex2rgb, hex2rgb,
lab2rgb, lab2rgb,
@ -24,52 +19,36 @@ import {
} from "../../../../common/string/format_number"; } from "../../../../common/string/format_number";
import "../../../../components/chart/ha-chart-base"; import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card"; 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 { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types"; import { LovelaceCard } from "../../types";
import { EnergyUsageGraphCardConfig } from "../types"; import { EnergyUsageGraphCardConfig } from "../types";
@customElement("hui-energy-usage-graph-card") @customElement("hui-energy-usage-graph-card")
export class HuiEnergyUsageGraphCard export class HuiEnergyUsageGraphCard
extends LitElement extends SubscribeMixin(LitElement)
implements LovelaceCard implements LovelaceCard
{ {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergyUsageGraphCardConfig; @state() private _config?: EnergyUsageGraphCardConfig;
@state() private _data?: Statistics;
@state() private _chartData: ChartData = { @state() private _chartData: ChartData = {
datasets: [], datasets: [],
}; };
@state() private _chartOptions?: ChartOptions; public hassSubscribe(): UnsubscribeFunc[] {
return [
private _fetching = false; getEnergyDataCollection(this.hass).subscribe((data) =>
this._getStatistics(data)
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 getCardSize(): Promise<number> | number { public getCardSize(): Promise<number> | number {
@ -80,30 +59,6 @@ export class HuiEnergyUsageGraphCard
this._config = config; 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 { protected render(): TemplateResult {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return html``; return html``;
@ -121,7 +76,10 @@ export class HuiEnergyUsageGraphCard
> >
<ha-chart-base <ha-chart-base
.data=${this._chartData} .data=${this._chartData}
.options=${this._chartOptions} .options=${this._createOptions(
getEnergyDataCollection(this.hass),
this.hass.locale
)}
chart-type="bar" chart-type="bar"
></ha-chart-base> ></ha-chart-base>
</div> </div>
@ -129,12 +87,14 @@ export class HuiEnergyUsageGraphCard
`; `;
} }
private _createOptions() { private _createOptions = memoizeOne(
const startDate = new Date(); (
startDate.setHours(0, 0, 0, 0); energyCollection: EnergyCollection,
const startTime = startDate.getTime(); locale: FrontendLocaleData
): ChartOptions => {
const startTime = energyCollection.start.getTime();
this._chartOptions = { return {
parsing: false, parsing: false,
animation: false, animation: false,
scales: { scales: {
@ -144,7 +104,7 @@ export class HuiEnergyUsageGraphCard
suggestedMax: startTime + 24 * 60 * 60 * 1000, suggestedMax: startTime + 24 * 60 * 60 * 1000,
adapters: { adapters: {
date: { date: {
locale: this.hass.locale, locale: locale,
}, },
}, },
ticks: { ticks: {
@ -173,8 +133,7 @@ export class HuiEnergyUsageGraphCard
}, },
ticks: { ticks: {
beginAtZero: true, beginAtZero: true,
callback: (value) => callback: (value) => formatNumber(Math.abs(value), locale),
formatNumber(Math.abs(value), this.hass.locale),
}, },
}, },
}, },
@ -188,7 +147,7 @@ export class HuiEnergyUsageGraphCard
label: (context) => label: (context) =>
`${context.dataset.label}: ${formatNumber( `${context.dataset.label}: ${formatNumber(
Math.abs(context.parsed.y), Math.abs(context.parsed.y),
this.hass.locale locale
)} kWh`, )} kWh`,
footer: (contexts) => { footer: (contexts) => {
let totalConsumed = 0; let totalConsumed = 0;
@ -206,13 +165,13 @@ export class HuiEnergyUsageGraphCard
totalConsumed totalConsumed
? `Total consumed: ${formatNumber( ? `Total consumed: ${formatNumber(
totalConsumed, totalConsumed,
this.hass.locale locale
)} kWh` )} kWh`
: "", : "",
totalReturned totalReturned
? `Total returned: ${formatNumber( ? `Total returned: ${formatNumber(
totalReturned, totalReturned,
this.hass.locale locale
)} kWh` )} kWh`
: "", : "",
].filter(Boolean); ].filter(Boolean);
@ -239,27 +198,19 @@ export class HuiEnergyUsageGraphCard
}, },
}, },
// @ts-expect-error // @ts-expect-error
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(locale),
}; };
} }
);
private async _getStatistics(): Promise<void> { private async _getStatistics(energyData: EnergyData): Promise<void> {
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;
const statistics: { const statistics: {
to_grid?: string[]; to_grid?: string[];
from_grid?: string[]; from_grid?: string[];
solar?: string[]; solar?: string[];
} = {}; } = {};
for (const source of prefs.energy_sources) { for (const source of energyData.prefs.energy_sources) {
if (source.type === "solar") { if (source.type === "solar") {
if (statistics.solar) { if (statistics.solar) {
statistics.solar.push(source.stat_energy_from); statistics.solar.push(source.stat_energy_from);
@ -286,19 +237,7 @@ export class HuiEnergyUsageGraphCard
} }
} }
try { const statisticsData = Object.values(energyData.stats);
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 datasets: ChartDataset<"bar">[] = []; const datasets: ChartDataset<"bar">[] = [];
let endTime: Date; let endTime: Date;
@ -346,7 +285,7 @@ export class HuiEnergyUsageGraphCard
const totalStats: { [start: string]: number } = {}; const totalStats: { [start: string]: number } = {};
const sets: { [statId: string]: { [start: string]: number } } = {}; const sets: { [statId: string]: { [start: string]: number } } = {};
statIds!.forEach((id) => { statIds!.forEach((id) => {
const stats = this._data![id]; const stats = energyData.stats[id];
if (!stats) { if (!stats) {
return; return;
} }

View File

@ -1,4 +1,3 @@
import { EnergyPreferences } from "../../../data/energy";
import { StatisticType } from "../../../data/history"; import { StatisticType } from "../../../data/history";
import { ActionConfig, LovelaceCardConfig } from "../../../data/lovelace"; import { ActionConfig, LovelaceCardConfig } from "../../../data/lovelace";
import { FullCalendarView } from "../../../types"; import { FullCalendarView } from "../../../types";
@ -93,54 +92,45 @@ export interface ButtonCardConfig extends LovelaceCardConfig {
export interface EnergySummaryCardConfig extends LovelaceCardConfig { export interface EnergySummaryCardConfig extends LovelaceCardConfig {
type: "energy-summary"; type: "energy-summary";
title?: string; title?: string;
prefs: EnergyPreferences;
} }
export interface EnergyDistributionCardConfig extends LovelaceCardConfig { export interface EnergyDistributionCardConfig extends LovelaceCardConfig {
type: "energy-distribution"; type: "energy-distribution";
title?: string; title?: string;
prefs: EnergyPreferences;
} }
export interface EnergyUsageGraphCardConfig extends LovelaceCardConfig { export interface EnergyUsageGraphCardConfig extends LovelaceCardConfig {
type: "energy-summary-graph"; type: "energy-summary-graph";
title?: string; title?: string;
prefs: EnergyPreferences;
} }
export interface EnergySolarGraphCardConfig extends LovelaceCardConfig { export interface EnergySolarGraphCardConfig extends LovelaceCardConfig {
type: "energy-solar-graph"; type: "energy-solar-graph";
title?: string; title?: string;
prefs: EnergyPreferences;
} }
export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig { export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig {
type: "energy-devices-graph"; type: "energy-devices-graph";
title?: string; title?: string;
prefs: EnergyPreferences;
} }
export interface EnergySourcesTableCardConfig extends LovelaceCardConfig { export interface EnergySourcesTableCardConfig extends LovelaceCardConfig {
type: "energy-sources-table"; type: "energy-sources-table";
title?: string; title?: string;
prefs: EnergyPreferences;
} }
export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig { export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig {
type: "energy-solar-consumed-gauge"; type: "energy-solar-consumed-gauge";
title?: string; title?: string;
prefs: EnergyPreferences;
} }
export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig { export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig {
type: "energy-grid-result-gauge"; type: "energy-grid-result-gauge";
title?: string; title?: string;
prefs: EnergyPreferences;
} }
export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig { export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig {
type: "energy-carbon-consumed-gauge"; type: "energy-carbon-consumed-gauge";
title?: string; title?: string;
prefs: EnergyPreferences;
} }
export interface EntityFilterCardConfig extends LovelaceCardConfig { export interface EntityFilterCardConfig extends LovelaceCardConfig {