From 2acd1ff5e88f7db942c6aaafeb5f40027780ba91 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 29 Nov 2023 10:01:54 +0100 Subject: [PATCH] Refactor and cleanup device picker (#18804) --- .../device/ha-area-devices-picker.ts | 325 ------------------ src/components/device/ha-device-picker.ts | 77 ++--- src/components/ha-area-picker.ts | 8 +- src/data/device_registry.ts | 4 +- 4 files changed, 29 insertions(+), 385 deletions(-) delete mode 100644 src/components/device/ha-area-devices-picker.ts diff --git a/src/components/device/ha-area-devices-picker.ts b/src/components/device/ha-area-devices-picker.ts deleted file mode 100644 index 8964ff566c..0000000000 --- a/src/components/device/ha-area-devices-picker.ts +++ /dev/null @@ -1,325 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; -import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { html, LitElement, PropertyValues, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { fireEvent } from "../../common/dom/fire_event"; -import { computeDomain } from "../../common/entity/compute_domain"; -import { stringCompare } from "../../common/string/compare"; -import { - AreaRegistryEntry, - subscribeAreaRegistry, -} from "../../data/area_registry"; -import { - DeviceEntityLookup, - DeviceRegistryEntry, - subscribeDeviceRegistry, -} from "../../data/device_registry"; -import { - EntityRegistryEntry, - subscribeEntityRegistry, -} from "../../data/entity_registry"; -import { SubscribeMixin } from "../../mixins/subscribe-mixin"; -import { ValueChangedEvent, HomeAssistant } from "../../types"; -import "../ha-icon-button"; -import "../ha-svg-icon"; -import "./ha-devices-picker"; - -interface DevicesByArea { - [areaId: string]: AreaDevices; -} - -interface AreaDevices { - id?: string; - name: string; - devices: string[]; -} - -const rowRenderer: ComboBoxLitRenderer = (item) => - html` - ${item.name} - ${item.devices.length} devices - `; - -@customElement("ha-area-devices-picker") -export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public label?: string; - - @property() public value?: string; - - @property() public area?: string; - - @property() public devices?: string[]; - - /** - * Show only devices with entities from specific domains. - * @type {Array} - * @attr include-domains - */ - @property({ type: Array, attribute: "include-domains" }) - public includeDomains?: string[]; - - /** - * Show no devices with entities of these domains. - * @type {Array} - * @attr exclude-domains - */ - @property({ type: Array, attribute: "exclude-domains" }) - public excludeDomains?: string[]; - - /** - * Show only deviced with entities of these device classes. - * @type {Array} - * @attr include-device-classes - */ - @property({ type: Array, attribute: "include-device-classes" }) - public includeDeviceClasses?: string[]; - - @state() private _areaPicker = true; - - @state() private _devices?: DeviceRegistryEntry[]; - - @state() private _areas?: AreaRegistryEntry[]; - - @state() private _entities?: EntityRegistryEntry[]; - - private _selectedDevices: string[] = []; - - private _filteredDevices: DeviceRegistryEntry[] = []; - - private _getAreasWithDevices = memoizeOne( - ( - devices: DeviceRegistryEntry[], - areas: AreaRegistryEntry[], - entities: EntityRegistryEntry[], - includeDomains: this["includeDomains"], - excludeDomains: this["excludeDomains"], - includeDeviceClasses: this["includeDeviceClasses"] - ): AreaDevices[] => { - if (!devices.length) { - return []; - } - - const deviceEntityLookup: DeviceEntityLookup = {}; - for (const entity of entities) { - if (!entity.device_id) { - continue; - } - if (!(entity.device_id in deviceEntityLookup)) { - deviceEntityLookup[entity.device_id] = []; - } - deviceEntityLookup[entity.device_id].push(entity); - } - - let inputDevices = [...devices]; - - if (includeDomains) { - inputDevices = inputDevices.filter((device) => { - const devEntities = deviceEntityLookup[device.id]; - if (!devEntities || !devEntities.length) { - return false; - } - return deviceEntityLookup[device.id].some((entity) => - includeDomains.includes(computeDomain(entity.entity_id)) - ); - }); - } - - if (excludeDomains) { - inputDevices = inputDevices.filter((device) => { - const devEntities = deviceEntityLookup[device.id]; - if (!devEntities || !devEntities.length) { - return true; - } - return entities.every( - (entity) => - !excludeDomains.includes(computeDomain(entity.entity_id)) - ); - }); - } - - if (includeDeviceClasses) { - inputDevices = inputDevices.filter((device) => { - const devEntities = deviceEntityLookup[device.id]; - if (!devEntities || !devEntities.length) { - return false; - } - return deviceEntityLookup[device.id].some((entity) => { - const stateObj = this.hass.states[entity.entity_id]; - if (!stateObj) { - return false; - } - return ( - stateObj.attributes.device_class && - includeDeviceClasses.includes(stateObj.attributes.device_class) - ); - }); - }); - } - - this._filteredDevices = inputDevices; - - const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; - for (const area of areas) { - areaLookup[area.area_id] = area; - } - - const devicesByArea: DevicesByArea = {}; - - for (const device of inputDevices) { - const areaId = device.area_id; - if (areaId) { - if (!(areaId in devicesByArea)) { - devicesByArea[areaId] = { - id: areaId, - name: areaLookup[areaId].name, - devices: [], - }; - } - devicesByArea[areaId].devices.push(device.id); - } - } - - const sorted = Object.keys(devicesByArea) - .sort((a, b) => - stringCompare( - devicesByArea[a].name || "", - devicesByArea[b].name || "", - this.hass.locale.language - ) - ) - .map((key) => devicesByArea[key]); - - return sorted; - } - ); - - public hassSubscribe(): UnsubscribeFunc[] { - return [ - subscribeDeviceRegistry(this.hass.connection!, (devices) => { - this._devices = devices; - }), - subscribeAreaRegistry(this.hass.connection!, (areas) => { - this._areas = areas; - }), - subscribeEntityRegistry(this.hass.connection!, (entities) => { - this._entities = entities; - }), - ]; - } - - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (changedProps.has("area") && this.area) { - this._areaPicker = true; - this.value = this.area; - } else if (changedProps.has("devices") && this.devices) { - this._areaPicker = false; - const filteredDeviceIds = this._filteredDevices.map( - (device) => device.id - ); - const selectedDevices = this.devices.filter((device) => - filteredDeviceIds.includes(device) - ); - this._setValue(selectedDevices); - } - } - - protected render() { - if (!this._devices || !this._areas || !this._entities) { - return nothing; - } - const areas = this._getAreasWithDevices( - this._devices, - this._areas, - this._entities, - this.includeDomains, - this.excludeDomains, - this.includeDeviceClasses - ); - if (!this._areaPicker || areas.length === 0) { - return html` - - ${areas.length > 0 - ? html` - Choose an area - ` - : ""} - `; - } - return html` - - - - Choose individual devices - - `; - } - - private get _value() { - return this.value || []; - } - - private async _switchPicker() { - this._areaPicker = !this._areaPicker; - } - - private async _areaPicked(ev: ValueChangedEvent) { - const value = ev.detail.value; - let selectedDevices = []; - const target = ev.target as any; - if (target.selectedItem) { - selectedDevices = target.selectedItem.devices; - } - - if (value !== this._value || this._selectedDevices !== selectedDevices) { - this._setValue(selectedDevices, value); - } - } - - private _devicesPicked(ev: CustomEvent) { - ev.stopPropagation(); - const selectedDevices = ev.detail.value; - this._setValue(selectedDevices); - } - - private _setValue(selectedDevices: string[], value = "") { - this.value = value; - this._selectedDevices = selectedDevices; - setTimeout(() => { - fireEvent(this, "value-changed", { value: selectedDevices }); - fireEvent(this, "change"); - }, 0); - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-area-devices-picker": HaAreaDevicesPicker; - } -} diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 6b8a2b4f7f..dd64cf68c2 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -1,35 +1,27 @@ -import "@material/mwc-list/mwc-list-item"; -import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; -import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; +import { HassEntity } from "home-assistant-js-websocket"; +import { LitElement, PropertyValues, TemplateResult, html } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { stringCompare } from "../../common/string/compare"; import { - AreaRegistryEntry, - subscribeAreaRegistry, -} from "../../data/area_registry"; + ScorableTextItem, + fuzzyFilterSort, +} from "../../common/string/filter/sequence-matching"; +import { AreaRegistryEntry } from "../../data/area_registry"; import { - computeDeviceName, - DeviceEntityLookup, + DeviceEntityDisplayLookup, DeviceRegistryEntry, - getDeviceEntityLookup, - subscribeDeviceRegistry, + computeDeviceName, + getDeviceEntityDisplayLookup, } from "../../data/device_registry"; -import { - EntityRegistryEntry, - subscribeEntityRegistry, -} from "../../data/entity_registry"; -import { SubscribeMixin } from "../../mixins/subscribe-mixin"; -import { ValueChangedEvent, HomeAssistant } from "../../types"; +import { EntityRegistryDisplayEntry } from "../../data/entity_registry"; +import { HomeAssistant, ValueChangedEvent } from "../../types"; import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; -import { - fuzzyFilterSort, - ScorableTextItem, -} from "../../common/string/filter/sequence-matching"; +import "../ha-list-item"; interface Device { name: string; @@ -46,13 +38,13 @@ export type HaDevicePickerDeviceFilterFunc = ( export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean; const rowRenderer: ComboBoxLitRenderer = (item) => - html` + html` ${item.name} ${item.area} - `; + `; @customElement("ha-device-picker") -export class HaDevicePicker extends SubscribeMixin(LitElement) { +export class HaDevicePicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public label?: string; @@ -61,12 +53,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { @property() public helper?: string; - @property() public devices?: DeviceRegistryEntry[]; - - @property() public areas?: AreaRegistryEntry[]; - - @property() public entities?: EntityRegistryEntry[]; - /** * Show only devices with entities from specific domains. * @type {Array} @@ -117,7 +103,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { ( devices: DeviceRegistryEntry[], areas: AreaRegistryEntry[], - entities: EntityRegistryEntry[], + entities: EntityRegistryDisplayEntry[], includeDomains: this["includeDomains"], excludeDomains: this["excludeDomains"], includeDeviceClasses: this["includeDeviceClasses"], @@ -136,7 +122,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { ]; } - let deviceEntityLookup: DeviceEntityLookup = {}; + let deviceEntityLookup: DeviceEntityDisplayLookup = {}; if ( includeDomains || @@ -144,13 +130,10 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { includeDeviceClasses || entityFilter ) { - deviceEntityLookup = getDeviceEntityLookup(entities); + deviceEntityLookup = getDeviceEntityDisplayLookup(entities); } - const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; - for (const area of areas) { - areaLookup[area.area_id] = area; - } + const areaLookup = areas; let inputDevices = devices.filter( (device) => device.id === this.value || !device.disabled_by @@ -276,30 +259,16 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { await this.comboBox?.focus(); } - public hassSubscribe(): UnsubscribeFunc[] { - return [ - subscribeDeviceRegistry(this.hass.connection!, (devices) => { - this.devices = devices; - }), - subscribeAreaRegistry(this.hass.connection!, (areas) => { - this.areas = areas; - }), - subscribeEntityRegistry(this.hass.connection!, (entities) => { - this.entities = entities; - }), - ]; - } - protected updated(changedProps: PropertyValues) { if ( - (!this._init && this.devices && this.areas && this.entities) || + (!this._init && this.hass) || (this._init && changedProps.has("_opened") && this._opened) ) { this._init = true; const devices = this._getDevices( - this.devices!, - this.areas!, - this.entities!, + Object.values(this.hass.devices), + Object.values(this.hass.areas), + Object.values(this.hass.entities), this.includeDomains, this.excludeDomains, this.includeDeviceClasses, diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index d4cb739adc..d2a0f689fb 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -1,4 +1,3 @@ -import "@material/mwc-list/mwc-list-item"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, PropertyValues, TemplateResult } from "lit"; @@ -25,21 +24,22 @@ import { showAlertDialog, showPromptDialog, } from "../dialogs/generic/show-dialog-box"; -import { ValueChangedEvent, HomeAssistant } from "../types"; +import { HomeAssistant, ValueChangedEvent } from "../types"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import "./ha-combo-box"; import type { HaComboBox } from "./ha-combo-box"; import "./ha-icon-button"; +import "./ha-list-item"; import "./ha-svg-icon"; type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry; const rowRenderer: ComboBoxLitRenderer = (item) => - html` ${item.name} - `; + `; @customElement("ha-area-picker") export class HaAreaPicker extends LitElement { diff --git a/src/data/device_registry.ts b/src/data/device_registry.ts index 488ee1f365..4e2d308e0e 100644 --- a/src/data/device_registry.ts +++ b/src/data/device_registry.ts @@ -45,7 +45,7 @@ export interface DeviceRegistryEntryMutableParams { export const fallbackDeviceName = ( hass: HomeAssistant, - entities: EntityRegistryEntry[] | string[] + entities: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[] ) => { for (const entity of entities || []) { const entityId = typeof entity === "string" ? entity : entity.entity_id; @@ -60,7 +60,7 @@ export const fallbackDeviceName = ( export const computeDeviceName = ( device: DeviceRegistryEntry, hass: HomeAssistant, - entities?: EntityRegistryEntry[] | string[] + entities?: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[] ) => device.name_by_user || device.name ||