diff --git a/gallery/src/pages/automation/describe-trigger.ts b/gallery/src/pages/automation/describe-trigger.ts index d02d1a265b..0eab67e81f 100644 --- a/gallery/src/pages/automation/describe-trigger.ts +++ b/gallery/src/pages/automation/describe-trigger.ts @@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../../../src/components/ha-card"; import "../../../../src/components/ha-yaml-editor"; -import type { Trigger } from "../../../../src/data/automation"; +import type { LegacyTrigger } from "../../../../src/data/automation"; import { describeTrigger } from "../../../../src/data/automation_i18n"; import { getEntity } from "../../../../src/fake_data/entity"; import { provideHass } from "../../../../src/fake_data/provide_hass"; @@ -66,7 +66,7 @@ const triggers = [ }, ]; -const initialTrigger: Trigger = { +const initialTrigger: LegacyTrigger = { trigger: "state", entity_id: "light.kitchen", }; diff --git a/src/components/ha-selector/ha-selector-state.ts b/src/components/ha-selector/ha-selector-state.ts index aa8859d0d8..539eff3185 100644 --- a/src/components/ha-selector/ha-selector-state.ts +++ b/src/components/ha-selector/ha-selector-state.ts @@ -1,6 +1,8 @@ +import type { HassServiceTarget } from "home-assistant-js-websocket"; import { html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import type { StateSelector } from "../../data/selector"; +import { extractFromTarget } from "../../data/target"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../types"; import "../entity/ha-entity-state-picker"; @@ -25,15 +27,29 @@ export class HaSelectorState extends SubscribeMixin(LitElement) { @property({ attribute: false }) public context?: { filter_attribute?: string; filter_entity?: string | string[]; + filter_target?: HassServiceTarget; }; + @state() private _entityIds?: string | string[]; + + willUpdate(changedProps) { + if (changedProps.has("selector") || changedProps.has("context")) { + this._resolveEntityIds( + this.selector.state?.entity_id, + this.context?.filter_entity, + this.context?.filter_target + ).then((entityIds) => { + this._entityIds = entityIds; + }); + } + } + protected render() { if (this.selector.state?.multiple) { return html` `; } + + private async _resolveEntityIds( + selectorEntityId: string | string[] | undefined, + contextFilterEntity: string | string[] | undefined, + contextFilterTarget: HassServiceTarget | undefined + ): Promise { + if (selectorEntityId !== undefined) { + return selectorEntityId; + } + if (contextFilterEntity !== undefined) { + return contextFilterEntity; + } + if (contextFilterTarget !== undefined) { + const result = await extractFromTarget(this.hass, contextFilterTarget); + return result.referenced_entities; + } + return undefined; + } } declare global { diff --git a/src/components/ha-trigger-icon.ts b/src/components/ha-trigger-icon.ts new file mode 100644 index 0000000000..0e2394427b --- /dev/null +++ b/src/components/ha-trigger-icon.ts @@ -0,0 +1,97 @@ +import { + mdiAvTimer, + mdiCalendar, + mdiClockOutline, + mdiCodeBraces, + mdiDevices, + mdiFormatListBulleted, + mdiGestureDoubleTap, + mdiHomeAssistant, + mdiMapMarker, + mdiMapMarkerRadius, + mdiMessageAlert, + mdiMicrophoneMessage, + mdiNfcVariant, + mdiNumeric, + mdiStateMachine, + mdiSwapHorizontal, + mdiWeatherSunny, + mdiWebhook, +} from "@mdi/js"; +import { html, LitElement, nothing } from "lit"; +import { customElement, property } from "lit/decorators"; +import { until } from "lit/directives/until"; +import { computeDomain } from "../common/entity/compute_domain"; +import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons"; +import type { HomeAssistant } from "../types"; +import "./ha-icon"; +import "./ha-svg-icon"; + +export const TRIGGER_ICONS = { + calendar: mdiCalendar, + device: mdiDevices, + event: mdiGestureDoubleTap, + state: mdiStateMachine, + geo_location: mdiMapMarker, + homeassistant: mdiHomeAssistant, + mqtt: mdiSwapHorizontal, + numeric_state: mdiNumeric, + sun: mdiWeatherSunny, + conversation: mdiMicrophoneMessage, + tag: mdiNfcVariant, + template: mdiCodeBraces, + time: mdiClockOutline, + time_pattern: mdiAvTimer, + webhook: mdiWebhook, + persistent_notification: mdiMessageAlert, + zone: mdiMapMarkerRadius, + list: mdiFormatListBulleted, +}; + +@customElement("ha-trigger-icon") +export class HaTriggerIcon extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public trigger?: string; + + @property() public icon?: string; + + protected render() { + if (this.icon) { + return html``; + } + + if (!this.trigger) { + return nothing; + } + + if (!this.hass) { + return this._renderFallback(); + } + + const icon = triggerIcon(this.hass, this.trigger).then((icn) => { + if (icn) { + return html``; + } + return this._renderFallback(); + }); + + return html`${until(icon)}`; + } + + private _renderFallback() { + const domain = computeDomain(this.trigger!); + + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-trigger-icon": HaTriggerIcon; + } +} diff --git a/src/data/action.ts b/src/data/action.ts index 3293ad4dac..0f1c733b63 100644 --- a/src/data/action.ts +++ b/src/data/action.ts @@ -50,7 +50,7 @@ export const ACTION_COLLECTIONS: AutomationElementGroupCollection[] = [ { groups: { device_id: {}, - serviceGroups: {}, + dynamicGroups: {}, }, }, { @@ -117,14 +117,6 @@ export const VIRTUAL_ACTIONS: Partial< }, } as const; -export const SERVICE_PREFIX = "__SERVICE__"; - -export const isService = (key: string | undefined): boolean | undefined => - key?.startsWith(SERVICE_PREFIX); - -export const getService = (key: string): string => - key.substring(SERVICE_PREFIX.length); - export const COLLAPSIBLE_ACTION_ELEMENTS = [ "ha-automation-action-choose", "ha-automation-action-condition", diff --git a/src/data/automation.ts b/src/data/automation.ts index 0b793c750e..a19e149706 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -1,8 +1,10 @@ import type { HassEntityAttributeBase, HassEntityBase, + HassServiceTarget, } from "home-assistant-js-websocket"; import { ensureArray } from "../common/array/ensure-array"; +import type { WeekdayShort } from "../common/datetime/weekday"; import { navigate } from "../common/navigate"; import type { LocalizeKeys } from "../common/translations/localize"; import { createSearchParam } from "../common/url/search-params"; @@ -12,11 +14,19 @@ import { CONDITION_BUILDING_BLOCKS } from "./condition"; import type { DeviceCondition, DeviceTrigger } from "./device_automation"; import type { Action, Field, MODES } from "./script"; import { migrateAutomationAction } from "./script"; -import type { WeekdayShort } from "../common/datetime/weekday"; +import type { TriggerDescription } from "./trigger"; export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single"; export const AUTOMATION_DEFAULT_MAX = 10; +export const DYNAMIC_PREFIX = "__DYNAMIC__"; + +export const isDynamic = (key: string | undefined): boolean | undefined => + key?.startsWith(DYNAMIC_PREFIX); + +export const getValueFromDynamic = (key: string): string => + key.substring(DYNAMIC_PREFIX.length); + export interface AutomationEntity extends HassEntityBase { attributes: HassEntityAttributeBase & { id?: string; @@ -86,6 +96,12 @@ export interface BaseTrigger { id?: string; variables?: Record; enabled?: boolean; + options?: Record; +} + +export interface PlatformTrigger extends BaseTrigger { + trigger: Exclude; + target?: HassServiceTarget; } export interface StateTrigger extends BaseTrigger { @@ -195,7 +211,7 @@ export interface CalendarTrigger extends BaseTrigger { offset: string; } -export type Trigger = +export type LegacyTrigger = | StateTrigger | MqttTrigger | GeoLocationTrigger @@ -212,8 +228,9 @@ export type Trigger = | TemplateTrigger | EventTrigger | DeviceTrigger - | CalendarTrigger - | TriggerList; + | CalendarTrigger; + +export type Trigger = LegacyTrigger | TriggerList | PlatformTrigger; interface BaseCondition { condition: string; @@ -575,6 +592,7 @@ export interface TriggerSidebarConfig extends BaseSidebarConfig { insertAfter: (value: Trigger | Trigger[]) => boolean; toggleYamlMode: () => void; config: Trigger; + description?: TriggerDescription; yamlMode: boolean; uiSupported: boolean; } diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index 55289b38c7..fb8413dfd7 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -16,8 +16,9 @@ import { formatListWithAnds, formatListWithOrs, } from "../common/string/format-list"; +import { hasTemplate } from "../common/string/has-template"; import type { HomeAssistant } from "../types"; -import type { Condition, ForDict, Trigger } from "./automation"; +import type { Condition, ForDict, LegacyTrigger, Trigger } from "./automation"; import type { DeviceCondition, DeviceTrigger } from "./device_automation"; import { localizeDeviceAutomationCondition, @@ -25,8 +26,7 @@ import { } from "./device_automation"; import type { EntityRegistryEntry } from "./entity_registry"; import type { FrontendLocaleData } from "./translation"; -import { isTriggerList } from "./trigger"; -import { hasTemplate } from "../common/string/has-template"; +import { getTriggerDomain, getTriggerObjectId, isTriggerList } from "./trigger"; const triggerTranslationBaseKey = "ui.panel.config.automation.editor.triggers.type"; @@ -121,6 +121,37 @@ const tryDescribeTrigger = ( return trigger.alias; } + const description = describeLegacyTrigger( + trigger as LegacyTrigger, + hass, + entityRegistry + ); + + if (description) { + return description; + } + + const triggerType = trigger.trigger; + + const domain = getTriggerDomain(trigger.trigger); + const type = getTriggerObjectId(trigger.trigger); + + return ( + hass.localize( + `component.${domain}.triggers.${type}.description_configured` + ) || + hass.localize( + `ui.panel.config.automation.editor.triggers.type.${triggerType as LegacyTrigger["trigger"]}.label` + ) || + hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`) + ); +}; + +const describeLegacyTrigger = ( + trigger: LegacyTrigger, + hass: HomeAssistant, + entityRegistry: EntityRegistryEntry[] +) => { // Event Trigger if (trigger.trigger === "event" && trigger.event_type) { const eventTypes: string[] = []; @@ -802,13 +833,7 @@ const tryDescribeTrigger = ( } ); } - - return ( - hass.localize( - `ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label` - ) || - hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`) - ); + return undefined; }; export const describeCondition = ( diff --git a/src/data/icons.ts b/src/data/icons.ts index 2384d45ab7..7e2371c488 100644 --- a/src/data/icons.ts +++ b/src/data/icons.ts @@ -59,6 +59,7 @@ import type { } from "./entity_registry"; import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg"; +import { getTriggerDomain, getTriggerObjectId } from "./trigger"; /** Icon to use when no icon specified for service. */ export const DEFAULT_SERVICE_ICON = mdiRoomService; @@ -133,14 +134,19 @@ const resources: { all?: Promise>; domains: Record>; }; + triggers: { + all?: Promise>; + domains: Record>; + }; } = { entity: {}, entity_component: {}, services: { domains: {} }, + triggers: { domains: {} }, }; interface IconResources< - T extends ComponentIcons | PlatformIcons | ServiceIcons, + T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons, > { resources: Record; } @@ -184,12 +190,22 @@ type ServiceIcons = Record< { service: string; sections?: Record } >; -export type IconCategory = "entity" | "entity_component" | "services"; +type TriggerIcons = Record< + string, + { trigger: string; sections?: Record } +>; + +export type IconCategory = + | "entity" + | "entity_component" + | "services" + | "triggers"; interface CategoryType { entity: PlatformIcons; entity_component: ComponentIcons; services: ServiceIcons; + triggers: TriggerIcons; } export const getHassIcons = async ( @@ -258,42 +274,59 @@ export const getComponentIcons = async ( return resources.entity_component.resources.then((res) => res[domain]); }; -export const getServiceIcons = async ( +export const getCategoryIcons = async < + T extends Exclude, +>( hass: HomeAssistant, + category: T, domain?: string, force = false -): Promise | undefined> => { +): Promise | undefined> => { if (!domain) { - if (!force && resources.services.all) { - return resources.services.all; + if (!force && resources[category].all) { + return resources[category].all as Promise< + Record + >; } - resources.services.all = getHassIcons(hass, "services", domain).then( - (res) => { - resources.services.domains = res.resources; - return res?.resources; - } - ); - return resources.services.all; + resources[category].all = getHassIcons(hass, category).then((res) => { + resources[category].domains = res.resources as any; + return res?.resources as Record; + }) as any; + return resources[category].all as Promise>; } - if (!force && domain in resources.services.domains) { - return resources.services.domains[domain]; + if (!force && domain in resources[category].domains) { + return resources[category].domains[domain] as Promise; } - if (resources.services.all && !force) { - await resources.services.all; - if (domain in resources.services.domains) { - return resources.services.domains[domain]; + if (resources[category].all && !force) { + await resources[category].all; + if (domain in resources[category].domains) { + return resources[category].domains[domain] as Promise; } } if (!isComponentLoaded(hass, domain)) { return undefined; } - const result = getHassIcons(hass, "services", domain); - resources.services.domains[domain] = result.then( + const result = getHassIcons(hass, category, domain); + resources[category].domains[domain] = result.then( (res) => res?.resources[domain] - ); - return resources.services.domains[domain]; + ) as any; + return resources[category].domains[domain] as Promise; }; +export const getServiceIcons = async ( + hass: HomeAssistant, + domain?: string, + force = false +): Promise | undefined> => + getCategoryIcons(hass, "services", domain, force); + +export const getTriggerIcons = async ( + hass: HomeAssistant, + domain?: string, + force = false +): Promise | undefined> => + getCategoryIcons(hass, "triggers", domain, force); + // Cache for sorted range keys const sortedRangeCache = new WeakMap, number[]>(); @@ -473,6 +506,26 @@ export const attributeIcon = async ( return icon; }; +export const triggerIcon = async ( + hass: HomeAssistant, + trigger: string +): Promise => { + let icon: string | undefined; + + const domain = getTriggerDomain(trigger); + const triggerName = getTriggerObjectId(trigger); + + const triggerIcons = await getTriggerIcons(hass, domain); + if (triggerIcons) { + const trgrIcon = triggerIcons[triggerName] as TriggerIcons[string]; + icon = trgrIcon?.trigger; + } + if (!icon) { + icon = await domainIcon(hass, domain); + } + return icon; +}; + export const serviceIcon = async ( hass: HomeAssistant, service: string diff --git a/src/data/translation.ts b/src/data/translation.ts index 35ef525260..3d11ee094f 100644 --- a/src/data/translation.ts +++ b/src/data/translation.ts @@ -73,7 +73,8 @@ export type TranslationCategory = | "application_credentials" | "issues" | "selector" - | "services"; + | "services" + | "triggers"; export const subscribeTranslationPreferences = ( hass: HomeAssistant, diff --git a/src/data/trigger.ts b/src/data/trigger.ts index 1eeb7361be..b3ac232be9 100644 --- a/src/data/trigger.ts +++ b/src/data/trigger.ts @@ -1,57 +1,20 @@ -import { - mdiAvTimer, - mdiCalendar, - mdiClockOutline, - mdiCodeBraces, - mdiDevices, - mdiFormatListBulleted, - mdiGestureDoubleTap, - mdiMapClock, - mdiMapMarker, - mdiMapMarkerRadius, - mdiMessageAlert, - mdiMicrophoneMessage, - mdiNfcVariant, - mdiNumeric, - mdiShape, - mdiStateMachine, - mdiSwapHorizontal, - mdiWeatherSunny, - mdiWebhook, -} from "@mdi/js"; +import { mdiMapClock, mdiShape } from "@mdi/js"; -import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg"; +import { computeDomain } from "../common/entity/compute_domain"; +import { computeObjectId } from "../common/entity/compute_object_id"; +import type { HomeAssistant } from "../types"; import type { AutomationElementGroupCollection, Trigger, TriggerList, } from "./automation"; - -export const TRIGGER_ICONS = { - calendar: mdiCalendar, - device: mdiDevices, - event: mdiGestureDoubleTap, - state: mdiStateMachine, - geo_location: mdiMapMarker, - homeassistant: mdiHomeAssistant, - mqtt: mdiSwapHorizontal, - numeric_state: mdiNumeric, - sun: mdiWeatherSunny, - conversation: mdiMicrophoneMessage, - tag: mdiNfcVariant, - template: mdiCodeBraces, - time: mdiClockOutline, - time_pattern: mdiAvTimer, - webhook: mdiWebhook, - persistent_notification: mdiMessageAlert, - zone: mdiMapMarkerRadius, - list: mdiFormatListBulleted, -}; +import type { Selector, TargetSelector } from "./selector"; export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [ { groups: { device: {}, + dynamicGroups: {}, entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } }, time_location: { icon: mdiMapClock, @@ -83,3 +46,33 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [ export const isTriggerList = (trigger: Trigger): trigger is TriggerList => "triggers" in trigger; + +export interface TriggerDescription { + target?: TargetSelector["target"]; + fields: Record< + string, + { + example?: string | boolean | number; + default?: unknown; + required?: boolean; + selector?: Selector; + context?: Record; + } + >; +} + +export type TriggerDescriptions = Record; + +export const subscribeTriggers = ( + hass: HomeAssistant, + callback: (triggers: TriggerDescriptions) => void +) => + hass.connection.subscribeMessage(callback, { + type: "trigger_platforms/subscribe", + }); + +export const getTriggerDomain = (trigger: string) => + trigger.includes(".") ? computeDomain(trigger) : trigger; + +export const getTriggerObjectId = (trigger: string) => + trigger.includes(".") ? computeObjectId(trigger) : "_"; diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts index 23eb1c8ec9..515dcf91c8 100644 --- a/src/panels/config/automation/action/ha-automation-action.ts +++ b/src/panels/config/automation/action/ha-automation-action.ts @@ -14,11 +14,13 @@ import "../../../../components/ha-sortable"; import "../../../../components/ha-svg-icon"; import { ACTION_BUILDING_BLOCKS, - getService, - isService, VIRTUAL_ACTIONS, } from "../../../../data/action"; -import type { AutomationClipboard } from "../../../../data/automation"; +import { + getValueFromDynamic, + isDynamic, + type AutomationClipboard, +} from "../../../../data/automation"; import type { Action } from "../../../../data/script"; import type { HomeAssistant } from "../../../../types"; import { @@ -217,9 +219,9 @@ export default class HaAutomationAction extends LitElement { actions = this.actions.concat(deepClone(this._clipboard!.action)); } else if (action in VIRTUAL_ACTIONS) { actions = this.actions.concat(VIRTUAL_ACTIONS[action]); - } else if (isService(action)) { + } else if (isDynamic(action)) { actions = this.actions.concat({ - action: getService(action), + action: getValueFromDynamic(action), metadata: {}, }); } else { diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index 96fd43225b..fb92041a93 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -5,6 +5,7 @@ import { mdiPlus, } from "@mdi/js"; import Fuse from "fuse.js"; +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { @@ -40,32 +41,39 @@ import "../../../components/ha-md-list"; import type { HaMdList } from "../../../components/ha-md-list"; import "../../../components/ha-md-list-item"; import "../../../components/ha-service-icon"; +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, - SERVICE_PREFIX, - getService, - isService, } from "../../../data/action"; -import type { - AutomationElementGroup, - AutomationElementGroupCollection, +import { + DYNAMIC_PREFIX, + getValueFromDynamic, + isDynamic, + type AutomationElementGroup, + type AutomationElementGroupCollection, } from "../../../data/automation"; import { CONDITION_BUILDING_BLOCKS_GROUP, CONDITION_COLLECTIONS, CONDITION_ICONS, } from "../../../data/condition"; -import { getServiceIcons } from "../../../data/icons"; +import { getServiceIcons, getTriggerIcons } from "../../../data/icons"; import type { IntegrationManifest } from "../../../data/integration"; import { domainToName, fetchIntegrationManifests, } from "../../../data/integration"; -import { TRIGGER_COLLECTIONS, TRIGGER_ICONS } from "../../../data/trigger"; +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 { HaFuse } from "../../../resources/fuse"; @@ -111,7 +119,7 @@ const ENTITY_DOMAINS_OTHER = new Set([ const ENTITY_DOMAINS_MAIN = new Set(["notify"]); -const ACTION_SERVICE_KEYWORDS = ["serviceGroups", "helpers", "other"]; +const ACTION_SERVICE_KEYWORDS = ["dynamicGroups", "helpers", "other"]; @customElement("add-automation-element-dialog") class DialogAddAutomationElement @@ -142,6 +150,8 @@ class DialogAddAutomationElement @state() private _narrow = false; + @state() private _triggerDescriptions: TriggerDescriptions = {}; + @query(".items ha-md-list ha-md-list-item") private _itemsListFirstElement?: HaMdList; @@ -152,6 +162,8 @@ class DialogAddAutomationElement private _removeKeyboardShortcuts?: () => void; + private _unsub?: Promise; + public showDialog(params): void { this._params = params; @@ -163,6 +175,17 @@ class DialogAddAutomationElement this._calculateUsedDomains(); getServiceIcons(this.hass); } + if (this._params?.type === "trigger") { + this.hass.loadBackendTranslation("triggers"); + this._fetchManifests(); + getTriggerIcons(this.hass); + this._unsub = subscribeTriggers(this.hass, (triggers) => { + this._triggerDescriptions = { + ...this._triggerDescriptions, + ...triggers, + }; + }); + } this._fullScreen = matchMedia( "all and (max-width: 450px), all and (max-height: 500px)" ).matches; @@ -176,6 +199,10 @@ class DialogAddAutomationElement public closeDialog() { this.removeKeyboardShortcuts(); + if (this._unsub) { + this._unsub.then((unsub) => unsub()); + this._unsub = undefined; + } if (this._params) { fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -317,6 +344,11 @@ class DialogAddAutomationElement ); const items = flattenGroups(groups).flat(); + if (type === "trigger") { + items.push( + ...this._triggers(localize, this._triggerDescriptions, manifests) + ); + } if (type === "action") { items.push(...this._services(localize, services, manifests)); } @@ -339,6 +371,7 @@ class DialogAddAutomationElement domains: Set | undefined, localize: LocalizeFunc, services: HomeAssistant["services"], + triggerDescriptions: TriggerDescriptions, manifests?: DomainManifestLookup ): { titleKey?: LocalizeKeys; @@ -362,7 +395,32 @@ class DialogAddAutomationElement services, manifests, domains, - collection.groups.serviceGroups + collection.groups.dynamicGroups + ? undefined + : collection.groups.helpers + ? "helper" + : "other" + ) + ); + + collectionGroups = collectionGroups.filter( + ([key]) => !ACTION_SERVICE_KEYWORDS.includes(key) + ); + } + + if ( + type === "trigger" && + Object.keys(collection.groups).some((item) => + ACTION_SERVICE_KEYWORDS.includes(item) + ) + ) { + groups.push( + ...this._triggerGroups( + localize, + triggerDescriptions, + manifests, + domains, + collection.groups.dynamicGroups ? undefined : collection.groups.helpers ? "helper" @@ -429,10 +487,19 @@ class DialogAddAutomationElement services: HomeAssistant["services"], manifests?: DomainManifestLookup ): ListItem[] => { - if (type === "action" && isService(group)) { + if (type === "action" && isDynamic(group)) { return this._services(localize, services, manifests, group); } + if (type === "trigger" && isDynamic(group)) { + return this._triggers( + localize, + this._triggerDescriptions, + manifests, + group + ); + } + const groups = this._getGroups(type, group, collectionIndex); const result = Object.entries(groups).map(([key, options]) => @@ -514,7 +581,7 @@ class DialogAddAutomationElement brand-fallback > `, - key: `${SERVICE_PREFIX}${domain}`, + key: `${DYNAMIC_PREFIX}${domain}`, name: domainToName(localize, domain, manifest), description: "", }); @@ -525,6 +592,102 @@ class DialogAddAutomationElement ); }; + private _triggerGroups = ( + localize: LocalizeFunc, + triggers: TriggerDescriptions, + manifests: DomainManifestLookup | undefined, + domains: Set | undefined, + type: "helper" | "other" | undefined + ): ListItem[] => { + if (!triggers || !manifests) { + return []; + } + const result: ListItem[] = []; + const addedDomains = new Set(); + 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` + + `, + 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, + _manifests: DomainManifestLookup | undefined, + group?: string + ): ListItem[] => { + if (!triggers) { + return []; + } + const result: ListItem[] = []; + + for (const trigger of Object.keys(triggers)) { + const domain = getTriggerDomain(trigger); + const triggerName = getTriggerObjectId(trigger); + + if (group && group !== `${DYNAMIC_PREFIX}${domain}`) { + continue; + } + + result.push({ + icon: html` + + `, + key: `${DYNAMIC_PREFIX}${trigger}`, + name: + localize(`component.${domain}.triggers.${triggerName}.name`) || + trigger, + description: + localize( + `component.${domain}.triggers.${triggerName}.description` + ) || trigger, + }); + } + return result; + } + ); + private _services = memoizeOne( ( localize: LocalizeFunc, @@ -539,8 +702,8 @@ class DialogAddAutomationElement let domain: string | undefined; - if (isService(group)) { - domain = getService(group!); + if (isDynamic(group)) { + domain = getValueFromDynamic(group!); } const addDomain = (dmn: string) => { @@ -554,7 +717,7 @@ class DialogAddAutomationElement .service=${`${dmn}.${service}`} > `, - key: `${SERVICE_PREFIX}${dmn}.${service}`, + key: `${DYNAMIC_PREFIX}${dmn}.${service}`, name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${ this.hass.localize(`component.${dmn}.services.${service}.name`) || services[dmn][service]?.name || @@ -668,14 +831,15 @@ class DialogAddAutomationElement this._domains, this.hass.localize, this.hass.services, + this._triggerDescriptions, this._manifests ); - const groupName = isService(this._selectedGroup) + const groupName = isDynamic(this._selectedGroup) ? domainToName( this.hass.localize, - getService(this._selectedGroup!), - this._manifests?.[getService(this._selectedGroup!)] + 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 diff --git a/src/panels/config/automation/sidebar/ha-automation-sidebar-action.ts b/src/panels/config/automation/sidebar/ha-automation-sidebar-action.ts index 00a706db13..b2470e9c3f 100644 --- a/src/panels/config/automation/sidebar/ha-automation-sidebar-action.ts +++ b/src/panels/config/automation/sidebar/ha-automation-sidebar-action.ts @@ -28,7 +28,6 @@ import type HaAutomationConditionEditor from "../action/ha-automation-action-edi import { getAutomationActionType } from "../action/ha-automation-action-row"; import { getRepeatType } from "../action/types/ha-automation-action-repeat"; import { overflowStyles, sidebarEditorStyles } from "../styles"; -import "../trigger/ha-automation-trigger-editor"; import "./ha-automation-sidebar-card"; @customElement("ha-automation-sidebar-action") diff --git a/src/panels/config/automation/sidebar/ha-automation-sidebar-card.ts b/src/panels/config/automation/sidebar/ha-automation-sidebar-card.ts index f94407d70d..1d8c9223c8 100644 --- a/src/panels/config/automation/sidebar/ha-automation-sidebar-card.ts +++ b/src/panels/config/automation/sidebar/ha-automation-sidebar-card.ts @@ -17,7 +17,6 @@ import "../../../../components/ha-dialog-header"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-md-button-menu"; import "../../../../components/ha-md-divider"; -import "../../../../components/ha-md-menu-item"; import type { HomeAssistant } from "../../../../types"; import "../ha-automation-editor-warning"; diff --git a/src/panels/config/automation/sidebar/ha-automation-sidebar-option.ts b/src/panels/config/automation/sidebar/ha-automation-sidebar-option.ts index 2d8b9e25d5..9f37a2fe39 100644 --- a/src/panels/config/automation/sidebar/ha-automation-sidebar-option.ts +++ b/src/panels/config/automation/sidebar/ha-automation-sidebar-option.ts @@ -6,6 +6,9 @@ import { } from "@mdi/js"; import { html, LitElement, nothing } from "lit"; import { customElement, property, query } from "lit/decorators"; +import "../../../../components/ha-md-divider"; +import "../../../../components/ha-md-menu-item"; +import "../../../../components/ha-svg-icon"; import type { OptionSidebarConfig } from "../../../../data/automation"; import type { HomeAssistant } from "../../../../types"; import { isMac } from "../../../../util/is_mac"; diff --git a/src/panels/config/automation/sidebar/ha-automation-sidebar-trigger.ts b/src/panels/config/automation/sidebar/ha-automation-sidebar-trigger.ts index 70f7bbfd6e..9e8684b7d4 100644 --- a/src/panels/config/automation/sidebar/ha-automation-sidebar-trigger.ts +++ b/src/panels/config/automation/sidebar/ha-automation-sidebar-trigger.ts @@ -15,8 +15,15 @@ import { customElement, property, query, state } from "lit/decorators"; import { keyed } from "lit/directives/keyed"; import { fireEvent } from "../../../../common/dom/fire_event"; import { handleStructError } from "../../../../common/structs/handle-errors"; -import type { TriggerSidebarConfig } from "../../../../data/automation"; -import { isTriggerList } from "../../../../data/trigger"; +import type { + LegacyTrigger, + TriggerSidebarConfig, +} from "../../../../data/automation"; +import { + getTriggerDomain, + getTriggerObjectId, + isTriggerList, +} from "../../../../data/trigger"; import type { HomeAssistant } from "../../../../types"; import { isMac } from "../../../../util/is_mac"; import { overflowStyles, sidebarEditorStyles } from "../styles"; @@ -72,9 +79,18 @@ export default class HaAutomationSidebarTrigger extends LitElement { "ui.panel.config.automation.editor.triggers.trigger" ); - const title = this.hass.localize( - `ui.panel.config.automation.editor.triggers.type.${type}.label` - ); + const domain = + "trigger" in this.config.config && + getTriggerDomain(this.config.config.trigger); + const triggerName = + "trigger" in this.config.config && + getTriggerObjectId(this.config.config.trigger); + + const title = + this.hass.localize( + `ui.panel.config.automation.editor.triggers.type.${type as LegacyTrigger["trigger"]}.label` + ) || + this.hass.localize(`component.${domain}.triggers.${triggerName}.name`); return html` - ${dynamicElement(`ha-automation-trigger-${type}`, { - hass: this.hass, - trigger: this.trigger, - disabled: this.disabled, - })} + ${this.description + ? html`` + : dynamicElement(`ha-automation-trigger-${type}`, { + hass: this.hass, + trigger: this.trigger, + disabled: this.disabled, + })} `} diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index 4429e04840..40297a0314 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -40,9 +40,11 @@ import "../../../../components/ha-md-button-menu"; import "../../../../components/ha-md-divider"; import "../../../../components/ha-md-menu-item"; import "../../../../components/ha-svg-icon"; +import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon"; import type { AutomationClipboard, Trigger, + TriggerList, TriggerSidebarConfig, } from "../../../../data/automation"; import { isTrigger, subscribeTrigger } from "../../../../data/automation"; @@ -50,7 +52,8 @@ import { describeTrigger } from "../../../../data/automation_i18n"; import { validateConfig } from "../../../../data/config"; import { fullEntitiesContext } from "../../../../data/context"; import type { EntityRegistryEntry } from "../../../../data/entity_registry"; -import { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger"; +import type { TriggerDescriptions } from "../../../../data/trigger"; +import { isTriggerList } from "../../../../data/trigger"; import { showAlertDialog, showPromptDialog, @@ -72,6 +75,7 @@ import "./types/ha-automation-trigger-list"; import "./types/ha-automation-trigger-mqtt"; import "./types/ha-automation-trigger-numeric_state"; import "./types/ha-automation-trigger-persistent_notification"; +import "./types/ha-automation-trigger-platform"; import "./types/ha-automation-trigger-state"; import "./types/ha-automation-trigger-sun"; import "./types/ha-automation-trigger-tag"; @@ -137,6 +141,9 @@ export default class HaAutomationTriggerRow extends LitElement { @state() private _warnings?: string[]; + @property({ attribute: false }) + public triggerDescriptions: TriggerDescriptions = {}; + @property({ type: Boolean }) public narrow = false; @query("ha-automation-trigger-editor") @@ -178,18 +185,24 @@ export default class HaAutomationTriggerRow extends LitElement { } private _renderRow() { - const type = this._getType(this.trigger); + const type = this._getType(this.trigger, this.triggerDescriptions); const supported = this._uiSupported(type); const yamlMode = this._yamlMode || !supported; return html` - + ${type === "list" + ? html`` + : html`).trigger} + >`}

${describeTrigger(this.trigger, this.hass, this._entityReg)}

@@ -393,6 +406,9 @@ export default class HaAutomationTriggerRow extends LitElement { { fireEvent(this, "value-changed", { value }); @@ -576,8 +593,14 @@ export default class HaAutomationTriggerRow extends LitElement { duplicate: this._duplicateTrigger, cut: this._cutTrigger, insertAfter: this._insertAfter, - config: trigger || this.trigger, - uiSupported: this._uiSupported(this._getType(trigger || this.trigger)), + config: trigger, + uiSupported: this._uiSupported( + this._getType(trigger, this.triggerDescriptions) + ), + description: + "trigger" in trigger + ? this.triggerDescriptions[trigger.trigger] + : undefined, yamlMode: this._yamlMode, } satisfies TriggerSidebarConfig); this._selected = true; @@ -759,8 +782,18 @@ export default class HaAutomationTriggerRow extends LitElement { }); } - private _getType = memoizeOne((trigger: Trigger) => - isTriggerList(trigger) ? "list" : trigger.trigger + private _getType = memoizeOne( + (trigger: Trigger, triggerDescriptions: TriggerDescriptions) => { + if (isTriggerList(trigger)) { + return "list"; + } + + if (trigger.trigger in triggerDescriptions) { + return "platform"; + } + + return trigger.trigger; + } ); private _uiSupported = memoizeOne( diff --git a/src/panels/config/automation/trigger/ha-automation-trigger.ts b/src/panels/config/automation/trigger/ha-automation-trigger.ts index 4395a69788..8e1c78d1d1 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger.ts @@ -4,6 +4,7 @@ import type { PropertyValues } from "lit"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { repeat } from "lit/directives/repeat"; +import { ensureArray } from "../../../../common/array/ensure-array"; import { storage } from "../../../../common/decorators/storage"; import { fireEvent } from "../../../../common/dom/fire_event"; import { stopPropagation } from "../../../../common/dom/stop_propagation"; @@ -12,12 +13,16 @@ import "../../../../components/ha-button"; import "../../../../components/ha-button-menu"; import "../../../../components/ha-sortable"; import "../../../../components/ha-svg-icon"; -import type { - AutomationClipboard, - Trigger, - TriggerList, +import { + getValueFromDynamic, + isDynamic, + type AutomationClipboard, + type Trigger, + type TriggerList, } from "../../../../data/automation"; -import { isTriggerList } from "../../../../data/trigger"; +import type { TriggerDescriptions } from "../../../../data/trigger"; +import { isTriggerList, subscribeTriggers } from "../../../../data/trigger"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import { PASTE_VALUE, @@ -26,10 +31,9 @@ import { import { automationRowsStyles } from "../styles"; import "./ha-automation-trigger-row"; import type HaAutomationTriggerRow from "./ha-automation-trigger-row"; -import { ensureArray } from "../../../../common/array/ensure-array"; @customElement("ha-automation-trigger") -export default class HaAutomationTrigger extends LitElement { +export default class HaAutomationTrigger extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public triggers!: Trigger[]; @@ -62,6 +66,23 @@ export default class HaAutomationTrigger extends LitElement { private _triggerKeys = new WeakMap(); + @state() private _triggerDescriptions: TriggerDescriptions = {}; + + protected hassSubscribe() { + return [ + subscribeTriggers(this.hass, (triggers) => this._addTriggers(triggers)), + ]; + } + + private _addTriggers(triggers: TriggerDescriptions) { + this._triggerDescriptions = { ...this._triggerDescriptions, ...triggers }; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this.hass.loadBackendTranslation("triggers"); + } + protected render() { return html` ["trigger"]; const elClass = customElements.get( diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-platform.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-platform.ts new file mode 100644 index 0000000000..9a7d19a4ce --- /dev/null +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-platform.ts @@ -0,0 +1,416 @@ +import { mdiHelpCircle } from "@mdi/js"; +import type { PropertyValues } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { computeDomain } from "../../../../../common/entity/compute_domain"; +import "../../../../../components/ha-checkbox"; +import "../../../../../components/ha-selector/ha-selector"; +import "../../../../../components/ha-settings-row"; +import type { PlatformTrigger } from "../../../../../data/automation"; +import type { IntegrationManifest } from "../../../../../data/integration"; +import { fetchIntegrationManifest } from "../../../../../data/integration"; +import type { TargetSelector } from "../../../../../data/selector"; +import { + getTriggerDomain, + getTriggerObjectId, + type TriggerDescription, +} from "../../../../../data/trigger"; +import type { HomeAssistant } from "../../../../../types"; +import { documentationUrl } from "../../../../../util/documentation-url"; + +const showOptionalToggle = (field: TriggerDescription["fields"][string]) => + field.selector && + !field.required && + !("boolean" in field.selector && field.default); + +@customElement("ha-automation-trigger-platform") +export class HaPlatformTrigger extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public trigger!: PlatformTrigger; + + @property({ attribute: false }) public description?: TriggerDescription; + + @property({ type: Boolean }) public disabled = false; + + @state() private _checkedKeys = new Set(); + + @state() private _manifest?: IntegrationManifest; + + public static get defaultConfig(): PlatformTrigger { + return { trigger: "" }; + } + + protected willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + if (!this.hasUpdated) { + this.hass.loadBackendTranslation("triggers"); + this.hass.loadBackendTranslation("selector"); + } + if (!changedProperties.has("trigger")) { + return; + } + const oldValue = changedProperties.get("trigger") as + | undefined + | this["trigger"]; + + // Fetch the manifest if we have a trigger selected and the trigger domain changed. + // If no trigger is selected, clear the manifest. + if (this.trigger?.trigger) { + const domain = getTriggerDomain(this.trigger.trigger); + + const oldDomain = getTriggerDomain(oldValue?.trigger || ""); + + if (domain !== oldDomain) { + this._fetchManifest(domain); + } + } else { + this._manifest = undefined; + } + } + + protected render() { + const domain = getTriggerDomain(this.trigger.trigger); + const triggerName = getTriggerObjectId(this.trigger.trigger); + + const description = this.hass.localize( + `component.${domain}.triggers.${triggerName}.description` + ); + + const triggerDesc = this.description; + + const shouldRenderDataYaml = !triggerDesc?.fields; + + const hasOptional = Boolean( + triggerDesc?.fields && + Object.values(triggerDesc.fields).some((field) => + showOptionalToggle(field) + ) + ); + + return html` +
+ ${description ? html`

${description}

` : nothing} + ${this._manifest + ? html` + + ` + : nothing} +
+ ${triggerDesc && "target" in triggerDesc + ? html` + ${hasOptional + ? html`
` + : nothing} + ${this.hass.localize( + "ui.components.service-control.target" + )} + ${this.hass.localize( + "ui.components.service-control.target_secondary" + )}
` + : nothing} + ${shouldRenderDataYaml + ? html`` + : Object.entries(triggerDesc.fields).map(([fieldName, dataField]) => + this._renderField( + fieldName, + dataField, + hasOptional, + domain, + triggerName + ) + )} + `; + } + + private _targetSelector = memoizeOne( + (targetSelector: TargetSelector["target"] | null | undefined) => + targetSelector ? { target: { ...targetSelector } } : { target: {} } + ); + + private _renderField = ( + fieldName: string, + dataField: TriggerDescription["fields"][string], + hasOptional: boolean, + domain: string | undefined, + triggerName: string | undefined + ) => { + const selector = dataField?.selector ?? { text: null }; + + const showOptional = showOptionalToggle(dataField); + + return dataField.selector + ? html` + ${!showOptional + ? hasOptional + ? html`
` + : nothing + : html``} + ${this.hass.localize( + `component.${domain}.triggers.${triggerName}.fields.${fieldName}.name` + ) || triggerName} + ${this.hass.localize( + `component.${domain}.triggers.${triggerName}.fields.${fieldName}.description` + )} + +
` + : nothing; + }; + + private _generateContext( + field: TriggerDescription["fields"][string] + ): Record | undefined { + if (!field.context) { + return undefined; + } + + const context = {}; + for (const [context_key, data_key] of Object.entries(field.context)) { + context[context_key] = + data_key === "target" + ? this.trigger.target + : this.trigger.options?.[data_key]; + } + return context; + } + + private _dataChanged(ev: CustomEvent) { + ev.stopPropagation(); + if (ev.detail.isValid === false) { + // Don't clear an object selector that returns invalid YAML + return; + } + const key = (ev.currentTarget as any).key; + const value = ev.detail.value; + if ( + this.trigger?.options?.[key] === value || + ((!this.trigger?.options || !(key in this.trigger.options)) && + (value === "" || value === undefined)) + ) { + return; + } + + const options = { ...this.trigger?.options, [key]: value }; + + if ( + value === "" || + value === undefined || + (typeof value === "object" && !Object.keys(value).length) + ) { + delete options[key]; + } + + fireEvent(this, "value-changed", { + value: { + ...this.trigger, + options, + }, + }); + } + + private _targetChanged(ev: CustomEvent): void { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { + ...this.trigger, + target: ev.detail.value, + }, + }); + } + + private _checkboxChanged(ev) { + const checked = ev.currentTarget.checked; + const key = ev.currentTarget.key; + let options; + + if (checked) { + this._checkedKeys.add(key); + const field = + this.description && + Object.entries(this.description).find(([k, _value]) => k === key)?.[1]; + let defaultValue = field?.default; + + if ( + defaultValue == null && + field?.selector && + "constant" in field.selector + ) { + defaultValue = field.selector.constant?.value; + } + + if ( + defaultValue == null && + field?.selector && + "boolean" in field.selector + ) { + defaultValue = false; + } + + if (defaultValue != null) { + options = { + ...this.trigger?.options, + [key]: defaultValue, + }; + } + } else { + this._checkedKeys.delete(key); + options = { ...this.trigger?.options }; + delete options[key]; + } + if (options) { + fireEvent(this, "value-changed", { + value: { + ...this.trigger, + options, + }, + }); + } + this.requestUpdate("_checkedKeys"); + } + + private _localizeValueCallback = (key: string) => { + if (!this.trigger?.trigger) { + return ""; + } + return this.hass.localize( + `component.${computeDomain(this.trigger.trigger)}.selector.${key}` + ); + }; + + private async _fetchManifest(integration: string) { + this._manifest = undefined; + try { + this._manifest = await fetchIntegrationManifest(this.hass, integration); + } catch (_err: any) { + // eslint-disable-next-line no-console + console.log(`Unable to fetch integration manifest for ${integration}`); + // Ignore if loading manifest fails. Probably bad JSON in manifest + } + } + + static styles = css` + ha-settings-row { + padding: 0 var(--ha-space-4); + } + ha-settings-row[narrow] { + padding-bottom: var(--ha-space-2); + } + ha-settings-row { + --settings-row-content-width: 100%; + --settings-row-prefix-display: contents; + border-top: var( + --service-control-items-border-top, + 1px solid var(--divider-color) + ); + } + ha-service-picker, + ha-entity-picker, + ha-yaml-editor { + display: block; + margin: 0 var(--ha-space-4); + } + ha-yaml-editor { + padding: var(--ha-space-4) 0; + } + p { + margin: 0 var(--ha-space-4); + padding: var(--ha-space-4) 0; + } + :host([hide-picker]) p { + padding-top: 0; + } + .checkbox-spacer { + width: 32px; + } + ha-checkbox { + margin-left: calc(var(--ha-space-4) * -1); + margin-inline-start: calc(var(--ha-space-4) * -1); + margin-inline-end: initial; + } + .help-icon { + color: var(--secondary-text-color); + } + .description { + justify-content: space-between; + display: flex; + align-items: center; + padding-right: 2px; + padding-inline-end: 2px; + padding-inline-start: initial; + } + .description p { + direction: ltr; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-trigger-platform": HaPlatformTrigger; + } +}