mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 15:26:36 +00:00
Logbook + History allow date/time filter (#6192)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
b9d6973a79
commit
7a13242077
@ -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",
|
||||
|
228
src/components/date-range-picker.ts
Normal file
228
src/components/date-range-picker.ts
Normal 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;
|
||||
}
|
||||
}
|
195
src/components/ha-date-range-picker.ts
Normal file
195
src/components/ha-date-range-picker.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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}`] = {};
|
||||
};
|
||||
|
@ -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);
|
204
src/panels/history/ha-panel-history.ts
Normal file
204
src/panels/history/ha-panel-history.ts
Normal 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);
|
@ -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);
|
@ -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);
|
279
src/panels/logbook/ha-panel-logbook.ts
Normal file
279
src/panels/logbook/ha-panel-logbook.ts
Normal 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%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -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": {
|
||||
|
17
yarn.lock
17
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user