Multiple entities on history panel bugfix and additional improvements (#13045)

Co-authored-by: Zack Barett <zackbarett@hey.com>
This commit is contained in:
D3v01dZA 2022-07-05 10:23:38 -04:00 committed by GitHub
parent 8fd5f53f96
commit 6cd38472cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 276 additions and 162 deletions

View File

@ -186,6 +186,7 @@ class StateHistoryCharts extends LitElement {
line-height: 60px; line-height: 60px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.container { .container {
max-height: var(--history-max-height); max-height: var(--history-max-height);
} }

View File

@ -42,8 +42,8 @@ import "./entity/ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker"; import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
import "./ha-area-picker"; import "./ha-area-picker";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-input-helper-text"; import "./ha-input-helper-text";
import "./ha-svg-icon";
@customElement("ha-target-picker") @customElement("ha-target-picker")
export class HaTargetPicker extends SubscribeMixin(LitElement) { export class HaTargetPicker extends SubscribeMixin(LitElement) {
@ -119,55 +119,68 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
if (!this._areas || !this._devices || !this._entities) { if (!this._areas || !this._devices || !this._entities) {
return html``; return html``;
} }
return html`<div class=${this.horizontal ? "horizontal-container" : ""}> return html`
${this.horizontal ? this._renderChips() : this._renderItems()} ${this.horizontal
${this._renderPicker()} ? html`
${this.horizontal ? this._renderItems() : this._renderChips()} <div class="horizontal-container">
</div>`; ${this._renderChips()} ${this._renderPicker()}
</div>
${this._renderItems()}
`
: html`
<div>
${this._renderItems()} ${this._renderPicker()}
${this._renderChips()}
</div>
`}
`;
} }
private _renderItems() { private _renderItems() {
return html`<div class="mdc-chip-set items"> return html`
${this.value?.area_id <div class="mdc-chip-set items">
? ensureArray(this.value.area_id).map((area_id) => { ${this.value?.area_id
const area = this._areas![area_id]; ? ensureArray(this.value.area_id).map((area_id) => {
return this._renderChip( const area = this._areas![area_id];
"area_id", return this._renderChip(
area_id, "area_id",
area?.name || area_id, area_id,
undefined, area?.name || area_id,
mdiSofa undefined,
); mdiSofa
}) );
: ""} })
${this.value?.device_id : ""}
? ensureArray(this.value.device_id).map((device_id) => { ${this.value?.device_id
const device = this._devices![device_id]; ? ensureArray(this.value.device_id).map((device_id) => {
return this._renderChip( const device = this._devices![device_id];
"device_id", return this._renderChip(
device_id, "device_id",
device ? computeDeviceName(device, this.hass) : device_id, device_id,
undefined, device ? computeDeviceName(device, this.hass) : device_id,
mdiDevices undefined,
); mdiDevices
}) );
: ""} })
${this.value?.entity_id : ""}
? ensureArray(this.value.entity_id).map((entity_id) => { ${this.value?.entity_id
const entity = this.hass.states[entity_id]; ? ensureArray(this.value.entity_id).map((entity_id) => {
return this._renderChip( const entity = this.hass.states[entity_id];
"entity_id", return this._renderChip(
entity_id, "entity_id",
entity ? computeStateName(entity) : entity_id, entity_id,
entity entity ? computeStateName(entity) : entity_id,
); entity
}) );
: ""} })
</div>`; : ""}
</div>
`;
} }
private _renderChips() { private _renderChips() {
return html`<div class="mdc-chip-set"> return html`
<div class="mdc-chip-set">
<div <div
class="mdc-chip area_id add" class="mdc-chip area_id add"
.type=${"area_id"} .type=${"area_id"}
@ -231,7 +244,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
</div> </div>
${this.helper ${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>` ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""} `; : ""}
`;
} }
private async _showPicker(ev) { private async _showPicker(ev) {
@ -320,51 +334,54 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
private _renderPicker() { private _renderPicker() {
switch (this._addMode) { switch (this._addMode) {
case "area_id": case "area_id":
return html`<ha-area-picker return html`
.hass=${this.hass} <ha-area-picker
id="input" .hass=${this.hass}
.type=${"area_id"} id="input"
.label=${this.hass.localize( .type=${"area_id"}
"ui.components.target-picker.add_area_id" .label=${this.hass.localize(
)} "ui.components.target-picker.add_area_id"
no-add )}
.deviceFilter=${this.deviceFilter} no-add
.entityFilter=${this.entityRegFilter} .deviceFilter=${this.deviceFilter}
.includeDeviceClasses=${this.includeDeviceClasses} .entityFilter=${this.entityRegFilter}
.includeDomains=${this.includeDomains} .includeDeviceClasses=${this.includeDeviceClasses}
class=${this.horizontal ? "hidden-picker" : ""} .includeDomains=${this.includeDomains}
@value-changed=${this._targetPicked} @value-changed=${this._targetPicked}
></ha-area-picker>`; ></ha-area-picker>
`;
case "device_id": case "device_id":
return html`<ha-device-picker return html`
.hass=${this.hass} <ha-device-picker
id="input" .hass=${this.hass}
.type=${"device_id"} id="input"
.label=${this.hass.localize( .type=${"device_id"}
"ui.components.target-picker.add_device_id" .label=${this.hass.localize(
)} "ui.components.target-picker.add_device_id"
.deviceFilter=${this.deviceFilter} )}
.entityFilter=${this.entityRegFilter} .deviceFilter=${this.deviceFilter}
.includeDeviceClasses=${this.includeDeviceClasses} .entityFilter=${this.entityRegFilter}
.includeDomains=${this.includeDomains} .includeDeviceClasses=${this.includeDeviceClasses}
class=${this.horizontal ? "hidden-picker" : ""} .includeDomains=${this.includeDomains}
@value-changed=${this._targetPicked} @value-changed=${this._targetPicked}
></ha-device-picker>`; ></ha-device-picker>
`;
case "entity_id": case "entity_id":
return html`<ha-entity-picker return html`
.hass=${this.hass} <ha-entity-picker
id="input" .hass=${this.hass}
.type=${"entity_id"} id="input"
.label=${this.hass.localize( .type=${"entity_id"}
"ui.components.target-picker.add_entity_id" .label=${this.hass.localize(
)} "ui.components.target-picker.add_entity_id"
.entityFilter=${this.entityFilter} )}
.includeDeviceClasses=${this.includeDeviceClasses} .entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains} .includeDeviceClasses=${this.includeDeviceClasses}
class=${this.horizontal ? "hidden-picker" : ""} .includeDomains=${this.includeDomains}
@value-changed=${this._targetPicked} @value-changed=${this._targetPicked}
allow-custom-entity allow-custom-entity
></ha-entity-picker>`; ></ha-entity-picker>
`;
} }
return html``; return html``;
} }
@ -553,12 +570,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
${unsafeCSS(chipStyles)} ${unsafeCSS(chipStyles)}
.hidden-picker {
height: 0px;
display: inline-block;
overflow: hidden;
position: absolute;
}
.horizontal-container { .horizontal-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -1,4 +1,4 @@
import { mdiRefresh } from "@mdi/js"; import { mdiCollapseAll, mdiRefresh } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import { import {
@ -34,6 +34,10 @@ import {
EntityRegistryEntry, EntityRegistryEntry,
subscribeEntityRegistry, subscribeEntityRegistry,
} from "../../data/entity_registry"; } from "../../data/entity_registry";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
@ -57,9 +61,23 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
@state() private _ranges?: DateRangePickerRanges; @state() private _ranges?: DateRangePickerRanges;
@state() private _entities?: EntityRegistryEntry[]; @state() private _devices?: { [deviceId: string]: DeviceRegistryEntry };
@state() private _stateEntities?: EntityRegistryEntry[]; @state() private _entities?: { [entityId: string]: EntityRegistryEntry };
@state() private _stateEntities?: { [entityId: string]: EntityRegistryEntry };
@state() private _deviceIdToEntities?: {
[deviceId: string]: EntityRegistryEntry[];
};
@state() private _areaIdToEntities?: {
[areaId: string]: EntityRegistryEntry[];
};
@state() private _areaIdToDevices?: {
[areaId: string]: DeviceRegistryEntry[];
};
public constructor() { public constructor() {
super(); super();
@ -76,7 +94,52 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ return [
subscribeEntityRegistry(this.hass.connection!, (entities) => { subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities; this._entities = entities.reduce((accumulator, current) => {
accumulator[current.entity_id] = current;
return accumulator;
}, {});
this._deviceIdToEntities = entities.reduce((accumulator, current) => {
if (!current.device_id) {
return accumulator;
}
let found = accumulator[current.device_id];
if (found === undefined) {
found = [];
accumulator[current.device_id] = found;
}
found.push(current);
return accumulator;
}, {});
this._areaIdToEntities = entities.reduce((accumulator, current) => {
if (!current.area_id) {
return accumulator;
}
let found = accumulator[current.area_id];
if (found === undefined) {
found = [];
accumulator[current.area_id] = found;
}
found.push(current);
return accumulator;
}, {});
}),
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._devices = devices.reduce((accumulator, current) => {
accumulator[current.id] = current;
return accumulator;
}, {});
this._areaIdToDevices = devices.reduce((accumulator, current) => {
if (!current.area_id) {
return accumulator;
}
let found = accumulator[current.area_id];
if (found === undefined) {
found = [];
accumulator[current.area_id] = found;
}
found.push(current);
return accumulator;
}, {});
}), }),
]; ];
} }
@ -91,6 +154,12 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-menu-button> ></ha-menu-button>
<div main-title>${this.hass.localize("panel.history")}</div> <div main-title>${this.hass.localize("panel.history")}</div>
<ha-icon-button
@click=${this._removeAll}
.disabled=${this._isLoading}
.path=${mdiCollapseAll}
.label=${this.hass.localize("ui.panel.history.remove_all")}
></ha-icon-button>
<ha-icon-button <ha-icon-button
@click=${this._refreshHistory} @click=${this._refreshHistory}
.disabled=${this._isLoading} .disabled=${this._isLoading}
@ -125,6 +194,10 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
alt=${this.hass.localize("ui.common.loading")} alt=${this.hass.localize("ui.common.loading")}
></ha-circular-progress> ></ha-circular-progress>
</div>` </div>`
: !this._targetPickerValue
? html`<div class="start-search">
${this.hass.localize("ui.panel.history.start_search")}
</div>`
: html` : html`
<state-history-charts <state-history-charts
.hass=${this.hass} .hass=${this.hass}
@ -135,24 +208,6 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
</state-history-charts> </state-history-charts>
`} `}
</div> </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
virtualize
.hass=${this.hass}
.historyData=${this._stateHistory}
.endTime=${this._endDate}
.narrow=${this.narrow}
no-single
>
</state-history-charts>
`}
</ha-app-layout> </ha-app-layout>
`; `;
} }
@ -199,8 +254,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
if ( if (
changedProps.has("_startDate") || changedProps.has("_startDate") ||
changedProps.has("_endDate") || changedProps.has("_endDate") ||
changedProps.has("_targetPickerValue") || changedProps.has("_targetPickerValue")
changedProps.has("_entities")
) { ) {
this._getHistory(); this._getHistory();
} }
@ -211,15 +265,13 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
this.rtl = computeRTL(this.hass); this.rtl = computeRTL(this.hass);
} }
if (this._entities) { if (this._entities) {
const stateEntities: EntityRegistryEntry[] = []; const stateEntities: { [entityId: string]: EntityRegistryEntry } = {};
const regEntityIds = new Set( const regEntityIds = new Set(Object.keys(this._entities));
this._entities.map((entity) => entity.entity_id)
);
for (const entityId of Object.keys(this.hass.states)) { for (const entityId of Object.keys(this.hass.states)) {
if (regEntityIds.has(entityId)) { if (regEntityIds.has(entityId)) {
continue; continue;
} }
stateEntities.push({ stateEntities[entityId] = {
name: computeStateName(this.hass.states[entityId]), name: computeStateName(this.hass.states[entityId]),
entity_id: entityId, entity_id: entityId,
platform: computeDomain(entityId), platform: computeDomain(entityId),
@ -230,17 +282,27 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
device_id: null, device_id: null,
icon: null, icon: null,
entity_category: null, entity_category: null,
}); };
} }
this._stateEntities = stateEntities; this._stateEntities = stateEntities;
} }
} }
} }
private _removeAll() {
this._targetPickerValue = undefined;
}
private _refreshHistory() { private _refreshHistory() {
this._getHistory(); this._getHistory();
} }
private _shouldShowEntityByLargerSelection(
entity: EntityRegistryEntry
): boolean {
return entity.entity_category === null;
}
private async _getHistory() { private async _getHistory() {
this._isLoading = true; this._isLoading = true;
const entityIds = this._getEntityIds(); const entityIds = this._getEntityIds();
@ -261,50 +323,79 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
this._isLoading = false; 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[] { private _getEntityIds(): string[] {
if ( if (
this._targetPickerValue === undefined || this._targetPickerValue === undefined ||
this._entities === undefined || this._entities === undefined ||
this._stateEntities === undefined this._stateEntities === undefined ||
this._devices === undefined ||
this._deviceIdToEntities === undefined ||
this._areaIdToEntities === undefined ||
this._areaIdToDevices === undefined
) { ) {
return []; return [];
} }
const entityIds = this._entities const entityIds = new Set<string>();
.filter((entity) => this._filterEntity(entity)) let {
.map((entity) => entity.entity_id); area_id: searchingAreaId,
const stateEntityIds = this._stateEntities device_id: searchingDeviceId,
.filter((entity) => this._filterEntity(entity)) entity_id: searchingEntityId,
.map((entity) => entity.entity_id); } = this._targetPickerValue;
return [...entityIds, ...stateEntityIds]; if (searchingAreaId !== undefined) {
if (typeof searchingAreaId === "string") {
searchingAreaId = [searchingAreaId];
}
for (const singleSearchingAreaId of searchingAreaId) {
const foundEntities = this._areaIdToEntities[singleSearchingAreaId];
if (foundEntities !== undefined) {
for (const foundEntity of foundEntities) {
if (this._shouldShowEntityByLargerSelection(foundEntity)) {
entityIds.add(foundEntity.entity_id);
}
}
}
const foundDevices = this._areaIdToDevices[singleSearchingAreaId];
if (foundDevices !== undefined) {
for (const foundDevice of foundDevices) {
const foundDeviceEntities =
this._deviceIdToEntities[foundDevice.id];
for (const foundDeviceEntity of foundDeviceEntities) {
if (
(!foundDeviceEntity.area_id ||
foundDeviceEntity.area_id === singleSearchingAreaId) &&
this._shouldShowEntityByLargerSelection(foundDeviceEntity)
) {
entityIds.add(foundDeviceEntity.entity_id);
}
}
}
}
}
}
if (searchingDeviceId !== undefined) {
if (typeof searchingDeviceId === "string") {
searchingDeviceId = [searchingDeviceId];
}
for (const singleSearchingDeviceId of searchingDeviceId) {
const foundEntities = this._deviceIdToEntities[singleSearchingDeviceId];
if (foundEntities !== undefined) {
for (const foundEntity of foundEntities) {
if (this._shouldShowEntityByLargerSelection(foundEntity)) {
entityIds.add(foundEntity.entity_id);
}
}
}
}
}
if (searchingEntityId !== undefined) {
if (typeof searchingEntityId === "string") {
searchingEntityId = [searchingEntityId];
}
for (const singleSearchingEntityId of searchingEntityId) {
entityIds.add(singleSearchingEntityId);
}
}
return [...entityIds];
} }
private _dateRangeChanged(ev) { private _dateRangeChanged(ev) {
@ -389,7 +480,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
.filters { .filters {
display: flex; display: flex;
align-items: flex-end; align-items: flex-start;
padding: 8px 16px 0; padding: 8px 16px 0;
} }
@ -429,6 +520,12 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
max-width: none; max-width: none;
width: 100%; width: 100%;
} }
.start-search {
padding-top: 16px;
text-align: center;
color: var(--secondary-text-color);
}
`, `,
]; ];
} }

View File

@ -4559,6 +4559,11 @@
"energy_sources_table_title": "Sources", "energy_sources_table_title": "Sources",
"energy_devices_graph_title": "Monitor individual devices" "energy_devices_graph_title": "Monitor individual devices"
} }
},
"history": {
"start_search": "Start by selecting an area, device or entity above",
"add_all": "Add all entities",
"remove_all": "Remove all selections"
} }
}, },
"tips": { "tips": {