Add filter options to entity and device selectors (#15302)

This commit is contained in:
Paul Bottein 2023-02-20 14:30:05 +01:00 committed by GitHub
parent cf377558ae
commit c4160e8368
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 331 additions and 188 deletions

View File

@ -2,6 +2,7 @@ type NonUndefined<T> = T extends undefined ? never : T;
export function ensureArray(value: undefined): undefined;
export function ensureArray<T>(value: T | T[]): NonUndefined<T>[];
export function ensureArray<T>(value: T | readonly T[]): NonUndefined<T>[];
export function ensureArray(value) {
if (value === undefined || Array.isArray(value)) {
return value;

View File

@ -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<Device> = (item) => html`<mwc-list-item
.twoline=${!!item.area}
>
@ -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
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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)
);
};
}

View File

@ -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}
></ha-devices-picker>
@ -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)
);
};
}

View File

@ -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`<ha-entity-picker
.hass=${this.hass}
@ -64,7 +76,7 @@ export class HaEntitySelector extends LitElement {
super.updated(changedProps);
if (
changedProps.has("selector") &&
this.selector.entity?.integration &&
this._hasIntegration(this.selector) &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
@ -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)
);
};
}

View File

@ -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}
></ha-target-picker>`;
}
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)
);
};

View File

@ -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,

View File

@ -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) {
<div class="mdc-chip-set items">
${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;
}

View File

@ -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<DeviceSelector["device"]>["integration"];
manufacturer?: NonNullable<DeviceSelector["device"]>["manufacturer"];
model?: NonNullable<DeviceSelector["device"]>["model"];
}
export interface SelectorEntity {
integration?: NonNullable<EntitySelector["entity"]>["integration"];
domain?: NonNullable<EntitySelector["entity"]>["domain"];
device_class?: NonNullable<EntitySelector["entity"]>["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<string, string[]> | 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,
};
};