Add tracing to scripts (#9486)

This commit is contained in:
Bram Kragten 2021-07-06 10:46:51 +02:00 committed by GitHub
parent de5a817953
commit 4b9487183b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 768 additions and 153 deletions

View File

@ -1,16 +1,16 @@
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../../components/ha-code-editor"; import "../ha-code-editor";
import "../../../../components/ha-icon-button"; import "../ha-icon-button";
import { AutomationTraceExtended } from "../../../../data/trace"; import { TraceExtended } from "../../data/trace";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../types";
@customElement("ha-automation-trace-blueprint-config") @customElement("ha-trace-blueprint-config")
export class HaAutomationTraceBlueprintConfig extends LitElement { export class HaTraceBlueprintConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public trace!: AutomationTraceExtended; @property({ attribute: false }) public trace!: TraceExtended;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
@ -24,6 +24,6 @@ export class HaAutomationTraceBlueprintConfig extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-automation-trace-blueprint-config": HaAutomationTraceBlueprintConfig; "ha-trace-blueprint-config": HaTraceBlueprintConfig;
} }
} }

View File

@ -1,16 +1,16 @@
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../../components/ha-code-editor"; import "../ha-code-editor";
import "../../../../components/ha-icon-button"; import "../ha-icon-button";
import { AutomationTraceExtended } from "../../../../data/trace"; import { TraceExtended } from "../../data/trace";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../types";
@customElement("ha-automation-trace-config") @customElement("ha-trace-config")
export class HaAutomationTraceConfig extends LitElement { export class HaTraceConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public trace!: AutomationTraceExtended; @property({ attribute: false }) public trace!: TraceExtended;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
@ -28,6 +28,6 @@ export class HaAutomationTraceConfig extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-automation-trace-config": HaAutomationTraceConfig; "ha-trace-config": HaTraceConfig;
} }
} }

View File

@ -1,16 +1,19 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../../components/trace/hat-logbook-note"; import { LogbookEntry } from "../../data/logbook";
import type { LogbookEntry } from "../../../../data/logbook"; import { HomeAssistant } from "../../types";
import type { HomeAssistant } from "../../../../types"; import "./hat-logbook-note";
import "../../../logbook/ha-logbook"; import "../../panels/logbook/ha-logbook";
import { TraceExtended } from "../../data/trace";
@customElement("ha-automation-trace-logbook") @customElement("ha-trace-logbook")
export class HaAutomationTraceLogbook extends LitElement { export class HaTraceLogbook extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow!: boolean; @property({ type: Boolean, reflect: true }) public narrow!: boolean;
@property({ attribute: false }) public trace!: TraceExtended;
@property({ attribute: false }) public logbookEntries!: LogbookEntry[]; @property({ attribute: false }) public logbookEntries!: LogbookEntry[];
protected render(): TemplateResult { protected render(): TemplateResult {
@ -22,7 +25,7 @@ export class HaAutomationTraceLogbook extends LitElement {
.entries=${this.logbookEntries} .entries=${this.logbookEntries}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-logbook> ></ha-logbook>
<hat-logbook-note></hat-logbook-note> <hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
` `
: html`<div class="padded-box"> : html`<div class="padded-box">
No Logbook entries found for this step. No Logbook entries found for this step.
@ -42,6 +45,6 @@ export class HaAutomationTraceLogbook extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-automation-trace-logbook": HaAutomationTraceLogbook; "ha-trace-logbook": HaTraceLogbook;
} }
} }

View File

@ -2,33 +2,33 @@ import { dump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import "../../../../components/ha-code-editor"; import "../ha-code-editor";
import "../../../../components/ha-icon-button"; import "../ha-icon-button";
import type { NodeInfo } from "../../../../components/trace/hat-graph"; import type { NodeInfo } from "./hat-graph";
import "../../../../components/trace/hat-logbook-note"; import "./hat-logbook-note";
import { LogbookEntry } from "../../../../data/logbook"; import { LogbookEntry } from "../../data/logbook";
import { import {
ActionTraceStep, ActionTraceStep,
AutomationTraceExtended,
ChooseActionTraceStep, ChooseActionTraceStep,
getDataFromPath, getDataFromPath,
} from "../../../../data/trace"; TraceExtended,
import { HomeAssistant } from "../../../../types"; } from "../../data/trace";
import "../../../logbook/ha-logbook"; import "../../panels/logbook/ha-logbook";
import { traceTabStyles } from "./styles"; import { traceTabStyles } from "./trace-tab-styles";
import { HomeAssistant } from "../../types";
@customElement("ha-automation-trace-path-details") @customElement("ha-trace-path-details")
export class HaAutomationTracePathDetails extends LitElement { export class HaTracePathDetails extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow!: boolean; @property({ type: Boolean, reflect: true }) public narrow!: boolean;
@property() private selected!: NodeInfo; @property({ attribute: false }) public trace!: TraceExtended;
@property() public trace!: AutomationTraceExtended; @property({ attribute: false }) public logbookEntries!: LogbookEntry[];
@property() public logbookEntries!: LogbookEntry[]; @property({ attribute: false }) public selected!: NodeInfo;
@property() renderedNodes: Record<string, any> = {}; @property() renderedNodes: Record<string, any> = {};
@ -230,7 +230,7 @@ export class HaAutomationTracePathDetails extends LitElement {
.entries=${entries} .entries=${entries}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-logbook> ></ha-logbook>
<hat-logbook-note></hat-logbook-note> <hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
` `
: html`<div class="padded-box"> : html`<div class="padded-box">
No Logbook entries found for this step. No Logbook entries found for this step.
@ -267,6 +267,6 @@ export class HaAutomationTracePathDetails extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-automation-trace-path-details": HaAutomationTracePathDetails; "ha-trace-path-details": HaTracePathDetails;
} }
} }

View File

@ -1,17 +1,17 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { NodeInfo } from "../../../../components/trace/hat-graph"; import type { NodeInfo } from "./hat-graph";
import "../../../../components/trace/hat-logbook-note"; import "./hat-logbook-note";
import "../../../../components/trace/hat-trace-timeline"; import "./hat-trace-timeline";
import type { LogbookEntry } from "../../../../data/logbook"; import type { LogbookEntry } from "../../data/logbook";
import type { AutomationTraceExtended } from "../../../../data/trace"; import type { TraceExtended } from "../../data/trace";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../types";
@customElement("ha-automation-trace-timeline") @customElement("ha-trace-timeline")
export class HaAutomationTraceTimeline extends LitElement { export class HaTraceTimeline extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trace!: AutomationTraceExtended; @property({ attribute: false }) public trace!: TraceExtended;
@property({ attribute: false }) public logbookEntries!: LogbookEntry[]; @property({ attribute: false }) public logbookEntries!: LogbookEntry[];
@ -27,7 +27,7 @@ export class HaAutomationTraceTimeline extends LitElement {
allowPick allowPick
> >
</hat-trace-timeline> </hat-trace-timeline>
<hat-logbook-note></hat-logbook-note> <hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
`; `;
} }
@ -45,6 +45,6 @@ export class HaAutomationTraceTimeline extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-automation-trace-timeline": HaAutomationTraceTimeline; "ha-trace-timeline": HaTraceTimeline;
} }
} }

View File

@ -8,7 +8,7 @@ export class HatGraphNode extends LitElement {
@property({ reflect: true, type: Boolean }) disabled?: boolean; @property({ reflect: true, type: Boolean }) disabled?: boolean;
@property({ reflect: true, type: Boolean }) graphstart?: boolean; @property({ reflect: true, type: Boolean }) graphStart?: boolean;
@property({ reflect: true, type: Boolean }) nofocus?: boolean; @property({ reflect: true, type: Boolean }) nofocus?: boolean;
@ -21,20 +21,20 @@ export class HatGraphNode extends LitElement {
} }
render() { render() {
const height = NODE_SIZE + (this.graphstart ? 2 : SPACING + 1); const height = NODE_SIZE + (this.graphStart ? 2 : SPACING + 1);
const width = SPACING + NODE_SIZE; const width = SPACING + NODE_SIZE;
return svg` return svg`
<svg <svg
width="${width}px" width="${width}px"
height="${height}px" height="${height}px"
viewBox="-${Math.ceil(width / 2)} -${ viewBox="-${Math.ceil(width / 2)} -${
this.graphstart this.graphStart
? Math.ceil(height / 2) ? Math.ceil(height / 2)
: Math.ceil((NODE_SIZE + SPACING * 2) / 2) : Math.ceil((NODE_SIZE + SPACING * 2) / 2)
} ${width} ${height}" } ${width} ${height}"
> >
${ ${
this.graphstart this.graphStart
? `` ? ``
: svg` : svg`
<path <path

View File

@ -30,16 +30,16 @@ export class HatGraph extends LitElement {
@property({ reflect: true, type: Boolean }) branching?: boolean; @property({ reflect: true, type: Boolean }) branching?: boolean;
@property({ reflect: true, converter: track_converter }) @property({ converter: track_converter })
track_start?: number[]; track_start?: number[];
@property({ reflect: true, converter: track_converter }) track_end?: number[]; @property({ converter: track_converter }) track_end?: number[];
@property({ reflect: true, type: Boolean }) disabled?: boolean; @property({ reflect: true, type: Boolean }) disabled?: boolean;
@property({ reflect: true, type: Boolean }) selected?: boolean; @property({ type: Boolean }) selected?: boolean;
@property({ reflect: true, type: Boolean }) short = false; @property({ type: Boolean }) short = false;
async updateChildren() { async updateChildren() {
this._num_items = this.children.length; this._num_items = this.children.length;
@ -68,7 +68,7 @@ export class HatGraph extends LitElement {
return html` return html`
<slot name="head" @slotchange=${this.updateChildren}> </slot> <slot name="head" @slotchange=${this.updateChildren}> </slot>
${this.branching ${this.branching && branches.some((branch) => !branch.start)
? svg` ? svg`
<svg <svg
id="top" id="top"

View File

@ -1,11 +1,13 @@
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@customElement("hat-logbook-note") @customElement("hat-logbook-note")
class HatLogbookNote extends LitElement { class HatLogbookNote extends LitElement {
@property() public domain = "automation";
render() { render() {
return html` return html`
Not all shown logbook entries might be related to this automation. Not all shown logbook entries might be related to this ${this.domain}.
`; `;
} }

View File

@ -36,9 +36,9 @@ import {
WaitForTriggerAction, WaitForTriggerAction,
} from "../../data/script"; } from "../../data/script";
import { import {
AutomationTraceExtended,
ChooseActionTraceStep, ChooseActionTraceStep,
ConditionTraceStep, ConditionTraceStep,
TraceExtended,
} from "../../data/trace"; } from "../../data/trace";
import "../ha-svg-icon"; import "../ha-svg-icon";
import { NodeInfo, NODE_SIZE, SPACING } from "./hat-graph"; import { NodeInfo, NODE_SIZE, SPACING } from "./hat-graph";
@ -53,7 +53,7 @@ declare global {
@customElement("hat-script-graph") @customElement("hat-script-graph")
class HatScriptGraph extends LitElement { class HatScriptGraph extends LitElement {
@property({ attribute: false }) public trace!: AutomationTraceExtended; @property({ attribute: false }) public trace!: TraceExtended;
@property({ attribute: false }) public selected; @property({ attribute: false }) public selected;
@ -137,7 +137,11 @@ class HatScriptGraph extends LitElement {
`; `;
} }
private render_choose_node(config: ChooseAction, path: string) { private render_choose_node(
config: ChooseAction,
path: string,
graphStart = false
) {
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined; const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
const trace_path = trace?.[0].result const trace_path = trace?.[0].result
? trace[0].result.choice === "default" ? trace[0].result.choice === "default"
@ -157,6 +161,7 @@ class HatScriptGraph extends LitElement {
})} })}
> >
<hat-graph-node <hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiCallSplit} .iconPath=${mdiCallSplit}
class=${classMap({ class=${classMap({
track: trace !== undefined, track: trace !== undefined,
@ -210,7 +215,11 @@ class HatScriptGraph extends LitElement {
`; `;
} }
private render_condition_node(node: Condition, path: string) { private render_condition_node(
node: Condition,
path: string,
graphStart = false
) {
const trace = (this.trace.trace[path] as ConditionTraceStep[]) || undefined; const trace = (this.trace.trace[path] as ConditionTraceStep[]) || undefined;
const track_path = const track_path =
trace?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2; trace?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2;
@ -228,23 +237,18 @@ class HatScriptGraph extends LitElement {
short short
> >
<hat-graph-node <hat-graph-node
.graphStart=${graphStart}
slot="head" slot="head"
class=${classMap({ class=${classMap({
track: Boolean(trace), track: Boolean(trace),
})} })}
.iconPath=${mdiAbTesting} .iconPath=${mdiAbTesting}
nofocus nofocus
graphEnd
></hat-graph-node> ></hat-graph-node>
<div <div style=${`width: ${NODE_SIZE + SPACING}px;`}></div>
style=${`width: ${NODE_SIZE + SPACING}px;`}
graphStart
graphEnd
></div>
<div></div> <div></div>
<hat-graph-node <hat-graph-node
.iconPath=${mdiClose} .iconPath=${mdiClose}
graphEnd
nofocus nofocus
class=${classMap({ class=${classMap({
track: track_path === 2, track: track_path === 2,
@ -254,9 +258,14 @@ class HatScriptGraph extends LitElement {
`; `;
} }
private render_delay_node(node: DelayAction, path: string) { private render_delay_node(
node: DelayAction,
path: string,
graphStart = false
) {
return html` return html`
<hat-graph-node <hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiTimerOutline} .iconPath=${mdiTimerOutline}
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
class=${classMap({ class=${classMap({
@ -268,9 +277,14 @@ class HatScriptGraph extends LitElement {
`; `;
} }
private render_device_node(node: DeviceAction, path: string) { private render_device_node(
node: DeviceAction,
path: string,
graphStart = false
) {
return html` return html`
<hat-graph-node <hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiDevices} .iconPath=${mdiDevices}
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
class=${classMap({ class=${classMap({
@ -282,9 +296,14 @@ class HatScriptGraph extends LitElement {
`; `;
} }
private render_event_node(node: EventAction, path: string) { private render_event_node(
node: EventAction,
path: string,
graphStart = false
) {
return html` return html`
<hat-graph-node <hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiExclamation} .iconPath=${mdiExclamation}
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
class=${classMap({ class=${classMap({
@ -296,7 +315,11 @@ class HatScriptGraph extends LitElement {
`; `;
} }
private render_repeat_node(node: RepeatAction, path: string) { private render_repeat_node(
node: RepeatAction,
path: string,
graphStart = false
) {
const trace: any = this.trace.trace[path]; const trace: any = this.trace.trace[path];
const track_path = trace ? [0, 1] : []; const track_path = trace ? [0, 1] : [];
const repeats = this.trace?.trace[`${path}/repeat/sequence/0`]?.length; const repeats = this.trace?.trace[`${path}/repeat/sequence/0`]?.length;
@ -313,6 +336,7 @@ class HatScriptGraph extends LitElement {
})} })}
> >
<hat-graph-node <hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiRefresh} .iconPath=${mdiRefresh}
class=${classMap({ class=${classMap({
track: trace, track: trace,
@ -337,9 +361,14 @@ class HatScriptGraph extends LitElement {
`; `;
} }
private render_scene_node(node: SceneAction, path: string) { private render_scene_node(
node: SceneAction,
path: string,
graphStart = false
) {
return html` return html`
<hat-graph-node <hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiExclamation} .iconPath=${mdiExclamation}
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
class=${classMap({ class=${classMap({
@ -351,9 +380,14 @@ class HatScriptGraph extends LitElement {
`; `;
} }
private render_service_node(node: ServiceAction, path: string) { private render_service_node(
node: ServiceAction,
path: string,
graphStart = false
) {
return html` return html`
<hat-graph-node <hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiChevronRight} .iconPath=${mdiChevronRight}
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
class=${classMap({ class=${classMap({
@ -367,10 +401,12 @@ class HatScriptGraph extends LitElement {
private render_wait_node( private render_wait_node(
node: WaitAction | WaitForTriggerAction, node: WaitAction | WaitForTriggerAction,
path: string path: string,
graphStart = false
) { ) {
return html` return html`
<hat-graph-node <hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiTrafficLight} .iconPath=${mdiTrafficLight}
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
class=${classMap({ class=${classMap({
@ -382,9 +418,10 @@ class HatScriptGraph extends LitElement {
`; `;
} }
private render_other_node(node: Action, path: string) { private render_other_node(node: Action, path: string, graphStart = false) {
return html` return html`
<hat-graph-node <hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiCodeBrackets} .iconPath=${mdiCodeBrackets}
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
class=${classMap({ class=${classMap({
@ -395,7 +432,7 @@ class HatScriptGraph extends LitElement {
`; `;
} }
private render_node(node: Action, path: string) { private render_node(node: Action, path: string, graphStart = false) {
const NODE_TYPES = { const NODE_TYPES = {
choose: this.render_choose_node, choose: this.render_choose_node,
condition: this.render_condition_node, condition: this.render_condition_node,
@ -411,7 +448,7 @@ class HatScriptGraph extends LitElement {
}; };
const type = Object.keys(NODE_TYPES).find((key) => key in node) || "other"; const type = Object.keys(NODE_TYPES).find((key) => key in node) || "other";
const nodeEl = NODE_TYPES[type].bind(this)(node, path); const nodeEl = NODE_TYPES[type].bind(this)(node, path, graphStart);
this.renderedNodes[path] = { config: node, path }; this.renderedNodes[path] = { config: node, path };
if (this.trace && path in this.trace.trace) { if (this.trace && path in this.trace.trace) {
this.trackedNodes[path] = this.renderedNodes[path]; this.trackedNodes[path] = this.renderedNodes[path];
@ -423,35 +460,47 @@ class HatScriptGraph extends LitElement {
const paths = Object.keys(this.trackedNodes); const paths = Object.keys(this.trackedNodes);
const manual_triggered = this.trace && "trigger" in this.trace.trace; const manual_triggered = this.trace && "trigger" in this.trace.trace;
let track_path = manual_triggered ? undefined : [0]; let track_path = manual_triggered ? undefined : [0];
const trigger_nodes = ensureArray(this.trace.config.trigger).map( const trigger_nodes =
(trigger, i) => { "trigger" in this.trace.config
if (this.trace && `trigger/${i}` in this.trace.trace) { ? ensureArray(this.trace.config.trigger).map((trigger, i) => {
track_path = [i]; if (this.trace && `trigger/${i}` in this.trace.trace) {
} track_path = [i];
return this.render_trigger(trigger, i); }
} return this.render_trigger(trigger, i);
); })
: undefined;
try { try {
return html` return html`
<hat-graph class="parent"> <hat-graph class="parent">
<div></div> <div></div>
<hat-graph ${trigger_nodes
branching ? html`<hat-graph
id="trigger" branching
.short=${trigger_nodes.length < 2} id="trigger"
.track_start=${track_path} .short=${trigger_nodes.length < 2}
.track_end=${track_path} .track_start=${track_path}
> .track_end=${track_path}
${trigger_nodes} >
</hat-graph> ${trigger_nodes}
<hat-graph id="condition"> </hat-graph>`
${ensureArray(this.trace.config.condition)?.map((condition, i) => : ""}
this.render_condition(condition!, i) ${"condition" in this.trace.config
)} ? html`<hat-graph id="condition">
</hat-graph> ${ensureArray(
${ensureArray(this.trace.config.action).map((action, i) => this.trace.config.condition
this.render_node(action, `action/${i}`) )?.map((condition, i) => this.render_condition(condition!, i))}
)} </hat-graph>`
: ""}
${"action" in this.trace.config
? html`${ensureArray(this.trace.config.action).map((action, i) =>
this.render_node(action, `action/${i}`)
)}`
: ""}
${"sequence" in this.trace.config
? html`${ensureArray(this.trace.config.sequence).map((action, i) =>
this.render_node(action, `sequence/${i}`, i === 0)
)}`
: ""}
</hat-graph> </hat-graph>
<div class="actions"> <div class="actions">
<mwc-icon-button <mwc-icon-button
@ -564,6 +613,7 @@ class HatScriptGraph extends LitElement {
} }
.parent { .parent {
margin-left: 8px; margin-left: 8px;
margin-top: 16px;
} }
.error { .error {
padding: 16px; padding: 16px;

View File

@ -7,6 +7,7 @@ import { computeObjectId } from "../common/entity/compute_object_id";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { Condition, Trigger } from "./automation"; import { Condition, Trigger } from "./automation";
import { BlueprintInput } from "./blueprint";
export const MODES = ["single", "restart", "queued", "parallel"] as const; export const MODES = ["single", "restart", "queued", "parallel"] as const;
export const MODES_MAX = ["queued", "parallel"]; export const MODES_MAX = ["queued", "parallel"];
@ -28,6 +29,10 @@ export interface ScriptConfig {
max?: number; max?: number;
} }
export interface BlueprintScriptConfig extends ScriptConfig {
use_blueprint: { path: string; input?: BlueprintInput };
}
export interface EventAction { export interface EventAction {
alias?: string; alias?: string;
event: string; event: string;

View File

@ -4,6 +4,7 @@ import {
BlueprintAutomationConfig, BlueprintAutomationConfig,
ManualAutomationConfig, ManualAutomationConfig,
} from "./automation"; } from "./automation";
import { BlueprintScriptConfig, ScriptConfig } from "./script";
interface BaseTraceStep { interface BaseTraceStep {
path: string; path: string;
@ -54,7 +55,7 @@ export type ActionTraceStep =
| ChooseActionTraceStep | ChooseActionTraceStep
| ChooseChoiceActionTraceStep; | ChooseChoiceActionTraceStep;
export interface AutomationTrace { interface BaseTrace {
domain: string; domain: string;
item_id: string; item_id: string;
last_step: string | null; last_step: string | null;
@ -81,23 +82,46 @@ export interface AutomationTrace {
// The exception is in the trace itself or in the last element of the trace // The exception is in the trace itself or in the last element of the trace
// Script execution stopped by async_stop called on the script run because home assistant is shutting down, script mode is SCRIPT_MODE_RESTART etc: // Script execution stopped by async_stop called on the script run because home assistant is shutting down, script mode is SCRIPT_MODE_RESTART etc:
| "cancelled"; | "cancelled";
// Automation only, should become it's own type when we support script in frontend }
interface BaseTraceExtended {
trace: Record<string, ActionTraceStep[]>;
context: Context;
error?: string;
}
export interface AutomationTrace extends BaseTrace {
domain: "automation";
trigger: string; trigger: string;
} }
export interface AutomationTraceExtended extends AutomationTrace { export interface AutomationTraceExtended
trace: Record<string, ActionTraceStep[]>; extends AutomationTrace,
context: Context; BaseTraceExtended {
config: ManualAutomationConfig; config: ManualAutomationConfig;
blueprint_inputs?: BlueprintAutomationConfig; blueprint_inputs?: BlueprintAutomationConfig;
error?: string;
} }
export interface ScriptTrace extends BaseTrace {
domain: "script";
}
export interface ScriptTraceExtended extends ScriptTrace, BaseTraceExtended {
config: ScriptConfig;
blueprint_inputs?: BlueprintScriptConfig;
}
export type TraceExtended = AutomationTraceExtended | ScriptTraceExtended;
interface TraceTypes { interface TraceTypes {
automation: { automation: {
short: AutomationTrace; short: AutomationTrace;
extended: AutomationTraceExtended; extended: AutomationTraceExtended;
}; };
script: {
short: ScriptTrace;
extended: ScriptTraceExtended;
};
} }
export const loadTrace = <T extends keyof TraceTypes>( export const loadTrace = <T extends keyof TraceTypes>(
@ -141,7 +165,7 @@ export const loadTraceContexts = (
}); });
export const getDataFromPath = ( export const getDataFromPath = (
config: ManualAutomationConfig, config: TraceExtended["config"],
path: string path: string
): any => { ): any => {
const parts = path.split("/").reverse(); const parts = path.split("/").reverse();

View File

@ -9,31 +9,28 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
import type { NodeInfo } from "../../../../components/trace/hat-graph"; import type { NodeInfo } from "../../../components/trace/hat-graph";
import "../../../../components/trace/hat-script-graph"; import "../../../components/trace/hat-script-graph";
import { AutomationEntity } from "../../../../data/automation"; import { AutomationEntity } from "../../../data/automation";
import { import { getLogbookDataForContext, LogbookEntry } from "../../../data/logbook";
getLogbookDataForContext,
LogbookEntry,
} from "../../../../data/logbook";
import { import {
AutomationTrace, AutomationTrace,
AutomationTraceExtended, AutomationTraceExtended,
loadTrace, loadTrace,
loadTraces, loadTraces,
} from "../../../../data/trace"; } from "../../../data/trace";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../../types"; import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "./ha-automation-trace-blueprint-config"; import "../../../components/trace/ha-trace-blueprint-config";
import "./ha-automation-trace-config"; import "../../../components/trace/ha-trace-config";
import "./ha-automation-trace-logbook"; import "../../../components/trace/ha-trace-logbook";
import "./ha-automation-trace-path-details"; import "../../../components/trace/ha-trace-path-details";
import "./ha-automation-trace-timeline"; import "../../../components/trace/ha-trace-timeline";
import { traceTabStyles } from "./styles"; import { traceTabStyles } from "../../../components/trace/trace-tab-styles";
@customElement("ha-automation-trace") @customElement("ha-automation-trace")
export class HaAutomationTrace extends LitElement { export class HaAutomationTrace extends LitElement {
@ -209,7 +206,7 @@ export class HaAutomationTrace extends LitElement {
@click=${this._showTab} @click=${this._showTab}
> >
Blueprint Config Blueprint Config
</div> </button>
` `
: ""} : ""}
</div> </div>
@ -219,46 +216,47 @@ export class HaAutomationTrace extends LitElement {
? "" ? ""
: this._view === "details" : this._view === "details"
? html` ? html`
<ha-automation-trace-path-details <ha-trace-path-details
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.trace=${this._trace} .trace=${this._trace}
.selected=${this._selected} .selected=${this._selected}
.logbookEntries=${this._logbookEntries} .logbookEntries=${this._logbookEntries}
.trackedNodes=${trackedNodes} .trackedNodes=${trackedNodes}
.renderedNodes=${renderedNodes} .renderedNodes=${renderedNodes!}
></ha-automation-trace-path-details> ></ha-trace-path-details>
` `
: this._view === "config" : this._view === "config"
? html` ? html`
<ha-automation-trace-config <ha-trace-config
.hass=${this.hass} .hass=${this.hass}
.trace=${this._trace} .trace=${this._trace}
></ha-automation-trace-config> ></ha-trace-config>
` `
: this._view === "logbook" : this._view === "logbook"
? html` ? html`
<ha-automation-trace-logbook <ha-trace-logbook
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.trace=${this._trace}
.logbookEntries=${this._logbookEntries} .logbookEntries=${this._logbookEntries}
></ha-automation-trace-logbook> ></ha-trace-logbook>
` `
: this._view === "blueprint" : this._view === "blueprint"
? html` ? html`
<ha-automation-trace-blueprint-config <ha-trace-blueprint-config
.hass=${this.hass} .hass=${this.hass}
.trace=${this._trace} .trace=${this._trace}
></ha-automation-trace-blueprint-config> ></ha-trace-blueprint-config>
` `
: html` : html`
<ha-automation-trace-timeline <ha-trace-timeline
.hass=${this.hass} .hass=${this.hass}
.trace=${this._trace} .trace=${this._trace}
.logbookEntries=${this._logbookEntries} .logbookEntries=${this._logbookEntries}
.selected=${this._selected} .selected=${this._selected}
@value-changed=${this._timelinePathPicked} @value-changed=${this._timelinePathPicked}
></ha-automation-trace-timeline> ></ha-trace-timeline>
`} `}
</div> </div>
</div> </div>

View File

@ -51,7 +51,7 @@ class HaConfigAutomation extends HassRouterPage {
}, },
trace: { trace: {
tag: "ha-automation-trace", tag: "ha-automation-trace",
load: () => import("./trace/ha-automation-trace"), load: () => import("./ha-automation-trace"),
}, },
}, },
}; };

View File

@ -42,6 +42,10 @@ class HaConfigScript extends HassRouterPage {
edit: { edit: {
tag: "ha-script-editor", tag: "ha-script-editor",
}, },
trace: {
tag: "ha-script-trace",
load: () => import("./ha-script-trace"),
},
}, },
}; };
@ -81,7 +85,7 @@ class HaConfigScript extends HassRouterPage {
if ( if (
(!changedProps || changedProps.has("route")) && (!changedProps || changedProps.has("route")) &&
this._currentPage === "edit" this._currentPage !== "dashboard"
) { ) {
pageEl.creatingNew = undefined; pageEl.creatingNew = undefined;
const scriptEntityId = this.routeTail.path.substr(1); const scriptEntityId = this.routeTail.path.substr(1);

View File

@ -297,7 +297,16 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
<div <div
class="card-actions layout horizontal justified center" class="card-actions layout horizontal justified center"
> >
<span></span> <a
href="/config/script/trace/${this
.scriptEntityId}"
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.script.editor.show_trace"
)}
</mwc-button>
</a>
<mwc-button <mwc-button
@click=${this._runScript} @click=${this._runScript}
title="${this.hass.localize( title="${this.hass.localize(

View File

@ -1,6 +1,7 @@
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import { import {
mdiHelpCircle, mdiHelpCircle,
mdiHistory,
mdiInformationOutline, mdiInformationOutline,
mdiPencil, mdiPencil,
mdiPlay, mdiPlay,
@ -140,6 +141,21 @@ class HaScriptPicker extends LitElement {
</mwc-icon-button> </mwc-icon-button>
`, `,
}; };
columns.trace = {
title: "",
type: "icon-button",
template: (_info, script: any) => html`
<a href="/config/script/trace/${script.entity_id}">
<mwc-icon-button
.label=${this.hass.localize(
"ui.panel.config.script.picker.dev_script"
)}
>
<ha-svg-icon .path=${mdiHistory}></ha-svg-icon>
</mwc-icon-button>
</a>
`,
};
columns.edit = { columns.edit = {
title: "", title: "",
type: "icon-button", type: "icon-button",

View File

@ -0,0 +1,502 @@
import {
mdiDownload,
mdiPencil,
mdiRayEndArrow,
mdiRayStartArrow,
mdiRefresh,
} from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
import type { NodeInfo } from "../../../components/trace/hat-graph";
import "../../../components/trace/hat-script-graph";
import { getLogbookDataForContext, LogbookEntry } from "../../../data/logbook";
import { ScriptEntity } from "../../../data/script";
import {
loadTrace,
loadTraces,
ScriptTrace,
ScriptTraceExtended,
} from "../../../data/trace";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { traceTabStyles } from "../../../components/trace/trace-tab-styles";
import { configSections } from "../ha-panel-config";
import "../../../components/trace/ha-trace-blueprint-config";
import "../../../components/trace/ha-trace-config";
import "../../../components/trace/ha-trace-logbook";
import "../../../components/trace/ha-trace-path-details";
import "../../../components/trace/ha-trace-timeline";
@customElement("ha-script-trace")
export class HaScriptTrace extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public scriptEntityId!: string;
@property({ attribute: false }) public scripts!: ScriptEntity[];
@property({ type: Boolean }) public isWide?: boolean;
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@state() private _traces?: ScriptTrace[];
@state() private _runId?: string;
@state() private _selected?: NodeInfo;
@state() private _trace?: ScriptTraceExtended;
@state() private _logbookEntries?: LogbookEntry[];
@state() private _view:
| "details"
| "config"
| "timeline"
| "logbook"
| "blueprint" = "details";
protected render(): TemplateResult {
const stateObj = this.scriptEntityId
? this.hass.states[this.scriptEntityId]
: undefined;
const graph = this.shadowRoot!.querySelector("hat-script-graph");
const trackedNodes = graph?.trackedNodes;
const renderedNodes = graph?.renderedNodes;
const title = stateObj?.attributes.friendly_name || this.scriptEntityId;
let devButtons: TemplateResult | string = "";
if (__DEV__) {
devButtons = html`<div style="position: absolute; right: 0;">
<button @click=${this._importTrace}>Import trace</button>
<button @click=${this._loadLocalStorageTrace}>Load stored trace</button>
</div>`;
}
const actionButtons = html`
<mwc-icon-button label="Refresh" @click=${() => this._loadTraces()}>
<ha-svg-icon .path=${mdiRefresh}></ha-svg-icon>
</mwc-icon-button>
<mwc-icon-button
.disabled=${!this._trace}
label="Download Trace"
@click=${this._downloadTrace}
>
<ha-svg-icon .path=${mdiDownload}></ha-svg-icon>
</mwc-icon-button>
`;
return html`
${devButtons}
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configSections.automation}
>
${this.narrow
? html`<span slot="header"> ${title} </span>
<div slot="toolbar-icon">${actionButtons}</div>`
: ""}
<div class="toolbar">
${!this.narrow
? html`<div>
${title}
<a
class="linkButton"
href="/config/script/edit/${this.scriptEntityId}"
>
<mwc-icon-button label="Edit Script" tabindex="-1">
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
</mwc-icon-button>
</a>
</div>`
: ""}
${this._traces && this._traces.length > 0
? html`
<div>
<mwc-icon-button
.disabled=${this._traces[this._traces.length - 1].run_id ===
this._runId}
label="Older trace"
@click=${this._pickOlderTrace}
>
<ha-svg-icon .path=${mdiRayEndArrow}></ha-svg-icon>
</mwc-icon-button>
<select .value=${this._runId} @change=${this._pickTrace}>
${repeat(
this._traces,
(trace) => trace.run_id,
(trace) =>
html`<option value=${trace.run_id}>
${formatDateTimeWithSeconds(
new Date(trace.timestamp.start),
this.hass.locale
)}
</option>`
)}
</select>
<mwc-icon-button
.disabled=${this._traces[0].run_id === this._runId}
label="Newer trace"
@click=${this._pickNewerTrace}
>
<ha-svg-icon .path=${mdiRayStartArrow}></ha-svg-icon>
</mwc-icon-button>
</div>
`
: ""}
${!this.narrow ? html`<div>${actionButtons}</div>` : ""}
</div>
${this._traces === undefined
? html`<div class="container">Loading…</div>`
: this._traces.length === 0
? html`<div class="container">No traces found</div>`
: this._trace === undefined
? ""
: html`
<div class="main">
<div class="graph">
<hat-script-graph
.trace=${this._trace}
.selected=${this._selected?.path}
@graph-node-selected=${this._pickNode}
></hat-script-graph>
</div>
<div class="info">
<div class="tabs top">
${[
["details", "Step Details"],
["timeline", "Trace Timeline"],
["logbook", "Related logbook entries"],
["config", "Script Config"],
].map(
([view, label]) => html`
<button
tabindex="0"
.view=${view}
class=${classMap({ active: this._view === view })}
@click=${this._showTab}
>
${label}
</button>
`
)}
${this._trace.blueprint_inputs
? html`
<button
tabindex="0"
.view=${"blueprint"}
class=${classMap({
active: this._view === "blueprint",
})}
@click=${this._showTab}
>
Blueprint Config
</button>
`
: ""}
</div>
${this._selected === undefined ||
this._logbookEntries === undefined ||
trackedNodes === undefined
? ""
: this._view === "details"
? html`
<ha-trace-path-details
.hass=${this.hass}
.narrow=${this.narrow}
.trace=${this._trace}
.selected=${this._selected}
.logbookEntries=${this._logbookEntries}
.trackedNodes=${trackedNodes}
.renderedNodes=${renderedNodes!}
></ha-trace-path-details>
`
: this._view === "config"
? html`
<ha-trace-config
.hass=${this.hass}
.trace=${this._trace}
></ha-trace-config>
`
: this._view === "logbook"
? html`
<ha-trace-logbook
.hass=${this.hass}
.narrow=${this.narrow}
.trace=${this._trace}
.logbookEntries=${this._logbookEntries}
></ha-trace-logbook>
`
: this._view === "blueprint"
? html`
<ha-trace-blueprint-config
.hass=${this.hass}
.trace=${this._trace}
></ha-trace-blueprint-config>
`
: html`
<ha-trace-timeline
.hass=${this.hass}
.trace=${this._trace}
.logbookEntries=${this._logbookEntries}
.selected=${this._selected}
@value-changed=${this._timelinePathPicked}
></ha-trace-timeline>
`}
</div>
</div>
`}
</hass-tabs-subpage>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (!this.scriptEntityId) {
return;
}
const params = new URLSearchParams(location.search);
this._loadTraces(params.get("run_id") || undefined);
}
protected updated(changedProps) {
super.updated(changedProps);
// Only reset if automationId has changed and we had one before.
if (changedProps.get("scriptEntityId")) {
this._traces = undefined;
this._runId = undefined;
this._trace = undefined;
this._logbookEntries = undefined;
if (this.scriptEntityId) {
this._loadTraces();
}
}
if (changedProps.has("_runId") && this._runId) {
this._trace = undefined;
this._logbookEntries = undefined;
this.shadowRoot!.querySelector("select")!.value = this._runId;
this._loadTrace();
}
}
private _pickOlderTrace() {
const curIndex = this._traces!.findIndex((tr) => tr.run_id === this._runId);
this._runId = this._traces![curIndex + 1].run_id;
this._selected = undefined;
}
private _pickNewerTrace() {
const curIndex = this._traces!.findIndex((tr) => tr.run_id === this._runId);
this._runId = this._traces![curIndex - 1].run_id;
this._selected = undefined;
}
private _pickTrace(ev) {
this._runId = ev.target.value;
this._selected = undefined;
}
private _pickNode(ev) {
this._selected = ev.detail;
}
private async _loadTraces(runId?: string) {
this._traces = await loadTraces(
this.hass,
"script",
this.scriptEntityId.split(".")[1]
);
// Newest will be on top.
this._traces.reverse();
if (runId) {
this._runId = runId;
}
// Check if current run ID still exists
if (
this._runId &&
!this._traces.some((trace) => trace.run_id === this._runId)
) {
this._runId = undefined;
this._selected = undefined;
// If we came here from a trace passed into the url, clear it.
if (runId) {
const params = new URLSearchParams(location.search);
params.delete("run_id");
history.replaceState(
null,
"",
`${location.pathname}?${params.toString()}`
);
}
await showAlertDialog(this, {
text: "Chosen trace is no longer available",
});
}
// See if we can set a default runID
if (!this._runId && this._traces.length > 0) {
this._runId = this._traces[0].run_id;
}
}
private async _loadTrace() {
const trace = await loadTrace(
this.hass,
"script",
this.scriptEntityId.split(".")[1],
this._runId!
);
this._logbookEntries = isComponentLoaded(this.hass, "logbook")
? await getLogbookDataForContext(
this.hass,
trace.timestamp.start,
trace.context.id
)
: [];
this._trace = trace;
}
private _downloadTrace() {
const aEl = document.createElement("a");
aEl.download = `trace ${this.scriptEntityId} ${
this._trace!.timestamp.start
}.json`;
aEl.href = `data:application/json;charset=utf-8,${encodeURI(
JSON.stringify(
{
trace: this._trace,
logbookEntries: this._logbookEntries,
},
undefined,
2
)
)}`;
aEl.click();
}
private _importTrace() {
const traceText = prompt("Enter downloaded trace");
if (!traceText) {
return;
}
localStorage.devTrace = traceText;
this._loadLocalTrace(traceText);
}
private _loadLocalStorageTrace() {
if (localStorage.devTrace) {
this._loadLocalTrace(localStorage.devTrace);
}
}
private _loadLocalTrace(traceText: string) {
const traceInfo = JSON.parse(traceText);
this._trace = traceInfo.trace;
this._logbookEntries = traceInfo.logbookEntries;
}
private _showTab(ev) {
this._view = (ev.target as any).view;
}
private _timelinePathPicked(ev) {
const path = ev.detail.value;
const nodes = this.shadowRoot!.querySelector("hat-script-graph")!
.trackedNodes;
if (nodes[path]) {
this._selected = nodes[path];
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
traceTabStyles,
css`
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 20px;
height: var(--header-height);
padding: 0 16px;
background-color: var(--primary-background-color);
font-weight: 400;
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
}
.toolbar > * {
display: flex;
align-items: center;
}
:host([narrow]) .toolbar > * {
display: contents;
}
.main {
height: calc(100% - 56px);
display: flex;
background-color: var(--card-background-color);
}
:host([narrow]) .main {
height: auto;
flex-direction: column;
}
.container {
padding: 16px;
}
.graph {
border-right: 1px solid var(--divider-color);
overflow-x: auto;
max-width: 50%;
}
:host([narrow]) .graph {
max-width: 100%;
}
.info {
flex: 1;
background-color: var(--card-background-color);
}
.linkButton {
color: var(--primary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-script-trace": HaScriptTrace;
}
}

View File

@ -303,7 +303,7 @@
"entries_not_found": "No logbook entries found.", "entries_not_found": "No logbook entries found.",
"by": "by", "by": "by",
"by_service": "by service", "by_service": "by service",
"show_trace": "Show trace", "show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
"retrieval_error": "Error during logbook entry retrieval", "retrieval_error": "Error during logbook entry retrieval",
"messages": { "messages": {
"was_away": "was detected away", "was_away": "was detected away",
@ -1640,6 +1640,7 @@
"show_info": "Show info about script", "show_info": "Show info about script",
"run_script": "Run script", "run_script": "Run script",
"edit_script": "Edit script", "edit_script": "Edit script",
"dev_script": "Debug script",
"headers": { "headers": {
"name": "Name" "name": "Name"
}, },
@ -1653,6 +1654,7 @@
"id_already_exists_save_error": "You can't save this script because the ID is not unique, pick another ID or leave it blank to automatically generate one.", "id_already_exists_save_error": "You can't save this script because the ID is not unique, pick another ID or leave it blank to automatically generate one.",
"id_already_exists": "This ID already exists", "id_already_exists": "This ID already exists",
"introduction": "Use scripts to run a sequence of actions.", "introduction": "Use scripts to run a sequence of actions.",
"show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
"header": "Script: {name}", "header": "Script: {name}",
"default_name": "New Script", "default_name": "New Script",
"modes": { "modes": {