mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 03:36:44 +00:00
Refactor trace rendering (#8693)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
4d48fc3d85
commit
5156c67226
@ -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`
|
||||
<ha-timeline label>
|
||||
${relativeTime(from, this.hass.localize, {
|
||||
compareTime: to,
|
||||
includeTense: false,
|
||||
})}
|
||||
later
|
||||
</ha-timeline>
|
||||
`);
|
||||
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`
|
||||
<ha-timeline .icon=${mdiCircleOutline} .moreItems=${moreItems}>
|
||||
${parts}
|
||||
</ha-timeline>
|
||||
`);
|
||||
|
||||
// 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}<br />`;
|
||||
}
|
||||
}
|
||||
|
||||
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<typeof getActionType>
|
||||
): 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`
|
||||
<ha-timeline .icon=${mdiRecordCircleOutline}>
|
||||
${description}
|
||||
</ha-timeline>
|
||||
`);
|
||||
}
|
||||
|
||||
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`
|
||||
<ha-timeline label>
|
||||
${relativeTime(lastReportedTime, this.hass.localize, {
|
||||
compareTime: nextItemTimestamp,
|
||||
includeTense: false,
|
||||
})}
|
||||
later
|
||||
</ha-timeline>
|
||||
`);
|
||||
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}<br />`;
|
||||
}
|
||||
|
||||
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`
|
||||
<ha-timeline .icon=${mdiCircleOutline} .moreItems=${moreItems}>
|
||||
${parts}
|
||||
</ha-timeline>
|
||||
`;
|
||||
}
|
||||
|
||||
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`
|
||||
<ha-timeline .icon=${mdiRecordCircleOutline}>
|
||||
${description}
|
||||
</ha-timeline>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
css`
|
||||
|
@ -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 {
|
||||
|
@ -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";
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user