import { mdiPlus, mdiShape } from "@mdi/js"; import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { html, LitElement, nothing, type PropertyValues } from "lit"; import { customElement, property, query } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeEntityNameList } from "../../common/entity/compute_entity_name_display"; import { isValidEntityId } from "../../common/entity/valid_entity_id"; import { computeRTL } from "../../common/util/compute_rtl"; import type { HaEntityPickerEntityFilterFunc } from "../../data/entity"; import { getEntities, type EntityComboBoxItem, } from "../../data/entity_registry"; import { domainToName } from "../../data/integration"; import { isHelperDomain, type HelperDomain, } from "../../panels/config/helpers/const"; import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail"; import type { HomeAssistant } from "../../types"; import "../ha-combo-box-item"; import "../ha-generic-picker"; import type { HaGenericPicker } from "../ha-generic-picker"; import type { PickerComboBoxSearchFn } from "../ha-picker-combo-box"; import type { PickerValueRenderer } from "../ha-picker-field"; import "../ha-svg-icon"; import "./state-badge"; const CREATE_ID = "___create-new-entity___"; @customElement("ha-entity-picker") export class HaEntityPicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; // eslint-disable-next-line lit/no-native-attributes @property({ type: Boolean }) public autofocus = false; @property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public required = false; @property({ type: Boolean, attribute: "allow-custom-entity" }) public allowCustomEntity; @property({ type: Boolean, attribute: "show-entity-id" }) public showEntityId = false; @property() public label?: string; @property() public value?: string; @property() public helper?: string; @property() public placeholder?: string; @property({ type: String, attribute: "search-label" }) public searchLabel?: string; @property({ attribute: false, type: Array }) public createDomains?: string[]; /** * Show entities from specific domains. * @type {Array} * @attr include-domains */ @property({ type: Array, attribute: "include-domains" }) public includeDomains?: string[]; /** * Show no entities of these domains. * @type {Array} * @attr exclude-domains */ @property({ type: Array, attribute: "exclude-domains" }) public excludeDomains?: string[]; /** * Show only entities of these device classes. * @type {Array} * @attr include-device-classes */ @property({ type: Array, attribute: "include-device-classes" }) public includeDeviceClasses?: string[]; /** * Show only entities with these unit of measuments. * @type {Array} * @attr include-unit-of-measurement */ @property({ type: Array, attribute: "include-unit-of-measurement" }) public includeUnitOfMeasurement?: string[]; /** * List of allowed entities to show. * @type {Array} * @attr include-entities */ @property({ type: Array, attribute: "include-entities" }) public includeEntities?: string[]; /** * List of entities to be excluded. * @type {Array} * @attr exclude-entities */ @property({ type: Array, attribute: "exclude-entities" }) public excludeEntities?: string[]; @property({ attribute: false }) public entityFilter?: HaEntityPickerEntityFilterFunc; @property({ attribute: "hide-clear-icon", type: Boolean }) public hideClearIcon = false; @query("ha-generic-picker") private _picker?: HaGenericPicker; protected firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); // Load title translations so it is available when the combo-box opens this.hass.loadBackendTranslation("title"); } private _valueRenderer: PickerValueRenderer = (value) => { const entityId = value || ""; const stateObj = this.hass.states[entityId]; if (!stateObj) { return html` ${entityId} `; } const [entityName, deviceName, areaName] = computeEntityNameList( stateObj, [{ type: "entity" }, { type: "device" }, { type: "area" }], this.hass.entities, this.hass.devices, this.hass.areas, this.hass.floors ); const isRTL = computeRTL(this.hass); const primary = entityName || deviceName || entityId; const secondary = [areaName, entityName ? deviceName : undefined] .filter(Boolean) .join(isRTL ? " ◂ " : " ▸ "); return html` ${primary} ${secondary} `; }; private get _showEntityId() { return this.showEntityId || this.hass.userData?.showEntityIdPicker; } private _rowRenderer: ComboBoxLitRenderer = ( item, { index } ) => { const showEntityId = this._showEntityId; return html` ${item.icon_path ? html` ` : html` `} ${item.primary} ${item.secondary ? html`${item.secondary}` : nothing} ${item.stateObj && showEntityId ? html` ${item.stateObj.entity_id} ` : nothing} ${item.domain_name && !showEntityId ? html`
${item.domain_name}
` : nothing}
`; }; private _getAdditionalItems = () => this._getCreateItems(this.hass.localize, this.createDomains); private _getCreateItems = memoizeOne( ( localize: this["hass"]["localize"], createDomains: this["createDomains"] ) => { if (!createDomains?.length) { return []; } return createDomains.map((domain) => { const primary = localize( "ui.components.entity.entity-picker.create_helper", { domain: isHelperDomain(domain) ? localize( `ui.panel.config.helpers.types.${domain as HelperDomain}` ) : domainToName(localize, domain), } ); return { id: CREATE_ID + domain, primary: primary, secondary: localize("ui.components.entity.entity-picker.new_entity"), icon_path: mdiPlus, } satisfies EntityComboBoxItem; }); } ); private _getEntitiesMemoized = memoizeOne(getEntities); private _getItems = () => this._getEntitiesMemoized( this.hass, this.includeDomains, this.excludeDomains, this.entityFilter, this.includeDeviceClasses, this.includeUnitOfMeasurement, this.includeEntities, this.excludeEntities, this.value ); protected render() { const placeholder = this.placeholder ?? this.hass.localize("ui.components.entity.entity-picker.placeholder"); const notFoundLabel = this.hass.localize( "ui.components.entity.entity-picker.no_match" ); return html` `; } private _searchFn: PickerComboBoxSearchFn = ( search, filteredItems ) => { // If there is exact match for entity id, put it first const index = filteredItems.findIndex( (item) => item.stateObj?.entity_id === search ); if (index === -1) { return filteredItems; } const [exactMatch] = filteredItems.splice(index, 1); filteredItems.unshift(exactMatch); return filteredItems; }; public async open() { await this.updateComplete; await this._picker?.open(); } private _valueChanged(ev) { ev.stopPropagation(); const value = ev.detail.value; if (!value) { this._setValue(undefined); return; } if (value.startsWith(CREATE_ID)) { const domain = value.substring(CREATE_ID.length); showHelperDetailDialog(this, { domain, dialogClosedCallback: (item) => { if (item.entityId) this._setValue(item.entityId); }, }); return; } if (!isValidEntityId(value)) { return; } this._setValue(value); } private _setValue(value: string | undefined) { this.value = value; fireEvent(this, "value-changed", { value }); fireEvent(this, "change"); } } declare global { interface HTMLElementTagNameMap { "ha-entity-picker": HaEntityPicker; } }