mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-09 11:19:49 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 776ae2416c |
@@ -51,6 +51,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public names?: Record<string, string>;
|
||||
|
||||
@property({ attribute: false }) public entityContext?: Record<string, string>;
|
||||
|
||||
@property({ attribute: false }) public colors?: Record<
|
||||
string,
|
||||
string | undefined
|
||||
@@ -180,6 +182,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
return html`${title}${datapoints.map((param) => {
|
||||
const entityId = this._entityIds[param.seriesIndex];
|
||||
const context = this.entityContext?.[entityId];
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const entry = this.hass.entities[entityId];
|
||||
const stateValue = String(param.value[1]);
|
||||
@@ -207,7 +210,11 @@ export class StateHistoryChartLine extends LitElement {
|
||||
></ha-chart-tooltip-marker>
|
||||
${param.seriesName
|
||||
? html`${param.seriesName}: `
|
||||
: nothing}${value}${statSuffix}`;
|
||||
: nothing}${value}${context
|
||||
? html`<br /><span style="opacity: 0.6"
|
||||
>${" ".repeat(2)}${context}</span
|
||||
>`
|
||||
: nothing}${statSuffix}`;
|
||||
})}`;
|
||||
};
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public names?: Record<string, string>;
|
||||
|
||||
@property({ attribute: false }) public entityContext?: Record<string, string>;
|
||||
|
||||
@property() public unit?: string;
|
||||
|
||||
@property() public identifier?: string;
|
||||
@@ -145,8 +147,18 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
const entityId = seriesName
|
||||
? this._chartData.find((d) => (d.name as string) === seriesName)?.id
|
||||
: undefined;
|
||||
const context = entityId
|
||||
? this.entityContext?.[entityId as string]
|
||||
: undefined;
|
||||
return html`${seriesName
|
||||
? html`<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
|
||||
: nothing}${context
|
||||
? html`<div style="text-align: center; opacity: 0.6; margin: 0 0 4px">
|
||||
${context}
|
||||
</div>`
|
||||
: nothing}<ha-chart-tooltip-marker
|
||||
.color=${String(color ?? "")}
|
||||
.rtl=${rtl}
|
||||
|
||||
@@ -58,6 +58,8 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public names?: Record<string, string>;
|
||||
|
||||
@property({ attribute: false }) public entityContext?: Record<string, string>;
|
||||
|
||||
@property({ attribute: false }) public colors?: Record<
|
||||
string,
|
||||
string | undefined
|
||||
@@ -197,6 +199,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
.endTime=${this._computedEndTime}
|
||||
.paddingYAxis=${this._maxYWidth}
|
||||
.names=${this.names}
|
||||
.entityContext=${this.entityContext}
|
||||
.colors=${this.colors}
|
||||
.chartIndex=${index}
|
||||
.clickForMoreInfo=${this.clickForMoreInfo}
|
||||
@@ -220,6 +223,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
.endTime=${this._computedEndTime}
|
||||
.showNames=${this.showNames}
|
||||
.names=${this.names}
|
||||
.entityContext=${this.entityContext}
|
||||
.narrow=${this.narrow}
|
||||
.chunked=${this.virtualize}
|
||||
.paddingYAxis=${this._maxYWidth}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
const deviceClassToName = (deviceClass: string): string =>
|
||||
deviceClass.charAt(0).toUpperCase() + deviceClass.slice(1).replace(/_/g, " ");
|
||||
|
||||
@customElement("ha-filter-device-classes")
|
||||
export class HaFilterDeviceClasses extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
@query("ha-list") private _list?: HTMLElement;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.history.filter.device_class")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilter}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@selected=${this._handleItemSelected}
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._deviceClasses(this.hass.states, this._filter),
|
||||
(i) => i,
|
||||
(deviceClass) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${deviceClass}
|
||||
.selected=${(this.value || []).includes(deviceClass)}
|
||||
>
|
||||
${deviceClassToName(deviceClass)}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _deviceClasses = memoizeOne(
|
||||
(states: HomeAssistant["states"], filter?: string) => {
|
||||
const deviceClasses = new Set<string>();
|
||||
Object.values(states).forEach((stateObj) => {
|
||||
const deviceClass = stateObj.attributes.device_class;
|
||||
if (deviceClass) {
|
||||
deviceClasses.add(deviceClass);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(deviceClasses.values())
|
||||
.filter(
|
||||
(deviceClass) =>
|
||||
!filter ||
|
||||
deviceClass.toLowerCase().includes(filter) ||
|
||||
deviceClassToName(deviceClass).toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
deviceClassToName(a),
|
||||
deviceClassToName(b),
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleItemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
const deviceClasses = this._deviceClasses(this.hass.states, this._filter);
|
||||
|
||||
const visible = new Set(deviceClasses);
|
||||
const preserved = (this.value || []).filter((d) => !visible.has(d));
|
||||
const selected = [...ev.detail.index]
|
||||
.map((i) => deviceClasses[i])
|
||||
.filter((d): d is string => !!d);
|
||||
|
||||
this.value = [...preserved, ...selected];
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value.length ? this.value : undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _clearFilter(ev) {
|
||||
ev.preventDefault();
|
||||
this.value = undefined;
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
this._filter = (target.value ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: 0;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-device-classes": HaFilterDeviceClasses;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,34 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import {
|
||||
mdiDevices,
|
||||
mdiDotsVertical,
|
||||
mdiDownload,
|
||||
mdiFilterRemove,
|
||||
mdiFilterVariant,
|
||||
mdiHome,
|
||||
mdiImagePlus,
|
||||
mdiTagOutline,
|
||||
mdiTextureBox,
|
||||
} from "@mdi/js";
|
||||
import { differenceInHours } from "date-fns";
|
||||
import type {
|
||||
HassEntity,
|
||||
HassServiceTarget,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket/dist/types";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import {
|
||||
computeEntityDisplayParts,
|
||||
computeHistoryNames,
|
||||
type HistoryNameResult,
|
||||
} from "./history-entity-names";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import {
|
||||
@@ -30,13 +44,21 @@ import { MIN_TIME_BETWEEN_UPDATES } from "../../components/chart/ha-chart-base";
|
||||
import "../../components/chart/state-history-charts";
|
||||
import type { StateHistoryCharts } from "../../components/chart/state-history-charts";
|
||||
import "../../components/date-picker/ha-date-range-picker";
|
||||
import "../../components/ha-bottom-sheet";
|
||||
import "../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
|
||||
import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-filter-device-classes";
|
||||
import "../../components/ha-filter-domains";
|
||||
import "../../components/ha-filter-floor-areas";
|
||||
import "../../components/ha-filter-integrations";
|
||||
import "../../components/ha-filter-labels";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/ha-state-icon";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-target-picker";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import "../../components/ha-two-pane-top-app-bar-fixed";
|
||||
import type { HistoryResult } from "../../data/history";
|
||||
import {
|
||||
computeHistory,
|
||||
@@ -52,6 +74,22 @@ import type { HomeAssistant } from "../../types";
|
||||
import { fileDownload } from "../../util/file_download";
|
||||
import { addEntitiesToLovelaceView } from "../lovelace/editor/add-entities-to-view";
|
||||
|
||||
interface HistoryFilters {
|
||||
domain?: string[];
|
||||
device_class?: string[];
|
||||
floor_areas?: { floors?: string[]; areas?: string[] };
|
||||
label?: string[];
|
||||
integration?: string[];
|
||||
}
|
||||
|
||||
interface TargetChip {
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
// For entity chips we render a state icon; other types use an mdi path.
|
||||
icon?: string;
|
||||
stateObj?: HassEntity;
|
||||
}
|
||||
|
||||
@customElement("ha-panel-history")
|
||||
class HaPanelHistory extends LitElement {
|
||||
@property({ attribute: false }) hass!: HomeAssistant;
|
||||
@@ -72,6 +110,24 @@ class HaPanelHistory extends LitElement {
|
||||
})
|
||||
private _targetPickerValue: HassServiceTarget = {};
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "historyFilters",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filters: HistoryFilters = {};
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "historyPaneCollapsed",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _paneCollapsed = false;
|
||||
|
||||
@state() private _sheetOpen = false;
|
||||
|
||||
@state() private _isLoading = false;
|
||||
|
||||
@state() private _stateHistory?: HistoryResult;
|
||||
@@ -90,6 +146,10 @@ class HaPanelHistory extends LitElement {
|
||||
|
||||
private _interval?: number;
|
||||
|
||||
private _showPaneController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width > 750,
|
||||
});
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
@@ -116,14 +176,37 @@ class HaPanelHistory extends LitElement {
|
||||
|
||||
protected render() {
|
||||
const entitiesSelected = this._getEntityIds().length > 0;
|
||||
const wide = this._showPaneController.value ?? !this.narrow;
|
||||
const showPane = wide && !this._paneCollapsed;
|
||||
const activeFilters = this._activeFilterCount();
|
||||
|
||||
return html`
|
||||
<ha-top-app-bar-fixed
|
||||
<ha-two-pane-top-app-bar-fixed
|
||||
.pane=${showPane}
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${!!this._showBack}
|
||||
>
|
||||
<h1 class="page-title" slot="title">
|
||||
${this.hass.localize("panel.history")}
|
||||
</h1>
|
||||
|
||||
${!showPane
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="actionItems"
|
||||
@click=${this._toggleFilters}
|
||||
.path=${mdiFilterVariant}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.subpage-data-table.filters"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
${activeFilters
|
||||
? html`<div class="filter-badge" slot="actionItems">
|
||||
${activeFilters}
|
||||
</div>`
|
||||
: nothing}
|
||||
`
|
||||
: nothing}
|
||||
${entitiesSelected
|
||||
? html`
|
||||
<ha-icon-button
|
||||
@@ -153,25 +236,14 @@ class HaPanelHistory extends LitElement {
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
|
||||
<div class="flex content ha-scrollbar">
|
||||
<div class="filters">
|
||||
<ha-date-range-picker
|
||||
?disabled=${this._isLoading}
|
||||
.startDate=${this._startDate}
|
||||
.endDate=${this._endDate}
|
||||
extended-presets
|
||||
time-picker
|
||||
@value-changed=${this._dateRangeChanged}
|
||||
></ha-date-range-picker>
|
||||
<ha-target-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._targetPickerValue}
|
||||
.disabled=${this._isLoading}
|
||||
add-on-top
|
||||
@value-changed=${this._targetsChanged}
|
||||
compact
|
||||
></ha-target-picker>
|
||||
</div>
|
||||
${showPane
|
||||
? html`<div slot="pane" class="pane-content">
|
||||
${this._renderFilters()}
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
<div class="content ha-scrollbar">
|
||||
${!showPane ? this._renderTargetSummary() : nothing}
|
||||
${this._isLoading
|
||||
? html`<div class="progress-wrapper">
|
||||
<ha-spinner></ha-spinner>
|
||||
@@ -184,6 +256,8 @@ class HaPanelHistory extends LitElement {
|
||||
<state-history-charts
|
||||
.hass=${this.hass}
|
||||
.historyData=${this._mungedStateHistory}
|
||||
.names=${this._getNameResult().names}
|
||||
.entityContext=${this._getNameResult().context}
|
||||
.startTime=${this._startDate}
|
||||
.endTime=${this._endDate}
|
||||
.narrow=${this.narrow}
|
||||
@@ -192,10 +266,162 @@ class HaPanelHistory extends LitElement {
|
||||
</state-history-charts>
|
||||
`}
|
||||
</div>
|
||||
</ha-top-app-bar-fixed>
|
||||
</ha-two-pane-top-app-bar-fixed>
|
||||
|
||||
${!showPane
|
||||
? html`<ha-bottom-sheet
|
||||
flexcontent
|
||||
.open=${this._sheetOpen}
|
||||
@closed=${this._closeSheet}
|
||||
>
|
||||
<div slot="header" class="sheet-header">
|
||||
${this.hass.localize("ui.components.subpage-data-table.filters")}
|
||||
${this._activeFilterCount() || this._targetCount()
|
||||
? html`<button class="link-button" @click=${this._removeAll}>
|
||||
${this.hass.localize("ui.common.clear")}
|
||||
</button>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="sheet-content">${this._renderFilters()}</div>
|
||||
</ha-bottom-sheet>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderFilters() {
|
||||
return html`
|
||||
<div class="filters">
|
||||
<ha-date-range-picker
|
||||
?disabled=${this._isLoading}
|
||||
.startDate=${this._startDate}
|
||||
.endDate=${this._endDate}
|
||||
extended-presets
|
||||
time-picker
|
||||
@value-changed=${this._dateRangeChanged}
|
||||
></ha-date-range-picker>
|
||||
<ha-target-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._targetPickerValue}
|
||||
.disabled=${this._isLoading}
|
||||
add-on-top
|
||||
@value-changed=${this._targetsChanged}
|
||||
></ha-target-picker>
|
||||
<div class="filter-panels">
|
||||
<ha-filter-domains
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters.domain}
|
||||
@data-table-filter-changed=${this._domainFilterChanged}
|
||||
></ha-filter-domains>
|
||||
<ha-filter-device-classes
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters.device_class}
|
||||
@data-table-filter-changed=${this._deviceClassFilterChanged}
|
||||
></ha-filter-device-classes>
|
||||
<ha-filter-floor-areas
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters.floor_areas}
|
||||
@data-table-filter-changed=${this._floorAreasFilterChanged}
|
||||
></ha-filter-floor-areas>
|
||||
<ha-filter-integrations
|
||||
.value=${this._filters.integration}
|
||||
@data-table-filter-changed=${this._integrationFilterChanged}
|
||||
></ha-filter-integrations>
|
||||
<ha-filter-labels
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters.label}
|
||||
@data-table-filter-changed=${this._labelFilterChanged}
|
||||
></ha-filter-labels>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTargetSummary() {
|
||||
const chips = this._targetChips();
|
||||
return html`
|
||||
<div class="target-summary" @click=${this._toggleFilters}>
|
||||
${chips.length
|
||||
? html`<div class="chips">
|
||||
${chips.map(
|
||||
(chip) =>
|
||||
html`<span
|
||||
class="chip"
|
||||
title=${chip.secondary
|
||||
? `${chip.secondary} ▸ ${chip.primary}`
|
||||
: chip.primary}
|
||||
>
|
||||
${chip.stateObj
|
||||
? html`<ha-state-icon
|
||||
.stateObj=${chip.stateObj}
|
||||
></ha-state-icon>`
|
||||
: html`<ha-svg-icon .path=${chip.icon}></ha-svg-icon>`}
|
||||
<span class="chip-text">
|
||||
<span class="chip-primary">${chip.primary}</span>
|
||||
${chip.secondary
|
||||
? html`<span class="chip-secondary"
|
||||
>${chip.secondary}</span
|
||||
>`
|
||||
: nothing}
|
||||
</span>
|
||||
</span>`
|
||||
)}
|
||||
</div>`
|
||||
: html`<span class="summary-placeholder">
|
||||
${this.hass.localize("ui.panel.history.start_search")}
|
||||
</span>`}
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariant}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.subpage-data-table.filters"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _targetChips(): TargetChip[] {
|
||||
const value = this._targetPickerValue;
|
||||
const chips: TargetChip[] = [];
|
||||
const toArray = (v?: string | string[]) =>
|
||||
v ? (Array.isArray(v) ? v : [v]) : [];
|
||||
|
||||
toArray(value.floor_id).forEach((id) => {
|
||||
const floor = this.hass.floors?.[id];
|
||||
chips.push({ primary: floor?.name ?? id, icon: mdiHome });
|
||||
});
|
||||
toArray(value.area_id).forEach((id) => {
|
||||
const area = this.hass.areas?.[id];
|
||||
const floor = area?.floor_id
|
||||
? this.hass.floors?.[area.floor_id]
|
||||
: undefined;
|
||||
chips.push({
|
||||
primary: (area && computeAreaName(area)) ?? id,
|
||||
secondary: floor?.name,
|
||||
icon: mdiTextureBox,
|
||||
});
|
||||
});
|
||||
toArray(value.device_id).forEach((id) => {
|
||||
const device = this.hass.devices?.[id];
|
||||
const area = device?.area_id
|
||||
? this.hass.areas?.[device.area_id]
|
||||
: undefined;
|
||||
chips.push({
|
||||
primary: (device && computeDeviceName(device)) ?? id,
|
||||
secondary: area ? computeAreaName(area) : undefined,
|
||||
icon: mdiDevices,
|
||||
});
|
||||
});
|
||||
toArray(value.entity_id).forEach((id) => {
|
||||
const stateObj = this.hass.states[id];
|
||||
const { primary, secondary } = computeEntityDisplayParts(this.hass, id);
|
||||
chips.push({ primary, secondary, stateObj });
|
||||
});
|
||||
toArray(value.label_id).forEach((id) =>
|
||||
chips.push({ primary: id, icon: mdiTagOutline })
|
||||
);
|
||||
return chips;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
@@ -204,7 +430,8 @@ class HaPanelHistory extends LitElement {
|
||||
changedProps.has("_statisticsHistory") ||
|
||||
changedProps.has("_startDate") ||
|
||||
changedProps.has("_endDate") ||
|
||||
changedProps.has("_targetPickerValue")
|
||||
changedProps.has("_targetPickerValue") ||
|
||||
changedProps.has("_filters")
|
||||
) {
|
||||
if (this._statisticsHistory && this._stateHistory) {
|
||||
this._mungedStateHistory = mergeHistoryResults(
|
||||
@@ -238,6 +465,8 @@ class HaPanelHistory extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
// The reused ha-filter-* components have their captions in the config fragment.
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
const searchParams = extractSearchParamsObject();
|
||||
if (searchParams.back === "1" && history.length > 1) {
|
||||
this._showBack = true;
|
||||
@@ -252,18 +481,99 @@ class HaPanelHistory extends LitElement {
|
||||
changedProps.has("_startDate") ||
|
||||
changedProps.has("_endDate") ||
|
||||
changedProps.has("_targetPickerValue") ||
|
||||
(!this._stateHistory &&
|
||||
(changedProps.has("_deviceEntityLookup") ||
|
||||
changedProps.has("_areaEntityLookup") ||
|
||||
changedProps.has("_areaDeviceLookup")))
|
||||
changedProps.has("_filters")
|
||||
) {
|
||||
this._getHistory();
|
||||
this._getStats();
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleFilters() {
|
||||
const wide = this._showPaneController.value ?? !this.narrow;
|
||||
if (wide) {
|
||||
// On wide screens, re-open the collapsed pane.
|
||||
this._paneCollapsed = false;
|
||||
} else {
|
||||
this._sheetOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _closeSheet() {
|
||||
this._sheetOpen = false;
|
||||
}
|
||||
|
||||
private _activeFilterCount(): number {
|
||||
const f = this._filters;
|
||||
return (
|
||||
(f.domain?.length || 0) +
|
||||
(f.device_class?.length || 0) +
|
||||
(f.integration?.length || 0) +
|
||||
(f.label?.length || 0) +
|
||||
(f.floor_areas?.areas?.length || 0) +
|
||||
(f.floor_areas?.floors?.length || 0)
|
||||
);
|
||||
}
|
||||
|
||||
private _targetCount(): number {
|
||||
const value = this._targetPickerValue;
|
||||
const len = (v?: string | string[]) =>
|
||||
v ? (Array.isArray(v) ? v.length : 1) : 0;
|
||||
return (
|
||||
len(value.entity_id) +
|
||||
len(value.device_id) +
|
||||
len(value.area_id) +
|
||||
len(value.floor_id) +
|
||||
len(value.label_id)
|
||||
);
|
||||
}
|
||||
|
||||
private _domainFilterChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._setFilter("domain", ev.detail.value);
|
||||
}
|
||||
|
||||
private _deviceClassFilterChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._setFilter("device_class", ev.detail.value);
|
||||
}
|
||||
|
||||
private _integrationFilterChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._setFilter("integration", ev.detail.value);
|
||||
}
|
||||
|
||||
private _labelFilterChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._setFilter("label", ev.detail.value);
|
||||
}
|
||||
|
||||
private _floorAreasFilterChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as
|
||||
| { floors?: string[]; areas?: string[] }
|
||||
| undefined;
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
floor_areas:
|
||||
value && (value.areas?.length || value.floors?.length)
|
||||
? value
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private _setFilter(
|
||||
key: "domain" | "device_class" | "integration" | "label",
|
||||
value: string[] | undefined
|
||||
) {
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
[key]: value?.length ? value : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private _removeAll() {
|
||||
this._targetPickerValue = {};
|
||||
this._filters = {};
|
||||
this._updatePath();
|
||||
}
|
||||
|
||||
@@ -374,6 +684,7 @@ class HaPanelHistory extends LitElement {
|
||||
private _getEntityIds(): string[] {
|
||||
return this.__getEntityIds(
|
||||
this._targetPickerValue,
|
||||
this._filters,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas
|
||||
@@ -383,13 +694,101 @@ class HaPanelHistory extends LitElement {
|
||||
private __getEntityIds = memoizeOne(
|
||||
(
|
||||
targetPickerValue: HassServiceTarget,
|
||||
filters: HistoryFilters,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"]
|
||||
): string[] =>
|
||||
resolveEntityIDs(this.hass, targetPickerValue, entities, devices, areas)
|
||||
): string[] => {
|
||||
const resolved = resolveEntityIDs(
|
||||
this.hass,
|
||||
targetPickerValue,
|
||||
entities,
|
||||
devices,
|
||||
areas
|
||||
);
|
||||
return this._applyFilters(resolved, filters);
|
||||
}
|
||||
);
|
||||
|
||||
private _getNameResult(): HistoryNameResult {
|
||||
return this.__getNameResult(
|
||||
this._getEntityIds(),
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
}
|
||||
|
||||
private __getNameResult = memoizeOne(
|
||||
(
|
||||
entityIds: string[],
|
||||
_entities: HomeAssistant["entities"],
|
||||
_devices: HomeAssistant["devices"],
|
||||
_areas: HomeAssistant["areas"],
|
||||
_floors: HomeAssistant["floors"]
|
||||
): HistoryNameResult => computeHistoryNames(this.hass, entityIds)
|
||||
);
|
||||
|
||||
private _applyFilters(
|
||||
entityIds: string[],
|
||||
filters: HistoryFilters
|
||||
): string[] {
|
||||
let result = entityIds;
|
||||
|
||||
if (filters.domain?.length) {
|
||||
const domains = new Set(filters.domain);
|
||||
result = result.filter((id) => domains.has(computeDomain(id)));
|
||||
}
|
||||
|
||||
if (filters.device_class?.length) {
|
||||
const deviceClasses = new Set(filters.device_class);
|
||||
result = result.filter((id) =>
|
||||
deviceClasses.has(this.hass.states[id]?.attributes.device_class as any)
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.integration?.length) {
|
||||
const integrations = new Set(filters.integration);
|
||||
result = result.filter((id) => {
|
||||
const platform = this.hass.entities[id]?.platform;
|
||||
return platform && integrations.has(platform);
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.label?.length) {
|
||||
const labels = new Set(filters.label);
|
||||
result = result.filter((id) =>
|
||||
this.hass.entities[id]?.labels.some((l) => labels.has(l))
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
filters.floor_areas?.areas?.length ||
|
||||
filters.floor_areas?.floors?.length
|
||||
) {
|
||||
const areaIds = new Set(filters.floor_areas.areas || []);
|
||||
const floorIds = new Set(filters.floor_areas.floors || []);
|
||||
// Expand selected floors to their areas.
|
||||
Object.values(this.hass.areas).forEach((area) => {
|
||||
if (area.floor_id && floorIds.has(area.floor_id)) {
|
||||
areaIds.add(area.area_id);
|
||||
}
|
||||
});
|
||||
result = result.filter((id) => {
|
||||
const entity = this.hass.entities[id];
|
||||
const areaId =
|
||||
entity?.area_id ??
|
||||
(entity?.device_id
|
||||
? this.hass.devices[entity.device_id]?.area_id
|
||||
: undefined);
|
||||
return areaId ? areaIds.has(areaId) : false;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _dateRangeChanged(ev) {
|
||||
this._startDate = ev.detail.value.startDate;
|
||||
this._endDate = ev.detail.value.endDate;
|
||||
@@ -558,7 +957,12 @@ class HaPanelHistory extends LitElement {
|
||||
haStyle,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
ha-top-app-bar-fixed {
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-two-pane-top-app-bar-fixed {
|
||||
--sidepane-width: 320px;
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: visible;
|
||||
@@ -582,9 +986,10 @@ class HaPanelHistory extends LitElement {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
:host([virtualize]) {
|
||||
.pane-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
--ha-generic-picker-max-width: 400px;
|
||||
}
|
||||
|
||||
.progress-wrapper {
|
||||
@@ -597,32 +1002,123 @@ class HaPanelHistory extends LitElement {
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-top: 16px;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.filter-panels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 -16px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-date-range-picker {
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
max-width: 100%;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
ha-target-picker {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1025px) {
|
||||
.filters {
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-date-range-picker {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.filter-badge {
|
||||
position: relative;
|
||||
inset-inline-end: 12px;
|
||||
top: 4px;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
background-color: var(--primary-color);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.target-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.target-summary .chips {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
flex: 1;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.target-summary .chips::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
font-size: var(--ha-font-size-s);
|
||||
background-color: var(--secondary-background-color);
|
||||
border-radius: var(--ha-border-radius-pill, 16px);
|
||||
padding: 4px 12px 4px 8px;
|
||||
}
|
||||
|
||||
.chip ha-state-icon,
|
||||
.chip ha-svg-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 auto;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.chip-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.chip-secondary {
|
||||
font-size: var(--ha-font-size-2xs, 11px);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.summary-placeholder {
|
||||
flex: 1;
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
|
||||
.sheet-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
|
||||
.sheet-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary-color);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.start-search {
|
||||
|
||||
Binary file not shown.
@@ -11219,14 +11219,17 @@
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"start_search": "Select areas, devices, entities or labels above",
|
||||
"start_search": "Select areas, devices, entities or labels to view their history",
|
||||
"add_all": "Add all entities",
|
||||
"remove_all": "Remove all selections",
|
||||
"download_data": "Download data",
|
||||
"download_data_error": "Unable to download data",
|
||||
"add_card": "Add current view as card",
|
||||
"add_card_error": "Unable to add card",
|
||||
"error_no_data": "You need to select some data sources first."
|
||||
"error_no_data": "You need to select some data sources first.",
|
||||
"filter": {
|
||||
"device_class": "Device class"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tips": {
|
||||
|
||||
Reference in New Issue
Block a user