Fix showing choose actions if default path chosen and other things (#8779)

This commit is contained in:
Paulus Schoutsen 2021-04-01 01:28:37 -07:00 committed by GitHub
parent 17b1f3e465
commit 5c1604e959
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 467 additions and 7 deletions

View File

@ -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`
<ha-card header="Actions">
${actions.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, conf as any)}</span>
<pre>${safeDump(conf)}</pre>
</div>
`
)}
</ha-card>
`;
}
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;
}
}

View File

@ -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`
<ha-card header="Conditions">
${conditions.map(
(conf) => html`
<div class="condition">
<span>${describeCondition(conf as any)}</span>
<pre>${safeDump(conf)}</pre>
</div>
`
)}
</ha-card>
`;
}
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;
}
}

View File

@ -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`
<ha-card header="Triggers">
${triggers.map(
(conf) => html`
<div class="trigger">
<span>${describeTrigger(conf as any)}</span>
<pre>${safeDump(conf)}</pre>
</div>
`
)}
</ha-card>
`;
}
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;
}
}

View File

@ -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`
<ha-card .heading=${trace.trace.config.alias}>
(trace, idx) => html`
<ha-card .header=${trace.trace.config.alias}>
<div class="card-content">
<hat-script-graph
.trace=${trace.trace}
.selected=${this._selected[idx]}
@graph-node-selected=${(ev) => {
this._selected = { ...this._selected, [idx]: ev.detail.path };
}}
></hat-script-graph>
<hat-trace-timeline
allowPick
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
.selectedPath=${this._selected[idx]}
@value-changed=${(ev) => {
this._selected = {
...this._selected,
[idx]: ev.detail.value,
};
}}
></hat-trace-timeline>
<button @click=${() => console.log(trace)}>Log trace</button>
</div>
</ha-card>
`
@ -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;
}
`;
}
}

View File

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

View File

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

View File

@ -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<string, any>;
@ -115,6 +116,16 @@ export interface ChooseAction {
default?: Action[];
}
export interface VariablesAction {
alias?: string;
variables: Record<string, unknown>;
}
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";

141
src/data/script_i18n.ts Normal file
View File

@ -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 = <T extends ActionType>(
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;
};

1
src/data/template.ts Normal file
View File

@ -0,0 +1 @@
export const isDynamicTemplate = (value: string) => value.includes("{{");