Add date range picker to energy period selector (#14337)

This commit is contained in:
Till 2023-10-12 18:29:04 +02:00 committed by GitHub
parent f8966a2114
commit 607175706b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 799 additions and 345 deletions

View File

@ -260,7 +260,6 @@ export class HcMain extends HassElement {
{
strategy: {
type: "energy",
show_date_selection: true,
},
},
],

View File

@ -5,12 +5,15 @@ import { FrontendLocaleData, TimeZone } from "../../data/translation";
const calcZonedDate = (
date: Date,
tz: string,
fn: (date: Date, options?: any) => Date,
fn: (date: Date, options?: any) => Date | number | boolean,
options?
) => {
const inputZoned = utcToZonedTime(date, tz);
const fnZoned = fn(inputZoned, options);
return zonedTimeToUtc(fnZoned, tz);
if (fnZoned instanceof Date) {
return zonedTimeToUtc(fnZoned, tz) as Date;
}
return fnZoned;
};
export const calcDate = (
@ -21,5 +24,16 @@ export const calcDate = (
options?
) =>
locale.time_zone === TimeZone.server
? calcZonedDate(date, config.time_zone, fn, options)
? (calcZonedDate(date, config.time_zone, fn, options) as Date)
: fn(date, options);
export const calcDateProperty = (
date: Date,
fn: (date: Date, options?: any) => boolean | number,
locale: FrontendLocaleData,
config: HassConfig,
options?
) =>
locale.time_zone === TimeZone.server
? (calcZonedDate(date, config.time_zone, fn, options) as number | boolean)
: fn(date, options);

View File

@ -37,6 +37,23 @@ const formatDateMem = memoizeOne(
})
);
// Aug 10, 2021
export const formatDateShort = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatDateShortMem(locale, config.time_zone).format(dateObj);
const formatDateShortMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "short",
day: "numeric",
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
})
);
// 10/08/2021
export const formatDateNumeric = (
dateObj: Date,
@ -102,13 +119,13 @@ const formatDateNumericMem = memoizeOne(
);
// Aug 10
export const formatDateShort = (
export const formatDateVeryShort = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatDateShortMem(locale, config.time_zone).format(dateObj);
) => formatDateVeryShortMem(locale, config.time_zone).format(dateObj);
const formatDateShortMem = memoizeOne(
const formatDateVeryShortMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
day: "numeric",

View File

@ -39,7 +39,7 @@ import {
formatDate,
formatDateMonth,
formatDateMonthYear,
formatDateShort,
formatDateVeryShort,
formatDateWeekdayDay,
formatDateYear,
} from "../../common/datetime/format_date";
@ -128,7 +128,7 @@ _adapters._date.override({
this.options.config
);
case "day":
return formatDateShort(
return formatDateVeryShort(
new Date(time),
this.options.locale,
this.options.config

View File

@ -31,6 +31,10 @@ const Component = Vue.extend({
type: Boolean,
default: true,
},
openingDirection: {
type: String,
default: "right",
},
disabled: {
type: Boolean,
default: false,
@ -66,7 +70,7 @@ const Component = Vue.extend({
props: {
"time-picker": this.timePicker,
"auto-apply": this.autoApply,
opens: "right",
opens: this.openingDirection,
"show-dropdowns": false,
"time-picker24-hour": this.twentyfourHours,
disabled: this.disabled,
@ -126,9 +130,9 @@ class DateRangePickerElement extends WrappedElement {
${dateRangePickerStyles}
.calendars {
display: flex;
flex-wrap: nowrap !important;
}
.daterangepicker {
left: 0px !important;
top: auto;
box-shadow: var(--ha-card-box-shadow, none);
background-color: var(--card-background-color);
@ -252,6 +256,10 @@ class DateRangePickerElement extends WrappedElement {
direction: ltr;
text-align: left;
}
.vue-daterange-picker{
min-width: unset !important;
display: block !important;
}
`;
const shadowRoot = this.shadowRoot!;
shadowRoot.appendChild(style);

View File

@ -15,6 +15,7 @@ import {
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
} from "lit";
@ -29,6 +30,7 @@ import { HomeAssistant } from "../types";
import "./date-range-picker";
import "./ha-svg-icon";
import "./ha-textfield";
import "./ha-icon-button";
export interface DateRangePickerRanges {
[key: string]: [Date, Date];
@ -54,7 +56,21 @@ export class HaDateRangePicker extends LitElement {
@property({ type: String }) private _rtlDirection = "ltr";
@property({ type: Boolean }) private minimal = false;
@property() private _openingDirection = "right";
protected willUpdate() {
// set dialog opening direction based on position
const datePickerPosition = this.getBoundingClientRect().x;
if (datePickerPosition > (2 * window.innerWidth) / 3) {
this._openingDirection = "left";
} else if (datePickerPosition < window.innerWidth / 3) {
this._openingDirection = "right";
} else {
this._openingDirection = "center";
}
if (!this.hasUpdated && this.ranges === undefined) {
const today = new Date();
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
@ -133,41 +149,61 @@ export class HaDateRangePicker extends LitElement {
<date-range-picker
?disabled=${this.disabled}
?auto-apply=${this.autoApply}
?time-picker=${this.timePicker}
time-picker=${this.timePicker}
twentyfour-hours=${this._hour24format}
start-date=${this.startDate}
end-date=${this.endDate}
?ranges=${this.ranges !== false}
opening-direction=${this._openingDirection}
first-day=${firstWeekdayIndex(this.hass.locale)}
>
<div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<ha-textfield
.value=${this.timePicker
? formatDateTime(
this.startDate,
this.hass.locale,
this.hass.config
)
: formatDate(this.startDate, this.hass.locale, this.hass.config)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></ha-textfield>
<ha-textfield
.value=${this.timePicker
? formatDateTime(this.endDate, this.hass.locale, this.hass.config)
: formatDate(this.endDate, this.hass.locale, this.hass.config)}
.label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></ha-textfield>
${!this.minimal
? html`<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<ha-textfield
.value=${this.timePicker
? formatDateTime(
this.startDate,
this.hass.locale,
this.hass.config
)
: formatDate(
this.startDate,
this.hass.locale,
this.hass.config
)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></ha-textfield>
<ha-textfield
.value=${this.timePicker
? formatDateTime(
this.endDate,
this.hass.locale,
this.hass.config
)
: formatDate(
this.endDate,
this.hass.locale,
this.hass.config
)}
.label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></ha-textfield>`
: html`<ha-icon-button
.label=${this.hass.localize(
"ui.components.date-range-picker.select_date_range"
)}
.path=${mdiCalendar}
></ha-icon-button>`}
</div>
${this.ranges
? html`<div
@ -181,7 +217,7 @@ export class HaDateRangePicker extends LitElement {
)}
</mwc-list>
</div>`
: ""}
: nothing}
<div slot="footer" class="date-range-footer">
<mwc-button @click=${this._cancelDateRange}
>${this.hass.localize("ui.common.cancel")}</mwc-button
@ -234,6 +270,10 @@ export class HaDateRangePicker extends LitElement {
direction: var(--direction);
}
ha-icon-button {
direction: var(--direction);
}
.date-range-inputs {
display: flex;
align-items: center;

View File

@ -4,11 +4,14 @@ import {
addMilliseconds,
addMonths,
differenceInDays,
differenceInMonths,
endOfDay,
startOfDay,
isFirstDayOfMonth,
isLastDayOfMonth,
} from "date-fns/esm";
import { Collection, getCollection } from "home-assistant-js-websocket";
import { calcDate } from "../common/datetime/calc_date";
import { calcDate, calcDateProperty } from "../common/datetime/calc_date";
import { formatTime24h } from "../common/datetime/format_time";
import { groupBy } from "../common/util/group-by";
import { HomeAssistant } from "../types";
@ -416,11 +419,42 @@ const getEnergyData = async (
let _waterStatsCompare: Statistics | Promise<Statistics> = {};
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);
if (
(calcDateProperty(
start,
isFirstDayOfMonth,
hass.locale,
hass.config
) as boolean) &&
(calcDateProperty(
end || new Date(),
isLastDayOfMonth,
hass.locale,
hass.config
) as boolean)
) {
// When comparing a month (or multiple), we want to start at the begining of the month
startCompare = calcDate(
start,
addMonths,
hass.locale,
hass.config,
-(calcDateProperty(
end || new Date(),
differenceInMonths,
hass.locale,
hass.config,
start
) as number) - 1
);
} else {
startCompare = addDays(start, (dayDifference + 1) * -1);
startCompare = calcDate(
start,
addDays,
hass.locale,
hass.config,
(dayDifference + 1) * -1
);
}
endCompare = addMilliseconds(start, -1);
if (energyStatIds.length) {

View File

@ -5,6 +5,7 @@ import {
LitElement,
PropertyValues,
TemplateResult,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-menu-button";
@ -60,31 +61,33 @@ class PanelEnergy extends LitElement {
protected render(): TemplateResult {
return html`
<ha-top-app-bar-fixed>
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
<div slot="title">${this.hass.localize("panel.energy")}</div>
${this.narrow
? ""
: html`
<hui-energy-period-selector
slot="actionItems"
.hass=${this.hass}
collectionKey="energy_dashboard"
.narrow=${false}
></hui-energy-period-selector>
`}
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
@reload-energy-panel=${this._reloadView}
></hui-view>
</ha-top-app-bar-fixed>
<div class="header">
<div class="toolbar">
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
${!this.narrow
? html`<div class="main-title">
${this.hass.localize("panel.energy")}
</div>`
: nothing}
<hui-energy-period-selector
.hass=${this.hass}
collectionKey="energy_dashboard"
></hui-energy-period-selector>
</div>
</div>
<hui-view
id="view"
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
@reload-energy-panel=${this._reloadView}
></hui-view>
`;
}
@ -116,13 +119,83 @@ class PanelEnergy extends LitElement {
return [
haStyle,
css`
hui-energy-period-selector {
:host hui-energy-period-selector {
width: 100%;
padding-left: 16px;
padding-inline-start: 16px;
padding-left: 32px;
padding-inline-start: 32px;
--disabled-text-color: rgba(var(--rgb-text-primary-color), 0.5);
direction: var(--direction);
}
:host([narrow]) hui-energy-period-selector {
padding-left: 0px;
padding-inline-start: 0px;
}
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
position: fixed;
top: 0;
width: var(--mdc-top-app-bar-width, 100%);
padding-top: env(safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar {
height: var(--header-height);
display: flex;
flex: 1;
align-items: center;
font-size: 20px;
padding: 0px 12px;
font-weight: 400;
box-sizing: border-box;
}
@media (max-width: 599px) {
.toolbar {
padding: 0 4px;
}
}
.main-title {
margin: 0 0 0 24px;
line-height: 20px;
flex-grow: 1;
}
#view {
position: relative;
display: flex;
padding-top: calc(var(--header-height) + env(safe-area-inset-top));
min-height: 100vh;
box-sizing: border-box;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
hui-view {
background: var(
--lovelace-background,
var(--primary-background-color)
);
}
#view > * {
flex: 1 1 100%;
max-width: 100%;
}
`,
];
}

View File

@ -10,7 +10,6 @@ import {
LovelaceViewConfig,
} from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { LovelaceStrategyParams } from "../../lovelace/strategies/types";
const setupWizard = async (): Promise<LovelaceViewConfig> => {
await import("../cards/energy-setup-wizard-card");
@ -24,16 +23,11 @@ const setupWizard = async (): Promise<LovelaceViewConfig> => {
};
};
export interface EnergeryViewStrategyConfig extends LovelaceStrategyConfig {
show_date_selection?: boolean;
}
@customElement("energy-view-strategy")
export class EnergyViewStrategy extends ReactiveElement {
static async generate(
config: EnergeryViewStrategyConfig,
hass: HomeAssistant,
params: LovelaceStrategyParams
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = { cards: [] };
@ -67,14 +61,6 @@ export class EnergyViewStrategy extends ReactiveElement {
(source) => source.type === "water"
);
if (params.narrow || config.show_date_selection) {
view.cards!.push({
type: "energy-date-selection",
collection_key: "energy_dashboard",
view_layout: { position: "sidebar" },
});
}
view.cards!.push({
type: "energy-compare",
collection_key: "energy_dashboard",

View File

@ -1,4 +1,4 @@
import { html, LitElement, nothing } from "lit";
import { html, LitElement, nothing, css, CSSResultGroup } from "lit";
import { customElement, property, state } from "lit/decorators";
import { HomeAssistant } from "../../../../types";
import "../../components/hui-energy-period-selector";
@ -28,10 +28,22 @@ export class HuiEnergyDateSelectionCard
}
return html`
<hui-energy-period-selector
.hass=${this.hass}
.collectionKey=${this._config.collection_key}
></hui-energy-period-selector>
<ha-card>
<div class="card-content">
<hui-energy-period-selector
.hass=${this.hass}
.collectionKey=${this._config.collection_key}
></hui-energy-period-selector>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return css`
.padded {
padding-left: 16px !important;
}
`;
}
}

View File

@ -24,7 +24,7 @@ import {
rgb2lab,
} from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateShort } from "../../../../common/datetime/format_date";
import { formatDateVeryShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time";
import {
formatNumber,
@ -228,7 +228,9 @@ export class HuiEnergyGasGraphCard
}
const date = new Date(datasets[0].parsed.x);
return `${
compare ? `${formatDateShort(date, locale, config)}: ` : ""
compare
? `${formatDateVeryShort(date, locale, config)}: `
: ""
}${formatTime(date, locale, config)} ${formatTime(
addHours(date, 1),
locale,

View File

@ -24,7 +24,7 @@ import {
rgb2lab,
} from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateShort } from "../../../../common/datetime/format_date";
import { formatDateVeryShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time";
import {
formatNumber,
@ -224,7 +224,9 @@ export class HuiEnergySolarGraphCard
}
const date = new Date(datasets[0].parsed.x);
return `${
compare ? `${formatDateShort(date, locale, config)}: ` : ""
compare
? `${formatDateVeryShort(date, locale, config)}: `
: ""
}${formatTime(date, locale, config)} ${formatTime(
addHours(date, 1),
locale,

View File

@ -24,7 +24,7 @@ import {
rgb2lab,
} from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateShort } from "../../../../common/datetime/format_date";
import { formatDateVeryShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time";
import {
formatNumber,
@ -233,7 +233,9 @@ export class HuiEnergyUsageGraphCard
}
const date = new Date(datasets[0].parsed.x);
return `${
compare ? `${formatDateShort(date, locale, config)}: ` : ""
compare
? `${formatDateVeryShort(date, locale, config)}: `
: ""
}${formatTime(date, locale, config)} ${formatTime(
addHours(date, 1),
locale,

View File

@ -24,7 +24,7 @@ import {
rgb2lab,
} from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateShort } from "../../../../common/datetime/format_date";
import { formatDateVeryShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time";
import {
formatNumber,
@ -228,7 +228,9 @@ export class HuiEnergyWaterGraphCard
}
const date = new Date(datasets[0].parsed.x);
return `${
compare ? `${formatDateShort(date, locale, config)}: ` : ""
compare
? `${formatDateVeryShort(date, locale, config)}: `
: ""
}${formatTime(date, locale, config)} ${formatTime(
addHours(date, 1),
locale,

View File

@ -1,43 +1,56 @@
import "@material/mwc-button/mwc-button";
import { mdiCompare, mdiCompareRemove } from "@mdi/js";
import { mdiDotsVertical } from "@mdi/js";
import {
addDays,
subDays,
addMonths,
addWeeks,
addYears,
differenceInDays,
endOfDay,
endOfMonth,
endOfToday,
endOfWeek,
endOfQuarter,
endOfYear,
isWithinInterval,
isFirstDayOfMonth,
isLastDayOfMonth,
differenceInMonths,
startOfDay,
startOfMonth,
startOfToday,
startOfWeek,
startOfQuarter,
startOfYear,
} from "date-fns/esm";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { calcDate } from "../../../common/datetime/calc_date";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { calcDate, calcDateProperty } from "../../../common/datetime/calc_date";
import { firstWeekdayIndex } from "../../../common/datetime/first_weekday";
import {
formatDate,
formatDateMonthYear,
formatDateShort,
formatDateVeryShort,
formatDateMonthYear,
formatDateYear,
} from "../../../common/datetime/format_date";
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-button-toggle-group";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-next";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item";
import { EnergyData, getEnergyDataCollection } from "../../../data/energy";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, ToggleButton } from "../../../types";
import { HomeAssistant } from "../../../types";
import "../../../components/ha-date-range-picker";
import type { DateRangePickerRanges } from "../../../components/ha-date-range-picker";
import { loadPolyfillIfNeeded } from "../../../resources/resize-observer.polyfill";
import { debounce } from "../../../common/util/debounce";
@customElement("hui-energy-period-selector")
export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
@ -51,16 +64,11 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
@state() _endDate?: Date;
@state() private _period?: "day" | "week" | "month" | "year";
@state() private _ranges?: DateRangePickerRanges;
@state() private _compare = false;
public connectedCallback() {
super.connectedCallback();
if (this.narrow !== false) {
toggleAttribute(this, "narrow", this.offsetWidth < 600);
}
}
private _resizeObserver?: ResizeObserver;
public hassSubscribe(): UnsubscribeFunc[] {
return [
@ -70,64 +78,143 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
];
}
private _measure() {
this.narrow = this.offsetWidth < 450;
}
private async _attachObserver(): Promise<void> {
if (!this._resizeObserver) {
await loadPolyfillIfNeeded();
this._resizeObserver = new ResizeObserver(
debounce(() => this._measure(), 250, false)
);
}
this._resizeObserver.observe(this);
}
protected firstUpdated(): void {
this._attachObserver();
}
public connectedCallback(): void {
super.connectedCallback();
this.updateComplete.then(() => this._attachObserver());
}
public disconnectedCallback(): void {
if (this._resizeObserver) {
this._resizeObserver.disconnect();
}
}
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._measure();
}
const today = new Date();
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
// pre defined date ranges
this._ranges = {
[this.hass.localize("ui.components.date-range-picker.ranges.today")]: [
calcDate(today, startOfDay, this.hass.locale, this.hass.config, {
weekStartsOn,
}),
calcDate(today, endOfDay, this.hass.locale, this.hass.config, {
weekStartsOn,
}),
],
[this.hass.localize("ui.components.date-range-picker.ranges.yesterday")]:
[
calcDate(
calcDate(today, subDays, this.hass.locale, this.hass.config, 1),
startOfDay,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
),
calcDate(
calcDate(today, subDays, this.hass.locale, this.hass.config, 1),
endOfDay,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
),
],
[this.hass.localize("ui.components.date-range-picker.ranges.this_week")]:
[
calcDate(today, startOfWeek, this.hass.locale, this.hass.config, {
weekStartsOn,
}),
calcDate(today, endOfWeek, this.hass.locale, this.hass.config, {
weekStartsOn,
}),
],
[this.hass.localize("ui.components.date-range-picker.ranges.this_month")]:
[
calcDate(today, startOfMonth, this.hass.locale, this.hass.config),
calcDate(today, endOfMonth, this.hass.locale, this.hass.config),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.this_quarter"
)]: [
calcDate(today, startOfQuarter, this.hass.locale, this.hass.config),
calcDate(today, endOfQuarter, this.hass.locale, this.hass.config),
],
[this.hass.localize("ui.components.date-range-picker.ranges.this_year")]:
[
calcDate(today, startOfYear, this.hass.locale, this.hass.config),
calcDate(today, endOfYear, this.hass.locale, this.hass.config),
],
};
}
protected render() {
if (!this.hass || !this._startDate) {
return nothing;
}
const viewButtons: ToggleButton[] = [
{
label: this.hass.localize(
"ui.panel.lovelace.components.energy_period_selector.day"
),
value: "day",
},
{
label: this.hass.localize(
"ui.panel.lovelace.components.energy_period_selector.week"
),
value: "week",
},
{
label: this.hass.localize(
"ui.panel.lovelace.components.energy_period_selector.month"
),
value: "month",
},
{
label: this.hass.localize(
"ui.panel.lovelace.components.energy_period_selector.year"
),
value: "year",
},
];
const simpleRange = this._simpleRange();
return html`
<div class="row">
<div class="label">
${this._period === "day"
? formatDate(this._startDate, this.hass.locale, this.hass.config)
: this._period === "month"
${simpleRange === "day"
? this.narrow
? formatDateShort(
this._startDate,
this.hass.locale,
this.hass.config
)
: formatDate(this._startDate, this.hass.locale, this.hass.config)
: simpleRange === "month"
? formatDateMonthYear(
this._startDate,
this.hass.locale,
this.hass.config
)
: this._period === "year"
: simpleRange === "year"
? formatDateYear(
this._startDate,
this.hass.locale,
this.hass.config
)
: `${formatDateShort(
: `${formatDateVeryShort(
this._startDate,
this.hass.locale,
this.hass.config
)} ${formatDateShort(
)} ${formatDateVeryShort(
this._endDate || new Date(),
this.hass.locale,
this.hass.config
)}`}
</div>
<div class="time-handle">
<ha-icon-button-prev
.label=${this.hass.localize(
"ui.panel.lovelace.components.energy_period_selector.previous"
@ -140,160 +227,375 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
)}
@click=${this._pickNext}
></ha-icon-button-next>
<mwc-button dense outlined @click=${this._pickToday}>
<ha-date-range-picker
.hass=${this.hass}
.startDate=${this._startDate}
.endDate=${this._endDate || new Date()}
.ranges=${this._ranges}
@change=${this._dateRangeChanged}
.timePicker=${false}
minimal
></ha-date-range-picker>
</div>
${!this.narrow
? html`<mwc-button dense outlined @click=${this._pickToday}>
${this.hass.localize(
"ui.panel.lovelace.components.energy_period_selector.today"
)}
</mwc-button>`
: nothing}
<ha-button-menu>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-check-list-item
left
@request-selected=${this._toggleCompare}
.selected=${this._compare}
>
${this.hass.localize(
"ui.panel.lovelace.components.energy_period_selector.today"
"ui.panel.lovelace.components.energy_period_selector.compare"
)}
</mwc-button>
</div>
<div class="period">
<ha-button-toggle-group
.buttons=${viewButtons}
.active=${this._period}
dense
@value-changed=${this._handleView}
.dir=${computeRTLDirection(this.hass)}
></ha-button-toggle-group>
${this.narrow
? html`<ha-icon-button
class="compare ${this._compare ? "active" : ""}"
.path=${this._compare ? mdiCompareRemove : mdiCompare}
@click=${this._toggleCompare}
dense
outlined
>
${this.hass.localize(
"ui.panel.lovelace.components.energy_period_selector.compare"
)}
</ha-icon-button>`
: html`<mwc-button
class="compare ${this._compare ? "active" : ""}"
@click=${this._toggleCompare}
dense
outlined
>
${this.hass.localize(
"ui.panel.lovelace.components.energy_period_selector.compare"
)}
</mwc-button>`}
</div>
</ha-check-list-item>
</ha-button-menu>
</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;
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
this._setDate(
this._period === "day"
? calcDate(start, startOfDay, this.hass.locale, this.hass.config)
: this._period === "week"
? calcDate(start, startOfWeek, this.hass.locale, this.hass.config, {
weekStartsOn,
})
: this._period === "month"
? calcDate(start, startOfMonth, this.hass.locale, this.hass.config)
: calcDate(start, startOfYear, this.hass.locale, this.hass.config)
);
private _simpleRange(): string {
if (differenceInDays(this._endDate!, this._startDate!) === 0) {
return "day";
}
if (
(calcDateProperty(
this._startDate!,
isFirstDayOfMonth,
this.hass.locale,
this.hass.config
) as boolean) &&
(calcDateProperty(
this._endDate!,
isLastDayOfMonth,
this.hass.locale,
this.hass.config
) as boolean)
) {
if (
(calcDateProperty(
this._endDate!,
differenceInMonths,
this.hass.locale,
this.hass.config,
this._startDate!
) as number) === 0
) {
return "month";
}
if (
(calcDateProperty(
this._endDate!,
differenceInMonths,
this.hass.locale,
this.hass.config,
this._startDate!
) as number) === 2 &&
this._startDate!.getMonth() % 3 === 0
) {
return "quarter";
}
}
if (
calcDateProperty(
this._startDate!,
isFirstDayOfMonth,
this.hass.locale,
this.hass.config
) &&
calcDateProperty(
this._endDate!,
isLastDayOfMonth,
this.hass.locale,
this.hass.config
) &&
calcDateProperty(
this._endDate!,
differenceInMonths,
this.hass.locale,
this.hass.config,
this._startDate!
) === 11
) {
return "year";
}
return "other";
}
private _pickToday() {
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
this._setDate(
this._period === "day"
? calcDate(new Date(), startOfDay, this.hass.locale, this.hass.config)
: this._period === "week"
? calcDate(
new Date(),
startOfWeek,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
)
: this._period === "month"
? calcDate(new Date(), startOfMonth, this.hass.locale, this.hass.config)
: calcDate(new Date(), startOfYear, this.hass.locale, this.hass.config)
);
}
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 _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 weekStartsOn = firstWeekdayIndex(this.hass.locale);
const endDate =
this._period === "day"
? calcDate(startDate, endOfDay, this.hass.locale, this.hass.config)
: this._period === "week"
? calcDate(startDate, endOfWeek, this.hass.locale, this.hass.config, {
weekStartsOn,
})
: this._period === "month"
? calcDate(startDate, endOfMonth, this.hass.locale, this.hass.config)
: calcDate(startDate, endOfYear, this.hass.locale, this.hass.config);
private _updateCollectionPeriod() {
const energyCollection = getEnergyDataCollection(this.hass, {
key: this.collectionKey,
});
energyCollection.setPeriod(startDate, endDate);
energyCollection.setPeriod(this._startDate!, this._endDate!);
energyCollection.refresh();
}
private _dateRangeChanged(ev) {
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
this._startDate = calcDate(
ev.detail.startDate,
startOfDay,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
);
this._endDate = calcDate(
ev.detail.endDate,
endOfDay,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
);
this._updateCollectionPeriod();
}
private _pickToday() {
if (!this._startDate) return;
const range = this._simpleRange();
const today = new Date();
if (range === "month") {
this._startDate = calcDate(
today,
startOfMonth,
this.hass.locale,
this.hass.config
);
this._endDate = calcDate(
today,
endOfMonth,
this.hass.locale,
this.hass.config
);
} else if (range === "quarter") {
this._startDate = calcDate(
today,
startOfQuarter,
this.hass.locale,
this.hass.config
);
this._endDate = calcDate(
today,
endOfQuarter,
this.hass.locale,
this.hass.config
);
} else if (range === "year") {
this._startDate = calcDate(
today,
startOfYear,
this.hass.locale,
this.hass.config
);
this._endDate = calcDate(
today,
endOfYear,
this.hass.locale,
this.hass.config
);
} else {
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
const weekStart = calcDate(
this._endDate!,
startOfWeek,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
);
const weekEnd = calcDate(
this._endDate!,
endOfWeek,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
);
// Check if a single week is selected
if (
this._startDate.getTime() === weekStart.getTime() &&
this._endDate!.getTime() === weekEnd.getTime()
) {
// Pick current week
this._startDate = calcDate(
today,
startOfWeek,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
);
this._endDate = calcDate(
today,
endOfWeek,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
);
} else {
// Custom date range
const difference = calcDateProperty(
this._endDate!,
differenceInDays,
this.hass.locale,
this.hass.config,
this._startDate
) as number;
this._startDate = calcDate(
calcDate(
today,
subDays,
this.hass.locale,
this.hass.config,
difference
),
startOfDay,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
);
this._endDate = calcDate(
today,
endOfDay,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
);
}
}
this._updateCollectionPeriod();
}
private _pickPrevious() {
this._shift(false);
}
private _pickNext() {
this._shift(true);
}
private _shift(forward: boolean) {
if (!this._startDate) return;
let start: Date;
let end: Date;
if (
(calcDateProperty(
this._startDate,
isFirstDayOfMonth,
this.hass.locale,
this.hass.config
) as boolean) &&
(calcDateProperty(
this._endDate!,
isLastDayOfMonth,
this.hass.locale,
this.hass.config
) as boolean)
) {
// Shift date range with respect to month/year selection
const difference =
((calcDateProperty(
this._endDate!,
differenceInMonths,
this.hass.locale,
this.hass.config,
this._startDate
) as number) +
1) *
(forward ? 1 : -1);
start = calcDate(
this._startDate,
addMonths,
this.hass.locale,
this.hass.config,
difference
);
end = calcDate(
calcDate(
this._endDate!,
addMonths,
this.hass.locale,
this.hass.config,
difference
),
endOfMonth,
this.hass.locale,
this.hass.config
);
} else {
// Shift date range by period length
const difference =
((calcDateProperty(
this._endDate!,
differenceInDays,
this.hass.locale,
this.hass.config,
this._startDate
) as number) +
1) *
(forward ? 1 : -1);
start = calcDate(
this._startDate,
addDays,
this.hass.locale,
this.hass.config,
difference
);
end = calcDate(
this._endDate!,
addDays,
this.hass.locale,
this.hass.config,
difference
);
}
this._startDate = start;
this._endDate = end;
this._updateCollectionPeriod();
}
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);
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;
}
private _toggleCompare() {
this._compare = !this._compare;
private _toggleCompare(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "interaction") {
return;
}
this._compare = ev.detail.selected;
const energyCollection = getEnergyDataCollection(this.hass, {
key: this.collectionKey,
});
@ -305,74 +607,35 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
return css`
.row {
display: flex;
justify-content: flex-end;
align-items: center;
}
:host([narrow]) .row {
flex-direction: column-reverse;
:host .time-handle {
display: flex;
justify-content: flex-end;
align-items: center;
}
:host([narrow]) .time-handle {
margin-left: auto;
}
.label {
display: flex;
justify-content: flex-end;
align-items: center;
justify-content: flex-end;
font-size: 20px;
margin-left: auto;
}
.period {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
}
:host([narrow]) .period {
margin-bottom: 8px;
:host([narrow]) .label {
margin-left: unset;
}
mwc-button {
margin-left: 8px;
}
ha-icon-button {
margin-left: 4px;
--mdc-icon-size: 20px;
}
ha-icon-button.active::before,
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);
}
ha-icon-button.active::before {
border-radius: 50%;
}
.compare {
position: relative;
}
:host {
flex-shrink: 0;
--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;
}
ha-icon-button {
--mdc-icon-button-size: 28px;
}
ha-button-toggle-group {
padding-left: 8px;
padding-inline-start: 8px;
direction: var(--direction);
}
mwc-button {
flex-shrink: 0;
}
`;
}

View File

@ -528,11 +528,15 @@
"start_date": "Start date",
"end_date": "End date",
"select": "Select",
"select_date_range": "Select time period",
"ranges": {
"today": "Today",
"yesterday": "Yesterday",
"this_week": "This week",
"last_week": "Last week"
"last_week": "Last week",
"this_quarter": "This quarter",
"this_month": "This month",
"this_year": "This year"
}
},
"relative_time": {
@ -5232,10 +5236,6 @@
},
"energy_period_selector": {
"today": "Today",
"day": "Day",
"week": "Week",
"month": "Month",
"year": "Year",
"previous": "Previous",
"next": "Next",
"compare": "Compare data"