diff --git a/gallery/src/data/traces/basic_trace.ts b/gallery/src/data/traces/basic_trace.ts index 24afe1fa07..1df0446a1e 100644 --- a/gallery/src/data/traces/basic_trace.ts +++ b/gallery/src/data/traces/basic_trace.ts @@ -2,8 +2,7 @@ import { DemoTrace } from "./types"; export const basicTrace: DemoTrace = { trace: { - last_action: "action/2", - last_condition: "condition/0", + last_step: "action/2", run_id: "0", state: "stopped", timestamp: { @@ -14,6 +13,12 @@ export const basicTrace: DemoTrace = { domain: "automation", item_id: "1615419646544", trace: { + "trigger/0": [ + { + path: "trigger/0", + timestamp: "2021-03-25T04:36:51.223693+00:00", + }, + ], "condition/0": [ { path: "condition/0", @@ -284,45 +289,7 @@ export const basicTrace: DemoTrace = { parent_id: "664d6d261450a9ecea6738e97269a149", user_id: null, }, - variables: { - trigger: { - platform: "state", - entity_id: "input_boolean.toggle_1", - from_state: { - entity_id: "input_boolean.toggle_1", - state: "on", - attributes: { - editable: true, - friendly_name: "Toggle 1", - }, - last_changed: "2021-03-24T19:03:59.141440+00:00", - last_updated: "2021-03-24T19:03:59.141440+00:00", - context: { - id: "5d0918eb379214d07554bdab6a08bcff", - parent_id: null, - user_id: null, - }, - }, - to_state: { - entity_id: "input_boolean.toggle_1", - state: "off", - attributes: { - editable: true, - friendly_name: "Toggle 1", - }, - last_changed: "2021-03-25T04:36:51.220696+00:00", - last_updated: "2021-03-25T04:36:51.220696+00:00", - context: { - id: "664d6d261450a9ecea6738e97269a149", - parent_id: null, - user_id: "d1b4e89da01445fa8bc98e39fac477ca", - }, - }, - for: null, - attribute: null, - description: "state of input_boolean.toggle_1", - }, - }, + script_execution: "finished", }, logbookEntries: [ { diff --git a/gallery/src/data/traces/mock-demo-trace.ts b/gallery/src/data/traces/mock-demo-trace.ts new file mode 100644 index 0000000000..04679bb007 --- /dev/null +++ b/gallery/src/data/traces/mock-demo-trace.ts @@ -0,0 +1,44 @@ +import { LogbookEntry } from "../../../../src/data/logbook"; +import { AutomationTraceExtended } from "../../../../src/data/trace"; +import { DemoTrace } from "./types"; + +export const mockDemoTrace = ( + tracePartial: Partial, + logbookEntries?: LogbookEntry[] +): DemoTrace => ({ + trace: { + last_step: "", + run_id: "0", + state: "stopped", + timestamp: { + start: "2021-03-25T04:36:51.223693+00:00", + finish: "2021-03-25T04:36:51.266132+00:00", + }, + trigger: "mocked trigger", + domain: "automation", + item_id: "1615419646544", + trace: { + "trigger/0": [ + { + path: "trigger/0", + changed_variables: { + trigger: { + description: "mocked trigger", + }, + }, + timestamp: "2021-03-25T04:36:51.223693+00:00", + }, + ], + }, + config: { + trigger: [], + action: [], + }, + context: { + id: "abcd", + }, + script_execution: "finished", + ...tracePartial, + }, + logbookEntries: logbookEntries || [], +}); diff --git a/gallery/src/data/traces/motion-light-trace.ts b/gallery/src/data/traces/motion-light-trace.ts index 9992989124..b735e28b03 100644 --- a/gallery/src/data/traces/motion-light-trace.ts +++ b/gallery/src/data/traces/motion-light-trace.ts @@ -2,8 +2,7 @@ import { DemoTrace } from "./types"; export const motionLightTrace: DemoTrace = { trace: { - last_action: "action/3", - last_condition: null, + last_step: "action/3", run_id: "1", state: "stopped", timestamp: { @@ -14,6 +13,12 @@ export const motionLightTrace: DemoTrace = { domain: "automation", item_id: "1614732497392", trace: { + "trigger/0": [ + { + path: "trigger/0", + timestamp: "2021-03-25T04:36:51.223693+00:00", + }, + ], "action/0": [ { path: "action/0", @@ -171,45 +176,7 @@ export const motionLightTrace: DemoTrace = { parent_id: "e22ddfd5f11dc4aad9a52fc10dab613b", user_id: null, }, - variables: { - trigger: { - platform: "state", - entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use", - from_state: { - entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use", - state: "off", - attributes: { - friendly_name: "Paulus’s MacBook Pro Camera In Use", - icon: "mdi:camera-off", - }, - last_changed: "2021-03-14T06:06:29.235325+00:00", - last_updated: "2021-03-14T06:06:29.235325+00:00", - context: { - id: "ad4864c5ce957c38a07b50378eeb245d", - parent_id: null, - user_id: null, - }, - }, - to_state: { - entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use", - state: "on", - attributes: { - friendly_name: "Paulus’s MacBook Pro Camera In Use", - icon: "mdi:camera", - }, - last_changed: "2021-03-14T06:07:01.762009+00:00", - last_updated: "2021-03-14T06:07:01.762009+00:00", - context: { - id: "e22ddfd5f11dc4aad9a52fc10dab613b", - parent_id: null, - user_id: null, - }, - }, - for: null, - attribute: null, - description: "state of binary_sensor.pauluss_macbook_pro_camera_in_use", - }, - }, + script_execution: "finished", }, logbookEntries: [ { diff --git a/gallery/src/demos/demo-automation-trace-timeline.ts b/gallery/src/demos/demo-automation-trace-timeline.ts new file mode 100644 index 0000000000..2a8ecce1fa --- /dev/null +++ b/gallery/src/demos/demo-automation-trace-timeline.ts @@ -0,0 +1,87 @@ +import { + customElement, + html, + css, + LitElement, + TemplateResult, + 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"; +import { mockDemoTrace } from "../data/traces/mock-demo-trace"; +import { DemoTrace } from "../data/traces/types"; + +const traces: DemoTrace[] = [ + mockDemoTrace({ state: "running" }), + mockDemoTrace({ state: "debugged" }), + mockDemoTrace({ state: "stopped", script_execution: "failed_condition" }), + mockDemoTrace({ state: "stopped", script_execution: "failed_single" }), + mockDemoTrace({ state: "stopped", script_execution: "failed_max_runs" }), + mockDemoTrace({ state: "stopped", script_execution: "finished" }), + mockDemoTrace({ state: "stopped", script_execution: "aborted" }), + mockDemoTrace({ + state: "stopped", + script_execution: "error", + error: 'Variable "beer" cannot be None', + }), + mockDemoTrace({ state: "stopped", script_execution: "cancelled" }), +]; + +@customElement("demo-automation-trace-timeline") +export class DemoAutomationTraceTimeline extends LitElement { + @property({ attribute: false }) hass?: HomeAssistant; + + protected render(): TemplateResult { + if (!this.hass) { + return html``; + } + return html` + ${traces.map( + (trace) => html` + +
+ + +
+
+ ` + )} + `; + } + + 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; + } + .card-content { + display: flex; + } + button { + position: absolute; + top: 0; + right: 0; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-automation-trace-timeline": DemoAutomationTraceTimeline; + } +} diff --git a/src/components/trace/hat-trace-timeline.ts b/src/components/trace/hat-trace-timeline.ts index ac1b957004..f453fa8f36 100644 --- a/src/components/trace/hat-trace-timeline.ts +++ b/src/components/trace/hat-trace-timeline.ts @@ -20,9 +20,11 @@ import { HomeAssistant } from "../../types"; import "./ha-timeline"; import type { HaTimeline } from "./ha-timeline"; import { + mdiAlertCircle, mdiCircle, mdiCircleOutline, - mdiPauseCircleOutline, + mdiProgressClock, + mdiProgressWrench, mdiRecordCircleOutline, } from "@mdi/js"; import { LogbookEntry } from "../../data/logbook"; @@ -34,6 +36,7 @@ import { import relativeTime from "../../common/datetime/relative_time"; import { fireEvent } from "../../common/dom/fire_event"; import { describeAction } from "../../data/script_i18n"; +import { ifDefined } from "lit-html/directives/if-defined"; const LOGBOOK_ENTRIES_BEFORE_FOLD = 2; @@ -273,7 +276,7 @@ class ActionRenderer { `Triggered ${ triggerStep.path === "trigger" ? "manually" - : `by the ${triggerStep.changed_variables.trigger.description}` + : `by the ${this.trace.trigger}` } at ${formatDateTimeWithSeconds( new Date(triggerStep.timestamp), @@ -421,29 +424,92 @@ export class HaAutomationTracer extends LitElement { logbookRenderer.flush(); + // Render footer + const renderFinishedAt = () => + formatDateTimeWithSeconds( + new Date(this.trace!.timestamp.finish!), + this.hass.locale + ); + const renderRuntime = () => `(runtime: + ${( + (new Date(this.trace!.timestamp.finish!).getTime() - + new Date(this.trace!.timestamp.start).getTime()) / + 1000 + ).toFixed(2)} + seconds)`; + + let entry: { + description: TemplateResult | string; + icon: string; + className?: string; + }; + + if (this.trace.state === "running") { + entry = { + description: "Still running", + icon: mdiProgressClock, + }; + } else if (this.trace.state === "debugged") { + entry = { + description: "Debugged", + icon: mdiProgressWrench, + }; + } else if (this.trace.script_execution === "finished") { + entry = { + description: `Finished at ${renderFinishedAt()} ${renderRuntime()}`, + icon: mdiCircle, + }; + } else if (this.trace.script_execution === "aborted") { + entry = { + description: `Aborted at ${renderFinishedAt()} ${renderRuntime()}`, + icon: mdiAlertCircle, + }; + } else if (this.trace.script_execution === "cancelled") { + entry = { + description: `Cancelled at ${renderFinishedAt()} ${renderRuntime()}`, + icon: mdiAlertCircle, + }; + } else { + let reason: string; + let isError = false; + let extra: TemplateResult | undefined; + + switch (this.trace.script_execution) { + case "failed_condition": + reason = "a condition failed"; + break; + case "failed_single": + reason = "only a single execution is allowed"; + break; + case "failed_max_runs": + reason = "maximum number of parallel runs reached"; + break; + case "error": + reason = "an error was encountered"; + isError = true; + extra = html`

${this.trace.error!}`; + break; + default: + reason = `of unknown reason "${this.trace.script_execution}"`; + isError = true; + } + + entry = { + description: html`Stopped because ${reason} at ${renderFinishedAt()} + ${renderRuntime()}${extra || ""}`, + icon: mdiAlertCircle, + className: isError ? "error" : undefined, + }; + } // null means it was stopped by a condition - if (this.trace.last_action !== null) { + if (entry) { entries.push(html` - ${this.trace.timestamp.finish - ? html`Finished at - ${formatDateTimeWithSeconds( - new Date(this.trace.timestamp.finish), - this.hass.locale - )} - (runtime: - ${( - (new Date(this.trace.timestamp.finish!).getTime() - - new Date(this.trace.timestamp.start).getTime()) / - 1000 - ).toFixed(2)} - seconds)` - : "Still running"} + ${entry.description} `); } @@ -506,6 +572,10 @@ export class HaAutomationTracer extends LitElement { ha-timeline[data-path] { cursor: pointer; } + .error { + --timeline-ball-color: var(--error-color); + color: var(--error-color); + } `, ]; } diff --git a/src/data/trace.ts b/src/data/trace.ts index 244ca952c6..fd38797ac8 100644 --- a/src/data/trace.ts +++ b/src/data/trace.ts @@ -54,22 +54,40 @@ export type ActionTraceStep = export interface AutomationTrace { domain: string; item_id: string; - last_action: string | null; - last_condition: string | null; + last_step: string | null; run_id: string; state: "running" | "stopped" | "debugged"; timestamp: { start: string; finish: string | null; }; - trigger: unknown; + script_execution: + | // The script was not executed because the automation's condition failed + "failed_condition" + // The script was not executed because the run mode is single + | "failed_single" + // The script was not executed because max parallel runs would be exceeded + | "failed_max_runs" + // All script steps finished: + | "finished" + // Script execution stopped by the script itself because a condition fails, wait_for_trigger timeouts etc: + | "aborted" + // Details about failing condition, timeout etc. is in the last element of the trace + // Script execution stops because of an unexpected exception: + | "error" + // 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: + | "cancelled" + | string; + // Automation only, should become it's own type when we support script in frontend + trigger: string; } export interface AutomationTraceExtended extends AutomationTrace { trace: Record; context: Context; - variables: Record; config: AutomationConfig; + error?: string; } interface TraceTypes { diff --git a/yarn.lock b/yarn.lock index 85e91b6876..da1bba399b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1926,7 +1926,7 @@ "@codemirror/state" "^0.18.0" "@codemirror/view" "^0.18.0" -"@codemirror/highlight@^0.18.0": +"@codemirror/highlight@^0.18.0", "@codemirror/highlight@^0.18.1": version "0.18.3" resolved "https://registry.yarnpkg.com/@codemirror/highlight/-/highlight-0.18.3.tgz#50e268630f113c322a2dc97c9f68d71934fffcb0" integrity sha512-NmRmkmWl8ht6Y6Y39ghov84AMPCqhUPIH9fmILs2NaWxZFZf4jGCTzrULnmREGsTie+26+LbKUncIU+PBu1Qng==