Add combine mode and fix hidden and entity category for service control

This commit is contained in:
Paul Bottein 2025-07-17 16:48:22 +02:00
parent be71c0b4fa
commit ea5c014552
No known key found for this signature in database
5 changed files with 96 additions and 28 deletions

View File

@ -11,6 +11,13 @@ import type { HaComboBox } from "../ha-combo-box";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
interface StateOption {
value: string;
label: string;
}
const DEFAULT_COMBINE_MODE: "union" | "intersection" = "union";
@customElement("ha-entity-state-picker")
class HaEntityStatePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -34,6 +41,9 @@ class HaEntityStatePicker extends LitElement {
@property({ attribute: false })
public hideStates?: string[];
@property({ attribute: "combine-mode" })
public combineMode?: "union" | "intersection";
@property() public label?: string;
@property() public value?: string;
@ -56,29 +66,55 @@ class HaEntityStatePicker extends LitElement {
changedProps.has("extraOptions")
) {
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
const options: { value: string; label: string }[] = [];
const statesSet = new Set<string>();
for (const entityId of entityIds) {
const entitiesOptions = entityIds.map<StateOption[]>((entityId) => {
const stateObj = this.hass.states[entityId];
if (!stateObj) {
return [];
}
const states = getStates(this.hass, stateObj, this.attribute).filter(
(s) => !this.hideStates || !this.hideStates.includes(s)
);
for (const s of states) {
if (!statesSet.has(s)) {
statesSet.add(s);
options.push({
value: s,
label: !this.attribute
? this.hass.formatEntityState(stateObj, s)
: this.hass.formatEntityAttributeValue(
stateObj,
this.attribute,
s
),
});
}
return states.map((s) => ({
value: s,
label: !this.attribute
? this.hass.formatEntityState(stateObj, s)
: this.hass.formatEntityAttributeValue(stateObj, this.attribute, s),
}));
});
const mode: "union" | "intersection" =
this.combineMode || DEFAULT_COMBINE_MODE;
let options: StateOption[] = [];
if (mode === "union") {
// Union: combine all unique states from all entities
options = entitiesOptions.reduce(
(acc, curr) => [
...acc,
...curr.filter(
(item) => !acc.some((existing) => existing.value === item.value)
),
],
[] as StateOption[]
);
} else if (mode === "intersection") {
// Intersection: only states that exist in ALL entities
if (entitiesOptions.length === 0) {
options = [];
} else if (entitiesOptions.length === 1) {
options = entitiesOptions[0];
} else {
options = entitiesOptions[0].filter((item) =>
entitiesOptions
.slice(1)
.every((entityOptions) =>
entityOptions.some((option) => option.value === item.value)
)
);
}
}

View File

@ -42,6 +42,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
.required=${this.required}
allow-custom-value
.hideStates=${this.selector.state?.hide_states}
.combineMode=${this.selector.state?.combine_mode}
></ha-entity-state-picker>
`;
}

View File

@ -55,6 +55,20 @@ const showOptionalToggle = (field) =>
!field.required &&
!("boolean" in field.selector && field.default);
const enrichSelector = (selector: Selector): Selector => {
// Default combine_mode to intersection for state selectors
if ("state" in selector) {
return {
...selector,
state: {
...selector.state,
combine_mode: selector.state?.combine_mode || "intersection",
},
};
}
return selector;
};
interface Field extends Omit<HassService["fields"][string], "selector"> {
key: string;
selector?: Selector;
@ -244,7 +258,9 @@ export class HaServiceControl extends LitElement {
).map(([key, value]) => ({
key,
...value,
selector: value.selector as Selector | undefined,
selector: (value.selector
? enrichSelector(value.selector)
: undefined) as Selector | undefined,
}));
const flatFields: Field[] = [];
@ -314,7 +330,12 @@ export class HaServiceControl extends LitElement {
targetSelector
);
targetDevices.push(...expanded.devices);
targetEntities.push(...expanded.entities);
const primaryEntities = expanded.entities.filter(
(entityId) =>
!this.hass.entities[entityId]?.entity_category &&
!this.hass.entities[entityId]?.hidden
);
targetEntities.push(primaryEntities);
targetAreas.push(...expanded.areas);
});
}
@ -338,20 +359,29 @@ export class HaServiceControl extends LitElement {
this.hass.entities,
targetSelector
);
targetEntities.push(...expanded.entities);
const primaryEntities = expanded.entities.filter(
(entityId) =>
!this.hass.entities[entityId]?.entity_category &&
!this.hass.entities[entityId]?.hidden
);
targetEntities.push(...primaryEntities);
targetDevices.push(...expanded.devices);
});
}
if (targetDevices.length) {
targetDevices.forEach((deviceId) => {
targetEntities.push(
...expandDeviceTarget(
this.hass,
deviceId,
this.hass.entities,
targetSelector
).entities
const expanded = expandDeviceTarget(
this.hass,
deviceId,
this.hass.entities,
targetSelector
);
const primaryEntities = expanded.entities.filter(
(entityId) =>
!this.hass.entities[entityId]?.entity_category &&
!this.hass.entities[entityId]?.hidden
);
targetEntities.push(...primaryEntities);
});
}
return targetEntities;

View File

@ -719,7 +719,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
}
private _entityRegMeetsFilter(entity: EntityRegistryDisplayEntry): boolean {
if (entity.entity_category) {
if (entity.hidden || entity.entity_category) {
return false;
}

View File

@ -396,6 +396,7 @@ export interface StateSelector {
entity_id?: string | string[];
attribute?: string;
hide_states?: string[];
combine_mode?: "union" | "intersection";
} | null;
}