import "@home-assistant/webawesome/dist/components/divider/divider"; import { consume } from "@lit/context"; import { mdiAppleKeyboardCommand, mdiArrowDown, mdiArrowUp, mdiContentCopy, mdiContentCut, mdiDelete, mdiDotsVertical, mdiInformation, mdiPlayCircleOutline, mdiPlaylistEdit, mdiPlusCircleMultipleOutline, mdiRenameBox, mdiStopCircleOutline, } from "@mdi/js"; import type { HassServiceTarget, UnsubscribeFunc, } from "home-assistant-js-websocket"; import { dump } from "js-yaml"; 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 memoizeOne from "memoize-one"; import { ensureArray } from "../../../../common/array/ensure-array"; import { storage } from "../../../../common/decorators/storage"; import { fireEvent } from "../../../../common/dom/fire_event"; import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation"; import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; import { handleStructError } from "../../../../common/structs/handle-errors"; import { copyToClipboard } from "../../../../common/util/copy-clipboard"; import { debounce } from "../../../../common/util/debounce"; import "../../../../components/ha-alert"; import "../../../../components/ha-automation-row"; import type { HaAutomationRow } from "../../../../components/ha-automation-row"; import "../../../../components/ha-card"; import "../../../../components/ha-dropdown"; import "../../../../components/ha-dropdown-item"; import type { HaDropdownItem } from "../../../../components/ha-dropdown-item"; import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-svg-icon"; import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon"; import type { AutomationClipboard, PlatformTrigger, Trigger, TriggerList, TriggerSidebarConfig, } from "../../../../data/automation"; import { isTrigger, subscribeTrigger } from "../../../../data/automation"; import { describeTrigger } from "../../../../data/automation_i18n"; import { validateConfig } from "../../../../data/config"; import { fullEntitiesContext } from "../../../../data/context"; import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry"; import type { TriggerDescriptions } from "../../../../data/trigger"; import { isTriggerList } from "../../../../data/trigger"; import { showAlertDialog, showPromptDialog, } from "../../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../../types"; import { isMac } from "../../../../util/is_mac"; import { showToast } from "../../../../util/toast"; import "../ha-automation-editor-warning"; import { overflowStyles, rowStyles } from "../styles"; import "../target/ha-automation-row-targets"; import "./ha-automation-trigger-editor"; import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor"; import "./types/ha-automation-trigger-calendar"; import "./types/ha-automation-trigger-conversation"; import "./types/ha-automation-trigger-device"; import "./types/ha-automation-trigger-event"; import "./types/ha-automation-trigger-geo_location"; import "./types/ha-automation-trigger-homeassistant"; import "./types/ha-automation-trigger-list"; 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"; import "./types/ha-automation-trigger-template"; import "./types/ha-automation-trigger-time"; import "./types/ha-automation-trigger-time_pattern"; import "./types/ha-automation-trigger-webhook"; import "./types/ha-automation-trigger-zone"; export interface TriggerElement extends LitElement { trigger: Trigger; } export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => { ev.stopPropagation(); const name = (ev.currentTarget as any)?.name; if (!name) { return; } const newVal = ev.detail?.value || (ev.currentTarget as any)?.value; if ((element.trigger[name] || "") === newVal) { return; } let newTrigger: Trigger; if (newVal === undefined || newVal === "") { newTrigger = { ...element.trigger }; delete newTrigger[name]; } else { newTrigger = { ...element.trigger, [name]: newVal }; } fireEvent(element, "value-changed", { value: newTrigger }); }; @customElement("ha-automation-trigger-row") export default class HaAutomationTriggerRow extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public trigger!: Trigger; @property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public first?: boolean; @property({ type: Boolean }) public last?: boolean; @property({ type: Boolean }) public highlight?: boolean; @property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar = false; @property({ type: Boolean, attribute: "sort-selected" }) public sortSelected = false; @state() private _yamlMode = false; @state() private _triggered?: Record; @state() private _triggerColor = false; @state() private _selected = false; @state() private _warnings?: string[]; @property({ attribute: false }) public triggerDescriptions: TriggerDescriptions = {}; @property({ type: Boolean }) public narrow = false; @query("ha-automation-trigger-editor") public triggerEditor?: HaAutomationTriggerEditor; @query("ha-automation-row") private _automationRowElement?: HaAutomationRow; @storage({ key: "automationClipboard", state: false, subscribe: true, storage: "sessionStorage", }) public _clipboard?: AutomationClipboard; @state() @consume({ context: fullEntitiesContext, subscribe: true }) _entityReg!: EntityRegistryEntry[]; get selected() { return this._selected; } private _triggerUnsub?: Promise; private _renderOverflowLabel(label: string, shortcut?: TemplateResult) { return html`
${label} ${this.optionsInSidebar && !this.narrow ? shortcut || html`` : nothing}
`; } private _renderRow() { 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)} ${type === "platform" && "target" in this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger] ? this._renderTargets((this.trigger as PlatformTrigger).target) : nothing}

${this._renderOverflowLabel( this.hass.localize( "ui.panel.config.automation.editor.triggers.rename" ) )} ${this._renderOverflowLabel( this.hass.localize( "ui.panel.config.automation.editor.actions.duplicate" ) )} ${this._renderOverflowLabel( this.hass.localize( "ui.panel.config.automation.editor.triggers.copy" ), html` ${isMac ? html`` : this.hass.localize( "ui.panel.config.automation.editor.ctrl" )} + C ` )} ${this._renderOverflowLabel( this.hass.localize( "ui.panel.config.automation.editor.triggers.cut" ), html` ${isMac ? html`` : this.hass.localize( "ui.panel.config.automation.editor.ctrl" )} + X ` )} ${!this.optionsInSidebar ? html` ${this.hass.localize( "ui.panel.config.automation.editor.move_up" )} ${this.hass.localize( "ui.panel.config.automation.editor.move_down" )} ` : nothing} ${this._renderOverflowLabel( this.hass.localize( `ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}` ) )} ${this._renderOverflowLabel( this.hass.localize( `ui.panel.config.automation.editor.actions.${"enabled" in this.trigger && this.trigger.enabled === false ? "enable" : "disable"}` ) )} ${this._renderOverflowLabel( this.hass.localize( "ui.panel.config.automation.editor.actions.delete" ), html` ${isMac ? html`` : this.hass.localize( "ui.panel.config.automation.editor.ctrl" )} + ${this.hass.localize( "ui.panel.config.automation.editor.del" )} ` )} ${!this.optionsInSidebar ? html`${this._warnings ? html` ` : nothing} ` : nothing} `; } protected render() { if (!this.trigger) return nothing; return html` ${"enabled" in this.trigger && this.trigger.enabled === false ? html`
${this.hass.localize( "ui.panel.config.automation.editor.actions.disabled" )}
` : nothing} ${this.optionsInSidebar ? html`${this._selected ? "selected" : nothing}${this._renderRow()}` : html` ${this._renderRow()} `}
${this.hass.localize( "ui.panel.config.automation.editor.triggers.triggered" )}
`; } private _renderTargets = memoizeOne( (target?: HassServiceTarget) => html`` ); protected willUpdate(changedProperties) { // on yaml toggle --> clear warnings if (changedProperties.has("yamlMode")) { this._warnings = undefined; } } protected override updated(changedProps: PropertyValues): void { super.updated(changedProps); if (changedProps.has("trigger")) { this._subscribeTrigger(); } } public connectedCallback(): void { super.connectedCallback(); if (this.hasUpdated && this.trigger) { this._subscribeTrigger(); } } public disconnectedCallback(): void { super.disconnectedCallback(); if (this._triggerUnsub) { this._triggerUnsub.then((unsub) => unsub()); this._triggerUnsub = undefined; } this._doSubscribeTrigger.cancel(); } private _subscribeTrigger() { // Clean up old trigger subscription. if (this._triggerUnsub) { this._triggerUnsub.then((unsub) => unsub()); this._triggerUnsub = undefined; } this._doSubscribeTrigger(); } private _doSubscribeTrigger = debounce(async () => { let untriggerTimeout: number | undefined; const showTriggeredTime = 5000; const trigger = this.trigger; // Clean up old trigger subscription. if (this._triggerUnsub) { this._triggerUnsub.then((unsub) => unsub()); this._triggerUnsub = undefined; } const validateResult = await validateConfig(this.hass, { triggers: trigger, }); // Don't do anything if trigger not valid or if trigger changed. if (!validateResult.triggers.valid || this.trigger !== trigger) { return; } const triggerUnsub = subscribeTrigger( this.hass, (result) => { if (untriggerTimeout !== undefined) { clearTimeout(untriggerTimeout); this._triggerColor = !this._triggerColor; } else { this._triggerColor = false; } this._triggered = result; untriggerTimeout = window.setTimeout(() => { this._triggered = undefined; untriggerTimeout = undefined; }, showTriggeredTime); }, trigger ); triggerUnsub.catch(() => { if (this._triggerUnsub === triggerUnsub) { this._triggerUnsub = undefined; } }); this._triggerUnsub = triggerUnsub; }, 5000); private _handleUiModeNotAvailable(ev: CustomEvent) { this._warnings = handleStructError(this.hass, ev.detail).warnings; if (!this._yamlMode) { this._yamlMode = true; } } private _toggleSidebar(ev: Event) { ev?.stopPropagation(); if (this._selected) { fireEvent(this, "request-close-sidebar"); return; } this.openSidebar(); } public openSidebar(trigger?: Trigger): void { trigger = trigger || this.trigger; fireEvent(this, "open-sidebar", { save: (value) => { fireEvent(this, "value-changed", { value }); }, close: (focus?: boolean) => { this._selected = false; fireEvent(this, "close-sidebar"); if (focus) { this.focus(); } }, rename: () => { this._renameTrigger(); }, toggleYamlMode: () => { this._toggleYamlMode(); this.openSidebar(); }, disable: this._onDisable, delete: this._onDelete, copy: this._copyTrigger, duplicate: this._duplicateTrigger, cut: this._cutTrigger, insertAfter: this._insertAfter, 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; if (this.narrow) { window.setTimeout(() => { this.scrollIntoView({ block: "start", behavior: "smooth", }); }, 180); } } private _setClipboard() { this._clipboard = { ...this._clipboard, trigger: this.trigger, }; copyToClipboard(dump(this.trigger)); } private _onDelete = () => { fireEvent(this, "value-changed", { value: null }); if (this._selected) { fireEvent(this, "close-sidebar"); } showToast(this, { message: this.hass.localize("ui.common.successfully_deleted"), duration: 4000, action: { text: this.hass.localize("ui.common.undo"), action: () => { fireEvent(window, "undo-change"); }, }, }); }; private _onDisable = () => { if (isTriggerList(this.trigger)) return; const enabled = !(this.trigger.enabled ?? true); const value = { ...this.trigger, enabled }; fireEvent(this, "value-changed", { value }); if (this._selected && this.optionsInSidebar) { this.openSidebar(value); // refresh sidebar } if (this._yamlMode && !this.optionsInSidebar) { this.triggerEditor?.yamlEditor?.setValue(value); } }; private _switchUiMode() { this._yamlMode = false; } private _switchYamlMode() { this._yamlMode = true; } private _showTriggeredInfo() { showAlertDialog(this, { title: this.hass.localize( "ui.panel.config.automation.editor.triggers.triggering_event_detail" ), text: html` `, }); } private _renameTrigger = async (): Promise => { if (isTriggerList(this.trigger)) return; const alias = await showPromptDialog(this, { title: this.hass.localize( "ui.panel.config.automation.editor.triggers.change_alias" ), inputLabel: this.hass.localize( "ui.panel.config.automation.editor.triggers.alias" ), inputType: "string", placeholder: capitalizeFirstLetter( describeTrigger(this.trigger, this.hass, this._entityReg, true) ), defaultValue: this.trigger.alias, confirmText: this.hass.localize("ui.common.submit"), }); if (alias !== null) { const value = { ...this.trigger }; if (alias === "") { delete value.alias; } else { value.alias = alias; } fireEvent(this, "value-changed", { value, }); if (this._selected && this.optionsInSidebar) { this.openSidebar(value); // refresh sidebar } else if (this._yamlMode) { this.triggerEditor?.yamlEditor?.setValue(value); } } }; private _duplicateTrigger = () => { fireEvent(this, "duplicate"); }; private _insertAfter = (value: Trigger | Trigger[]) => { if (ensureArray(value).some((val) => !isTrigger(val))) { return false; } fireEvent(this, "insert-after", { value }); return true; }; private _copyTrigger = () => { this._setClipboard(); showToast(this, { message: this.hass.localize( "ui.panel.config.automation.editor.triggers.copied_to_clipboard" ), duration: 2000, }); }; private _cutTrigger = () => { this._setClipboard(); fireEvent(this, "value-changed", { value: null }); if (this._selected) { fireEvent(this, "close-sidebar"); } showToast(this, { message: this.hass.localize( "ui.panel.config.automation.editor.triggers.cut_to_clipboard" ), duration: 2000, }); }; private _moveUp = () => { fireEvent(this, "move-up"); }; private _moveDown = () => { fireEvent(this, "move-down"); }; private _toggleYamlMode = (item?: HTMLElement) => { if (this._yamlMode) { this._switchUiMode(); } else { this._switchYamlMode(); } if (!this.optionsInSidebar) { this.expand(); } else if (item) { this.openSidebar(); } }; public expand() { this.updateComplete.then(() => { this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true; }); } 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( (type: string) => customElements.get(`ha-automation-trigger-${type}`) !== undefined ); public focus() { this._automationRowElement?.focus(); } private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { const action = ev.detail?.item?.value; if (!action) { return; } switch (action) { case "rename": this._renameTrigger(); break; case "duplicate": this._duplicateTrigger(); break; case "copy": this._copyTrigger(); break; case "cut": this._cutTrigger(); break; case "move_up": this._moveUp(); break; case "move_down": this._moveDown(); break; case "toggle_yaml_mode": this._toggleYamlMode(ev.target as HTMLElement); break; case "disable": this._onDisable(); break; case "delete": this._onDelete(); break; } } static get styles(): CSSResultGroup { return [ rowStyles, overflowStyles, css` .triggered { cursor: pointer; position: absolute; top: 0px; right: 0px; left: 0px; text-transform: uppercase; font-size: var(--ha-font-size-m); font-weight: var(--ha-font-weight-bold); background-color: var(--primary-color); color: var(--text-primary-color); max-height: 0px; overflow: hidden; transition: max-height 0.3s; text-align: center; border-top-right-radius: var( --ha-card-border-radius, var(--ha-border-radius-lg) ); border-top-left-radius: var( --ha-card-border-radius, var(--ha-border-radius-lg) ); display: flex; justify-content: center; align-items: center; gap: var(--ha-space-1); line-height: 1; padding: 0; } .triggered ha-svg-icon { --mdc-icon-size: 16px; } .triggered.active { max-height: 100px; padding: 4px; } .triggered:hover { opacity: 0.8; } .triggered.accent { background-color: var(--accent-color); color: var(--text-accent-color, var(--text-primary-color)); } `, ]; } } declare global { interface HTMLElementTagNameMap { "ha-automation-trigger-row": HaAutomationTriggerRow; } }