mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add filter options to entity and device selectors (#15302)
This commit is contained in:
parent
cf377558ae
commit
c4160e8368
@ -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;
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user