diff --git a/demo/src/stubs/config.ts b/demo/src/stubs/config.ts new file mode 100644 index 0000000000..73beb19e1e --- /dev/null +++ b/demo/src/stubs/config.ts @@ -0,0 +1,9 @@ +import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +export const mockConfig = (hass: MockHomeAssistant) => { + hass.mockWS("validate_config", () => ({ + actions: { valid: true }, + conditions: { valid: true }, + triggers: { valid: true }, + })); +}; diff --git a/demo/src/stubs/tags.ts b/demo/src/stubs/tags.ts new file mode 100644 index 0000000000..0634d40093 --- /dev/null +++ b/demo/src/stubs/tags.ts @@ -0,0 +1,6 @@ +import { Tag } from "../../../src/data/tag"; +import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +export const mockTags = (hass: MockHomeAssistant) => { + hass.mockWS("tag/list", () => [{ id: "my-tag", name: "My Tag" }] as Tag[]); +}; diff --git a/gallery/src/pages/automation/describe-trigger.ts b/gallery/src/pages/automation/describe-trigger.ts index f8b8117a83..8c462060b0 100644 --- a/gallery/src/pages/automation/describe-trigger.ts +++ b/gallery/src/pages/automation/describe-trigger.ts @@ -58,6 +58,12 @@ const triggers = [ command: ["Turn on the lights", "Turn the lights on"], }, { trigger: "event", event_type: "homeassistant_started" }, + { + triggers: [ + { trigger: "state", entity_id: "light.kitchen", to: "on" }, + { trigger: "state", entity_id: "light.kitchen", to: "off" }, + ], + }, ]; const initialTrigger: Trigger = { diff --git a/gallery/src/pages/automation/editor-trigger.ts b/gallery/src/pages/automation/editor-trigger.ts index a138a46e9e..1d94c5676c 100644 --- a/gallery/src/pages/automation/editor-trigger.ts +++ b/gallery/src/pages/automation/editor-trigger.ts @@ -8,6 +8,9 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; +import { mockConfig } from "../../../../demo/src/stubs/config"; +import { mockTags } from "../../../../demo/src/stubs/tags"; +import { mockAuth } from "../../../../demo/src/stubs/auth"; import type { Trigger } from "../../../../src/data/automation"; import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location"; import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event"; @@ -26,6 +29,7 @@ import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt"; import "../../../../src/panels/config/automation/trigger/ha-automation-trigger"; import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation"; +import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list"; const SCHEMAS: { name: string; triggers: Trigger[] }[] = [ { @@ -116,6 +120,10 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [ }, ], }, + { + name: "Trigger list", + triggers: [{ ...HaTriggerList.defaultConfig }], + }, ]; @customElement("demo-automation-editor-trigger") @@ -135,6 +143,9 @@ export class DemoAutomationEditorTrigger extends LitElement { mockDeviceRegistry(hass); mockAreaRegistry(hass); mockHassioSupervisor(hass); + mockConfig(hass); + mockTags(hass); + mockAuth(hass); } protected render(): TemplateResult { diff --git a/src/components/trace/hat-script-graph.ts b/src/components/trace/hat-script-graph.ts index 80ad22cddb..267e046168 100644 --- a/src/components/trace/hat-script-graph.ts +++ b/src/components/trace/hat-script-graph.ts @@ -94,7 +94,7 @@ export class HatScriptGraph extends LitElement { @focus=${this.selectNode(config, path)} ?active=${this.selected === path} .iconPath=${mdiAsterisk} - .notEnabled=${config.enabled === false} + .notEnabled=${"enabled" in config && config.enabled === false} .error=${this.trace.trace[path]?.some((tr) => tr.error)} tabindex=${track ? "0" : "-1"} > diff --git a/src/data/automation.ts b/src/data/automation.ts index ffd63f67e9..f08170c2cc 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -206,7 +206,8 @@ export type Trigger = | TemplateTrigger | EventTrigger | DeviceTrigger - | CalendarTrigger; + | CalendarTrigger + | TriggerList; interface BaseCondition { condition: string; diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index 474db8345c..c6bbbbeb77 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -22,6 +22,7 @@ import { formatListWithAnds, formatListWithOrs, } from "../common/string/format-list"; +import { isTriggerList } from "./trigger"; const triggerTranslationBaseKey = "ui.panel.config.automation.editor.triggers.type"; @@ -98,6 +99,20 @@ const tryDescribeTrigger = ( entityRegistry: EntityRegistryEntry[], ignoreAlias = false ) => { + if (isTriggerList(trigger)) { + const triggers = ensureArray(trigger.triggers); + + if (!triggers || triggers.length === 0) { + return hass.localize( + `${triggerTranslationBaseKey}.list.description.no_trigger` + ); + } + const count = triggers.length; + return hass.localize(`${triggerTranslationBaseKey}.list.description.full`, { + count: count, + }); + } + if (trigger.alias && !ignoreAlias) { return trigger.alias; } diff --git a/src/data/trigger.ts b/src/data/trigger.ts index c68fed612b..88877e722f 100644 --- a/src/data/trigger.ts +++ b/src/data/trigger.ts @@ -5,6 +5,7 @@ import { mdiCodeBraces, mdiDevices, mdiDotsHorizontal, + mdiFormatListBulleted, mdiGestureDoubleTap, mdiMapClock, mdiMapMarker, @@ -21,7 +22,7 @@ import { } from "@mdi/js"; import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg"; -import { AutomationElementGroup } from "./automation"; +import { AutomationElementGroup, Trigger, TriggerList } from "./automation"; export const TRIGGER_ICONS = { calendar: mdiCalendar, @@ -41,6 +42,7 @@ export const TRIGGER_ICONS = { webhook: mdiWebhook, persistent_notification: mdiMessageAlert, zone: mdiMapMarkerRadius, + list: mdiFormatListBulleted, }; export const TRIGGER_GROUPS: AutomationElementGroup = { @@ -65,3 +67,6 @@ export const TRIGGER_GROUPS: AutomationElementGroup = { }, }, } as const; + +export const isTriggerList = (trigger: Trigger): trigger is TriggerList => + "triggers" in trigger; diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts b/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts index 713e665abe..4f8462b9f0 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts @@ -15,6 +15,21 @@ import type { } from "../../../../../data/automation"; import type { HomeAssistant } from "../../../../../types"; +const getTriggersIds = (triggers: Trigger[]): string[] => { + const ids: Set = new Set(); + triggers.forEach((trigger) => { + if ("triggers" in trigger) { + const newIds = getTriggersIds(ensureArray(trigger.triggers)); + for (const id of newIds) { + ids.add(id); + } + } else if (trigger.id) { + ids.add(trigger.id); + } + }); + return Array.from(ids); +}; + @customElement("ha-automation-condition-trigger") export class HaTriggerCondition extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -23,7 +38,7 @@ export class HaTriggerCondition extends LitElement { @property({ type: Boolean }) public disabled = false; - @state() private _triggers: Trigger[] = []; + @state() private _triggerIds: string[] = []; private _unsub?: UnsubscribeFunc; @@ -35,14 +50,14 @@ export class HaTriggerCondition extends LitElement { } private _schema = memoizeOne( - (triggers: Trigger[]) => + (triggerIds: string[]) => [ { name: "id", selector: { select: { multiple: true, - options: triggers.map((trigger) => trigger.id!), + options: triggerIds, }, }, required: true, @@ -65,13 +80,13 @@ export class HaTriggerCondition extends LitElement { } protected render() { - if (!this._triggers.length) { + if (!this._triggerIds.length) { return this.hass.localize( "ui.panel.config.automation.editor.conditions.type.trigger.no_triggers" ); } - const schema = this._schema(this._triggers); + const schema = this._schema(this._triggerIds); return html` t.id && (seenIds.has(t.id) ? false : seenIds.add(t.id)) - ) + this._triggerIds = config?.triggers + ? getTriggersIds(ensureArray(config.triggers)) : []; } @@ -106,12 +118,12 @@ export class HaTriggerCondition extends LitElement { const newValue = ev.detail.value; if (typeof newValue.id === "string") { - if (!this._triggers.some((trigger) => trigger.id === newValue.id)) { + if (!this._triggerIds.some((id) => id === newValue.id)) { newValue.id = ""; } } else if (Array.isArray(newValue.id)) { - newValue.id = newValue.id.filter((id) => - this._triggers.some((trigger) => trigger.id === id) + newValue.id = newValue.id.filter((_id) => + this._triggerIds.some((id) => id === _id) ); if (!newValue.id.length) { newValue.id = ""; 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 31e996fab0..174233692e 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -29,6 +29,7 @@ import { classMap } from "lit/directives/class-map"; import { storage } from "../../../../common/decorators/storage"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { preventDefault } from "../../../../common/dom/prevent_default"; import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; import { handleStructError } from "../../../../common/structs/handle-errors"; @@ -50,7 +51,7 @@ import { describeTrigger } from "../../../../data/automation_i18n"; import { validateConfig } from "../../../../data/config"; import { fullEntitiesContext } from "../../../../data/context"; import { EntityRegistryEntry } from "../../../../data/entity_registry"; -import { TRIGGER_ICONS } from "../../../../data/trigger"; +import { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger"; import { showAlertDialog, showConfirmationDialog, @@ -64,6 +65,7 @@ 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-mqtt"; import "./types/ha-automation-trigger-numeric_state"; import "./types/ha-automation-trigger-persistent_notification"; @@ -75,7 +77,6 @@ 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"; -import { preventDefault } from "../../../../common/dom/prevent_default"; export interface TriggerElement extends LitElement { trigger: Trigger; @@ -87,7 +88,7 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => { if (!name) { return; } - const newVal = (ev.target as any)?.value; + const newVal = ev.detail?.value || (ev.currentTarget as any)?.value; if ((element.trigger[name] || "") === newVal) { return; @@ -146,15 +147,17 @@ export default class HaAutomationTriggerRow extends LitElement { protected render() { if (!this.trigger) return nothing; + const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger; + const supported = - customElements.get(`ha-automation-trigger-${this.trigger.trigger}`) !== - undefined; + customElements.get(`ha-automation-trigger-${type}`) !== undefined; + const yamlMode = this._yamlMode || !supported; const showId = "id" in this.trigger || this._requestShowId; return html` - ${this.trigger.enabled === false + ${"enabled" in this.trigger && this.trigger.enabled === false ? html`
${this.hass.localize( @@ -168,7 +171,7 @@ export default class HaAutomationTriggerRow extends LitElement {

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

@@ -188,14 +191,20 @@ export default class HaAutomationTriggerRow extends LitElement { .path=${mdiDotsVertical} > - + ${this.hass.localize( "ui.panel.config.automation.editor.triggers.rename" )} - + ${this.hass.localize( "ui.panel.config.automation.editor.triggers.edit_id" )} @@ -274,8 +283,11 @@ export default class HaAutomationTriggerRow extends LitElement {
  • - - ${this.trigger.enabled === false + + ${"enabled" in this.trigger && this.trigger.enabled === false ? this.hass.localize( "ui.panel.config.automation.editor.actions.enable" ) @@ -284,7 +296,8 @@ export default class HaAutomationTriggerRow extends LitElement { )} @@ -308,7 +321,8 @@ export default class HaAutomationTriggerRow extends LitElement {
    ${this._warnings @@ -336,7 +350,7 @@ export default class HaAutomationTriggerRow extends LitElement { ? html` ${this.hass.localize( "ui.panel.config.automation.editor.triggers.unsupported_platform", - { platform: this.trigger.trigger } + { platform: type } )} ` : ""} @@ -348,7 +362,7 @@ export default class HaAutomationTriggerRow extends LitElement { > ` : html` - ${showId + ${showId && !isTriggerList(this.trigger) ? html` - ${dynamicElement( - `ha-automation-trigger-${this.trigger.trigger}`, - { - hass: this.hass, - trigger: this.trigger, - disabled: this.disabled, - path: this.path, - } - )} + ${dynamicElement(`ha-automation-trigger-${type}`, { + hass: this.hass, + trigger: this.trigger, + disabled: this.disabled, + path: this.path, + })}
    `}
    @@ -546,6 +557,7 @@ export default class HaAutomationTriggerRow extends LitElement { } private _onDisable() { + if (isTriggerList(this.trigger)) return; const enabled = !(this.trigger.enabled ?? true); const value = { ...this.trigger, enabled }; fireEvent(this, "value-changed", { value }); @@ -555,7 +567,9 @@ export default class HaAutomationTriggerRow extends LitElement { } private _idChanged(ev: CustomEvent) { + if (isTriggerList(this.trigger)) return; const newId = (ev.target as any).value; + if (newId === (this.trigger.id ?? "")) { return; } @@ -583,6 +597,7 @@ export default class HaAutomationTriggerRow extends LitElement { } private _onUiChanged(ev: CustomEvent) { + if (isTriggerList(this.trigger)) return; ev.stopPropagation(); const value = { ...(this.trigger.alias ? { alias: this.trigger.alias } : {}), @@ -617,6 +632,7 @@ export default class HaAutomationTriggerRow extends LitElement { } private async _renameTrigger(): Promise { + if (isTriggerList(this.trigger)) return; const alias = await showPromptDialog(this, { title: this.hass.localize( "ui.panel.config.automation.editor.triggers.change_alias" diff --git a/src/panels/config/automation/trigger/ha-automation-trigger.ts b/src/panels/config/automation/trigger/ha-automation-trigger.ts index 39da68e029..5fe2a100a1 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger.ts @@ -18,7 +18,11 @@ import "../../../../components/ha-button"; import "../../../../components/ha-button-menu"; import "../../../../components/ha-sortable"; import "../../../../components/ha-svg-icon"; -import { AutomationClipboard, Trigger } from "../../../../data/automation"; +import { + AutomationClipboard, + Trigger, + TriggerList, +} from "../../../../data/automation"; import { HomeAssistant, ItemPath } from "../../../../types"; import { PASTE_VALUE, @@ -26,6 +30,7 @@ import { } from "../show-add-automation-element-dialog"; import "./ha-automation-trigger-row"; import type HaAutomationTriggerRow from "./ha-automation-trigger-row"; +import { isTriggerList } from "../../../../data/trigger"; @customElement("ha-automation-trigger") export default class HaAutomationTrigger extends LitElement { @@ -130,7 +135,11 @@ export default class HaAutomationTrigger extends LitElement { showAddAutomationElementDialog(this, { type: "trigger", add: this._addTrigger, - clipboardItem: this._clipboard?.trigger?.trigger, + clipboardItem: !this._clipboard?.trigger + ? undefined + : isTriggerList(this._clipboard.trigger) + ? "list" + : this._clipboard?.trigger?.trigger, }); } @@ -139,7 +148,7 @@ export default class HaAutomationTrigger extends LitElement { if (value === PASTE_VALUE) { triggers = this.triggers.concat(deepClone(this._clipboard!.trigger)); } else { - const trigger = value as Trigger["trigger"]; + const trigger = value as Exclude["trigger"]; const elClass = customElements.get( `ha-automation-trigger-${trigger}` ) as CustomElementConstructor & { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-list.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-list.ts new file mode 100644 index 0000000000..8a74813908 --- /dev/null +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-list.ts @@ -0,0 +1,54 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { ensureArray } from "../../../../../common/array/ensure-array"; +import type { TriggerList } from "../../../../../data/automation"; +import type { HomeAssistant, ItemPath } from "../../../../../types"; +import "../ha-automation-trigger"; +import { + handleChangeEvent, + TriggerElement, +} from "../ha-automation-trigger-row"; + +@customElement("ha-automation-trigger-list") +export class HaTriggerList extends LitElement implements TriggerElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public trigger!: TriggerList; + + @property({ attribute: false }) public path?: ItemPath; + + @property({ type: Boolean }) public disabled = false; + + public static get defaultConfig(): TriggerList { + return { + triggers: [], + }; + } + + protected render() { + const triggers = ensureArray(this.trigger.triggers); + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + handleChangeEvent(this, ev); + } + + static styles = css``; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-trigger-list": HaTriggerList; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index e68d165f77..005c8ef552 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3093,6 +3093,13 @@ "picker": "When someone (or something) enters or leaves a zone.", "full": "When {entity} {event, select, \n enter {enters}\n leave {leaves} other {} \n} {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n}" } + }, + "list": { + "label": "List", + "description": { + "no_trigger": "When any trigger matches", + "full": "When any of {count} {count, plural,\n one {trigger}\n other {triggers}\n} triggers" + } } } },