frontend/src/panels/config/automation/trigger/ha-automation-trigger-row.ts

490 lines
15 KiB
TypeScript

import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import "../../../../components/ha-textfield";
import { subscribeTrigger, Trigger } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "./types/ha-automation-trigger-calendar";
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-mqtt";
import "./types/ha-automation-trigger-numeric_state";
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";
import { describeTrigger } from "../../../../data/automation_i18n";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
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.target 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 });
};
const preventDefault = (ev) => ev.preventDefault();
@customElement("ha-automation-trigger-row")
export default class HaAutomationTriggerRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trigger!: Trigger;
@state() private _warnings?: string[];
@state() private _yamlMode = false;
@state() private _requestShowId = false;
@state() private _triggered?: Record<string, unknown>;
@state() private _triggerColor = false;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _triggerUnsub?: Promise<UnsubscribeFunc>;
protected render() {
const supported =
customElements.get(`ha-automation-trigger-${this.trigger.platform}`) !==
undefined;
const yamlMode = this._yamlMode || !supported;
const showId = "id" in this.trigger || this._requestShowId;
return html`
<ha-card outlined>
${this.trigger.enabled === false
? html`
<div class="disabled-bar">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.disabled"
)}
</div>
`
: ""}
<ha-expansion-panel
leftChevron
.header=${capitalizeFirstLetter(describeTrigger(this.trigger))}
>
<ha-button-menu
slot="icons"
fixed
corner="BOTTOM_START"
@action=${this._handleAction}
@click=${preventDefault}
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
</mwc-list-item>
<mwc-list-item .disabled=${!supported}>
${yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</mwc-list-item>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</mwc-list-item>
<mwc-list-item>
${this.trigger.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
</mwc-list-item>
<mwc-list-item class="warning">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</mwc-list-item>
</ha-button-menu>
<div
class=${classMap({
"card-content": true,
disabled: this.trigger.enabled === false,
})}
>
${this._warnings
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.errors.config.editor_not_supported"
)}
>
${this._warnings.length && this._warnings[0] !== undefined
? html` <ul>
${this._warnings.map(
(warning) => html`<li>${warning}</li>`
)}
</ul>`
: ""}
${this.hass.localize(
"ui.errors.config.edit_in_yaml_supported"
)}
</ha-alert>`
: ""}
${yamlMode
? html`
${!supported
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.unsupported_platform",
"platform",
this.trigger.platform
)}
`
: ""}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.trigger}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
${showId
? html`
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id"
)}
.value=${this.trigger.id || ""}
@change=${this._idChanged}
>
</ha-textfield>
`
: ""}
<div @ui-mode-not-available=${this._handleUiModeNotAvailable}>
${dynamicElement(
`ha-automation-trigger-${this.trigger.platform}`,
{ hass: this.hass, trigger: this.trigger }
)}
</div>
`}
</div>
</ha-expansion-panel>
<div
class="triggered ${classMap({
active: this._triggered !== undefined,
accent: this._triggerColor,
})}"
@click=${this._showTriggeredInfo}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.triggered"
)}
</div>
</ha-card>
`;
}
protected override updated(changedProps: PropertyValues<this>): 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, {
trigger,
});
// Don't do anything if trigger not valid or if trigger changed.
if (!validateResult.trigger.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 _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._requestShowId = true;
this.expand();
break;
case 1:
this._switchYamlMode();
this.expand();
break;
case 2:
fireEvent(this, "duplicate");
break;
case 3:
this._onDisable();
break;
case 4:
this._onDelete();
break;
}
}
private _onDelete() {
showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.automation.editor.triggers.delete_confirm"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
confirm: () => {
fireEvent(this, "value-changed", { value: null });
},
});
}
private _onDisable() {
const enabled = !(this.trigger.enabled ?? true);
const value = { ...this.trigger, enabled };
fireEvent(this, "value-changed", { value });
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
}
private _idChanged(ev: CustomEvent) {
const newId = (ev.target as any).value;
if (newId === (this.trigger.id ?? "")) {
return;
}
this._requestShowId = true;
const value = { ...this.trigger };
if (!newId) {
delete value.id;
} else {
value.id = newId;
}
fireEvent(this, "value-changed", {
value,
});
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
this._warnings = undefined;
fireEvent(this, "value-changed", { value: ev.detail.value });
}
private _switchYamlMode() {
this._warnings = undefined;
this._yamlMode = !this._yamlMode;
}
private _showTriggeredInfo() {
showAlertDialog(this, {
text: html`
<ha-yaml-editor
readOnly
.hass=${this.hass}
.defaultValue=${this._triggered}
></ha-yaml-editor>
`,
});
}
public expand() {
this.updateComplete.then(() => {
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-button-menu {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
.card-content {
padding: 16px;
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: var(--ha-card-border-radius);
border-top-left-radius: var(--ha-card-border-radius);
}
.triggered {
cursor: pointer;
position: absolute;
top: 0px;
right: 0px;
left: 0px;
text-transform: uppercase;
font-weight: bold;
font-size: 14px;
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, 4px);
border-top-left-radius: var(--ha-card-border-radius, 4px);
}
.triggered.active {
max-height: 100px;
}
.triggered:hover {
opacity: 0.8;
}
.triggered.accent {
background-color: var(--accent-color);
color: var(--text-accent-color, var(--text-primary-color));
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-textfield {
display: block;
margin-bottom: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-row": HaAutomationTriggerRow;
}
}