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

View File

@@ -17,6 +17,7 @@ import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ensureArray } from "../../../common/array/ensure-array";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { mainWindow } from "../../../common/dom/get_main_window"; import { mainWindow } from "../../../common/dom/get_main_window";
import { computeAreaName } from "../../../common/entity/compute_area_name"; import { computeAreaName } from "../../../common/entity/compute_area_name";
@@ -98,6 +99,7 @@ import {
domainToName, domainToName,
fetchIntegrationManifests, fetchIntegrationManifests,
} from "../../../data/integration"; } from "../../../data/integration";
import { filterSelectorEntities } from "../../../data/selector";
import type { LabelRegistryEntry } from "../../../data/label/label_registry"; import type { LabelRegistryEntry } from "../../../data/label/label_registry";
import { subscribeLabFeature } from "../../../data/labs"; import { subscribeLabFeature } from "../../../data/labs";
import { 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() { private async _loadConfigEntries() {
const configEntries = await getConfigEntries(this.hass); const configEntries = await getConfigEntries(this.hass);
this._configEntryLookup = Object.fromEntries( this._configEntryLookup = Object.fromEntries(
@@ -426,6 +508,11 @@ class DialogAddAutomationElement
manifests[manifest.domain] = manifest; manifests[manifest.domain] = manifest;
} }
this._manifests = manifests; 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 { public disconnectedCallback(): void {
@@ -905,7 +992,8 @@ class DialogAddAutomationElement
this._domains, this._domains,
this.hass.localize, this.hass.localize,
this.hass.services, this.hass.services,
this._manifests this._manifests,
this._systemDomains?.byEntityDomain
), ),
}, },
] ]
@@ -954,10 +1042,17 @@ class DialogAddAutomationElement
const items = flattenGroups(groups).flat(); const items = flattenGroups(groups).flat();
if (type === "trigger") { if (type === "trigger") {
items.push(...this._triggers(localize, this._triggerDescriptions)); items.push(
...this._triggers(localize, this._triggerDescriptions, undefined)
);
} else if (type === "condition") { } else if (type === "condition") {
items.push( items.push(
...this._conditions(localize, this._conditionDescriptions, manifests) ...this._conditions(
localize,
this._conditionDescriptions,
manifests,
undefined
)
); );
} else if (type === "action") { } else if (type === "action") {
items.push(...this._services(localize, services, manifests)); items.push(...this._services(localize, services, manifests));
@@ -1141,16 +1236,23 @@ class DialogAddAutomationElement
domains: Set<string> | undefined, domains: Set<string> | undefined,
localize: LocalizeFunc, localize: LocalizeFunc,
services: HomeAssistant["services"], services: HomeAssistant["services"],
manifests?: DomainManifestLookup manifests?: DomainManifestLookup,
systemDomainsByEntityDomain?: Map<string, Set<string>>
): AddAutomationElementListItem[] => { ): AddAutomationElementListItem[] => {
if (type === "trigger" && isDynamic(group)) { 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)) { if (type === "condition" && isDynamic(group)) {
return this._conditions( return this._conditions(
localize, localize,
this._conditionDescriptions, this._conditionDescriptions,
manifests, manifests,
systemDomainsByEntityDomain,
group 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 = ( private _triggerGroups = (
localize: LocalizeFunc, localize: LocalizeFunc,
triggers: TriggerDescriptions, triggers: TriggerDescriptions,
@@ -1273,19 +1407,7 @@ class DialogAddAutomationElement
const manifest = manifests[domain]; const manifest = manifests[domain];
const domainUsed = !domains ? true : domains.has(domain); const domainUsed = !domains ? true : domains.has(domain);
if ( if (this._domainMatchesGroupType(domain, manifest, domainUsed, type)) {
(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 || "")))
) {
result.push({ result.push({
icon: html` icon: html`
<ha-domain-icon <ha-domain-icon
@@ -1309,17 +1431,30 @@ class DialogAddAutomationElement
( (
localize: LocalizeFunc, localize: LocalizeFunc,
triggers: TriggerDescriptions, triggers: TriggerDescriptions,
systemDomainsByEntityDomain: Map<string, Set<string>> | undefined,
group?: string group?: string
): AddAutomationElementListItem[] => { ): AddAutomationElementListItem[] => {
if (!triggers) { if (!triggers) {
return []; 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( return this._getTriggerListItems(
localize, localize,
Object.keys(triggers).filter((trigger) => { Object.keys(triggers).filter((trigger) => {
const domain = getTriggerDomain(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 manifest = manifests[domain];
const domainUsed = !domains ? true : domains.has(domain); const domainUsed = !domains ? true : domains.has(domain);
if ( if (this._domainMatchesGroupType(domain, manifest, domainUsed, type)) {
(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 || "")))
) {
result.push({ result.push({
icon: html` icon: html`
<ha-domain-icon <ha-domain-icon
@@ -1385,6 +1508,7 @@ class DialogAddAutomationElement
localize: LocalizeFunc, localize: LocalizeFunc,
conditions: ConditionDescriptions, conditions: ConditionDescriptions,
_manifests: DomainManifestLookup | undefined, _manifests: DomainManifestLookup | undefined,
systemDomainsByEntityDomain: Map<string, Set<string>> | undefined,
group?: string group?: string
): AddAutomationElementListItem[] => { ): AddAutomationElementListItem[] => {
if (!conditions) { if (!conditions) {
@@ -1392,10 +1516,21 @@ class DialogAddAutomationElement
} }
const result: AddAutomationElementListItem[] = []; 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)) { for (const condition of Object.keys(conditions)) {
const domain = getConditionDomain(condition); const domain = getConditionDomain(condition);
if (group && group !== `${DYNAMIC_PREFIX}${domain}`) { if (
group &&
group !== `${DYNAMIC_PREFIX}${domain}` &&
!(systemDomainsForGroup?.has(domain) ?? false)
) {
continue; continue;
} }
@@ -1491,13 +1626,23 @@ class DialogAddAutomationElement
); );
private _getDomainType(domain: string) { private _getDomainType(domain: string) {
return ENTITY_DOMAINS_MAIN.has(domain) || if (
(this._manifests?.[domain].integration_type === "entity" && ENTITY_DOMAINS_MAIN.has(domain) ||
(this._manifests?.[domain]?.integration_type === "entity" &&
!ENTITY_DOMAINS_OTHER.has(domain)) !ENTITY_DOMAINS_OTHER.has(domain))
? "dynamicGroups" ) {
: this._manifests?.[domain].integration_type === "helper" return "dynamicGroups";
? "helpers" }
: "other"; 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( private _sortDomainsByCollection(
@@ -1602,30 +1747,56 @@ class DialogAddAutomationElement
iconPath: options.icon || TYPES[type].icons[key], iconPath: options.icon || TYPES[type].icons[key],
}); });
private _getDomainGroupedTriggerListItems( private _getDomainGroupedListItems(
localize: LocalizeFunc, localize: LocalizeFunc,
triggerIds: string[] ids: string[],
getDomain: (id: string) => string,
getListItem: (
localize: LocalizeFunc,
domain: string,
id: string
) => AddAutomationElementListItem
): { title: string; items: AddAutomationElementListItem[] }[] { ): { title: string; items: AddAutomationElementListItem[] }[] {
const items: Record< const items: Record<
string, string,
{ title: string; items: AddAutomationElementListItem[] } { title: string; items: AddAutomationElementListItem[] }
> = {}; > = {};
triggerIds.forEach((trigger) => { // When a specific entity is selected, system domain items are merged
const domain = getTriggerDomain(trigger); // 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]) { ids.forEach((id) => {
items[domain] = { const itemDomain = getDomain(id);
title: domainToName(localize, domain, this._manifests?.[domain]), 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: [],
}; };
} }
items[domain].items.push( items[groupDomain].items.push(getListItem(localize, itemDomain, id));
this._getTriggerListItem(localize, domain, trigger)
);
items[domain].items.sort((a, b) => items[groupDomain].items.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language) 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 // #endregion render prepare
// #region interaction // #region interaction
@@ -1909,9 +2047,11 @@ class DialogAddAutomationElement
this._selectedTarget this._selectedTarget
); );
const grouped = this._getDomainGroupedTriggerListItems( const grouped = this._getDomainGroupedListItems(
this.hass.localize, this.hass.localize,
items items,
getTriggerDomain,
this._getTriggerListItem
); );
if (this._selectedTarget.entity_id) { if (this._selectedTarget.entity_id) {
grouped.push({ grouped.push({
@@ -1930,9 +2070,11 @@ class DialogAddAutomationElement
this._selectedTarget this._selectedTarget
); );
const grouped = this._getDomainGroupedConditionListItems( const grouped = this._getDomainGroupedListItems(
this.hass.localize, this.hass.localize,
items items,
getConditionDomain,
this._getConditionListItem
); );
if (this._selectedTarget.entity_id) { if (this._selectedTarget.entity_id) {
grouped.push({ grouped.push({