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(value: undefined): undefined;
export function ensureArray<T>(value: T | T[]): NonUndefined<T>[]; export function ensureArray<T>(value: T | T[]): NonUndefined<T>[];
export function ensureArray<T>(value: T | readonly T[]): NonUndefined<T>[];
export function ensureArray(value) { export function ensureArray(value) {
if (value === undefined || Array.isArray(value)) { if (value === undefined || Array.isArray(value)) {
return value; return value;

View File

@ -1,5 +1,5 @@
import "@material/mwc-list/mwc-list-item"; 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 { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@ -37,6 +37,8 @@ export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry device: DeviceRegistryEntry
) => boolean; ) => boolean;
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<mwc-list-item const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<mwc-list-item
.twoline=${!!item.area} .twoline=${!!item.area}
> >
@ -94,6 +96,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: HaDevicePickerEntityFilterFunc;
@property({ type: Boolean }) public disabled?: boolean; @property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean; @property({ type: Boolean }) public required?: boolean;
@ -113,6 +117,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
excludeDomains: this["excludeDomains"], excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"], includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"], deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
excludeDevices: this["excludeDevices"] excludeDevices: this["excludeDevices"]
): Device[] => { ): Device[] => {
if (!devices.length) { if (!devices.length) {
@ -127,7 +132,12 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
const deviceEntityLookup: DeviceEntityLookup = {}; const deviceEntityLookup: DeviceEntityLookup = {};
if (includeDomains || excludeDomains || includeDeviceClasses) { if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
entityFilter
) {
for (const entity of entities) { for (const entity of entities) {
if (!entity.device_id) { if (!entity.device_id) {
continue; 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) { if (deviceFilter) {
inputDevices = inputDevices.filter( inputDevices = inputDevices.filter(
(device) => (device) =>
@ -274,6 +300,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
this.excludeDomains, this.excludeDomains,
this.includeDeviceClasses, this.includeDeviceClasses,
this.deviceFilter, this.deviceFilter,
this.entityFilter,
this.excludeDevices this.excludeDevices
); );
} }

View File

@ -1,4 +1,6 @@
import "@material/mwc-list/mwc-list-item";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
@ -83,7 +85,7 @@ export class HaAreaPicker extends LitElement {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean; @property() public entityFilter?: (entity: HassEntity) => boolean;
@property({ type: Boolean }) public disabled?: boolean; @property({ type: Boolean }) public disabled?: boolean;
@ -135,7 +137,12 @@ export class HaAreaPicker extends LitElement {
let inputDevices: DeviceRegistryEntry[] | undefined; let inputDevices: DeviceRegistryEntry[] | undefined;
let inputEntities: EntityRegistryEntry[] | undefined; let inputEntities: EntityRegistryEntry[] | undefined;
if (includeDomains || excludeDomains || includeDeviceClasses) { if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
entityFilter
) {
for (const entity of entities) { for (const entity of entities) {
if (!entity.device_id) { if (!entity.device_id) {
continue; continue;
@ -145,16 +152,9 @@ export class HaAreaPicker extends LitElement {
} }
deviceEntityLookup[entity.device_id].push(entity); deviceEntityLookup[entity.device_id].push(entity);
} }
}
inputDevices = devices; inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id); inputEntities = entities.filter((entity) => entity.area_id);
} else {
if (deviceFilter) {
inputDevices = devices;
}
if (entityFilter) {
inputEntities = entities.filter((entity) => entity.area_id);
}
}
if (includeDomains) { if (includeDomains) {
inputDevices = inputDevices!.filter((device) => { inputDevices = inputDevices!.filter((device) => {
@ -218,9 +218,23 @@ export class HaAreaPicker extends LitElement {
} }
if (entityFilter) { if (entityFilter) {
inputEntities = inputEntities!.filter((entity) => inputDevices = inputDevices!.filter((device) => {
entityFilter!(entity) 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; let outputAreas = areas;

View File

@ -1,7 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { EntityRegistryEntry } from "../data/entity_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
@ -48,7 +48,7 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean; @property() public entityFilter?: (entity: HassEntity) => boolean;
@property({ attribute: "picked-area-label" }) @property({ attribute: "picked-area-label" })
public pickedAreaLabel?: string; 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 { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device_registry"; import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry"; import { getDeviceIntegrationLookup } from "../../data/device_registry";
import { 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 { protected updated(changedProperties: PropertyValues): void {
if ( if (
changedProperties.has("selector") && changedProperties.has("selector") &&
(this.selector.area?.device?.integration || this._hasIntegration(this.selector) &&
this.selector.area?.entity?.integration) &&
!this._entitySources !this._entitySources
) { ) {
fetchEntitySourcesWithCache(this.hass).then((sources) => { fetchEntitySourcesWithCache(this.hass).then((sources) => {
@ -66,11 +77,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if ( if (this._hasIntegration(this.selector) && !this._entitySources) {
(this.selector.area?.device?.integration ||
this.selector.area?.entity?.integration) &&
!this._entitySources
) {
return html``; return html``;
} }
@ -110,10 +117,8 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
return true; return true;
} }
return filterSelectorEntities( return ensureArray(this.selector.area.entity).some((filter) =>
this.selector.area.entity, filterSelectorEntities(filter, entity, this._entitySources)
entity,
this._entitySources
); );
}; };
@ -127,10 +132,8 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
? this._deviceIntegrationLookup(this._entitySources, this._entities) ? this._deviceIntegrationLookup(this._entitySources, this._entities)
: undefined; : undefined;
return filterSelectorDevices( return ensureArray(this.selector.area.device).some((filter) =>
this.selector.area.device, filterSelectorDevices(filter, device, deviceIntegrations)
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 { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device_registry"; import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry"; import { getDeviceIntegrationLookup } from "../../data/device_registry";
import { import {
@ -13,7 +14,10 @@ import {
fetchEntitySourcesWithCache, fetchEntitySourcesWithCache,
} from "../../data/entity_sources"; } from "../../data/entity_sources";
import type { DeviceSelector } from "../../data/selector"; import type { DeviceSelector } from "../../data/selector";
import { filterSelectorDevices } from "../../data/selector"; import {
filterSelectorDevices,
filterSelectorEntities,
} from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../device/ha-device-picker"; 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 { protected updated(changedProperties): void {
super.updated(changedProperties); super.updated(changedProperties);
if ( if (
changedProperties.has("selector") && changedProperties.has("selector") &&
this.selector.device?.integration && this._hasIntegration(this.selector) &&
!this._entitySources !this._entitySources
) { ) {
fetchEntitySourcesWithCache(this.hass).then((sources) => { fetchEntitySourcesWithCache(this.hass).then((sources) => {
@ -63,7 +80,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
} }
protected render() { protected render() {
if (this.selector.device?.integration && !this._entitySources) { if (this._hasIntegration(this.selector) && !this._entitySources) {
return html``; return html``;
} }
@ -75,12 +92,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
.label=${this.label} .label=${this.label}
.helper=${this.helper} .helper=${this.helper}
.deviceFilter=${this._filterDevices} .deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device?.entity?.device_class .entityFilter=${this._filterEntities}
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device?.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
allow-custom-entity allow-custom-entity
@ -95,12 +107,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
.value=${this.value} .value=${this.value}
.helper=${this.helper} .helper=${this.helper}
.deviceFilter=${this._filterDevices} .deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device.entity?.device_class .entityFilter=${this._filterEntities}
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
></ha-devices-picker> ></ha-devices-picker>
@ -108,18 +115,25 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
} }
private _filterDevices = (device: DeviceRegistryEntry): boolean => { private _filterDevices = (device: DeviceRegistryEntry): boolean => {
if (!this.selector.device?.filter) {
return true;
}
const deviceIntegrations = const deviceIntegrations =
this._entitySources && this._entities this._entitySources && this._entities
? this._deviceIntegrationLookup(this._entitySources, this._entities) ? this._deviceIntegrationLookup(this._entitySources, this._entities)
: undefined; : 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 true;
} }
return filterSelectorDevices( return ensureArray(this.selector.device.entity).some((filter) =>
this.selector.device, filterSelectorEntities(filter, entity, this._entitySources)
device,
deviceIntegrations
); );
}; };
} }

View File

@ -1,6 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues } from "lit"; import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import { import {
EntitySources, EntitySources,
fetchEntitySourcesWithCache, fetchEntitySourcesWithCache,
@ -29,7 +30,18 @@ export class HaEntitySelector extends LitElement {
@property({ type: Boolean }) public required = true; @property({ type: Boolean }) public required = true;
private _hasIntegration(selector: EntitySelector) {
return (
selector.entity?.filter &&
ensureArray(selector.entity.filter).some((filter) => filter.integration)
);
}
protected render() { protected render() {
if (this._hasIntegration(this.selector) && !this._entitySources) {
return html``;
}
if (!this.selector.entity?.multiple) { if (!this.selector.entity?.multiple) {
return html`<ha-entity-picker return html`<ha-entity-picker
.hass=${this.hass} .hass=${this.hass}
@ -64,7 +76,7 @@ export class HaEntitySelector extends LitElement {
super.updated(changedProps); super.updated(changedProps);
if ( if (
changedProps.has("selector") && changedProps.has("selector") &&
this.selector.entity?.integration && this._hasIntegration(this.selector) &&
!this._entitySources !this._entitySources
) { ) {
fetchEntitySourcesWithCache(this.hass).then((sources) => { fetchEntitySourcesWithCache(this.hass).then((sources) => {
@ -74,13 +86,11 @@ export class HaEntitySelector extends LitElement {
} }
private _filterEntities = (entity: HassEntity): boolean => { private _filterEntities = (entity: HassEntity): boolean => {
if (!this.selector?.entity) { if (!this.selector?.entity?.filter) {
return true; return true;
} }
return filterSelectorEntities( return ensureArray(this.selector.entity.filter).some((filter) =>
this.selector.entity, filterSelectorEntities(filter, entity, this._entitySources)
entity,
this._entitySources
); );
}; };
} }

View File

@ -14,7 +14,6 @@ import {
DeviceRegistryEntry, DeviceRegistryEntry,
getDeviceIntegrationLookup, getDeviceIntegrationLookup,
} from "../../data/device_registry"; } from "../../data/device_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { import {
EntitySources, EntitySources,
fetchEntitySourcesWithCache, fetchEntitySourcesWithCache,
@ -45,12 +44,24 @@ export class HaTargetSelector extends LitElement {
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); 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 { protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties); super.updated(changedProperties);
if ( if (
changedProperties.has("selector") && changedProperties.has("selector") &&
(this.selector.target?.device?.integration || this._hasIntegration(this.selector) &&
this.selector.target?.entity?.integration) &&
!this._entitySources !this._entitySources
) { ) {
fetchEntitySourcesWithCache(this.hass).then((sources) => { fetchEntitySourcesWithCache(this.hass).then((sources) => {
@ -60,11 +71,7 @@ export class HaTargetSelector extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if ( if (this._hasIntegration(this.selector) && !this._entitySources) {
(this.selector.target?.device?.integration ||
this.selector.target?.entity?.integration) &&
!this._entitySources
) {
return html``; return html``;
} }
@ -73,39 +80,21 @@ export class HaTargetSelector extends LitElement {
.value=${this.value} .value=${this.value}
.helper=${this.helper} .helper=${this.helper}
.deviceFilter=${this._filterDevices} .deviceFilter=${this._filterDevices}
.entityFilter=${this._filterStates} .entityFilter=${this._filterEntities}
.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}
.disabled=${this.disabled} .disabled=${this.disabled}
></ha-target-picker>`; ></ha-target-picker>`;
} }
private _filterStates = (entity: HassEntity): boolean => { private _filterEntities = (entity: HassEntity): boolean => {
if (!this.selector.target?.entity) { if (!this.selector.target?.entity) {
return true; return true;
} }
return filterSelectorEntities( return ensureArray(this.selector.target.entity).some((filter) =>
this.selector.target.entity, filterSelectorEntities(filter, entity, this._entitySources)
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 => { private _filterDevices = (device: DeviceRegistryEntry): boolean => {
if (!this.selector.target?.device) { if (!this.selector.target?.device) {
return true; return true;
@ -118,10 +107,8 @@ export class HaTargetSelector extends LitElement {
) )
: undefined; : undefined;
return filterSelectorDevices( return ensureArray(this.selector.target.device).some((filter) =>
this.selector.target.device, filterSelectorDevices(filter, device, deviceIntegrations)
device,
deviceIntegrations
); );
}; };

View File

@ -1,7 +1,12 @@
import { html, LitElement, PropertyValues } from "lit"; import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../common/dom/dynamic-element-directive"; 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"; import type { HomeAssistant } from "../../types";
const LOAD_ELEMENTS = { 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() { protected render() {
return html` return html`
${dynamicElement(`ha-selector-${this._type}`, { ${dynamicElement(`ha-selector-${this._type}`, {
hass: this.hass, hass: this.hass,
name: this.name, name: this.name,
selector: this.selector, selector: this._handleLegacySelector(this.selector),
value: this.value, value: this.value,
label: this.label, label: this.label,
placeholder: this.placeholder, placeholder: this.placeholder,

View File

@ -9,32 +9,19 @@ import {
mdiUnfoldMoreVertical, mdiUnfoldMoreVertical,
} from "@mdi/js"; } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { import { HassEntity, HassServiceTarget } from "home-assistant-js-websocket";
HassEntity,
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, unsafeCSS } from "lit"; import { css, CSSResultGroup, html, LitElement, unsafeCSS } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event";
import { ensureArray } from "../common/array/ensure-array"; import { ensureArray } from "../common/array/ensure-array";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../data/area_registry";
import { import {
computeDeviceName, computeDeviceName,
DeviceRegistryEntry, DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../data/device_registry"; } from "../data/device_registry";
import { import { EntityRegistryEntry } from "../data/entity_registry";
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../data/entity_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./device/ha-device-picker"; import "./device/ha-device-picker";
import type { HaDevicePickerDeviceFilterFunc } from "./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"; import "./ha-svg-icon";
@customElement("ha-target-picker") @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 hass!: HomeAssistant;
@property({ attribute: false }) public value?: HassServiceTarget; @property({ attribute: false }) public value?: HassServiceTarget;
@ -73,52 +60,17 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityRegFilter?: (entity: EntityRegistryEntry) => boolean;
@property() public entityFilter?: HaEntityPickerEntityFilterFunc; @property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean, reflect: true }) public disabled = false; @property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public horizontal = 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"; @state() private _addMode?: "area_id" | "entity_id" | "device_id";
@query("#input") private _inputElement?; @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() { protected render() {
if (!this._areas || !this._devices || !this._entities) {
return html``;
}
return html` return html`
${this.horizontal ${this.horizontal
? html` ? html`
@ -141,7 +93,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
<div class="mdc-chip-set items"> <div class="mdc-chip-set items">
${this.value?.area_id ${this.value?.area_id
? ensureArray(this.value.area_id).map((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( return this._renderChip(
"area_id", "area_id",
area_id, area_id,
@ -153,7 +105,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
: ""} : ""}
${this.value?.device_id ${this.value?.device_id
? ensureArray(this.value.device_id).map((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( return this._renderChip(
"device_id", "device_id",
device_id, device_id,
@ -342,7 +294,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
)} )}
no-add no-add
.deviceFilter=${this.deviceFilter} .deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityRegFilter} .entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
.excludeAreas=${ensureArray(this.value?.area_id)} .excludeAreas=${ensureArray(this.value?.area_id)}
@ -359,6 +311,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
"ui.components.target-picker.add_device_id" "ui.components.target-picker.add_device_id"
)} )}
.deviceFilter=${this.deviceFilter} .deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
.excludeDevices=${ensureArray(this.value?.device_id)} .excludeDevices=${ensureArray(this.value?.device_id)}
@ -419,7 +372,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
const newDevices: string[] = []; const newDevices: string[] = [];
const newEntities: string[] = []; const newEntities: string[] = [];
if (target.type === "area_id") { if (target.type === "area_id") {
Object.values(this._devices!).forEach((device) => { Object.values(this.hass.devices).forEach((device) => {
if ( if (
device.area_id === target.id && device.area_id === target.id &&
!this.value!.device_id?.includes(device.id) && !this.value!.device_id?.includes(device.id) &&
@ -428,7 +381,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
newDevices.push(device.id); newDevices.push(device.id);
} }
}); });
this._entities!.forEach((entity) => { Object.values(this.hass.entities).forEach((entity) => {
if ( if (
entity.area_id === target.id && entity.area_id === target.id &&
!this.value!.entity_id?.includes(entity.entity_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") { } else if (target.type === "device_id") {
this._entities!.forEach((entity) => { Object.values(this.hass.entities).forEach((entity) => {
if ( if (
entity.device_id === target.id && entity.device_id === target.id &&
!this.value!.entity_id?.includes(entity.entity_id) && !this.value!.entity_id?.includes(entity.entity_id) &&
@ -502,9 +455,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
} }
private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean { private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean {
const devEntities = this._entities?.filter( const devEntities = Object.values(this.hass.entities).filter(
(entity) => entity.device_id === device.id (entity) => entity.device_id === device.id
); );
if (this.includeDomains) { if (this.includeDomains) {
if (!devEntities || !devEntities.length) { if (!devEntities || !devEntities.length) {
return false; return false;
@ -541,7 +495,23 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
} }
if (this.deviceFilter) { 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; return true;
} }
@ -550,6 +520,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
if (entity.entity_category) { if (entity.entity_category) {
return false; return false;
} }
if ( if (
this.includeDomains && this.includeDomains &&
!this.includeDomains.includes(computeDomain(entity.entity_id)) !this.includeDomains.includes(computeDomain(entity.entity_id))
@ -568,8 +539,15 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return false; 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; return true;
} }

View File

@ -16,8 +16,10 @@ export type Selector =
| DateSelector | DateSelector
| DateTimeSelector | DateTimeSelector
| DeviceSelector | DeviceSelector
| LegacyDeviceSelector
| DurationSelector | DurationSelector
| EntitySelector | EntitySelector
| LegacyEntitySelector
| FileSelector | FileSelector
| IconSelector | IconSelector
| LocationSelector | LocationSelector
@ -48,22 +50,10 @@ export interface AddonSelector {
} | null; } | 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 { export interface AreaSelector {
area: { area: {
entity?: SelectorEntity; entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
device?: SelectorDevice; device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
multiple?: boolean; multiple?: boolean;
} | null; } | null;
} }
@ -108,33 +98,77 @@ export interface DateTimeSelector {
datetime: {} | null; datetime: {} | null;
} }
export interface DeviceSelector { interface DeviceSelectorFilter {
device: {
integration?: string; integration?: string;
manufacturer?: string; manufacturer?: string;
model?: string; model?: string;
entity?: SelectorEntity; }
export interface DeviceSelector {
device: {
filter?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
multiple?: boolean; multiple?: boolean;
} | null; } | 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 { export interface DurationSelector {
duration: { duration: {
enable_day?: boolean; enable_day?: boolean;
} | null; } | null;
} }
export interface EntitySelector { interface EntitySelectorFilter {
entity: {
integration?: string; integration?: string;
domain?: string | readonly string[]; domain?: string | readonly string[];
device_class?: string; device_class?: string | readonly string[];
}
export interface EntitySelector {
entity: {
multiple?: boolean; multiple?: boolean;
include_entities?: string[]; include_entities?: string[];
exclude_entities?: string[]; exclude_entities?: string[];
filter?: EntitySelectorFilter | readonly EntitySelectorFilter[];
} | null; } | 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 { export interface StatisticSelector {
statistic: { statistic: {
device_class?: string; device_class?: string;
@ -250,8 +284,8 @@ export interface StringSelector {
export interface TargetSelector { export interface TargetSelector {
target: { target: {
entity?: SelectorEntity; entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
device?: SelectorDevice; device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
} | null; } | null;
} }
@ -281,7 +315,7 @@ export interface UiColorSelector {
} }
export const filterSelectorDevices = ( export const filterSelectorDevices = (
filterDevice: SelectorDevice, filterDevice: DeviceSelectorFilter,
device: DeviceRegistryEntry, device: DeviceRegistryEntry,
deviceIntegrationLookup: Record<string, string[]> | undefined deviceIntegrationLookup: Record<string, string[]> | undefined
): boolean => { ): boolean => {
@ -308,7 +342,7 @@ export const filterSelectorDevices = (
}; };
export const filterSelectorEntities = ( export const filterSelectorEntities = (
filterEntity: SelectorEntity, filterEntity: EntitySelectorFilter,
entity: HassEntity, entity: HassEntity,
entitySources?: EntitySources entitySources?: EntitySources
): boolean => { ): boolean => {
@ -329,12 +363,16 @@ export const filterSelectorEntities = (
} }
} }
if (filterDeviceClass) {
const entityDeviceClass = entity.attributes.device_class;
if ( if (
filterDeviceClass && entityDeviceClass && Array.isArray(filterDeviceClass)
entity.attributes.device_class !== filterDeviceClass ? !filterDeviceClass.includes(entityDeviceClass)
: entityDeviceClass !== filterDeviceClass
) { ) {
return false; return false;
} }
}
if ( if (
filterIntegration && filterIntegration &&
@ -345,3 +383,59 @@ export const filterSelectorEntities = (
return true; 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,
};
};