mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-26 14:27:20 +00:00
Add option to compare energy graphs with previous period (#12723)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
a0a7ce014f
commit
6ab19d66d5
@ -1,5 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
|
addDays,
|
||||||
addHours,
|
addHours,
|
||||||
|
addMilliseconds,
|
||||||
|
addMonths,
|
||||||
differenceInDays,
|
differenceInDays,
|
||||||
endOfToday,
|
endOfToday,
|
||||||
endOfYesterday,
|
endOfYesterday,
|
||||||
@ -14,9 +17,9 @@ import { ConfigEntry, getConfigEntries } from "./config_entries";
|
|||||||
import { subscribeEntityRegistry } from "./entity_registry";
|
import { subscribeEntityRegistry } from "./entity_registry";
|
||||||
import {
|
import {
|
||||||
fetchStatistics,
|
fetchStatistics,
|
||||||
|
getStatisticMetadata,
|
||||||
Statistics,
|
Statistics,
|
||||||
StatisticsMetaData,
|
StatisticsMetaData,
|
||||||
getStatisticMetadata,
|
|
||||||
} from "./history";
|
} from "./history";
|
||||||
|
|
||||||
const energyCollectionKeys: (string | undefined)[] = [];
|
const energyCollectionKeys: (string | undefined)[] = [];
|
||||||
@ -232,19 +235,24 @@ export const energySourcesByType = (prefs: EnergyPreferences) =>
|
|||||||
export interface EnergyData {
|
export interface EnergyData {
|
||||||
start: Date;
|
start: Date;
|
||||||
end?: Date;
|
end?: Date;
|
||||||
|
startCompare?: Date;
|
||||||
|
endCompare?: Date;
|
||||||
prefs: EnergyPreferences;
|
prefs: EnergyPreferences;
|
||||||
info: EnergyInfo;
|
info: EnergyInfo;
|
||||||
stats: Statistics;
|
stats: Statistics;
|
||||||
|
statsCompare: Statistics;
|
||||||
co2SignalConfigEntry?: ConfigEntry;
|
co2SignalConfigEntry?: ConfigEntry;
|
||||||
co2SignalEntity?: string;
|
co2SignalEntity?: string;
|
||||||
fossilEnergyConsumption?: FossilEnergyConsumption;
|
fossilEnergyConsumption?: FossilEnergyConsumption;
|
||||||
|
fossilEnergyConsumptionCompare?: FossilEnergyConsumption;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEnergyData = async (
|
const getEnergyData = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
prefs: EnergyPreferences,
|
prefs: EnergyPreferences,
|
||||||
start: Date,
|
start: Date,
|
||||||
end?: Date
|
end?: Date,
|
||||||
|
compare?: boolean
|
||||||
): Promise<EnergyData> => {
|
): Promise<EnergyData> => {
|
||||||
const [configEntries, entityRegistryEntries, info] = await Promise.all([
|
const [configEntries, entityRegistryEntries, info] = await Promise.all([
|
||||||
getConfigEntries(hass, { domain: "co2signal" }),
|
getConfigEntries(hass, { domain: "co2signal" }),
|
||||||
@ -350,6 +358,8 @@ const getEnergyData = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dayDifference = differenceInDays(end || new Date(), start);
|
const dayDifference = differenceInDays(end || new Date(), start);
|
||||||
|
const period =
|
||||||
|
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour";
|
||||||
|
|
||||||
// Subtract 1 hour from start to get starting point data
|
// Subtract 1 hour from start to get starting point data
|
||||||
const startMinHour = addHours(start, -1);
|
const startMinHour = addHours(start, -1);
|
||||||
@ -359,10 +369,34 @@ const getEnergyData = async (
|
|||||||
startMinHour,
|
startMinHour,
|
||||||
end,
|
end,
|
||||||
statIDs,
|
statIDs,
|
||||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
period
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let statsCompare;
|
||||||
|
let startCompare;
|
||||||
|
let endCompare;
|
||||||
|
if (compare) {
|
||||||
|
if (dayDifference > 27 && dayDifference < 32) {
|
||||||
|
// When comparing a month, we want to start at the begining of the month
|
||||||
|
startCompare = addMonths(start, -1);
|
||||||
|
} else {
|
||||||
|
startCompare = addDays(start, (dayDifference + 1) * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const compareStartMinHour = addHours(startCompare, -1);
|
||||||
|
endCompare = addMilliseconds(start, -1);
|
||||||
|
|
||||||
|
statsCompare = await fetchStatistics(
|
||||||
|
hass!,
|
||||||
|
compareStartMinHour,
|
||||||
|
endCompare,
|
||||||
|
statIDs,
|
||||||
|
period
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let fossilEnergyConsumption: FossilEnergyConsumption | undefined;
|
let fossilEnergyConsumption: FossilEnergyConsumption | undefined;
|
||||||
|
let fossilEnergyConsumptionCompare: FossilEnergyConsumption | undefined;
|
||||||
|
|
||||||
if (co2SignalEntity !== undefined) {
|
if (co2SignalEntity !== undefined) {
|
||||||
fossilEnergyConsumption = await getFossilEnergyConsumption(
|
fossilEnergyConsumption = await getFossilEnergyConsumption(
|
||||||
@ -373,6 +407,16 @@ const getEnergyData = async (
|
|||||||
end,
|
end,
|
||||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||||
);
|
);
|
||||||
|
if (compare) {
|
||||||
|
fossilEnergyConsumptionCompare = await getFossilEnergyConsumption(
|
||||||
|
hass!,
|
||||||
|
startCompare,
|
||||||
|
consumptionStatIDs,
|
||||||
|
co2SignalEntity,
|
||||||
|
endCompare,
|
||||||
|
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.values(stats).forEach((stat) => {
|
Object.values(stats).forEach((stat) => {
|
||||||
@ -388,15 +432,19 @@ const getEnergyData = async (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = {
|
const data: EnergyData = {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
startCompare,
|
||||||
|
endCompare,
|
||||||
info,
|
info,
|
||||||
prefs,
|
prefs,
|
||||||
stats,
|
stats,
|
||||||
|
statsCompare,
|
||||||
co2SignalConfigEntry,
|
co2SignalConfigEntry,
|
||||||
co2SignalEntity,
|
co2SignalEntity,
|
||||||
fossilEnergyConsumption,
|
fossilEnergyConsumption,
|
||||||
|
fossilEnergyConsumptionCompare,
|
||||||
};
|
};
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@ -405,9 +453,11 @@ const getEnergyData = async (
|
|||||||
export interface EnergyCollection extends Collection<EnergyData> {
|
export interface EnergyCollection extends Collection<EnergyData> {
|
||||||
start: Date;
|
start: Date;
|
||||||
end?: Date;
|
end?: Date;
|
||||||
|
compare?: boolean;
|
||||||
prefs?: EnergyPreferences;
|
prefs?: EnergyPreferences;
|
||||||
clearPrefs(): void;
|
clearPrefs(): void;
|
||||||
setPeriod(newStart: Date, newEnd?: Date): void;
|
setPeriod(newStart: Date, newEnd?: Date): void;
|
||||||
|
setCompare(compare: boolean): void;
|
||||||
_refreshTimeout?: number;
|
_refreshTimeout?: number;
|
||||||
_updatePeriodTimeout?: number;
|
_updatePeriodTimeout?: number;
|
||||||
_active: number;
|
_active: number;
|
||||||
@ -478,7 +528,8 @@ export const getEnergyDataCollection = (
|
|||||||
hass,
|
hass,
|
||||||
collection.prefs,
|
collection.prefs,
|
||||||
collection.start,
|
collection.start,
|
||||||
collection.end
|
collection.end,
|
||||||
|
collection.compare
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
) as EnergyCollection;
|
) as EnergyCollection;
|
||||||
@ -534,6 +585,9 @@ export const getEnergyDataCollection = (
|
|||||||
collection._updatePeriodTimeout = undefined;
|
collection._updatePeriodTimeout = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
collection.setCompare = (compare: boolean) => {
|
||||||
|
collection.compare = compare;
|
||||||
|
};
|
||||||
return collection;
|
return collection;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import "@polymer/app-layout/app-header/app-header";
|
import "@polymer/app-layout/app-header/app-header";
|
||||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||||
import "@material/mwc-tab";
|
|
||||||
import "@material/mwc-tab-bar";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@ -12,14 +10,13 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import "../../components/ha-menu-button";
|
import "../../components/ha-menu-button";
|
||||||
import "../../layouts/ha-app-layout";
|
|
||||||
|
|
||||||
import { haStyle } from "../../resources/styles";
|
|
||||||
import "../lovelace/views/hui-view";
|
|
||||||
import { HomeAssistant } from "../../types";
|
|
||||||
import { Lovelace } from "../lovelace/types";
|
|
||||||
import { LovelaceConfig } from "../../data/lovelace";
|
import { LovelaceConfig } from "../../data/lovelace";
|
||||||
|
import "../../layouts/ha-app-layout";
|
||||||
|
import { haStyle } from "../../resources/styles";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
import "../lovelace/components/hui-energy-period-selector";
|
import "../lovelace/components/hui-energy-period-selector";
|
||||||
|
import { Lovelace } from "../lovelace/types";
|
||||||
|
import "../lovelace/views/hui-view";
|
||||||
|
|
||||||
const LOVELACE_CONFIG: LovelaceConfig = {
|
const LOVELACE_CONFIG: LovelaceConfig = {
|
||||||
views: [
|
views: [
|
||||||
|
@ -60,6 +60,11 @@ export class EnergyStrategy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
view.cards!.push({
|
||||||
|
type: "energy-compare",
|
||||||
|
collection_key: "energy_dashboard",
|
||||||
|
});
|
||||||
|
|
||||||
// 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({
|
||||||
|
106
src/panels/lovelace/cards/energy/hui-energy-compare-card.ts
Normal file
106
src/panels/lovelace/cards/energy/hui-energy-compare-card.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { differenceInDays, endOfDay } from "date-fns";
|
||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { formatDate } from "../../../../common/datetime/format_date";
|
||||||
|
import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
|
||||||
|
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||||
|
import { HomeAssistant } from "../../../../types";
|
||||||
|
import { LovelaceCard } from "../../types";
|
||||||
|
import { EnergyCardBaseConfig } from "../types";
|
||||||
|
|
||||||
|
@customElement("hui-energy-compare-card")
|
||||||
|
export class HuiEnergyCompareCard
|
||||||
|
extends SubscribeMixin(LitElement)
|
||||||
|
implements LovelaceCard
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _config?: EnergyCardBaseConfig;
|
||||||
|
|
||||||
|
@state() private _start?: Date;
|
||||||
|
|
||||||
|
@state() private _end?: Date;
|
||||||
|
|
||||||
|
@state() private _startCompare?: Date;
|
||||||
|
|
||||||
|
@state() private _endCompare?: Date;
|
||||||
|
|
||||||
|
public getCardSize(): Promise<number> | number {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(config: EnergyCardBaseConfig): void {
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||||
|
|
||||||
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
|
return [
|
||||||
|
getEnergyDataCollection(this.hass, {
|
||||||
|
key: this._config!.collection_key,
|
||||||
|
}).subscribe((data) => this._update(data)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this._startCompare || !this._endCompare) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayDifference = differenceInDays(
|
||||||
|
this._endCompare,
|
||||||
|
this._startCompare
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-alert dismissable @alert-dismissed-clicked=${this._stopCompare}>
|
||||||
|
You are comparing the period
|
||||||
|
<b
|
||||||
|
>${formatDate(this._start!, this.hass.locale)}${dayDifference > 0
|
||||||
|
? ` -
|
||||||
|
${formatDate(this._end || endOfDay(new Date()), this.hass.locale)}`
|
||||||
|
: ""}</b
|
||||||
|
>
|
||||||
|
with period
|
||||||
|
<b
|
||||||
|
>${formatDate(this._startCompare, this.hass.locale)}${dayDifference >
|
||||||
|
0
|
||||||
|
? ` -
|
||||||
|
${formatDate(this._endCompare, this.hass.locale)}`
|
||||||
|
: ""}</b
|
||||||
|
>
|
||||||
|
</ha-alert>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _update(data: EnergyData): void {
|
||||||
|
this._start = data.start;
|
||||||
|
this._end = data.end;
|
||||||
|
this._startCompare = data.startCompare;
|
||||||
|
this._endCompare = data.endCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopCompare(): void {
|
||||||
|
const energyCollection = getEnergyDataCollection(this.hass, {
|
||||||
|
key: this._config!.collection_key,
|
||||||
|
});
|
||||||
|
energyCollection.setCompare(false);
|
||||||
|
energyCollection.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
mwc-button {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-energy-compare-card": HuiEnergyCompareCard;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
import { LovelaceCard } from "../../types";
|
import { LovelaceCard } from "../../types";
|
||||||
import { EnergyDevicesGraphCardConfig } from "../types";
|
import { EnergyCardBaseConfig } from "../types";
|
||||||
import "../../components/hui-energy-period-selector";
|
import "../../components/hui-energy-period-selector";
|
||||||
|
|
||||||
@customElement("hui-energy-date-selection-card")
|
@customElement("hui-energy-date-selection-card")
|
||||||
@ -12,13 +12,13 @@ export class HuiEnergyDateSelectionCard
|
|||||||
{
|
{
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _config?: EnergyDevicesGraphCardConfig;
|
@state() private _config?: EnergyCardBaseConfig;
|
||||||
|
|
||||||
public getCardSize(): Promise<number> | number {
|
public getCardSize(): Promise<number> | number {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setConfig(config: EnergyDevicesGraphCardConfig): void {
|
public setConfig(config: EnergyCardBaseConfig): void {
|
||||||
this._config = config;
|
this._config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,10 +34,6 @@ export class HuiEnergyDateSelectionCard
|
|||||||
></hui-energy-period-selector>
|
></hui-energy-period-selector>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return css``;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
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 {
|
import {
|
||||||
ChartData,
|
ChartData,
|
||||||
ChartDataset,
|
ChartDataset,
|
||||||
@ -13,13 +7,16 @@ import {
|
|||||||
import {
|
import {
|
||||||
addHours,
|
addHours,
|
||||||
differenceInDays,
|
differenceInDays,
|
||||||
|
differenceInHours,
|
||||||
endOfToday,
|
endOfToday,
|
||||||
isToday,
|
isToday,
|
||||||
startOfToday,
|
startOfToday,
|
||||||
} from "date-fns/esm";
|
} from "date-fns";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { LovelaceCard } from "../../types";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { EnergyGasGraphCardConfig } from "../types";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import {
|
import {
|
||||||
hex2rgb,
|
hex2rgb,
|
||||||
lab2rgb,
|
lab2rgb,
|
||||||
@ -27,21 +24,27 @@ import {
|
|||||||
rgb2lab,
|
rgb2lab,
|
||||||
} from "../../../../common/color/convert-color";
|
} from "../../../../common/color/convert-color";
|
||||||
import { labBrighten, labDarken } from "../../../../common/color/lab";
|
import { labBrighten, labDarken } from "../../../../common/color/lab";
|
||||||
import {
|
import { formatDateShort } from "../../../../common/datetime/format_date";
|
||||||
EnergyData,
|
import { formatTime } from "../../../../common/datetime/format_time";
|
||||||
getEnergyDataCollection,
|
|
||||||
getEnergyGasUnit,
|
|
||||||
GasSourceTypeEnergyPreference,
|
|
||||||
} from "../../../../data/energy";
|
|
||||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||||
import "../../../../components/chart/ha-chart-base";
|
|
||||||
import {
|
import {
|
||||||
formatNumber,
|
formatNumber,
|
||||||
numberFormatToLocale,
|
numberFormatToLocale,
|
||||||
} from "../../../../common/number/format_number";
|
} from "../../../../common/number/format_number";
|
||||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
import "../../../../components/chart/ha-chart-base";
|
||||||
|
import "../../../../components/ha-card";
|
||||||
|
import {
|
||||||
|
EnergyData,
|
||||||
|
GasSourceTypeEnergyPreference,
|
||||||
|
getEnergyDataCollection,
|
||||||
|
getEnergyGasUnit,
|
||||||
|
} from "../../../../data/energy";
|
||||||
|
import { Statistics } from "../../../../data/history";
|
||||||
import { FrontendLocaleData } from "../../../../data/translation";
|
import { FrontendLocaleData } from "../../../../data/translation";
|
||||||
import { formatTime } from "../../../../common/datetime/format_time";
|
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||||
|
import { HomeAssistant } from "../../../../types";
|
||||||
|
import { LovelaceCard } from "../../types";
|
||||||
|
import { EnergyGasGraphCardConfig } from "../types";
|
||||||
|
|
||||||
@customElement("hui-energy-gas-graph-card")
|
@customElement("hui-energy-gas-graph-card")
|
||||||
export class HuiEnergyGasGraphCard
|
export class HuiEnergyGasGraphCard
|
||||||
@ -60,6 +63,10 @@ export class HuiEnergyGasGraphCard
|
|||||||
|
|
||||||
@state() private _end = endOfToday();
|
@state() private _end = endOfToday();
|
||||||
|
|
||||||
|
@state() private _compareStart?: Date;
|
||||||
|
|
||||||
|
@state() private _compareEnd?: Date;
|
||||||
|
|
||||||
@state() private _unit?: string;
|
@state() private _unit?: string;
|
||||||
|
|
||||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||||
@ -101,7 +108,9 @@ export class HuiEnergyGasGraphCard
|
|||||||
this._start,
|
this._start,
|
||||||
this._end,
|
this._end,
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
this._unit
|
this._unit,
|
||||||
|
this._compareStart,
|
||||||
|
this._compareEnd
|
||||||
)}
|
)}
|
||||||
chart-type="bar"
|
chart-type="bar"
|
||||||
></ha-chart-base>
|
></ha-chart-base>
|
||||||
@ -124,10 +133,24 @@ export class HuiEnergyGasGraphCard
|
|||||||
start: Date,
|
start: Date,
|
||||||
end: Date,
|
end: Date,
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendLocaleData,
|
||||||
unit?: string
|
unit?: string,
|
||||||
|
compareStart?: Date,
|
||||||
|
compareEnd?: Date
|
||||||
): ChartOptions => {
|
): ChartOptions => {
|
||||||
const dayDifference = differenceInDays(end, start);
|
const dayDifference = differenceInDays(end, start);
|
||||||
return {
|
const compare = compareStart !== undefined && compareEnd !== undefined;
|
||||||
|
if (compare) {
|
||||||
|
const difference = differenceInHours(end, start);
|
||||||
|
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
|
||||||
|
// If the compare period doesn't match the main period, adjust them to match
|
||||||
|
if (differenceCompare > difference) {
|
||||||
|
end = addHours(end, differenceCompare - difference);
|
||||||
|
} else if (difference > differenceCompare) {
|
||||||
|
compareEnd = addHours(compareEnd!, difference - differenceCompare);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: ChartOptions = {
|
||||||
parsing: false,
|
parsing: false,
|
||||||
animation: false,
|
animation: false,
|
||||||
scales: {
|
scales: {
|
||||||
@ -193,7 +216,9 @@ export class HuiEnergyGasGraphCard
|
|||||||
return datasets[0].label;
|
return datasets[0].label;
|
||||||
}
|
}
|
||||||
const date = new Date(datasets[0].parsed.x);
|
const date = new Date(datasets[0].parsed.x);
|
||||||
return `${formatTime(date, locale)} – ${formatTime(
|
return `${
|
||||||
|
compare ? `${formatDateShort(date, locale)}: ` : ""
|
||||||
|
}${formatTime(date, locale)} – ${formatTime(
|
||||||
addHours(date, 1),
|
addHours(date, 1),
|
||||||
locale
|
locale
|
||||||
)}`;
|
)}`;
|
||||||
@ -227,6 +252,15 @@ export class HuiEnergyGasGraphCard
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
locale: numberFormatToLocale(locale),
|
locale: numberFormatToLocale(locale),
|
||||||
};
|
};
|
||||||
|
if (compare) {
|
||||||
|
options.scales!.xAxisCompare = {
|
||||||
|
...(options.scales!.x as Record<string, any>),
|
||||||
|
suggestedMin: compareStart!.getTime(),
|
||||||
|
suggestedMax: compareEnd!.getTime(),
|
||||||
|
display: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -238,15 +272,58 @@ export class HuiEnergyGasGraphCard
|
|||||||
|
|
||||||
this._unit = getEnergyGasUnit(this.hass, energyData.prefs) || "m³";
|
this._unit = getEnergyGasUnit(this.hass, energyData.prefs) || "m³";
|
||||||
|
|
||||||
const datasets: ChartDataset<"bar">[] = [];
|
const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
|
||||||
|
|
||||||
const computedStyles = getComputedStyle(this);
|
const computedStyles = getComputedStyle(this);
|
||||||
const gasColor = computedStyles
|
const gasColor = computedStyles
|
||||||
.getPropertyValue("--energy-gas-color")
|
.getPropertyValue("--energy-gas-color")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
datasets.push(
|
||||||
|
...this._processDataSet(energyData.stats, gasSources, gasColor)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (energyData.statsCompare) {
|
||||||
|
// Add empty dataset to align the bars
|
||||||
|
datasets.push({
|
||||||
|
order: 0,
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
datasets.push({
|
||||||
|
order: 999,
|
||||||
|
data: [],
|
||||||
|
xAxisID: "xAxisCompare",
|
||||||
|
});
|
||||||
|
|
||||||
|
datasets.push(
|
||||||
|
...this._processDataSet(
|
||||||
|
energyData.statsCompare,
|
||||||
|
gasSources,
|
||||||
|
gasColor,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._start = energyData.start;
|
||||||
|
this._end = energyData.end || endOfToday();
|
||||||
|
|
||||||
|
this._compareStart = energyData.startCompare;
|
||||||
|
this._compareEnd = energyData.endCompare;
|
||||||
|
|
||||||
|
this._chartData = {
|
||||||
|
datasets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _processDataSet(
|
||||||
|
statistics: Statistics,
|
||||||
|
gasSources: GasSourceTypeEnergyPreference[],
|
||||||
|
gasColor: string,
|
||||||
|
compare = false
|
||||||
|
) {
|
||||||
|
const data: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
|
||||||
gasSources.forEach((source, idx) => {
|
gasSources.forEach((source, idx) => {
|
||||||
const data: ChartDataset<"bar" | "line">[] = [];
|
|
||||||
const entity = this.hass.states[source.stat_energy_from];
|
const entity = this.hass.states[source.stat_energy_from];
|
||||||
|
|
||||||
const modifiedColor =
|
const modifiedColor =
|
||||||
@ -265,8 +342,8 @@ export class HuiEnergyGasGraphCard
|
|||||||
const gasConsumptionData: ScatterDataPoint[] = [];
|
const gasConsumptionData: ScatterDataPoint[] = [];
|
||||||
|
|
||||||
// Process gas consumption data.
|
// Process gas consumption data.
|
||||||
if (source.stat_energy_from in energyData.stats) {
|
if (source.stat_energy_from in statistics) {
|
||||||
const stats = energyData.stats[source.stat_energy_from];
|
const stats = statistics[source.stat_energy_from];
|
||||||
|
|
||||||
for (const point of stats) {
|
for (const point of stats) {
|
||||||
if (point.sum === null) {
|
if (point.sum === null) {
|
||||||
@ -290,26 +367,17 @@ export class HuiEnergyGasGraphCard
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gasConsumptionData.length) {
|
data.push({
|
||||||
data.push({
|
label: entity ? computeStateName(entity) : source.stat_energy_from,
|
||||||
label: entity ? computeStateName(entity) : source.stat_energy_from,
|
borderColor: compare ? borderColor + "7F" : borderColor,
|
||||||
borderColor,
|
backgroundColor: compare ? borderColor + "32" : borderColor + "7F",
|
||||||
backgroundColor: borderColor + "7F",
|
data: gasConsumptionData,
|
||||||
data: gasConsumptionData,
|
order: 1,
|
||||||
stack: "gas",
|
stack: "gas",
|
||||||
});
|
xAxisID: compare ? "xAxisCompare" : undefined,
|
||||||
}
|
});
|
||||||
|
|
||||||
// Concat two arrays
|
|
||||||
Array.prototype.push.apply(datasets, data);
|
|
||||||
});
|
});
|
||||||
|
return data;
|
||||||
this._start = energyData.start;
|
|
||||||
this._end = energyData.end || endOfToday();
|
|
||||||
|
|
||||||
this._chartData = {
|
|
||||||
datasets,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
addHours,
|
addHours,
|
||||||
differenceInDays,
|
differenceInDays,
|
||||||
|
differenceInHours,
|
||||||
endOfToday,
|
endOfToday,
|
||||||
isToday,
|
isToday,
|
||||||
startOfToday,
|
startOfToday,
|
||||||
@ -23,6 +24,7 @@ import {
|
|||||||
rgb2lab,
|
rgb2lab,
|
||||||
} from "../../../../common/color/convert-color";
|
} from "../../../../common/color/convert-color";
|
||||||
import { labBrighten, labDarken } from "../../../../common/color/lab";
|
import { labBrighten, labDarken } from "../../../../common/color/lab";
|
||||||
|
import { formatDateShort } from "../../../../common/datetime/format_date";
|
||||||
import { formatTime } from "../../../../common/datetime/format_time";
|
import { formatTime } from "../../../../common/datetime/format_time";
|
||||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||||
import {
|
import {
|
||||||
@ -38,6 +40,7 @@ import {
|
|||||||
getEnergySolarForecasts,
|
getEnergySolarForecasts,
|
||||||
SolarSourceTypeEnergyPreference,
|
SolarSourceTypeEnergyPreference,
|
||||||
} from "../../../../data/energy";
|
} from "../../../../data/energy";
|
||||||
|
import { Statistics } from "../../../../data/history";
|
||||||
import { FrontendLocaleData } from "../../../../data/translation";
|
import { FrontendLocaleData } from "../../../../data/translation";
|
||||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
@ -61,6 +64,10 @@ export class HuiEnergySolarGraphCard
|
|||||||
|
|
||||||
@state() private _end = endOfToday();
|
@state() private _end = endOfToday();
|
||||||
|
|
||||||
|
@state() private _compareStart?: Date;
|
||||||
|
|
||||||
|
@state() private _compareEnd?: Date;
|
||||||
|
|
||||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||||
|
|
||||||
public hassSubscribe(): UnsubscribeFunc[] {
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
@ -99,7 +106,9 @@ export class HuiEnergySolarGraphCard
|
|||||||
.options=${this._createOptions(
|
.options=${this._createOptions(
|
||||||
this._start,
|
this._start,
|
||||||
this._end,
|
this._end,
|
||||||
this.hass.locale
|
this.hass.locale,
|
||||||
|
this._compareStart,
|
||||||
|
this._compareEnd
|
||||||
)}
|
)}
|
||||||
chart-type="bar"
|
chart-type="bar"
|
||||||
></ha-chart-base>
|
></ha-chart-base>
|
||||||
@ -118,9 +127,27 @@ export class HuiEnergySolarGraphCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _createOptions = memoizeOne(
|
private _createOptions = memoizeOne(
|
||||||
(start: Date, end: Date, locale: FrontendLocaleData): ChartOptions => {
|
(
|
||||||
|
start: Date,
|
||||||
|
end: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
compareStart?: Date,
|
||||||
|
compareEnd?: Date
|
||||||
|
): ChartOptions => {
|
||||||
const dayDifference = differenceInDays(end, start);
|
const dayDifference = differenceInDays(end, start);
|
||||||
return {
|
const compare = compareStart !== undefined && compareEnd !== undefined;
|
||||||
|
if (compare) {
|
||||||
|
const difference = differenceInHours(end, start);
|
||||||
|
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
|
||||||
|
// If the compare period doesn't match the main period, adjust them to match
|
||||||
|
if (differenceCompare > difference) {
|
||||||
|
end = addHours(end, differenceCompare - difference);
|
||||||
|
} else if (difference > differenceCompare) {
|
||||||
|
compareEnd = addHours(compareEnd!, difference - differenceCompare);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: ChartOptions = {
|
||||||
parsing: false,
|
parsing: false,
|
||||||
animation: false,
|
animation: false,
|
||||||
scales: {
|
scales: {
|
||||||
@ -163,7 +190,6 @@ export class HuiEnergySolarGraphCard
|
|||||||
? "day"
|
? "day"
|
||||||
: "hour",
|
: "hour",
|
||||||
},
|
},
|
||||||
offset: true,
|
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
stacked: true,
|
stacked: true,
|
||||||
@ -186,7 +212,9 @@ export class HuiEnergySolarGraphCard
|
|||||||
return datasets[0].label;
|
return datasets[0].label;
|
||||||
}
|
}
|
||||||
const date = new Date(datasets[0].parsed.x);
|
const date = new Date(datasets[0].parsed.x);
|
||||||
return `${formatTime(date, locale)} – ${formatTime(
|
return `${
|
||||||
|
compare ? `${formatDateShort(date, locale)}: ` : ""
|
||||||
|
}${formatTime(date, locale)} – ${formatTime(
|
||||||
addHours(date, 1),
|
addHours(date, 1),
|
||||||
locale
|
locale
|
||||||
)}`;
|
)}`;
|
||||||
@ -224,6 +252,15 @@ export class HuiEnergySolarGraphCard
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
locale: numberFormatToLocale(locale),
|
locale: numberFormatToLocale(locale),
|
||||||
};
|
};
|
||||||
|
if (compare) {
|
||||||
|
options.scales!.xAxisCompare = {
|
||||||
|
...(options.scales!.x as Record<string, any>),
|
||||||
|
suggestedMin: compareStart!.getTime(),
|
||||||
|
suggestedMax: compareEnd!.getTime(),
|
||||||
|
display: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -244,20 +281,71 @@ export class HuiEnergySolarGraphCard
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const datasets: ChartDataset<"bar">[] = [];
|
const datasets: ChartDataset<"bar" | "line">[] = [];
|
||||||
|
|
||||||
const computedStyles = getComputedStyle(this);
|
const computedStyles = getComputedStyle(this);
|
||||||
const solarColor = computedStyles
|
const solarColor = computedStyles
|
||||||
.getPropertyValue("--energy-solar-color")
|
.getPropertyValue("--energy-solar-color")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
const dayDifference = differenceInDays(
|
datasets.push(
|
||||||
energyData.end || new Date(),
|
...this._processDataSet(energyData.stats, solarSources, solarColor)
|
||||||
energyData.start
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (energyData.statsCompare) {
|
||||||
|
// Add empty dataset to align the bars
|
||||||
|
datasets.push({
|
||||||
|
order: 0,
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
datasets.push({
|
||||||
|
order: 999,
|
||||||
|
data: [],
|
||||||
|
xAxisID: "xAxisCompare",
|
||||||
|
});
|
||||||
|
|
||||||
|
datasets.push(
|
||||||
|
...this._processDataSet(
|
||||||
|
energyData.statsCompare,
|
||||||
|
solarSources,
|
||||||
|
solarColor,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forecasts) {
|
||||||
|
datasets.push(
|
||||||
|
...this._processForecast(
|
||||||
|
forecasts,
|
||||||
|
solarSources,
|
||||||
|
computedStyles.getPropertyValue("--primary-text-color"),
|
||||||
|
energyData.start,
|
||||||
|
energyData.end
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._start = energyData.start;
|
||||||
|
this._end = energyData.end || endOfToday();
|
||||||
|
|
||||||
|
this._compareStart = energyData.startCompare;
|
||||||
|
this._compareEnd = energyData.endCompare;
|
||||||
|
|
||||||
|
this._chartData = {
|
||||||
|
datasets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _processDataSet(
|
||||||
|
statistics: Statistics,
|
||||||
|
solarSources: SolarSourceTypeEnergyPreference[],
|
||||||
|
solarColor: string,
|
||||||
|
compare = false
|
||||||
|
) {
|
||||||
|
const data: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
|
||||||
|
|
||||||
solarSources.forEach((source, idx) => {
|
solarSources.forEach((source, idx) => {
|
||||||
const data: ChartDataset<"bar" | "line">[] = [];
|
|
||||||
const entity = this.hass.states[source.stat_energy_from];
|
const entity = this.hass.states[source.stat_energy_from];
|
||||||
|
|
||||||
const modifiedColor =
|
const modifiedColor =
|
||||||
@ -276,8 +364,8 @@ export class HuiEnergySolarGraphCard
|
|||||||
const solarProductionData: ScatterDataPoint[] = [];
|
const solarProductionData: ScatterDataPoint[] = [];
|
||||||
|
|
||||||
// Process solar production data.
|
// Process solar production data.
|
||||||
if (source.stat_energy_from in energyData.stats) {
|
if (source.stat_energy_from in statistics) {
|
||||||
const stats = energyData.stats[source.stat_energy_from];
|
const stats = statistics[source.stat_energy_from];
|
||||||
|
|
||||||
for (const point of stats) {
|
for (const point of stats) {
|
||||||
if (point.sum === null) {
|
if (point.sum === null) {
|
||||||
@ -301,23 +389,41 @@ export class HuiEnergySolarGraphCard
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (solarProductionData.length) {
|
data.push({
|
||||||
data.push({
|
label: this.hass.localize(
|
||||||
label: this.hass.localize(
|
"ui.panel.lovelace.cards.energy.energy_solar_graph.production",
|
||||||
"ui.panel.lovelace.cards.energy.energy_solar_graph.production",
|
{
|
||||||
{
|
name: entity ? computeStateName(entity) : source.stat_energy_from,
|
||||||
name: entity ? computeStateName(entity) : source.stat_energy_from,
|
}
|
||||||
}
|
),
|
||||||
),
|
borderColor: compare ? borderColor + "7F" : borderColor,
|
||||||
borderColor,
|
backgroundColor: compare ? borderColor + "32" : borderColor + "7F",
|
||||||
backgroundColor: borderColor + "7F",
|
data: solarProductionData,
|
||||||
data: solarProductionData,
|
order: 1,
|
||||||
stack: "solar",
|
stack: "solar",
|
||||||
});
|
xAxisID: compare ? "xAxisCompare" : undefined,
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _processForecast(
|
||||||
|
forecasts: EnergySolarForecasts,
|
||||||
|
solarSources: SolarSourceTypeEnergyPreference[],
|
||||||
|
borderColor: string,
|
||||||
|
start: Date,
|
||||||
|
end?: Date
|
||||||
|
) {
|
||||||
|
const data: ChartDataset<"line">[] = [];
|
||||||
|
|
||||||
|
const dayDifference = differenceInDays(end || new Date(), start);
|
||||||
|
|
||||||
|
// Process solar forecast data.
|
||||||
|
solarSources.forEach((source) => {
|
||||||
|
if (source.config_entry_solar_forecast) {
|
||||||
|
const entity = this.hass.states[source.stat_energy_from];
|
||||||
|
|
||||||
// Process solar forecast data.
|
|
||||||
if (forecasts && source.config_entry_solar_forecast) {
|
|
||||||
const forecastsData: Record<string, number> | undefined = {};
|
const forecastsData: Record<string, number> | undefined = {};
|
||||||
source.config_entry_solar_forecast.forEach((configEntryId) => {
|
source.config_entry_solar_forecast.forEach((configEntryId) => {
|
||||||
if (!forecasts![configEntryId]) {
|
if (!forecasts![configEntryId]) {
|
||||||
@ -326,10 +432,7 @@ export class HuiEnergySolarGraphCard
|
|||||||
Object.entries(forecasts![configEntryId].wh_hours).forEach(
|
Object.entries(forecasts![configEntryId].wh_hours).forEach(
|
||||||
([date, value]) => {
|
([date, value]) => {
|
||||||
const dateObj = new Date(date);
|
const dateObj = new Date(date);
|
||||||
if (
|
if (dateObj < start || (end && dateObj > end)) {
|
||||||
dateObj < energyData.start ||
|
|
||||||
(energyData.end && dateObj > energyData.end)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (dayDifference > 35) {
|
if (dayDifference > 35) {
|
||||||
@ -372,9 +475,7 @@ export class HuiEnergySolarGraphCard
|
|||||||
),
|
),
|
||||||
fill: false,
|
fill: false,
|
||||||
stepped: false,
|
stepped: false,
|
||||||
borderColor: computedStyles.getPropertyValue(
|
borderColor,
|
||||||
"--primary-text-color"
|
|
||||||
),
|
|
||||||
borderDash: [7, 5],
|
borderDash: [7, 5],
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
data: solarForecastData,
|
data: solarForecastData,
|
||||||
@ -382,17 +483,9 @@ export class HuiEnergySolarGraphCard
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Concat two arrays
|
|
||||||
Array.prototype.push.apply(datasets, data);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this._start = energyData.start;
|
return data;
|
||||||
this._end = energyData.end || endOfToday();
|
|
||||||
|
|
||||||
this._chartData = {
|
|
||||||
datasets,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
import {
|
||||||
|
ChartData,
|
||||||
|
ChartDataset,
|
||||||
|
ChartOptions,
|
||||||
|
ScatterDataPoint,
|
||||||
|
} from "chart.js";
|
||||||
import {
|
import {
|
||||||
addHours,
|
addHours,
|
||||||
differenceInDays,
|
differenceInDays,
|
||||||
|
differenceInHours,
|
||||||
endOfToday,
|
endOfToday,
|
||||||
isToday,
|
isToday,
|
||||||
startOfToday,
|
startOfToday,
|
||||||
@ -18,6 +24,7 @@ import {
|
|||||||
rgb2lab,
|
rgb2lab,
|
||||||
} from "../../../../common/color/convert-color";
|
} from "../../../../common/color/convert-color";
|
||||||
import { labBrighten, labDarken } from "../../../../common/color/lab";
|
import { labBrighten, labDarken } from "../../../../common/color/lab";
|
||||||
|
import { formatDateShort } from "../../../../common/datetime/format_date";
|
||||||
import { formatTime } from "../../../../common/datetime/format_time";
|
import { formatTime } from "../../../../common/datetime/format_time";
|
||||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||||
import {
|
import {
|
||||||
@ -27,6 +34,7 @@ import {
|
|||||||
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 { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
|
||||||
|
import { Statistics } from "../../../../data/history";
|
||||||
import { FrontendLocaleData } from "../../../../data/translation";
|
import { FrontendLocaleData } from "../../../../data/translation";
|
||||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
@ -50,6 +58,10 @@ export class HuiEnergyUsageGraphCard
|
|||||||
|
|
||||||
@state() private _end = endOfToday();
|
@state() private _end = endOfToday();
|
||||||
|
|
||||||
|
@state() private _compareStart?: Date;
|
||||||
|
|
||||||
|
@state() private _compareEnd?: Date;
|
||||||
|
|
||||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||||
|
|
||||||
public hassSubscribe(): UnsubscribeFunc[] {
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
@ -88,7 +100,9 @@ export class HuiEnergyUsageGraphCard
|
|||||||
.options=${this._createOptions(
|
.options=${this._createOptions(
|
||||||
this._start,
|
this._start,
|
||||||
this._end,
|
this._end,
|
||||||
this.hass.locale
|
this.hass.locale,
|
||||||
|
this._compareStart,
|
||||||
|
this._compareEnd
|
||||||
)}
|
)}
|
||||||
chart-type="bar"
|
chart-type="bar"
|
||||||
></ha-chart-base>
|
></ha-chart-base>
|
||||||
@ -107,9 +121,27 @@ export class HuiEnergyUsageGraphCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _createOptions = memoizeOne(
|
private _createOptions = memoizeOne(
|
||||||
(start: Date, end: Date, locale: FrontendLocaleData): ChartOptions => {
|
(
|
||||||
|
start: Date,
|
||||||
|
end: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
compareStart?: Date,
|
||||||
|
compareEnd?: Date
|
||||||
|
): ChartOptions => {
|
||||||
const dayDifference = differenceInDays(end, start);
|
const dayDifference = differenceInDays(end, start);
|
||||||
return {
|
const compare = compareStart !== undefined && compareEnd !== undefined;
|
||||||
|
if (compare) {
|
||||||
|
const difference = differenceInHours(end, start);
|
||||||
|
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
|
||||||
|
// If the compare period doesn't match the main period, adjust them to match
|
||||||
|
if (differenceCompare > difference) {
|
||||||
|
end = addHours(end, differenceCompare - difference);
|
||||||
|
} else if (difference > differenceCompare) {
|
||||||
|
compareEnd = addHours(compareEnd!, difference - differenceCompare);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: ChartOptions = {
|
||||||
parsing: false,
|
parsing: false,
|
||||||
animation: false,
|
animation: false,
|
||||||
scales: {
|
scales: {
|
||||||
@ -152,7 +184,6 @@ export class HuiEnergyUsageGraphCard
|
|||||||
? "day"
|
? "day"
|
||||||
: "hour",
|
: "hour",
|
||||||
},
|
},
|
||||||
offset: true,
|
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
stacked: true,
|
stacked: true,
|
||||||
@ -179,7 +210,9 @@ export class HuiEnergyUsageGraphCard
|
|||||||
return datasets[0].label;
|
return datasets[0].label;
|
||||||
}
|
}
|
||||||
const date = new Date(datasets[0].parsed.x);
|
const date = new Date(datasets[0].parsed.x);
|
||||||
return `${formatTime(date, locale)} – ${formatTime(
|
return `${
|
||||||
|
compare ? `${formatDateShort(date, locale)}: ` : ""
|
||||||
|
}${formatTime(date, locale)} – ${formatTime(
|
||||||
addHours(date, 1),
|
addHours(date, 1),
|
||||||
locale
|
locale
|
||||||
)}`;
|
)}`;
|
||||||
@ -240,13 +273,22 @@ export class HuiEnergyUsageGraphCard
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
locale: numberFormatToLocale(locale),
|
locale: numberFormatToLocale(locale),
|
||||||
};
|
};
|
||||||
|
if (compare) {
|
||||||
|
options.scales!.xAxisCompare = {
|
||||||
|
...(options.scales!.x as Record<string, any>),
|
||||||
|
suggestedMin: compareStart!.getTime(),
|
||||||
|
suggestedMax: compareEnd!.getTime(),
|
||||||
|
display: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
||||||
const datasets: ChartDataset<"bar">[] = [];
|
const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
|
||||||
|
|
||||||
const statistics: {
|
const statIds: {
|
||||||
to_grid?: string[];
|
to_grid?: string[];
|
||||||
from_grid?: string[];
|
from_grid?: string[];
|
||||||
solar?: string[];
|
solar?: string[];
|
||||||
@ -256,21 +298,21 @@ export class HuiEnergyUsageGraphCard
|
|||||||
|
|
||||||
for (const source of energyData.prefs.energy_sources) {
|
for (const source of energyData.prefs.energy_sources) {
|
||||||
if (source.type === "solar") {
|
if (source.type === "solar") {
|
||||||
if (statistics.solar) {
|
if (statIds.solar) {
|
||||||
statistics.solar.push(source.stat_energy_from);
|
statIds.solar.push(source.stat_energy_from);
|
||||||
} else {
|
} else {
|
||||||
statistics.solar = [source.stat_energy_from];
|
statIds.solar = [source.stat_energy_from];
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source.type === "battery") {
|
if (source.type === "battery") {
|
||||||
if (statistics.to_battery) {
|
if (statIds.to_battery) {
|
||||||
statistics.to_battery.push(source.stat_energy_to);
|
statIds.to_battery.push(source.stat_energy_to);
|
||||||
statistics.from_battery!.push(source.stat_energy_from);
|
statIds.from_battery!.push(source.stat_energy_from);
|
||||||
} else {
|
} else {
|
||||||
statistics.to_battery = [source.stat_energy_to];
|
statIds.to_battery = [source.stat_energy_to];
|
||||||
statistics.from_battery = [source.stat_energy_from];
|
statIds.from_battery = [source.stat_energy_from];
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -281,41 +323,21 @@ export class HuiEnergyUsageGraphCard
|
|||||||
|
|
||||||
// grid source
|
// grid source
|
||||||
for (const flowFrom of source.flow_from) {
|
for (const flowFrom of source.flow_from) {
|
||||||
if (statistics.from_grid) {
|
if (statIds.from_grid) {
|
||||||
statistics.from_grid.push(flowFrom.stat_energy_from);
|
statIds.from_grid.push(flowFrom.stat_energy_from);
|
||||||
} else {
|
} else {
|
||||||
statistics.from_grid = [flowFrom.stat_energy_from];
|
statIds.from_grid = [flowFrom.stat_energy_from];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const flowTo of source.flow_to) {
|
for (const flowTo of source.flow_to) {
|
||||||
if (statistics.to_grid) {
|
if (statIds.to_grid) {
|
||||||
statistics.to_grid.push(flowTo.stat_energy_to);
|
statIds.to_grid.push(flowTo.stat_energy_to);
|
||||||
} else {
|
} else {
|
||||||
statistics.to_grid = [flowTo.stat_energy_to];
|
statIds.to_grid = [flowTo.stat_energy_to];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._start = energyData.start;
|
|
||||||
this._end = energyData.end || endOfToday();
|
|
||||||
|
|
||||||
const combinedData: {
|
|
||||||
to_grid?: { [statId: string]: { [start: string]: number } };
|
|
||||||
to_battery?: { [statId: string]: { [start: string]: number } };
|
|
||||||
from_grid?: { [statId: string]: { [start: string]: number } };
|
|
||||||
used_grid?: { [statId: string]: { [start: string]: number } };
|
|
||||||
used_solar?: { [statId: string]: { [start: string]: number } };
|
|
||||||
used_battery?: { [statId: string]: { [start: string]: number } };
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
const summedData: {
|
|
||||||
to_grid?: { [start: string]: number };
|
|
||||||
from_grid?: { [start: string]: number };
|
|
||||||
to_battery?: { [start: string]: number };
|
|
||||||
from_battery?: { [start: string]: number };
|
|
||||||
solar?: { [start: string]: number };
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
const computedStyles = getComputedStyle(this);
|
const computedStyles = getComputedStyle(this);
|
||||||
const colors = {
|
const colors = {
|
||||||
to_grid: computedStyles
|
to_grid: computedStyles
|
||||||
@ -349,7 +371,88 @@ export class HuiEnergyUsageGraphCard
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(statistics).forEach(([key, statIds]) => {
|
this._start = energyData.start;
|
||||||
|
this._end = energyData.end || endOfToday();
|
||||||
|
|
||||||
|
this._compareStart = energyData.startCompare;
|
||||||
|
this._compareEnd = energyData.endCompare;
|
||||||
|
|
||||||
|
datasets.push(
|
||||||
|
...this._processDataSet(energyData.stats, statIds, colors, labels, false)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (energyData.statsCompare) {
|
||||||
|
// Add empty dataset to align the bars
|
||||||
|
datasets.push({
|
||||||
|
order: 0,
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
datasets.push({
|
||||||
|
order: 999,
|
||||||
|
data: [],
|
||||||
|
xAxisID: "xAxisCompare",
|
||||||
|
});
|
||||||
|
|
||||||
|
datasets.push(
|
||||||
|
...this._processDataSet(
|
||||||
|
energyData.statsCompare,
|
||||||
|
statIds,
|
||||||
|
colors,
|
||||||
|
labels,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._chartData = {
|
||||||
|
datasets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _processDataSet(
|
||||||
|
statistics: Statistics,
|
||||||
|
statIdsByCat: {
|
||||||
|
to_grid?: string[] | undefined;
|
||||||
|
from_grid?: string[] | undefined;
|
||||||
|
solar?: string[] | undefined;
|
||||||
|
to_battery?: string[] | undefined;
|
||||||
|
from_battery?: string[] | undefined;
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
to_grid: string;
|
||||||
|
to_battery: string;
|
||||||
|
from_grid: string;
|
||||||
|
used_grid: string;
|
||||||
|
used_solar: string;
|
||||||
|
used_battery: string;
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
used_grid: string;
|
||||||
|
used_solar: string;
|
||||||
|
used_battery: string;
|
||||||
|
},
|
||||||
|
compare = false
|
||||||
|
) {
|
||||||
|
const data: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
|
||||||
|
|
||||||
|
const combinedData: {
|
||||||
|
to_grid?: { [statId: string]: { [start: string]: number } };
|
||||||
|
to_battery?: { [statId: string]: { [start: string]: number } };
|
||||||
|
from_grid?: { [statId: string]: { [start: string]: number } };
|
||||||
|
used_grid?: { [statId: string]: { [start: string]: number } };
|
||||||
|
used_solar?: { [statId: string]: { [start: string]: number } };
|
||||||
|
used_battery?: { [statId: string]: { [start: string]: number } };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
const summedData: {
|
||||||
|
to_grid?: { [start: string]: number };
|
||||||
|
from_grid?: { [start: string]: number };
|
||||||
|
to_battery?: { [start: string]: number };
|
||||||
|
from_battery?: { [start: string]: number };
|
||||||
|
solar?: { [start: string]: number };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
Object.entries(statIdsByCat).forEach(([key, statIds]) => {
|
||||||
const sum = [
|
const sum = [
|
||||||
"solar",
|
"solar",
|
||||||
"to_grid",
|
"to_grid",
|
||||||
@ -361,7 +464,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 = energyData.stats[id];
|
const stats = statistics[id];
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -477,7 +580,6 @@ export class HuiEnergyUsageGraphCard
|
|||||||
|
|
||||||
Object.entries(combinedData).forEach(([type, sources]) => {
|
Object.entries(combinedData).forEach(([type, sources]) => {
|
||||||
Object.entries(sources).forEach(([statId, source], idx) => {
|
Object.entries(sources).forEach(([statId, source], idx) => {
|
||||||
const data: ChartDataset<"bar">[] = [];
|
|
||||||
const entity = this.hass.states[statId];
|
const entity = this.hass.states[statId];
|
||||||
|
|
||||||
const modifiedColor =
|
const modifiedColor =
|
||||||
@ -490,6 +592,20 @@ export class HuiEnergyUsageGraphCard
|
|||||||
? rgb2hex(lab2rgb(modifiedColor))
|
? rgb2hex(lab2rgb(modifiedColor))
|
||||||
: colors[type];
|
: colors[type];
|
||||||
|
|
||||||
|
const points: ScatterDataPoint[] = [];
|
||||||
|
// Process chart data.
|
||||||
|
for (const key of uniqueKeys) {
|
||||||
|
const value = source[key] || 0;
|
||||||
|
const date = new Date(key);
|
||||||
|
points.push({
|
||||||
|
x: date.getTime(),
|
||||||
|
y:
|
||||||
|
value && ["to_grid", "to_battery"].includes(type)
|
||||||
|
? -1 * value
|
||||||
|
: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
data.push({
|
data.push({
|
||||||
label:
|
label:
|
||||||
type in labels
|
type in labels
|
||||||
@ -499,38 +615,19 @@ export class HuiEnergyUsageGraphCard
|
|||||||
: statId,
|
: statId,
|
||||||
order:
|
order:
|
||||||
type === "used_solar"
|
type === "used_solar"
|
||||||
? 0
|
? 1
|
||||||
: type === "to_battery"
|
: type === "to_battery"
|
||||||
? Object.keys(combinedData).length
|
? Object.keys(combinedData).length
|
||||||
: idx + 1,
|
: idx + 2,
|
||||||
borderColor,
|
borderColor: compare ? borderColor + "7F" : borderColor,
|
||||||
backgroundColor: borderColor + "7F",
|
backgroundColor: compare ? borderColor + "32" : borderColor + "7F",
|
||||||
stack: "stack",
|
stack: "stack",
|
||||||
data: [],
|
data: points,
|
||||||
|
xAxisID: compare ? "xAxisCompare" : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process chart data.
|
|
||||||
for (const key of uniqueKeys) {
|
|
||||||
const value = source[key] || 0;
|
|
||||||
const date = new Date(key);
|
|
||||||
// @ts-expect-error
|
|
||||||
data[0].data.push({
|
|
||||||
x: date.getTime(),
|
|
||||||
y:
|
|
||||||
value && ["to_grid", "to_battery"].includes(type)
|
|
||||||
? -1 * value
|
|
||||||
: value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Concat two arrays
|
|
||||||
Array.prototype.push.apply(datasets, data);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
return data;
|
||||||
this._chartData = {
|
|
||||||
datasets,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@ -97,6 +97,10 @@ export interface ButtonCardConfig extends LovelaceCardConfig {
|
|||||||
show_state?: boolean;
|
show_state?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EnergyCardBaseConfig extends LovelaceCardConfig {
|
||||||
|
collection_key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EnergySummaryCardConfig extends LovelaceCardConfig {
|
export interface EnergySummaryCardConfig extends LovelaceCardConfig {
|
||||||
type: "energy-summary";
|
type: "energy-summary";
|
||||||
title?: string;
|
title?: string;
|
||||||
|
@ -46,6 +46,8 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _period?: "day" | "week" | "month" | "year";
|
@state() private _period?: "day" | "week" | "month" | "year";
|
||||||
|
|
||||||
|
@state() private _compare? = false;
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
toggleAttribute(this, "narrow", this.offsetWidth < 600);
|
toggleAttribute(this, "narrow", this.offsetWidth < 600);
|
||||||
@ -134,6 +136,14 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
|||||||
dense
|
dense
|
||||||
@value-changed=${this._handleView}
|
@value-changed=${this._handleView}
|
||||||
></ha-button-toggle-group>
|
></ha-button-toggle-group>
|
||||||
|
<mwc-button
|
||||||
|
class="compare ${this._compare ? "active" : ""}"
|
||||||
|
@click=${this._toggleCompare}
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
Compare data
|
||||||
|
</mwc-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -216,6 +226,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _updateDates(energyData: EnergyData): void {
|
private _updateDates(energyData: EnergyData): void {
|
||||||
|
this._compare = energyData.startCompare !== undefined;
|
||||||
this._startDate = energyData.start;
|
this._startDate = energyData.start;
|
||||||
this._endDate = energyData.end || endOfToday();
|
this._endDate = energyData.end || endOfToday();
|
||||||
const dayDifference = differenceInDays(this._endDate, this._startDate);
|
const dayDifference = differenceInDays(this._endDate, this._startDate);
|
||||||
@ -231,6 +242,15 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _toggleCompare() {
|
||||||
|
this._compare = !this._compare;
|
||||||
|
const energyCollection = getEnergyDataCollection(this.hass, {
|
||||||
|
key: "energy_dashboard",
|
||||||
|
});
|
||||||
|
energyCollection.setCompare(this._compare);
|
||||||
|
energyCollection.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
.row {
|
.row {
|
||||||
@ -251,12 +271,37 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
.period {
|
.period {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
mwc-button.active::before {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
background-color: currentColor;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
content: "";
|
||||||
|
transition: opacity 15ms linear, background-color 15ms linear;
|
||||||
|
opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
|
||||||
|
}
|
||||||
|
.compare {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 8px;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
:host([narrow]) .compare {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
:host {
|
:host {
|
||||||
--mdc-button-outline-color: currentColor;
|
--mdc-button-outline-color: currentColor;
|
||||||
--primary-color: currentColor;
|
--primary-color: currentColor;
|
||||||
--mdc-theme-primary: currentColor;
|
--mdc-theme-primary: currentColor;
|
||||||
|
--mdc-theme-on-primary: currentColor;
|
||||||
--mdc-button-disabled-outline-color: var(--disabled-text-color);
|
--mdc-button-disabled-outline-color: var(--disabled-text-color);
|
||||||
--mdc-button-disabled-ink-color: var(--disabled-text-color);
|
--mdc-button-disabled-ink-color: var(--disabled-text-color);
|
||||||
--mdc-icon-button-ripple-opacity: 0.2;
|
--mdc-icon-button-ripple-opacity: 0.2;
|
||||||
|
@ -35,6 +35,7 @@ const LAZY_LOAD_TYPES = {
|
|||||||
calendar: () => import("../cards/hui-calendar-card"),
|
calendar: () => import("../cards/hui-calendar-card"),
|
||||||
conditional: () => import("../cards/hui-conditional-card"),
|
conditional: () => import("../cards/hui-conditional-card"),
|
||||||
"empty-state": () => import("../cards/hui-empty-state-card"),
|
"empty-state": () => import("../cards/hui-empty-state-card"),
|
||||||
|
"energy-compare": () => import("../cards/energy/hui-energy-compare-card"),
|
||||||
"energy-carbon-consumed-gauge": () =>
|
"energy-carbon-consumed-gauge": () =>
|
||||||
import("../cards/energy/hui-energy-carbon-consumed-gauge-card"),
|
import("../cards/energy/hui-energy-carbon-consumed-gauge-card"),
|
||||||
"energy-date-selection": () =>
|
"energy-date-selection": () =>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user