Logbook + History allow date/time filter (#6192)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Bram Kragten 2020-06-20 15:39:52 +02:00 committed by GitHub
parent b9d6973a79
commit 7a13242077
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1003 additions and 624 deletions

View File

@ -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",

View File

@ -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<HTMLElement> = 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;
}
}

View File

@ -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`
<date-range-picker
?disabled=${this.disabled}
twentyfour-hours=${this._hour24format}
start-date=${this.startDate}
end-date=${this.endDate}
?ranges=${this.ranges !== undefined}
>
<div slot="input" class="date-range-inputs">
<ha-svg-icon path=${mdiCalendar}></ha-svg-icon>
<paper-input
.value=${formatDateTime(this.startDate, this.hass.language)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></paper-input>
<paper-input
.value=${formatDateTime(this.endDate, this.hass.language)}
label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></paper-input>
</div>
${this.ranges
? html`<div slot="ranges" class="date-range-ranges">
<mwc-list @click=${this._setDateRange}>
${Object.entries(this.ranges).map(
([name, dates]) => html`<mwc-list-item
.activated=${this.startDate.getTime() ===
dates[0].getTime() &&
this.endDate.getTime() === dates[1].getTime()}
.startDate=${dates[0]}
.endDate=${dates[1]}
>
${name}
</mwc-list-item>`
)}
</mwc-list>
</div>`
: ""}
<div slot="footer" class="date-range-footer">
<mwc-button @click=${this._cancelDateRange}
>${this.hass.localize("ui.common.cancel")}</mwc-button
>
<mwc-button @click=${this._applyDateRange}
>${this.hass.localize(
"ui.components.date-range-picker.select"
)}</mwc-button
>
</div>
</date-range-picker>
`;
}
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;
}
}

View File

@ -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<LogbookEntry[]> };
} = {};
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<LogbookEntry[]>("GET", url);
};
export const clearLogbookCache = (startDate, endDate) => {
DATA_CACHE[`${startDate}${endDate}`] = {};
};

View File

@ -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`
<style include="iron-flex ha-style">
.content {
padding: 0 16px 16px;
}
vaadin-date-picker {
margin-right: 16px;
max-width: 200px;
}
paper-dropdown-menu {
max-width: 100px;
margin-right: 16px;
margin-top: 5px;
--paper-input-container-label-floating: {
padding-bottom: 11px;
}
--paper-input-suffix: {
height: 24px;
}
}
:host([rtl]) paper-dropdown-menu {
text-align: right;
}
paper-item {
cursor: pointer;
white-space: nowrap;
}
</style>
<ha-state-history-data
hass="[[hass]]"
filter-type="[[_filterType]]"
start-time="[[_computeStartTime(_currentDate)]]"
end-time="[[endTime]]"
data="{{stateHistory}}"
is-loading="{{isLoadingData}}"
></ha-state-history-data>
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button
hass="[[hass]]"
narrow="[[narrow]]"
></ha-menu-button>
<div main-title>[[localize('panel.history')]]</div>
</app-toolbar>
</app-header>
<div class="flex content">
<div class="flex layout horizontal wrap">
<vaadin-date-picker
id="picker"
value="{{_currentDate}}"
label="[[localize('ui.panel.history.showing_entries')]]"
disabled="[[isLoadingData]]"
required
></vaadin-date-picker>
<paper-dropdown-menu
label-float
label="[[localize('ui.panel.history.period')]]"
disabled="[[isLoadingData]]"
>
<paper-listbox
slot="dropdown-content"
selected="{{_periodIndex}}"
>
<paper-item
>[[localize('ui.duration.day', 'count', 1)]]</paper-item
>
<paper-item
>[[localize('ui.duration.day', 'count', 3)]]</paper-item
>
<paper-item
>[[localize('ui.duration.week', 'count', 1)]]</paper-item
>
</paper-listbox>
</paper-dropdown-menu>
</div>
<state-history-charts
hass="[[hass]]"
history-data="[[stateHistory]]"
is-loading-data="[[isLoadingData]]"
end-time="[[endTime]]"
no-single
>
</state-history-charts>
</div>
</app-header-layout>
`;
}
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);

View File

@ -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`
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
<div main-title>${this.hass.localize("panel.history")}</div>
</app-toolbar>
</app-header>
<div class="flex content">
<div class="flex layout horizontal wrap">
<ha-date-range-picker
.hass=${this.hass}
?disabled=${this._isLoading}
.startDate=${this._startDate}
.endDate=${this._endDate}
.ranges=${this._ranges}
@change=${this._dateRangeChanged}
></ha-date-range-picker>
</div>
${this._isLoading
? html`<paper-spinner
active
alt=${this.hass.localize("ui.common.loading")}
></paper-spinner>`
: html`
<state-history-charts
.hass=${this.hass}
.historyData=${this._stateHistory}
.endTime=${this._endDate}
no-single
>
</state-history-charts>
`}
</div>
</app-header-layout>
`;
}
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);

View File

@ -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);

View File

@ -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`
<style include="ha-style">
ha-logbook {
height: calc(100vh - 136px);
}
:host([narrow]) ha-logbook {
height: calc(100vh - 198px);
}
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;
}
vaadin-date-picker {
max-width: 200px;
margin-right: 16px;
}
:host([rtl]) vaadin-date-picker {
margin-right: 0;
margin-left: 16px;
}
paper-dropdown-menu {
max-width: 100px;
margin-right: 16px;
--paper-input-container-label-floating: {
padding-bottom: 11px;
}
--paper-input-suffix: {
height: 24px;
}
}
:host([rtl]) paper-dropdown-menu {
text-align: right;
margin-right: 0;
margin-left: 16px;
}
paper-item {
cursor: pointer;
white-space: nowrap;
}
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%;
}
[hidden] {
display: none !important;
}
</style>
<ha-logbook-data
hass="[[hass]]"
is-loading="{{isLoading}}"
entries="{{entries}}"
filter-date="[[_computeFilterDate(_currentDate)]]"
filter-period="[[_computeFilterDays(_periodIndex)]]"
filter-entity="[[entityId]]"
></ha-logbook-data>
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button
hass="[[hass]]"
narrow="[[narrow]]"
></ha-menu-button>
<div main-title>[[localize('panel.logbook')]]</div>
<ha-icon-button
icon="hass:refresh"
on-click="refreshLogbook"
hidden$="[[isLoading]]"
></ha-icon-button>
</app-toolbar>
</app-header>
<paper-spinner
active="[[isLoading]]"
hidden$="[[!isLoading]]"
alt="[[localize('ui.common.loading')]]"
></paper-spinner>
<div class="filters">
<vaadin-date-picker
id="picker"
value="{{_currentDate}}"
label="[[localize('ui.panel.logbook.showing_entries')]]"
disabled="[[isLoading]]"
required
></vaadin-date-picker>
<paper-dropdown-menu
label-float
label="[[localize('ui.panel.logbook.period')]]"
disabled="[[isLoading]]"
>
<paper-listbox slot="dropdown-content" selected="{{_periodIndex}}">
<paper-item
>[[localize('ui.duration.day', 'count', 1)]]</paper-item
>
<paper-item
>[[localize('ui.duration.day', 'count', 3)]]</paper-item
>
<paper-item
>[[localize('ui.duration.week', 'count', 1)]]</paper-item
>
</paper-listbox>
</paper-dropdown-menu>
<ha-entity-picker
hass="[[hass]]"
value="{{_entityId}}"
label="[[localize('ui.components.entity.entity-picker.entity')]]"
disabled="[[isLoading]]"
on-change="_entityPicked"
></ha-entity-picker>
</div>
<ha-logbook
hass="[[hass]]"
entries="[[entries]]"
hidden$="[[isLoading]]"
></ha-logbook>
</app-header-layout>
`;
}
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);

View File

@ -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`
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
<div main-title>${this.hass.localize("panel.logbook")}</div>
<mwc-icon-button
@click=${this._refreshLogbook}
.disabled=${this._isLoading}
>
<ha-svg-icon path=${mdiRefresh}></ha-svg-icon>
</mwc-icon-button>
</app-toolbar>
</app-header>
${this._isLoading ? html`` : ""}
<div class="filters">
<ha-date-range-picker
.hass=${this.hass}
?disabled=${this._isLoading}
.startDate=${this._startDate}
.endDate=${this._endDate}
.ranges=${this._ranges}
@change=${this._dateRangeChanged}
></ha-date-range-picker>
<ha-entity-picker
.hass=${this.hass}
.value=${this._entityId}
.label=${this.hass.localize(
"ui.components.entity.entity-picker.entity"
)}
.disabled=${this._isLoading}
@change=${this._entityPicked}
></ha-entity-picker>
</div>
${this._isLoading
? html`<paper-spinner
active
alt=${this.hass.localize("ui.common.loading")}
></paper-spinner>`
: html`<ha-logbook
.hass=${this.hass}
.entries=${this._entries}
></ha-logbook>`}
</app-header-layout>
`;
}
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%;
}
`,
];
}
}

View File

@ -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";

View File

@ -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": {

View File

@ -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"