diff --git a/gallery/src/data/traces/basic_trace.ts b/gallery/src/data/traces/basic_trace.ts new file mode 100644 index 0000000000..fa17713af2 --- /dev/null +++ b/gallery/src/data/traces/basic_trace.ts @@ -0,0 +1,182 @@ +import { DemoTrace } from "./types"; + +export const basicTrace: DemoTrace = { + trace: { + last_action: "action/0", + last_condition: "condition/0", + run_id: "0", + state: "stopped", + timestamp: { + start: "2021-03-12T21:38:48.050464+00:00", + finish: "2021-03-12T21:38:48.068458+00:00", + }, + trigger: "state of input_boolean.toggle_1", + unique_id: "1615419646544", + action_trace: { + "action/0": [ + { + timestamp: "2021-03-12T21:38:48.054395+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-12T21:38:03.262985+00:00", + last_updated: "2021-03-12T21:38:03.262985+00:00", + context: { + id: "4ad34793b237d7cb5e541e8a331e7bf9", + 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-12T21:38:48.049816+00:00", + last_updated: "2021-03-12T21:38:48.049816+00:00", + context: { + id: "2d83ca81663c85df51fae2a1f940d0e7", + parent_id: null, + user_id: "d1b4e89da01445fa8bc98e39fac477ca", + }, + }, + for: null, + attribute: null, + description: "state of input_boolean.toggle_1", + }, + context: { + id: "febeaa3d50152bae8017d783ed3c0751", + parent_id: "2d83ca81663c85df51fae2a1f940d0e7", + user_id: null, + }, + }, + }, + ], + }, + condition_trace: { + "condition/0": [ + { + timestamp: "2021-03-12T21:38:48.050783+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-12T21:38:03.262985+00:00", + last_updated: "2021-03-12T21:38:03.262985+00:00", + context: { + id: "4ad34793b237d7cb5e541e8a331e7bf9", + 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-12T21:38:48.049816+00:00", + last_updated: "2021-03-12T21:38:48.049816+00:00", + context: { + id: "2d83ca81663c85df51fae2a1f940d0e7", + parent_id: null, + user_id: "d1b4e89da01445fa8bc98e39fac477ca", + }, + }, + for: null, + attribute: null, + description: "state of input_boolean.toggle_1", + }, + }, + result: { result: true }, + }, + ], + }, + config: { + id: "1615419646544", + alias: "Basic Trace Example", + description: "", + trigger: [{ platform: "state", entity_id: "input_boolean.toggle_1" }], + condition: [ + { + condition: "template", + alias: "Test if Paulus is home", + value_template: "{{ true }}", + }, + ], + action: [ + { + service: "input_boolean.toggle", + alias: "Enable party mode", + target: { entity_id: "input_boolean.toggle_2" }, + }, + ], + mode: "single", + }, + context: { + id: "febeaa3d50152bae8017d783ed3c0751", + parent_id: "2d83ca81663c85df51fae2a1f940d0e7", + 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-12T21:38:03.262985+00:00", + last_updated: "2021-03-12T21:38:03.262985+00:00", + context: { + id: "4ad34793b237d7cb5e541e8a331e7bf9", + 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-12T21:38:48.049816+00:00", + last_updated: "2021-03-12T21:38:48.049816+00:00", + context: { + id: "2d83ca81663c85df51fae2a1f940d0e7", + parent_id: null, + user_id: "d1b4e89da01445fa8bc98e39fac477ca", + }, + }, + for: null, + attribute: null, + description: "state of input_boolean.toggle_1", + }, + }, + }, + logbookEntries: [ + { + name: "Ensure Party mode", + message: "has been triggered by state of input_boolean.toggle_1", + source: "state of input_boolean.toggle_1", + entity_id: "automation.toggle_toggles", + when: "2021-03-12T21:38:48.051460+00:00", + domain: "automation", + }, + { + when: "2021-03-12T21:38:48.064184+00:00", + name: "Toggle 2", + state: "off", + entity_id: "input_boolean.toggle_2", + context_entity_id: "automation.toggle_toggles", + context_entity_id_name: "Ensure Party mode", + context_event_type: "automation_triggered", + context_domain: "automation", + context_name: "Ensure Party mode", + }, + ], +}; diff --git a/gallery/src/data/traces/types.ts b/gallery/src/data/traces/types.ts new file mode 100644 index 0000000000..11ccf0e36e --- /dev/null +++ b/gallery/src/data/traces/types.ts @@ -0,0 +1,7 @@ +import { AutomationTraceExtended } from "../../../../src/data/automation_debug"; +import { LogbookEntry } from "../../../../src/data/logbook"; + +export interface DemoTrace { + trace: AutomationTraceExtended; + logbookEntries: LogbookEntry[]; +} diff --git a/gallery/src/demos/demo-automation-trace.ts b/gallery/src/demos/demo-automation-trace.ts new file mode 100644 index 0000000000..c7bca8f523 --- /dev/null +++ b/gallery/src/demos/demo-automation-trace.ts @@ -0,0 +1,62 @@ +import { + customElement, + html, + css, + LitElement, + TemplateResult, + property, +} from "lit-element"; +import "../../../src/components/ha-card"; +import "../../../src/components/trace/hat-trace"; +import { provideHass } from "../../../src/fake_data/provide_hass"; +import { HomeAssistant } from "../../../src/types"; +import { basicTrace } from "../data/traces/basic_trace"; +import { DemoTrace } from "../data/traces/types"; + +const traces: DemoTrace[] = [basicTrace]; + +@customElement("demo-automation-trace") +export class DemoAutomationTrace 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); + provideHass(this); + } + + static get styles() { + return css` + ha-card { + max-width: 600px; + margin: 24px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-automation-trace": DemoAutomationTrace; + } +} diff --git a/gallery/src/demos/demo-more-info-light.ts b/gallery/src/demos/demo-more-info-light.ts index 3a05f1becf..d469da2212 100644 --- a/gallery/src/demos/demo-more-info-light.ts +++ b/gallery/src/demos/demo-more-info-light.ts @@ -81,4 +81,8 @@ class DemoMoreInfoLight extends LitElement { } } -customElements.define("demo-more-info-light", DemoMoreInfoLight); +declare global { + interface HTMLElementTagNameMap { + "demo-more-info-light": DemoMoreInfoLight; + } +} diff --git a/gallery/src/ha-gallery.js b/gallery/src/ha-gallery.js index c1de40203d..f94fd0ab53 100644 --- a/gallery/src/ha-gallery.js +++ b/gallery/src/ha-gallery.js @@ -111,29 +111,9 @@ class HaGallery extends PolymerElement { - - - - More info screens show up when an entity is clicked. - - - - - - {{ item }} - - - - - - - - - - Test pages for our utility functions. - - - + + + {{ item }} @@ -178,13 +158,9 @@ class HaGallery extends PolymerElement { type: Array, computed: "_computeLovelace(_demos)", }, - _moreInfoDemos: { + _restDemos: { type: Array, - computed: "_computeMoreInfos(_demos)", - }, - _utilDemos: { - type: Array, - computed: "_computeUtil(_demos)", + computed: "_computeRest(_demos)", }, }; } @@ -237,12 +213,8 @@ class HaGallery extends PolymerElement { return demos.filter((demo) => demo.includes("hui")); } - _computeMoreInfos(demos) { - return demos.filter((demo) => demo.includes("more-info")); - } - - _computeUtil(demos) { - return demos.filter((demo) => demo.includes("util")); + _computeRest(demos) { + return demos.filter((demo) => !demo.includes("hui")); } } diff --git a/src/components/trace/ha-timeline.ts b/src/components/trace/ha-timeline.ts new file mode 100644 index 0000000000..caaa32e066 --- /dev/null +++ b/src/components/trace/ha-timeline.ts @@ -0,0 +1,65 @@ +import { mdiCircleOutline } from "@mdi/js"; +import { LitElement, customElement, html, css, property } from "lit-element"; +import "../ha-svg-icon"; + +@customElement("ha-timeline") +class HaTimeline extends LitElement { + @property({ type: Boolean }) public lastItem = false; + + @property({ type: String }) public icon?: string; + + protected render() { + return html` + + + ${this.lastItem ? "" : html``} + + + `; + } + + static get styles() { + return css` + :host { + display: flex; + flex-direction: row; + } + :host(:not([lastItem])) { + min-height: 50px; + } + .timeline-start { + display: flex; + flex-direction: column; + align-items: center; + margin-right: 4px; + } + ha-svg-icon { + color: var( + --timeline-ball-color, + var(--timeline-color, var(--secondary-text-color)) + ); + } + .line { + flex: 1; + width: 2px; + background-color: var( + --timeline-line-color, + var(--timeline-color, var(--secondary-text-color)) + ); + margin: 4px 0; + } + .content { + margin-top: 2px; + } + :host(:not([lastItem])) .content { + padding-bottom: 16px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-timeline": HaTimeline; + } +} diff --git a/src/components/trace/hat-trace.ts b/src/components/trace/hat-trace.ts new file mode 100644 index 0000000000..05e990aef0 --- /dev/null +++ b/src/components/trace/hat-trace.ts @@ -0,0 +1,185 @@ +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; +import { + ActionTrace, + AutomationTraceExtended, + getConfigFromPath, +} from "../../data/automation_debug"; +import { HomeAssistant } from "../../types"; +import "./ha-timeline"; +import { + mdiCheckCircleOutline, + mdiCircle, + mdiCircleOutline, + mdiPauseCircleOutline, + mdiRecordCircleOutline, + mdiStopCircleOutline, +} from "@mdi/js"; +import { LogbookEntry } from "../../data/logbook"; + +const pathToName = (path: string) => path.split("/").join(" "); + +@customElement("hat-trace") +export class HaAutomationTracer extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) private trace?: AutomationTraceExtended; + + @property({ attribute: false }) private logbookEntries?: LogbookEntry[]; + + protected render(): TemplateResult { + 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.language + )} + + `, + ]; + + if (this.trace.condition_trace) { + for (const [path, value] of Object.entries(this.trace.condition_trace)) { + entries.push(html` + + ${getConfigFromPath(this.trace!.config, path).alias || + pathToName(path)} + ${value[0].result.result ? "passed" : "failed"} + + `); + } + } + + if (this.trace.action_trace && this.logbookEntries) { + const actionTraces = Object.entries(this.trace.action_trace); + + let logbookIndex = 0; + let actionTraceIndex = 0; + + 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; + } + + // Find next item time-wise. + const logbookItem = this.logbookEntries[logbookIndex]; + const actionTrace = actionTraces[actionTraceIndex]; + + if ( + new Date(logbookItem.when) > new Date(actionTrace[1][0].timestamp) + ) { + actionTraceIndex++; + entries.push(this._renderActionTrace(...actionTrace)); + } else { + logbookIndex++; + entries.push(this._renderLogbookEntry(logbookItem)); + } + } + + // Append all leftover items + while (logbookIndex < this.logbookEntries.length) { + entries.push( + this._renderLogbookEntry(this.logbookEntries[logbookIndex]) + ); + logbookIndex++; + } + + while (actionTraceIndex < actionTraces.length) { + entries.push( + this._renderActionTrace(...actionTraces[actionTraceIndex]) + ); + actionTraceIndex++; + } + } + + // null means it was stopped by a condition + if (this.trace.last_action !== null) { + entries.push(html` + + ${this.trace.timestamp.finish + ? html`Finished at + ${formatDateTimeWithSeconds( + new Date(this.trace.timestamp.finish), + this.hass.language + )} + (runtime: + ${( + (Number(new Date(this.trace.timestamp.finish!)) - + Number(new Date(this.trace.timestamp.start))) / + 1000 + ).toFixed(2)} + seconds)` + : "Still running"} + + `); + } + + return html`${entries}`; + } + + private _renderLogbookEntry(entry: LogbookEntry) { + return html` + + ${entry.name} (${entry.entity_id}) turned ${entry.state} + + `; + } + + private _renderActionTrace(path: string, _value: ActionTrace[]) { + return html` + + ${getConfigFromPath(this.trace!.config, path).alias || pathToName(path)} + + `; + } + + static get styles(): CSSResult[] { + return [ + css` + ha-timeline[lastItem].condition { + --timeline-ball-color: var(--error-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hat-trace": HaAutomationTracer; + } +} diff --git a/src/data/automation.ts b/src/data/automation.ts index f8ebcc2a05..2310f86610 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -149,11 +149,13 @@ export type Trigger = export interface LogicalCondition { condition: "and" | "not" | "or"; + alias?: string; conditions: Condition[]; } export interface StateCondition { condition: "state"; + alias?: string; entity_id: string; attribute?: string; state: string | number; @@ -162,6 +164,7 @@ export interface StateCondition { export interface NumericStateCondition { condition: "numeric_state"; + alias?: string; entity_id: string; attribute?: string; above?: number; @@ -171,6 +174,7 @@ export interface NumericStateCondition { export interface SunCondition { condition: "sun"; + alias?: string; after_offset: number; before_offset: number; after: "sunrise" | "sunset"; @@ -179,12 +183,14 @@ export interface SunCondition { export interface ZoneCondition { condition: "zone"; + alias?: string; entity_id: string; zone: string; } export interface TimeCondition { condition: "time"; + alias?: string; after?: string; before?: string; weekday?: string | string[]; @@ -192,6 +198,7 @@ export interface TimeCondition { export interface TemplateCondition { condition: "template"; + alias?: string; value_template: string; } diff --git a/src/data/automation_debug.ts b/src/data/automation_debug.ts new file mode 100644 index 0000000000..21a2cb0109 --- /dev/null +++ b/src/data/automation_debug.ts @@ -0,0 +1,68 @@ +import { HomeAssistant, Context } from "../types"; +import { AutomationConfig, Condition } from "./automation"; +import { Action } from "./script"; + +interface TraceVariables extends Record { + trigger: { + description: string; + [key: string]: unknown; + }; +} + +interface BaseTrace { + timestamp: string; + changed_variables: Record; +} + +export interface ConditionTrace extends BaseTrace { + result: { result: boolean }; +} + +export type ActionTrace = BaseTrace; + +export interface AutomationTrace { + last_action: string | null; + last_condition: string | null; + run_id: string; + state: "running" | "stopped" | "debugged"; + timestamp: { + start: string; + finish: string | null; + }; + trigger: unknown; + unique_id: string; +} + +export interface AutomationTraceExtended extends AutomationTrace { + condition_trace: Record; + action_trace: Record; + context: Context; + variables: TraceVariables; + config: AutomationConfig; +} + +export const loadAutomationTrace = ( + hass: HomeAssistant, + automation_id: string, + run_id: string +): Promise => + hass.callWS({ + type: "automation/trace/get", + automation_id, + run_id, + }); + +export const loadAutomationTraces = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "automation/trace/list", + }); + +export const getConfigFromPath = ( + config: AutomationConfig, + path: string +): T => { + const parts = path.split("/"); + return config[parts[0]][Number(parts[1])]; +}; diff --git a/src/data/device_automation.ts b/src/data/device_automation.ts index 8319f8772b..f94a6e6ba7 100644 --- a/src/data/device_automation.ts +++ b/src/data/device_automation.ts @@ -3,6 +3,7 @@ import { HaFormSchema } from "../components/ha-form/ha-form"; import { HomeAssistant } from "../types"; export interface DeviceAutomation { + alias?: string; device_id: string; domain: string; entity_id: string; diff --git a/src/data/logbook.ts b/src/data/logbook.ts index d6b55d91c1..50a3eec209 100644 --- a/src/data/logbook.ts +++ b/src/data/logbook.ts @@ -14,7 +14,8 @@ export interface LogbookEntry { message?: string; entity_id?: string; icon?: string; - domain: string; + source?: string; + domain?: string; context_user_id?: string; context_event_type?: string; context_domain?: string; @@ -29,6 +30,20 @@ const DATA_CACHE: { [cacheKey: string]: { [entityId: string]: Promise }; } = {}; +export const getLogbookDataForContext = async ( + hass: HomeAssistant, + startDate: string, + contextId?: string +) => + getLogbookDataFromServer( + hass, + startDate, + undefined, + undefined, + undefined, + contextId + ); + export const getLogbookData = async ( hass: HomeAssistant, startDate: string, @@ -100,15 +115,30 @@ export const getLogbookDataCache = async ( const getLogbookDataFromServer = async ( hass: HomeAssistant, startDate: string, - endDate: string, + endDate?: string, entityId?: string, - entity_matches_only?: boolean + entitymatchesOnly?: boolean, + contextId?: string ) => { - const url = `logbook/${startDate}?end_time=${endDate}${ - entityId ? `&entity=${entityId}` : "" - }${entity_matches_only ? `&entity_matches_only` : ""}`; + const params = new URLSearchParams(); - return hass.callApi("GET", url); + if (endDate) { + params.append("end_time", endDate); + } + if (entityId) { + params.append("entity", entityId); + } + if (entitymatchesOnly) { + params.append("entity_matches_only", ""); + } + if (contextId) { + params.append("context_id", contextId); + } + + return hass.callApi( + "GET", + `logbook/${startDate}?${params.toString()}` + ); }; export const clearLogbookCache = (startDate: string, endDate: string) => { diff --git a/src/data/script.ts b/src/data/script.ts index bb04af28e5..ff598dcf76 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -29,12 +29,14 @@ export interface ScriptConfig { } export interface EventAction { + alias?: string; event: string; event_data?: Record; event_data_template?: Record; } export interface ServiceAction { + alias?: string; service: string; entity_id?: string; target?: HassServiceTarget; @@ -42,6 +44,7 @@ export interface ServiceAction { } export interface DeviceAction { + alias?: string; device_id: string; domain: string; entity_id: string; @@ -55,26 +58,31 @@ export interface DelayActionParts { days?: number; } export interface DelayAction { + alias?: string; delay: number | Partial | string; } export interface SceneAction { + alias?: string; scene: string; } export interface WaitAction { + alias?: string; wait_template: string; timeout?: number; continue_on_timeout?: boolean; } export interface WaitForTriggerAction { + alias?: string; wait_for_trigger: Trigger[]; timeout?: number; continue_on_timeout?: boolean; } export interface RepeatAction { + alias?: string; repeat: CountRepeat | WhileRepeat | UntilRepeat; } @@ -95,6 +103,7 @@ export interface UntilRepeat extends BaseRepeat { } export interface ChooseAction { + alias?: string; choose: [{ conditions: Condition[]; sequence: Action[] }]; default?: Action[]; } diff --git a/src/panels/config/automation/ha-config-automation.ts b/src/panels/config/automation/ha-config-automation.ts index 39322da66e..a7936d91ce 100644 --- a/src/panels/config/automation/ha-config-automation.ts +++ b/src/panels/config/automation/ha-config-automation.ts @@ -9,8 +9,8 @@ import { RouterOptions, } from "../../../layouts/hass-router-page"; import { HomeAssistant } from "../../../types"; -import "./ha-automation-editor"; import "./ha-automation-picker"; +import "./ha-automation-editor"; const equal = (a: AutomationEntity[], b: AutomationEntity[]): boolean => { if (a.length !== b.length) { @@ -48,6 +48,10 @@ class HaConfigAutomation extends HassRouterPage { edit: { tag: "ha-automation-editor", }, + trace: { + tag: "ha-automation-trace", + load: () => import("./trace/ha-automation-trace"), + }, }, }; @@ -81,7 +85,7 @@ class HaConfigAutomation extends HassRouterPage { if ( (!changedProps || changedProps.has("route")) && - this._currentPage === "edit" + this._currentPage !== "dashboard" ) { const automationId = this.routeTail.path.substr(1); pageEl.automationId = automationId === "new" ? null : automationId; diff --git a/src/panels/config/automation/trace/ha-automation-trace.ts b/src/panels/config/automation/trace/ha-automation-trace.ts new file mode 100644 index 0000000000..26857668a7 --- /dev/null +++ b/src/panels/config/automation/trace/ha-automation-trace.ts @@ -0,0 +1,161 @@ +import { + css, + CSSResult, + customElement, + html, + internalProperty, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { AutomationEntity } from "../../../../data/automation"; +import { + AutomationTraceExtended, + loadAutomationTrace, + loadAutomationTraces, +} from "../../../../data/automation_debug"; +import "../../../../components/ha-card"; +import "../../../../components/trace/hat-trace"; +import { haStyle } from "../../../../resources/styles"; +import { HomeAssistant, Route } from "../../../../types"; +import { configSections } from "../../ha-panel-config"; +import { + getLogbookDataForContext, + LogbookEntry, +} from "../../../../data/logbook"; + +@customElement("ha-automation-trace") +export class HaAutomationTrace extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public automationId!: string; + + @property() public automations!: AutomationEntity[]; + + @property() public isWide?: boolean; + + @property() public narrow!: boolean; + + @property() public route!: Route; + + @internalProperty() private _entityId?: string; + + @internalProperty() private _trace?: AutomationTraceExtended; + + @internalProperty() private _logbookEntries?: LogbookEntry[]; + + protected render(): TemplateResult { + const stateObj = this._entityId + ? this.hass.states[this._entityId] + : undefined; + + return html` + this._backTapped()} + .tabs=${configSections.automation} + > + + + Load last trace + + ${this._trace + ? html` + + + + ` + : ""} + + + `; + } + + protected updated(changedProps) { + super.updated(changedProps); + + if (changedProps.has("automationId")) { + this._entityId = undefined; + this._trace = undefined; + this._logbookEntries = undefined; + this._loadTrace(); + } + + if ( + changedProps.has("automations") && + this.automationId && + !this._entityId + ) { + this._setEntityId(); + } + } + + private async _loadTrace() { + const traces = await loadAutomationTraces(this.hass); + const automationTraces = traces[this.automationId]; + + if (!automationTraces || automationTraces.length === 0) { + // TODO no trace found. + alert("NO traces found"); + return; + } + + const trace = await loadAutomationTrace( + this.hass, + this.automationId, + automationTraces[automationTraces.length - 1].run_id + ); + this._logbookEntries = await getLogbookDataForContext( + this.hass, + trace.timestamp.start, + trace.context.id + ); + + this._trace = trace; + } + + private _setEntityId() { + const automation = this.automations.find( + (entity: AutomationEntity) => entity.attributes.id === this.automationId + ); + this._entityId = automation?.entity_id; + } + + private _backTapped(): void { + history.back(); + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + ha-card { + max-width: 800px; + margin: 24px auto; + } + + .load-last { + position: absolute; + top: 8px; + right: 8px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-trace": HaAutomationTrace; + } +} diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts index 9f9be9eb5b..29e90f80a7 100644 --- a/src/panels/logbook/ha-logbook.ts +++ b/src/panels/logbook/ha-logbook.ts @@ -117,7 +117,10 @@ class HaLogbook extends LitElement { : undefined; const item_username = item.context_user_id && this.userIdToName[item.context_user_id]; - const domain = item.entity_id ? computeDomain(item.entity_id) : item.domain; + const domain = item.entity_id + ? computeDomain(item.entity_id) + : // Domain is there if there is no entity ID. + item.domain!; return html` diff --git a/src/types.ts b/src/types.ts index 91b06b564b..3c9e872e43 100644 --- a/src/types.ts +++ b/src/types.ts @@ -168,7 +168,7 @@ export interface Resources { export interface Context { id: string; parent_id?: string; - user_id?: string; + user_id?: string | null; } export interface ServiceCallResponse {
- More info screens show up when an entity is clicked. -
- Test pages for our utility functions. -