Files
frontend/src/panels/config/automation/add-automation-element-dialog.ts

2158 lines
64 KiB
TypeScript

import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiClose,
mdiContentPaste,
mdiPlus,
} from "@mdi/js";
import type {
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
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 { fireEvent } from "../../../common/dom/fire_event";
import { computeAreaName } from "../../../common/entity/compute_area_name";
import { computeDeviceName } from "../../../common/entity/compute_device_name";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display";
import { computeFloorName } from "../../../common/entity/compute_floor_name";
import { stringCompare } from "../../../common/string/compare";
import type {
LocalizeFunc,
LocalizeKeys,
} from "../../../common/translations/localize";
import { computeRTL } from "../../../common/util/compute_rtl";
import { debounce } from "../../../common/util/debounce";
import { deepEqual } from "../../../common/util/deep-equal";
import "../../../components/entity/state-badge";
import "../../../components/ha-bottom-sheet";
import "../../../components/ha-button";
import "../../../components/ha-button-toggle-group";
import "../../../components/ha-combo-box-item";
import { CONDITION_ICONS } from "../../../components/ha-condition-icon";
import "../../../components/ha-dialog-header";
import "../../../components/ha-domain-icon";
import "../../../components/ha-floor-icon";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
import "../../../components/ha-section-title";
import "../../../components/ha-service-icon";
import "../../../components/ha-tooltip";
import { TRIGGER_ICONS } from "../../../components/ha-trigger-icon";
import "../../../components/ha-wa-dialog";
import "../../../components/search-input";
import {
ACTION_BUILDING_BLOCKS_GROUP,
ACTION_COLLECTIONS,
ACTION_ICONS,
} from "../../../data/action";
import type { FloorComboBoxItem } from "../../../data/area_floor";
import {
getAreaDeviceLookup,
getAreaEntityLookup,
} from "../../../data/area_registry";
import {
DYNAMIC_PREFIX,
getValueFromDynamic,
isDynamic,
type AutomationElementGroup,
type AutomationElementGroupCollection,
} from "../../../data/automation";
import type { ConditionDescriptions } from "../../../data/condition";
import {
CONDITION_BUILDING_BLOCKS_GROUP,
CONDITION_COLLECTIONS,
getConditionDomain,
getConditionObjectId,
subscribeConditions,
} from "../../../data/condition";
import {
getConfigEntries,
type ConfigEntry,
} from "../../../data/config_entries";
import { labelsContext } from "../../../data/context";
import { getDeviceEntityLookup } from "../../../data/device_registry";
import type { EntityComboBoxItem } from "../../../data/entity_registry";
import { getFloorAreaLookup } from "../../../data/floor_registry";
import {
getConditionIcons,
getServiceIcons,
getTriggerIcons,
} from "../../../data/icons";
import type { DomainManifestLookup } from "../../../data/integration";
import {
domainToName,
fetchIntegrationManifests,
} from "../../../data/integration";
import type { LabelRegistryEntry } from "../../../data/label_registry";
import { subscribeLabFeatures } from "../../../data/labs";
import {
TARGET_SEPARATOR,
getConditionsForTarget,
getServicesForTarget,
getTargetComboBoxItemType,
getTriggersForTarget,
type SingleHassServiceTarget,
} from "../../../data/target";
import type { TriggerDescriptions } from "../../../data/trigger";
import {
TRIGGER_COLLECTIONS,
getTriggerDomain,
getTriggerObjectId,
subscribeTriggers,
} from "../../../data/trigger";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import type { HomeAssistant } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import "./add-automation-element/ha-automation-add-from-target";
import type HaAutomationAddFromTarget from "./add-automation-element/ha-automation-add-from-target";
import "./add-automation-element/ha-automation-add-items";
import "./add-automation-element/ha-automation-add-search";
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
const TYPES = {
trigger: { collections: TRIGGER_COLLECTIONS, icons: TRIGGER_ICONS },
condition: {
collections: CONDITION_COLLECTIONS,
icons: CONDITION_ICONS,
},
action: {
collections: ACTION_COLLECTIONS,
icons: ACTION_ICONS,
},
};
export interface AutomationItemComboBoxItem extends PickerComboBoxItem {
renderedIcon?: TemplateResult;
type: "trigger" | "condition" | "action" | "block";
}
export interface AddAutomationElementListItem {
key: string;
name: string;
description: string;
iconPath?: string;
icon?: TemplateResult;
}
const ENTITY_DOMAINS_OTHER = new Set([
"date",
"datetime",
"device_tracker",
"text",
"time",
"tts",
"update",
"weather",
"image_processing",
]);
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
const DYNAMIC_KEYWORDS = ["dynamicGroups", "helpers", "other"];
@customElement("add-automation-element-dialog")
class DialogAddAutomationElement
extends KeyboardShortcutMixin(LitElement)
implements HassDialog
{
@property({ attribute: false }) public hass!: HomeAssistant;
// #region state
@state() private _open = true;
@state() private _params?: AddAutomationElementDialogParams;
@state() private _selectedCollectionIndex?: number;
@state() private _selectedGroup?: string;
@state() private _selectedTarget?: SingleHassServiceTarget;
@state() private _tab: "targets" | "groups" | "blocks" = "targets";
@state() private _filter = "";
@state() private _manifests?: DomainManifestLookup;
@state() private _domains?: Set<string>;
@state() private _bottomSheetMode = false;
@state() private _narrow = false;
@state() private _triggerDescriptions: TriggerDescriptions = {};
@state() private _targetItems?: {
title: string;
items: AddAutomationElementListItem[];
}[];
@state() private _loadItemsError = false;
@state() private _newTriggersAndConditions = false;
@state() private _conditionDescriptions: ConditionDescriptions = {};
@state()
@consume({ context: labelsContext, subscribe: true })
private _labelRegistry!: LabelRegistryEntry[];
// #endregion state
// #region queries
@query("ha-automation-add-from-target")
private _targetPickerElement?: HaAutomationAddFromTarget;
@query("ha-automation-add-items")
private _itemsListElement?: HTMLDivElement;
@query(".content")
private _contentElement?: HTMLDivElement;
// #endregion queries
// #region variables
private _unsub?: Promise<UnsubscribeFunc>;
private _unsubscribeLabFeatures?: UnsubscribeFunc;
private _configEntryLookup: Record<string, ConfigEntry> = {};
// #endregion variables
// #region lifecycle
protected willUpdate(changedProps: PropertyValues) {
if (
changedProps.has("hass") &&
changedProps.get("hass")?.states !== this.hass.states
) {
this._calculateUsedDomains();
}
if (changedProps.has("_newTriggersAndConditions")) {
this._subscribeDescriptions();
}
}
private _subscribeDescriptions() {
this._unsubscribe();
if (this._params?.type === "trigger") {
this._triggerDescriptions = {};
this._unsub = subscribeTriggers(this.hass, (triggers) => {
this._triggerDescriptions = {
...this._triggerDescriptions,
...triggers,
};
});
} else if (this._params?.type === "condition") {
this._conditionDescriptions = {};
this._unsub = subscribeConditions(this.hass, (conditions) => {
this._conditionDescriptions = {
...this._conditionDescriptions,
...conditions,
};
});
}
}
public showDialog(params): void {
this._params = params;
this.addKeyboardShortcuts();
this._loadConfigEntries();
this._unsubscribe();
this._fetchManifests();
this._calculateUsedDomains();
this._unsubscribeLabFeatures = subscribeLabFeatures(
this.hass.connection,
(features) => {
this._newTriggersAndConditions =
features.find(
(feature) =>
feature.domain === "automation" &&
feature.preview_feature === "new_triggers_conditions"
)?.enabled ?? false;
this._tab = this._newTriggersAndConditions ? "targets" : "groups";
}
);
if (this._params?.type === "action") {
this.hass.loadBackendTranslation("services");
getServiceIcons(this.hass);
} else if (this._params?.type === "trigger") {
this.hass.loadBackendTranslation("triggers");
getTriggerIcons(this.hass);
this._subscribeDescriptions();
} else if (this._params?.type === "condition") {
this.hass.loadBackendTranslation("conditions");
getConditionIcons(this.hass);
this._subscribeDescriptions();
}
window.addEventListener("resize", this._updateNarrow);
this._updateNarrow();
// prevent view mode switch when resizing window
this._bottomSheetMode = this._narrow;
}
public closeDialog() {
this.removeKeyboardShortcuts();
this._unsubscribe();
if (this._params) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._open = true;
this._params = undefined;
this._selectedCollectionIndex = undefined;
this._selectedGroup = undefined;
this._selectedTarget = undefined;
this._tab = this._newTriggersAndConditions ? "targets" : "groups";
this._filter = "";
this._manifests = undefined;
this._domains = undefined;
this._bottomSheetMode = false;
this._narrow = false;
this._targetItems = undefined;
this._loadItemsError = false;
return true;
}
private _updateNarrow = () => {
this._narrow =
window.matchMedia("(max-width: 870px)").matches ||
window.matchMedia("(max-height: 500px)").matches;
};
private _calculateUsedDomains() {
const domains = new Set(Object.keys(this.hass.states).map(computeDomain));
if (!deepEqual(domains, this._domains)) {
this._domains = domains;
}
}
private async _loadConfigEntries() {
const configEntries = await getConfigEntries(this.hass);
this._configEntryLookup = Object.fromEntries(
configEntries.map((entry) => [entry.entry_id, entry])
);
}
private async _fetchManifests() {
const manifests = {};
const fetched = await fetchIntegrationManifests(this.hass);
for (const manifest of fetched) {
manifests[manifest.domain] = manifest;
}
this._manifests = manifests;
}
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener("resize", this._updateNarrow);
this._unsubscribe();
}
protected supportedShortcuts(): SupportedShortcuts {
return {
v: () => this._addClipboard(),
};
}
private _unsubscribe() {
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
if (this._unsubscribeLabFeatures) {
this._unsubscribeLabFeatures();
this._unsubscribeLabFeatures = undefined;
}
}
// #endregion lifecycle
// #region render
protected render() {
if (!this._params) {
return nothing;
}
if (this._bottomSheetMode) {
return html`
<ha-bottom-sheet
.open=${this._open}
@closed=${this.closeDialog}
flexcontent
>
${this._renderContent()}
</ha-bottom-sheet>
`;
}
return html`
<ha-wa-dialog
width="large"
.open=${this._open}
@closed=${this.closeDialog}
flexcontent
>
${this._renderContent()}
</ha-wa-dialog>
`;
}
private _renderContent() {
const automationElementType = this._params!.type;
const tabButtons = [
{
label: this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.name`
),
value: "groups",
},
];
if (this._newTriggersAndConditions) {
tabButtons.unshift({
label: this.hass.localize(`ui.panel.config.automation.editor.targets`),
value: "targets",
});
}
if (this._params?.type !== "trigger") {
tabButtons.push({
label: this.hass.localize("ui.panel.config.automation.editor.blocks"),
value: "blocks",
});
}
const hideCollections =
this._filter ||
this._tab === "blocks" ||
this._tab === "targets" ||
(this._narrow && this._selectedGroup);
const collections = hideCollections
? []
: this._getCollections(
automationElementType,
TYPES[automationElementType].collections,
this._domains,
this.hass.localize,
this.hass.services,
this._triggerDescriptions,
this._conditionDescriptions,
this._manifests
);
return html`
<div slot="header">
${this._renderHeader()}
${!this._narrow || (!this._selectedGroup && !this._selectedTarget)
? html`
<search-input
?autofocus=${!this._narrow}
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._debounceFilterChanged}
.label=${this.hass.localize(`ui.common.search`)}
></search-input>
`
: nothing}
${!this._filter &&
tabButtons.length > 1 &&
(!this._narrow || (!this._selectedGroup && !this._selectedTarget))
? html`<ha-button-toggle-group
variant="neutral"
active-variant="brand"
.buttons=${tabButtons}
.active=${this._tab}
size="small"
full-width
@value-changed=${this._switchTab}
></ha-button-toggle-group>`
: nothing}
</div>
<div
class=${classMap({
content: true,
column:
this._filter ||
(this._narrow &&
this._selectedTarget &&
Object.values(this._selectedTarget)[0] &&
!this._getAddFromTargetHidden(
this._narrow,
this._selectedTarget
)),
})}
>
${this._filter
? html`<ha-automation-add-search
.hass=${this.hass}
.filter=${this._filter}
.configEntryLookup=${this._configEntryLookup}
.manifests=${this._manifests}
.narrow=${this._narrow}
.addElementType=${this._params!.type}
.items=${this._items(
automationElementType,
this.hass.localize,
this.hass.services,
this._manifests
)}
.convertToItem=${this._convertToItem}
.newTriggersAndConditions=${this._newTriggersAndConditions}
@search-element-picked=${this._searchItemSelected}
>
</ha-automation-add-search>`
: this._tab === "targets"
? html`<ha-automation-add-from-target
.hass=${this.hass}
.value=${this._selectedTarget}
@value-changed=${this._handleTargetSelected}
.narrow=${this._narrow}
class=${this._getAddFromTargetHidden(
this._narrow,
this._selectedTarget
)}
.manifests=${this._manifests}
></ha-automation-add-from-target>`
: html`
<ha-md-list
class=${classMap({
groups: true,
hidden: hideCollections,
})}
>
${this._params!.clipboardItem
? html`<ha-md-list-item
interactive
type="button"
class="paste"
.value=${PASTE_VALUE}
@click=${this._selected}
>
<div class="shortcut-label">
<div class="label">
<div>
${this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.paste`
)}
</div>
<div class="supporting-text">
${this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label`
)}
</div>
</div>
${!this._narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span>V</span>
</span>`
: nothing}
</div>
<ha-svg-icon
slot="start"
.path=${mdiContentPaste}
></ha-svg-icon
><ha-svg-icon
class="plus"
slot="end"
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
<ha-md-divider
role="separator"
tabindex="-1"
></ha-md-divider>`
: nothing}
${collections.map(
(collection, index) => html`
${collection.titleKey && collection.groups.length
? html`<ha-section-title>
${this.hass.localize(collection.titleKey)}
</ha-section-title>`
: nothing}
${repeat(
collection.groups,
(item) => item.key,
(item) => html`
<ha-md-list-item
interactive
type="button"
.value=${item.key}
.index=${index}
@click=${this._groupSelected}
class=${item.key === this._selectedGroup
? "selected"
: ""}
>
<div slot="headline">${item.name}</div>
${item.icon
? html`<span slot="start">${item.icon}</span>`
: item.iconPath
? html`<ha-svg-icon
slot="start"
.path=${item.iconPath}
></ha-svg-icon>`
: nothing}
${this._narrow
? html`<ha-icon-next slot="end"></ha-icon-next>`
: nothing}
</ha-md-list-item>
`
)}
`
)}
</ha-md-list>
`}
${!this._filter
? html`
<ha-automation-add-items
.hass=${this.hass}
.items=${this._getItems()}
.error=${this._tab === "targets" && this._loadItemsError
? this.hass.localize(
"ui.panel.config.automation.editor.load_target_items_failed"
)
: undefined}
.selectLabel=${this.hass.localize(
`ui.panel.config.automation.editor.${this._tab === "groups" ? `${automationElementType}s.select` : "select_target"}` as LocalizeKeys
)}
.emptyLabel=${this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.no_items_for_target`
)}
.tooltipDescription=${this._tab === "targets"}
.target=${(this._tab === "targets" &&
this._selectedTarget &&
([
...this._extractTypeAndIdFromTarget(this._selectedTarget),
this._getSelectedTargetLabel(this._selectedTarget),
] as [string, string | undefined, string | undefined])) ||
undefined}
.getLabel=${this._getLabel}
.configEntryLookup=${this._configEntryLookup}
class=${this._narrow &&
!this._selectedGroup &&
(!this._selectedTarget ||
(this._selectedTarget &&
!Object.values(this._selectedTarget)[0])) &&
this._tab !== "blocks"
? "hidden"
: ""}
@value-changed=${this._selected}
>
</ha-automation-add-items>
`
: nothing}
</div>
`;
}
private _renderHeader() {
return html`
<ha-dialog-header subtitle-position="above">
<span slot="title">${this._getDialogTitle()}</span>
${this._renderDialogSubtitle()}
${this._narrow && (this._selectedGroup || this._selectedTarget)
? html`<ha-icon-button-prev
slot="navigationIcon"
@click=${this._back}
></ha-icon-button-prev>`
: html`<ha-icon-button
.path=${mdiClose}
@click=${this._close}
slot="navigationIcon"
></ha-icon-button>`}
</ha-dialog-header>
`;
}
private _renderDialogSubtitle() {
if (!this._narrow) {
return nothing;
}
if (this._selectedGroup) {
return html`<span slot="subtitle"
>${this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.add`
)}</span
>`;
}
if (this._selectedTarget) {
let subtitle: string | undefined;
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
this._selectedTarget
);
if (targetId) {
if (targetType === "area" && this.hass.areas[targetId]?.floor_id) {
const floorId = this.hass.areas[targetId].floor_id;
subtitle = computeFloorName(this.hass.floors[floorId]) || floorId;
}
if (targetType === "device" && this.hass.devices[targetId]?.area_id) {
const areaId = this.hass.devices[targetId].area_id;
subtitle = computeAreaName(this.hass.areas[areaId]) || areaId;
}
if (targetType === "entity" && this.hass.states[targetId]) {
const entity = this.hass.entities[targetId];
if (entity && !entity.device_id && !entity.area_id) {
const domain = targetId.split(".", 2)[0];
subtitle = domainToName(
this.hass.localize,
domain,
this._manifests?.[domain]
);
} else {
const stateObj = this.hass.states[targetId];
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
subtitle = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
}
}
}
if (subtitle) {
return html`<span slot="subtitle">${subtitle}</span>`;
}
}
return nothing;
}
// #endregion render
// #region data
private _getItems = () =>
!this._filter && this._tab === "blocks"
? [
{
title: this.hass.localize(
"ui.panel.config.automation.editor.blocks"
),
items: this._getBlockItems(this._params!.type, this.hass.localize),
},
]
: !this._filter && this._tab === "groups" && this._selectedGroup
? [
{
title: this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.name`
),
items: this._getGroupItems(
this._params!.type,
this._selectedGroup,
this._selectedCollectionIndex ?? 0,
this._domains,
this.hass.localize,
this.hass.services,
this._manifests
),
},
]
: !this._filter &&
this._tab === "targets" &&
this._selectedTarget &&
this._targetItems
? this._targetItems
: undefined;
private _getGroups = (
type: AddAutomationElementDialogParams["type"],
group?: string,
collectionIndex?: number
): AutomationElementGroup => {
if (group && collectionIndex !== undefined) {
return (
TYPES[type].collections[collectionIndex].groups[group].members || {
[group]: {},
}
);
}
return TYPES[type].collections.reduce(
(acc, collection) => ({ ...acc, ...collection.groups }),
{} as AutomationElementGroup
);
};
private _items = memoizeOne(
(
type: AddAutomationElementDialogParams["type"],
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests?: DomainManifestLookup
): AddAutomationElementListItem[] => {
const groups = this._getGroups(type);
const flattenGroups = (grp: AutomationElementGroup) =>
Object.entries(grp).map(([key, options]) =>
options.members
? flattenGroups(options.members)
: this._convertToItem(key, options, type, localize)
);
const items = flattenGroups(groups).flat();
if (type === "trigger") {
items.push(...this._triggers(localize, this._triggerDescriptions));
} else if (type === "condition") {
items.push(
...this._conditions(localize, this._conditionDescriptions, manifests)
);
} else if (type === "action") {
items.push(...this._services(localize, services, manifests));
}
return items.filter(({ name }) => name);
}
);
private _getCollections = memoizeOne(
(
type: AddAutomationElementDialogParams["type"],
collections: AutomationElementGroupCollection[],
domains: Set<string> | undefined,
localize: LocalizeFunc,
services: HomeAssistant["services"],
triggerDescriptions: TriggerDescriptions,
conditionDescriptions: ConditionDescriptions,
manifests?: DomainManifestLookup
): {
titleKey?: LocalizeKeys;
groups: AddAutomationElementListItem[];
}[] => {
const generatedCollections: any = [];
collections.forEach((collection) => {
let collectionGroups = Object.entries(collection.groups);
const groups: AddAutomationElementListItem[] = [];
if (
type === "trigger" &&
Object.keys(collection.groups).some((item) =>
DYNAMIC_KEYWORDS.includes(item)
)
) {
groups.push(
...this._triggerGroups(
localize,
triggerDescriptions,
manifests,
domains,
collection.groups.dynamicGroups
? undefined
: collection.groups.helpers
? "helper"
: "other"
)
);
collectionGroups = collectionGroups.filter(
([key]) => !DYNAMIC_KEYWORDS.includes(key)
);
} else if (
type === "condition" &&
Object.keys(collection.groups).some((item) =>
DYNAMIC_KEYWORDS.includes(item)
)
) {
groups.push(
...this._conditionGroups(
localize,
conditionDescriptions,
manifests,
domains,
collection.groups.dynamicGroups
? undefined
: collection.groups.helpers
? "helper"
: "other"
)
);
collectionGroups = collectionGroups.filter(
([key]) => !DYNAMIC_KEYWORDS.includes(key)
);
} else if (
type === "action" &&
Object.keys(collection.groups).some((item) =>
DYNAMIC_KEYWORDS.includes(item)
)
) {
groups.push(
...this._serviceGroups(
localize,
services,
manifests,
domains,
collection.groups.dynamicGroups
? undefined
: collection.groups.helpers
? "helper"
: "other"
)
);
collectionGroups = collectionGroups.filter(
([key]) => !DYNAMIC_KEYWORDS.includes(key)
);
}
groups.push(
...collectionGroups.map(([key, options]) =>
this._convertToItem(key, options, type, localize)
)
);
generatedCollections.push({
titleKey: collection.titleKey,
groups: groups.sort((a, b) => {
// make sure device is always on top
if (a.key === "device" || a.key === "device_id") {
return -1;
}
if (b.key === "device" || b.key === "device_id") {
return 1;
}
return stringCompare(a.name, b.name, this.hass.locale.language);
}),
});
});
return generatedCollections;
}
);
private _getBlockItems = memoizeOne(
(
type: AddAutomationElementDialogParams["type"],
localize: LocalizeFunc
): AddAutomationElementListItem[] => {
const groups =
type === "action"
? ACTION_BUILDING_BLOCKS_GROUP
: CONDITION_BUILDING_BLOCKS_GROUP;
const result = Object.entries(groups).map(([key, options]) =>
this._convertToItem(key, options, type, localize)
);
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
}
);
private _getGroupItems = memoizeOne(
(
type: AddAutomationElementDialogParams["type"],
group: string,
collectionIndex: number,
domains: Set<string> | undefined,
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests?: DomainManifestLookup
): AddAutomationElementListItem[] => {
if (type === "trigger" && isDynamic(group)) {
return this._triggers(localize, this._triggerDescriptions, group);
}
if (type === "condition" && isDynamic(group)) {
return this._conditions(
localize,
this._conditionDescriptions,
manifests,
group
);
}
if (type === "action" && isDynamic(group)) {
return this._services(localize, services, manifests, group);
}
const groups = this._getGroups(type, group, collectionIndex);
const result = Object.entries(groups).map(([key, options]) =>
this._convertToItem(key, options, type, localize)
);
if (type === "action") {
if (!this._selectedGroup) {
result.unshift(
...this._serviceGroups(
localize,
services,
manifests,
domains,
undefined
)
);
} else if (this._selectedGroup === "helpers") {
result.unshift(
...this._serviceGroups(
localize,
services,
manifests,
domains,
"helper"
)
);
} else if (this._selectedGroup === "other") {
result.unshift(
...this._serviceGroups(
localize,
services,
manifests,
domains,
"other"
)
);
}
}
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
}
);
private _serviceGroups = (
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests: DomainManifestLookup | undefined,
domains: Set<string> | undefined,
type: "helper" | "other" | undefined
): AddAutomationElementListItem[] => {
if (!services || !manifests) {
return [];
}
const result: AddAutomationElementListItem[] = [];
Object.keys(services).forEach((domain) => {
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 || "")))
) {
result.push({
icon: html`
<ha-domain-icon
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>
`,
key: `${DYNAMIC_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
description: "",
});
}
});
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
};
private _triggerGroups = (
localize: LocalizeFunc,
triggers: TriggerDescriptions,
manifests: DomainManifestLookup | undefined,
domains: Set<string> | undefined,
type: "helper" | "other" | undefined
): AddAutomationElementListItem[] => {
if (!triggers || !manifests) {
return [];
}
const result: AddAutomationElementListItem[] = [];
const addedDomains = new Set<string>();
Object.keys(triggers).forEach((trigger) => {
const domain = getTriggerDomain(trigger);
if (addedDomains.has(domain)) {
return;
}
addedDomains.add(domain);
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 || "")))
) {
result.push({
icon: html`
<ha-domain-icon
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>
`,
key: `${DYNAMIC_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
description: "",
});
}
});
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
};
private _triggers = memoizeOne(
(
localize: LocalizeFunc,
triggers: TriggerDescriptions,
group?: string
): AddAutomationElementListItem[] => {
if (!triggers) {
return [];
}
return this._getTriggerListItems(
localize,
Object.keys(triggers).filter((trigger) => {
const domain = getTriggerDomain(trigger);
return !group || group === `${DYNAMIC_PREFIX}${domain}`;
})
);
}
);
private _conditionGroups = (
localize: LocalizeFunc,
conditions: ConditionDescriptions,
manifests: DomainManifestLookup | undefined,
domains: Set<string> | undefined,
type: "helper" | "other" | undefined
): AddAutomationElementListItem[] => {
if (!conditions || !manifests) {
return [];
}
const result: AddAutomationElementListItem[] = [];
const addedDomains = new Set<string>();
Object.keys(conditions).forEach((condition) => {
const domain = getConditionDomain(condition);
if (addedDomains.has(domain)) {
return;
}
addedDomains.add(domain);
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 || "")))
) {
result.push({
icon: html`
<ha-domain-icon
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>
`,
key: `${DYNAMIC_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
description: "",
});
}
});
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
};
private _conditions = memoizeOne(
(
localize: LocalizeFunc,
conditions: ConditionDescriptions,
_manifests: DomainManifestLookup | undefined,
group?: string
): AddAutomationElementListItem[] => {
if (!conditions) {
return [];
}
const result: AddAutomationElementListItem[] = [];
for (const condition of Object.keys(conditions)) {
const domain = getConditionDomain(condition);
if (group && group !== `${DYNAMIC_PREFIX}${domain}`) {
continue;
}
result.push(this._getConditionListItem(localize, domain, condition));
}
return result;
}
);
private _services = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests: DomainManifestLookup | undefined,
group?: string
): AddAutomationElementListItem[] => {
if (!services) {
return [];
}
const result: AddAutomationElementListItem[] = [];
let domain: string | undefined;
if (isDynamic(group)) {
domain = getValueFromDynamic(group!);
}
const addDomain = (dmn: string) => {
const services_keys = Object.keys(services[dmn]);
for (const service of services_keys) {
result.push({
icon: html`
<ha-service-icon
.hass=${this.hass}
.service=${`${dmn}.${service}`}
></ha-service-icon>
`,
key: `${DYNAMIC_PREFIX}${dmn}.${service}`,
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
this.hass.localize(
`component.${dmn}.services.${service}.name`,
this.hass.services[dmn][service].description_placeholders
) ||
services[dmn][service]?.name ||
service
}`,
description:
this.hass.localize(
`component.${dmn}.services.${service}.description`,
this.hass.services[dmn][service].description_placeholders
) ||
services[dmn][service]?.description ||
"",
});
}
};
if (domain) {
addDomain(domain);
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
}
if (group && !["helpers", "other"].includes(group)) {
return [];
}
Object.keys(services)
.sort()
.forEach((dmn) => {
const manifest = manifests?.[dmn];
if (group === "helpers" && manifest?.integration_type !== "helper") {
return;
}
if (
group === "other" &&
(ENTITY_DOMAINS_OTHER.has(dmn) ||
["helper", "entity"].includes(manifest?.integration_type || ""))
) {
return;
}
addDomain(dmn);
});
return result;
}
);
private _getLabel = memoizeOne((labelId) =>
this._labelRegistry?.find(({ label_id }) => label_id === labelId)
);
private _getDomainType(domain: string) {
return 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";
}
private _sortDomainsByCollection(
type: AddAutomationElementDialogParams["type"],
entries: [
string,
{ title: string; items: AddAutomationElementListItem[] },
][]
): { title: string; items: AddAutomationElementListItem[] }[] {
const order: string[] = [];
TYPES[type].collections.forEach((collection) => {
order.push(...Object.keys(collection.groups));
});
return entries
.sort((a, b) => {
const domainA = a[0];
const domainB = b[0];
if (order.includes(domainA) && order.includes(domainB)) {
return order.indexOf(domainA) - order.indexOf(domainB);
}
let typeA = domainA;
let typeB = domainB;
if (!order.includes(domainA)) {
typeA = this._getDomainType(domainA);
}
if (!order.includes(domainB)) {
typeB = this._getDomainType(domainB);
}
if (typeA === typeB) {
return stringCompare(
a[1].title,
b[1].title,
this.hass.locale.language
);
}
return order.indexOf(typeA) - order.indexOf(typeB);
})
.map((entry) => entry[1]);
}
// #endregion data
// #region data memoize
private _getFloorAreaLookupMemoized = memoizeOne(
(areas: HomeAssistant["areas"]) => getFloorAreaLookup(Object.values(areas))
);
private _getAreaDeviceLookupMemoized = memoizeOne(
(devices: HomeAssistant["devices"]) =>
getAreaDeviceLookup(Object.values(devices))
);
private _getAreaEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) =>
getAreaEntityLookup(Object.values(entities))
);
private _getDeviceEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) =>
getDeviceEntityLookup(Object.values(entities))
);
private _extractTypeAndIdFromTarget = memoizeOne(
(target: SingleHassServiceTarget): [string, string | undefined] => {
const [targetTypeId, targetId] = Object.entries(target)[0];
const targetType = targetTypeId.replace("_id", "");
return [targetType, targetId];
}
);
// #endregion data memoize
// #region render prepare
private _convertToItem = (
key: string,
options,
type: AddAutomationElementDialogParams["type"],
localize: LocalizeFunc
): AddAutomationElementListItem => ({
key,
name: localize(
// @ts-ignore
`ui.panel.config.automation.editor.${type}s.${
options.members ? "groups" : "type"
}.${key}.label`
),
description: localize(
// @ts-ignore
`ui.panel.config.automation.editor.${type}s.${
options.members ? "groups" : "type"
}.${key}.description${options.members ? "" : ".picker"}`
),
iconPath: options.icon || TYPES[type].icons[key],
});
private _getDomainGroupedTriggerListItems(
localize: LocalizeFunc,
triggerIds: string[]
): { title: string; items: AddAutomationElementListItem[] }[] {
const items: Record<
string,
{ title: string; items: AddAutomationElementListItem[] }
> = {};
triggerIds.forEach((trigger) => {
const domain = getTriggerDomain(trigger);
if (!items[domain]) {
items[domain] = {
title: domainToName(localize, domain, this._manifests?.[domain]),
items: [],
};
}
items[domain].items.push(
this._getTriggerListItem(localize, domain, trigger)
);
items[domain].items.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
});
return this._sortDomainsByCollection(
this._params!.type,
Object.entries(items)
);
}
private _getTriggerListItems(
localize: LocalizeFunc,
triggerIds: string[]
): AddAutomationElementListItem[] {
return triggerIds
.map((trigger) => {
const domain = getTriggerDomain(trigger);
return this._getTriggerListItem(localize, domain, trigger);
})
.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language));
}
private _getTriggerListItem(
localize: LocalizeFunc,
domain: string,
trigger: string
): AddAutomationElementListItem {
const triggerName = getTriggerObjectId(trigger);
return {
icon: html`
<ha-trigger-icon
.hass=${this.hass}
.trigger=${trigger}
></ha-trigger-icon>
`,
key: `${DYNAMIC_PREFIX}${trigger}`,
name:
localize(`component.${domain}.triggers.${triggerName}.name`) || trigger,
description:
localize(`component.${domain}.triggers.${triggerName}.description`) ||
trigger,
};
}
private _getConditionListItem(
localize: LocalizeFunc,
domain: string,
condition: string
): AddAutomationElementListItem {
const conditionName = getConditionObjectId(condition);
return {
icon: html`
<ha-condition-icon
.hass=${this.hass}
.condition=${condition}
></ha-condition-icon>
`,
key: `${DYNAMIC_PREFIX}${condition}`,
name:
localize(`component.${domain}.conditions.${conditionName}.name`) ||
condition,
description:
localize(
`component.${domain}.conditions.${conditionName}.description`
) || condition,
};
}
private _getDomainGroupedActionListItems(
localize: LocalizeFunc,
serviceIds: string[]
): { title: string; items: AddAutomationElementListItem[] }[] {
const items: Record<
string,
{ title: string; items: AddAutomationElementListItem[] }
> = {};
serviceIds.forEach((service) => {
const [domain, serviceName] = service.split(".", 2);
if (!items[domain]) {
items[domain] = {
title: domainToName(localize, domain, this._manifests?.[domain]),
items: [],
};
}
items[domain].items.push({
icon: html`
<ha-service-icon
.hass=${this.hass}
.service=${`${domain}.${serviceName}`}
></ha-service-icon>
`,
key: `${DYNAMIC_PREFIX}${domain}.${serviceName}`,
name: `${domain ? "" : `${domainToName(localize, domain)}: `}${
this.hass.localize(
`component.${domain}.services.${serviceName}.name`
) ||
this.hass.services[domain][serviceName]?.name ||
serviceName
}`,
description:
this.hass.localize(
`component.${domain}.services.${serviceName}.description`
) ||
this.hass.services[domain][serviceName]?.description ||
"",
});
items[domain].items.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
});
return this._sortDomainsByCollection(
this._params!.type,
Object.entries(items)
);
}
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
private _close() {
this._open = false;
}
private _back() {
if (this._selectedTarget) {
this._targetPickerElement?.navigateBack();
return;
}
this._selectedGroup = undefined;
}
private _groupSelected(ev) {
const group = ev.currentTarget;
if (this._selectedGroup === group.value) {
this._selectedGroup = undefined;
this._selectedCollectionIndex = undefined;
return;
}
this._selectedGroup = group.value;
this._selectedCollectionIndex = ev.currentTarget.index;
requestAnimationFrame(() => {
this._itemsListElement?.scrollTo(0, 0);
});
}
private _selected(ev: CustomEvent<{ value: string }>) {
let target: HassServiceTarget | undefined;
if (
this._tab === "targets" &&
this._selectedTarget &&
Object.values(this._selectedTarget)[0]
) {
target = this._selectedTarget;
}
this._params!.add(ev.detail.value, target);
this.closeDialog();
}
private _handleTargetSelected = (
ev: CustomEvent<{ value: SingleHassServiceTarget }>
) => {
this._targetItems = undefined;
this._loadItemsError = false;
this._selectedTarget = ev.detail.value;
requestAnimationFrame(() => {
if (this._narrow) {
this._contentElement?.scrollTo(0, 0);
} else {
this._itemsListElement?.scrollTo(0, 0);
}
});
this._getItemsByTarget();
};
private async _getItemsByTarget() {
if (!this._selectedTarget) {
return;
}
try {
if (this._params!.type === "trigger") {
const items = await getTriggersForTarget(
this.hass.callWS,
this._selectedTarget
);
this._targetItems = this._getDomainGroupedTriggerListItems(
this.hass.localize,
items
);
return;
}
if (this._params!.type === "condition") {
const items = await getConditionsForTarget(
this.hass.callWS,
this._selectedTarget
);
this._targetItems = this._getDomainGroupedConditionListItems(
this.hass.localize,
items
);
return;
}
if (this._params!.type === "action") {
const items: string[] = await getServicesForTarget(
this.hass.callWS,
this._selectedTarget
);
const filteredItems = items.filter(
// homeassistant services are too generic to be applied on the selected target
(service) => !service.startsWith("homeassistant.")
);
this._targetItems = this._getDomainGroupedActionListItems(
this.hass.localize,
filteredItems
);
}
} catch (err) {
this._loadItemsError = true;
// eslint-disable-next-line no-console
console.error(`Error fetching ${this._params!.type}s for target`, err);
}
}
private _debounceFilterChanged = debounce(
(ev) => this._filterChanged(ev),
200
);
private _filterChanged = (ev) => {
this._filter = ev.detail.value;
};
private _addClipboard = () => {
if (this._params?.clipboardItem) {
this._params!.add(PASTE_VALUE);
showToast(this, {
message: this.hass.localize(
"ui.panel.config.automation.editor.item_pasted",
{
item: this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`
),
}
),
});
this.closeDialog();
}
};
private _switchTab(ev) {
this._tab = ev.detail.value;
}
private _searchItemSelected(
ev: CustomEvent<PickerComboBoxItem | FloorComboBoxItem | EntityComboBoxItem>
) {
const item = ev.detail;
if (
(item as AutomationItemComboBoxItem).type &&
!["floor", "area"].includes((item as AutomationItemComboBoxItem).type)
) {
this._params!.add(item.id);
this.closeDialog();
return;
}
const targetType = getTargetComboBoxItemType(item);
this._filter = "";
this._selectedTarget = {
[`${targetType}_id`]: item.id.split(TARGET_SEPARATOR, 2)[1],
};
this._tab = "targets";
}
// #region interaction
// #region render helpers
private _getSelectedTargetLabel = memoizeOne(
(selectedTarget: SingleHassServiceTarget): string | undefined => {
const [targetType, targetId] =
this._extractTypeAndIdFromTarget(selectedTarget);
if (targetId === undefined && targetType === "floor") {
return this.hass.localize(
"ui.panel.config.automation.editor.other_areas"
);
}
if (targetId === undefined && targetType === "area") {
return this.hass.localize(
"ui.panel.config.automation.editor.unassigned_devices"
);
}
if (targetId === undefined && targetType === "service") {
return this.hass.localize("ui.panel.config.automation.editor.services");
}
if (targetId === undefined && targetType === "device") {
return this.hass.localize(
"ui.panel.config.automation.editor.unassigned_entities"
);
}
if (targetId === undefined && targetType === "helper") {
return this.hass.localize("ui.panel.config.automation.editor.helpers");
}
if (
targetId === undefined &&
(targetType.startsWith("entity_") || targetType.startsWith("helper_"))
) {
const domain = targetType.substring(7);
return domainToName(
this.hass.localize,
domain,
this._manifests?.[domain]
);
}
if (targetId) {
if (targetType === "floor") {
return computeFloorName(this.hass.floors[targetId]) || targetId;
}
if (targetType === "area") {
return computeAreaName(this.hass.areas[targetId]) || targetId;
}
if (targetType === "device") {
return computeDeviceName(this.hass.devices[targetId]) || targetId;
}
if (targetType === "entity" && this.hass.states[targetId]) {
const stateObj = this.hass.states[targetId];
const [entityName, deviceName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
return entityName || deviceName || targetId;
}
if (targetType === "label") {
const label = this._getLabel(targetId);
return label?.name || targetId;
}
}
return undefined;
}
);
private _getDialogTitle() {
if (this._narrow && this._selectedGroup) {
return isDynamic(this._selectedGroup)
? domainToName(
this.hass.localize,
getValueFromDynamic(this._selectedGroup!),
this._manifests?.[getValueFromDynamic(this._selectedGroup!)]
)
: this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys
) ||
this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.type.${this._selectedGroup}.label` as LocalizeKeys
);
}
if (this._narrow && this._selectedTarget) {
const targetTitle = this._getSelectedTargetLabel(this._selectedTarget);
if (targetTitle) {
return targetTitle;
}
}
return this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.add`
);
}
private _getAddFromTargetHidden = memoizeOne(
(narrow: boolean, target?: SingleHassServiceTarget) => {
if (narrow && target) {
const [targetType, targetId] = this._extractTypeAndIdFromTarget(target);
if (
targetId &&
((targetType === "floor" &&
!(
this._getFloorAreaLookupMemoized(this.hass.areas)[targetId]
?.length > 0
)) ||
(targetType === "area" &&
!(
this._getAreaDeviceLookupMemoized(this.hass.devices)[targetId]
?.length > 0
) &&
!(
this._getAreaEntityLookupMemoized(this.hass.entities)[targetId]
?.length > 0
)) ||
(targetType === "device" &&
!(
this._getDeviceEntityLookupMemoized(this.hass.entities)[
targetId
]?.length > 0
)) ||
targetType === "entity" ||
targetType === "label")
) {
return "hidden";
}
}
return "";
}
);
// #endregion render helpers
// #region styles
static get styles(): CSSResultGroup {
return [
css`
ha-bottom-sheet {
--ha-bottom-sheet-height: 90vh;
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
--ha-bottom-sheet-max-width: 888px;
--ha-bottom-sheet-padding: var(--ha-space-0);
--ha-bottom-sheet-surface-background: var(--card-background-color);
}
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-0);
--ha-dialog-min-height: min(
800px,
calc(
100vh - max(
var(--safe-area-inset-bottom),
var(--ha-space-4)
) - max(var(--safe-area-inset-top), var(--ha-space-4))
)
);
--ha-dialog-min-height: min(
800px,
calc(
100dvh - max(
var(--safe-area-inset-bottom),
var(--ha-space-4)
) - max(var(--safe-area-inset-top), var(--ha-space-4))
)
);
--ha-dialog-max-height: var(--ha-dialog-min-height);
}
search-input {
display: block;
margin: var(--ha-space-0) var(--ha-space-4);
}
ha-button-toggle-group {
--ha-button-toggle-group-padding: var(--ha-space-3) var(--ha-space-4)
0;
}
.content {
flex: 1;
min-height: 0;
height: 100%;
display: flex;
}
.content.column {
flex-direction: column;
}
ha-md-list {
padding: 0;
}
ha-automation-add-from-target,
.groups {
border-radius: var(--ha-border-radius-xl);
border: 1px solid var(--ha-color-border-neutral-quiet);
margin: var(--ha-space-3);
}
ha-automation-add-from-target,
.groups {
overflow: auto;
flex: 4;
margin-inline-end: var(--ha-space-0);
}
ha-automation-add-from-target.hidden {
display: none;
}
.groups {
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-1);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-family-body);
--md-list-item-one-line-container-height: var(--ha-space-10);
}
ha-bottom-sheet .groups,
ha-bottom-sheet ha-automation-add-from-target {
margin: var(--ha-space-3);
}
.groups .selected {
background-color: var(--ha-color-fill-primary-normal-active);
--md-list-item-label-text-color: var(--ha-color-on-primary-normal);
--icon-primary-color: var(--ha-color-on-primary-normal);
}
.groups .selected ha-svg-icon {
color: var(--ha-color-on-primary-normal);
}
ha-section-title {
top: 0;
position: sticky;
z-index: 1;
}
ha-automation-add-items {
flex: 6;
}
.content.column ha-automation-add-from-target,
.content.column ha-automation-add-items {
flex: none;
}
.content.column ha-automation-add-items {
min-height: 160px;
}
.content.column ha-automation-add-from-target {
overflow: hidden;
}
ha-wa-dialog ha-automation-add-items {
margin-top: var(--ha-space-3);
}
ha-bottom-sheet .groups {
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-4));
}
ha-automation-add-items.hidden,
.groups.hidden {
display: none;
}
.groups {
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
}
ha-icon-next {
width: var(--ha-space-6);
}
ha-md-list-item.paste {
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
}
ha-svg-icon.plus {
color: var(--primary-color);
}
.shortcut-label {
display: flex;
gap: var(--ha-space-3);
justify-content: space-between;
}
.shortcut-label .supporting-text {
color: var(--secondary-text-color);
font-size: var(--ha-font-size-s);
}
.shortcut-label .shortcut {
--mdc-icon-size: var(--ha-space-3);
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
.shortcut-label .shortcut span {
font-size: var(--ha-font-size-s);
font-family: var(--ha-font-family-code);
color: var(--ha-color-text-secondary);
}
.section-title-wrapper {
height: 0;
position: relative;
}
.section-title-wrapper ha-section-title {
position: absolute;
top: 0;
width: calc(100% - var(--ha-space-4));
z-index: 1;
}
ha-automation-add-search {
flex: 1;
}
`,
];
}
// #endregion styles
}
declare global {
interface HTMLElementTagNameMap {
"add-automation-element-dialog": DialogAddAutomationElement;
}
}