Add UI support for trigger list (#22133)

* Add UI support for trigger list

* Update gallery

* Fix gallery
pull/22135/head
Paul Bottein 2024-09-27 16:56:22 +02:00 committed by GitHub
parent 1c12c2b714
commit 94e321a364
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 194 additions and 43 deletions

9
demo/src/stubs/config.ts Normal file
View File

@ -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 },
}));
};

6
demo/src/stubs/tags.ts Normal file
View File

@ -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[]);
};

View File

@ -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 = {

View File

@ -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 {

View File

@ -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"}
></hat-graph-node>

View File

@ -206,7 +206,8 @@ export type Trigger =
| TemplateTrigger
| EventTrigger
| DeviceTrigger
| CalendarTrigger;
| CalendarTrigger
| TriggerList;
interface BaseCondition {
condition: string;

View File

@ -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;
}

View File

@ -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;

View File

@ -15,6 +15,21 @@ import type {
} from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
const getTriggersIds = (triggers: Trigger[]): string[] => {
const ids: Set<string> = 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`
<ha-form
@ -93,11 +108,8 @@ export class HaTriggerCondition extends LitElement {
);
private _automationUpdated(config?: AutomationConfig) {
const seenIds = new Set();
this._triggers = config?.triggers
? ensureArray(config.triggers).filter(
(t) => 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 = "";

View File

@ -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`
<ha-card outlined>
${this.trigger.enabled === false
${"enabled" in this.trigger && this.trigger.enabled === false
? html`
<div class="disabled-bar">
${this.hass.localize(
@ -168,7 +171,7 @@ export default class HaAutomationTriggerRow extends LitElement {
<h3 slot="header">
<ha-svg-icon
class="trigger-icon"
.path=${TRIGGER_ICONS[this.trigger.trigger]}
.path=${TRIGGER_ICONS[type]}
></ha-svg-icon>
${describeTrigger(this.trigger, this.hass, this._entityReg)}
</h3>
@ -188,14 +191,20 @@ export default class HaAutomationTriggerRow extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
<mwc-list-item
graphic="icon"
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</mwc-list-item>
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
<mwc-list-item
graphic="icon"
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
@ -274,8 +283,11 @@ export default class HaAutomationTriggerRow extends LitElement {
<li divider role="separator"></li>
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
${this.trigger.enabled === false
<mwc-list-item
graphic="icon"
.disabled=${this.disabled || type === "list"}
>
${"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 {
)}
<ha-svg-icon
slot="graphic"
.path=${this.trigger.enabled === false
.path=${"enabled" in this.trigger &&
this.trigger.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
@ -308,7 +321,8 @@ export default class HaAutomationTriggerRow extends LitElement {
<div
class=${classMap({
"card-content": true,
disabled: this.trigger.enabled === false,
disabled:
"enabled" in this.trigger && this.trigger.enabled === false,
})}
>
${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 {
></ha-yaml-editor>
`
: html`
${showId
${showId && !isTriggerList(this.trigger)
? html`
<ha-textfield
.label=${this.hass.localize(
@ -365,15 +379,12 @@ export default class HaAutomationTriggerRow extends LitElement {
@ui-mode-not-available=${this._handleUiModeNotAvailable}
@value-changed=${this._onUiChanged}
>
${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,
})}
</div>
`}
</div>
@ -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<void> {
if (isTriggerList(this.trigger)) return;
const alias = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.triggers.change_alias"

View File

@ -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, TriggerList>["trigger"];
const elClass = customElements.get(
`ha-automation-trigger-${trigger}`
) as CustomElementConstructor & {

View File

@ -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`
<ha-automation-trigger
.path=${[...(this.path ?? []), "triggers"]}
.triggers=${triggers}
.hass=${this.hass}
.disabled=${this.disabled}
.name=${"triggers"}
@value-changed=${this._valueChanged}
></ha-automation-trigger>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
static styles = css``;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-list": HaTriggerList;
}
}

View File

@ -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"
}
}
}
},