Compare commits

...

2 Commits

Author SHA1 Message Date
Aidan Timson db650a0c86 Cleanup callers 2026-06-24 09:19:13 +01:00
Aidan Timson 48ffe09c68 Migrate entity picker to lazy context 2026-06-24 09:16:47 +01:00
26 changed files with 94 additions and 81 deletions
@@ -125,7 +125,15 @@ export interface EntityPickerDisplay {
}
export const computeEntityPickerDisplay = (
hass: HomeAssistant,
hass: Pick<
HomeAssistant,
| "entities"
| "devices"
| "areas"
| "floors"
| "language"
| "translationMetadata"
>,
stateObj: HassEntity
): EntityPickerDisplay => {
const [entityName, deviceName, areaName] = computeEntityNameList(
+1 -9
View File
@@ -9,15 +9,13 @@ import {
} from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import type { ValueChangedEvent } from "../../types";
import "../ha-sortable";
import "./ha-entity-picker";
import type { HaEntityPicker } from "./ha-entity-picker";
@customElement("ha-entities-picker")
class HaEntitiesPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Array }) public value?: string[];
@property({ type: Boolean }) public disabled = false;
@@ -87,10 +85,6 @@ class HaEntitiesPicker extends LitElement {
public reorder = false;
protected render() {
if (!this.hass) {
return nothing;
}
const currentEntities = this._currentEntities;
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
@@ -105,7 +99,6 @@ class HaEntitiesPicker extends LitElement {
<div class="entity">
<ha-entity-picker
.curValue=${entityId}
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities}
@@ -133,7 +126,6 @@ class HaEntitiesPicker extends LitElement {
</ha-sortable>
<div>
<ha-entity-picker
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities}
+73 -40
View File
@@ -1,5 +1,5 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { consume } from "@lit/context";
import { consume, type ContextType } from "@lit/context";
import { mdiPlus, mdiShape } from "@mdi/js";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -8,7 +8,14 @@ import { fireEvent } from "../../common/dom/fire_event";
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { RelatedIdSets } from "../../common/search/related-context";
import { relatedContext } from "../../data/context";
import type { LocalizeFunc } from "../../common/translations/localize";
import {
configContext,
internationalizationContext,
registriesContext,
relatedContext,
statesContext,
} from "../../data/context";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import {
entityComboBoxKeys,
@@ -38,7 +45,21 @@ const CREATE_ID = "___create-new-entity___";
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: ContextType<typeof statesContext>;
@state()
@consume({ context: registriesContext, subscribe: true })
private _registries!: ContextType<typeof registriesContext>;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: configContext, subscribe: true })
private _config!: ContextType<typeof configContext>;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@@ -150,12 +171,11 @@ export class HaEntityPicker extends LitElement {
);
}
protected willUpdate(changedProperties: PropertyValues<this>) {
protected willUpdate(changedProperties: PropertyValues) {
if (
this._pendingEntityId &&
changedProperties.has("hass") &&
this.hass.states !== changedProperties.get("hass")?.states &&
this.hass.states[this._pendingEntityId]
changedProperties.has("_states") &&
this._states[this._pendingEntityId]
) {
this._setValue(this._pendingEntityId);
this._pendingEntityId = undefined;
@@ -165,7 +185,7 @@ export class HaEntityPicker extends LitElement {
protected firstUpdated(changedProperties: PropertyValues<this>): void {
super.firstUpdated(changedProperties);
// Load title translations so it is available when the combo-box opens
this.hass.loadBackendTranslation("title");
this._i18n.loadBackendTranslation("title");
}
private _findExtraOption(value: string | undefined) {
@@ -176,7 +196,7 @@ export class HaEntityPicker extends LitElement {
private _renderExtraOptionStart(extraOption: EntitySelectorExtraOption) {
const stateObj = extraOption.entity_id
? this.hass.states[extraOption.entity_id]
? this._states[extraOption.entity_id]
: undefined;
if (stateObj) {
return html`
@@ -212,7 +232,7 @@ export class HaEntityPicker extends LitElement {
`;
}
const stateObj = this.hass.states[entityId];
const stateObj = this._states[entityId];
if (!stateObj) {
return html`
@@ -226,7 +246,11 @@ export class HaEntityPicker extends LitElement {
}
const { primary, secondary } = computeEntityPickerDisplay(
this.hass,
{
...this._registries,
language: this._i18n.language,
translationMetadata: this._i18n.translationMetadata,
},
stateObj
);
@@ -238,7 +262,7 @@ export class HaEntityPicker extends LitElement {
};
private get _showEntityId() {
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
return this.showEntityId || this._config.userData?.showEntityIdPicker;
}
private _rowRenderer: RenderItemFunction<EntityComboBoxItem> = (
@@ -286,17 +310,14 @@ export class HaEntityPicker extends LitElement {
};
private _getAdditionalItems = () =>
this._getCreateItems(this.hass.localize, this.createDomains);
this._getCreateItems(this._i18n.localize, this.createDomains);
private _getCreateItems = memoizeOne(
(
localize: this["hass"]["localize"],
createDomains: this["createDomains"]
) => {
(localize: LocalizeFunc, createDomains: this["createDomains"]) => {
if (!createDomains?.length) {
return [];
}
this.hass.loadFragmentTranslation("config");
this._i18n.loadFragmentTranslation("config");
return createDomains.map((domain) => {
const primary = localize(
"ui.components.entity.entity-picker.create_helper",
@@ -321,7 +342,9 @@ export class HaEntityPicker extends LitElement {
private _getEntitiesMemoized = memoizeOne(
(
hass: HomeAssistant,
states: ContextType<typeof statesContext>,
registries: ContextType<typeof registriesContext>,
i18n: ContextType<typeof internationalizationContext>,
includeDomains?: string[],
excludeDomains?: string[],
entityFilter?: HaEntityPickerEntityFilterFunc,
@@ -331,16 +354,25 @@ export class HaEntityPicker extends LitElement {
excludeEntities?: string[],
value?: string
) =>
getEntities(hass, {
includeDomains,
excludeDomains,
entityFilter,
includeDeviceClasses,
includeUnitOfMeasurement,
includeEntities,
excludeEntities,
value,
})
getEntities(
{
states,
...registries,
language: i18n.language,
translationMetadata: i18n.translationMetadata,
localize: i18n.localize,
},
{
includeDomains,
excludeDomains,
entityFilter,
includeDeviceClasses,
includeUnitOfMeasurement,
includeEntities,
excludeEntities,
value,
}
)
);
private _sortByRelatedContext = memoizeOne(
@@ -359,7 +391,9 @@ export class HaEntityPicker extends LitElement {
private _getItems = () => {
const entityItems = this._getEntitiesMemoized(
this.hass,
this._states,
this._registries,
this._i18n,
this.includeDomains,
this.excludeDomains,
this.entityFilter,
@@ -373,15 +407,15 @@ export class HaEntityPicker extends LitElement {
? this._sortByRelatedContext(
entityItems,
this._relatedIdSets!,
this.hass.entities,
this.hass.devices,
this.hass.locale.language
this._registries.entities,
this._registries.devices,
this._i18n.locale.language
)
: entityItems;
if (this.extraOptions?.length) {
const resolvedExtras = this.extraOptions.map((opt) => ({
...opt,
stateObj: opt.entity_id ? this.hass.states[opt.entity_id] : undefined,
stateObj: opt.entity_id ? this._states[opt.entity_id] : undefined,
}));
return [...resolvedExtras, ...sortedItems];
}
@@ -395,11 +429,10 @@ export class HaEntityPicker extends LitElement {
protected render() {
const placeholder =
this.placeholder ??
this.hass.localize("ui.components.entity.entity-picker.placeholder");
this._i18n.localize("ui.components.entity.entity-picker.placeholder");
return html`
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
.autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity}
@@ -421,9 +454,9 @@ export class HaEntityPicker extends LitElement {
use-top-label
.addButtonLabel=${this.addButton
? (this.addButtonLabel ??
this.hass.localize("ui.components.entity.entity-picker.add"))
this._i18n.localize("ui.components.entity.entity-picker.add"))
: undefined}
.unknownItemText=${this.hass.localize(
.unknownItemText=${this._i18n.localize(
"ui.components.entity.entity-picker.unknown"
)}
@value-changed=${this._valueChanged}
@@ -476,7 +509,7 @@ export class HaEntityPicker extends LitElement {
domain,
dialogClosedCallback: (item) => {
if (item.entityId) {
if (this.hass.states[item.entityId]) {
if (this._states[item.entityId]) {
this._setValue(item.entityId);
} else {
this._pendingEntityId = item.entityId;
@@ -502,7 +535,7 @@ export class HaEntityPicker extends LitElement {
}
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.entity.entity-picker.no_match", {
this._i18n.localize("ui.components.entity.entity-picker.no_match", {
term: html`<b>${search}</b>`,
});
}
@@ -63,7 +63,6 @@ export class HaEntitySelector extends LitElement {
if (!this.selector.entity?.multiple) {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${typeof this.value === "string" ? this.value : ""}
.label=${this.label}
.placeholder=${this.placeholder}
@@ -80,7 +79,6 @@ export class HaEntitySelector extends LitElement {
return html`
<ha-entities-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.placeholder=${this.placeholder}
@@ -91,7 +91,6 @@ export class HaMediaSelector extends LitElement {
? nothing
: html`
<ha-entity-picker
.hass=${this.hass}
.value=${entityId}
.label=${this.label ||
this.hass.localize(
-1
View File
@@ -558,7 +558,6 @@ export class HaServiceControl extends LitElement {
></ha-settings-row>`
: entityId
? html`<ha-entity-picker
.hass=${this.hass}
.disabled=${this.disabled}
.value=${this._value?.data?.entity_id}
.label=${this.hass.localize(
+11 -1
View File
@@ -58,7 +58,17 @@ export interface GetEntitiesOptions {
}
export const getEntities = (
hass: HomeAssistant,
hass: Pick<
HomeAssistant,
| "states"
| "entities"
| "devices"
| "areas"
| "floors"
| "language"
| "translationMetadata"
| "localize"
>,
options?: GetEntitiesOptions
): EntityComboBoxItem[] => {
const {
@@ -228,7 +228,6 @@ class DialogCalendarEventEditor extends DirtyStateProviderMixin<CalendarEventFor
></ha-textarea>
<ha-entity-picker
name="calendar"
.hass=${this.hass}
.label=${this.hass.localize("ui.components.calendar.label")}
.value=${this._calendarId!}
.includeDomains=${CALENDAR_DOMAINS}
@@ -251,7 +251,6 @@ class DialogAreaDetail
>
<div class="content">
<ha-entity-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.areas.editor.temperature_entity"
)}
@@ -266,7 +265,6 @@ class DialogAreaDetail
></ha-entity-picker>
<ha-entity-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.areas.editor.humidity_entity"
)}
@@ -38,7 +38,6 @@ export class HaZoneCondition extends LitElement {
)}
.value=${entity_id}
@value-changed=${this._entityPicked}
.hass=${this.hass}
.disabled=${this.disabled}
.entityFilter=${zoneAndLocationFilter}
></ha-entity-picker>
@@ -48,7 +47,6 @@ export class HaZoneCondition extends LitElement {
)}
.value=${zone}
@value-changed=${this._zonePicked}
.hass=${this.hass}
.disabled=${this.disabled}
.includeDomains=${includeDomains}
></ha-entity-picker>
@@ -45,7 +45,6 @@ export class HaZoneTrigger extends LitElement {
.value=${entity_id ? ensureArray(entity_id) : []}
.disabled=${this.disabled}
@value-changed=${this._entityPicked}
.hass=${this.hass}
.entityFilter=${zoneAndLocationFilter}
></ha-entities-picker>
<ha-entity-picker
@@ -55,7 +54,6 @@ export class HaZoneTrigger extends LitElement {
.value=${zone}
.disabled=${this.disabled}
@value-changed=${this._zonePicked}
.hass=${this.hass}
.includeDomains=${includeDomains}
></ha-entity-picker>
-2
View File
@@ -99,7 +99,6 @@ export class AITaskPref extends LitElement {
</span>
<ha-entity-picker
data-name="gen_data_entity_id"
.hass=${this.hass}
.disabled=${this._prefs === undefined &&
isComponentLoaded(this.hass.config, "ai_task")}
.value=${this._gen_data_entity_id ||
@@ -119,7 +118,6 @@ export class AITaskPref extends LitElement {
</span>
<ha-entity-picker
data-name="gen_image_entity_id"
.hass=${this.hass}
.disabled=${this._prefs === undefined &&
isComponentLoaded(this.hass.config, "ai_task")}
.value=${this._gen_image_entity_id ||
@@ -50,7 +50,6 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
>
<div class="card-content">
<ha-entity-picker
.hass=${this.hass}
.helper=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.entity_diagnostic.description"
)}
@@ -220,7 +220,6 @@ class HaPanelDevState extends LitElement {
<div class="inputs">
<ha-entity-picker
autofocus
.hass=${this.hass}
.value=${this._entityId}
@value-changed=${this._entityIdChanged}
show-entity-id
@@ -269,7 +269,6 @@ export class DialogEnergyGasSettings
: this._costs === "entity"
? html`<ha-entity-picker
class="price-options"
.hass=${this.hass}
include-domains='["sensor", "input_number"]'
.value=${this._source.entity_energy_price}
.label=${this.hass.localize(
@@ -328,7 +328,6 @@ export class DialogEnergyGridSettings
${this._importCostType === "entity"
? html`
<ha-entity-picker
.hass=${this.hass}
.value=${this._source.entity_energy_price}
.label=${this.hass.localize(
"ui.panel.config.energy.grid.dialog.cost_entity_label"
@@ -416,7 +415,6 @@ export class DialogEnergyGridSettings
${this._exportCostType === "entity"
? html`
<ha-entity-picker
.hass=${this.hass}
.value=${this._source.entity_energy_price_export}
.label=${this.hass.localize(
"ui.panel.config.energy.grid.dialog.compensation_entity_label"
@@ -230,7 +230,6 @@ export class DialogEnergyWaterSettings
: this._costs === "entity"
? html`<ha-entity-picker
class="price-options"
.hass=${this.hass}
include-domains='["sensor", "input_number"]'
.value=${this._source.entity_energy_price}
.label=${this.hass.localize(
@@ -747,7 +747,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
SCANNER_SOURCE_TYPES.includes(stateObj?.attributes?.source_type)
? html`
<ha-entity-picker
.hass=${this.hass}
.value=${this._associatedZone}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.associated_zone"
@@ -219,7 +219,6 @@ class DialogPersonDetail
)}
</p>
<ha-entities-picker
.hass=${this.hass}
.value=${this._deviceTrackers}
.includeDomains=${includeDomains}
.pickedEntityLabel=${this.hass.localize(
@@ -584,7 +584,6 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
<ha-entity-picker
@value-changed=${this._entityPicked}
.excludeDomains=${SCENE_IGNORED_DOMAINS}
.hass=${this.hass}
label=${this.hass.localize(
"ui.panel.config.scene.editor.entities.add"
)}
@@ -50,7 +50,6 @@ export class HomeFavoritesEditor extends LitElement {
</ha-sortable>
<ha-entity-picker
add-button
.hass=${this.hass}
.addButtonLabel=${this.hass.localize(
"ui.panel.lovelace.editor.strategy.home.add_favorite_entity"
)}
@@ -168,7 +168,6 @@ export class HuiEntityEditor extends LitElement {
></ha-svg-icon>
</div>
<ha-entity-picker
.hass=${this.hass}
.value=${entityConf.entity}
.index=${index}
.entityFilter=${this.entityFilter}
@@ -180,7 +179,6 @@ export class HuiEntityEditor extends LitElement {
</div>
</ha-sortable>`}
<ha-entity-picker
.hass=${this.hass}
.entityFilter=${this.entityFilter}
@value-changed=${this._addEntity}
add-button
@@ -100,7 +100,6 @@ export class HuiCalendarCardEditor
")"}
</h3>
<ha-entities-picker
.hass=${this.hass!}
.value=${this._config.entities}
.includeDomains=${["calendar"]}
@value-changed=${this._entitiesChanged}
@@ -54,7 +54,6 @@ export class HuiGraphFooterEditor
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)}
.hass=${this.hass}
.value=${this._entity}
.configValue=${"entity"}
.includeDomains=${includeDomains}
@@ -81,7 +81,6 @@ export class HuiEntitiesCardRowEditor extends LitElement {
: html`
<ha-entity-picker
hide-clear-icon
.hass=${this.hass}
.value=${(entityConf as EntityConfig).entity}
.index=${index}
@value-changed=${this._valueChanged}
@@ -112,7 +111,6 @@ export class HuiEntitiesCardRowEditor extends LitElement {
</ha-sortable>
<ha-entity-picker
class="add-entity"
.hass=${this.hass}
@value-changed=${this._addEntity}
add-button
></ha-entity-picker>
@@ -27,7 +27,6 @@ export class HuiHomeDashboardStrategyEditor
return html`
<ha-entities-picker
.hass=${this.hass}
.value=${this._config.favorite_entities || []}
label=${this.hass.localize(
"ui.panel.lovelace.editor.strategy.home.favorite_entities"