diff --git a/src/common/util/debounce.ts b/src/common/util/debounce.ts index 70aaddf2e0..cd61f072b9 100644 --- a/src/common/util/debounce.ts +++ b/src/common/util/debounce.ts @@ -11,7 +11,7 @@ export const debounce = ( immediate = false ) => { let timeout: number | undefined; - return (...args: T): void => { + const debouncedFunc = (...args: T): void => { const later = () => { timeout = undefined; if (!immediate) { @@ -25,4 +25,8 @@ export const debounce = ( func(...args); } }; + debouncedFunc.cancel = () => { + clearTimeout(timeout); + }; + return debouncedFunc; }; diff --git a/src/data/config.ts b/src/data/config.ts new file mode 100644 index 0000000000..281e2debbf --- /dev/null +++ b/src/data/config.ts @@ -0,0 +1,19 @@ +import { HomeAssistant } from "../types"; + +interface ValidationResult { + valid: boolean; + error: string | null; +} + +type ValidKeys = "trigger" | "action" | "condition"; + +export const validateConfig = < + T extends Partial<{ [key in ValidKeys]: unknown }> +>( + hass: HomeAssistant, + config: T +): Promise> => + hass.callWS({ + type: "validate_config", + ...config, + }); 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 ecf1f1a4b7..7d3c15c3ed 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -1,11 +1,13 @@ +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { mdiDotsVertical } from "@mdi/js"; import "@material/mwc-select"; import type { Select } from "@material/mwc-select"; -import { css, CSSResultGroup, html, LitElement } from "lit"; +import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { classMap } from "lit/directives/class-map"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../common/dom/fire_event"; import { stringCompare } from "../../../../common/string/compare"; @@ -16,7 +18,7 @@ import "../../../../components/ha-card"; import "../../../../components/ha-alert"; import "../../../../components/ha-textfield"; import "../../../../components/ha-icon-button"; -import type { Trigger } from "../../../../data/automation"; +import { subscribeTrigger, Trigger } from "../../../../data/automation"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; @@ -34,6 +36,8 @@ 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 { debounce } from "../../../../common/util/debounce"; +import { validateConfig } from "../../../../data/config"; const OPTIONS = [ "device", @@ -90,6 +94,12 @@ export default class HaAutomationTriggerRow extends LitElement { @state() private _requestShowId = false; + @state() private _triggered = false; + + @state() private _triggerColor = false; + + private _triggerUnsub?: Promise; + private _processedTypes = memoizeOne( (localize: LocalizeFunc): [string, string][] => OPTIONS.map( @@ -219,10 +229,98 @@ export default class HaAutomationTriggerRow extends LitElement { `} +
+ ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.triggered" + )} +
`; } + 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, { + trigger: this.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, + () => { + if (untriggerTimeout !== undefined) { + clearTimeout(untriggerTimeout); + this._triggerColor = !this._triggerColor; + } else { + this._triggerColor = false; + } + this._triggered = true; + untriggerTimeout = window.setTimeout(() => { + this._triggered = false; + 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) { @@ -327,6 +425,31 @@ export default class HaAutomationTriggerRow extends LitElement { z-index: 3; --mdc-theme-text-primary-on-background: var(--primary-text-color); } + .triggered { + position: absolute; + top: 0px; + right: 0px; + left: 0px; + text-transform: uppercase; + pointer-events: none; + 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.accent { + background-color: var(--accent-color); + color: var(--text-accent-color, var(--text-primary-color)); + } .rtl .card-menu { float: left; } diff --git a/src/translations/en.json b/src/translations/en.json index 7e214663dd..20c6c5d86f 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1623,6 +1623,7 @@ "header": "Triggers", "introduction": "Triggers are what starts the processing of an automation rule. It is possible to specify multiple triggers for the same rule. Once a trigger starts, Home Assistant will validate the conditions, if any, and call the action.", "learn_more": "Learn more about triggers", + "triggered": "Triggered", "add": "Add trigger", "id": "Trigger ID", "edit_id": "Edit trigger ID",