Compare commits

...

1 Commits

Author SHA1 Message Date
Bram Kragten 776ae2416c WIP 2026-07-09 12:54:03 +02:00
7 changed files with 783 additions and 50 deletions
@@ -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}
+211
View File
@@ -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;
}
}
+543 -47
View File
@@ -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.
+5 -2
View File
@@ -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": {