Add option to compare energy graphs with previous period (#12723)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Bram Kragten 2022-05-20 06:20:18 +02:00 committed by GitHub
parent a0a7ce014f
commit 6ab19d66d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 648 additions and 182 deletions

View File

@ -1,5 +1,8 @@
import {
addDays,
addHours,
addMilliseconds,
addMonths,
differenceInDays,
endOfToday,
endOfYesterday,
@ -14,9 +17,9 @@ import { ConfigEntry, getConfigEntries } from "./config_entries";
import { subscribeEntityRegistry } from "./entity_registry";
import {
fetchStatistics,
getStatisticMetadata,
Statistics,
StatisticsMetaData,
getStatisticMetadata,
} from "./history";
const energyCollectionKeys: (string | undefined)[] = [];
@ -232,19 +235,24 @@ export const energySourcesByType = (prefs: EnergyPreferences) =>
export interface EnergyData {
start: Date;
end?: Date;
startCompare?: Date;
endCompare?: Date;
prefs: EnergyPreferences;
info: EnergyInfo;
stats: Statistics;
statsCompare: Statistics;
co2SignalConfigEntry?: ConfigEntry;
co2SignalEntity?: string;
fossilEnergyConsumption?: FossilEnergyConsumption;
fossilEnergyConsumptionCompare?: FossilEnergyConsumption;
}
const getEnergyData = async (
hass: HomeAssistant,
prefs: EnergyPreferences,
start: Date,
end?: Date
end?: Date,
compare?: boolean
): Promise<EnergyData> => {
const [configEntries, entityRegistryEntries, info] = await Promise.all([
getConfigEntries(hass, { domain: "co2signal" }),
@ -350,6 +358,8 @@ const getEnergyData = async (
}
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
const startMinHour = addHours(start, -1);
@ -359,10 +369,34 @@ const getEnergyData = async (
startMinHour,
end,
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 fossilEnergyConsumptionCompare: FossilEnergyConsumption | undefined;
if (co2SignalEntity !== undefined) {
fossilEnergyConsumption = await getFossilEnergyConsumption(
@ -373,6 +407,16 @@ const getEnergyData = async (
end,
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) => {
@ -388,15 +432,19 @@ const getEnergyData = async (
}
});
const data = {
const data: EnergyData = {
start,
end,
startCompare,
endCompare,
info,
prefs,
stats,
statsCompare,
co2SignalConfigEntry,
co2SignalEntity,
fossilEnergyConsumption,
fossilEnergyConsumptionCompare,
};
return data;
@ -405,9 +453,11 @@ const getEnergyData = async (
export interface EnergyCollection extends Collection<EnergyData> {
start: Date;
end?: Date;
compare?: boolean;
prefs?: EnergyPreferences;
clearPrefs(): void;
setPeriod(newStart: Date, newEnd?: Date): void;
setCompare(compare: boolean): void;
_refreshTimeout?: number;
_updatePeriodTimeout?: number;
_active: number;
@ -478,7 +528,8 @@ export const getEnergyDataCollection = (
hass,
collection.prefs,
collection.start,
collection.end
collection.end,
collection.compare
);
}
) as EnergyCollection;
@ -534,6 +585,9 @@ export const getEnergyDataCollection = (
collection._updatePeriodTimeout = undefined;
}
};
collection.setCompare = (compare: boolean) => {
collection.compare = compare;
};
return collection;
};

View File

@ -1,7 +1,5 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import {
css,
CSSResultGroup,
@ -12,14 +10,13 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
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 "../../layouts/ha-app-layout";
import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import "../lovelace/components/hui-energy-period-selector";
import { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
const LOVELACE_CONFIG: LovelaceConfig = {
views: [

View File

@ -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.
if (hasGrid) {
view.cards!.push({

View 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;
}
}

View File

@ -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 { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergyDevicesGraphCardConfig } from "../types";
import { EnergyCardBaseConfig } from "../types";
import "../../components/hui-energy-period-selector";
@customElement("hui-energy-date-selection-card")
@ -12,13 +12,13 @@ export class HuiEnergyDateSelectionCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergyDevicesGraphCardConfig;
@state() private _config?: EnergyCardBaseConfig;
public getCardSize(): Promise<number> | number {
return 1;
}
public setConfig(config: EnergyDevicesGraphCardConfig): void {
public setConfig(config: EnergyCardBaseConfig): void {
this._config = config;
}
@ -34,10 +34,6 @@ export class HuiEnergyDateSelectionCard
></hui-energy-period-selector>
`;
}
static get styles(): CSSResultGroup {
return css``;
}
}
declare global {

View File

@ -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 {
ChartData,
ChartDataset,
@ -13,13 +7,16 @@ import {
import {
addHours,
differenceInDays,
differenceInHours,
endOfToday,
isToday,
startOfToday,
} from "date-fns/esm";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergyGasGraphCardConfig } from "../types";
} 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 { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import {
hex2rgb,
lab2rgb,
@ -27,21 +24,27 @@ import {
rgb2lab,
} from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import {
EnergyData,
getEnergyDataCollection,
getEnergyGasUnit,
GasSourceTypeEnergyPreference,
} from "../../../../data/energy";
import { formatDateShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import "../../../../components/chart/ha-chart-base";
import {
formatNumber,
numberFormatToLocale,
} 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 { 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")
export class HuiEnergyGasGraphCard
@ -60,6 +63,10 @@ export class HuiEnergyGasGraphCard
@state() private _end = endOfToday();
@state() private _compareStart?: Date;
@state() private _compareEnd?: Date;
@state() private _unit?: string;
protected hassSubscribeRequiredHostProps = ["_config"];
@ -101,7 +108,9 @@ export class HuiEnergyGasGraphCard
this._start,
this._end,
this.hass.locale,
this._unit
this._unit,
this._compareStart,
this._compareEnd
)}
chart-type="bar"
></ha-chart-base>
@ -124,10 +133,24 @@ export class HuiEnergyGasGraphCard
start: Date,
end: Date,
locale: FrontendLocaleData,
unit?: string
unit?: string,
compareStart?: Date,
compareEnd?: Date
): ChartOptions => {
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,
animation: false,
scales: {
@ -193,7 +216,9 @@ export class HuiEnergyGasGraphCard
return datasets[0].label;
}
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),
locale
)}`;
@ -227,6 +252,15 @@ export class HuiEnergyGasGraphCard
// @ts-expect-error
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³";
const datasets: ChartDataset<"bar">[] = [];
const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
const computedStyles = getComputedStyle(this);
const gasColor = computedStyles
.getPropertyValue("--energy-gas-color")
.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) => {
const data: ChartDataset<"bar" | "line">[] = [];
const entity = this.hass.states[source.stat_energy_from];
const modifiedColor =
@ -265,8 +342,8 @@ export class HuiEnergyGasGraphCard
const gasConsumptionData: ScatterDataPoint[] = [];
// Process gas consumption data.
if (source.stat_energy_from in energyData.stats) {
const stats = energyData.stats[source.stat_energy_from];
if (source.stat_energy_from in statistics) {
const stats = statistics[source.stat_energy_from];
for (const point of stats) {
if (point.sum === null) {
@ -290,26 +367,17 @@ export class HuiEnergyGasGraphCard
}
}
if (gasConsumptionData.length) {
data.push({
label: entity ? computeStateName(entity) : source.stat_energy_from,
borderColor,
backgroundColor: borderColor + "7F",
data: gasConsumptionData,
stack: "gas",
});
}
// Concat two arrays
Array.prototype.push.apply(datasets, data);
data.push({
label: entity ? computeStateName(entity) : source.stat_energy_from,
borderColor: compare ? borderColor + "7F" : borderColor,
backgroundColor: compare ? borderColor + "32" : borderColor + "7F",
data: gasConsumptionData,
order: 1,
stack: "gas",
xAxisID: compare ? "xAxisCompare" : undefined,
});
});
this._start = energyData.start;
this._end = energyData.end || endOfToday();
this._chartData = {
datasets,
};
return data;
}
static get styles(): CSSResultGroup {

View File

@ -7,6 +7,7 @@ import {
import {
addHours,
differenceInDays,
differenceInHours,
endOfToday,
isToday,
startOfToday,
@ -23,6 +24,7 @@ import {
rgb2lab,
} from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import {
@ -38,6 +40,7 @@ import {
getEnergySolarForecasts,
SolarSourceTypeEnergyPreference,
} from "../../../../data/energy";
import { Statistics } from "../../../../data/history";
import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types";
@ -61,6 +64,10 @@ export class HuiEnergySolarGraphCard
@state() private _end = endOfToday();
@state() private _compareStart?: Date;
@state() private _compareEnd?: Date;
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
@ -99,7 +106,9 @@ export class HuiEnergySolarGraphCard
.options=${this._createOptions(
this._start,
this._end,
this.hass.locale
this.hass.locale,
this._compareStart,
this._compareEnd
)}
chart-type="bar"
></ha-chart-base>
@ -118,9 +127,27 @@ export class HuiEnergySolarGraphCard
}
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);
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,
animation: false,
scales: {
@ -163,7 +190,6 @@ export class HuiEnergySolarGraphCard
? "day"
: "hour",
},
offset: true,
},
y: {
stacked: true,
@ -186,7 +212,9 @@ export class HuiEnergySolarGraphCard
return datasets[0].label;
}
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),
locale
)}`;
@ -224,6 +252,15 @@ export class HuiEnergySolarGraphCard
// @ts-expect-error
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 solarColor = computedStyles
.getPropertyValue("--energy-solar-color")
.trim();
const dayDifference = differenceInDays(
energyData.end || new Date(),
energyData.start
datasets.push(
...this._processDataSet(energyData.stats, solarSources, solarColor)
);
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) => {
const data: ChartDataset<"bar" | "line">[] = [];
const entity = this.hass.states[source.stat_energy_from];
const modifiedColor =
@ -276,8 +364,8 @@ export class HuiEnergySolarGraphCard
const solarProductionData: ScatterDataPoint[] = [];
// Process solar production data.
if (source.stat_energy_from in energyData.stats) {
const stats = energyData.stats[source.stat_energy_from];
if (source.stat_energy_from in statistics) {
const stats = statistics[source.stat_energy_from];
for (const point of stats) {
if (point.sum === null) {
@ -301,23 +389,41 @@ export class HuiEnergySolarGraphCard
}
}
if (solarProductionData.length) {
data.push({
label: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_solar_graph.production",
{
name: entity ? computeStateName(entity) : source.stat_energy_from,
}
),
borderColor,
backgroundColor: borderColor + "7F",
data: solarProductionData,
stack: "solar",
});
}
data.push({
label: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_solar_graph.production",
{
name: entity ? computeStateName(entity) : source.stat_energy_from,
}
),
borderColor: compare ? borderColor + "7F" : borderColor,
backgroundColor: compare ? borderColor + "32" : borderColor + "7F",
data: solarProductionData,
order: 1,
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 = {};
source.config_entry_solar_forecast.forEach((configEntryId) => {
if (!forecasts![configEntryId]) {
@ -326,10 +432,7 @@ export class HuiEnergySolarGraphCard
Object.entries(forecasts![configEntryId].wh_hours).forEach(
([date, value]) => {
const dateObj = new Date(date);
if (
dateObj < energyData.start ||
(energyData.end && dateObj > energyData.end)
) {
if (dateObj < start || (end && dateObj > end)) {
return;
}
if (dayDifference > 35) {
@ -372,9 +475,7 @@ export class HuiEnergySolarGraphCard
),
fill: false,
stepped: false,
borderColor: computedStyles.getPropertyValue(
"--primary-text-color"
),
borderColor,
borderDash: [7, 5],
pointRadius: 0,
data: solarForecastData,
@ -382,17 +483,9 @@ export class HuiEnergySolarGraphCard
}
}
}
// Concat two arrays
Array.prototype.push.apply(datasets, data);
});
this._start = energyData.start;
this._end = energyData.end || endOfToday();
this._chartData = {
datasets,
};
return data;
}
static get styles(): CSSResultGroup {

View File

@ -1,7 +1,13 @@
import { ChartData, ChartDataset, ChartOptions } from "chart.js";
import {
ChartData,
ChartDataset,
ChartOptions,
ScatterDataPoint,
} from "chart.js";
import {
addHours,
differenceInDays,
differenceInHours,
endOfToday,
isToday,
startOfToday,
@ -18,6 +24,7 @@ import {
rgb2lab,
} from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import {
@ -27,6 +34,7 @@ import {
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
import { Statistics } from "../../../../data/history";
import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types";
@ -50,6 +58,10 @@ export class HuiEnergyUsageGraphCard
@state() private _end = endOfToday();
@state() private _compareStart?: Date;
@state() private _compareEnd?: Date;
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
@ -88,7 +100,9 @@ export class HuiEnergyUsageGraphCard
.options=${this._createOptions(
this._start,
this._end,
this.hass.locale
this.hass.locale,
this._compareStart,
this._compareEnd
)}
chart-type="bar"
></ha-chart-base>
@ -107,9 +121,27 @@ export class HuiEnergyUsageGraphCard
}
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);
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,
animation: false,
scales: {
@ -152,7 +184,6 @@ export class HuiEnergyUsageGraphCard
? "day"
: "hour",
},
offset: true,
},
y: {
stacked: true,
@ -179,7 +210,9 @@ export class HuiEnergyUsageGraphCard
return datasets[0].label;
}
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),
locale
)}`;
@ -240,13 +273,22 @@ export class HuiEnergyUsageGraphCard
// @ts-expect-error
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> {
const datasets: ChartDataset<"bar">[] = [];
const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
const statistics: {
const statIds: {
to_grid?: string[];
from_grid?: string[];
solar?: string[];
@ -256,21 +298,21 @@ export class HuiEnergyUsageGraphCard
for (const source of energyData.prefs.energy_sources) {
if (source.type === "solar") {
if (statistics.solar) {
statistics.solar.push(source.stat_energy_from);
if (statIds.solar) {
statIds.solar.push(source.stat_energy_from);
} else {
statistics.solar = [source.stat_energy_from];
statIds.solar = [source.stat_energy_from];
}
continue;
}
if (source.type === "battery") {
if (statistics.to_battery) {
statistics.to_battery.push(source.stat_energy_to);
statistics.from_battery!.push(source.stat_energy_from);
if (statIds.to_battery) {
statIds.to_battery.push(source.stat_energy_to);
statIds.from_battery!.push(source.stat_energy_from);
} else {
statistics.to_battery = [source.stat_energy_to];
statistics.from_battery = [source.stat_energy_from];
statIds.to_battery = [source.stat_energy_to];
statIds.from_battery = [source.stat_energy_from];
}
continue;
}
@ -281,41 +323,21 @@ export class HuiEnergyUsageGraphCard
// grid source
for (const flowFrom of source.flow_from) {
if (statistics.from_grid) {
statistics.from_grid.push(flowFrom.stat_energy_from);
if (statIds.from_grid) {
statIds.from_grid.push(flowFrom.stat_energy_from);
} else {
statistics.from_grid = [flowFrom.stat_energy_from];
statIds.from_grid = [flowFrom.stat_energy_from];
}
}
for (const flowTo of source.flow_to) {
if (statistics.to_grid) {
statistics.to_grid.push(flowTo.stat_energy_to);
if (statIds.to_grid) {
statIds.to_grid.push(flowTo.stat_energy_to);
} 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 colors = {
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 = [
"solar",
"to_grid",
@ -361,7 +464,7 @@ export class HuiEnergyUsageGraphCard
const totalStats: { [start: string]: number } = {};
const sets: { [statId: string]: { [start: string]: number } } = {};
statIds!.forEach((id) => {
const stats = energyData.stats[id];
const stats = statistics[id];
if (!stats) {
return;
}
@ -477,7 +580,6 @@ export class HuiEnergyUsageGraphCard
Object.entries(combinedData).forEach(([type, sources]) => {
Object.entries(sources).forEach(([statId, source], idx) => {
const data: ChartDataset<"bar">[] = [];
const entity = this.hass.states[statId];
const modifiedColor =
@ -490,6 +592,20 @@ export class HuiEnergyUsageGraphCard
? rgb2hex(lab2rgb(modifiedColor))
: 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({
label:
type in labels
@ -499,38 +615,19 @@ export class HuiEnergyUsageGraphCard
: statId,
order:
type === "used_solar"
? 0
? 1
: type === "to_battery"
? Object.keys(combinedData).length
: idx + 1,
borderColor,
backgroundColor: borderColor + "7F",
: idx + 2,
borderColor: compare ? borderColor + "7F" : borderColor,
backgroundColor: compare ? borderColor + "32" : borderColor + "7F",
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);
});
});
this._chartData = {
datasets,
};
return data;
}
static get styles(): CSSResultGroup {

View File

@ -97,6 +97,10 @@ export interface ButtonCardConfig extends LovelaceCardConfig {
show_state?: boolean;
}
export interface EnergyCardBaseConfig extends LovelaceCardConfig {
collection_key?: string;
}
export interface EnergySummaryCardConfig extends LovelaceCardConfig {
type: "energy-summary";
title?: string;

View File

@ -46,6 +46,8 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
@state() private _period?: "day" | "week" | "month" | "year";
@state() private _compare? = false;
public connectedCallback() {
super.connectedCallback();
toggleAttribute(this, "narrow", this.offsetWidth < 600);
@ -134,6 +136,14 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
dense
@value-changed=${this._handleView}
></ha-button-toggle-group>
<mwc-button
class="compare ${this._compare ? "active" : ""}"
@click=${this._toggleCompare}
dense
outlined
>
Compare data
</mwc-button>
</div>
</div>
`;
@ -216,6 +226,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
}
private _updateDates(energyData: EnergyData): void {
this._compare = energyData.startCompare !== undefined;
this._startDate = energyData.start;
this._endDate = energyData.end || endOfToday();
const dayDifference = differenceInDays(this._endDate, this._startDate);
@ -231,6 +242,15 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
: 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 {
return css`
.row {
@ -251,12 +271,37 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
}
.period {
display: flex;
flex-wrap: wrap;
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 {
--mdc-button-outline-color: currentColor;
--primary-color: currentColor;
--mdc-theme-primary: currentColor;
--mdc-theme-on-primary: currentColor;
--mdc-button-disabled-outline-color: var(--disabled-text-color);
--mdc-button-disabled-ink-color: var(--disabled-text-color);
--mdc-icon-button-ripple-opacity: 0.2;

View File

@ -35,6 +35,7 @@ const LAZY_LOAD_TYPES = {
calendar: () => import("../cards/hui-calendar-card"),
conditional: () => import("../cards/hui-conditional-card"),
"empty-state": () => import("../cards/hui-empty-state-card"),
"energy-compare": () => import("../cards/energy/hui-energy-compare-card"),
"energy-carbon-consumed-gauge": () =>
import("../cards/energy/hui-energy-carbon-consumed-gauge-card"),
"energy-date-selection": () =>