diff --git a/src/components/trace/hat-trace.ts b/src/components/trace/hat-trace.ts
index 76d671e93e..402b57b704 100644
--- a/src/components/trace/hat-trace.ts
+++ b/src/components/trace/hat-trace.ts
@@ -9,8 +9,8 @@ import {
} from "lit-element";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
- ActionTrace,
AutomationTraceExtended,
+ ChooseActionTrace,
getDataFromPath,
} from "../../data/automation_debug";
import { HomeAssistant } from "../../types";
@@ -24,16 +24,300 @@ import {
mdiStopCircleOutline,
} from "@mdi/js";
import { LogbookEntry } from "../../data/logbook";
-import { describeAction } from "../../data/script";
+import { getActionType } from "../../data/script";
import relativeTime from "../../common/datetime/relative_time";
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
const SIGNIFICANT_TIME_CHANGE = 5000; // 5 seconds
+const isSignificantTimeChange = (a: Date, b: Date) =>
+ Math.abs(b.getTime() - a.getTime()) > SIGNIFICANT_TIME_CHANGE;
+
+class RenderedTimeTracker {
+ private lastReportedTime: Date;
+
+ constructor(
+ private hass: HomeAssistant,
+ private entries: TemplateResult[],
+ trace: AutomationTraceExtended
+ ) {
+ this.lastReportedTime = new Date(trace.timestamp.start);
+ }
+
+ setLastReportedTime(date: Date) {
+ this.lastReportedTime = date;
+ }
+
+ renderTime(from: Date, to: Date): void {
+ this.entries.push(html`
+
+ ${relativeTime(from, this.hass.localize, {
+ compareTime: to,
+ includeTense: false,
+ })}
+ later
+
+ `);
+ this.lastReportedTime = to;
+ }
+
+ maybeRenderTime(timestamp: Date): boolean {
+ if (!isSignificantTimeChange(timestamp, this.lastReportedTime)) {
+ this.lastReportedTime = timestamp;
+ return false;
+ }
+
+ this.renderTime(this.lastReportedTime, timestamp);
+ return true;
+ }
+}
+
+class LogbookRenderer {
+ private curIndex: number;
+
+ private pendingItems: Array<[Date, LogbookEntry]> = [];
+
+ constructor(
+ private entries: TemplateResult[],
+ private timeTracker: RenderedTimeTracker,
+ private logbookEntries: LogbookEntry[]
+ ) {
+ // Skip the "automation got triggered item"
+ this.curIndex =
+ logbookEntries.length > 0 && logbookEntries[0].domain === "automation"
+ ? 1
+ : 0;
+ }
+
+ get curItem() {
+ return this.logbookEntries[this.curIndex];
+ }
+
+ get hasNext() {
+ return this.curIndex !== this.logbookEntries.length;
+ }
+
+ maybeRenderItem() {
+ const logbookEntry = this.curItem;
+ this.curIndex++;
+ const entryDate = new Date(logbookEntry.when);
+
+ if (this.pendingItems.length === 0) {
+ this.pendingItems.push([entryDate, logbookEntry]);
+ return;
+ }
+
+ const previousEntryDate = this.pendingItems[
+ this.pendingItems.length - 1
+ ][0];
+
+ // If logbook entry is too long after the last one,
+ // add a time passed label
+ if (isSignificantTimeChange(previousEntryDate, entryDate)) {
+ this._renderLogbookEntries();
+ this.timeTracker.renderTime(previousEntryDate, entryDate);
+ }
+
+ this.pendingItems.push([entryDate, logbookEntry]);
+ }
+
+ flush() {
+ if (this.pendingItems.length > 0) {
+ this._renderLogbookEntries();
+ }
+ }
+
+ private _renderLogbookEntries() {
+ this.timeTracker.maybeRenderTime(this.pendingItems[0][0]);
+
+ const parts: TemplateResult[] = [];
+
+ let i;
+
+ for (
+ i = 0;
+ i < Math.min(this.pendingItems.length, LOGBOOK_ENTRIES_BEFORE_FOLD);
+ i++
+ ) {
+ parts.push(this._renderLogbookEntryHelper(this.pendingItems[i][1]));
+ }
+
+ let moreItems: TemplateResult[] | undefined;
+
+ // If we didn't render all items, push rest into `moreItems`
+ if (i < this.pendingItems.length) {
+ moreItems = [];
+ for (; i < this.pendingItems.length; i++) {
+ moreItems.push(this._renderLogbookEntryHelper(this.pendingItems[i][1]));
+ }
+ }
+
+ this.entries.push(html`
+
+ ${parts}
+
+ `);
+
+ // Clear rendered items.
+ this.timeTracker.setLastReportedTime(
+ this.pendingItems[this.pendingItems.length - 1][0]
+ );
+ this.pendingItems = [];
+ }
+
+ private _renderLogbookEntryHelper(entry: LogbookEntry) {
+ return html`${entry.name} (${entry.entity_id}) turned ${entry.state}
`;
+ }
+}
+
+class ActionRenderer {
+ private curIndex = 0;
+
+ private keys: string[];
+
+ constructor(
+ private entries: TemplateResult[],
+ private trace: AutomationTraceExtended,
+ private timeTracker: RenderedTimeTracker
+ ) {
+ this.keys = Object.keys(trace.action_trace);
+ }
+
+ get curItem() {
+ return this._getItem(this.curIndex);
+ }
+
+ get hasNext() {
+ return this.curIndex !== this.keys.length;
+ }
+
+ renderItem() {
+ this.curIndex = this._renderItem(this.curIndex);
+ }
+
+ private _getItem(index: number) {
+ return this.trace.action_trace[this.keys[index]];
+ }
+
+ private _renderItem(
+ index: number,
+ actionType?: ReturnType
+ ): number {
+ const value = this._getItem(index);
+ const timestamp = new Date(value[0].timestamp);
+
+ this.timeTracker.maybeRenderTime(timestamp);
+
+ const path = value[0].path;
+ let data;
+ try {
+ data = getDataFromPath(this.trace.config, path);
+ } catch (err) {
+ this.entries.push(
+ html`Unable to extract path ${path}. Download trace and report as bug`
+ );
+ return index + 1;
+ }
+
+ const isTopLevel = path.split("/").length === 2;
+
+ if (!isTopLevel && !actionType) {
+ this._renderEntry(path.replace(/\//g, " "));
+ return index + 1;
+ }
+
+ if (!actionType) {
+ actionType = getActionType(data);
+ }
+
+ if (actionType === "choose") {
+ return this._handleChoose(index);
+ }
+
+ this._renderEntry(data.alias || actionType);
+ return index + 1;
+ }
+
+ private _handleChoose(index: number): number {
+ // startLevel: choose root config
+
+ // +1: 'default
+ // +2: executed sequence
+
+ // +1: 'choose'
+ // +2: current choice
+
+ // +3: 'conditions'
+ // +4: evaluated condition
+
+ // +3: 'sequence'
+ // +4: executed sequence
+
+ const startLevel = this.keys[index].split("/").length - 1;
+
+ const chooseTrace = this._getItem(index)[0] as ChooseActionTrace;
+ const defaultExecuted = chooseTrace.result.choice === "default";
+
+ if (defaultExecuted) {
+ this._renderEntry(`Choose: Default action executed`);
+ } else {
+ this._renderEntry(`Choose: Choice ${chooseTrace.result.choice} executed`);
+ }
+
+ let i;
+
+ // Skip over conditions
+ for (i = index + 1; i < this.keys.length; i++) {
+ const parts = this.keys[i].split("/");
+
+ // We're done if no more sequence in current level
+ if (parts.length <= startLevel) {
+ return i;
+ }
+
+ // We're going to skip all conditions
+ if (parts[startLevel + 3] === "sequence") {
+ break;
+ }
+ }
+
+ // Render choice
+ for (; i < this.keys.length; i++) {
+ const path = this.keys[i];
+ const parts = path.split("/");
+
+ // We're done if no more sequence in current level
+ if (parts.length <= startLevel) {
+ return i;
+ }
+
+ // We know it's an action sequence, so force the type like that
+ // for rendering.
+ this._renderItem(i, getActionType(this._getDataFromPath(path)));
+ }
+
+ return i;
+ }
+
+ private _renderEntry(description: string) {
+ this.entries.push(html`
+
+ ${description}
+
+ `);
+ }
+
+ private _getDataFromPath(path: string) {
+ return getDataFromPath(this.trace.config, path);
+ }
+}
+
@customElement("hat-trace")
export class HaAutomationTracer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -77,84 +361,44 @@ export class HaAutomationTracer extends LitElement {
}
if (this.trace.action_trace && this.logbookEntries) {
- const actionTraces = Object.values(this.trace.action_trace);
-
- let logbookIndex = 0;
- let actionTraceIndex = 0;
- let lastReportedTime = new Date(this.trace.timestamp.start);
-
- const maybeRenderTime = (nextItemTimestamp: Date) => {
- if (
- nextItemTimestamp.getTime() - lastReportedTime.getTime() <
- SIGNIFICANT_TIME_CHANGE
- ) {
- return;
- }
-
- entries.push(html`
-
- ${relativeTime(lastReportedTime, this.hass.localize, {
- compareTime: nextItemTimestamp,
- includeTense: false,
- })}
- later
-
- `);
- lastReportedTime = nextItemTimestamp;
- };
-
- let groupedLogbookItems: LogbookEntry[] = [];
-
- while (
- logbookIndex < this.logbookEntries.length &&
- actionTraceIndex < actionTraces.length
- ) {
- // Find next item.
-
- // Skip the "automation got triggered item"
- if (
- logbookIndex === 0 &&
- this.logbookEntries[0].domain === "automation"
- ) {
- logbookIndex++;
- continue;
- }
+ const timeTracker = new RenderedTimeTracker(
+ this.hass,
+ entries,
+ this.trace
+ );
+ const logbookRenderer = new LogbookRenderer(
+ entries,
+ timeTracker,
+ this.logbookEntries
+ );
+ const actionRenderer = new ActionRenderer(
+ entries,
+ this.trace,
+ timeTracker
+ );
+ while (logbookRenderer.hasNext && actionRenderer.hasNext) {
// Find next item time-wise.
- const logbookItem = this.logbookEntries[logbookIndex];
- const actionTrace = actionTraces[actionTraceIndex];
+ const logbookItem = logbookRenderer.curItem;
+ const actionTrace = actionRenderer.curItem;
const actionTimestamp = new Date(actionTrace[0].timestamp);
if (new Date(logbookItem.when) > actionTimestamp) {
- actionTraceIndex++;
- if (groupedLogbookItems.length > 0) {
- maybeRenderTime(new Date(groupedLogbookItems[0].when));
- entries.push(this._renderLogbookEntries(groupedLogbookItems));
- groupedLogbookItems = [];
- }
- maybeRenderTime(actionTimestamp);
- entries.push(this._renderActionTrace(actionTrace));
+ logbookRenderer.flush();
+ actionRenderer.renderItem();
} else {
- logbookIndex++;
- groupedLogbookItems.push(logbookItem);
+ logbookRenderer.maybeRenderItem();
}
}
- while (logbookIndex < this.logbookEntries.length) {
- groupedLogbookItems.push(this.logbookEntries[logbookIndex]);
- logbookIndex++;
+ while (logbookRenderer.hasNext) {
+ logbookRenderer.maybeRenderItem();
}
- if (groupedLogbookItems.length > 0) {
- maybeRenderTime(new Date(groupedLogbookItems[0].when));
- entries.push(this._renderLogbookEntries(groupedLogbookItems));
- }
+ logbookRenderer.flush();
- while (actionTraceIndex < actionTraces.length) {
- const trace = actionTraces[actionTraceIndex];
- maybeRenderTime(new Date(trace[0].timestamp));
- entries.push(this._renderActionTrace(trace));
- actionTraceIndex++;
+ while (actionRenderer.hasNext) {
+ actionRenderer.renderItem();
}
}
@@ -188,62 +432,6 @@ export class HaAutomationTracer extends LitElement {
return html`${entries}`;
}
- private _renderLogbookEntryHelper(entry: LogbookEntry) {
- return html`${entry.name} (${entry.entity_id}) turned ${entry.state}
`;
- }
-
- private _renderLogbookEntries(entries: LogbookEntry[]) {
- const parts: TemplateResult[] = [];
-
- let i;
-
- for (
- i = 0;
- i < Math.min(entries.length, LOGBOOK_ENTRIES_BEFORE_FOLD);
- i++
- ) {
- parts.push(this._renderLogbookEntryHelper(entries[i]));
- }
-
- let moreItems: TemplateResult[] | undefined;
-
- if (i < entries.length) {
- moreItems = [];
- for (i = 0; i < entries.length; i++) {
- moreItems.push(this._renderLogbookEntryHelper(entries[i]));
- }
- }
-
- return html`
-
- ${parts}
-
- `;
- }
-
- private _renderActionTrace(value: ActionTrace[]) {
- const path = value[0].path;
- let data;
- try {
- data = getDataFromPath(this.trace!.config, path);
- } catch (err) {
- return html`Unable to extract path ${path}. Download trace and report as
- bug`;
- }
-
- const description =
- // Top-level we know it's an action
- path.split("/").length === 2
- ? data.alias || describeAction(data, this.hass.localize)
- : path.replace(/\//g, " ");
-
- return html`
-
- ${description}
-
- `;
- }
-
static get styles(): CSSResult[] {
return [
css`
diff --git a/src/data/automation_debug.ts b/src/data/automation_debug.ts
index a29b874556..f5fca26c84 100644
--- a/src/data/automation_debug.ts
+++ b/src/data/automation_debug.ts
@@ -27,7 +27,7 @@ export interface CallServiceActionTrace extends BaseTrace {
}
export interface ChooseActionTrace extends BaseTrace {
- result: { choice: number };
+ result: { choice: number | "default" };
}
export interface ChooseChoiceActionTrace extends BaseTrace {
diff --git a/src/data/script.ts b/src/data/script.ts
index bce5274c2b..da1b8f525f 100644
--- a/src/data/script.ts
+++ b/src/data/script.ts
@@ -5,7 +5,6 @@ import {
} from "home-assistant-js-websocket";
import { computeObjectId } from "../common/entity/compute_object_id";
import { navigate } from "../common/navigate";
-import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import { Condition, Trigger } from "./automation";
@@ -165,40 +164,40 @@ export const getScriptEditorInitData = () => {
return data;
};
-export const describeAction = (action: Action, _localize: LocalizeFunc) => {
+export const getActionType = (action: Action) => {
// Check based on config_validation.py#determine_script_action
if ("delay" in action) {
- return "Delay";
+ return "delay";
}
if ("wait_template" in action) {
- return "Wait";
+ return "wait_template";
}
if ("condition" in action) {
- return "Check condition";
+ return "check_condition";
}
if ("event" in action) {
- return "Fire event";
+ return "fire_event";
}
if ("device_id" in action) {
- return "Run Device Action";
+ return "device_action";
}
if ("scene" in action) {
- return "Activate a scene";
+ return "activate_scene";
}
if ("repeat" in action) {
- return "Repeat an action multiple times";
+ return "repeat";
}
if ("choose" in action) {
- return "Choose an action";
+ return "choose";
}
if ("wait_for_trigger" in action) {
- return "Wait for a trigger";
+ return "wait_for_trigger";
}
if ("variables" in action) {
- return "Define variables";
+ return "variables";
}
if ("service" in action) {
- return "Call service";
+ return "service";
}
- return "Unknown action";
+ return "unknown";
};