mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-18 23:06:40 +00:00
Trace foundation (#8608)
This commit is contained in:
parent
dc3ee7c779
commit
7bd4eeb0df
182
gallery/src/data/traces/basic_trace.ts
Normal file
182
gallery/src/data/traces/basic_trace.ts
Normal file
@ -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",
|
||||
},
|
||||
],
|
||||
};
|
7
gallery/src/data/traces/types.ts
Normal file
7
gallery/src/data/traces/types.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { AutomationTraceExtended } from "../../../../src/data/automation_debug";
|
||||
import { LogbookEntry } from "../../../../src/data/logbook";
|
||||
|
||||
export interface DemoTrace {
|
||||
trace: AutomationTraceExtended;
|
||||
logbookEntries: LogbookEntry[];
|
||||
}
|
62
gallery/src/demos/demo-automation-trace.ts
Normal file
62
gallery/src/demos/demo-automation-trace.ts
Normal file
@ -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`
|
||||
<ha-card .heading=${trace.trace.config.alias}>
|
||||
<div class="card-content">
|
||||
<hat-trace
|
||||
.hass=${this.hass}
|
||||
.trace=${trace.trace}
|
||||
.logbookEntries=${trace.logbookEntries}
|
||||
></hat-trace>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -81,4 +81,8 @@ class DemoMoreInfoLight extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-more-info-light", DemoMoreInfoLight);
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-light": DemoMoreInfoLight;
|
||||
}
|
||||
}
|
||||
|
@ -111,29 +111,9 @@ class HaGallery extends PolymerElement {
|
||||
</template>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="More Info Demos">
|
||||
<div class="card-content intro">
|
||||
<p>
|
||||
More info screens show up when an entity is clicked.
|
||||
</p>
|
||||
</div>
|
||||
<template is="dom-repeat" items="[[_moreInfoDemos]]">
|
||||
<a href="#[[item]]">
|
||||
<paper-item>
|
||||
<paper-item-body>{{ item }}</paper-item-body>
|
||||
<ha-icon icon="hass:chevron-right"></ha-icon>
|
||||
</paper-item>
|
||||
</a>
|
||||
</template>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Util Demos">
|
||||
<div class="card-content intro">
|
||||
<p>
|
||||
Test pages for our utility functions.
|
||||
</p>
|
||||
</div>
|
||||
<template is="dom-repeat" items="[[_utilDemos]]">
|
||||
<ha-card header="Other Demos">
|
||||
<div class="card-content intro"></div>
|
||||
<template is="dom-repeat" items="[[_restDemos]]">
|
||||
<a href="#[[item]]">
|
||||
<paper-item>
|
||||
<paper-item-body>{{ item }}</paper-item-body>
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
65
src/components/trace/ha-timeline.ts
Normal file
65
src/components/trace/ha-timeline.ts
Normal file
@ -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`
|
||||
<div class="timeline-start">
|
||||
<ha-svg-icon .path=${this.icon || mdiCircleOutline}></ha-svg-icon>
|
||||
${this.lastItem ? "" : html`<div class="line"></div>`}
|
||||
</div>
|
||||
<div class="content"><slot></slot></div>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
185
src/components/trace/hat-trace.ts
Normal file
185
src/components/trace/hat-trace.ts
Normal file
@ -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`
|
||||
<ha-timeline .icon=${mdiCircle}>
|
||||
Triggered by the ${this.trace.variables.trigger.description} at
|
||||
${formatDateTimeWithSeconds(
|
||||
new Date(this.trace.timestamp.start),
|
||||
this.hass.language
|
||||
)}
|
||||
</ha-timeline>
|
||||
`,
|
||||
];
|
||||
|
||||
if (this.trace.condition_trace) {
|
||||
for (const [path, value] of Object.entries(this.trace.condition_trace)) {
|
||||
entries.push(html`
|
||||
<ha-timeline
|
||||
?lastItem=${!value[0].result.result}
|
||||
class="condition"
|
||||
.icon=${value[0].result.result
|
||||
? mdiCheckCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
>
|
||||
${getConfigFromPath(this.trace!.config, path).alias ||
|
||||
pathToName(path)}
|
||||
${value[0].result.result ? "passed" : "failed"}
|
||||
</ha-timeline>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
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`
|
||||
<ha-timeline
|
||||
lastItem
|
||||
.icon=${this.trace.timestamp.finish
|
||||
? mdiCircle
|
||||
: mdiPauseCircleOutline}
|
||||
>
|
||||
${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"}
|
||||
</ha-timeline>
|
||||
`);
|
||||
}
|
||||
|
||||
return html`${entries}`;
|
||||
}
|
||||
|
||||
private _renderLogbookEntry(entry: LogbookEntry) {
|
||||
return html`
|
||||
<ha-timeline .icon=${mdiCircleOutline}>
|
||||
${entry.name} (${entry.entity_id}) turned ${entry.state}
|
||||
</ha-timeline>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderActionTrace(path: string, _value: ActionTrace[]) {
|
||||
return html`
|
||||
<ha-timeline .icon=${mdiRecordCircleOutline}>
|
||||
${getConfigFromPath(this.trace!.config, path).alias || pathToName(path)}
|
||||
</ha-timeline>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
css`
|
||||
ha-timeline[lastItem].condition {
|
||||
--timeline-ball-color: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hat-trace": HaAutomationTracer;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
68
src/data/automation_debug.ts
Normal file
68
src/data/automation_debug.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { HomeAssistant, Context } from "../types";
|
||||
import { AutomationConfig, Condition } from "./automation";
|
||||
import { Action } from "./script";
|
||||
|
||||
interface TraceVariables extends Record<string, unknown> {
|
||||
trigger: {
|
||||
description: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
interface BaseTrace {
|
||||
timestamp: string;
|
||||
changed_variables: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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<string, ConditionTrace[]>;
|
||||
action_trace: Record<string, ActionTrace[]>;
|
||||
context: Context;
|
||||
variables: TraceVariables;
|
||||
config: AutomationConfig;
|
||||
}
|
||||
|
||||
export const loadAutomationTrace = (
|
||||
hass: HomeAssistant,
|
||||
automation_id: string,
|
||||
run_id: string
|
||||
): Promise<AutomationTraceExtended> =>
|
||||
hass.callWS({
|
||||
type: "automation/trace/get",
|
||||
automation_id,
|
||||
run_id,
|
||||
});
|
||||
|
||||
export const loadAutomationTraces = (
|
||||
hass: HomeAssistant
|
||||
): Promise<AutomationTrace[]> =>
|
||||
hass.callWS({
|
||||
type: "automation/trace/list",
|
||||
});
|
||||
|
||||
export const getConfigFromPath = <T extends Condition | Action>(
|
||||
config: AutomationConfig,
|
||||
path: string
|
||||
): T => {
|
||||
const parts = path.split("/");
|
||||
return config[parts[0]][Number(parts[1])];
|
||||
};
|
@ -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;
|
||||
|
@ -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<LogbookEntry[]> };
|
||||
} = {};
|
||||
|
||||
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<LogbookEntry[]>("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<LogbookEntry[]>(
|
||||
"GET",
|
||||
`logbook/${startDate}?${params.toString()}`
|
||||
);
|
||||
};
|
||||
|
||||
export const clearLogbookCache = (startDate: string, endDate: string) => {
|
||||
|
@ -29,12 +29,14 @@ export interface ScriptConfig {
|
||||
}
|
||||
|
||||
export interface EventAction {
|
||||
alias?: string;
|
||||
event: string;
|
||||
event_data?: Record<string, any>;
|
||||
event_data_template?: Record<string, any>;
|
||||
}
|
||||
|
||||
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<DelayActionParts> | 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[];
|
||||
}
|
||||
|
@ -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;
|
||||
|
161
src/panels/config/automation/trace/ha-automation-trace.ts
Normal file
161
src/panels/config/automation/trace/ha-automation-trace.ts
Normal file
@ -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`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.backCallback=${() => this._backTapped()}
|
||||
.tabs=${configSections.automation}
|
||||
>
|
||||
<ha-card
|
||||
.header=${`Trace for ${
|
||||
stateObj?.attributes.friendly_name || this._entityId
|
||||
}`}
|
||||
>
|
||||
<button class="load-last" @click=${this._loadTrace}>
|
||||
Load last trace
|
||||
</button>
|
||||
${this._trace
|
||||
? html`
|
||||
<div class="card-content">
|
||||
<hat-trace
|
||||
.hass=${this.hass}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
></hat-trace>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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`
|
||||
<div class="entry-container">
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user