From 81870d0e7d5972c96b448d81c1131dcc0c26ce4f Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 17 Jul 2025 14:23:29 +0200 Subject: [PATCH] Add support for multiple entities for attribute selector --- .../entity/ha-entity-attribute-picker.ts | 76 ++++++++++++------- .../entity/ha-entity-state-picker.ts | 14 ++-- .../ha-selector/ha-selector-attribute.ts | 16 ++-- .../ha-selector/ha-selector-state.ts | 2 +- src/data/selector.ts | 2 +- .../types/ha-automation-trigger-state.ts | 12 ++- 6 files changed, 74 insertions(+), 48 deletions(-) diff --git a/src/components/entity/ha-entity-attribute-picker.ts b/src/components/entity/ha-entity-attribute-picker.ts index 7e08060e33..e7efb31ae9 100644 --- a/src/components/entity/ha-entity-attribute-picker.ts +++ b/src/components/entity/ha-entity-attribute-picker.ts @@ -6,6 +6,8 @@ import { computeAttributeNameDisplay } from "../../common/entity/compute_attribu import type { HomeAssistant, ValueChangedEvent } from "../../types"; import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; +import { ensureArray } from "../../common/array/ensure-array"; +import { fireEvent } from "../../common/dom/fire_event"; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; @@ -13,7 +15,7 @@ export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; class HaEntityAttributePicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entityId?: string; + @property({ attribute: false }) public entityId?: string | string[]; /** * List of attributes to be hidden. @@ -48,23 +50,37 @@ class HaEntityAttributePicker extends LitElement { } protected updated(changedProps: PropertyValues) { - if (changedProps.has("_opened") && this._opened) { - const entityState = this.entityId - ? this.hass.states[this.entityId] - : undefined; - (this._comboBox as any).items = entityState - ? Object.keys(entityState.attributes) - .filter((key) => !this.hideAttributes?.includes(key)) - .map((key) => ({ - value: key, + if ( + (changedProps.has("_opened") && this._opened) || + changedProps.has("entityId") || + changedProps.has("attribute") + ) { + const entityIds = this.entityId ? ensureArray(this.entityId) : []; + const options: { value: string; label: string }[] = []; + const attributesSet = new Set(); + + for (const entityId of entityIds) { + const stateObj = this.hass.states[entityId]; + const attributes = Object.keys(stateObj.attributes).filter( + (a) => !this.hideAttributes?.includes(a) + ); + for (const a of attributes) { + if (!attributesSet.has(a)) { + attributesSet.add(a); + options.push({ + value: a, label: computeAttributeNameDisplay( this.hass.localize, - entityState, + stateObj, this.hass.entities, - key + a ), - })) - : []; + }); + } + } + } + + (this._comboBox as any).filteredItems = options; } } @@ -73,21 +89,10 @@ class HaEntityAttributePicker extends LitElement { return nothing; } - const stateObj = this.hass.states[this.entityId!] as HassEntity | undefined; - return html` ) { this._opened = ev.detail.value; } private _valueChanged(ev: ValueChangedEvent) { - this.value = ev.detail.value; + ev.stopPropagation(); + const newValue = ev.detail.value; + if (newValue !== this._value) { + this._setValue(newValue); + } + } + + private _setValue(value: string) { + this.value = value; + setTimeout(() => { + fireEvent(this, "value-changed", { value }); + fireEvent(this, "change"); + }, 0); } } diff --git a/src/components/entity/ha-entity-state-picker.ts b/src/components/entity/ha-entity-state-picker.ts index 3b0cedeb0c..2d86651b7e 100644 --- a/src/components/entity/ha-entity-state-picker.ts +++ b/src/components/entity/ha-entity-state-picker.ts @@ -32,7 +32,7 @@ class HaEntityStatePicker extends LitElement { public allowCustomValue; @property({ attribute: false }) - public excludeStates?: string[]; + public hideStates?: string[]; @property() public label?: string; @@ -56,19 +56,19 @@ class HaEntityStatePicker extends LitElement { changedProps.has("extraOptions") ) { const entityIds = this.entityId ? ensureArray(this.entityId) : []; - const stateOptions: { value: string; label: string }[] = []; + const options: { value: string; label: string }[] = []; const statesSet = new Set(); for (const entityId of entityIds) { const stateObj = this.hass.states[entityId]; const states = getStates(this.hass, stateObj, this.attribute).filter( - (s) => !this.excludeStates || !this.excludeStates.includes(s) + (s) => !this.hideStates || !this.hideStates.includes(s) ); for (const s of states) { if (!statesSet.has(s)) { statesSet.add(s); - const options = { + options.push({ value: s, label: !this.attribute ? this.hass.formatEntityState(stateObj, s) @@ -77,15 +77,14 @@ class HaEntityStatePicker extends LitElement { this.attribute, s ), - }; - stateOptions.push(options); + }); } } } (this._comboBox as any).filteredItems = [ ...(this.extraOptions ?? []), - ...stateOptions, + ...options, ]; } } @@ -106,6 +105,7 @@ class HaEntityStatePicker extends LitElement { .required=${this.required} .helper=${this.helper} .allowCustomValue=${this.allowCustomValue} + item-id-path="value" item-value-path="value" item-label-path="label" @opened-changed=${this._openedChanged} diff --git a/src/components/ha-selector/ha-selector-attribute.ts b/src/components/ha-selector/ha-selector-attribute.ts index 5296bc4be4..ca607ccf14 100644 --- a/src/components/ha-selector/ha-selector-attribute.ts +++ b/src/components/ha-selector/ha-selector-attribute.ts @@ -5,6 +5,7 @@ import { fireEvent } from "../../common/dom/fire_event"; import type { AttributeSelector } from "../../data/selector"; import type { HomeAssistant } from "../../types"; import "../entity/ha-entity-attribute-picker"; +import { ensureArray } from "../../common/array/ensure-array"; @customElement("ha-selector-attribute") export class HaSelectorAttribute extends LitElement { @@ -23,7 +24,7 @@ export class HaSelectorAttribute extends LitElement { @property({ type: Boolean }) public required = true; @property({ attribute: false }) public context?: { - filter_entity?: string; + filter_entity?: string | string[]; }; protected render() { @@ -69,11 +70,16 @@ export class HaSelectorAttribute extends LitElement { // Validate that that the attribute is still valid for this entity, else unselect. let invalid = false; if (this.context.filter_entity) { - const stateObj = this.hass.states[this.context.filter_entity]; + const entityIds = ensureArray(this.context.filter_entity); - if (!(stateObj && this.value in stateObj.attributes)) { - invalid = true; - } + invalid = !entityIds.some((entityId) => { + const stateObj = this.hass.states[entityId]; + return ( + stateObj && + this.value in stateObj.attributes && + stateObj.attributes[this.value] !== undefined + ); + }); } else { invalid = this.value !== undefined; } diff --git a/src/components/ha-selector/ha-selector-state.ts b/src/components/ha-selector/ha-selector-state.ts index cb5dc0cd87..fac976e879 100644 --- a/src/components/ha-selector/ha-selector-state.ts +++ b/src/components/ha-selector/ha-selector-state.ts @@ -41,7 +41,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) { .disabled=${this.disabled} .required=${this.required} allow-custom-value - .excludeStates=${this.selector.state?.exclude_states} + .hideStates=${this.selector.state?.hide_states} > `; } diff --git a/src/data/selector.ts b/src/data/selector.ts index b064d34641..b8ae831dc4 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -395,7 +395,7 @@ export interface StateSelector { extra_options?: { label: string; value: any }[]; entity_id?: string | string[]; attribute?: string; - exclude_states?: string[]; + hide_states?: string[]; } | null; } diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts index d95a39e36e..be0f286ba6 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts @@ -57,7 +57,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement { } private _schema = memoizeOne( - (localize: LocalizeFunc, entityId, attribute) => + (localize: LocalizeFunc, attribute) => [ { name: "entity_id", @@ -66,9 +66,11 @@ export class HaStateTrigger extends LitElement implements TriggerElement { }, { name: "attribute", + context: { + filter_entity: "entity_id", + }, selector: { attribute: { - entity_id: entityId ? entityId[0] : undefined, hide_attributes: [ "access_token", "available_modes", @@ -211,11 +213,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement { if (!data.attribute && data.from === null) { data.from = ANY_STATE_VALUE; } - const schema = this._schema( - this.hass.localize, - this.trigger.entity_id, - this.trigger.attribute - ); + const schema = this._schema(this.hass.localize, this.trigger.attribute); return html`