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: { strategy: {
type: "energy", type: "energy",
show_date_selection: true,
}, },
}, },
], ],

View File

@ -5,12 +5,15 @@ import { FrontendLocaleData, TimeZone } from "../../data/translation";
const calcZonedDate = ( const calcZonedDate = (
date: Date, date: Date,
tz: string, tz: string,
fn: (date: Date, options?: any) => Date, fn: (date: Date, options?: any) => Date | number | boolean,
options? options?
) => { ) => {
const inputZoned = utcToZonedTime(date, tz); const inputZoned = utcToZonedTime(date, tz);
const fnZoned = fn(inputZoned, options); const fnZoned = fn(inputZoned, options);
return zonedTimeToUtc(fnZoned, tz); if (fnZoned instanceof Date) {
return zonedTimeToUtc(fnZoned, tz) as Date;
}
return fnZoned;
}; };
export const calcDate = ( export const calcDate = (
@ -21,5 +24,16 @@ export const calcDate = (
options? options?
) => ) =>
locale.time_zone === TimeZone.server 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); : 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 // 10/08/2021
export const formatDateNumeric = ( export const formatDateNumeric = (
dateObj: Date, dateObj: Date,
@ -102,13 +119,13 @@ const formatDateNumericMem = memoizeOne(
); );
// Aug 10 // Aug 10
export const formatDateShort = ( export const formatDateVeryShort = (
dateObj: Date, dateObj: Date,
locale: FrontendLocaleData, locale: FrontendLocaleData,
config: HassConfig 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) => (locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, { new Intl.DateTimeFormat(locale.language, {
day: "numeric", day: "numeric",

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import {
LitElement, LitElement,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../components/ha-menu-button"; import "../../components/ha-menu-button";
@ -60,31 +61,33 @@ class PanelEnergy extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-top-app-bar-fixed> <div class="header">
<ha-menu-button <div class="toolbar">
slot="navigationIcon" <ha-menu-button
.hass=${this.hass} slot="navigationIcon"
.narrow=${this.narrow} .hass=${this.hass}
></ha-menu-button> .narrow=${this.narrow}
<div slot="title">${this.hass.localize("panel.energy")}</div> ></ha-menu-button>
${this.narrow ${!this.narrow
? "" ? html`<div class="main-title">
: html` ${this.hass.localize("panel.energy")}
<hui-energy-period-selector </div>`
slot="actionItems" : nothing}
.hass=${this.hass}
collectionKey="energy_dashboard" <hui-energy-period-selector
.narrow=${false} .hass=${this.hass}
></hui-energy-period-selector> collectionKey="energy_dashboard"
`} ></hui-energy-period-selector>
<hui-view </div>
.hass=${this.hass} </div>
.narrow=${this.narrow} <hui-view
.lovelace=${this._lovelace} id="view"
.index=${this._viewIndex} .hass=${this.hass}
@reload-energy-panel=${this._reloadView} .narrow=${this.narrow}
></hui-view> .lovelace=${this._lovelace}
</ha-top-app-bar-fixed> .index=${this._viewIndex}
@reload-energy-panel=${this._reloadView}
></hui-view>
`; `;
} }
@ -116,13 +119,83 @@ class PanelEnergy extends LitElement {
return [ return [
haStyle, haStyle,
css` css`
hui-energy-period-selector { :host hui-energy-period-selector {
width: 100%; width: 100%;
padding-left: 16px; padding-left: 32px;
padding-inline-start: 16px; padding-inline-start: 32px;
--disabled-text-color: rgba(var(--rgb-text-primary-color), 0.5); --disabled-text-color: rgba(var(--rgb-text-primary-color), 0.5);
direction: var(--direction); 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, LovelaceViewConfig,
} from "../../../data/lovelace"; } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { LovelaceStrategyParams } from "../../lovelace/strategies/types";
const setupWizard = async (): Promise<LovelaceViewConfig> => { const setupWizard = async (): Promise<LovelaceViewConfig> => {
await import("../cards/energy-setup-wizard-card"); 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") @customElement("energy-view-strategy")
export class EnergyViewStrategy extends ReactiveElement { export class EnergyViewStrategy extends ReactiveElement {
static async generate( static async generate(
config: EnergeryViewStrategyConfig, _config: LovelaceStrategyConfig,
hass: HomeAssistant, hass: HomeAssistant
params: LovelaceStrategyParams
): Promise<LovelaceViewConfig> { ): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = { cards: [] }; const view: LovelaceViewConfig = { cards: [] };
@ -67,14 +61,6 @@ export class EnergyViewStrategy extends ReactiveElement {
(source) => source.type === "water" (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({ view.cards!.push({
type: "energy-compare", type: "energy-compare",
collection_key: "energy_dashboard", 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 { customElement, property, state } from "lit/decorators";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import "../../components/hui-energy-period-selector"; import "../../components/hui-energy-period-selector";
@ -28,10 +28,22 @@ export class HuiEnergyDateSelectionCard
} }
return html` return html`
<hui-energy-period-selector <ha-card>
.hass=${this.hass} <div class="card-content">
.collectionKey=${this._config.collection_key} <hui-energy-period-selector
></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, rgb2lab,
} from "../../../../common/color/convert-color"; } from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab"; import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateShort } from "../../../../common/datetime/format_date"; import { formatDateVeryShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time"; import { formatTime } from "../../../../common/datetime/format_time";
import { import {
formatNumber, formatNumber,
@ -228,7 +228,9 @@ export class HuiEnergyGasGraphCard
} }
const date = new Date(datasets[0].parsed.x); const date = new Date(datasets[0].parsed.x);
return `${ return `${
compare ? `${formatDateShort(date, locale, config)}: ` : "" compare
? `${formatDateVeryShort(date, locale, config)}: `
: ""
}${formatTime(date, locale, config)} ${formatTime( }${formatTime(date, locale, config)} ${formatTime(
addHours(date, 1), addHours(date, 1),
locale, locale,

View File

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

View File

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

View File

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

View File

@ -1,43 +1,56 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiCompare, mdiCompareRemove } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import { import {
addDays, addDays,
subDays,
addMonths, addMonths,
addWeeks,
addYears,
differenceInDays, differenceInDays,
endOfDay, endOfDay,
endOfMonth, endOfMonth,
endOfToday, endOfToday,
endOfWeek, endOfWeek,
endOfQuarter,
endOfYear, endOfYear,
isWithinInterval, isFirstDayOfMonth,
isLastDayOfMonth,
differenceInMonths,
startOfDay, startOfDay,
startOfMonth, startOfMonth,
startOfToday,
startOfWeek, startOfWeek,
startOfQuarter,
startOfYear, startOfYear,
} from "date-fns/esm"; } from "date-fns/esm";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; 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 { 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 { firstWeekdayIndex } from "../../../common/datetime/first_weekday";
import { import {
formatDate, formatDate,
formatDateMonthYear,
formatDateShort, formatDateShort,
formatDateVeryShort,
formatDateMonthYear,
formatDateYear, formatDateYear,
} from "../../../common/datetime/format_date"; } 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-next";
import "../../../components/ha-icon-button-prev"; import "../../../components/ha-icon-button-prev";
import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item";
import { EnergyData, getEnergyDataCollection } from "../../../data/energy"; import { EnergyData, getEnergyDataCollection } from "../../../data/energy";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; 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") @customElement("hui-energy-period-selector")
export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
@ -51,16 +64,11 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
@state() _endDate?: Date; @state() _endDate?: Date;
@state() private _period?: "day" | "week" | "month" | "year"; @state() private _ranges?: DateRangePickerRanges;
@state() private _compare = false; @state() private _compare = false;
public connectedCallback() { private _resizeObserver?: ResizeObserver;
super.connectedCallback();
if (this.narrow !== false) {
toggleAttribute(this, "narrow", this.offsetWidth < 600);
}
}
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ 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() { protected render() {
if (!this.hass || !this._startDate) { if (!this.hass || !this._startDate) {
return nothing; return nothing;
} }
const viewButtons: ToggleButton[] = [ const simpleRange = this._simpleRange();
{
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",
},
];
return html` return html`
<div class="row"> <div class="row">
<div class="label"> <div class="label">
${this._period === "day" ${simpleRange === "day"
? formatDate(this._startDate, this.hass.locale, this.hass.config) ? this.narrow
: this._period === "month" ? formatDateShort(
this._startDate,
this.hass.locale,
this.hass.config
)
: formatDate(this._startDate, this.hass.locale, this.hass.config)
: simpleRange === "month"
? formatDateMonthYear( ? formatDateMonthYear(
this._startDate, this._startDate,
this.hass.locale, this.hass.locale,
this.hass.config this.hass.config
) )
: this._period === "year" : simpleRange === "year"
? formatDateYear( ? formatDateYear(
this._startDate, this._startDate,
this.hass.locale, this.hass.locale,
this.hass.config this.hass.config
) )
: `${formatDateShort( : `${formatDateVeryShort(
this._startDate, this._startDate,
this.hass.locale, this.hass.locale,
this.hass.config this.hass.config
)} ${formatDateShort( )} ${formatDateVeryShort(
this._endDate || new Date(), this._endDate || new Date(),
this.hass.locale, this.hass.locale,
this.hass.config this.hass.config
)}`} )}`}
</div>
<div class="time-handle">
<ha-icon-button-prev <ha-icon-button-prev
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.lovelace.components.energy_period_selector.previous" "ui.panel.lovelace.components.energy_period_selector.previous"
@ -140,160 +227,375 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
)} )}
@click=${this._pickNext} @click=${this._pickNext}
></ha-icon-button-next> ></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( ${this.hass.localize(
"ui.panel.lovelace.components.energy_period_selector.today" "ui.panel.lovelace.components.energy_period_selector.compare"
)} )}
</mwc-button> </ha-check-list-item>
</div> </ha-button-menu>
<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>
</div> </div>
`; `;
} }
private _handleView(ev: CustomEvent): void { private _simpleRange(): string {
this._period = ev.detail.value; if (differenceInDays(this._endDate!, this._startDate!) === 0) {
const today = startOfToday(); return "day";
const start = }
!this._startDate || if (
isWithinInterval(today, { (calcDateProperty(
start: this._startDate, this._startDate!,
end: this._endDate || endOfToday(), isFirstDayOfMonth,
}) this.hass.locale,
? today this.hass.config
: this._startDate; ) as boolean) &&
(calcDateProperty(
const weekStartsOn = firstWeekdayIndex(this.hass.locale); this._endDate!,
isLastDayOfMonth,
this._setDate( this.hass.locale,
this._period === "day" this.hass.config
? calcDate(start, startOfDay, this.hass.locale, this.hass.config) ) as boolean)
: this._period === "week" ) {
? calcDate(start, startOfWeek, this.hass.locale, this.hass.config, { if (
weekStartsOn, (calcDateProperty(
}) this._endDate!,
: this._period === "month" differenceInMonths,
? calcDate(start, startOfMonth, this.hass.locale, this.hass.config) this.hass.locale,
: calcDate(start, startOfYear, this.hass.locale, this.hass.config) 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() { private _updateCollectionPeriod() {
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);
const energyCollection = getEnergyDataCollection(this.hass, { const energyCollection = getEnergyDataCollection(this.hass, {
key: this.collectionKey, key: this.collectionKey,
}); });
energyCollection.setPeriod(startDate, endDate); energyCollection.setPeriod(this._startDate!, this._endDate!);
energyCollection.refresh(); 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 { private _updateDates(energyData: EnergyData): void {
this._compare = energyData.startCompare !== undefined; this._compare = energyData.startCompare !== undefined;
this._startDate = energyData.start; this._startDate = energyData.start;
this._endDate = energyData.end || endOfToday(); this._endDate = energyData.end || endOfToday();
const dayDifference = differenceInDays(this._endDate, this._startDate);
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() { private _toggleCompare(ev: CustomEvent<RequestSelectedDetail>) {
this._compare = !this._compare; if (ev.detail.source !== "interaction") {
return;
}
this._compare = ev.detail.selected;
const energyCollection = getEnergyDataCollection(this.hass, { const energyCollection = getEnergyDataCollection(this.hass, {
key: this.collectionKey, key: this.collectionKey,
}); });
@ -305,74 +607,35 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
return css` return css`
.row { .row {
display: flex; display: flex;
justify-content: flex-end; align-items: center;
} }
:host([narrow]) .row { :host .time-handle {
flex-direction: column-reverse; display: flex;
justify-content: flex-end;
align-items: center;
}
:host([narrow]) .time-handle {
margin-left: auto;
} }
.label { .label {
display: flex; display: flex;
justify-content: flex-end;
align-items: center; align-items: center;
justify-content: flex-end;
font-size: 20px; font-size: 20px;
margin-left: auto;
} }
.period { :host([narrow]) .label {
display: flex; margin-left: unset;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
}
:host([narrow]) .period {
margin-bottom: 8px;
} }
mwc-button { mwc-button {
margin-left: 8px; margin-left: 8px;
} flex-shrink: 0;
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 {
--mdc-button-outline-color: currentColor; --mdc-button-outline-color: currentColor;
--primary-color: currentColor; --primary-color: currentColor;
--mdc-theme-primary: currentColor; --mdc-theme-primary: currentColor;
--mdc-theme-on-primary: currentColor; --mdc-theme-on-primary: currentColor;
--mdc-button-disabled-outline-color: var(--disabled-text-color); --mdc-button-disabled-outline-color: var(--disabled-text-color);
--mdc-button-disabled-ink-color: var(--disabled-text-color); --mdc-button-disabled-ink-color: var(--disabled-text-color);
--mdc-icon-button-ripple-opacity: 0.2;
}
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", "start_date": "Start date",
"end_date": "End date", "end_date": "End date",
"select": "Select", "select": "Select",
"select_date_range": "Select time period",
"ranges": { "ranges": {
"today": "Today", "today": "Today",
"yesterday": "Yesterday", "yesterday": "Yesterday",
"this_week": "This week", "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": { "relative_time": {
@ -5232,10 +5236,6 @@
}, },
"energy_period_selector": { "energy_period_selector": {
"today": "Today", "today": "Today",
"day": "Day",
"week": "Week",
"month": "Month",
"year": "Year",
"previous": "Previous", "previous": "Previous",
"next": "Next", "next": "Next",
"compare": "Compare data" "compare": "Compare data"