From 7a13242077d0722fc9f415677b8326c8d12598d0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 20 Jun 2020 15:39:52 +0200 Subject: [PATCH] Logbook + History allow date/time filter (#6192) Co-authored-by: Paulus Schoutsen --- package.json | 3 + src/components/date-range-picker.ts | 228 ++++++++++++++ src/components/ha-date-range-picker.ts | 195 ++++++++++++ src/data/logbook.ts | 59 ++++ src/panels/history/ha-panel-history.js | 215 ------------- src/panels/history/ha-panel-history.ts | 204 +++++++++++++ src/panels/logbook/ha-logbook-data.js | 120 -------- src/panels/logbook/ha-panel-logbook.js | 283 ------------------ src/panels/logbook/ha-panel-logbook.ts | 279 +++++++++++++++++ .../lovelace/cards/hui-history-graph-card.ts | 1 - src/translations/en.json | 23 +- yarn.lock | 17 ++ 12 files changed, 1003 insertions(+), 624 deletions(-) create mode 100644 src/components/date-range-picker.ts create mode 100644 src/components/ha-date-range-picker.ts delete mode 100644 src/panels/history/ha-panel-history.js create mode 100644 src/panels/history/ha-panel-history.ts delete mode 100644 src/panels/logbook/ha-logbook-data.js delete mode 100644 src/panels/logbook/ha-panel-logbook.js create mode 100644 src/panels/logbook/ha-panel-logbook.ts diff --git a/package.json b/package.json index 78d0f054ff..24a13c5954 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@thomasloven/round-slider": "0.5.0", "@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-date-picker": "^4.0.7", + "@vue/web-component-wrapper": "^1.2.0", "@webcomponents/webcomponentsjs": "^2.2.7", "chart.js": "~2.8.0", "chartjs-chart-timeline": "^0.3.0", @@ -106,6 +107,8 @@ "roboto-fontface": "^0.10.0", "superstruct": "^0.6.1", "unfetch": "^4.1.0", + "vue": "^2.6.11", + "vue2-daterange-picker": "^0.5.1", "web-animations-js": "^2.3.2", "workbox-core": "^5.1.3", "workbox-precaching": "^5.1.3", diff --git a/src/components/date-range-picker.ts b/src/components/date-range-picker.ts new file mode 100644 index 0000000000..48dd936f91 --- /dev/null +++ b/src/components/date-range-picker.ts @@ -0,0 +1,228 @@ +import Vue from "vue"; +import wrap from "@vue/web-component-wrapper"; +import DateRangePicker from "vue2-daterange-picker"; +// @ts-ignore +import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css"; +import { fireEvent } from "../common/dom/fire_event"; +import { Constructor } from "../types"; +import { customElement } from "lit-element/lib/decorators"; + +const Component = Vue.extend({ + props: { + twentyfourHours: { + type: Boolean, + default: true, + }, + disabled: { + type: Boolean, + default: false, + }, + ranges: { + type: Boolean, + default: true, + }, + startDate: { + type: [String, Date], + default() { + return new Date(); + }, + }, + endDate: { + type: [String, Date], + default() { + return new Date(); + }, + }, + }, + render(createElement) { + // @ts-ignore + return createElement(DateRangePicker, { + props: { + "time-picker": true, + "auto-apply": false, + opens: "right", + "show-dropdowns": false, + "time-picker24-hour": this.twentyfourHours, + disabled: this.disabled, + ranges: this.ranges ? {} : false, + }, + model: { + value: { + startDate: this.startDate, + endDate: this.endDate, + }, + callback: (value) => { + // @ts-ignore + fireEvent(this.$el as HTMLElement, "change", value); + }, + expression: "dateRange", + }, + scopedSlots: { + input() { + return createElement("slot", { + domProps: { name: "input" }, + }); + }, + header() { + return createElement("slot", { + domProps: { name: "header" }, + }); + }, + ranges() { + return createElement("slot", { + domProps: { name: "ranges" }, + }); + }, + footer() { + return createElement("slot", { + domProps: { name: "footer" }, + }); + }, + }, + }); + }, +}); + +const WrappedElement: Constructor = wrap(Vue, Component); + +@customElement("date-range-picker") +class DateRangePickerElement extends WrappedElement { + constructor() { + super(); + const style = document.createElement("style"); + style.innerHTML = ` + ${dateRangePickerStyles} + .calendars { + display: flex; + } + .daterangepicker { + left: 0px !important; + top: auto; + background-color: var(--card-background-color); + border: none; + border-radius: var(--ha-card-border-radius, 4px); + box-shadow: var( + --ha-card-box-shadow, + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), + 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12) + ); + color: var(--primary-text-color); + min-width: initial !important; + } + .daterangepicker:after { + border-bottom: 6px solid var(--card-background-color); + } + .daterangepicker .calendar-table { + background-color: var(--card-background-color); + border: none; + } + .daterangepicker .calendar-table td, + .daterangepicker .calendar-table th { + background-color: transparent; + color: var(--secondary-text-color); + border-radius: 0; + outline: none; + width: 32px; + height: 32px; + } + .daterangepicker td.off, + .daterangepicker td.off.end-date, + .daterangepicker td.off.in-range, + .daterangepicker td.off.start-date { + background-color: var(--secondary-background-color); + color: var(--disabled-text-color); + } + .daterangepicker td.in-range { + background-color: var(--light-primary-color); + color: var(--primary-text-color); + } + .daterangepicker td.active, + .daterangepicker td.active:hover { + background-color: var(--primary-color); + color: var(--text-primary-color); + } + .daterangepicker td.start-date.end-date { + border-radius: 50%; + } + .daterangepicker td.start-date { + border-radius: 50% 0 0 50%; + } + .daterangepicker td.end-date { + border-radius: 0 50% 50% 0; + } + .reportrange-text { + background: none !important; + padding: 0 !important; + border: none !important; + } + .daterangepicker .calendar-table .next span, + .daterangepicker .calendar-table .prev span { + border: solid var(--primary-text-color); + border-width: 0 2px 2px 0; + } + .daterangepicker .ranges li { + outline: none; + } + .daterangepicker .ranges li:hover { + background-color: var(--secondary-background-color); + } + .daterangepicker .ranges li.active { + background-color: var(--primary-color); + color: var(--text-primary-color); + } + .daterangepicker select.ampmselect, + .daterangepicker select.hourselect, + .daterangepicker select.minuteselect, + .daterangepicker select.secondselect { + background: transparent; + border: 1px solid var(--divider-color); + color: var(--primary-color); + } + .daterangepicker .drp-buttons .btn { + border: 1px solid var(--primary-color); + background-color: transparent; + color: var(--primary-color); + border-radius: 4px; + padding: 8px; + cursor: pointer; + } + .calendars-container { + flex-direction: column; + align-items: center; + } + .drp-calendar.col.right .calendar-table { + display: none; + } + .daterangepicker.show-ranges .drp-calendar.left { + border-left: 0px; + } + .daterangepicker .drp-calendar.left { + padding: 8px; + } + .daterangepicker.show-calendar .ranges { + margin-top: 0; + padding-top: 8px; + border-right: 1px solid var(--divider-color); + } + @media only screen and (max-width: 800px) { + .calendars { + flex-direction: column; + } + } + .calendar-table { + padding: 0 !important; + } + `; + const shadowRoot = this.shadowRoot!; + shadowRoot.appendChild(style); + // Stop click events from reaching the document, otherwise it will close the picker immediately. + shadowRoot.addEventListener("click", (ev) => ev.stopPropagation()); + } +} + +declare global { + interface HTMLElementTagNameMap { + "date-range-picker": DateRangePickerElement; + } +} diff --git a/src/components/ha-date-range-picker.ts b/src/components/ha-date-range-picker.ts new file mode 100644 index 0000000000..66bb2f1eae --- /dev/null +++ b/src/components/ha-date-range-picker.ts @@ -0,0 +1,195 @@ +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, + PropertyValues, +} from "lit-element"; +import { HomeAssistant } from "../types"; +import { mdiCalendar } from "@mdi/js"; +import { formatDateTime } from "../common/datetime/format_date_time"; +import "@material/mwc-button/mwc-button"; +import "@material/mwc-list/mwc-list-item"; +import "./ha-svg-icon"; +import "@polymer/paper-input/paper-input"; +import "@material/mwc-list/mwc-list"; +import "./date-range-picker"; + +export interface DateRangePickerRanges { + [key: string]: [Date, Date]; +} + +@customElement("ha-date-range-picker") +export class HaDateRangePicker extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public startDate!: Date; + + @property() public endDate!: Date; + + @property() public ranges?: DateRangePickerRanges; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) private _hour24format = false; + + protected updated(changedProps: PropertyValues) { + if (changedProps.has("hass")) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if (!oldHass || oldHass.language !== this.hass.language) { + this._hour24format = this._compute24hourFormat(); + } + } + } + + protected render(): TemplateResult { + return html` + +
+ + + +
+ ${this.ranges + ? html`
+ + ${Object.entries(this.ranges).map( + ([name, dates]) => html` + ${name} + ` + )} + +
` + : ""} + +
+ `; + } + + private _compute24hourFormat() { + return ( + new Intl.DateTimeFormat(this.hass.language, { + hour: "numeric", + }) + .formatToParts(new Date(2020, 0, 1, 13)) + .find((part) => part.type === "hour")!.value.length === 2 + ); + } + + private _setDateRange(ev: Event) { + const target = ev.target as any; + const startDate = target.startDate; + const endDate = target.endDate; + const dateRangePicker = this._dateRangePicker; + dateRangePicker.clickRange([startDate, endDate]); + dateRangePicker.clickedApply(); + } + + private _cancelDateRange() { + this._dateRangePicker.clickCancel(); + } + + private _applyDateRange() { + this._dateRangePicker.clickedApply(); + } + + private get _dateRangePicker() { + const dateRangePicker = this.shadowRoot!.querySelector( + "date-range-picker" + ) as any; + return dateRangePicker.vueComponent.$children[0]; + } + + private _handleInputClick() { + // close the date picker, so it will open again on the click event + if (this._dateRangePicker.open) { + this._dateRangePicker.open = false; + } + } + + static get styles(): CSSResult { + return css` + ha-svg-icon { + margin-right: 8px; + } + + .date-range-inputs { + display: flex; + align-items: center; + } + + .date-range-ranges { + border-right: 1px solid var(--divider-color); + } + + @media only screen and (max-width: 800px) { + .date-range-ranges { + border-right: none; + border-bottom: 1px solid var(--divider-color); + } + } + + .date-range-footer { + display: flex; + justify-content: flex-end; + padding: 8px; + border-top: 1px solid var(--divider-color); + } + + paper-input { + display: inline-block; + max-width: 200px; + } + + paper-input:last-child { + margin-left: 8px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-date-range-picker": HaDateRangePicker; + } +} diff --git a/src/data/logbook.ts b/src/data/logbook.ts index cdb5a8197b..fd79c87301 100644 --- a/src/data/logbook.ts +++ b/src/data/logbook.ts @@ -1,3 +1,5 @@ +import { HomeAssistant } from "../types"; + export interface LogbookEntry { when: string; name: string; @@ -6,3 +8,60 @@ export interface LogbookEntry { domain: string; context_user_id?: string; } + +const DATA_CACHE: { + [cacheKey: string]: { [entityId: string]: Promise }; +} = {}; + +export const getLogbookData = ( + hass: HomeAssistant, + startDate: string, + endDate: string, + entityId?: string +) => { + const ALL_ENTITIES = "*"; + + if (!entityId) { + entityId = ALL_ENTITIES; + } + + const cacheKey = `${startDate}${endDate}`; + + if (!DATA_CACHE[cacheKey]) { + DATA_CACHE[cacheKey] = {}; + } + + if (DATA_CACHE[cacheKey][entityId]) { + return DATA_CACHE[cacheKey][entityId]; + } + + if (entityId !== ALL_ENTITIES && DATA_CACHE[cacheKey][ALL_ENTITIES]) { + return DATA_CACHE[cacheKey][ALL_ENTITIES].then((entities) => + entities.filter((entity) => entity.entity_id === entityId) + ); + } + + DATA_CACHE[cacheKey][entityId] = getLogbookDataFromServer( + hass, + startDate, + endDate, + entityId !== ALL_ENTITIES ? entityId : undefined + ).then((entries) => entries.reverse()); + return DATA_CACHE[cacheKey][entityId]; +}; + +const getLogbookDataFromServer = async ( + hass: HomeAssistant, + startDate: string, + endDate: string, + entityId?: string +) => { + const url = `logbook/${startDate}?end_time=${endDate}${ + entityId ? `&entity=${entityId}` : "" + }`; + return hass.callApi("GET", url); +}; + +export const clearLogbookCache = (startDate, endDate) => { + DATA_CACHE[`${startDate}${endDate}`] = {}; +}; diff --git a/src/panels/history/ha-panel-history.js b/src/panels/history/ha-panel-history.js deleted file mode 100644 index 93e2a8b2ba..0000000000 --- a/src/panels/history/ha-panel-history.js +++ /dev/null @@ -1,215 +0,0 @@ -import "@polymer/app-layout/app-header-layout/app-header-layout"; -import "@polymer/app-layout/app-header/app-header"; -import "@polymer/app-layout/app-toolbar/app-toolbar"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker"; -import { formatDate } from "../../common/datetime/format_date"; -import { computeRTL } from "../../common/util/compute_rtl"; -import "../../components/ha-menu-button"; -import "../../components/state-history-charts"; -import "../../data/ha-state-history-data"; -import LocalizeMixin from "../../mixins/localize-mixin"; -import "../../resources/ha-date-picker-style"; -import "../../styles/polymer-ha-style"; - -/* - * @appliesMixin LocalizeMixin - */ -class HaPanelHistory extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - - - - - - -
[[localize('panel.history')]]
-
-
- -
-
- - - - - [[localize('ui.duration.day', 'count', 1)]] - [[localize('ui.duration.day', 'count', 3)]] - [[localize('ui.duration.week', 'count', 1)]] - - -
- - -
-
- `; - } - - static get properties() { - return { - hass: Object, - narrow: Boolean, - - stateHistory: { - type: Object, - value: null, - }, - - _periodIndex: { - type: Number, - value: 0, - }, - - isLoadingData: { - type: Boolean, - value: false, - }, - - endTime: { - type: Object, - computed: "_computeEndTime(_currentDate, _periodIndex)", - }, - - // ISO8601 formatted date string - _currentDate: { - type: String, - value: function () { - var value = new Date(); - var today = new Date( - Date.UTC(value.getFullYear(), value.getMonth(), value.getDate()) - ); - return today.toISOString().split("T")[0]; - }, - }, - - _filterType: { - type: String, - value: "date", - }, - - rtl: { - type: Boolean, - reflectToAttribute: true, - computed: "_computeRTL(hass)", - }, - }; - } - - datepickerFocus() { - this.datePicker.adjustPosition(); - } - - connectedCallback() { - super.connectedCallback(); - // We are unable to parse date because we use intl api to render date - this.$.picker.set("i18n.parseDate", null); - this.$.picker.set("i18n.formatDate", (date) => - formatDate(new Date(date.year, date.month, date.day), this.hass.language) - ); - } - - _computeStartTime(_currentDate) { - if (!_currentDate) return undefined; - var parts = _currentDate.split("-"); - parts[1] = parseInt(parts[1]) - 1; - return new Date(parts[0], parts[1], parts[2]); - } - - _computeEndTime(_currentDate, periodIndex) { - var startTime = this._computeStartTime(_currentDate); - var endTime = new Date(startTime); - endTime.setDate(startTime.getDate() + this._computeFilterDays(periodIndex)); - return endTime; - } - - _computeFilterDays(periodIndex) { - switch (periodIndex) { - case 1: - return 3; - case 2: - return 7; - default: - return 1; - } - } - - _computeRTL(hass) { - return computeRTL(hass); - } -} - -customElements.define("ha-panel-history", HaPanelHistory); diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts new file mode 100644 index 0000000000..e177933923 --- /dev/null +++ b/src/panels/history/ha-panel-history.ts @@ -0,0 +1,204 @@ +import "@polymer/app-layout/app-header-layout/app-header-layout"; +import "@polymer/app-layout/app-header/app-header"; +import "@polymer/app-layout/app-toolbar/app-toolbar"; +import { computeRTL } from "../../common/util/compute_rtl"; +import "../../components/ha-menu-button"; +import "../../components/state-history-charts"; +import { LitElement, css, property, PropertyValues } from "lit-element"; +import { html } from "lit-html"; +import { haStyle } from "../../resources/styles"; +import { HomeAssistant } from "../../types"; +import type { DateRangePickerRanges } from "../../components/ha-date-range-picker"; +import "../../components/ha-date-range-picker"; +import { fetchDate, computeHistory } from "../../data/history"; +import "@polymer/paper-spinner/paper-spinner"; + +class HaPanelHistory extends LitElement { + @property() hass!: HomeAssistant; + + @property({ reflect: true, type: Boolean }) narrow!: boolean; + + @property() _startDate: Date; + + @property() _endDate: Date; + + @property() _entityId = ""; + + @property() _isLoading = false; + + @property() _stateHistory?; + + @property({ reflect: true, type: Boolean }) rtl = false; + + @property() private _ranges?: DateRangePickerRanges; + + public constructor() { + super(); + + const start = new Date(); + start.setHours(start.getHours() - 2); + start.setMinutes(0); + start.setSeconds(0); + this._startDate = start; + + const end = new Date(); + end.setHours(end.getHours() + 1); + end.setMinutes(0); + end.setSeconds(0); + this._endDate = end; + } + + protected render() { + return html` + + + + +
${this.hass.localize("panel.history")}
+
+
+ +
+
+ +
+ ${this._isLoading + ? html`` + : html` + + + `} +
+
+ `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + const todayEnd = new Date(today); + todayEnd.setDate(todayEnd.getDate() + 1); + todayEnd.setMilliseconds(todayEnd.getMilliseconds() - 1); + + const todayCopy = new Date(today); + + const yesterday = new Date(todayCopy.setDate(today.getDate() - 1)); + const yesterdayEnd = new Date(yesterday); + yesterdayEnd.setDate(yesterdayEnd.getDate() + 1); + yesterdayEnd.setMilliseconds(yesterdayEnd.getMilliseconds() - 1); + + const thisWeekStart = new Date( + todayCopy.setDate(today.getDate() - today.getDay()) + ); + const thisWeekEnd = new Date( + todayCopy.setDate(today.getDate() - today.getDay() + 7) + ); + thisWeekEnd.setMilliseconds(thisWeekEnd.getMilliseconds() - 1); + + const lastWeekStart = new Date( + todayCopy.setDate(today.getDate() - today.getDay() - 7) + ); + const lastWeekEnd = new Date( + todayCopy.setDate(today.getDate() - today.getDay()) + ); + lastWeekEnd.setMilliseconds(lastWeekEnd.getMilliseconds() - 1); + + this._ranges = { + [this.hass.localize("ui.panel.history.ranges.today")]: [today, todayEnd], + [this.hass.localize("ui.panel.history.ranges.yesterday")]: [ + yesterday, + yesterdayEnd, + ], + [this.hass.localize("ui.panel.history.ranges.this_week")]: [ + thisWeekStart, + thisWeekEnd, + ], + [this.hass.localize("ui.panel.history.ranges.last_week")]: [ + lastWeekStart, + lastWeekEnd, + ], + }; + } + + protected updated(changedProps: PropertyValues) { + if ( + changedProps.has("_startDate") || + changedProps.has("_endDate") || + changedProps.has("_entityId") + ) { + this._getHistory(); + } + + if (changedProps.has("hass")) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if (!oldHass || oldHass.language !== this.hass.language) { + this.rtl = computeRTL(this.hass); + } + } + } + + private async _getHistory() { + this._isLoading = true; + const dateHistory = await fetchDate( + this.hass, + this._startDate, + this._endDate + ); + this._stateHistory = computeHistory( + this.hass, + dateHistory, + this.hass.localize, + this.hass.language + ); + this._isLoading = false; + } + + private _dateRangeChanged(ev) { + this._startDate = ev.detail.startDate; + const endDate = ev.detail.endDate; + if (endDate.getHours() === 0 && endDate.getMinutes() === 0) { + endDate.setDate(endDate.getDate() + 1); + endDate.setMilliseconds(endDate.getMilliseconds() - 1); + } + this._endDate = endDate; + } + + static get styles() { + return [ + haStyle, + css` + .content { + padding: 0 16px 16px; + } + paper-spinner { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } + `, + ]; + } +} + +customElements.define("ha-panel-history", HaPanelHistory); diff --git a/src/panels/logbook/ha-logbook-data.js b/src/panels/logbook/ha-logbook-data.js deleted file mode 100644 index 461fea1f60..0000000000 --- a/src/panels/logbook/ha-logbook-data.js +++ /dev/null @@ -1,120 +0,0 @@ -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -const DATA_CACHE = {}; -const ALL_ENTITIES = "*"; - -class HaLogbookData extends PolymerElement { - static get properties() { - return { - hass: { - type: Object, - observer: "hassChanged", - }, - - filterDate: { - type: String, - observer: "filterDataChanged", - }, - - filterPeriod: { - type: Number, - observer: "filterDataChanged", - }, - - filterEntity: { - type: String, - observer: "filterDataChanged", - }, - - isLoading: { - type: Boolean, - value: true, - readOnly: true, - notify: true, - }, - - entries: { - type: Object, - value: null, - readOnly: true, - notify: true, - }, - }; - } - - hassChanged(newHass, oldHass) { - if (!oldHass && this.filterDate) { - this.updateData(); - } - } - - filterDataChanged(newValue, oldValue) { - if (oldValue !== undefined) { - this.updateData(); - } - } - - updateData() { - if (!this.hass) return; - - this._setIsLoading(true); - - this.getData(this.filterDate, this.filterPeriod, this.filterEntity).then( - (logbookEntries) => { - this._setEntries(logbookEntries); - this._setIsLoading(false); - } - ); - } - - getData(date, period, entityId) { - if (!entityId) entityId = ALL_ENTITIES; - - if (!DATA_CACHE[period]) DATA_CACHE[period] = []; - if (!DATA_CACHE[period][date]) DATA_CACHE[period][date] = []; - - if (DATA_CACHE[period][date][entityId]) { - return DATA_CACHE[period][date][entityId]; - } - - if (entityId !== ALL_ENTITIES && DATA_CACHE[period][date][ALL_ENTITIES]) { - return DATA_CACHE[period][date][ALL_ENTITIES].then(function (entities) { - return entities.filter(function (entity) { - return entity.entity_id === entityId; - }); - }); - } - - DATA_CACHE[period][date][entityId] = this._getFromServer( - date, - period, - entityId - ); - return DATA_CACHE[period][date][entityId]; - } - - _getFromServer(date, period, entityId) { - let url = "logbook/" + date + "?period=" + period; - if (entityId !== ALL_ENTITIES) { - url += "&entity=" + entityId; - } - - return this.hass.callApi("GET", url).then( - function (logbookEntries) { - logbookEntries.reverse(); - return logbookEntries; - }, - function () { - return null; - } - ); - } - - refreshLogbook() { - DATA_CACHE[this.filterPeriod][this.filterDate] = []; - this.updateData(); - } -} - -customElements.define("ha-logbook-data", HaLogbookData); diff --git a/src/panels/logbook/ha-panel-logbook.js b/src/panels/logbook/ha-panel-logbook.js deleted file mode 100644 index 293092dc0a..0000000000 --- a/src/panels/logbook/ha-panel-logbook.js +++ /dev/null @@ -1,283 +0,0 @@ -import "@polymer/app-layout/app-header-layout/app-header-layout"; -import "@polymer/app-layout/app-header/app-header"; -import "@polymer/app-layout/app-toolbar/app-toolbar"; -import "../../components/ha-icon-button"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-spinner/paper-spinner"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker"; -import { formatDate } from "../../common/datetime/format_date"; -import { computeRTL } from "../../common/util/compute_rtl"; -import "../../components/entity/ha-entity-picker"; -import "../../components/ha-menu-button"; -import LocalizeMixin from "../../mixins/localize-mixin"; -import "../../resources/ha-date-picker-style"; -import "../../styles/polymer-ha-style"; -import "./ha-logbook"; -import "./ha-logbook-data"; - -/* - * @appliesMixin LocalizeMixin - */ -class HaPanelLogbook extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - - - - - - - -
[[localize('panel.logbook')]]
- -
-
- - - -
- - - - - [[localize('ui.duration.day', 'count', 1)]] - [[localize('ui.duration.day', 'count', 3)]] - [[localize('ui.duration.week', 'count', 1)]] - - - - -
- - -
- `; - } - - static get properties() { - return { - hass: Object, - - narrow: { type: Boolean, reflectToAttribute: true }, - - // ISO8601 formatted date string - _currentDate: { - type: String, - value: function () { - const value = new Date(); - const today = new Date( - Date.UTC(value.getFullYear(), value.getMonth(), value.getDate()) - ); - return today.toISOString().split("T")[0]; - }, - }, - - _periodIndex: { - type: Number, - value: 0, - }, - - _entityId: { - type: String, - value: "", - }, - - entityId: { - type: String, - value: "", - readOnly: true, - }, - - isLoading: { - type: Boolean, - }, - - entries: { - type: Array, - }, - - datePicker: { - type: Object, - }, - - rtl: { - type: Boolean, - reflectToAttribute: true, - computed: "_computeRTL(hass)", - }, - }; - } - - ready() { - super.ready(); - this.hass.loadBackendTranslation("title"); - } - - connectedCallback() { - super.connectedCallback(); - // We are unable to parse date because we use intl api to render date - this.$.picker.set("i18n.parseDate", null); - this.$.picker.set("i18n.formatDate", (date) => - formatDate(new Date(date.year, date.month, date.day), this.hass.language) - ); - } - - _computeFilterDate(_currentDate) { - if (!_currentDate) return undefined; - var parts = _currentDate.split("-"); - parts[1] = parseInt(parts[1]) - 1; - return new Date(parts[0], parts[1], parts[2]).toISOString(); - } - - _computeFilterDays(periodIndex) { - switch (periodIndex) { - case 1: - return 3; - case 2: - return 7; - default: - return 1; - } - } - - _entityPicked(ev) { - this._setEntityId(ev.target.value); - } - - refreshLogbook() { - this.shadowRoot.querySelector("ha-logbook-data").refreshLogbook(); - } - - _computeRTL(hass) { - return computeRTL(hass); - } -} - -customElements.define("ha-panel-logbook", HaPanelLogbook); diff --git a/src/panels/logbook/ha-panel-logbook.ts b/src/panels/logbook/ha-panel-logbook.ts new file mode 100644 index 0000000000..ac59b78f90 --- /dev/null +++ b/src/panels/logbook/ha-panel-logbook.ts @@ -0,0 +1,279 @@ +import "@polymer/app-layout/app-header-layout/app-header-layout"; +import "@polymer/app-layout/app-header/app-header"; +import "@polymer/app-layout/app-toolbar/app-toolbar"; +import "../../components/ha-icon-button"; +import "@polymer/paper-spinner/paper-spinner"; +import { computeRTL } from "../../common/util/compute_rtl"; +import "../../components/entity/ha-entity-picker"; +import "../../components/ha-menu-button"; +import "./ha-logbook"; +import { + LitElement, + property, + customElement, + html, + css, + PropertyValues, +} from "lit-element"; +import { HomeAssistant } from "../../types"; +import { haStyle } from "../../resources/styles"; +import { + clearLogbookCache, + getLogbookData, + LogbookEntry, +} from "../../data/logbook"; +import { mdiRefresh } from "@mdi/js"; +import "../../components/ha-date-range-picker"; +import type { DateRangePickerRanges } from "../../components/ha-date-range-picker"; + +@customElement("ha-panel-logbook") +export class HaPanelLogbook extends LitElement { + @property() hass!: HomeAssistant; + + @property({ reflect: true, type: Boolean }) narrow!: boolean; + + @property() _startDate: Date; + + @property() _endDate: Date; + + @property() _entityId = ""; + + @property() _isLoading = false; + + @property() _entries: LogbookEntry[] = []; + + @property({ reflect: true, type: Boolean }) rtl = false; + + @property() private _ranges?: DateRangePickerRanges; + + public constructor() { + super(); + + const start = new Date(); + start.setHours(start.getHours() - 2); + start.setMinutes(0); + start.setSeconds(0); + this._startDate = start; + + const end = new Date(); + end.setHours(end.getHours() + 1); + end.setMinutes(0); + end.setSeconds(0); + this._endDate = end; + } + + protected render() { + return html` + + + + +
${this.hass.localize("panel.logbook")}
+ + + +
+
+ + ${this._isLoading ? html`` : ""} + +
+ + + +
+ + ${this._isLoading + ? html`` + : html``} +
+ `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this.hass.loadBackendTranslation("title"); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + const todayEnd = new Date(today); + todayEnd.setDate(todayEnd.getDate() + 1); + todayEnd.setMilliseconds(todayEnd.getMilliseconds() - 1); + + const todayCopy = new Date(today); + + const yesterday = new Date(todayCopy.setDate(today.getDate() - 1)); + const yesterdayEnd = new Date(yesterday); + yesterdayEnd.setDate(yesterdayEnd.getDate() + 1); + yesterdayEnd.setMilliseconds(yesterdayEnd.getMilliseconds() - 1); + + const thisWeekStart = new Date( + todayCopy.setDate(today.getDate() - today.getDay()) + ); + const thisWeekEnd = new Date( + todayCopy.setDate(today.getDate() - today.getDay() + 7) + ); + thisWeekEnd.setMilliseconds(thisWeekEnd.getMilliseconds() - 1); + + const lastWeekStart = new Date( + todayCopy.setDate(today.getDate() - today.getDay() - 7) + ); + const lastWeekEnd = new Date( + todayCopy.setDate(today.getDate() - today.getDay()) + ); + lastWeekEnd.setMilliseconds(lastWeekEnd.getMilliseconds() - 1); + + this._ranges = { + [this.hass.localize("ui.panel.logbook.ranges.today")]: [today, todayEnd], + [this.hass.localize("ui.panel.logbook.ranges.yesterday")]: [ + yesterday, + yesterdayEnd, + ], + [this.hass.localize("ui.panel.logbook.ranges.this_week")]: [ + thisWeekStart, + thisWeekEnd, + ], + [this.hass.localize("ui.panel.logbook.ranges.last_week")]: [ + lastWeekStart, + lastWeekEnd, + ], + }; + } + + protected updated(changedProps: PropertyValues) { + if ( + changedProps.has("_startDate") || + changedProps.has("_endDate") || + changedProps.has("_entityId") + ) { + this._getData(); + } + + if (changedProps.has("hass")) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if (!oldHass || oldHass.language !== this.hass.language) { + this.rtl = computeRTL(this.hass); + } + } + } + + private _dateRangeChanged(ev) { + this._startDate = ev.detail.startDate; + const endDate = ev.detail.endDate; + if (endDate.getHours() === 0 && endDate.getMinutes() === 0) { + endDate.setDate(endDate.getDate() + 1); + endDate.setMilliseconds(endDate.getMilliseconds() - 1); + } + this._endDate = endDate; + } + + private _entityPicked(ev) { + this._entityId = ev.target.value; + } + + private _refreshLogbook() { + this._entries = []; + clearLogbookCache( + this._startDate.toISOString(), + this._endDate.toISOString() + ); + this._getData(); + } + + private async _getData() { + this._isLoading = true; + this._entries = await getLogbookData( + this.hass, + this._startDate.toISOString(), + this._endDate.toISOString(), + this._entityId + ); + this._isLoading = false; + } + + static get styles() { + return [ + haStyle, + css` + ha-logbook { + height: calc(100vh - 136px); + } + + :host([narrow]) ha-logbook { + height: calc(100vh - 198px); + } + + ha-date-range-picker { + margin-right: 16px; + max-width: 100%; + } + + :host([narrow]) ha-date-range-picker { + margin-right: 0; + } + + paper-spinner { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } + + .wrap { + margin-bottom: 24px; + } + + .filters { + display: flex; + align-items: flex-end; + padding: 0 16px; + } + + :host([narrow]) .filters { + flex-wrap: wrap; + } + + ha-entity-picker { + display: inline-block; + flex-grow: 1; + max-width: 400px; + --paper-input-suffix: { + height: 24px; + } + } + + :host([narrow]) ha-entity-picker { + max-width: none; + width: 100%; + } + `, + ]; + } +} diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index 404df75173..1e347183c6 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -12,7 +12,6 @@ import { classMap } from "lit-html/directives/class-map"; import "../../../components/ha-card"; import "../../../components/state-history-charts"; import { CacheConfig, getRecentWithCache } from "../../../data/cached-history"; -import "../../../data/ha-state-history-data"; import { HomeAssistant } from "../../../types"; import { findEntities } from "../common/find-entites"; import { processConfigEntities } from "../common/process-config-entities"; diff --git a/src/translations/en.json b/src/translations/en.json index 395cef344d..6c19dc6969 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -278,6 +278,11 @@ "failed_create_area": "Failed to create area." } }, + "date-range-picker": { + "start_date": "Start date", + "end_date": "End date", + "select": "Select" + }, "relative_time": { "past": "{time} ago", "future": "In {time}", @@ -1709,13 +1714,21 @@ } }, "history": { - "showing_entries": "Showing entries for", - "period": "Period" + "ranges": { + "today": "Today", + "yesterday": "Yesterday", + "this_week": "This week", + "last_week": "Last week" + } }, "logbook": { - "showing_entries": "[%key:ui::panel::history::showing_entries%]", - "period": "Period", - "entries_not_found": "No logbook entries found." + "entries_not_found": "No logbook entries found.", + "ranges": { + "today": "Today", + "yesterday": "Yesterday", + "this_week": "This week", + "last_week": "Last week" + } }, "lovelace": { "cards": { diff --git a/yarn.lock b/yarn.lock index 14318d7282..c7f51c7378 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2738,6 +2738,11 @@ dependencies: "@vaadin/vaadin-development-mode-detector" "^2.0.0" +"@vue/web-component-wrapper@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@vue/web-component-wrapper/-/web-component-wrapper-1.2.0.tgz#bb0e46f1585a7e289b4ee6067dcc5a6ae62f1dd1" + integrity sha512-Xn/+vdm9CjuC9p3Ae+lTClNutrVhsXpzxvoTXXtoys6kVRX9FkueSUAqSWAyZntmVLlR4DosBV4pH8y5Z/HbUw== + "@webassemblyjs/ast@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" @@ -11829,6 +11834,18 @@ vscode-uri@^1.0.6: resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.8.tgz#9769aaececae4026fb6e22359cb38946580ded59" integrity sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ== +vue2-daterange-picker@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/vue2-daterange-picker/-/vue2-daterange-picker-0.5.1.tgz#f41f3cd20b242b7f34ce16eeea9534d9cbe9f4d7" + integrity sha512-p0y9RyI6wqqwffKM5EYgxvNM51un/fBu9hLZ/GxXVOBqTMxjDuV8mz9iUTj4p5R80lWSBwIY7GshW5RYgS8+rw== + dependencies: + vue "^2.6.10" + +vue@^2.6.10, vue@^2.6.11: + version "2.6.11" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5" + integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== + watchpack@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"