diff --git a/gallery/src/data/traces/basic_trace.ts b/gallery/src/data/traces/basic_trace.ts index 6bf8d93f0f..24afe1fa07 100644 --- a/gallery/src/data/traces/basic_trace.ts +++ b/gallery/src/data/traces/basic_trace.ts @@ -13,7 +13,55 @@ export const basicTrace: DemoTrace = { trigger: "state of input_boolean.toggle_1", domain: "automation", item_id: "1615419646544", - action_trace: { + trace: { + "condition/0": [ + { + path: "condition/0", + timestamp: "2021-03-25T04:36:51.228243+00:00", + changed_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", + }, + }, + result: { + result: true, + }, + }, + ], "action/0": [ { path: "action/0", @@ -158,56 +206,7 @@ export const basicTrace: DemoTrace = { }, ], }, - condition_trace: { - "condition/0": [ - { - path: "condition/0", - timestamp: "2021-03-25T04:36:51.228243+00:00", - changed_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", - }, - }, - result: { - result: true, - }, - }, - ], - }, + config: { id: "1615419646544", alias: "Ensure Party mode", diff --git a/gallery/src/data/traces/motion-light-trace.ts b/gallery/src/data/traces/motion-light-trace.ts index 0c6e1cfdc9..9992989124 100644 --- a/gallery/src/data/traces/motion-light-trace.ts +++ b/gallery/src/data/traces/motion-light-trace.ts @@ -13,7 +13,7 @@ export const motionLightTrace: DemoTrace = { trigger: "state of binary_sensor.pauluss_macbook_pro_camera_in_use", domain: "automation", item_id: "1614732497392", - action_trace: { + trace: { "action/0": [ { path: "action/0", @@ -124,7 +124,6 @@ export const motionLightTrace: DemoTrace = { }, ], }, - condition_trace: {}, config: { mode: "restart", max_exceeded: "silent", diff --git a/src/components/trace/hat-trace-timeline.ts b/src/components/trace/hat-trace-timeline.ts index fccb2e6023..1aef30f3ae 100644 --- a/src/components/trace/hat-trace-timeline.ts +++ b/src/components/trace/hat-trace-timeline.ts @@ -11,19 +11,18 @@ import { import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { AutomationTraceExtended, - ChooseActionTrace, + ChooseActionTraceStep, getDataFromPath, + TriggerTraceStep, } from "../../data/trace"; import { HomeAssistant } from "../../types"; import "./ha-timeline"; import type { HaTimeline } from "./ha-timeline"; import { - mdiCheckCircleOutline, mdiCircle, mdiCircleOutline, mdiPauseCircleOutline, mdiRecordCircleOutline, - mdiStopCircleOutline, } from "@mdi/js"; import { LogbookEntry } from "../../data/logbook"; import { @@ -36,8 +35,6 @@ import { fireEvent } from "../../common/dom/fire_event"; const LOGBOOK_ENTRIES_BEFORE_FOLD = 2; -const pathToName = (path: string) => path.split("/").join(" "); - /* eslint max-classes-per-file: "off" */ // Report time entry when more than this time has passed @@ -190,12 +187,13 @@ class ActionRenderer { private keys: string[]; constructor( + private hass: HomeAssistant, private entries: TemplateResult[], private trace: AutomationTraceExtended, private logbookRenderer: LogbookRenderer, private timeTracker: RenderedTimeTracker ) { - this.keys = Object.keys(trace.action_trace); + this.keys = Object.keys(trace.trace); } get curItem() { @@ -211,7 +209,7 @@ class ActionRenderer { } private _getItem(index: number) { - return this.trace.action_trace[this.keys[index]]; + return this.trace.trace[this.keys[index]]; } private _renderItem( @@ -219,6 +217,11 @@ class ActionRenderer { actionType?: ReturnType ): number { const value = this._getItem(index); + + if (value[0].path === "trigger") { + return this._handleTrigger(index, value[0] as TriggerTraceStep); + } + const timestamp = new Date(value[0].timestamp); // Render all logbook items that are in front of this item. @@ -262,6 +265,20 @@ class ActionRenderer { return index + 1; } + private _handleTrigger(index: number, triggerStep: TriggerTraceStep): number { + this._renderEntry( + "trigger", + `Triggered by the + ${triggerStep.changed_variables.trigger.description} at + ${formatDateTimeWithSeconds( + new Date(triggerStep.timestamp), + this.hass.locale + )}`, + mdiCircle + ); + return index + 1; + } + private _handleChoose(index: number): number { // startLevel: choose root config @@ -280,7 +297,7 @@ class ActionRenderer { const choosePath = this.keys[index]; const startLevel = choosePath.split("/").length - 1; - const chooseTrace = this._getItem(index)[0] as ChooseActionTrace; + const chooseTrace = this._getItem(index)[0] as ChooseActionTraceStep; const defaultExecuted = chooseTrace.result.choice === "default"; const chooseConfig = this._getDataFromPath( this.keys[index] @@ -333,9 +350,13 @@ class ActionRenderer { return i; } - private _renderEntry(path: string, description: string) { + private _renderEntry( + path: string, + description: string, + icon = mdiRecordCircleOutline + ) { this.entries.push(html` - + ${description} `); @@ -362,66 +383,33 @@ export class HaAutomationTracer extends LitElement { if (!this.trace) { return html``; } - const entries = [ - html` - - Triggered by the ${this.trace.variables.trigger.description} at - ${formatDateTimeWithSeconds( - new Date(this.trace.timestamp.start), - this.hass.locale - )} - - `, - ]; - if (this.trace.condition_trace) { - for (const [path, value] of Object.entries(this.trace.condition_trace)) { - entries.push(html` - - ${getDataFromPath(this.trace!.config, path).alias || - pathToName(path)} - ${value[0].result.result ? "passed" : "failed"} - - `); - } + const entries: TemplateResult[] = []; + + const timeTracker = new RenderedTimeTracker(this.hass, entries, this.trace); + const logbookRenderer = new LogbookRenderer( + entries, + timeTracker, + this.logbookEntries || [] + ); + const actionRenderer = new ActionRenderer( + this.hass, + entries, + this.trace, + logbookRenderer, + timeTracker + ); + + while (actionRenderer.hasNext) { + actionRenderer.renderItem(); } - if (this.trace.action_trace && this.logbookEntries) { - const timeTracker = new RenderedTimeTracker( - this.hass, - entries, - this.trace - ); - const logbookRenderer = new LogbookRenderer( - entries, - timeTracker, - this.logbookEntries - ); - const actionRenderer = new ActionRenderer( - entries, - this.trace, - logbookRenderer, - timeTracker - ); - - while (actionRenderer.hasNext) { - actionRenderer.renderItem(); - } - - while (logbookRenderer.hasNext) { - logbookRenderer.maybeRenderItem(); - } - - logbookRenderer.flush(); + while (logbookRenderer.hasNext) { + logbookRenderer.maybeRenderItem(); } + logbookRenderer.flush(); + // null means it was stopped by a condition if (this.trace.last_action !== null) { entries.push(html` @@ -456,7 +444,13 @@ export class HaAutomationTracer extends LitElement { super.updated(props); // Pick first path when we load a new trace. - if (this.allowPick && props.has("trace")) { + if ( + this.allowPick && + props.has("trace") && + this.trace && + this.selectedPath && + !(this.selectedPath in this.trace.trace) + ) { const element = this.shadowRoot!.querySelector( "ha-timeline[data-path]" ); diff --git a/src/components/trace/script-to-graph.ts b/src/components/trace/script-to-graph.ts index 2798cdee9c..318c78d861 100644 --- a/src/components/trace/script-to-graph.ts +++ b/src/components/trace/script-to-graph.ts @@ -14,16 +14,16 @@ import { mdiCheckboxBlankOutline, mdiAsterisk, mdiDevices, + mdiFlare, } from "@mdi/js"; import memoizeOne from "memoize-one"; import { Condition } from "../../data/automation"; import { Action, ChooseAction, RepeatAction } from "../../data/script"; import { - ActionTrace, AutomationTraceExtended, - ChooseActionTrace, - ChooseChoiceActionTrace, - ConditionTrace, + ChooseActionTraceStep, + ChooseChoiceActionTraceStep, + ConditionTraceStep, } from "../../data/trace"; import { NodeInfo, TreeNode } from "./hat-graph"; @@ -106,7 +106,7 @@ export class ActionHandler { } _createGraph = memoizeOne((_actions, _selected, _trace) => - this._renderConditions().concat( + this._renderTraceHead().concat( this.actions.map((action, idx) => this._createTreeNode( idx, @@ -118,23 +118,44 @@ export class ActionHandler { ) ); - _renderConditions(): TreeNode[] { - // action/ = default pathPrefix for trace-based actions - if ( - this.pathPrefix !== TRACE_ACTION_PREFIX || - !this.trace?.config.condition - ) { + _renderTraceHead(): TreeNode[] { + if (this.pathPrefix !== TRACE_ACTION_PREFIX) { return []; } - return this.trace.config.condition.map((condition, idx) => - this._createConditionNode( - "condition/", - this.trace?.condition_trace, - idx, - condition, - !this.actions.length && this.trace!.config.condition!.length === idx + 1 - ) - ); + + const triggerNodeInfo = { + path: "trigger", + // Just all triggers for now. + config: this.trace!.config.trigger, + }; + + const nodes: TreeNode[] = [ + { + icon: mdiFlare, + nodeInfo: triggerNodeInfo, + clickCallback: () => { + this._selectNode(triggerNodeInfo); + }, + isActive: this.selected === "trigger", + isTracked: true, + }, + ]; + + if (this.trace!.config.condition) { + this.trace!.config.condition.forEach((condition, idx) => + nodes.push( + this._createConditionNode( + "condition/", + idx, + condition, + !this.actions.length && + this.trace!.config.condition!.length === idx + 1 + ) + ) + ); + } + + return nodes; } _updateAction(idx: number, action) { @@ -192,7 +213,7 @@ export class ActionHandler { this._selectNode(nodeInfo); }, isActive: path === this.selected, - isTracked: this.trace && path in this.trace.action_trace, + isTracked: this.trace && path in this.trace.trace, end, }; } @@ -212,13 +233,7 @@ export class ActionHandler { (idx: number, action: any, end: boolean) => TreeNode > = { condition: (idx, action: Condition, end: boolean): TreeNode => - this._createConditionNode( - this.pathPrefix, - this.trace?.action_trace, - idx, - action, - end - ), + this._createConditionNode(this.pathPrefix, idx, action, end), repeat: (idx, action: RepeatAction, end: boolean): TreeNode => { let seq: Array = action.repeat.sequence; @@ -227,11 +242,10 @@ export class ActionHandler { } const path = `${this.pathPrefix}${idx}`; - const isTracked = this.trace && path in this.trace.action_trace; + const isTracked = this.trace && path in this.trace.trace; const repeats = - this.trace && - this.trace.action_trace[`${path}/repeat/sequence/0`]?.length; + this.trace && this.trace.trace[`${path}/repeat/sequence/0`]?.length; const nodeInfo: NodeInfo = { path, @@ -268,10 +282,10 @@ export class ActionHandler { const choosePath = `${this.pathPrefix}${idx}`; let choice: number | "default" | undefined; - if (this.trace?.action_trace && choosePath in this.trace.action_trace) { - const chooseResult = this.trace.action_trace[ + if (this.trace?.trace && choosePath in this.trace.trace) { + const chooseResult = this.trace.trace[ choosePath - ] as ChooseActionTrace[]; + ] as ChooseActionTraceStep[]; choice = chooseResult[0].result.choice; } @@ -280,10 +294,10 @@ export class ActionHandler { // If we have a trace, highlight the chosen track here. const choicePath = `${this.pathPrefix}${idx}/choose/${choiceIdx}`; let chosen = false; - if (this.trace && choicePath in this.trace.action_trace) { - const choiceResult = this.trace.action_trace[ + if (this.trace && choicePath in this.trace.trace) { + const choiceResult = this.trace.trace[ choicePath - ] as ChooseChoiceActionTrace[]; + ] as ChooseChoiceActionTraceStep[]; chosen = choiceResult[0].result.result; } const choiceNodeInfo: NodeInfo = { @@ -380,7 +394,6 @@ export class ActionHandler { private _createConditionNode( pathPrefix: string, - tracePaths: Record | undefined, idx: number, action: Condition, end: boolean @@ -389,8 +402,8 @@ export class ActionHandler { let result: boolean | undefined; let isTracked = false; - if (tracePaths && path in tracePaths) { - const conditionResult = tracePaths[path] as ConditionTrace[]; + if (this.trace && path in this.trace.trace) { + const conditionResult = this.trace.trace[path] as ConditionTraceStep[]; result = conditionResult[0].result.result; isTracked = true; } diff --git a/src/data/trace.ts b/src/data/trace.ts index 734f9ea92e..8439f332b1 100644 --- a/src/data/trace.ts +++ b/src/data/trace.ts @@ -1,24 +1,27 @@ import { HomeAssistant, Context } from "../types"; import { AutomationConfig } from "./automation"; -interface TraceVariables extends Record { - trigger: { - description: string; - [key: string]: unknown; - }; -} - -interface BaseTrace { +interface BaseTraceStep { path: string; timestamp: string; changed_variables?: Record; } -export interface ConditionTrace extends BaseTrace { +export interface TriggerTraceStep extends BaseTraceStep { + changed_variables: { + trigger: { + description: string; + [key: string]: unknown; + }; + [key: string]: unknown; + }; +} + +export interface ConditionTraceStep extends BaseTraceStep { result: { result: boolean }; } -export interface CallServiceActionTrace extends BaseTrace { +export interface CallServiceActionTraceStep extends BaseTraceStep { result: { limit: number; running_script: boolean; @@ -31,19 +34,20 @@ export interface CallServiceActionTrace extends BaseTrace { }; } -export interface ChooseActionTrace extends BaseTrace { +export interface ChooseActionTraceStep extends BaseTraceStep { result: { choice: number | "default" }; } -export interface ChooseChoiceActionTrace extends BaseTrace { +export interface ChooseChoiceActionTraceStep extends BaseTraceStep { result: { result: boolean }; } -export type ActionTrace = - | BaseTrace - | CallServiceActionTrace - | ChooseActionTrace - | ChooseChoiceActionTrace; +export type ActionTraceStep = + | BaseTraceStep + | ConditionTraceStep + | CallServiceActionTraceStep + | ChooseActionTraceStep + | ChooseChoiceActionTraceStep; export interface AutomationTrace { domain: string; @@ -60,10 +64,9 @@ export interface AutomationTrace { } export interface AutomationTraceExtended extends AutomationTrace { - condition_trace: Record; - action_trace: Record; + trace: Record; context: Context; - variables: TraceVariables; + variables: Record; config: AutomationConfig; } diff --git a/src/panels/config/automation/trace/ha-automation-trace-path-details.ts b/src/panels/config/automation/trace/ha-automation-trace-path-details.ts index 23ad06a379..68c42eed1b 100644 --- a/src/panels/config/automation/trace/ha-automation-trace-path-details.ts +++ b/src/panels/config/automation/trace/ha-automation-trace-path-details.ts @@ -10,9 +10,9 @@ import { TemplateResult, } from "lit-element"; import { - ActionTrace, + ActionTraceStep, AutomationTraceExtended, - ChooseActionTrace, + ChooseActionTraceStep, getDataFromPath, } from "../../../../data/trace"; import "../../../../components/ha-icon-button"; @@ -77,14 +77,8 @@ export class HaAutomationTracePathDetails extends LitElement { `; } - private _getPaths() { - return this.selected.path.split("/")[0] === "condition" - ? this.trace!.condition_trace - : this.trace!.action_trace; - } - private _renderSelectedTraceInfo() { - const paths = this._getPaths(); + const paths = this.trace.trace; if (!this.selected?.path) { return "Select a node on the left for more information."; @@ -95,7 +89,7 @@ export class HaAutomationTracePathDetails extends LitElement { if (pathParts[pathParts.length - 1] === "default") { const parentTraceInfo = paths[ pathParts.slice(0, pathParts.length - 1).join("/") - ] as ChooseActionTrace[]; + ] as ChooseActionTraceStep[]; if (parentTraceInfo && parentTraceInfo[0]?.result?.choice === "default") { return "The default node was executed because no choices matched."; @@ -106,7 +100,7 @@ export class HaAutomationTracePathDetails extends LitElement { return "This node was not executed and so no further trace information is available."; } - const data: ActionTrace[] = paths[this.selected.path]; + const data: ActionTraceStep[] = paths[this.selected.path]; return data.map((trace, idx) => { const { @@ -146,16 +140,11 @@ export class HaAutomationTracePathDetails extends LitElement { } private _renderChangedVars() { - const paths = this._getPaths(); - const data: ActionTrace[] = paths[this.selected.path]; + const paths = this.trace.trace; + const data: ActionTraceStep[] = paths[this.selected.path]; return html`
-

- The following variables have changed while the step ran. If this is - the first condition or action, this will include the trigger - variables. -

${data.map( (trace, idx) => html` ${idx > 0 ? html`

Iteration ${idx + 1}

` : ""} @@ -171,15 +160,9 @@ ${safeDump(trace.changed_variables).trimRight()}