Add period selection to energy dashboard (#9756)

This commit is contained in:
Bram Kragten 2021-08-11 01:22:27 +02:00 committed by GitHub
parent 3897e3d452
commit dc50e54afc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 672 additions and 262 deletions

View File

@ -1,4 +1,4 @@
import { addHours, differenceInHours } from "date-fns";
import { addHours, differenceInHours, endOfDay } from "date-fns";
import { HassEntity } from "home-assistant-js-websocket";
import { StatisticValue } from "../../../src/data/history";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
@ -222,6 +222,7 @@ const statisticsFunctions: Record<
"sensor.energy_production_tarif_2": (id, start, end) => {
const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
const dayEnd = new Date(endOfDay(productionEnd));
const production = generateCurvedStatistics(
id,
productionStart,
@ -237,15 +238,17 @@ const statisticsFunctions: Record<
const evening = generateSumStatistics(
id,
productionEnd,
end,
dayEnd,
productionFinalVal,
0
);
return [...morning, ...production, ...evening];
const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 1);
return [...morning, ...production, ...evening, ...rest];
},
"sensor.solar_production": (id, start, end) => {
const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
const dayEnd = new Date(endOfDay(productionEnd));
const production = generateCurvedStatistics(
id,
productionStart,
@ -261,11 +264,12 @@ const statisticsFunctions: Record<
const evening = generateSumStatistics(
id,
productionEnd,
end,
dayEnd,
productionFinalVal,
0
);
return [...morning, ...production, ...evening];
const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 2);
return [...morning, ...production, ...evening, ...rest];
},
"sensor.grid_fossil_fuel_percentage": (id, start, end) =>
generateMeanStatistics(id, start, end, 35, 1.3),

View File

@ -17,7 +17,7 @@
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types",
"format": "yarn run format:eslint && yarn run format:prettier",
"mocha": "ts-mocha -p test-mocha/tsconfig.test.json \"test-mocha/**/*.ts\"",
"test": "yarn run lint && yarn run mocha"
"test": "yarn run mocha"
},
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",

View File

@ -3,33 +3,11 @@ import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
const formatDateMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "long",
day: "numeric",
})
);
export const formatDate = toLocaleDateStringSupportsOptions
// Tuesday, August 10
export const formatDateWeekday = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "longDate");
const formatDateShortMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
day: "numeric",
month: "short",
})
);
export const formatDateShort = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateShortMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "shortDate");
formatDateWeekdayMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "dddd, MMMM D");
const formatDateWeekdayMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
@ -39,7 +17,80 @@ const formatDateWeekdayMem = memoizeOne(
})
);
export const formatDateWeekday = toLocaleDateStringSupportsOptions
// August 10, 2021
export const formatDate = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateWeekdayMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "dddd, MMM D");
formatDateMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY");
const formatDateMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "long",
day: "numeric",
})
);
// 10/08/2021
export const formatDateNumeric = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateNumericMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "M/D/YYYY");
const formatDateNumericMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "numeric",
day: "numeric",
})
);
// Aug 10
export const formatDateShort = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateShortMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "MMM D");
const formatDateShortMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
day: "numeric",
month: "short",
})
);
// August 2021
export const formatDateMonthYear = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateMonthYearMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "MMMM YYYY");
const formatDateMonthYearMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
month: "long",
year: "numeric",
})
);
// August
export const formatDateMonth = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateMonthMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "MMMM");
const formatDateMonthMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
month: "long",
})
);
// 2021
export const formatDateYear = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateYearMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "YYYY");
const formatDateYearMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
})
);

View File

@ -4,6 +4,12 @@ import { FrontendLocaleData } from "../../data/translation";
import { toLocaleStringSupportsOptions } from "./check_options_support";
import { useAmPm } from "./use_am_pm";
// August 9, 2021, 8:23 AM
export const formatDateTime = toLocaleStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateTimeMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "MMMM D, YYYY, HH:mm" + useAmPm(locale) ? " A" : "");
const formatDateTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
@ -16,12 +22,12 @@ const formatDateTimeMem = memoizeOne(
})
);
export const formatDateTime = toLocaleStringSupportsOptions
// August 9, 2021, 8:23:15 AM
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateTimeMem(locale).format(dateObj)
formatDateTimeWithSecondsMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "MMMM D, YYYY, HH:mm" + useAmPm(locale) ? " A" : "");
format(dateObj, "MMMM D, YYYY, HH:mm:ss" + useAmPm(locale) ? " A" : "");
const formatDateTimeWithSecondsMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
@ -35,8 +41,20 @@ const formatDateTimeWithSecondsMem = memoizeOne(
})
);
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
// 9/8/2021, 8:23 AM
export const formatDateTimeNumeric = toLocaleStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateTimeWithSecondsMem(locale).format(dateObj)
formatDateTimeNumericMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "MMMM D, YYYY, HH:mm:ss" + useAmPm(locale) ? " A" : "");
format(dateObj, "M/D/YYYY, HH:mm" + useAmPm(locale) ? " A" : "");
const formatDateTimeNumericMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: useAmPm(locale),
})
);

View File

@ -4,6 +4,12 @@ import { FrontendLocaleData } from "../../data/translation";
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
import { useAmPm } from "./use_am_pm";
// 9:15 PM || 21:15
export const formatTime = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatTimeMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "shortTime" + useAmPm(locale) ? " A" : "");
const formatTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
@ -13,12 +19,12 @@ const formatTimeMem = memoizeOne(
})
);
export const formatTime = toLocaleTimeStringSupportsOptions
// 9:15:24 PM || 21:15:24
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatTimeMem(locale).format(dateObj)
formatTimeWithSecondsMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "shortTime" + useAmPm(locale) ? " A" : "");
format(dateObj, "mediumTime" + useAmPm(locale) ? " A" : "");
const formatTimeWithSecondsMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
@ -29,12 +35,12 @@ const formatTimeWithSecondsMem = memoizeOne(
})
);
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
// Tuesday 7:00 PM || Tuesday 19:00
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatTimeWithSecondsMem(locale).format(dateObj)
formatTimeWeekdayMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "mediumTime" + useAmPm(locale) ? " A" : "");
format(dateObj, "dddd, HH:mm" + useAmPm(locale) ? " A" : "");
const formatTimeWeekdayMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
@ -44,9 +50,3 @@ const formatTimeWeekdayMem = memoizeOne(
hour12: useAmPm(locale),
})
);
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatTimeWeekdayMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "dddd, HH:mm" + useAmPm(locale) ? " A" : "");

View File

@ -1,6 +1,7 @@
import memoizeOne from "memoize-one";
import { FrontendLocaleData, TimeFormat } from "../../data/translation";
export const useAmPm = (locale: FrontendLocaleData): boolean => {
export const useAmPm = memoizeOne((locale: FrontendLocaleData): boolean => {
if (
locale.time_format === TimeFormat.language ||
locale.time_format === TimeFormat.system
@ -12,4 +13,4 @@ export const useAmPm = (locale: FrontendLocaleData): boolean => {
}
return locale.time_format === TimeFormat.am_pm;
};
});

View File

@ -35,7 +35,14 @@ import {
endOfQuarter,
endOfYear,
} from "date-fns";
import { formatDate, formatDateShort } from "../../common/datetime/format_date";
import {
formatDate,
formatDateMonth,
formatDateMonthYear,
formatDateShort,
formatDateWeekday,
formatDateYear,
} from "../../common/datetime/format_date";
import {
formatDateTime,
formatDateTimeWithSeconds,
@ -53,8 +60,11 @@ const FORMATS = {
minute: "minute",
hour: "hour",
day: "day",
date: "date",
weekday: "weekday",
week: "week",
month: "month",
monthyear: "monthyear",
quarter: "quarter",
year: "year",
};
@ -81,16 +91,22 @@ _adapters._date.override({
return formatTime(new Date(time), this.options.locale);
case "hour":
return formatTime(new Date(time), this.options.locale);
case "weekday":
return formatDateWeekday(new Date(time), this.options.locale);
case "date":
return formatDate(new Date(time), this.options.locale);
case "day":
return formatDateShort(new Date(time), this.options.locale);
case "week":
return formatDate(new Date(time), this.options.locale);
case "month":
return formatDate(new Date(time), this.options.locale);
return formatDateMonth(new Date(time), this.options.locale);
case "monthyear":
return formatDateMonthYear(new Date(time), this.options.locale);
case "quarter":
return formatDate(new Date(time), this.options.locale);
case "year":
return formatDate(new Date(time), this.options.locale);
return formatDateYear(new Date(time), this.options.locale);
default:
return "";
}

View File

@ -15,6 +15,8 @@ export class HaButtonToggleGroup extends LitElement {
@property({ type: Boolean }) public fullWidth = false;
@property({ type: Boolean }) public dense = false;
protected render(): TemplateResult {
return html`
<div>
@ -34,6 +36,8 @@ export class HaButtonToggleGroup extends LitElement {
? `${100 / this.buttons.length}%`
: "initial",
})}
outlined
.dense=${this.dense}
.value=${button.value}
?active=${this.active === button.value}
@click=${this._handleClick}
@ -56,10 +60,16 @@ export class HaButtonToggleGroup extends LitElement {
--mdc-icon-button-size: var(--button-toggle-size, 36px);
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
}
mwc-icon-button,
mwc-button {
--mdc-shape-small: 0;
--mdc-button-outline-width: 1px 0 1px 1px;
}
mwc-icon-button {
border: 1px solid var(--primary-color);
border-right-width: 0px;
}
mwc-icon-button,
mwc-button {
position: relative;
cursor: pointer;
}
@ -82,16 +92,19 @@ export class HaButtonToggleGroup extends LitElement {
}
mwc-icon-button:first-child,
mwc-button:first-child {
--mdc-shape-small: 4px 0 0 4px;
border-radius: 4px 0 0 4px;
}
mwc-icon-button:last-child,
mwc-button:last-child {
border-radius: 0 4px 4px 0;
border-right-width: 1px;
--mdc-shape-small: 0 4px 4px 0;
--mdc-button-outline-width: 1px;
}
mwc-icon-button:only-child,
mwc-button:only-child {
border-radius: 4px;
--mdc-shape-small: 4px;
border-right-width: 1px;
}
`;

View File

@ -1,10 +1,8 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { useAmPm } from "../../common/datetime/use_am_pm";
import { fireEvent } from "../../common/dom/fire_event";
import { TimeSelector } from "../../data/selector";
import { FrontendLocaleData } from "../../data/translation";
import { HomeAssistant } from "../../types";
import "../paper-time-input";
@ -20,12 +18,8 @@ export class HaTimeSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
private _useAmPmMem = memoizeOne((locale: FrontendLocaleData): boolean =>
useAmPm(locale)
);
protected render() {
const useAMPM = this._useAmPmMem(this.hass.locale);
const useAMPM = useAmPm(this.hass.locale);
const parts = this.value?.split(":") || [];
const hours = parts[0];
@ -50,7 +44,7 @@ export class HaTimeSelector extends LitElement {
private _timeChanged(ev) {
let value = ev.target.value;
const useAMPM = this._useAmPmMem(this.hass.locale);
const useAMPM = useAmPm(this.hass.locale);
let hours = Number(ev.target.hour || 0);
if (value && useAMPM) {
if (ev.target.amPm === "PM") {

View File

@ -1,3 +1,4 @@
import { addDays, addMonths, startOfDay, startOfMonth } from "date-fns";
import { HassEntity } from "home-assistant-js-websocket";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateDomain } from "../common/entity/compute_state_domain";
@ -406,3 +407,90 @@ export const calculateStatisticsSumGrowthWithPercentage = (
return sum;
};
export const reduceSumStatisticsByDay = (
values: StatisticValue[]
): StatisticValue[] => {
if (!values?.length) {
return [];
}
const result: StatisticValue[] = [];
if (
values.length > 1 &&
new Date(values[0].start).getDate() === new Date(values[1].start).getDate()
) {
// add init value if the first value isn't end of previous period
result.push({
...values[0]!,
start: startOfMonth(addDays(new Date(values[0].start), -1)).toISOString(),
});
}
let lastValue: StatisticValue;
let prevDate: number | undefined;
for (const value of values) {
const date = new Date(value.start).getDate();
if (prevDate === undefined) {
prevDate = date;
}
if (prevDate !== date) {
// Last value of the day
result.push({
...lastValue!,
start: startOfDay(new Date(lastValue!.start)).toISOString(),
});
prevDate = date;
}
lastValue = value;
}
// Add final value
result.push({
...lastValue!,
start: startOfDay(new Date(lastValue!.start)).toISOString(),
});
return result;
};
export const reduceSumStatisticsByMonth = (
values: StatisticValue[]
): StatisticValue[] => {
if (!values?.length) {
return [];
}
const result: StatisticValue[] = [];
if (
values.length > 1 &&
new Date(values[0].start).getMonth() ===
new Date(values[1].start).getMonth()
) {
// add init value if the first value isn't end of previous period
result.push({
...values[0]!,
start: startOfMonth(
addMonths(new Date(values[0].start), -1)
).toISOString(),
});
}
let lastValue: StatisticValue;
let prevMonth: number | undefined;
for (const value of values) {
const month = new Date(value.start).getMonth();
if (prevMonth === undefined) {
prevMonth = month;
}
if (prevMonth !== month) {
// Last value of the day
result.push({
...lastValue!,
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
});
prevMonth = month;
}
lastValue = value;
}
// Add final value
result.push({
...lastValue!,
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
});
return result;
};

View File

@ -10,7 +10,13 @@ import {
ChartOptions,
ScatterDataPoint,
} from "chart.js";
import { endOfToday, isToday, startOfToday } from "date-fns";
import {
addHours,
differenceInDays,
endOfToday,
isToday,
startOfToday,
} from "date-fns";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergySolarGraphCardConfig } from "../types";
@ -39,6 +45,10 @@ import {
} from "../../../../common/string/format_number";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { FrontendLocaleData } from "../../../../data/translation";
import {
reduceSumStatisticsByMonth,
reduceSumStatisticsByDay,
} from "../../../../data/history";
@customElement("hui-energy-solar-graph-card")
export class HuiEnergySolarGraphCard
@ -110,14 +120,22 @@ export class HuiEnergySolarGraphCard
}
private _createOptions = memoizeOne(
(start: Date, end: Date, locale: FrontendLocaleData): ChartOptions => ({
(start: Date, end: Date, locale: FrontendLocaleData): ChartOptions => {
const dayDifference = differenceInDays(end, start);
return {
parsing: false,
animation: false,
scales: {
x: {
type: "time",
suggestedMin: start.getTime(),
suggestedMax: end.getTime(),
suggestedMin: (dayDifference > 2
? addHours(start, -11)
: start
).getTime(),
suggestedMax: (dayDifference > 2
? addHours(end, -11)
: end
).getTime(),
adapters: {
date: {
locale: locale,
@ -136,7 +154,22 @@ export class HuiEnergySolarGraphCard
: {},
},
time: {
tooltipFormat: "datetime",
tooltipFormat:
dayDifference > 35
? "monthyear"
: dayDifference > 7
? "date"
: dayDifference > 2
? "weekday"
: dayDifference > 0
? "datetime"
: "hour",
minUnit:
dayDifference > 35
? "month"
: dayDifference > 2
? "day"
: "hour",
},
offset: true,
},
@ -187,7 +220,8 @@ export class HuiEnergySolarGraphCard
},
// @ts-expect-error
locale: numberFormatToLocale(locale),
})
};
}
);
private async _getStatistics(energyData: EnergyData): Promise<void> {
@ -229,6 +263,11 @@ export class HuiEnergySolarGraphCard
.getPropertyValue("--energy-solar-color")
.trim();
const dayDifference = differenceInDays(
energyData.end || new Date(),
energyData.start
);
solarSources.forEach((source, idx) => {
const data: ChartDataset<"bar" | "line">[] = [];
const entity = this.hass.states[source.stat_energy_from];
@ -244,9 +283,20 @@ export class HuiEnergySolarGraphCard
const solarProductionData: ScatterDataPoint[] = [];
// Process solar production data.
if (energyData.stats[source.stat_energy_from]) {
for (const point of energyData.stats[source.stat_energy_from]) {
if (!point.sum) {
if (source.stat_energy_from in energyData.stats) {
const stats =
dayDifference > 35
? reduceSumStatisticsByMonth(
energyData.stats[source.stat_energy_from]
)
: dayDifference > 2
? reduceSumStatisticsByDay(
energyData.stats[source.stat_energy_from]
)
: energyData.stats[source.stat_energy_from];
for (const point of stats) {
if (point.sum === null) {
continue;
}
if (prevValue === null) {
@ -294,7 +344,14 @@ export class HuiEnergySolarGraphCard
) {
return;
}
if (dayDifference > 35) {
dateObj.setDate(1);
}
if (dayDifference > 2) {
dateObj.setHours(0, 0, 0, 0);
} else {
dateObj.setMinutes(0, 0, 0);
}
const time = dateObj.getTime();
if (time in forecastsData) {
forecastsData[time] += value;

View File

@ -1,5 +1,11 @@
import { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { startOfToday, endOfToday, isToday } from "date-fns";
import {
startOfToday,
endOfToday,
isToday,
differenceInDays,
addHours,
} 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";
@ -21,6 +27,10 @@ import {
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
import {
reduceSumStatisticsByDay,
reduceSumStatisticsByMonth,
} from "../../../../data/history";
import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types";
@ -97,14 +107,22 @@ export class HuiEnergyUsageGraphCard
}
private _createOptions = memoizeOne(
(start: Date, end: Date, locale: FrontendLocaleData): ChartOptions => ({
(start: Date, end: Date, locale: FrontendLocaleData): ChartOptions => {
const dayDifference = differenceInDays(end, start);
return {
parsing: false,
animation: false,
scales: {
x: {
type: "time",
suggestedMin: start.getTime(),
suggestedMax: end.getTime(),
suggestedMin: (dayDifference > 2
? addHours(start, -11)
: start
).getTime(),
suggestedMax: (dayDifference > 2
? addHours(end, -11)
: end
).getTime(),
adapters: {
date: {
locale: locale,
@ -123,7 +141,22 @@ export class HuiEnergyUsageGraphCard
: {},
},
time: {
tooltipFormat: "datetime",
tooltipFormat:
dayDifference > 35
? "monthyear"
: dayDifference > 7
? "date"
: dayDifference > 2
? "weekday"
: dayDifference > 0
? "datetime"
: "hour",
minUnit:
dayDifference > 35
? "month"
: dayDifference > 2
? "day"
: "hour",
},
offset: true,
},
@ -166,10 +199,16 @@ export class HuiEnergyUsageGraphCard
}
return [
totalConsumed
? `Total consumed: ${formatNumber(totalConsumed, locale)} kWh`
? `Total consumed: ${formatNumber(
totalConsumed,
locale
)} kWh`
: "",
totalReturned
? `Total returned: ${formatNumber(totalReturned, locale)} kWh`
? `Total returned: ${formatNumber(
totalReturned,
locale
)} kWh`
: "",
].filter(Boolean);
},
@ -196,7 +235,8 @@ export class HuiEnergyUsageGraphCard
},
// @ts-expect-error
locale: numberFormatToLocale(locale),
})
};
}
);
private async _getStatistics(energyData: EnergyData): Promise<void> {
@ -233,7 +273,13 @@ export class HuiEnergyUsageGraphCard
}
}
const dayDifference = differenceInDays(
energyData.end || new Date(),
energyData.start
);
const statisticsData = Object.values(energyData.stats);
const datasets: ChartDataset<"bar">[] = [];
let endTime: Date;
@ -287,14 +333,20 @@ 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 =
dayDifference > 35
? reduceSumStatisticsByMonth(energyData.stats[id])
: dayDifference > 2
? reduceSumStatisticsByDay(energyData.stats[id])
: energyData.stats[id];
if (!stats) {
return;
}
const set = {};
let prevValue: number;
stats.forEach((stat) => {
if (!stat.sum) {
if (stat.sum === null) {
return;
}
if (!prevValue) {

View File

@ -1,15 +1,45 @@
import { mdiChevronLeft, mdiChevronRight } from "@mdi/js";
import { endOfToday, addDays, endOfDay, isToday, startOfToday } from "date-fns";
import {
endOfToday,
addDays,
endOfDay,
startOfToday,
endOfWeek,
endOfMonth,
startOfDay,
startOfWeek,
startOfMonth,
addMonths,
addWeeks,
startOfYear,
addYears,
endOfYear,
isWithinInterval,
differenceInDays,
} 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 {
formatDate,
formatDateMonthYear,
formatDateShort,
formatDateYear,
} from "../../../common/datetime/format_date";
import { EnergyData, getEnergyDataCollection } from "../../../data/energy";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../types";
import { HomeAssistant, ToggleButton } from "../../../types";
import "@material/mwc-icon-button/mwc-icon-button";
import "../../../components/ha-svg-icon";
import "@material/mwc-button/mwc-button";
import "../../../components/ha-button-toggle-group";
const viewButtons: ToggleButton[] = [
{ label: "Day", value: "day" },
{ label: "Week", value: "week" },
{ label: "Month", value: "month" },
{ label: "Year", value: "year" },
];
@customElement("hui-energy-period-selector")
export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
@ -21,6 +51,8 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
@state() _endDate?: Date;
@state() private _period?: "day" | "week" | "month" | "year";
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass, {
@ -37,41 +69,110 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
return html`
<div class="row">
<div class="label">
${formatDate(this._startDate, this.hass.locale)}
${this._period === "day"
? formatDate(this._startDate, this.hass.locale)
: this._period === "month"
? formatDateMonthYear(this._startDate, this.hass.locale)
: this._period === "year"
? formatDateYear(this._startDate, this.hass.locale)
: `${formatDateShort(
this._startDate,
this.hass.locale
)} - ${formatDateShort(
this._endDate || new Date(),
this.hass.locale
)}`}
</div>
<mwc-icon-button label="Previous Day" @click=${this._pickPreviousDay}>
<mwc-icon-button label="Previous" @click=${this._pickPrevious}>
<ha-svg-icon .path=${mdiChevronLeft}></ha-svg-icon>
</mwc-icon-button>
<mwc-icon-button label="Next Day" @click=${this._pickNextDay}>
<mwc-icon-button label="Next" @click=${this._pickNext}>
<ha-svg-icon .path=${mdiChevronRight}></ha-svg-icon>
</mwc-icon-button>
<mwc-button
<mwc-button dense outlined @click=${this._pickToday}>
Today
</mwc-button>
<ha-button-toggle-group
.buttons=${viewButtons}
.active=${this._period}
dense
outlined
.disabled=${isToday(this._startDate)}
@click=${this._pickToday}
>Today</mwc-button
>
@value-changed=${this._handleView}
></ha-button-toggle-group>
</div>
`;
}
private _handleView(ev: CustomEvent): void {
this._period = ev.detail.value;
const today = startOfToday();
const start =
!this._startDate ||
isWithinInterval(today, {
start: this._startDate,
end: this._endDate || endOfToday(),
})
? today
: this._startDate;
this._setDate(
this._period === "day"
? startOfDay(start)
: this._period === "week"
? startOfWeek(start, { weekStartsOn: 1 })
: this._period === "month"
? startOfMonth(start)
: startOfYear(start)
);
}
private _pickToday() {
this._setDate(startOfToday());
this._setDate(
this._period === "day"
? startOfToday()
: this._period === "week"
? startOfWeek(new Date(), { weekStartsOn: 1 })
: this._period === "month"
? startOfMonth(new Date())
: startOfYear(new Date())
);
}
private _pickPreviousDay() {
this._setDate(addDays(this._startDate!, -1));
private _pickPrevious() {
const newStart =
this._period === "day"
? addDays(this._startDate!, -1)
: this._period === "week"
? addWeeks(this._startDate!, -1)
: this._period === "month"
? addMonths(this._startDate!, -1)
: addYears(this._startDate!, -1);
this._setDate(newStart);
}
private _pickNextDay() {
this._setDate(addDays(this._startDate!, +1));
private _pickNext() {
const newStart =
this._period === "day"
? addDays(this._startDate!, 1)
: this._period === "week"
? addWeeks(this._startDate!, 1)
: this._period === "month"
? addMonths(this._startDate!, 1)
: addYears(this._startDate!, 1);
this._setDate(newStart);
}
private _setDate(startDate: Date) {
const endDate = endOfDay(startDate);
const endDate =
this._period === "day"
? endOfDay(startDate)
: this._period === "week"
? endOfWeek(startDate, { weekStartsOn: 1 })
: this._period === "month"
? endOfMonth(startDate)
: endOfYear(startDate);
const energyCollection = getEnergyDataCollection(this.hass, {
key: this.collectionKey,
});
@ -82,6 +183,17 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
private _updateDates(energyData: EnergyData): void {
this._startDate = energyData.start;
this._endDate = energyData.end || endOfToday();
const dayDifference = differenceInDays(this._endDate, this._startDate);
this._period =
dayDifference < 1
? "day"
: dayDifference === 6
? "week"
: dayDifference > 26 && dayDifference < 31 // 28, 29, 30 or 31 days in a month
? "month"
: dayDifference === 364 || dayDifference === 365 // Leap year
? "year"
: undefined;
}
static get styles(): CSSResultGroup {
@ -96,16 +208,20 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
text-align: center;
font-size: 20px;
}
:host {
--mdc-button-outline-color: currentColor;
--primary-color: currentColor;
--mdc-theme-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;
}
mwc-icon-button {
--mdc-icon-button-size: 28px;
}
mwc-button {
mwc-button,
ha-button-toggle-group {
padding-left: 8px;
--mdc-theme-primary: currentColor;
--mdc-button-outline-color: currentColor;
--mdc-button-disabled-outline-color: var(--disabled-text-color);
--mdc-button-disabled-ink-color: var(--disabled-text-color);
}
`;
}

View File

@ -130,7 +130,7 @@ export type FullCalendarView =
export interface ToggleButton {
label: string;
iconPath: string;
iconPath?: string;
value: string;
}