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 { 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<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 {
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,
});
}

View File

@ -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`<hui-warning>
${createEntityNotFoundWarning(this.hass, this._co2SignalEntity)}
${createEntityNotFoundWarning(this.hass, this._data.co2SignalEntity)}
</hui-warning>`;
}
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<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 {
return css`
ha-card {

View File

@ -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> | 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`<ha-chart-base
.data=${this._chartData}
.options=${this._chartOptions}
.options=${this._createOptions(this.hass.locale)}
chart-type="bar"
></ha-chart-base>`
: ""}
@ -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<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;
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<void> {
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;

View File

@ -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> | 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> | 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<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`
:host {
--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 { 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<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 {
return css`
ha-card {

View File

@ -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<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 {
return css`
ha-card {

View File

@ -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<string, ForecastSolarForecast>;
@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> | 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
>
<ha-chart-base
.data=${this._chartData}
.options=${this._chartOptions}
.options=${this._createOptions(
getEnergyDataCollection(this.hass),
this.hass.locale
)}
chart-type="bar"
></ha-chart-base>
</div>
@ -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<void> {
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<void> {
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;
}

View File

@ -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> | 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
</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 {
return css`
${unsafeCSS(dataTableStyles)}

View File

@ -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> | 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
>
<ha-chart-base
.data=${this._chartData}
.options=${this._chartOptions}
.options=${this._createOptions(
getEnergyDataCollection(this.hass),
this.hass.locale
)}
chart-type="bar"
></ha-chart-base>
</div>
@ -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<void> {
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<void> {
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;
}

View File

@ -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 {