diff --git a/gallery/src/data/traces/basic_trace.ts b/gallery/src/data/traces/basic_trace.ts index fa17713af2..2038166544 100644 --- a/gallery/src/data/traces/basic_trace.ts +++ b/gallery/src/data/traces/basic_trace.ts @@ -15,6 +15,7 @@ export const basicTrace: DemoTrace = { action_trace: { "action/0": [ { + path: "action/0", timestamp: "2021-03-12T21:38:48.054395+00:00", changed_variables: { trigger: { @@ -60,6 +61,7 @@ export const basicTrace: DemoTrace = { condition_trace: { "condition/0": [ { + path: "condition/0", timestamp: "2021-03-12T21:38:48.050783+00:00", changed_variables: { trigger: { diff --git a/gallery/src/data/traces/device_trigger_event_trace.ts b/gallery/src/data/traces/device_trigger_event_trace.ts deleted file mode 100644 index d2a58c1595..0000000000 --- a/gallery/src/data/traces/device_trigger_event_trace.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { DemoTrace } from "./types"; - -export const deviceTriggerEventTrace: DemoTrace = { - trace: { - last_action: "action/0", - last_condition: null, - run_id: "3", - state: "stopped", - timestamp: { - start: "2021-03-13T10:30:30.058878+00:00", - finish: "2021-03-13T10:30:30.205801+00:00", - }, - trigger: "event 'lutron_caseta_button_event'", - unique_id: "1578616228911", - action_trace: { - "action/0": [ - { - timestamp: "2021-03-13T10:30:30.059607+00:00", - changed_variables: { - trigger: { - platform: "device", - event: { - event_type: "lutron_caseta_button_event", - data: { - serial: 47850540, - type: "Pico3ButtonRaiseLower", - button_number: 4, - device_name: "Right Light Pico", - area_name: "Master Bed", - action: "press", - }, - origin: "LOCAL", - time_fired: "2021-03-13T10:30:30.053185+00:00", - context: { - id: "e5387dff0c615c67e8fa43bf9d5d72ca", - parent_id: null, - user_id: null, - }, - }, - description: "event 'lutron_caseta_button_event'", - }, - context: { - id: "1c7d0dd26e031960e0ccbf0d9e0d8a16", - parent_id: "e5387dff0c615c67e8fa43bf9d5d72ca", - user_id: null, - }, - }, - }, - ], - }, - condition_trace: {}, - config: { - id: "1578616228911", - alias: "Turn Off Master Bed Lights from Picos", - description: "", - trigger: [ - { - platform: "device", - device_id: "36fd7cb4103ad0ce927e26a7ee44fa3a", - domain: "lutron_caseta", - type: "press", - subtype: "off", - }, - { - platform: "device", - device_id: "392111b5a9a362db57e2c49ec68b7a40", - domain: "lutron_caseta", - type: "press", - subtype: "off", - }, - ], - condition: [], - action: [ - { - entity_id: "light.master_bed_lights", - service: "light.turn_off", - }, - ], - mode: "single", - }, - context: { - id: "1c7d0dd26e031960e0ccbf0d9e0d8a16", - parent_id: "e5387dff0c615c67e8fa43bf9d5d72ca", - user_id: null, - }, - variables: { - trigger: { - platform: "device", - event: { - event_type: "lutron_caseta_button_event", - data: { - serial: 47850540, - type: "Pico3ButtonRaiseLower", - button_number: 4, - device_name: "Right Light Pico", - area_name: "Master Bed", - action: "press", - }, - origin: "LOCAL", - time_fired: "2021-03-13T10:30:30.053185+00:00", - context: { - id: "e5387dff0c615c67e8fa43bf9d5d72ca", - parent_id: null, - user_id: null, - }, - }, - description: "event 'lutron_caseta_button_event'", - }, - }, - }, - logbookEntries: [ - { - name: "Turn Off Master Bed Lights from Picos", - message: "has been triggered by event 'lutron_caseta_button_event'", - source: "event 'lutron_caseta_button_event'", - entity_id: "automation.turn_off_master_bed_lights_from_picos", - when: "2021-03-13T10:30:30.059052+00:00", - domain: "automation", - }, - { - when: "2021-03-13T10:30:30.200532+00:00", - name: "Master Bed Lights", - state: "off", - entity_id: "light.master_bed_lights", - context_entity_id: "automation.turn_off_master_bed_lights_from_picos", - context_entity_id_name: "Turn Off Master Bed Lights from Picos", - context_event_type: "automation_triggered", - context_domain: "automation", - context_name: "Turn Off Master Bed Lights from Picos", - }, - { - when: "2021-03-13T10:30:30.200532+00:00", - name: "Master Bed Lights", - state: "off", - entity_id: "light.master_bed_lights", - context_entity_id: "automation.turn_off_master_bed_lights_from_picos", - context_entity_id_name: "Turn Off Master Bed Lights from Picos", - context_event_type: "automation_triggered", - context_domain: "automation", - context_name: "Turn Off Master Bed Lights from Picos", - }, - { - when: "2021-03-13T10:30:30.200532+00:00", - name: "Master Bed Lights", - state: "off", - entity_id: "light.master_bed_lights", - context_entity_id: "automation.turn_off_master_bed_lights_from_picos", - context_entity_id_name: "Turn Off Master Bed Lights from Picos", - context_event_type: "automation_triggered", - context_domain: "automation", - context_name: "Turn Off Master Bed Lights from Picos", - }, - { - when: "2021-03-13T10:30:30.200532+00:00", - name: "Master Bed Lights", - state: "off", - entity_id: "light.master_bed_lights", - context_entity_id: "automation.turn_off_master_bed_lights_from_picos", - context_entity_id_name: "Turn Off Master Bed Lights from Picos", - context_event_type: "automation_triggered", - context_domain: "automation", - context_name: "Turn Off Master Bed Lights from Picos", - }, - { - when: "2021-03-13T10:30:30.200532+00:00", - name: "Master Bed Lights", - state: "off", - entity_id: "light.master_bed_lights", - context_entity_id: "automation.turn_off_master_bed_lights_from_picos", - context_entity_id_name: "Turn Off Master Bed Lights from Picos", - context_event_type: "automation_triggered", - context_domain: "automation", - context_name: "Turn Off Master Bed Lights from Picos", - }, - { - when: "2021-03-13T10:30:30.200532+00:00", - name: "Master Bed Lights", - state: "off", - entity_id: "light.master_bed_lights", - context_entity_id: "automation.turn_off_master_bed_lights_from_picos", - context_entity_id_name: "Turn Off Master Bed Lights from Picos", - context_event_type: "automation_triggered", - context_domain: "automation", - context_name: "Turn Off Master Bed Lights from Picos", - }, - ], -}; diff --git a/gallery/src/data/traces/motion-light-trace.ts b/gallery/src/data/traces/motion-light-trace.ts index 353912a04c..0c72f82af6 100644 --- a/gallery/src/data/traces/motion-light-trace.ts +++ b/gallery/src/data/traces/motion-light-trace.ts @@ -15,6 +15,7 @@ export const motionLightTrace: DemoTrace = { action_trace: { "action/0": [ { + path: "action/0", timestamp: "2021-03-14T06:07:01.771038+00:00", changed_variables: { trigger: { @@ -64,12 +65,11 @@ export const motionLightTrace: DemoTrace = { }, ], "action/1": [ - { - timestamp: "2021-03-14T06:07:01.875316+00:00", - }, + { path: "action/1", timestamp: "2021-03-14T06:07:01.875316+00:00" }, ], "action/2": [ { + path: "action/2", timestamp: "2021-03-14T06:07:53.195013+00:00", changed_variables: { wait: { @@ -118,6 +118,7 @@ export const motionLightTrace: DemoTrace = { ], "action/3": [ { + path: "action/3", timestamp: "2021-03-14T06:07:53.196014+00:00", }, ], diff --git a/gallery/src/demos/demo-automation-trace.ts b/gallery/src/demos/demo-automation-trace.ts index 4e56323037..bdf2135a21 100644 --- a/gallery/src/demos/demo-automation-trace.ts +++ b/gallery/src/demos/demo-automation-trace.ts @@ -13,13 +13,8 @@ import { HomeAssistant } from "../../../src/types"; import { DemoTrace } from "../data/traces/types"; import { basicTrace } from "../data/traces/basic_trace"; import { motionLightTrace } from "../data/traces/motion-light-trace"; -import { deviceTriggerEventTrace } from "../data/traces/device_trigger_event_trace"; -const traces: DemoTrace[] = [ - basicTrace, - motionLightTrace, - deviceTriggerEventTrace, -]; +const traces: DemoTrace[] = [basicTrace, motionLightTrace]; @customElement("demo-automation-trace") export class DemoAutomationTrace extends LitElement { diff --git a/src/components/trace/hat-trace.ts b/src/components/trace/hat-trace.ts index 7be7d67594..76d671e93e 100644 --- a/src/components/trace/hat-trace.ts +++ b/src/components/trace/hat-trace.ts @@ -11,7 +11,7 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim import { ActionTrace, AutomationTraceExtended, - getConfigFromPath, + getDataFromPath, } from "../../data/automation_debug"; import { HomeAssistant } from "../../types"; import "./ha-timeline"; @@ -24,7 +24,7 @@ import { mdiStopCircleOutline, } from "@mdi/js"; import { LogbookEntry } from "../../data/logbook"; -import { Action, describeAction } from "../../data/script"; +import { describeAction } from "../../data/script"; import relativeTime from "../../common/datetime/relative_time"; const LOGBOOK_ENTRIES_BEFORE_FOLD = 2; @@ -68,7 +68,7 @@ export class HaAutomationTracer extends LitElement { ? mdiCheckCircleOutline : mdiStopCircleOutline} > - ${getConfigFromPath(this.trace!.config, path).alias || + ${getDataFromPath(this.trace!.config, path).alias || pathToName(path)} ${value[0].result.result ? "passed" : "failed"} @@ -77,7 +77,7 @@ export class HaAutomationTracer extends LitElement { } if (this.trace.action_trace && this.logbookEntries) { - const actionTraces = Object.entries(this.trace.action_trace); + const actionTraces = Object.values(this.trace.action_trace); let logbookIndex = 0; let actionTraceIndex = 0; @@ -123,7 +123,7 @@ export class HaAutomationTracer extends LitElement { // Find next item time-wise. const logbookItem = this.logbookEntries[logbookIndex]; const actionTrace = actionTraces[actionTraceIndex]; - const actionTimestamp = new Date(actionTrace[1][0].timestamp); + const actionTimestamp = new Date(actionTrace[0].timestamp); if (new Date(logbookItem.when) > actionTimestamp) { actionTraceIndex++; @@ -133,7 +133,7 @@ export class HaAutomationTracer extends LitElement { groupedLogbookItems = []; } maybeRenderTime(actionTimestamp); - entries.push(this._renderActionTrace(...actionTrace)); + entries.push(this._renderActionTrace(actionTrace)); } else { logbookIndex++; groupedLogbookItems.push(logbookItem); @@ -152,8 +152,8 @@ export class HaAutomationTracer extends LitElement { while (actionTraceIndex < actionTraces.length) { const trace = actionTraces[actionTraceIndex]; - maybeRenderTime(new Date(trace[1][0].timestamp)); - entries.push(this._renderActionTrace(...trace)); + maybeRenderTime(new Date(trace[0].timestamp)); + entries.push(this._renderActionTrace(trace)); actionTraceIndex++; } } @@ -221,11 +221,25 @@ export class HaAutomationTracer extends LitElement { `; } - private _renderActionTrace(path: string, _value: ActionTrace[]) { - const action = getConfigFromPath(this.trace!.config, path) as Action; + 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` - ${action.alias || describeAction(action, this.hass.localize)} + ${description} `; } diff --git a/src/data/automation.ts b/src/data/automation.ts index b3a8679103..acf8e04fa2 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -38,6 +38,7 @@ export interface ManualAutomationConfig { | "info" | "debug" | "notset"; + variables?: Record; } export interface BlueprintAutomationConfig extends ManualAutomationConfig { @@ -55,7 +56,7 @@ export interface StateTrigger { entity_id: string; attribute?: string; from?: string | number; - to?: string | number; + to?: string | string[] | number; for?: string | number | ForDict; } diff --git a/src/data/automation_debug.ts b/src/data/automation_debug.ts index 075a1d1a03..01251bb0d3 100644 --- a/src/data/automation_debug.ts +++ b/src/data/automation_debug.ts @@ -1,6 +1,5 @@ import { HomeAssistant, Context } from "../types"; -import { AutomationConfig, Condition } from "./automation"; -import { Action } from "./script"; +import { AutomationConfig } from "./automation"; interface TraceVariables extends Record { trigger: { @@ -10,6 +9,7 @@ interface TraceVariables extends Record { } interface BaseTrace { + path: string; timestamp: string; changed_variables?: Record; } @@ -18,7 +18,18 @@ export interface ConditionTrace extends BaseTrace { result: { result: boolean }; } -export type ActionTrace = BaseTrace; +export interface ChooseActionTrace extends BaseTrace { + result: { choice: number }; +} + +export interface ChooseChoiceActionTrace extends BaseTrace { + result: { result: boolean }; +} + +export type ActionTrace = + | BaseTrace + | ChooseActionTrace + | ChooseChoiceActionTrace; export interface AutomationTrace { last_action: string | null; @@ -53,16 +64,40 @@ export const loadAutomationTrace = ( }); export const loadAutomationTraces = ( - hass: HomeAssistant + hass: HomeAssistant, + automation_id?: string ): Promise => hass.callWS({ type: "automation/trace/list", + automation_id, }); -export const getConfigFromPath = ( +export const getDataFromPath = ( config: AutomationConfig, path: string -): T => { - const parts = path.split("/"); - return config[parts[0]][Number(parts[1])]; +): any => { + const parts = path.split("/").reverse(); + + let result: any = config; + + while (parts.length) { + const raw = parts.pop()!; + const asNumber = Number(raw); + + if (isNaN(asNumber)) { + result = result[raw]; + continue; + } + + if (Array.isArray(result)) { + result = result[asNumber]; + continue; + } + + if (asNumber !== 0) { + throw new Error("If config is not an array, can only return index 0"); + } + } + + return result; }; diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 87f5896443..8bce620971 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -118,6 +118,36 @@ class HaAutomationPicker extends LitElement { > `, }; + columns.trace = { + title: "", + type: "icon-button", + template: (_info, automation: any) => html` + + + + ${!automation.attributes.id + ? html` + + ${this.hass.localize( + "ui.panel.config.automation.picker.dev_only_editable" + )} + + ` + : ""} + `, + }; columns.edit = { title: "", type: "icon-button", diff --git a/src/panels/config/automation/trace/ha-automation-trace.ts b/src/panels/config/automation/trace/ha-automation-trace.ts index 27b99127af..bc0f8bf060 100644 --- a/src/panels/config/automation/trace/ha-automation-trace.ts +++ b/src/panels/config/automation/trace/ha-automation-trace.ts @@ -10,6 +10,7 @@ import { } from "lit-element"; import { AutomationEntity } from "../../../../data/automation"; import { + AutomationTrace, AutomationTraceExtended, loadAutomationTrace, loadAutomationTraces, @@ -23,6 +24,9 @@ import { getLogbookDataForContext, LogbookEntry, } from "../../../../data/logbook"; +import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time"; +import { repeat } from "lit-html/directives/repeat"; +import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; @customElement("ha-automation-trace") export class HaAutomationTrace extends LitElement { @@ -40,6 +44,10 @@ export class HaAutomationTrace extends LitElement { @internalProperty() private _entityId?: string; + @internalProperty() private _traces?: AutomationTrace[]; + + @internalProperty() private _runId?: string; + @internalProperty() private _trace?: AutomationTraceExtended; @internalProperty() private _logbookEntries?: LogbookEntry[]; @@ -63,24 +71,45 @@ export class HaAutomationTrace extends LitElement { }`} >
-
- ${this._trace - ? html` -
+
+ ${this._traces === undefined + ? "Loading…" + : this._traces.length === 0 + ? "No traces found" + : this._trace === undefined + ? "Loading…" + : html` -
- ` - : ""} + `} +
`; @@ -90,10 +119,22 @@ export class HaAutomationTrace extends LitElement { super.updated(changedProps); if (changedProps.has("automationId")) { + this._traces = undefined; this._entityId = undefined; + this._runId = undefined; this._trace = undefined; this._logbookEntries = undefined; - this._loadTrace(); + if (this.automationId) { + this._loadTraces(); + } + } + + if (changedProps.has("_runId") && this._runId) { + this._trace = undefined; + this._logbookEntries = undefined; + if (this._runId) { + this._loadTrace(); + } } if ( @@ -101,24 +142,44 @@ export class HaAutomationTrace extends LitElement { this.automationId && !this._entityId ) { - this._setEntityId(); + const automation = this.automations.find( + (entity: AutomationEntity) => entity.attributes.id === this.automationId + ); + this._entityId = automation?.entity_id; + } + } + + private _pickTrace(ev) { + this._runId = ev.target.value; + } + + private async _loadTraces() { + this._traces = await loadAutomationTraces(this.hass, this.automationId); + // Newest will be on top. + this._traces.reverse(); + + // Check if current run ID still exists + if ( + this._runId && + !this._traces.some((trace) => trace.run_id === this._runId) + ) { + await showAlertDialog(this, { + text: "Chosen trace is no longer available", + }); + this._runId = undefined; + } + + // See if we can set a default runID + if (!this._runId && this._traces.length > 0) { + this._runId = this._traces[0].run_id; } } 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._runId! ); this._logbookEntries = await getLogbookDataForContext( this.hass, @@ -129,20 +190,15 @@ export class HaAutomationTrace extends LitElement { 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(); } private _downloadTrace() { const aEl = document.createElement("a"); - aEl.download = `trace-${this._entityId}.json`; + aEl.download = `trace ${this._entityId} ${ + this._trace!.timestamp.start + }.json`; aEl.href = `data:application/json;charset=utf-8,${encodeURI( JSON.stringify( { diff --git a/src/translations/en.json b/src/translations/en.json index 6eab53161e..c523f09742 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1180,7 +1180,9 @@ "no_automations": "We couldn’t find any editable automations", "add_automation": "Add automation", "only_editable": "Only automations defined in automations.yaml are editable.", + "dev_only_editable": "Only automations defined in automations.yaml are debuggable.", "edit_automation": "Edit automation", + "dev_automation": "Debug automation", "show_info_automation": "Show info about automation", "delete_automation": "Delete automation", "delete_confirm": "Are you sure you want to delete this automation?",