diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts index 1453afa07b..7545a19a36 100644 --- a/src/components/ha-target-picker.ts +++ b/src/components/ha-target-picker.ts @@ -79,6 +79,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { @property({ type: Boolean, reflect: true }) public disabled = false; + @property({ type: Boolean }) public horizontal = false; + @state() private _areas?: { [areaId: string]: AreaRegistryEntry }; @state() private _devices?: { @@ -117,45 +119,55 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { if (!this._areas || !this._devices || !this._entities) { return html``; } - return html`
- ${this.value?.area_id - ? ensureArray(this.value.area_id).map((area_id) => { - const area = this._areas![area_id]; - return this._renderChip( - "area_id", - area_id, - area?.name || area_id, - undefined, - mdiSofa - ); - }) - : ""} - ${this.value?.device_id - ? ensureArray(this.value.device_id).map((device_id) => { - const device = this._devices![device_id]; - return this._renderChip( - "device_id", - device_id, - device ? computeDeviceName(device, this.hass) : device_id, - undefined, - mdiDevices - ); - }) - : ""} - ${this.value?.entity_id - ? ensureArray(this.value.entity_id).map((entity_id) => { - const entity = this.hass.states[entity_id]; - return this._renderChip( - "entity_id", - entity_id, - entity ? computeStateName(entity) : entity_id, - entity - ); - }) - : ""} -
+ return html`
+ ${this.horizontal ? this._renderChips() : this._renderItems()} ${this._renderPicker()} -
+ ${this.horizontal ? this._renderItems() : this._renderChips()} +
`; + } + + private _renderItems() { + return html`
+ ${this.value?.area_id + ? ensureArray(this.value.area_id).map((area_id) => { + const area = this._areas![area_id]; + return this._renderChip( + "area_id", + area_id, + area?.name || area_id, + undefined, + mdiSofa + ); + }) + : ""} + ${this.value?.device_id + ? ensureArray(this.value.device_id).map((device_id) => { + const device = this._devices![device_id]; + return this._renderChip( + "device_id", + device_id, + device ? computeDeviceName(device, this.hass) : device_id, + undefined, + mdiDevices + ); + }) + : ""} + ${this.value?.entity_id + ? ensureArray(this.value.entity_id).map((entity_id) => { + const entity = this.hass.states[entity_id]; + return this._renderChip( + "entity_id", + entity_id, + entity ? computeStateName(entity) : entity_id, + entity + ); + }) + : ""} +
`; + } + + private _renderChips() { + return html`
- ${this.helper ? html`${this.helper}` : ""} `; @@ -321,6 +332,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { .entityFilter=${this.entityRegFilter} .includeDeviceClasses=${this.includeDeviceClasses} .includeDomains=${this.includeDomains} + class=${this.horizontal ? "hidden-picker" : ""} @value-changed=${this._targetPicked} >`; case "device_id": @@ -335,6 +347,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { .entityFilter=${this.entityRegFilter} .includeDeviceClasses=${this.includeDeviceClasses} .includeDomains=${this.includeDomains} + class=${this.horizontal ? "hidden-picker" : ""} @value-changed=${this._targetPicked} >`; case "entity_id": @@ -348,6 +361,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { .entityFilter=${this.entityFilter} .includeDeviceClasses=${this.includeDeviceClasses} .includeDomains=${this.includeDomains} + class=${this.horizontal ? "hidden-picker" : ""} @value-changed=${this._targetPicked} allow-custom-entity >`; @@ -539,6 +553,16 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { static get styles(): CSSResultGroup { return css` ${unsafeCSS(chipStyles)} + .hidden-picker { + height: 0px; + display: inline-block; + overflow: hidden; + position: absolute; + } + .horizontal-container { + display: flex; + flex-wrap: wrap; + } .mdc-chip { color: var(--primary-text-color); } diff --git a/src/data/history.ts b/src/data/history.ts index 7de7651a75..0fb7a8857c 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -223,16 +223,12 @@ export const fetchDate = ( hass: HomeAssistant, startTime: Date, endTime: Date, - entityId?: string + entityIds: string[] ): Promise => hass.callApi( "GET", `history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${ - entityId ? `&filter_entity_id=${entityId}` : `` - }${ - entityId && !entityIdHistoryNeedsAttributes(hass, entityId) - ? `&no_attributes` - : `` + entityIds ? `&filter_entity_id=${entityIds.join(",")}` : `` }` ); @@ -240,19 +236,19 @@ export const fetchDateWS = ( hass: HomeAssistant, startTime: Date, endTime: Date, - entityId?: string + entityIds: string[] ) => { const params = { type: "history/history_during_period", start_time: startTime.toISOString(), end_time: endTime.toISOString(), minimal_response: true, - no_attributes: !!( - entityId && !entityIdHistoryNeedsAttributes(hass, entityId) - ), + no_attributes: !entityIds + .map((entityId) => entityIdHistoryNeedsAttributes(hass, entityId)) + .reduce((cur, next) => cur || next, false), }; - if (entityId) { - return hass.callWS({ ...params, entity_ids: [entityId] }); + if (entityIds.length !== 0) { + return hass.callWS({ ...params, entity_ids: entityIds }); } return hass.callWS(params); }; diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts index 7caa849c0a..9deb2cf1b1 100644 --- a/src/panels/history/ha-panel-history.ts +++ b/src/panels/history/ha-panel-history.ts @@ -12,6 +12,7 @@ import { } from "date-fns/esm"; import { css, html, LitElement, PropertyValues } from "lit"; import { property, state } from "lit/decorators"; +import { UnsubscribeFunc } from "home-assistant-js-websocket/dist/types"; import { navigate } from "../../common/navigate"; import { createSearchParam, @@ -19,7 +20,7 @@ import { } from "../../common/url/search-params"; import { computeRTL } from "../../common/util/compute_rtl"; import "../../components/chart/state-history-charts"; -import "../../components/entity/ha-entity-picker"; +import "../../components/ha-target-picker"; import "../../components/ha-circular-progress"; import "../../components/ha-date-range-picker"; import type { DateRangePickerRanges } from "../../components/ha-date-range-picker"; @@ -29,8 +30,15 @@ import { computeHistory, fetchDateWS } from "../../data/history"; import "../../layouts/ha-app-layout"; import { haStyle } from "../../resources/styles"; import { HomeAssistant } from "../../types"; +import { + EntityRegistryEntry, + subscribeEntityRegistry, +} from "../../data/entity_registry"; +import { SubscribeMixin } from "../../mixins/subscribe-mixin"; +import { computeStateName } from "../../common/entity/compute_state_name"; +import { computeDomain } from "../../common/entity/compute_domain"; -class HaPanelHistory extends LitElement { +class HaPanelHistory extends SubscribeMixin(LitElement) { @property() hass!: HomeAssistant; @property({ reflect: true, type: Boolean }) narrow!: boolean; @@ -39,7 +47,7 @@ class HaPanelHistory extends LitElement { @property() _endDate: Date; - @property() _entityId = ""; + @property() _targetPickerValue?; @property() _isLoading = false; @@ -49,6 +57,10 @@ class HaPanelHistory extends LitElement { @state() private _ranges?: DateRangePickerRanges; + @state() private _entities?: EntityRegistryEntry[]; + + @state() private _stateEntities?: EntityRegistryEntry[]; + public constructor() { super(); @@ -61,6 +73,14 @@ class HaPanelHistory extends LitElement { this._endDate = end; } + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeEntityRegistry(this.hass.connection!, (entities) => { + this._entities = entities; + }), + ]; + } + protected render() { return html` @@ -80,25 +100,40 @@ class HaPanelHistory extends LitElement { -
- - - +
+
+ + +
+ ${this._isLoading + ? html`
+ +
` + : html` + + + `}
${this._isLoading ? html`
@@ -142,7 +177,13 @@ class HaPanelHistory extends LitElement { [addDays(weekStart, -7), addDays(weekEnd, -7)], }; - this._entityId = extractSearchParam("entity_id") ?? ""; + const entityIds = extractSearchParam("entity_id"); + if (entityIds) { + const splitEntityIds = entityIds.split(","); + this._targetPickerValue = { + entity_id: splitEntityIds, + }; + } const startDate = extractSearchParam("start_date"); if (startDate) { @@ -158,16 +199,41 @@ class HaPanelHistory extends LitElement { if ( changedProps.has("_startDate") || changedProps.has("_endDate") || - changedProps.has("_entityId") + changedProps.has("_targetPickerValue") || + changedProps.has("_entities") ) { this._getHistory(); } - if (changedProps.has("hass")) { + if (changedProps.has("hass") || changedProps.has("_entities")) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; if (!oldHass || oldHass.language !== this.hass.language) { this.rtl = computeRTL(this.hass); } + if (this._entities) { + const stateEntities: EntityRegistryEntry[] = []; + const regEntityIds = new Set( + this._entities.map((entity) => entity.entity_id) + ); + for (const entityId of Object.keys(this.hass.states)) { + if (regEntityIds.has(entityId)) { + continue; + } + stateEntities.push({ + name: computeStateName(this.hass.states[entityId]), + entity_id: entityId, + platform: computeDomain(entityId), + disabled_by: null, + hidden_by: null, + area_id: null, + config_entry_id: null, + device_id: null, + icon: null, + entity_category: null, + }); + } + this._stateEntities = stateEntities; + } } } @@ -177,12 +243,16 @@ class HaPanelHistory extends LitElement { private async _getHistory() { this._isLoading = true; - const dateHistory = await fetchDateWS( - this.hass, - this._startDate, - this._endDate, - this._entityId - ); + const entityIds = this._getEntityIds(); + const dateHistory = + entityIds.length === 0 + ? {} + : await fetchDateWS( + this.hass, + this._startDate, + this._endDate, + entityIds + ); this._stateHistory = computeHistory( this.hass, dateHistory, @@ -191,6 +261,52 @@ class HaPanelHistory extends LitElement { this._isLoading = false; } + private _filterEntity(entity: EntityRegistryEntry): boolean { + const { area_id, device_id, entity_id } = this._targetPickerValue; + if (area_id !== undefined) { + if (typeof area_id === "string" && area_id === entity.area_id) { + return true; + } + if (Array.isArray(area_id) && area_id.includes(entity.area_id)) { + return true; + } + } + if (device_id !== undefined) { + if (typeof device_id === "string" && device_id === entity.device_id) { + return true; + } + if (Array.isArray(device_id) && device_id.includes(entity.device_id)) { + return true; + } + } + if (entity_id !== undefined) { + if (typeof entity_id === "string" && entity_id === entity.entity_id) { + return true; + } + if (Array.isArray(entity_id) && entity_id.includes(entity.entity_id)) { + return true; + } + } + return false; + } + + private _getEntityIds(): string[] { + if ( + this._targetPickerValue === undefined || + this._entities === undefined || + this._stateEntities === undefined + ) { + return []; + } + const entityIds = this._entities + .filter((entity) => this._filterEntity(entity)) + .map((entity) => entity.entity_id); + const stateEntityIds = this._stateEntities + .filter((entity) => this._filterEntity(entity)) + .map((entity) => entity.entity_id); + return [...entityIds, ...stateEntityIds]; + } + private _dateRangeChanged(ev) { this._startDate = ev.detail.startDate; const endDate = ev.detail.endDate; @@ -203,8 +319,8 @@ class HaPanelHistory extends LitElement { this._updatePath(); } - private _entityPicked(ev) { - this._entityId = ev.target.value; + private _entitiesChanged(ev) { + this._targetPickerValue = ev.detail.value; this._updatePath(); } @@ -212,8 +328,8 @@ class HaPanelHistory extends LitElement { private _updatePath() { const params: Record = {}; - if (this._entityId) { - params.entity_id = this._entityId; + if (this._targetPickerValue) { + params.entity_id = this._getEntityIds().join(","); } if (this._startDate) { @@ -255,6 +371,18 @@ class HaPanelHistory extends LitElement { height: 100%; } + :host([narrow]) .narrow-wrap { + flex-wrap: wrap; + } + + .horizontal { + align-items: center; + } + + :host(:not([narrow])) .selector-padding { + padding-left: 32px; + } + .progress-wrapper { position: relative; }