From 5c1604e95989a6e8b601dc04a3a075ea67cb3ac0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Apr 2021 01:28:37 -0700 Subject: [PATCH] Fix showing choose actions if default path chosen and other things (#8779) --- .../demos/demo-automation-describe-action.ts | 102 +++++++++++++ .../demo-automation-describe-condition.ts | 65 ++++++++ .../demos/demo-automation-describe-trigger.ts | 68 +++++++++ gallery/src/demos/demo-automation-trace.ts | 38 ++++- src/components/trace/hat-trace-timeline.ts | 8 +- src/data/automation_i18n.ts | 15 ++ src/data/script.ts | 36 ++++- src/data/script_i18n.ts | 141 ++++++++++++++++++ src/data/template.ts | 1 + 9 files changed, 467 insertions(+), 7 deletions(-) create mode 100644 gallery/src/demos/demo-automation-describe-action.ts create mode 100644 gallery/src/demos/demo-automation-describe-condition.ts create mode 100644 gallery/src/demos/demo-automation-describe-trigger.ts create mode 100644 src/data/automation_i18n.ts create mode 100644 src/data/script_i18n.ts create mode 100644 src/data/template.ts diff --git a/gallery/src/demos/demo-automation-describe-action.ts b/gallery/src/demos/demo-automation-describe-action.ts new file mode 100644 index 0000000000..642bcfb58c --- /dev/null +++ b/gallery/src/demos/demo-automation-describe-action.ts @@ -0,0 +1,102 @@ +import { safeDump } from "js-yaml"; +import { + customElement, + html, + css, + LitElement, + TemplateResult, + property, +} from "lit-element"; +import "../../../src/components/ha-card"; +import { describeAction } from "../../../src/data/script_i18n"; +import { provideHass } from "../../../src/fake_data/provide_hass"; +import { HomeAssistant } from "../../../src/types"; + +const actions = [ + { wait_template: "{{ true }}", alias: "Something with an alias" }, + { delay: "0:05" }, + { wait_template: "{{ true }}" }, + { + condition: "template", + value_template: "{{ true }}", + }, + { event: "happy_event" }, + { + device_id: "abcdefgh", + domain: "plex", + entity_id: "media_player.kitchen", + }, + { scene: "scene.kitchen_morning" }, + { + wait_for_trigger: [ + { + platform: "state", + entity_id: "input_boolean.toggle_1", + }, + ], + }, + { + variables: { + hello: "world", + }, + }, + { + service: "input_boolean.toggle", + target: { + entity_id: "input_boolean.toggle_4", + }, + }, +]; + +@customElement("demo-automation-describe-action") +export class DemoAutomationDescribeAction extends LitElement { + @property({ attribute: false }) hass!: HomeAssistant; + + protected render(): TemplateResult { + if (!this.hass) { + return html``; + } + return html` + + ${actions.map( + (conf) => html` +
+ ${describeAction(this.hass, conf as any)} +
${safeDump(conf)}
+
+ ` + )} +
+ `; + } + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + const hass = provideHass(this); + hass.updateTranslations(null, "en"); + } + + static get styles() { + return css` + ha-card { + max-width: 600px; + margin: 24px auto; + } + .action { + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + } + span { + margin-right: 16px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-automation-describe-action": DemoAutomationDescribeAction; + } +} diff --git a/gallery/src/demos/demo-automation-describe-condition.ts b/gallery/src/demos/demo-automation-describe-condition.ts new file mode 100644 index 0000000000..ef57b9a1e5 --- /dev/null +++ b/gallery/src/demos/demo-automation-describe-condition.ts @@ -0,0 +1,65 @@ +import { safeDump } from "js-yaml"; +import { + customElement, + html, + css, + LitElement, + TemplateResult, +} from "lit-element"; +import "../../../src/components/ha-card"; +import { describeCondition } from "../../../src/data/automation_i18n"; + +const conditions = [ + { condition: "and" }, + { condition: "not" }, + { condition: "or" }, + { condition: "state" }, + { condition: "numeric_state" }, + { condition: "sun", after: "sunset" }, + { condition: "sun", after: "sunrise" }, + { condition: "zone" }, + { condition: "time" }, + { condition: "template" }, +]; + +@customElement("demo-automation-describe-condition") +export class DemoAutomationDescribeCondition extends LitElement { + protected render(): TemplateResult { + return html` + + ${conditions.map( + (conf) => html` +
+ ${describeCondition(conf as any)} +
${safeDump(conf)}
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-card { + max-width: 600px; + margin: 24px auto; + } + .condition { + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + } + span { + margin-right: 16px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-automation-describe-condition": DemoAutomationDescribeCondition; + } +} diff --git a/gallery/src/demos/demo-automation-describe-trigger.ts b/gallery/src/demos/demo-automation-describe-trigger.ts new file mode 100644 index 0000000000..f870db44d7 --- /dev/null +++ b/gallery/src/demos/demo-automation-describe-trigger.ts @@ -0,0 +1,68 @@ +import { safeDump } from "js-yaml"; +import { + customElement, + html, + css, + LitElement, + TemplateResult, +} from "lit-element"; +import "../../../src/components/ha-card"; +import { describeTrigger } from "../../../src/data/automation_i18n"; + +const triggers = [ + { platform: "state" }, + { platform: "mqtt" }, + { platform: "geo_location" }, + { platform: "homeassistant" }, + { platform: "numeric_state" }, + { platform: "sun" }, + { platform: "time_pattern" }, + { platform: "webhook" }, + { platform: "zone" }, + { platform: "tag" }, + { platform: "time" }, + { platform: "template" }, + { platform: "event" }, +]; + +@customElement("demo-automation-describe-trigger") +export class DemoAutomationDescribeTrigger extends LitElement { + protected render(): TemplateResult { + return html` + + ${triggers.map( + (conf) => html` +
+ ${describeTrigger(conf as any)} +
${safeDump(conf)}
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-card { + max-width: 600px; + margin: 24px auto; + } + .trigger { + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + } + span { + margin-right: 16px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-automation-describe-trigger": DemoAutomationDescribeTrigger; + } +} diff --git a/gallery/src/demos/demo-automation-trace.ts b/gallery/src/demos/demo-automation-trace.ts index a7124ab527..8187c01630 100644 --- a/gallery/src/demos/demo-automation-trace.ts +++ b/gallery/src/demos/demo-automation-trace.ts @@ -4,9 +4,11 @@ import { css, LitElement, TemplateResult, + internalProperty, property, } from "lit-element"; import "../../../src/components/ha-card"; +import "../../../src/components/trace/hat-script-graph"; import "../../../src/components/trace/hat-trace-timeline"; import { provideHass } from "../../../src/fake_data/provide_hass"; import { HomeAssistant } from "../../../src/types"; @@ -20,20 +22,38 @@ const traces: DemoTrace[] = [basicTrace, motionLightTrace]; export class DemoAutomationTrace extends LitElement { @property({ attribute: false }) hass?: HomeAssistant; + @internalProperty() private _selected = {}; + protected render(): TemplateResult { if (!this.hass) { return html``; } return html` ${traces.map( - (trace) => html` - + (trace, idx) => html` +
+ { + this._selected = { ...this._selected, [idx]: ev.detail.path }; + }} + > { + this._selected = { + ...this._selected, + [idx]: ev.detail.value, + }; + }} > +
` @@ -53,6 +73,20 @@ export class DemoAutomationTrace extends LitElement { max-width: 600px; margin: 24px; } + .card-content { + display: flex; + } + .card-content > * { + margin-right: 16px; + } + .card-content > *:last-child { + margin-right: 0; + } + button { + position: absolute; + top: 0; + right: 0; + } `; } } diff --git a/src/components/trace/hat-trace-timeline.ts b/src/components/trace/hat-trace-timeline.ts index ad7ce559e2..ac1b957004 100644 --- a/src/components/trace/hat-trace-timeline.ts +++ b/src/components/trace/hat-trace-timeline.ts @@ -33,6 +33,7 @@ import { } from "../../data/script"; import relativeTime from "../../common/datetime/relative_time"; import { fireEvent } from "../../common/dom/fire_event"; +import { describeAction } from "../../data/script_i18n"; const LOGBOOK_ENTRIES_BEFORE_FOLD = 2; @@ -262,7 +263,7 @@ class ActionRenderer { return this._handleChoose(index); } - this._renderEntry(path, data.alias || actionType); + this._renderEntry(path, describeAction(this.hass, data, actionType)); return index + 1; } @@ -334,7 +335,10 @@ class ActionRenderer { } // We're going to skip all conditions - if (parts[startLevel + 3] === "sequence") { + if ( + (defaultExecuted && parts[startLevel + 1] === "default") || + (!defaultExecuted && parts[startLevel + 3] === "sequence") + ) { break; } } diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts new file mode 100644 index 0000000000..c4c9410a75 --- /dev/null +++ b/src/data/automation_i18n.ts @@ -0,0 +1,15 @@ +import { Trigger, Condition } from "./automation"; + +export const describeTrigger = (trigger: Trigger) => { + return `${trigger.platform} trigger`; +}; + +export const describeCondition = (condition: Condition) => { + if (condition.alias) { + return condition.alias; + } + if (condition.condition === "template") { + return "Test a template"; + } + return `${condition.condition} condition`; +}; diff --git a/src/data/script.ts b/src/data/script.ts index 5bc023b161..bad7b3650c 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -37,7 +37,8 @@ export interface EventAction { export interface ServiceAction { alias?: string; - service: string; + service?: string; + service_template?: string; entity_id?: string; target?: HassServiceTarget; data?: Record; @@ -115,6 +116,16 @@ export interface ChooseAction { default?: Action[]; } +export interface VariablesAction { + alias?: string; + variables: Record; +} + +interface UnknownAction { + alias?: string; + [key: string]: unknown; +} + export type Action = | EventAction | DeviceAction @@ -125,7 +136,26 @@ export type Action = | WaitAction | WaitForTriggerAction | RepeatAction - | ChooseAction; + | ChooseAction + | VariablesAction + | UnknownAction; + +export interface ActionTypes { + delay: DelayAction; + wait_template: WaitAction; + check_condition: Condition; + fire_event: EventAction; + device_action: DeviceAction; + activate_scene: SceneAction; + repeat: RepeatAction; + choose: ChooseAction; + wait_for_trigger: WaitForTriggerAction; + variables: VariablesAction; + service: ServiceAction; + unknown: UnknownAction; +} + +export type ActionType = keyof ActionTypes; export const triggerScript = ( hass: HomeAssistant, @@ -166,7 +196,7 @@ export const getScriptEditorInitData = () => { return data; }; -export const getActionType = (action: Action) => { +export const getActionType = (action: Action): ActionType => { // Check based on config_validation.py#determine_script_action if ("delay" in action) { return "delay"; diff --git a/src/data/script_i18n.ts b/src/data/script_i18n.ts new file mode 100644 index 0000000000..d920987351 --- /dev/null +++ b/src/data/script_i18n.ts @@ -0,0 +1,141 @@ +import secondsToDuration from "../common/datetime/seconds_to_duration"; +import { computeStateName } from "../common/entity/compute_state_name"; +import { HomeAssistant } from "../types"; +import { Condition } from "./automation"; +import { describeCondition, describeTrigger } from "./automation_i18n"; +import { + ActionType, + getActionType, + DelayAction, + SceneAction, + WaitForTriggerAction, + ActionTypes, + VariablesAction, + EventAction, +} from "./script"; +import { isDynamicTemplate } from "./template"; + +export const describeAction = ( + hass: HomeAssistant, + action: ActionTypes[T], + actionType?: T +): string => { + if (action.alias) { + return action.alias; + } + if (!actionType) { + actionType = getActionType(action) as T; + } + + if (actionType === "service") { + const config = action as ActionTypes["service"]; + + let base: string | undefined; + + if ( + config.service_template || + (config.service && isDynamicTemplate(config.service)) + ) { + base = "Call a service based on a template"; + } else if (config.service) { + base = `Call service ${config.service}`; + } else { + return actionType; + } + if (config.target) { + const targets: string[] = []; + + for (const [key, label] of Object.entries({ + area_id: "areas", + device_id: "devices", + entity_id: "entities", + })) { + if (!(key in config.target)) { + continue; + } + const keyConf: string[] = Array.isArray(config.target[key]) + ? config.target[key] + : [config.target[key]]; + + const values: string[] = []; + + let renderValues = true; + + for (const targetThing of keyConf) { + if (isDynamicTemplate(targetThing)) { + targets.push(`templated ${label}`); + renderValues = false; + break; + } else { + values.push(targetThing); + } + } + + if (renderValues) { + targets.push(`${label} ${values.join(", ")}`); + } + } + if (targets.length > 0) { + base += ` on ${targets.join(", ")}`; + } + } + + return base; + } + + if (actionType === "delay") { + const config = action as DelayAction; + + let duration: string; + + if (typeof config.delay === "number") { + duration = `for ${secondsToDuration(config.delay)!}`; + } else if (typeof config.delay === "string") { + duration = isDynamicTemplate(config.delay) + ? "based on a template" + : `for ${config.delay}`; + } else { + duration = `for ${JSON.stringify(config.delay)}`; + } + + return `Delay ${duration}`; + } + + if (actionType === "activate_scene") { + const config = action as SceneAction; + const sceneStateObj = hass.states[config.scene]; + return `Activate scene ${ + sceneStateObj ? computeStateName(sceneStateObj) : config.scene + }`; + } + + if (actionType === "wait_for_trigger") { + const config = action as WaitForTriggerAction; + return `Wait for ${config.wait_for_trigger + .map((trigger) => describeTrigger(trigger)) + .join(", ")}`; + } + + if (actionType === "variables") { + const config = action as VariablesAction; + return `Define variables ${Object.keys(config.variables).join(", ")}`; + } + + if (actionType === "fire_event") { + const config = action as EventAction; + if (isDynamicTemplate(config.event)) { + return "Fire event based on a template"; + } + return `Fire event ${config.event}`; + } + + if (actionType === "wait_template") { + return "Wait for a template to render true"; + } + + if (actionType === "check_condition") { + return `Test ${describeCondition(action as Condition)}`; + } + + return actionType; +}; diff --git a/src/data/template.ts b/src/data/template.ts new file mode 100644 index 0000000000..452a36043a --- /dev/null +++ b/src/data/template.ts @@ -0,0 +1 @@ +export const isDynamicTemplate = (value: string) => value.includes("{{");