From e4d233afa8797e129361979335ead15f345f65fe Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 18 Jul 2022 15:07:55 -0500 Subject: [PATCH] Filter Integration in Target and Area selectors + clean up some code (#13202) --- .../ha-selector/ha-selector-area.ts | 102 ++++------- .../ha-selector/ha-selector-device.ts | 56 ++---- .../ha-selector/ha-selector-entity.ts | 37 +--- .../ha-selector/ha-selector-target.ts | 161 ++++++++---------- src/data/device_registry.ts | 27 ++- src/data/entity_registry.ts | 13 ++ src/data/selector.ts | 112 +++++++++--- 7 files changed, 246 insertions(+), 262 deletions(-) diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index bdc8ec9b12..c6f1be6efd 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -1,8 +1,9 @@ -import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { html, LitElement } from "lit"; +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { DeviceRegistryEntry } from "../../data/device_registry"; +import type { DeviceRegistryEntry } from "../../data/device_registry"; +import { getDeviceIntegrationLookup } from "../../data/device_registry"; import { EntityRegistryEntry, subscribeEntityRegistry, @@ -11,7 +12,11 @@ import { EntitySources, fetchEntitySourcesWithCache, } from "../../data/entity_sources"; -import { AreaSelector } from "../../data/selector"; +import type { AreaSelector } from "../../data/selector"; +import { + filterSelectorDevices, + filterSelectorEntities, +} from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../types"; import "../ha-area-picker"; @@ -29,13 +34,15 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { @property() public helper?: string; + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + @state() private _entitySources?: EntitySources; @state() private _entities?: EntityRegistryEntry[]; - @property({ type: Boolean }) public disabled = false; - - @property({ type: Boolean }) public required = true; + private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); public hassSubscribe(): UnsubscribeFunc[] { return [ @@ -45,7 +52,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { ]; } - protected updated(changedProperties) { + protected updated(changedProperties: PropertyValues): void { if ( changedProperties.has("selector") && (this.selector.area.device?.integration || @@ -58,7 +65,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { } } - protected render() { + protected render(): TemplateResult { if ( (this.selector.area.device?.integration || this.selector.area.entity?.integration) && @@ -77,12 +84,6 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { no-add .deviceFilter=${this._filterDevices} .entityFilter=${this._filterEntities} - .includeDeviceClasses=${this.selector.area.entity?.device_class - ? [this.selector.area.entity.device_class] - : undefined} - .includeDomains=${this.selector.area.entity?.domain - ? [this.selector.area.entity.domain] - : undefined} .disabled=${this.disabled} .required=${this.required} > @@ -98,27 +99,22 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { no-add .deviceFilter=${this._filterDevices} .entityFilter=${this._filterEntities} - .includeDeviceClasses=${this.selector.area.entity?.device_class - ? [this.selector.area.entity.device_class] - : undefined} - .includeDomains=${this.selector.area.entity?.domain - ? [this.selector.area.entity.domain] - : undefined} .disabled=${this.disabled} .required=${this.required} > `; } - private _filterEntities = (entity: EntityRegistryEntry): boolean => { - const filterIntegration = this.selector.area.entity?.integration; - if ( - filterIntegration && - this._entitySources?.[entity.entity_id]?.domain !== filterIntegration - ) { - return false; + private _filterEntities = (entity: HassEntity): boolean => { + if (!this.selector.area.entity) { + return true; } - return true; + + return filterSelectorEntities( + this.selector.area.entity, + entity, + this._entitySources + ); }; private _filterDevices = (device: DeviceRegistryEntry): boolean => { @@ -126,47 +122,17 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { return true; } - const { - manufacturer: filterManufacturer, - model: filterModel, - integration: filterIntegration, - } = this.selector.area.device; + const deviceIntegrations = + this._entitySources && this._entities + ? this._deviceIntegrationLookup(this._entitySources, this._entities) + : undefined; - if (filterManufacturer && device.manufacturer !== filterManufacturer) { - return false; - } - if (filterModel && device.model !== filterModel) { - return false; - } - if (filterIntegration && this._entitySources && this._entities) { - const deviceIntegrations = this._deviceIntegrations( - this._entitySources, - this._entities - ); - if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) { - return false; - } - } - return true; + return filterSelectorDevices( + this.selector.area.device, + device, + deviceIntegrations + ); }; - - private _deviceIntegrations = memoizeOne( - (entitySources: EntitySources, entities: EntityRegistryEntry[]) => { - const deviceIntegrations: Record = {}; - - for (const entity of entities) { - const source = entitySources[entity.entity_id]; - if (!source?.domain) { - continue; - } - if (!deviceIntegrations[entity.device_id!]) { - deviceIntegrations[entity.device_id!] = []; - } - deviceIntegrations[entity.device_id!].push(source.domain); - } - return deviceIntegrations; - } - ); } declare global { diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index ae0dbb07f7..bfb0928524 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -2,8 +2,8 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { ConfigEntry } from "../../data/config_entries"; import type { DeviceRegistryEntry } from "../../data/device_registry"; +import { getDeviceIntegrationLookup } from "../../data/device_registry"; import { EntityRegistryEntry, subscribeEntityRegistry, @@ -13,6 +13,7 @@ import { fetchEntitySourcesWithCache, } from "../../data/entity_sources"; import type { DeviceSelector } from "../../data/selector"; +import { filterSelectorDevices } from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../types"; import "../device/ha-device-picker"; @@ -34,12 +35,12 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) { @property() public helper?: string; - @state() public _configEntries?: ConfigEntry[]; - @property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public required = true; + private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); + public hassSubscribe(): UnsubscribeFunc[] { return [ subscribeEntityRegistry(this.hass.connection!, (entities) => { @@ -107,48 +108,17 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) { } private _filterDevices = (device: DeviceRegistryEntry): boolean => { - const { - manufacturer: filterManufacturer, - model: filterModel, - integration: filterIntegration, - } = this.selector.device; + const deviceIntegrations = + this._entitySources && this._entities + ? this._deviceIntegrationLookup(this._entitySources, this._entities) + : undefined; - if (filterManufacturer && device.manufacturer !== filterManufacturer) { - return false; - } - if (filterModel && device.model !== filterModel) { - return false; - } - if (filterIntegration && this._entitySources && this._entities) { - const deviceIntegrations = this._deviceIntegrations( - this._entitySources, - this._entities - ); - if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) { - return false; - } - } - return true; + return filterSelectorDevices( + this.selector.device, + device, + deviceIntegrations + ); }; - - private _deviceIntegrations = memoizeOne( - (entitySources: EntitySources, entities: EntityRegistryEntry[]) => { - const deviceIntegrations: Record = {}; - - for (const entity of entities) { - const source = entitySources[entity.entity_id]; - if (!source?.domain) { - continue; - } - - if (!deviceIntegrations[entity.device_id!]) { - deviceIntegrations[entity.device_id!] = []; - } - deviceIntegrations[entity.device_id!].push(source.domain); - } - return deviceIntegrations; - } - ); } declare global { diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index 8cb925f5d1..2c7062171c 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -1,12 +1,12 @@ import { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { EntitySources, fetchEntitySourcesWithCache, } from "../../data/entity_sources"; -import { EntitySelector } from "../../data/selector"; +import type { EntitySelector } from "../../data/selector"; +import { filterSelectorEntities } from "../../data/selector"; import { HomeAssistant } from "../../types"; import "../entity/ha-entities-picker"; import "../entity/ha-entity-picker"; @@ -73,37 +73,8 @@ export class HaEntitySelector extends LitElement { } } - private _filterEntities = (entity: HassEntity): boolean => { - const { - domain: filterDomain, - device_class: filterDeviceClass, - integration: filterIntegration, - } = this.selector.entity; - - if (filterDomain) { - const entityDomain = computeStateDomain(entity); - if ( - Array.isArray(filterDomain) - ? !filterDomain.includes(entityDomain) - : entityDomain !== filterDomain - ) { - return false; - } - } - if ( - filterDeviceClass && - entity.attributes.device_class !== filterDeviceClass - ) { - return false; - } - if ( - filterIntegration && - this._entitySources?.[entity.entity_id]?.domain !== filterIntegration - ) { - return false; - } - return true; - }; + private _filterEntities = (entity: HassEntity): boolean => + filterSelectorEntities(this.selector.entity, entity, this._entitySources); } declare global { diff --git a/src/components/ha-selector/ha-selector-target.ts b/src/components/ha-selector/ha-selector-target.ts index 4720598a77..70443a097a 100644 --- a/src/components/ha-selector/ha-selector-target.ts +++ b/src/components/ha-selector/ha-selector-target.ts @@ -3,17 +3,33 @@ import { HassServiceTarget, UnsubscribeFunc, } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { ConfigEntry, getConfigEntries } from "../../data/config_entries"; -import { DeviceRegistryEntry } from "../../data/device_registry"; import { - EntityRegistryEntry, - subscribeEntityRegistry, -} from "../../data/entity_registry"; -import { TargetSelector } from "../../data/selector"; + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { + DeviceRegistryEntry, + getDeviceIntegrationLookup, +} from "../../data/device_registry"; +import type { EntityRegistryEntry } from "../../data/entity_registry"; +import { subscribeEntityRegistry } from "../../data/entity_registry"; +import { + EntitySources, + fetchEntitySourcesWithCache, +} from "../../data/entity_sources"; +import { + filterSelectorDevices, + filterSelectorEntities, + TargetSelector, +} from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; -import { HomeAssistant } from "../../types"; +import type { HomeAssistant } from "../../types"; import "../ha-target-picker"; @customElement("ha-selector-target") @@ -28,119 +44,82 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { @property() public helper?: string; - @state() private _entityPlaformLookup?: Record; - - @state() private _configEntries?: ConfigEntry[]; - @property({ type: Boolean }) public disabled = false; + @state() private _entitySources?: EntitySources; + + @state() private _entities?: EntityRegistryEntry[]; + + private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); + public hassSubscribe(): UnsubscribeFunc[] { return [ subscribeEntityRegistry(this.hass.connection!, (entities) => { - const entityLookup = {}; - for (const confEnt of entities) { - if (!confEnt.platform) { - continue; - } - entityLookup[confEnt.entity_id] = confEnt.platform; - } - this._entityPlaformLookup = entityLookup; + this._entities = entities.filter((entity) => entity.device_id !== null); }), ]; } - protected updated(changedProperties) { - if (changedProperties.has("selector")) { - const oldSelector = changedProperties.get("selector"); - if ( - oldSelector !== this.selector && - (this.selector.target.device?.integration || - this.selector.target.entity?.integration) - ) { - this._loadConfigEntries(); - } + protected updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + if ( + changedProperties.has("selector") && + this.selector.target.device?.integration && + !this._entitySources + ) { + fetchEntitySourcesWithCache(this.hass).then((sources) => { + this._entitySources = sources; + }); } } - protected render() { + protected render(): TemplateResult { + if ( + (this.selector.target.device?.integration || + this.selector.target.entity?.integration) && + !this._entitySources + ) { + return html``; + } + return html``; } private _filterEntities = (entity: HassEntity): boolean => { - if ( - this.selector.target.entity?.integration || - this.selector.target.device?.integration - ) { - if ( - !this._entityPlaformLookup || - this._entityPlaformLookup[entity.entity_id] !== - (this.selector.target.entity?.integration || - this.selector.target.device?.integration) - ) { - return false; - } + if (!this.selector.target.entity) { + return true; } - return true; - }; - private _filterRegEntities = (entity: EntityRegistryEntry): boolean => { - if (this.selector.target.entity?.integration) { - if (entity.platform !== this.selector.target.entity.integration) { - return false; - } - } - return true; + return filterSelectorEntities( + this.selector.target.entity, + entity, + this._entitySources + ); }; private _filterDevices = (device: DeviceRegistryEntry): boolean => { - if ( - this.selector.target.device?.manufacturer && - device.manufacturer !== this.selector.target.device.manufacturer - ) { - return false; + if (!this.selector.target.device) { + return true; } - if ( - this.selector.target.device?.model && - device.model !== this.selector.target.device.model - ) { - return false; - } - if ( - this.selector.target.device?.integration || - this.selector.target.entity?.integration - ) { - if ( - !this._configEntries?.some((entry) => - device.config_entries.includes(entry.entry_id) - ) - ) { - return false; - } - } - return true; - }; - private async _loadConfigEntries() { - this._configEntries = (await getConfigEntries(this.hass)).filter( - (entry) => - entry.domain === this.selector.target.device?.integration || - entry.domain === this.selector.target.entity?.integration + const deviceIntegrations = + this._entitySources && this._entities + ? this._deviceIntegrationLookup(this._entitySources, this._entities) + : undefined; + + return filterSelectorDevices( + this.selector.target.device, + device, + deviceIntegrations ); - } + }; static get styles(): CSSResultGroup { return css` diff --git a/src/data/device_registry.ts b/src/data/device_registry.ts index 63b050c856..72601269c0 100644 --- a/src/data/device_registry.ts +++ b/src/data/device_registry.ts @@ -1,10 +1,11 @@ import { Connection, createCollection } from "home-assistant-js-websocket"; -import { Store } from "home-assistant-js-websocket/dist/store"; +import type { Store } from "home-assistant-js-websocket/dist/store"; import { computeStateName } from "../common/entity/compute_state_name"; import { caseInsensitiveStringCompare } from "../common/string/compare"; import { debounce } from "../common/util/debounce"; -import { HomeAssistant } from "../types"; -import { EntityRegistryEntry } from "./entity_registry"; +import type { HomeAssistant } from "../types"; +import type { EntityRegistryEntry } from "./entity_registry"; +import type { EntitySources } from "./entity_sources"; export interface DeviceRegistryEntry { id: string; @@ -142,3 +143,23 @@ export const getDeviceEntityLookup = ( } return deviceEntityLookup; }; + +export const getDeviceIntegrationLookup = ( + entitySources: EntitySources, + entities: EntityRegistryEntry[] +): Record => { + const deviceIntegrations: Record = {}; + + for (const entity of entities) { + const source = entitySources[entity.entity_id]; + if (!source?.domain || entity.device_id === null) { + continue; + } + + if (!deviceIntegrations[entity.device_id!]) { + deviceIntegrations[entity.device_id!] = []; + } + deviceIntegrations[entity.device_id!].push(source.domain); + } + return deviceIntegrations; +}; diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 00ff94b00d..65f451ab26 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -160,3 +160,16 @@ export const sortEntityRegistryByName = (entries: EntityRegistryEntry[]) => entries.sort((entry1, entry2) => caseInsensitiveStringCompare(entry1.name || "", entry2.name || "") ); + +export const getEntityPlatformLookup = ( + entities: EntityRegistryEntry[] +): Record => { + const entityLookup = {}; + for (const confEnt of entities) { + if (!confEnt.platform) { + continue; + } + entityLookup[confEnt.entity_id] = confEnt.platform; + } + return entityLookup; +}; diff --git a/src/data/selector.ts b/src/data/selector.ts index d0f3b0e61c..1b3997b6de 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -1,3 +1,8 @@ +import type { HassEntity } from "home-assistant-js-websocket"; +import { computeStateDomain } from "../common/entity/compute_state_domain"; +import type { DeviceRegistryEntry } from "./device_registry"; +import type { EntitySources } from "./entity_sources"; + export type Selector = | ActionSelector | AddonSelector @@ -35,18 +40,22 @@ export interface AddonSelector { }; } +export interface SelectorDevice { + integration?: DeviceSelector["device"]["integration"]; + manufacturer?: DeviceSelector["device"]["manufacturer"]; + model?: DeviceSelector["device"]["model"]; +} + +export interface SelectorEntity { + integration?: EntitySelector["entity"]["integration"]; + domain?: EntitySelector["entity"]["domain"]; + device_class?: EntitySelector["entity"]["device_class"]; +} + export interface AreaSelector { area: { - entity?: { - integration?: EntitySelector["entity"]["integration"]; - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; - }; - device?: { - integration?: DeviceSelector["device"]["integration"]; - manufacturer?: DeviceSelector["device"]["manufacturer"]; - model?: DeviceSelector["device"]["model"]; - }; + entity?: SelectorEntity; + device?: SelectorDevice; multiple?: boolean; }; } @@ -89,10 +98,7 @@ export interface DeviceSelector { integration?: string; manufacturer?: string; model?: string; - entity?: { - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; - }; + entity?: SelectorEntity; multiple?: boolean; }; } @@ -201,16 +207,8 @@ export interface StringSelector { export interface TargetSelector { target: { - entity?: { - integration?: EntitySelector["entity"]["integration"]; - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; - }; - device?: { - integration?: DeviceSelector["device"]["integration"]; - manufacturer?: DeviceSelector["device"]["manufacturer"]; - model?: DeviceSelector["device"]["model"]; - }; + entity?: SelectorEntity; + device?: SelectorDevice; }; } @@ -227,3 +225,69 @@ export interface TimeSelector { // eslint-disable-next-line @typescript-eslint/ban-types time: {}; } + +export const filterSelectorDevices = ( + filterDevice: SelectorDevice, + device: DeviceRegistryEntry, + deviceIntegrationLookup: Record | undefined +): boolean => { + const { + manufacturer: filterManufacturer, + model: filterModel, + integration: filterIntegration, + } = filterDevice; + + if (filterManufacturer && device.manufacturer !== filterManufacturer) { + return false; + } + + if (filterModel && device.model !== filterModel) { + return false; + } + + if (filterIntegration && deviceIntegrationLookup) { + if (!deviceIntegrationLookup?.[device.id]?.includes(filterIntegration)) { + return false; + } + } + return true; +}; + +export const filterSelectorEntities = ( + filterEntity: SelectorEntity, + entity: HassEntity, + entitySources?: EntitySources +): boolean => { + const { + domain: filterDomain, + device_class: filterDeviceClass, + integration: filterIntegration, + } = filterEntity; + + if (filterDomain) { + const entityDomain = computeStateDomain(entity); + if ( + Array.isArray(filterDomain) + ? !filterDomain.includes(entityDomain) + : entityDomain !== filterDomain + ) { + return false; + } + } + + if ( + filterDeviceClass && + entity.attributes.device_class !== filterDeviceClass + ) { + return false; + } + + if ( + filterIntegration && + entitySources?.[entity.entity_id]?.domain !== filterIntegration + ) { + return false; + } + + return true; +};