mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 17:26:42 +00:00
Multiple entities on history panel (#9946)
Co-authored-by: Zack Barett <zackbarett@hey.com>
This commit is contained in:
parent
0f580a91c9
commit
cff3f51d34
@ -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`<div class="mdc-chip-set items">
|
||||
${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
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
</div>
|
||||
return html`<div class=${this.horizontal ? "horizontal-container" : ""}>
|
||||
${this.horizontal ? this._renderChips() : this._renderItems()}
|
||||
${this._renderPicker()}
|
||||
<div class="mdc-chip-set">
|
||||
${this.horizontal ? this._renderItems() : this._renderChips()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderItems() {
|
||||
return html`<div class="mdc-chip-set items">
|
||||
${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
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderChips() {
|
||||
return html`<div class="mdc-chip-set">
|
||||
<div
|
||||
class="mdc-chip area_id add"
|
||||
.type=${"area_id"}
|
||||
@ -217,7 +229,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: ""} `;
|
||||
@ -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}
|
||||
></ha-area-picker>`;
|
||||
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}
|
||||
></ha-device-picker>`;
|
||||
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
|
||||
></ha-entity-picker>`;
|
||||
@ -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);
|
||||
}
|
||||
|
@ -223,16 +223,12 @@ export const fetchDate = (
|
||||
hass: HomeAssistant,
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
entityId?: string
|
||||
entityIds: string[]
|
||||
): Promise<HassEntity[][]> =>
|
||||
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<HistoryStates>({ ...params, entity_ids: [entityId] });
|
||||
if (entityIds.length !== 0) {
|
||||
return hass.callWS<HistoryStates>({ ...params, entity_ids: entityIds });
|
||||
}
|
||||
return hass.callWS<HistoryStates>(params);
|
||||
};
|
||||
|
@ -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`
|
||||
<ha-app-layout>
|
||||
@ -80,25 +100,40 @@ class HaPanelHistory extends LitElement {
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
<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 class="flex content">
|
||||
<div class="filters flex layout horizontal narrow-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>
|
||||
<ha-target-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._targetPickerValue}
|
||||
.disabled=${this._isLoading}
|
||||
horizontal
|
||||
@value-changed=${this._entitiesChanged}
|
||||
></ha-target-picker>
|
||||
</div>
|
||||
${this._isLoading
|
||||
? html`<div class="progress-wrapper">
|
||||
<ha-circular-progress
|
||||
active
|
||||
alt=${this.hass.localize("ui.common.loading")}
|
||||
></ha-circular-progress>
|
||||
</div>`
|
||||
: html`
|
||||
<state-history-charts
|
||||
.hass=${this.hass}
|
||||
.historyData=${this._stateHistory}
|
||||
.endTime=${this._endDate}
|
||||
no-single
|
||||
>
|
||||
</state-history-charts>
|
||||
`}
|
||||
</div>
|
||||
${this._isLoading
|
||||
? html`<div class="progress-wrapper">
|
||||
@ -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<string, string> = {};
|
||||
|
||||
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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user