1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 00:27:49 +01:00

Triggers/conditions Add usage and grouping to new multi domains (#51287)

This commit is contained in:
Bram Kragten
2026-03-31 14:42:15 +02:00
committed by GitHub
parent 5cc223a582
commit 8e3b1dc6ac
2 changed files with 253 additions and 87 deletions

View File

@@ -3,8 +3,10 @@ import {
mdiAirFilter,
mdiAlert,
mdiAppleSafari,
mdiBattery,
mdiBell,
mdiBookmark,
mdiBrightness6,
mdiBullhorn,
mdiButtonPointer,
mdiCalendar,
@@ -16,13 +18,18 @@ import {
mdiCog,
mdiCommentAlert,
mdiCounter,
mdiDoorOpen,
mdiEye,
mdiFlash,
mdiFlower,
mdiFormatListBulleted,
mdiFormTextbox,
mdiForumOutline,
mdiGarageOpen,
mdiGate,
mdiGoogleAssistant,
mdiGoogleCirclesCommunities,
mdiHomeAccount,
mdiHomeAutomation,
mdiImage,
mdiImageFilterFrames,
@@ -30,6 +37,7 @@ import {
mdiLightbulb,
mdiMapMarkerRadius,
mdiMicrophoneMessage,
mdiMotionSensor,
mdiPalette,
mdiRayVertex,
mdiRemote,
@@ -41,10 +49,14 @@ import {
mdiSpeakerMessage,
mdiStarFourPoints,
mdiThermostat,
mdiThermometer,
mdiTimerOutline,
mdiToggleSwitch,
mdiWater,
mdiWaterPercent,
mdiWeatherPartlyCloudy,
mdiWhiteBalanceSunny,
mdiWindowClosed,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { isComponentLoaded } from "../common/config/is_component_loaded";
@@ -75,6 +87,7 @@ export const FALLBACK_DOMAIN_ICONS = {
air_quality: mdiAirFilter,
alert: mdiAlert,
automation: mdiRobot,
battery: mdiBattery,
calendar: mdiCalendar,
climate: mdiThermostat,
configurator: mdiCog,
@@ -84,10 +97,15 @@ export const FALLBACK_DOMAIN_ICONS = {
datetime: mdiCalendarClock,
demo: mdiHomeAssistant,
device_tracker: mdiAccount,
door: mdiDoorOpen,
garage_door: mdiGarageOpen,
gate: mdiGate,
google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities,
homeassistant: mdiHomeAssistant,
homekit: mdiHomeAutomation,
humidity: mdiWaterPercent,
illuminance: mdiBrightness6,
image_processing: mdiImageFilterFrames,
image: mdiImage,
infrared: mdiLedOn,
@@ -99,11 +117,15 @@ export const FALLBACK_DOMAIN_ICONS = {
input_text: mdiFormTextbox,
lawn_mower: mdiRobotMower,
light: mdiLightbulb,
moisture: mdiWater,
motion: mdiMotionSensor,
notify: mdiCommentAlert,
number: mdiRayVertex,
occupancy: mdiHomeAccount,
persistent_notification: mdiBell,
person: mdiAccount,
plant: mdiFlower,
power: mdiFlash,
proximity: mdiAppleSafari,
remote: mdiRemote,
scene: mdiPalette,
@@ -115,6 +137,7 @@ export const FALLBACK_DOMAIN_ICONS = {
siren: mdiBullhorn,
stt: mdiMicrophoneMessage,
sun: mdiWhiteBalanceSunny,
temperature: mdiThermometer,
text: mdiFormTextbox,
time: mdiClock,
timer: mdiTimerOutline,
@@ -124,6 +147,7 @@ export const FALLBACK_DOMAIN_ICONS = {
vacuum: mdiRobotVacuum,
wake_word: mdiChatSleep,
weather: mdiWeatherPartlyCloudy,
window: mdiWindowClosed,
zone: mdiMapMarkerRadius,
};

View File

@@ -17,6 +17,7 @@ import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../../common/array/ensure-array";
import { fireEvent } from "../../../common/dom/fire_event";
import { mainWindow } from "../../../common/dom/get_main_window";
import { computeAreaName } from "../../../common/entity/compute_area_name";
@@ -98,6 +99,7 @@ import {
domainToName,
fetchIntegrationManifests,
} from "../../../data/integration";
import { filterSelectorEntities } from "../../../data/selector";
import type { LabelRegistryEntry } from "../../../data/label/label_registry";
import { subscribeLabFeature } from "../../../data/labs";
import {
@@ -412,6 +414,86 @@ class DialogAddAutomationElement
}
}
private _calculateActiveSystemDomains = memoizeOne(
(
descriptions: TriggerDescriptions | ConditionDescriptions,
manifests: DomainManifestLookup,
getDomain: (key: string) => string
): { active: Set<string>; byEntityDomain: Map<string, Set<string>> } => {
const active = new Set<string>();
// Group all entity filters by system domain
const domainFilters: Record<
string,
Parameters<typeof filterSelectorEntities>[0][]
> = {};
// Also collect which entity domains each system domain targets
const entityDomainsPerSystemDomain: Record<string, Set<string>> = {};
for (const [key, desc] of Object.entries(descriptions)) {
const domain = getDomain(key);
if (manifests[domain]?.integration_type !== "system") {
continue;
}
if (!domainFilters[domain]) {
domainFilters[domain] = [];
entityDomainsPerSystemDomain[domain] = new Set();
}
const entityFilters = ensureArray(desc.target?.entity);
if (entityFilters) {
// target.entity can be EntitySelectorFilter | readonly EntitySelectorFilter[]
// ensureArray wraps it but each element may still be an array, so flatten
for (const filterOrArray of entityFilters) {
const filters = ensureArray(filterOrArray);
domainFilters[domain].push(...filters);
for (const filter of filters) {
for (const entityDomain of ensureArray(filter.domain) ?? []) {
entityDomainsPerSystemDomain[domain].add(entityDomain);
}
}
}
}
}
// Check each entity in hass.states against the filters
for (const entity of Object.values(this.hass.states)) {
for (const [domain, filters] of Object.entries(domainFilters)) {
if (active.has(domain)) {
continue;
}
if (filters.some((f) => filterSelectorEntities(f, entity))) {
active.add(domain);
}
}
}
// Build reverse map: entity domain → set of system domains that cover it
const byEntityDomain = new Map<string, Set<string>>();
for (const [systemDomain, entityDomains] of Object.entries(
entityDomainsPerSystemDomain
)) {
for (const entityDomain of entityDomains) {
if (!byEntityDomain.has(entityDomain)) {
byEntityDomain.set(entityDomain, new Set());
}
byEntityDomain.get(entityDomain)!.add(systemDomain);
}
}
return { active, byEntityDomain };
}
);
private get _systemDomains() {
if (!this._manifests) {
return undefined;
}
const descriptions =
this._params?.type === "trigger"
? this._triggerDescriptions
: this._conditionDescriptions;
return this._calculateActiveSystemDomains(
descriptions,
this._manifests,
this._params?.type === "trigger" ? getTriggerDomain : getConditionDomain
);
}
private async _loadConfigEntries() {
const configEntries = await getConfigEntries(this.hass);
this._configEntryLookup = Object.fromEntries(
@@ -426,6 +508,11 @@ class DialogAddAutomationElement
manifests[manifest.domain] = manifest;
}
this._manifests = manifests;
// If a target was already selected and items computed before manifests
// loaded, recompute so system domain grouping applies correctly.
if (this._selectedTarget && this._targetItems) {
this._getItemsByTarget();
}
}
public disconnectedCallback(): void {
@@ -905,7 +992,8 @@ class DialogAddAutomationElement
this._domains,
this.hass.localize,
this.hass.services,
this._manifests
this._manifests,
this._systemDomains?.byEntityDomain
),
},
]
@@ -954,10 +1042,17 @@ class DialogAddAutomationElement
const items = flattenGroups(groups).flat();
if (type === "trigger") {
items.push(...this._triggers(localize, this._triggerDescriptions));
items.push(
...this._triggers(localize, this._triggerDescriptions, undefined)
);
} else if (type === "condition") {
items.push(
...this._conditions(localize, this._conditionDescriptions, manifests)
...this._conditions(
localize,
this._conditionDescriptions,
manifests,
undefined
)
);
} else if (type === "action") {
items.push(...this._services(localize, services, manifests));
@@ -1141,16 +1236,23 @@ class DialogAddAutomationElement
domains: Set<string> | undefined,
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests?: DomainManifestLookup
manifests?: DomainManifestLookup,
systemDomainsByEntityDomain?: Map<string, Set<string>>
): AddAutomationElementListItem[] => {
if (type === "trigger" && isDynamic(group)) {
return this._triggers(localize, this._triggerDescriptions, group);
return this._triggers(
localize,
this._triggerDescriptions,
systemDomainsByEntityDomain,
group
);
}
if (type === "condition" && isDynamic(group)) {
return this._conditions(
localize,
this._conditionDescriptions,
manifests,
systemDomainsByEntityDomain,
group
);
}
@@ -1250,6 +1352,38 @@ class DialogAddAutomationElement
);
};
private _domainMatchesGroupType(
domain: string,
manifest: DomainManifestLookup[string] | undefined,
domainUsed: boolean,
type: "helper" | "other" | undefined
): boolean {
if (type === undefined) {
return (
ENTITY_DOMAINS_MAIN.has(domain) ||
(manifest?.integration_type === "entity" &&
domainUsed &&
!ENTITY_DOMAINS_OTHER.has(domain)) ||
(manifest?.integration_type === "system" &&
(this._systemDomains?.active.has(domain) ?? false))
);
}
if (type === "helper") {
return manifest?.integration_type === "helper";
}
// type === "other"
return (
!ENTITY_DOMAINS_MAIN.has(domain) &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
(!domainUsed && manifest?.integration_type === "entity") ||
(manifest?.integration_type === "system" &&
!(this._systemDomains?.active.has(domain) ?? false)) ||
!["helper", "entity", "system"].includes(
manifest?.integration_type || ""
))
);
}
private _triggerGroups = (
localize: LocalizeFunc,
triggers: TriggerDescriptions,
@@ -1273,19 +1407,7 @@ class DialogAddAutomationElement
const manifest = manifests[domain];
const domainUsed = !domains ? true : domains.has(domain);
if (
(type === undefined &&
(ENTITY_DOMAINS_MAIN.has(domain) ||
(manifest?.integration_type === "entity" &&
domainUsed &&
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
(type === "helper" && manifest?.integration_type === "helper") ||
(type === "other" &&
!ENTITY_DOMAINS_MAIN.has(domain) &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
(!domainUsed && manifest?.integration_type === "entity") ||
!["helper", "entity"].includes(manifest?.integration_type || "")))
) {
if (this._domainMatchesGroupType(domain, manifest, domainUsed, type)) {
result.push({
icon: html`
<ha-domain-icon
@@ -1309,17 +1431,30 @@ class DialogAddAutomationElement
(
localize: LocalizeFunc,
triggers: TriggerDescriptions,
systemDomainsByEntityDomain: Map<string, Set<string>> | undefined,
group?: string
): AddAutomationElementListItem[] => {
if (!triggers) {
return [];
}
const browsedEntityDomain =
group && isDynamic(group) ? getValueFromDynamic(group) : undefined;
// System domains that should be merged into this entity domain group
const systemDomainsForGroup = browsedEntityDomain
? systemDomainsByEntityDomain?.get(browsedEntityDomain)
: undefined;
return this._getTriggerListItems(
localize,
Object.keys(triggers).filter((trigger) => {
const domain = getTriggerDomain(trigger);
return !group || group === `${DYNAMIC_PREFIX}${domain}`;
if (!group || group === `${DYNAMIC_PREFIX}${domain}`) {
return true;
}
// Also include system domain triggers that cover the browsed entity domain
return systemDomainsForGroup?.has(domain) ?? false;
})
);
}
@@ -1348,19 +1483,7 @@ class DialogAddAutomationElement
const manifest = manifests[domain];
const domainUsed = !domains ? true : domains.has(domain);
if (
(type === undefined &&
(ENTITY_DOMAINS_MAIN.has(domain) ||
(manifest?.integration_type === "entity" &&
domainUsed &&
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
(type === "helper" && manifest?.integration_type === "helper") ||
(type === "other" &&
!ENTITY_DOMAINS_MAIN.has(domain) &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
(!domainUsed && manifest?.integration_type === "entity") ||
!["helper", "entity"].includes(manifest?.integration_type || "")))
) {
if (this._domainMatchesGroupType(domain, manifest, domainUsed, type)) {
result.push({
icon: html`
<ha-domain-icon
@@ -1385,6 +1508,7 @@ class DialogAddAutomationElement
localize: LocalizeFunc,
conditions: ConditionDescriptions,
_manifests: DomainManifestLookup | undefined,
systemDomainsByEntityDomain: Map<string, Set<string>> | undefined,
group?: string
): AddAutomationElementListItem[] => {
if (!conditions) {
@@ -1392,10 +1516,21 @@ class DialogAddAutomationElement
}
const result: AddAutomationElementListItem[] = [];
const browsedEntityDomain =
group && isDynamic(group) ? getValueFromDynamic(group) : undefined;
const systemDomainsForGroup = browsedEntityDomain
? systemDomainsByEntityDomain?.get(browsedEntityDomain)
: undefined;
for (const condition of Object.keys(conditions)) {
const domain = getConditionDomain(condition);
if (group && group !== `${DYNAMIC_PREFIX}${domain}`) {
if (
group &&
group !== `${DYNAMIC_PREFIX}${domain}` &&
!(systemDomainsForGroup?.has(domain) ?? false)
) {
continue;
}
@@ -1491,13 +1626,23 @@ class DialogAddAutomationElement
);
private _getDomainType(domain: string) {
return ENTITY_DOMAINS_MAIN.has(domain) ||
(this._manifests?.[domain].integration_type === "entity" &&
if (
ENTITY_DOMAINS_MAIN.has(domain) ||
(this._manifests?.[domain]?.integration_type === "entity" &&
!ENTITY_DOMAINS_OTHER.has(domain))
? "dynamicGroups"
: this._manifests?.[domain].integration_type === "helper"
? "helpers"
: "other";
) {
return "dynamicGroups";
}
if (this._manifests?.[domain]?.integration_type === "helper") {
return "helpers";
}
if (
this._manifests?.[domain]?.integration_type === "system" &&
this._systemDomains?.active.has(domain)
) {
return "dynamicGroups";
}
return "other";
}
private _sortDomainsByCollection(
@@ -1602,30 +1747,56 @@ class DialogAddAutomationElement
iconPath: options.icon || TYPES[type].icons[key],
});
private _getDomainGroupedTriggerListItems(
private _getDomainGroupedListItems(
localize: LocalizeFunc,
triggerIds: string[]
ids: string[],
getDomain: (id: string) => string,
getListItem: (
localize: LocalizeFunc,
domain: string,
id: string
) => AddAutomationElementListItem
): { title: string; items: AddAutomationElementListItem[] }[] {
const items: Record<
string,
{ title: string; items: AddAutomationElementListItem[] }
> = {};
triggerIds.forEach((trigger) => {
const domain = getTriggerDomain(trigger);
// When a specific entity is selected, system domain items are merged
// under the entity's real domain rather than under their system domain name.
const targetEntityId = this._selectedTarget?.entity_id;
const targetEntityDomain =
targetEntityId &&
this._manifests?.[computeDomain(targetEntityId)]?.integration_type !==
"system"
? computeDomain(targetEntityId)
: undefined;
if (!items[domain]) {
items[domain] = {
title: domainToName(localize, domain, this._manifests?.[domain]),
ids.forEach((id) => {
const itemDomain = getDomain(id);
const isSystemDomain =
this._manifests?.[itemDomain]?.integration_type === "system";
// System domain items are grouped under the entity's real domain (if
// a specific entity is selected), so they appear alongside that domain's
// own items rather than in a separate section.
const groupDomain =
isSystemDomain && targetEntityDomain ? targetEntityDomain : itemDomain;
if (!items[groupDomain]) {
items[groupDomain] = {
title: domainToName(
localize,
groupDomain,
this._manifests?.[groupDomain]
),
items: [],
};
}
items[domain].items.push(
this._getTriggerListItem(localize, domain, trigger)
);
items[groupDomain].items.push(getListItem(localize, itemDomain, id));
items[domain].items.sort((a, b) =>
items[groupDomain].items.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
});
@@ -1747,39 +1918,6 @@ class DialogAddAutomationElement
);
}
private _getDomainGroupedConditionListItems(
localize: LocalizeFunc,
conditionIds: string[]
): { title: string; items: AddAutomationElementListItem[] }[] {
const items: Record<
string,
{ title: string; items: AddAutomationElementListItem[] }
> = {};
conditionIds.forEach((condition) => {
const domain = getConditionDomain(condition);
if (!items[domain]) {
items[domain] = {
title: domainToName(localize, domain, this._manifests?.[domain]),
items: [],
};
}
items[domain].items.push(
this._getConditionListItem(localize, domain, condition)
);
items[domain].items.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
});
return this._sortDomainsByCollection(
this._params!.type,
Object.entries(items)
);
}
// #endregion render prepare
// #region interaction
@@ -1909,9 +2047,11 @@ class DialogAddAutomationElement
this._selectedTarget
);
const grouped = this._getDomainGroupedTriggerListItems(
const grouped = this._getDomainGroupedListItems(
this.hass.localize,
items
items,
getTriggerDomain,
this._getTriggerListItem
);
if (this._selectedTarget.entity_id) {
grouped.push({
@@ -1930,9 +2070,11 @@ class DialogAddAutomationElement
this._selectedTarget
);
const grouped = this._getDomainGroupedConditionListItems(
const grouped = this._getDomainGroupedListItems(
this.hass.localize,
items
items,
getConditionDomain,
this._getConditionListItem
);
if (this._selectedTarget.entity_id) {
grouped.push({