diff --git a/src/common/array/ensure-array.ts b/src/common/array/ensure-array.ts index cdc9ea89ed..70869a91f1 100644 --- a/src/common/array/ensure-array.ts +++ b/src/common/array/ensure-array.ts @@ -2,6 +2,7 @@ type NonUndefined = T extends undefined ? never : T; export function ensureArray(value: undefined): undefined; export function ensureArray(value: T | T[]): NonUndefined[]; +export function ensureArray(value: T | readonly T[]): NonUndefined[]; export function ensureArray(value) { if (value === undefined || Array.isArray(value)) { return value; diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 95a3332fe0..715a42c1db 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -1,5 +1,5 @@ import "@material/mwc-list/mwc-list-item"; -import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -37,6 +37,8 @@ export type HaDevicePickerDeviceFilterFunc = ( device: DeviceRegistryEntry ) => boolean; +export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean; + const rowRenderer: ComboBoxLitRenderer = (item) => html` @@ -94,6 +96,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; + @property() public entityFilter?: HaDevicePickerEntityFilterFunc; + @property({ type: Boolean }) public disabled?: boolean; @property({ type: Boolean }) public required?: boolean; @@ -113,6 +117,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { excludeDomains: this["excludeDomains"], includeDeviceClasses: this["includeDeviceClasses"], deviceFilter: this["deviceFilter"], + entityFilter: this["entityFilter"], excludeDevices: this["excludeDevices"] ): Device[] => { if (!devices.length) { @@ -127,7 +132,12 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { const deviceEntityLookup: DeviceEntityLookup = {}; - if (includeDomains || excludeDomains || includeDeviceClasses) { + if ( + includeDomains || + excludeDomains || + includeDeviceClasses || + entityFilter + ) { for (const entity of entities) { if (!entity.device_id) { continue; @@ -198,6 +208,22 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { }); } + if (entityFilter) { + 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 entityFilter(stateObj); + }); + }); + } + if (deviceFilter) { inputDevices = inputDevices.filter( (device) => @@ -274,6 +300,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { this.excludeDomains, this.includeDeviceClasses, this.deviceFilter, + this.entityFilter, this.excludeDevices ); } diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index 9557836e4a..0f47ed05b9 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -1,4 +1,6 @@ +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"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; @@ -83,7 +85,7 @@ export class HaAreaPicker extends LitElement { @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; - @property() public entityFilter?: (entity: EntityRegistryEntry) => boolean; + @property() public entityFilter?: (entity: HassEntity) => boolean; @property({ type: Boolean }) public disabled?: boolean; @@ -135,7 +137,12 @@ export class HaAreaPicker extends LitElement { let inputDevices: DeviceRegistryEntry[] | undefined; let inputEntities: EntityRegistryEntry[] | undefined; - if (includeDomains || excludeDomains || includeDeviceClasses) { + if ( + includeDomains || + excludeDomains || + includeDeviceClasses || + entityFilter + ) { for (const entity of entities) { if (!entity.device_id) { continue; @@ -145,16 +152,9 @@ export class HaAreaPicker extends LitElement { } deviceEntityLookup[entity.device_id].push(entity); } - inputDevices = devices; - inputEntities = entities.filter((entity) => entity.area_id); - } else { - if (deviceFilter) { - inputDevices = devices; - } - if (entityFilter) { - inputEntities = entities.filter((entity) => entity.area_id); - } } + inputDevices = devices; + inputEntities = entities.filter((entity) => entity.area_id); if (includeDomains) { inputDevices = inputDevices!.filter((device) => { @@ -218,9 +218,23 @@ export class HaAreaPicker extends LitElement { } if (entityFilter) { - inputEntities = inputEntities!.filter((entity) => - entityFilter!(entity) - ); + 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 entityFilter(stateObj); + }); + }); + inputEntities = inputEntities!.filter((entity) => { + const stateObj = this.hass.states[entity.entity_id]; + return entityFilter!(stateObj); + }); } let outputAreas = areas; diff --git a/src/components/ha-areas-picker.ts b/src/components/ha-areas-picker.ts index 2b162e56d7..e4f2948e98 100644 --- a/src/components/ha-areas-picker.ts +++ b/src/components/ha-areas-picker.ts @@ -1,7 +1,7 @@ +import { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; -import type { EntityRegistryEntry } from "../data/entity_registry"; import { SubscribeMixin } from "../mixins/subscribe-mixin"; import type { HomeAssistant } from "../types"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; @@ -48,7 +48,7 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) { @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; - @property() public entityFilter?: (entity: EntityRegistryEntry) => boolean; + @property() public entityFilter?: (entity: HassEntity) => boolean; @property({ attribute: "picked-area-label" }) public pickedAreaLabel?: string; diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index 704a36ed2c..e8c2539295 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -2,6 +2,7 @@ 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 { ensureArray } from "../../common/array/ensure-array"; import type { DeviceRegistryEntry } from "../../data/device_registry"; import { getDeviceIntegrationLookup } from "../../data/device_registry"; import { @@ -52,11 +53,21 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { ]; } + private _hasIntegration(selector: AreaSelector) { + return ( + (selector.area?.entity && + ensureArray(selector.area.entity).some( + (filter) => filter.integration + )) || + (selector.area?.device && + ensureArray(selector.area.device).some((device) => device.integration)) + ); + } + protected updated(changedProperties: PropertyValues): void { if ( changedProperties.has("selector") && - (this.selector.area?.device?.integration || - this.selector.area?.entity?.integration) && + this._hasIntegration(this.selector) && !this._entitySources ) { fetchEntitySourcesWithCache(this.hass).then((sources) => { @@ -66,11 +77,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { } protected render(): TemplateResult { - if ( - (this.selector.area?.device?.integration || - this.selector.area?.entity?.integration) && - !this._entitySources - ) { + if (this._hasIntegration(this.selector) && !this._entitySources) { return html``; } @@ -110,10 +117,8 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { return true; } - return filterSelectorEntities( - this.selector.area.entity, - entity, - this._entitySources + return ensureArray(this.selector.area.entity).some((filter) => + filterSelectorEntities(filter, entity, this._entitySources) ); }; @@ -127,10 +132,8 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { ? this._deviceIntegrationLookup(this._entitySources, this._entities) : undefined; - return filterSelectorDevices( - this.selector.area.device, - device, - deviceIntegrations + return ensureArray(this.selector.area.device).some((filter) => + filterSelectorDevices(filter, device, deviceIntegrations) ); }; } diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index e56808b766..db080201c0 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -1,7 +1,8 @@ -import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { ensureArray } from "../../common/array/ensure-array"; import type { DeviceRegistryEntry } from "../../data/device_registry"; import { getDeviceIntegrationLookup } from "../../data/device_registry"; import { @@ -13,7 +14,10 @@ import { fetchEntitySourcesWithCache, } from "../../data/entity_sources"; import type { DeviceSelector } from "../../data/selector"; -import { filterSelectorDevices } from "../../data/selector"; +import { + filterSelectorDevices, + filterSelectorEntities, +} from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../types"; import "../device/ha-device-picker"; @@ -49,11 +53,24 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) { ]; } + private _hasIntegration(selector: DeviceSelector) { + return ( + (selector.device?.filter && + ensureArray(selector.device.filter).some( + (filter) => filter.integration + )) || + (selector.device?.entity && + ensureArray(selector.device.entity).some( + (device) => device.integration + )) + ); + } + protected updated(changedProperties): void { super.updated(changedProperties); if ( changedProperties.has("selector") && - this.selector.device?.integration && + this._hasIntegration(this.selector) && !this._entitySources ) { fetchEntitySourcesWithCache(this.hass).then((sources) => { @@ -63,7 +80,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) { } protected render() { - if (this.selector.device?.integration && !this._entitySources) { + if (this._hasIntegration(this.selector) && !this._entitySources) { return html``; } @@ -75,12 +92,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) { .label=${this.label} .helper=${this.helper} .deviceFilter=${this._filterDevices} - .includeDeviceClasses=${this.selector.device?.entity?.device_class - ? [this.selector.device.entity.device_class] - : undefined} - .includeDomains=${this.selector.device?.entity?.domain - ? [this.selector.device.entity.domain] - : undefined} + .entityFilter=${this._filterEntities} .disabled=${this.disabled} .required=${this.required} allow-custom-entity @@ -95,12 +107,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) { .value=${this.value} .helper=${this.helper} .deviceFilter=${this._filterDevices} - .includeDeviceClasses=${this.selector.device.entity?.device_class - ? [this.selector.device.entity.device_class] - : undefined} - .includeDomains=${this.selector.device.entity?.domain - ? [this.selector.device.entity.domain] - : undefined} + .entityFilter=${this._filterEntities} .disabled=${this.disabled} .required=${this.required} > @@ -108,18 +115,25 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) { } private _filterDevices = (device: DeviceRegistryEntry): boolean => { + if (!this.selector.device?.filter) { + return true; + } const deviceIntegrations = this._entitySources && this._entities ? this._deviceIntegrationLookup(this._entitySources, this._entities) : undefined; - if (!this.selector.device) { + return ensureArray(this.selector.device.filter).some((filter) => + filterSelectorDevices(filter, device, deviceIntegrations) + ); + }; + + private _filterEntities = (entity: HassEntity): boolean => { + if (!this.selector.device?.entity) { return true; } - return filterSelectorDevices( - this.selector.device, - device, - deviceIntegrations + return ensureArray(this.selector.device.entity).some((filter) => + filterSelectorEntities(filter, entity, this._entitySources) ); }; } diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index b56ecf1723..6138b274a7 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -1,6 +1,7 @@ import { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { ensureArray } from "../../common/array/ensure-array"; import { EntitySources, fetchEntitySourcesWithCache, @@ -29,7 +30,18 @@ export class HaEntitySelector extends LitElement { @property({ type: Boolean }) public required = true; + private _hasIntegration(selector: EntitySelector) { + return ( + selector.entity?.filter && + ensureArray(selector.entity.filter).some((filter) => filter.integration) + ); + } + protected render() { + if (this._hasIntegration(this.selector) && !this._entitySources) { + return html``; + } + if (!this.selector.entity?.multiple) { return html` { @@ -74,13 +86,11 @@ export class HaEntitySelector extends LitElement { } private _filterEntities = (entity: HassEntity): boolean => { - if (!this.selector?.entity) { + if (!this.selector?.entity?.filter) { return true; } - return filterSelectorEntities( - this.selector.entity, - entity, - this._entitySources + return ensureArray(this.selector.entity.filter).some((filter) => + filterSelectorEntities(filter, entity, this._entitySources) ); }; } diff --git a/src/components/ha-selector/ha-selector-target.ts b/src/components/ha-selector/ha-selector-target.ts index 9853d1552e..6a0b2a4dcc 100644 --- a/src/components/ha-selector/ha-selector-target.ts +++ b/src/components/ha-selector/ha-selector-target.ts @@ -14,7 +14,6 @@ import { DeviceRegistryEntry, getDeviceIntegrationLookup, } from "../../data/device_registry"; -import { EntityRegistryEntry } from "../../data/entity_registry"; import { EntitySources, fetchEntitySourcesWithCache, @@ -45,12 +44,24 @@ export class HaTargetSelector extends LitElement { private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); + private _hasIntegration(selector: TargetSelector) { + return ( + (selector.target?.entity && + ensureArray(selector.target.entity).some( + (filter) => filter.integration + )) || + (selector.target?.device && + ensureArray(selector.target.device).some( + (device) => device.integration + )) + ); + } + protected updated(changedProperties: PropertyValues): void { super.updated(changedProperties); if ( changedProperties.has("selector") && - (this.selector.target?.device?.integration || - this.selector.target?.entity?.integration) && + this._hasIntegration(this.selector) && !this._entitySources ) { fetchEntitySourcesWithCache(this.hass).then((sources) => { @@ -60,11 +71,7 @@ export class HaTargetSelector extends LitElement { } protected render(): TemplateResult { - if ( - (this.selector.target?.device?.integration || - this.selector.target?.entity?.integration) && - !this._entitySources - ) { + if (this._hasIntegration(this.selector) && !this._entitySources) { return html``; } @@ -73,39 +80,21 @@ export class HaTargetSelector extends LitElement { .value=${this.value} .helper=${this.helper} .deviceFilter=${this._filterDevices} - .entityFilter=${this._filterStates} - .entityRegFilter=${this._filterRegEntities} - .includeDeviceClasses=${this.selector.target?.entity?.device_class - ? [this.selector.target?.entity.device_class] - : undefined} - .includeDomains=${this.selector.target?.entity?.domain - ? ensureArray(this.selector.target.entity.domain as string | string[]) - : undefined} + .entityFilter=${this._filterEntities} .disabled=${this.disabled} >`; } - private _filterStates = (entity: HassEntity): boolean => { + private _filterEntities = (entity: HassEntity): boolean => { if (!this.selector.target?.entity) { return true; } - return filterSelectorEntities( - this.selector.target.entity, - entity, - this._entitySources + return ensureArray(this.selector.target.entity).some((filter) => + filterSelectorEntities(filter, entity, this._entitySources) ); }; - private _filterRegEntities = (entity: EntityRegistryEntry): boolean => { - if (this.selector.target?.entity?.integration) { - if (entity.platform !== this.selector.target.entity.integration) { - return false; - } - } - return true; - }; - private _filterDevices = (device: DeviceRegistryEntry): boolean => { if (!this.selector.target?.device) { return true; @@ -118,10 +107,8 @@ export class HaTargetSelector extends LitElement { ) : undefined; - return filterSelectorDevices( - this.selector.target.device, - device, - deviceIntegrations + return ensureArray(this.selector.target.device).some((filter) => + filterSelectorDevices(filter, device, deviceIntegrations) ); }; diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 1f44b3503f..b613245545 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -1,7 +1,12 @@ import { html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { dynamicElement } from "../../common/dom/dynamic-element-directive"; -import type { Selector } from "../../data/selector"; +import { + Selector, + handleLegacyEntitySelector, + handleLegacyDeviceSelector, +} from "../../data/selector"; import type { HomeAssistant } from "../../types"; const LOAD_ELEMENTS = { @@ -75,12 +80,22 @@ export class HaSelector extends LitElement { } } + private _handleLegacySelector = memoizeOne((selector: Selector) => { + if ("entity" in selector) { + return handleLegacyEntitySelector(selector); + } + if ("device" in selector) { + return handleLegacyDeviceSelector(selector); + } + return selector; + }); + protected render() { return html` ${dynamicElement(`ha-selector-${this._type}`, { hass: this.hass, name: this.name, - selector: this.selector, + selector: this._handleLegacySelector(this.selector), value: this.value, label: this.label, placeholder: this.placeholder, diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts index 3aa581b441..599720075a 100644 --- a/src/components/ha-target-picker.ts +++ b/src/components/ha-target-picker.ts @@ -9,32 +9,19 @@ import { mdiUnfoldMoreVertical, } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; -import { - HassEntity, - HassServiceTarget, - UnsubscribeFunc, -} from "home-assistant-js-websocket"; +import { HassEntity, HassServiceTarget } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, unsafeCSS } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { fireEvent } from "../common/dom/fire_event"; import { ensureArray } from "../common/array/ensure-array"; +import { fireEvent } from "../common/dom/fire_event"; import { computeDomain } from "../common/entity/compute_domain"; import { computeStateName } from "../common/entity/compute_state_name"; -import { - AreaRegistryEntry, - subscribeAreaRegistry, -} from "../data/area_registry"; import { computeDeviceName, DeviceRegistryEntry, - subscribeDeviceRegistry, } from "../data/device_registry"; -import { - EntityRegistryEntry, - subscribeEntityRegistry, -} from "../data/entity_registry"; -import { SubscribeMixin } from "../mixins/subscribe-mixin"; +import { EntityRegistryEntry } from "../data/entity_registry"; import { HomeAssistant } from "../types"; import "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; @@ -46,7 +33,7 @@ import "./ha-input-helper-text"; import "./ha-svg-icon"; @customElement("ha-target-picker") -export class HaTargetPicker extends SubscribeMixin(LitElement) { +export class HaTargetPicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public value?: HassServiceTarget; @@ -73,52 +60,17 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; - @property() public entityRegFilter?: (entity: EntityRegistryEntry) => boolean; - @property() public entityFilter?: HaEntityPickerEntityFilterFunc; @property({ type: Boolean, reflect: true }) public disabled = false; @property({ type: Boolean }) public horizontal = false; - @state() private _areas?: { [areaId: string]: AreaRegistryEntry }; - - @state() private _devices?: { - [deviceId: string]: DeviceRegistryEntry; - }; - - @state() private _entities?: EntityRegistryEntry[]; - @state() private _addMode?: "area_id" | "entity_id" | "device_id"; @query("#input") private _inputElement?; - public hassSubscribe(): UnsubscribeFunc[] { - return [ - subscribeAreaRegistry(this.hass.connection!, (areas) => { - const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; - for (const area of areas) { - areaLookup[area.area_id] = area; - } - this._areas = areaLookup; - }), - subscribeDeviceRegistry(this.hass.connection!, (devices) => { - const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {}; - for (const device of devices) { - deviceLookup[device.id] = device; - } - this._devices = deviceLookup; - }), - subscribeEntityRegistry(this.hass.connection!, (entities) => { - this._entities = entities; - }), - ]; - } - protected render() { - if (!this._areas || !this._devices || !this._entities) { - return html``; - } return html` ${this.horizontal ? html` @@ -141,7 +93,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
${this.value?.area_id ? ensureArray(this.value.area_id).map((area_id) => { - const area = this._areas![area_id]; + const area = this.hass.devices![area_id]; return this._renderChip( "area_id", area_id, @@ -153,7 +105,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { : ""} ${this.value?.device_id ? ensureArray(this.value.device_id).map((device_id) => { - const device = this._devices![device_id]; + const device = this.hass.devices![device_id]; return this._renderChip( "device_id", device_id, @@ -342,7 +294,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { )} no-add .deviceFilter=${this.deviceFilter} - .entityFilter=${this.entityRegFilter} + .entityFilter=${this.entityFilter} .includeDeviceClasses=${this.includeDeviceClasses} .includeDomains=${this.includeDomains} .excludeAreas=${ensureArray(this.value?.area_id)} @@ -359,6 +311,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { "ui.components.target-picker.add_device_id" )} .deviceFilter=${this.deviceFilter} + .entityFilter=${this.entityFilter} .includeDeviceClasses=${this.includeDeviceClasses} .includeDomains=${this.includeDomains} .excludeDevices=${ensureArray(this.value?.device_id)} @@ -419,7 +372,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { const newDevices: string[] = []; const newEntities: string[] = []; if (target.type === "area_id") { - Object.values(this._devices!).forEach((device) => { + Object.values(this.hass.devices).forEach((device) => { if ( device.area_id === target.id && !this.value!.device_id?.includes(device.id) && @@ -428,7 +381,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { newDevices.push(device.id); } }); - this._entities!.forEach((entity) => { + Object.values(this.hass.entities).forEach((entity) => { if ( entity.area_id === target.id && !this.value!.entity_id?.includes(entity.entity_id) && @@ -438,7 +391,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { } }); } else if (target.type === "device_id") { - this._entities!.forEach((entity) => { + Object.values(this.hass.entities).forEach((entity) => { if ( entity.device_id === target.id && !this.value!.entity_id?.includes(entity.entity_id) && @@ -502,9 +455,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { } private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean { - const devEntities = this._entities?.filter( + const devEntities = Object.values(this.hass.entities).filter( (entity) => entity.device_id === device.id ); + if (this.includeDomains) { if (!devEntities || !devEntities.length) { return false; @@ -541,7 +495,23 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { } if (this.deviceFilter) { - return this.deviceFilter(device); + if (!this.deviceFilter(device)) { + return false; + } + } + + if (this.entityFilter) { + if ( + !devEntities.some((entity) => { + const stateObj = this.hass.states[entity.entity_id]; + if (!stateObj) { + return false; + } + return this.entityFilter!(stateObj); + }) + ) { + return false; + } } return true; } @@ -550,6 +520,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { if (entity.entity_category) { return false; } + if ( this.includeDomains && !this.includeDomains.includes(computeDomain(entity.entity_id)) @@ -568,8 +539,15 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { return false; } } - if (this.entityRegFilter) { - return this.entityRegFilter(entity); + + if (this.entityFilter) { + const stateObj = this.hass.states[entity.entity_id]; + if (!stateObj) { + return false; + } + if (!this.entityFilter!(stateObj)) { + return false; + } } return true; } diff --git a/src/data/selector.ts b/src/data/selector.ts index e8012b49d1..3ac4661a87 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -16,8 +16,10 @@ export type Selector = | DateSelector | DateTimeSelector | DeviceSelector + | LegacyDeviceSelector | DurationSelector | EntitySelector + | LegacyEntitySelector | FileSelector | IconSelector | LocationSelector @@ -48,22 +50,10 @@ export interface AddonSelector { } | null; } -export interface SelectorDevice { - integration?: NonNullable["integration"]; - manufacturer?: NonNullable["manufacturer"]; - model?: NonNullable["model"]; -} - -export interface SelectorEntity { - integration?: NonNullable["integration"]; - domain?: NonNullable["domain"]; - device_class?: NonNullable["device_class"]; -} - export interface AreaSelector { area: { - entity?: SelectorEntity; - device?: SelectorDevice; + entity?: EntitySelectorFilter | readonly EntitySelectorFilter[]; + device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[]; multiple?: boolean; } | null; } @@ -108,33 +98,77 @@ export interface DateTimeSelector { datetime: {} | null; } +interface DeviceSelectorFilter { + integration?: string; + manufacturer?: string; + model?: string; +} + export interface DeviceSelector { device: { - integration?: string; - manufacturer?: string; - model?: string; - entity?: SelectorEntity; + filter?: DeviceSelectorFilter | readonly DeviceSelectorFilter[]; + entity?: EntitySelectorFilter | readonly EntitySelectorFilter[]; multiple?: boolean; } | null; } +export interface LegacyDeviceSelector { + device: + | DeviceSelector["device"] & { + /** + * @deprecated Use filter instead + */ + integration?: DeviceSelectorFilter["integration"]; + /** + * @deprecated Use filter instead + */ + manufacturer?: DeviceSelectorFilter["manufacturer"]; + /** + * @deprecated Use filter instead + */ + model?: DeviceSelectorFilter["model"]; + }; +} + export interface DurationSelector { duration: { enable_day?: boolean; } | null; } +interface EntitySelectorFilter { + integration?: string; + domain?: string | readonly string[]; + device_class?: string | readonly string[]; +} + export interface EntitySelector { entity: { - integration?: string; - domain?: string | readonly string[]; - device_class?: string; multiple?: boolean; include_entities?: string[]; exclude_entities?: string[]; + filter?: EntitySelectorFilter | readonly EntitySelectorFilter[]; } | null; } +export interface LegacyEntitySelector { + entity: + | EntitySelector["entity"] & { + /** + * @deprecated Use filter instead + */ + integration?: EntitySelectorFilter["integration"]; + /** + * @deprecated Use filter instead + */ + domain?: EntitySelectorFilter["domain"]; + /** + * @deprecated Use filter instead + */ + device_class?: EntitySelectorFilter["device_class"]; + }; +} + export interface StatisticSelector { statistic: { device_class?: string; @@ -250,8 +284,8 @@ export interface StringSelector { export interface TargetSelector { target: { - entity?: SelectorEntity; - device?: SelectorDevice; + entity?: EntitySelectorFilter | readonly EntitySelectorFilter[]; + device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[]; } | null; } @@ -281,7 +315,7 @@ export interface UiColorSelector { } export const filterSelectorDevices = ( - filterDevice: SelectorDevice, + filterDevice: DeviceSelectorFilter, device: DeviceRegistryEntry, deviceIntegrationLookup: Record | undefined ): boolean => { @@ -308,7 +342,7 @@ export const filterSelectorDevices = ( }; export const filterSelectorEntities = ( - filterEntity: SelectorEntity, + filterEntity: EntitySelectorFilter, entity: HassEntity, entitySources?: EntitySources ): boolean => { @@ -329,11 +363,15 @@ export const filterSelectorEntities = ( } } - if ( - filterDeviceClass && - entity.attributes.device_class !== filterDeviceClass - ) { - return false; + if (filterDeviceClass) { + const entityDeviceClass = entity.attributes.device_class; + if ( + entityDeviceClass && Array.isArray(filterDeviceClass) + ? !filterDeviceClass.includes(entityDeviceClass) + : entityDeviceClass !== filterDeviceClass + ) { + return false; + } } if ( @@ -345,3 +383,59 @@ export const filterSelectorEntities = ( return true; }; + +export const handleLegacyEntitySelector = ( + selector: LegacyEntitySelector | EntitySelector +): EntitySelector => { + if (!selector.entity) return { entity: null }; + + if ("filter" in selector.entity) return selector; + + const { domain, integration, device_class, ...rest } = ( + selector as LegacyEntitySelector + ).entity!; + + if (domain || integration || device_class) { + return { + entity: { + ...rest, + filter: { + domain, + integration, + device_class, + }, + }, + }; + } + return { + entity: rest, + }; +}; + +export const handleLegacyDeviceSelector = ( + selector: LegacyDeviceSelector | DeviceSelector +): DeviceSelector => { + if (!selector.device) return { device: null }; + + if ("filter" in selector.device) return selector; + + const { integration, manufacturer, model, ...rest } = ( + selector as LegacyDeviceSelector + ).device!; + + if (integration || manufacturer || model) { + return { + device: { + ...rest, + filter: { + integration, + manufacturer, + model, + }, + }, + }; + } + return { + device: rest, + }; +};